From b3f7cfd040f5bf9873585ef289e0a9aa2466361e Mon Sep 17 00:00:00 2001 From: 0xallam Date: Sat, 25 Apr 2026 15:17:46 -0700 Subject: [PATCH] refactor: nuke ``strix_tool`` shim + dead package re-exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``@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//__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) --- pyproject.toml | 3 - strix/agents/__init__.py | 25 ++---- strix/orchestration/__init__.py | 24 ++--- strix/runtime/session_manager.py | 10 --- strix/tools/__init__.py | 12 +-- strix/tools/_decorator.py | 73 --------------- strix/tools/agents_graph/__init__.py | 18 ---- strix/tools/agents_graph/tools.py | 129 ++++++++++++++++++--------- strix/tools/finish/__init__.py | 4 - strix/tools/finish/tool.py | 6 +- strix/tools/notes/__init__.py | 10 --- strix/tools/notes/tools.py | 34 ++++--- strix/tools/proxy/__init__.py | 10 --- strix/tools/proxy/tools.py | 111 ++++++++++++++++------- strix/tools/reporting/__init__.py | 4 - strix/tools/reporting/tool.py | 6 +- strix/tools/thinking/__init__.py | 4 - strix/tools/thinking/tool.py | 4 +- strix/tools/todo/__init__.py | 18 ---- strix/tools/todo/tools.py | 62 +++++++------ strix/tools/web_search/__init__.py | 4 - strix/tools/web_search/tool.py | 6 +- 22 files changed, 257 insertions(+), 320 deletions(-) delete mode 100644 strix/tools/_decorator.py diff --git a/pyproject.toml b/pyproject.toml index 00fc2e3..22cf775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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, diff --git a/strix/agents/__init__.py b/strix/agents/__init__.py index c80aa04..85c335a 100644 --- a/strix/agents/__init__.py +++ b/strix/agents/__init__.py @@ -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", -] diff --git a/strix/orchestration/__init__.py b/strix/orchestration/__init__.py index 8b0ada6..3451984 100644 --- a/strix/orchestration/__init__.py +++ b/strix/orchestration/__init__.py @@ -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", -] diff --git a/strix/runtime/session_manager.py b/strix/runtime/session_manager.py index 326da68..277092a 100644 --- a/strix/runtime/session_manager.py +++ b/strix/runtime/session_manager.py @@ -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() diff --git a/strix/tools/__init__.py b/strix/tools/__init__.py index 2a11b59..ab4dadc 100644 --- a/strix/tools/__init__.py +++ b/strix/tools/__init__.py @@ -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. +""" diff --git a/strix/tools/_decorator.py b/strix/tools/_decorator.py deleted file mode 100644 index 634ccaf..0000000 --- a/strix/tools/_decorator.py +++ /dev/null @@ -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, - ) diff --git a/strix/tools/agents_graph/__init__.py b/strix/tools/agents_graph/__init__.py index 07f5f08..e69de29 100644 --- a/strix/tools/agents_graph/__init__.py +++ b/strix/tools/agents_graph/__init__.py @@ -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", -] diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index c919a0e..11c9bd5 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -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, ) diff --git a/strix/tools/finish/__init__.py b/strix/tools/finish/__init__.py index a41ffde..e69de29 100644 --- a/strix/tools/finish/__init__.py +++ b/strix/tools/finish/__init__.py @@ -1,4 +0,0 @@ -from .tool import finish_scan - - -__all__ = ["finish_scan"] diff --git a/strix/tools/finish/tool.py b/strix/tools/finish/tool.py index 4b5f84c..95dc623 100644 --- a/strix/tools/finish/tool.py +++ b/strix/tools/finish/tool.py @@ -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, diff --git a/strix/tools/notes/__init__.py b/strix/tools/notes/__init__.py index 287e6e4..e69de29 100644 --- a/strix/tools/notes/__init__.py +++ b/strix/tools/notes/__init__.py @@ -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", -] diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py index 50c382d..1818a95 100644 --- a/strix/tools/notes/tools.py +++ b/strix/tools/notes/tools.py @@ -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 + ) diff --git a/strix/tools/proxy/__init__.py b/strix/tools/proxy/__init__.py index 4a4b324..e69de29 100644 --- a/strix/tools/proxy/__init__.py +++ b/strix/tools/proxy/__init__.py @@ -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", -] diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py index aca9024..b68fb04 100644 --- a/strix/tools/proxy/tools.py +++ b/strix/tools/proxy/tools.py @@ -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, ) diff --git a/strix/tools/reporting/__init__.py b/strix/tools/reporting/__init__.py index ddfa722..e69de29 100644 --- a/strix/tools/reporting/__init__.py +++ b/strix/tools/reporting/__init__.py @@ -1,4 +0,0 @@ -from .tool import create_vulnerability_report - - -__all__ = ["create_vulnerability_report"] diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index a9d6566..b2b8810 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -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, diff --git a/strix/tools/thinking/__init__.py b/strix/tools/thinking/__init__.py index a4e81e1..e69de29 100644 --- a/strix/tools/thinking/__init__.py +++ b/strix/tools/thinking/__init__.py @@ -1,4 +0,0 @@ -from .tool import think - - -__all__ = ["think"] diff --git a/strix/tools/thinking/tool.py b/strix/tools/thinking/tool.py index 511815e..42e4df5 100644 --- a/strix/tools/thinking/tool.py +++ b/strix/tools/thinking/tool.py @@ -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. diff --git a/strix/tools/todo/__init__.py b/strix/tools/todo/__init__.py index 0fc7399..e69de29 100644 --- a/strix/tools/todo/__init__.py +++ b/strix/tools/todo/__init__.py @@ -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", -] diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py index 74e00c8..6ba5ddf 100644 --- a/strix/tools/todo/tools.py +++ b/strix/tools/todo/tools.py @@ -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) diff --git a/strix/tools/web_search/__init__.py b/strix/tools/web_search/__init__.py index 2330083..e69de29 100644 --- a/strix/tools/web_search/__init__.py +++ b/strix/tools/web_search/__init__.py @@ -1,4 +0,0 @@ -from .tool import web_search - - -__all__ = ["web_search"] diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py index be0628c..4c72ef1 100644 --- a/strix/tools/web_search/tool.py +++ b/strix/tools/web_search/tool.py @@ -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.