refactor: dedupe `_dump` helper, collapse retry-policy plumbing, scrub test scars

Tools:
- Add a single ``dump_tool_result`` helper in ``tools/_decorator.py``
  and remove the eight identical ``_dump`` definitions from
  ``proxy/tools.py``, ``file_edit/tools.py``, ``python/tool.py``,
  ``terminal/tool.py``, ``todo/tools.py``, ``browser/tool.py``,
  ``notes/tools.py``, ``agents_graph/tools.py``. Imports trimmed.
  Net -50 LoC across the tool modules.

run_config_factory:
- Inline the four retry-policy plumbing pieces
  (``_RETRYABLE_HTTP_STATUSES``, ``_DEFAULT_MAX_RETRIES``,
  ``_DEFAULT_BACKOFF``, ``_default_retry_policy()``) into a single
  module-level ``_DEFAULT_RETRY`` ``ModelRetrySettings`` literal. The
  inputs were never overridden and the helper had one caller.

Tests:
- Drop migration scars from ``tests/test_run_config_factory.py``
  (``Phase 1`` / ``C1`` / ``C11`` / ``C21`` / ``HARNESS_WIKI`` / ``AUDIT``
  references). Replace the ``_RETRYABLE_HTTP_STATUSES``-touching test
  with a ``retry.policy is not None`` smoke check now that the constant
  has been inlined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 12:54:44 -07:00
parent d959fe2163
commit 49c38de3b2
11 changed files with 114 additions and 164 deletions
+11
View File
@@ -14,6 +14,7 @@ Defaults:
from __future__ import annotations
import json
from collections.abc import Callable
from typing import Any, Literal
@@ -25,6 +26,16 @@ _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,
+20 -24
View File
@@ -26,7 +26,7 @@ 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 strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
if TYPE_CHECKING:
@@ -38,10 +38,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
def _ctx(ctx: RunContextWrapper) -> dict[str, Any]:
return ctx.context if isinstance(ctx.context, dict) else {}
@@ -61,7 +57,7 @@ async def view_agent_graph(ctx: RunContextWrapper) -> str:
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None:
return _dump({"success": False, "error": "Bus not initialized in context."})
return dump_tool_result({"success": False, "error": "Bus not initialized in context."})
async with bus._lock:
parent_of = dict(bus.parent_of)
@@ -90,7 +86,7 @@ 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(
return dump_tool_result(
{
"success": True,
"graph_structure": "\n".join(lines) or "(no agents)",
@@ -115,17 +111,17 @@ async def agent_status(ctx: RunContextWrapper, agent_id: str) -> str:
inner = _ctx(ctx)
bus = inner.get("bus")
if bus is None:
return _dump({"success": False, "error": "Bus not initialized in context."})
return dump_tool_result({"success": False, "error": "Bus not initialized in context."})
async with bus._lock:
if agent_id not in bus.statuses:
return _dump(
return dump_tool_result(
{
"success": False,
"error": f"Unknown agent_id: {agent_id}",
}
)
return _dump(
return dump_tool_result(
{
"success": True,
"agent_id": agent_id,
@@ -173,11 +169,11 @@ 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({"success": False, "error": "Bus or agent_id missing in context."})
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
async with bus._lock:
if target_agent_id not in bus.statuses:
return _dump(
return dump_tool_result(
{
"success": False,
"error": f"Target agent '{target_agent_id}' not found.",
@@ -186,7 +182,7 @@ async def send_message_to_agent(
target_status = bus.statuses.get(target_agent_id)
if target_status in ("completed", "crashed", "stopped"):
return _dump(
return dump_tool_result(
{
"success": False,
"error": f"Target agent '{target_agent_id}' is {target_status}; message dropped.",
@@ -204,7 +200,7 @@ async def send_message_to_agent(
"priority": priority,
},
)
return _dump(
return dump_tool_result(
{
"success": True,
"message_id": msg_id,
@@ -255,7 +251,7 @@ async def wait_for_message(
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None or me is None:
return _dump({"success": False, "error": "Bus or agent_id missing in context."})
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
async with bus._lock:
bus.statuses[me] = "waiting"
@@ -268,7 +264,7 @@ async def wait_for_message(
if pending > 0:
async with bus._lock:
bus.statuses[me] = "running"
return _dump(
return dump_tool_result(
{
"success": True,
"status": "message_arrived",
@@ -284,7 +280,7 @@ async def wait_for_message(
if bus.statuses.get(me) == "waiting":
bus.statuses[me] = "running"
return _dump(
return dump_tool_result(
{
"success": True,
"status": "timeout",
@@ -348,9 +344,9 @@ async def create_agent(
factory: Callable[..., SDKAgent] | None = inner.get("agent_factory")
if bus is None or parent_id is None:
return _dump({"success": False, "error": "Bus or agent_id missing in context."})
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
if factory is None:
return _dump(
return dump_tool_result(
{
"success": False,
"error": (
@@ -366,7 +362,7 @@ 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(
return dump_tool_result(
{
"success": False,
"error": f"agent_factory failed: {e!s}",
@@ -444,7 +440,7 @@ async def create_agent(
async with bus._lock:
bus.tasks[child_id] = task_handle
return _dump(
return dump_tool_result(
{
"success": True,
"agent_id": child_id,
@@ -502,11 +498,11 @@ async def agent_finish(
bus = inner.get("bus")
me = inner.get("agent_id")
if bus is None or me is None:
return _dump({"success": False, "error": "Bus or agent_id missing in context."})
return dump_tool_result({"success": False, "error": "Bus or agent_id missing in context."})
parent_id = inner.get("parent_id")
if parent_id is None:
return _dump(
return dump_tool_result(
{
"success": False,
"agent_completed": False,
@@ -547,7 +543,7 @@ async def agent_finish(
)
parent_notified = True
return _dump(
return dump_tool_result(
{
"success": True,
"agent_completed": True,
+3 -8
View File
@@ -13,19 +13,14 @@ the model.
from __future__ import annotations
import json
from typing import Any, Literal
from typing import Literal
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
from strix.tools._sandbox_dispatch import post_to_sandbox
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
BrowserAction = Literal[
"launch",
"goto",
@@ -137,7 +132,7 @@ async def browser_action(
clear: For ``get_console_logs``, clear logs after retrieval
(default False).
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"browser_action",
+4 -11
View File
@@ -13,19 +13,12 @@ sandbox-only dependency).
from __future__ import annotations
import json
from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
from strix.tools._sandbox_dispatch import post_to_sandbox
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
@strix_tool(timeout=180)
async def str_replace_editor(
ctx: RunContextWrapper,
@@ -66,7 +59,7 @@ async def str_replace_editor(
insert_line: Required for ``insert``; new content goes AFTER
this line.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"str_replace_editor",
@@ -98,7 +91,7 @@ async def list_files(
path: Directory path; relative paths anchor at ``/workspace``.
recursive: When True, walks subdirectories.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"list_files",
@@ -126,7 +119,7 @@ async def search_files(
file_pattern: Glob filter (e.g. ``"*.py"``, ``"*.{js,ts}"``).
Defaults to all files.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"search_files",
+6 -10
View File
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
logger = logging.getLogger(__name__)
@@ -36,10 +36,6 @@ _loaded_notes_run_dir: str | None = None
_DEFAULT_CONTENT_PREVIEW_CHARS = 280
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
def _get_run_dir() -> Path | None:
try:
from strix.telemetry.tracer import get_global_tracer
@@ -441,7 +437,7 @@ async def create_note(
category: One of the categories above. Default ``"general"``.
tags: Optional free-form tags.
"""
return _dump(
return dump_tool_result(
await asyncio.to_thread(_create_note_impl, title, content, category, tags),
)
@@ -472,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(
return dump_tool_result(
await asyncio.to_thread(
_list_notes_impl,
category=category,
@@ -490,7 +486,7 @@ async def get_note(ctx: RunContextWrapper, note_id: str) -> str:
Args:
note_id: Note id from ``create_note`` or a ``list_notes`` entry.
"""
return _dump(await asyncio.to_thread(_get_note_impl, note_id))
return dump_tool_result(await asyncio.to_thread(_get_note_impl, note_id))
@strix_tool(timeout=30)
@@ -513,7 +509,7 @@ async def update_note(
content: New content, or ``None`` to keep.
tags: New tags list, or ``None`` to keep.
"""
return _dump(
return dump_tool_result(
await asyncio.to_thread(
_update_note_impl,
note_id=note_id,
@@ -531,4 +527,4 @@ async def delete_note(ctx: RunContextWrapper, note_id: str) -> str:
Args:
note_id: Note id to delete.
"""
return _dump(await asyncio.to_thread(_delete_note_impl, note_id))
return dump_tool_result(await asyncio.to_thread(_delete_note_impl, note_id))
+8 -13
View File
@@ -10,19 +10,14 @@ scope_rules, list_sitemap, view_sitemap_entry.
from __future__ import annotations
import json
from typing import Any, Literal
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
from strix.tools._sandbox_dispatch import post_to_sandbox
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
RequestPart = Literal["request", "response"]
SortBy = Literal[
"timestamp",
@@ -80,7 +75,7 @@ async def list_requests(
sort_order: ``asc`` or ``desc``.
scope_id: Restrict to a scope (managed via ``scope_rules``).
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"list_requests",
@@ -131,7 +126,7 @@ async def view_request(
page: 1-indexed page number (only when no ``search_pattern``).
page_size: Lines per page.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"view_request",
@@ -172,7 +167,7 @@ async def send_request(
body: Optional request body string.
timeout: Per-request timeout in seconds (default 30).
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"send_request",
@@ -221,7 +216,7 @@ async def repeat_request(
- ``body`` — replace the body string entirely.
- ``cookies`` — dict of cookies to add/update.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"repeat_request",
@@ -280,7 +275,7 @@ async def scope_rules(
scope_id: Required for ``get`` / ``update`` / ``delete``.
scope_name: Required for ``create`` / ``update``.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"scope_rules",
@@ -328,7 +323,7 @@ async def list_sitemap(
depth: ``"DIRECT"`` (immediate children) or ``"ALL"`` (recursive).
page: 1-indexed page (30 entries/page).
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"list_sitemap",
@@ -353,6 +348,6 @@ async def view_sitemap_entry(ctx: RunContextWrapper, entry_id: str) -> str:
Args:
entry_id: Sitemap entry id from ``list_sitemap``.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(ctx, "view_sitemap_entry", {"entry_id": entry_id}),
)
+3 -8
View File
@@ -7,19 +7,14 @@ across multiple ``execute`` calls. Pure pass-through wrapper.
from __future__ import annotations
import json
from typing import Any, Literal
from typing import Literal
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
from strix.tools._sandbox_dispatch import post_to_sandbox
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
PythonAction = Literal["new_session", "execute", "close", "list_sessions"]
@@ -82,7 +77,7 @@ async def python_action(
session_id: Required for ``execute`` / ``close``. Optional for
``new_session`` (auto-generated when omitted).
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"python_action",
+2 -9
View File
@@ -7,19 +7,12 @@ host-side wrapper is a thin pass-through.
from __future__ import annotations
import json
from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
from strix.tools._sandbox_dispatch import post_to_sandbox
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
@strix_tool(timeout=180)
async def terminal_execute(
ctx: RunContextWrapper,
@@ -92,7 +85,7 @@ async def terminal_execute(
concurrent sessions.
no_enter: When True, sends keystrokes without a trailing return.
"""
return _dump(
return dump_tool_result(
await post_to_sandbox(
ctx,
"terminal_execute",
+19 -19
View File
@@ -15,7 +15,7 @@ from typing import Any
from agents import RunContextWrapper
from strix.tools._decorator import strix_tool
from strix.tools._decorator import dump_tool_result, strix_tool
VALID_PRIORITIES = ["low", "normal", "high", "critical"]
@@ -39,10 +39,6 @@ def _todo_sort_key(todo: dict[str, Any]) -> tuple[int, int, str]:
_todos_storage: dict[str, dict[str, dict[str, Any]]] = {}
def _dump(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False, default=str)
def _agent_id_from(ctx: RunContextWrapper) -> str:
inner = ctx.context if isinstance(ctx.context, dict) else {}
return str(inner.get("agent_id") or "default")
@@ -252,7 +248,7 @@ async def create_todo(
},
)
if not tasks:
return _dump(
return dump_tool_result(
{
"success": False,
"error": "Provide a title or 'todos' list to create.",
@@ -277,9 +273,11 @@ async def create_todo(
}
created.append({"todo_id": todo_id, "title": task["title"], "priority": task_priority})
except (ValueError, TypeError) as e:
return _dump({"success": False, "error": f"Failed to create todo: {e}", "todo_id": None})
return dump_tool_result(
{"success": False, "error": f"Failed to create todo: {e}", "todo_id": None}
)
return _dump(
return dump_tool_result(
{
"success": True,
"created": created,
@@ -329,7 +327,7 @@ async def list_todos(
sv = todo.get("status", "pending")
summary[sv] = summary.get(sv, 0) + 1
except (ValueError, TypeError) as e:
return _dump(
return dump_tool_result(
{
"success": False,
"error": f"Failed to list todos: {e}",
@@ -339,7 +337,7 @@ async def list_todos(
},
)
return _dump(
return dump_tool_result(
{
"success": True,
"todos": todos_list,
@@ -389,7 +387,7 @@ async def update_todo(
},
)
if not updates_to_apply:
return _dump(
return dump_tool_result(
{"success": False, "error": "Provide todo_id or 'updates' list to update."},
)
@@ -409,7 +407,7 @@ async def update_todo(
else:
updated.append(upd["todo_id"])
except (ValueError, TypeError) as e:
return _dump({"success": False, "error": str(e)})
return dump_tool_result({"success": False, "error": str(e)})
response: dict[str, Any] = {
"success": len(errors) == 0,
@@ -420,7 +418,7 @@ async def update_todo(
}
if errors:
response["errors"] = errors
return _dump(response)
return dump_tool_result(response)
def _mark(
@@ -439,7 +437,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({"success": False, "error": msg})
return dump_tool_result({"success": False, "error": msg})
marked: list[str] = []
errors: list[dict[str, Any]] = []
@@ -454,7 +452,7 @@ def _mark(
todo["updated_at"] = timestamp
marked.append(tid)
except (ValueError, TypeError) as e:
return _dump({"success": False, "error": str(e)})
return dump_tool_result({"success": False, "error": str(e)})
key = "marked_done" if new_status == "done" else "marked_pending"
response: dict[str, Any] = {
@@ -466,7 +464,7 @@ def _mark(
}
if errors:
response["errors"] = errors
return _dump(response)
return dump_tool_result(response)
@strix_tool(timeout=30)
@@ -531,7 +529,9 @@ async def delete_todo(
if todo_id is not None:
ids.append(todo_id)
if not ids:
return _dump({"success": False, "error": "Provide todo_id or todo_ids to delete."})
return dump_tool_result(
{"success": False, "error": "Provide todo_id or todo_ids to delete."}
)
deleted: list[str] = []
errors: list[dict[str, Any]] = []
@@ -542,7 +542,7 @@ async def delete_todo(
del agent_todos[tid]
deleted.append(tid)
except (ValueError, TypeError) as e:
return _dump({"success": False, "error": str(e)})
return dump_tool_result({"success": False, "error": str(e)})
response: dict[str, Any] = {
"success": len(errors) == 0,
@@ -553,4 +553,4 @@ async def delete_todo(
}
if errors:
response["errors"] = errors
return _dump(response)
return dump_tool_result(response)