refactor: nuke `strix_tool` shim + dead package re-exports

``@strix_tool`` was passing through every kwarg to ``@function_tool``
with the same defaults — zero Strix-specific value-add. The docstring
also still claimed terminal/browser/python tools opted into
``timeout_behavior="raise_exception"``, but those tools were all
deleted in the recent migrations.

- Replace 30 ``@strix_tool(...)`` callsites with ``@function_tool(...)``.
- Inline ``dump_tool_result(x)`` as ``json.dumps(x, ensure_ascii=False,
  default=str)`` at all 64 callsites — no helper.
- Delete ``strix/tools/_decorator.py``.

Drive-by: gut dead package re-exports.

- ``strix/{agents,orchestration,tools}/__init__.py`` re-exported
  symbols nobody imports via the package — every consumer uses deep
  paths (``from strix.agents.factory import build_strix_agent``).
- The 8 ``strix/tools/<sub>/__init__.py`` re-exports only fed the
  splat ``from .agents_graph import *`` etc. in the parent package
  init, which is also gone now.
- Reduced to docstrings (or empty) so ``import strix.tools`` doesn't
  drag every tool's transitive deps in eagerly.

Drive-by: drop dead helpers in ``runtime.session_manager``
(``cached_scan_ids``, ``_reset_cache_for_tests``) — zero callers since
``tests/`` was nuked in ``a6d578c``.

Verified all tool timeouts preserved (think=10, list_requests=120,
finish_scan=60, web_search=330) and ruff/mypy at baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 15:17:46 -07:00
parent 6990fd4ef1
commit b3f7cfd040
22 changed files with 257 additions and 320 deletions
-3
View File
@@ -214,9 +214,6 @@ ignore = [
"strix/llm/multi_provider_setup.py" = [
"TC002", # Model, ModelProvider imported for annotations
]
"strix/tools/_decorator.py" = [
"TC002", # FunctionTool imported for annotation
]
# SDK function-tool wrappers: the SDK calls get_type_hints() at registration
# time to derive the JSON schema, which evaluates annotations at runtime —
# so RunContextWrapper / Tool / TResponseInputItem must be imported eagerly,
+9 -16
View File
@@ -1,19 +1,12 @@
"""Strix agent package.
"""Strix agent assembly.
Public surface:
- :func:`strix.agents.factory.build_strix_agent` — assemble a root or
child ``SandboxAgent``.
- :func:`strix.agents.factory.make_child_factory` — closure factory
passed via context to the multi-agent ``create_agent`` graph tool.
- :func:`strix.agents.prompt.render_system_prompt` — render the Jinja
system prompt.
- :func:`build_strix_agent` — assemble a root or child ``agents.Agent``.
- :func:`make_child_factory` — closure factory passed via context to
the multi-agent ``create_agent`` graph tool.
- :func:`render_system_prompt` — render the Jinja system prompt.
Import deeply so ``import strix.agents`` doesn't pull every submodule's
deps in eagerly.
"""
from .factory import build_strix_agent, make_child_factory
from .prompt import render_system_prompt
__all__ = [
"build_strix_agent",
"make_child_factory",
"render_system_prompt",
]
+9 -15
View File
@@ -1,18 +1,12 @@
"""Strix multi-agent orchestration on top of OpenAI Agents SDK.
Provides:
- AgentMessageBus: peer-to-peer agent inbox + status + stats aggregation
- inject_messages_filter: SDK call_model_input_filter for inbox drain
- StrixOrchestrationHooks: SDK RunHooks subclass for lifecycle wiring
- :class:`AgentMessageBus` — peer-to-peer agent inbox + status + stats.
- :func:`inject_messages_filter` — SDK ``call_model_input_filter`` for
inbox drain at the top of each LLM turn.
- :class:`StrixOrchestrationHooks` — SDK ``RunHooks`` subclass for
lifecycle wiring.
Import deeply (``from strix.orchestration.bus import AgentMessageBus``)
so ``import strix.orchestration`` doesn't drag every submodule's deps
in eagerly.
"""
from strix.orchestration.bus import AgentMessageBus
from strix.orchestration.filter import inject_messages_filter
from strix.orchestration.hooks import StrixOrchestrationHooks
__all__ = [
"AgentMessageBus",
"StrixOrchestrationHooks",
"inject_messages_filter",
]
-10
View File
@@ -144,13 +144,3 @@ async def cleanup(scan_id: str) -> None:
"cleanup(%s): client.delete raised; container may need manual reaping",
scan_id,
)
def cached_scan_ids() -> list[str]:
"""Snapshot of currently-cached scan ids. Used by the TUI / CLI."""
return list(_SESSION_CACHE.keys())
def _reset_cache_for_tests() -> None:
"""Test helper — clears the module cache between unit tests."""
_SESSION_CACHE.clear()
+3 -9
View File
@@ -5,13 +5,7 @@ imported directly by :mod:`strix.agents.factory`. The sandbox-bound
shell + filesystem tools are emitted by the SDK's ``Shell`` and
``Filesystem`` capabilities and bound to the live sandbox session
per-run.
"""
from .agents_graph import * # noqa: F403
from .finish import * # noqa: F403
from .notes import * # noqa: F403
from .proxy import * # noqa: F403
from .reporting import * # noqa: F403
from .thinking import * # noqa: F403
from .todo import * # noqa: F403
from .web_search import * # noqa: F403
Import deeply so ``import strix.tools`` doesn't pull every submodule's
deps in eagerly.
"""
-73
View File
@@ -1,73 +0,0 @@
"""``strix_tool`` — ``function_tool`` factory with Strix defaults.
Every tool uses ``@strix_tool`` instead of bare ``@function_tool`` so
defaults stay consistent across the suite. Override per call when
needed.
Defaults:
- ``timeout``: 120s.
- ``timeout_behavior``: ``"error_as_result"`` for idempotent tools.
Critical sandbox tools (terminal, browser, python) opt into
``timeout_behavior="raise_exception"`` explicitly so the SDK
fails the run rather than letting the model retry a hung call.
"""
from __future__ import annotations
import json
from collections.abc import Callable
from typing import Any, Literal
from agents import function_tool
from agents.tool import FunctionTool
_ToolFn = Callable[..., Any]
_ToolBehavior = Literal["error_as_result", "raise_exception"]
def dump_tool_result(result: dict[str, Any]) -> str:
"""Serialize a tool's dict result to JSON for the LLM.
Every Strix tool returns a dict; the SDK passes the tool's return
straight into ``str(result)``, which produces ugly Python repr
output. JSON is what the model expects.
"""
return json.dumps(result, ensure_ascii=False, default=str)
def strix_tool(
*,
timeout: float = 120.0,
timeout_behavior: _ToolBehavior = "error_as_result",
name_override: str | None = None,
description_override: str | None = None,
strict_mode: bool = True,
) -> Callable[[_ToolFn], FunctionTool]:
"""Wrap ``agents.function_tool`` with Strix defaults.
Strict mode is on by default (forbids free-form ``dict[str, X]``
parameters because the strict JSON schema needs
``additionalProperties: false``). A few tools that take arbitrary
header / modification dicts opt out via ``strict_mode=False``.
Usage::
@strix_tool()
async def my_tool(ctx: RunContextWrapper, x: int) -> str: ...
@strix_tool(timeout=300, timeout_behavior="raise_exception")
async def critical_tool(ctx: RunContextWrapper, ...) -> str: ...
@strix_tool(strict_mode=False)
async def free_form_dict_tool(
ctx: RunContextWrapper, headers: dict[str, str],
) -> str: ...
"""
return function_tool(
timeout=timeout,
timeout_behavior=timeout_behavior,
name_override=name_override,
description_override=description_override,
strict_mode=strict_mode,
)
-18
View File
@@ -1,18 +0,0 @@
from .tools import (
agent_finish,
agent_status,
create_agent,
send_message_to_agent,
view_agent_graph,
wait_for_message,
)
__all__ = [
"agent_finish",
"agent_status",
"create_agent",
"send_message_to_agent",
"view_agent_graph",
"wait_for_message",
]
+89 -40
View File
@@ -21,12 +21,11 @@ import logging
import uuid
from typing import TYPE_CHECKING, Any, Literal
from agents import RunContextWrapper, Runner
from agents import RunContextWrapper, Runner, function_tool
from agents.items import TResponseInputItem
from strix.orchestration.hooks import StrixOrchestrationHooks
from strix.run_config_factory import make_agent_context, make_run_config
from strix.tools._decorator import dump_tool_result, strix_tool
if TYPE_CHECKING:
@@ -42,7 +41,7 @@ def _ctx(ctx: RunContextWrapper) -> dict[str, Any]:
return ctx.context if isinstance(ctx.context, dict) else {}
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def view_agent_graph(ctx: RunContextWrapper) -> str:
"""Print the multi-agent tree — every agent, its parent, its status.
@@ -57,7 +56,11 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str:
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None:
return dump_tool_result({"success": False, "error": "Bus not initialized in context."})
return json.dumps(
{"success": False, "error": "Bus not initialized in context."},
ensure_ascii=False,
default=str,
)
async with bus._lock:
parent_of = dict(bus.parent_of)
@@ -86,16 +89,18 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str:
"crashed": sum(1 for s in statuses.values() if s == "crashed"),
"stopped": sum(1 for s in statuses.values() if s == "stopped"),
}
return dump_tool_result(
return json.dumps(
{
"success": True,
"graph_structure": "\n".join(lines) or "(no agents)",
"summary": summary,
}
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def agent_status(ctx: RunContextWrapper, agent_id: str) -> str:
"""Look up one agent's lifecycle state + pending message count.
@@ -111,17 +116,23 @@ async def agent_status(ctx: RunContextWrapper, agent_id: str) -> str:
inner = _ctx(ctx)
bus = inner.get("bus")
if bus is None:
return dump_tool_result({"success": False, "error": "Bus not initialized in context."})
return json.dumps(
{"success": False, "error": "Bus not initialized in context."},
ensure_ascii=False,
default=str,
)
async with bus._lock:
if agent_id not in bus.statuses:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"Unknown agent_id: {agent_id}",
}
},
ensure_ascii=False,
default=str,
)
return dump_tool_result(
return json.dumps(
{
"success": True,
"agent_id": agent_id,
@@ -129,11 +140,13 @@ async def agent_status(ctx: RunContextWrapper, agent_id: str) -> str:
"status": bus.statuses.get(agent_id),
"parent_id": bus.parent_of.get(agent_id),
"pending_messages": len(bus.inboxes.get(agent_id, [])),
}
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def send_message_to_agent(
ctx: RunContextWrapper,
target_agent_id: str,
@@ -169,24 +182,32 @@ async def send_message_to_agent(
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None or me is None:
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
return json.dumps(
{"success": False, "error": "Bus or agent_id missing in context."},
ensure_ascii=False,
default=str,
)
async with bus._lock:
if target_agent_id not in bus.statuses:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"Target agent '{target_agent_id}' not found.",
}
},
ensure_ascii=False,
default=str,
)
target_status = bus.statuses.get(target_agent_id)
if target_status in ("completed", "crashed", "stopped"):
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"Target agent '{target_agent_id}' is {target_status}; message dropped.",
}
},
ensure_ascii=False,
default=str,
)
msg_id = f"msg_{uuid.uuid4().hex[:8]}"
@@ -200,13 +221,15 @@ async def send_message_to_agent(
"priority": priority,
},
)
return dump_tool_result(
return json.dumps(
{
"success": True,
"message_id": msg_id,
"target_agent_id": target_agent_id,
"delivery_status": "queued",
}
},
ensure_ascii=False,
default=str,
)
@@ -215,7 +238,7 @@ async def send_message_to_agent(
_WAIT_POLL_SECONDS = 1.0
@strix_tool(timeout=601)
@function_tool(timeout=601)
async def wait_for_message(
ctx: RunContextWrapper,
reason: str = "Waiting for messages from other agents",
@@ -251,7 +274,11 @@ async def wait_for_message(
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None or me is None:
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
return json.dumps(
{"success": False, "error": "Bus or agent_id missing in context."},
ensure_ascii=False,
default=str,
)
async with bus._lock:
bus.statuses[me] = "waiting"
@@ -264,13 +291,15 @@ async def wait_for_message(
if pending > 0:
async with bus._lock:
bus.statuses[me] = "running"
return dump_tool_result(
return json.dumps(
{
"success": True,
"status": "message_arrived",
"pending_messages": pending,
"reason": reason,
}
},
ensure_ascii=False,
default=str,
)
await asyncio.sleep(_WAIT_POLL_SECONDS)
finally:
@@ -280,18 +309,20 @@ async def wait_for_message(
if bus.statuses.get(me) == "waiting":
bus.statuses[me] = "running"
return dump_tool_result(
return json.dumps(
{
"success": True,
"status": "timeout",
"timeout_seconds": timeout_seconds,
"reason": reason,
"note": "No messages within timeout — continue work or call agent_finish.",
}
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=120)
@function_tool(timeout=120)
async def create_agent(
ctx: RunContextWrapper,
name: str,
@@ -344,16 +375,22 @@ async def create_agent(
factory: Callable[..., SDKAgent] | None = inner.get("agent_factory")
if bus is None or parent_id is None:
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
return json.dumps(
{"success": False, "error": "Bus or agent_id missing in context."},
ensure_ascii=False,
default=str,
)
if factory is None:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": (
"No agent_factory in context. "
"The root assembly must inject one via make_agent_context."
),
}
},
ensure_ascii=False,
default=str,
)
child_id = uuid.uuid4().hex[:8]
@@ -362,11 +399,13 @@ async def create_agent(
child_agent = factory(name=name, skills=skills or [])
except Exception as e:
logger.exception("agent_factory raised while building child '%s'", name)
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"agent_factory failed: {e!s}",
}
},
ensure_ascii=False,
default=str,
)
await bus.register(child_id, name, parent_id)
@@ -437,18 +476,20 @@ async def create_agent(
async with bus._lock:
bus.tasks[child_id] = task_handle
return dump_tool_result(
return json.dumps(
{
"success": True,
"agent_id": child_id,
"name": name,
"parent_id": parent_id,
"message": f"Spawned '{name}' ({child_id}) running in parallel.",
}
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def agent_finish(
ctx: RunContextWrapper,
result_summary: str,
@@ -495,11 +536,15 @@ async def agent_finish(
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None or me is None:
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
return json.dumps(
{"success": False, "error": "Bus or agent_id missing in context."},
ensure_ascii=False,
default=str,
)
parent_id = inner.get("parent_id")
if parent_id is None:
return dump_tool_result(
return json.dumps(
{
"success": False,
"agent_completed": False,
@@ -507,7 +552,9 @@ async def agent_finish(
"agent_finish is for subagents. Root/main agents must call finish_scan instead."
),
"parent_notified": False,
}
},
ensure_ascii=False,
default=str,
)
inner["agent_finish_called"] = True
@@ -540,7 +587,7 @@ async def agent_finish(
)
parent_notified = True
return dump_tool_result(
return json.dumps(
{
"success": True,
"agent_completed": True,
@@ -549,5 +596,7 @@ async def agent_finish(
"summary": result_summary,
"findings_count": len(findings or []),
"has_recommendations": bool(final_recommendations),
}
},
ensure_ascii=False,
default=str,
)
-4
View File
@@ -1,4 +0,0 @@
from .tool import finish_scan
__all__ = ["finish_scan"]
+2 -4
View File
@@ -7,9 +7,7 @@ import json
import logging
from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from agents import RunContextWrapper, function_tool
logger = logging.getLogger(__name__)
@@ -71,7 +69,7 @@ def _do_finish(
return {"success": False, "message": f"Failed to complete scan: {e!s}"}
@strix_tool(timeout=60)
@function_tool(timeout=60)
async def finish_scan(
ctx: RunContextWrapper,
executive_summary: str,
-10
View File
@@ -1,10 +0,0 @@
from .tools import create_note, delete_note, get_note, list_notes, update_note
__all__ = [
"create_note",
"delete_note",
"get_note",
"list_notes",
"update_note",
]
+21 -13
View File
@@ -21,9 +21,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pathlib import Path
from agents import RunContextWrapper
from strix.tools._decorator import dump_tool_result, strix_tool
from agents import RunContextWrapper, function_tool
logger = logging.getLogger(__name__)
@@ -397,7 +395,7 @@ def _delete_note_impl(note_id: str) -> dict[str, Any]:
# --- public tools ---------------------------------------------------------
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def create_note(
ctx: RunContextWrapper,
title: str,
@@ -437,12 +435,14 @@ async def create_note(
category: One of the categories above. Default ``"general"``.
tags: Optional free-form tags.
"""
return dump_tool_result(
return json.dumps(
await asyncio.to_thread(_create_note_impl, title, content, category, tags),
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def list_notes(
ctx: RunContextWrapper,
category: str | None = None,
@@ -468,7 +468,7 @@ async def list_notes(
include_content: When False (default) entries have a preview;
when True the full ``content`` is included.
"""
return dump_tool_result(
return json.dumps(
await asyncio.to_thread(
_list_notes_impl,
category=category,
@@ -476,20 +476,24 @@ async def list_notes(
search=search,
include_content=include_content,
),
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def get_note(ctx: RunContextWrapper, note_id: str) -> str:
"""Fetch one note by its 5-char ID. Returns the full content.
Args:
note_id: Note id from ``create_note`` or a ``list_notes`` entry.
"""
return dump_tool_result(await asyncio.to_thread(_get_note_impl, note_id))
return json.dumps(
await asyncio.to_thread(_get_note_impl, note_id), ensure_ascii=False, default=str
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def update_note(
ctx: RunContextWrapper,
note_id: str,
@@ -509,7 +513,7 @@ async def update_note(
content: New content, or ``None`` to keep.
tags: New tags list, or ``None`` to keep.
"""
return dump_tool_result(
return json.dumps(
await asyncio.to_thread(
_update_note_impl,
note_id=note_id,
@@ -517,14 +521,18 @@ async def update_note(
content=content,
tags=tags,
),
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def delete_note(ctx: RunContextWrapper, note_id: str) -> str:
"""Delete a note. For wiki notes, also removes the rendered Markdown file.
Args:
note_id: Note id to delete.
"""
return dump_tool_result(await asyncio.to_thread(_delete_note_impl, note_id))
return json.dumps(
await asyncio.to_thread(_delete_note_impl, note_id), ensure_ascii=False, default=str
)
-10
View File
@@ -1,10 +0,0 @@
from .tools import list_requests, repeat_request, scope_rules, send_request, view_request
__all__ = [
"list_requests",
"repeat_request",
"scope_rules",
"send_request",
"view_request",
]
+81 -30
View File
@@ -12,6 +12,7 @@ Tools: ``list_requests``, ``view_request``, ``send_request``,
from __future__ import annotations
import dataclasses
import json
import re
import time
from dataclasses import is_dataclass
@@ -19,7 +20,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from agents import RunContextWrapper
from agents import RunContextWrapper, function_tool
from caido_sdk_client.types import (
ConnectionInfoInput,
CreateReplaySessionFromRaw,
@@ -30,8 +31,6 @@ from caido_sdk_client.types import (
UpdateScopeOptions,
)
from strix.tools._decorator import dump_tool_result, strix_tool
if TYPE_CHECKING:
from caido_sdk_client import Client
@@ -92,18 +91,20 @@ def _serialize(value: Any) -> Any:
def _no_client() -> str:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": "Caido client not initialized in context.",
},
ensure_ascii=False,
default=str,
)
# ----------------------------------------------------------------------
# list_requests
# ----------------------------------------------------------------------
@strix_tool(timeout=120)
@function_tool(timeout=120)
async def list_requests(
ctx: RunContextWrapper,
httpql_filter: str | None = None,
@@ -201,7 +202,7 @@ async def list_requests(
},
)
return dump_tool_result(
return json.dumps(
{
"success": True,
"entries": entries,
@@ -212,15 +213,21 @@ async def list_requests(
"end_cursor": connection.page_info.end_cursor,
},
},
ensure_ascii=False,
default=str,
)
except Exception as exc: # noqa: BLE001
return dump_tool_result({"success": False, "error": f"list_requests failed: {exc}"})
return json.dumps(
{"success": False, "error": f"list_requests failed: {exc}"},
ensure_ascii=False,
default=str,
)
# ----------------------------------------------------------------------
# view_request
# ----------------------------------------------------------------------
@strix_tool(timeout=60)
@function_tool(timeout=60)
async def view_request(
ctx: RunContextWrapper,
request_id: str,
@@ -265,8 +272,10 @@ async def view_request(
)
result = await client.request.get(request_id, opts)
if result is None:
return dump_tool_result(
return json.dumps(
{"success": False, "error": f"Request {request_id} not found"},
ensure_ascii=False,
default=str,
)
raw_bytes = (
@@ -275,20 +284,30 @@ async def view_request(
else (result.response.raw if result.response is not None else None)
)
if raw_bytes is None:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"No raw {part} for {request_id}",
},
ensure_ascii=False,
default=str,
)
content = raw_bytes.decode("utf-8", errors="replace")
if search_pattern:
return dump_tool_result(_regex_hits(content, search_pattern))
return json.dumps(_regex_hits(content, search_pattern), ensure_ascii=False, default=str)
return dump_tool_result(_paginate_lines(content, page=page, page_size=page_size))
return json.dumps(
_paginate_lines(content, page=page, page_size=page_size),
ensure_ascii=False,
default=str,
)
except Exception as exc: # noqa: BLE001
return dump_tool_result({"success": False, "error": f"view_request failed: {exc}"})
return json.dumps(
{"success": False, "error": f"view_request failed: {exc}"},
ensure_ascii=False,
default=str,
)
def _regex_hits(content: str, pattern: str) -> dict[str, Any]:
@@ -333,7 +352,7 @@ def _paginate_lines(content: str, *, page: int, page_size: int) -> dict[str, Any
# ----------------------------------------------------------------------
# send_request
# ----------------------------------------------------------------------
@strix_tool(timeout=120, strict_mode=False)
@function_tool(timeout=120, strict_mode=False)
async def send_request(
ctx: RunContextWrapper,
method: str,
@@ -367,13 +386,17 @@ async def send_request(
)
return await _replay_send(client, raw=raw, connection=connection)
except Exception as exc: # noqa: BLE001
return dump_tool_result({"success": False, "error": f"send_request failed: {exc}"})
return json.dumps(
{"success": False, "error": f"send_request failed: {exc}"},
ensure_ascii=False,
default=str,
)
# ----------------------------------------------------------------------
# repeat_request
# ----------------------------------------------------------------------
@strix_tool(timeout=120, strict_mode=False)
@function_tool(timeout=120, strict_mode=False)
async def repeat_request(
ctx: RunContextWrapper,
request_id: str,
@@ -412,8 +435,10 @@ async def repeat_request(
try:
result = await client.request.get(request_id, RequestGetOptions(request_raw=True))
if result is None or result.request.raw is None:
return dump_tool_result(
return json.dumps(
{"success": False, "error": f"Request {request_id} not found"},
ensure_ascii=False,
default=str,
)
original = result.request
@@ -429,13 +454,17 @@ async def repeat_request(
)
return await _replay_send(client, raw=raw, connection=connection)
except Exception as exc: # noqa: BLE001
return dump_tool_result({"success": False, "error": f"repeat_request failed: {exc}"})
return json.dumps(
{"success": False, "error": f"repeat_request failed: {exc}"},
ensure_ascii=False,
default=str,
)
# ----------------------------------------------------------------------
# scope_rules
# ----------------------------------------------------------------------
@strix_tool(timeout=60)
@function_tool(timeout=60)
async def scope_rules(
ctx: RunContextWrapper,
action: ScopeAction,
@@ -488,20 +517,28 @@ async def scope_rules(
try:
if action == "list":
scopes = await client.scope.list()
return dump_tool_result(
return json.dumps(
{"success": True, "scopes": [_serialize(s) for s in scopes]},
ensure_ascii=False,
default=str,
)
if action == "get":
if not scope_id:
return dump_tool_result(
return json.dumps(
{"success": False, "error": "scope_id required for get"},
ensure_ascii=False,
default=str,
)
scope = await client.scope.get(scope_id)
return dump_tool_result({"success": True, "scope": _serialize(scope)})
return json.dumps(
{"success": True, "scope": _serialize(scope)}, ensure_ascii=False, default=str
)
if action == "create":
if not scope_name:
return dump_tool_result(
return json.dumps(
{"success": False, "error": "scope_name required for create"},
ensure_ascii=False,
default=str,
)
scope = await client.scope.create(
CreateScopeOptions(
@@ -510,14 +547,18 @@ async def scope_rules(
denylist=list(denylist or []),
),
)
return dump_tool_result({"success": True, "scope": _serialize(scope)})
return json.dumps(
{"success": True, "scope": _serialize(scope)}, ensure_ascii=False, default=str
)
if action == "update":
if not scope_id or not scope_name:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": "scope_id and scope_name required for update",
},
ensure_ascii=False,
default=str,
)
scope = await client.scope.update(
scope_id,
@@ -527,16 +568,24 @@ async def scope_rules(
denylist=list(denylist or []),
),
)
return dump_tool_result({"success": True, "scope": _serialize(scope)})
return json.dumps(
{"success": True, "scope": _serialize(scope)}, ensure_ascii=False, default=str
)
# action == "delete" — exhaustive Literal
if not scope_id:
return dump_tool_result(
return json.dumps(
{"success": False, "error": "scope_id required for delete"},
ensure_ascii=False,
default=str,
)
await client.scope.delete(scope_id)
return dump_tool_result({"success": True, "deleted": scope_id})
return json.dumps({"success": True, "deleted": scope_id}, ensure_ascii=False, default=str)
except Exception as exc: # noqa: BLE001
return dump_tool_result({"success": False, "error": f"scope_rules failed: {exc}"})
return json.dumps(
{"success": False, "error": f"scope_rules failed: {exc}"},
ensure_ascii=False,
default=str,
)
# ----------------------------------------------------------------------
@@ -670,7 +719,7 @@ async def _replay_send(
"raw": response_raw.decode("utf-8", errors="replace"),
}
return dump_tool_result(
return json.dumps(
{
"success": result.status == "DONE",
"status": result.status,
@@ -679,4 +728,6 @@ async def _replay_send(
"elapsed_ms": elapsed_ms,
"response": response,
},
ensure_ascii=False,
default=str,
)
-4
View File
@@ -1,4 +0,0 @@
from .tool import create_vulnerability_report
__all__ = ["create_vulnerability_report"]
+2 -4
View File
@@ -9,9 +9,7 @@ import re
from pathlib import PurePosixPath
from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from agents import RunContextWrapper, function_tool
logger = logging.getLogger(__name__)
@@ -293,7 +291,7 @@ def _do_create( # noqa: PLR0912
# large scans can have many existing reports to compare against.
# strict_mode=False because cvss_breakdown is a dict[str, str] and
# code_locations is list[dict] — both free-form for the strict schema.
@strix_tool(timeout=180, strict_mode=False)
@function_tool(timeout=180, strict_mode=False)
async def create_vulnerability_report(
ctx: RunContextWrapper,
title: str,
-4
View File
@@ -1,4 +0,0 @@
from .tool import think
__all__ = ["think"]
+2 -2
View File
@@ -4,10 +4,10 @@ from __future__ import annotations
import json
from strix.tools._decorator import strix_tool
from agents import function_tool
@strix_tool(timeout=10)
@function_tool(timeout=10)
async def think(thought: str) -> str:
"""Record a private chain-of-thought note. No side effects, no new info.
-18
View File
@@ -1,18 +0,0 @@
from .tools import (
create_todo,
delete_todo,
list_todos,
mark_todo_done,
mark_todo_pending,
update_todo,
)
__all__ = [
"create_todo",
"delete_todo",
"list_todos",
"mark_todo_done",
"mark_todo_pending",
"update_todo",
]
+37 -25
View File
@@ -13,9 +13,7 @@ import uuid
from datetime import UTC, datetime
from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import dump_tool_result, strix_tool
from agents import RunContextWrapper, function_tool
VALID_PRIORITIES = ["low", "normal", "high", "critical"]
@@ -198,7 +196,7 @@ def _apply_single_update(
# --- public tools ---------------------------------------------------------
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def create_todo(
ctx: RunContextWrapper,
title: str | None = None,
@@ -248,12 +246,14 @@ async def create_todo(
},
)
if not tasks:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": "Provide a title or 'todos' list to create.",
"todo_id": None,
},
ensure_ascii=False,
default=str,
)
agent_todos = _get_agent_todos(agent_id)
@@ -273,11 +273,13 @@ async def create_todo(
}
created.append({"todo_id": todo_id, "title": task["title"], "priority": task_priority})
except (ValueError, TypeError) as e:
return dump_tool_result(
{"success": False, "error": f"Failed to create todo: {e}", "todo_id": None}
return json.dumps(
{"success": False, "error": f"Failed to create todo: {e}", "todo_id": None},
ensure_ascii=False,
default=str,
)
return dump_tool_result(
return json.dumps(
{
"success": True,
"created": created,
@@ -285,10 +287,12 @@ async def create_todo(
"todos": _sorted_todos(agent_id),
"total_count": len(_get_agent_todos(agent_id)),
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def list_todos(
ctx: RunContextWrapper,
status: str | None = None,
@@ -327,7 +331,7 @@ async def list_todos(
sv = todo.get("status", "pending")
summary[sv] = summary.get(sv, 0) + 1
except (ValueError, TypeError) as e:
return dump_tool_result(
return json.dumps(
{
"success": False,
"error": f"Failed to list todos: {e}",
@@ -335,19 +339,23 @@ async def list_todos(
"total_count": 0,
"summary": {"pending": 0, "in_progress": 0, "done": 0},
},
ensure_ascii=False,
default=str,
)
return dump_tool_result(
return json.dumps(
{
"success": True,
"todos": todos_list,
"total_count": len(todos_list),
"summary": summary,
},
ensure_ascii=False,
default=str,
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def update_todo(
ctx: RunContextWrapper,
todo_id: str | None = None,
@@ -387,8 +395,10 @@ async def update_todo(
},
)
if not updates_to_apply:
return dump_tool_result(
return json.dumps(
{"success": False, "error": "Provide todo_id or 'updates' list to update."},
ensure_ascii=False,
default=str,
)
updated: list[str] = []
@@ -407,7 +417,7 @@ async def update_todo(
else:
updated.append(upd["todo_id"])
except (ValueError, TypeError) as e:
return dump_tool_result({"success": False, "error": str(e)})
return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
response: dict[str, Any] = {
"success": len(errors) == 0,
@@ -418,7 +428,7 @@ async def update_todo(
}
if errors:
response["errors"] = errors
return dump_tool_result(response)
return json.dumps(response, ensure_ascii=False, default=str)
def _mark(
@@ -437,7 +447,7 @@ def _mark(
ids.append(todo_id)
if not ids:
msg = f"Provide todo_id or todo_ids to mark as {new_status}."
return dump_tool_result({"success": False, "error": msg})
return json.dumps({"success": False, "error": msg}, ensure_ascii=False, default=str)
marked: list[str] = []
errors: list[dict[str, Any]] = []
@@ -452,7 +462,7 @@ def _mark(
todo["updated_at"] = timestamp
marked.append(tid)
except (ValueError, TypeError) as e:
return dump_tool_result({"success": False, "error": str(e)})
return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
key = "marked_done" if new_status == "done" else "marked_pending"
response: dict[str, Any] = {
@@ -464,10 +474,10 @@ def _mark(
}
if errors:
response["errors"] = errors
return dump_tool_result(response)
return json.dumps(response, ensure_ascii=False, default=str)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def mark_todo_done(
ctx: RunContextWrapper,
todo_id: str | None = None,
@@ -488,7 +498,7 @@ async def mark_todo_done(
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def mark_todo_pending(
ctx: RunContextWrapper,
todo_id: str | None = None,
@@ -508,7 +518,7 @@ async def mark_todo_pending(
)
@strix_tool(timeout=30)
@function_tool(timeout=30)
async def delete_todo(
ctx: RunContextWrapper,
todo_id: str | None = None,
@@ -529,8 +539,10 @@ async def delete_todo(
if todo_id is not None:
ids.append(todo_id)
if not ids:
return dump_tool_result(
{"success": False, "error": "Provide todo_id or todo_ids to delete."}
return json.dumps(
{"success": False, "error": "Provide todo_id or todo_ids to delete."},
ensure_ascii=False,
default=str,
)
deleted: list[str] = []
@@ -542,7 +554,7 @@ async def delete_todo(
del agent_todos[tid]
deleted.append(tid)
except (ValueError, TypeError) as e:
return dump_tool_result({"success": False, "error": str(e)})
return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
response: dict[str, Any] = {
"success": len(errors) == 0,
@@ -553,4 +565,4 @@ async def delete_todo(
}
if errors:
response["errors"] = errors
return dump_tool_result(response)
return json.dumps(response, ensure_ascii=False, default=str)
-4
View File
@@ -1,4 +0,0 @@
from .tool import web_search
__all__ = ["web_search"]
+2 -4
View File
@@ -8,9 +8,7 @@ import os
from typing import Any
import requests
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from agents import RunContextWrapper, function_tool
_SYSTEM_PROMPT = """You are assisting a cybersecurity agent specialized in vulnerability scanning
@@ -84,7 +82,7 @@ def _do_search(query: str) -> dict[str, Any]:
# Perplexity request timeout is 300s; give the SDK a slightly larger
# budget so the round-trip + JSON decode doesn't push us over.
@strix_tool(timeout=330)
@function_tool(timeout=330)
async def web_search(ctx: RunContextWrapper, query: str) -> str:
"""Real-time web search via Perplexity — your primary research tool.