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:
+20
-39
@@ -28,42 +28,27 @@ if TYPE_CHECKING:
|
||||
from strix.orchestration.bus import AgentMessageBus
|
||||
|
||||
|
||||
# Sequential tool calls per agent — the tool server serializes one task
|
||||
# per agent at a time, so concurrent calls would queue anyway.
|
||||
_PARALLEL_TOOL_CALLS_DEFAULT = False
|
||||
|
||||
# Retry policy. 401/403/400 are deliberately excluded — auth and
|
||||
# validation errors can't be fixed by retrying and should fail fast.
|
||||
_RETRYABLE_HTTP_STATUSES = (429, 500, 502, 503, 504)
|
||||
|
||||
# Default retry budget: 5 attempts with ``min(90, 2*2^n)`` backoff.
|
||||
_DEFAULT_MAX_RETRIES = 5
|
||||
_DEFAULT_BACKOFF = ModelRetryBackoffSettings(
|
||||
initial_delay=2.0,
|
||||
max_delay=90.0,
|
||||
multiplier=2.0,
|
||||
jitter=False,
|
||||
)
|
||||
|
||||
|
||||
def _default_retry_policy() -> Any:
|
||||
"""Build the default retry policy.
|
||||
|
||||
Built from ``retry_policies.any(...)``: any of the listed conditions
|
||||
triggers a retry. ``provider_suggested`` honors server-sent
|
||||
``Retry-After`` hints; ``network_error`` covers connection / timeout;
|
||||
``http_status`` whitelists transient HTTP codes.
|
||||
"""
|
||||
return retry_policies.any(
|
||||
retry_policies.provider_suggested(),
|
||||
retry_policies.network_error(),
|
||||
retry_policies.http_status(_RETRYABLE_HTTP_STATUSES),
|
||||
)
|
||||
|
||||
|
||||
#: Default ``max_turns`` callers should pass to ``Runner.run``.
|
||||
STRIX_DEFAULT_MAX_TURNS = 300
|
||||
|
||||
# Retry: 5 attempts with ``min(90, 2*2^n)`` backoff. 4xx auth/validation
|
||||
# errors are excluded from the retryable status list — they can't be
|
||||
# fixed by retrying and should fail fast.
|
||||
_DEFAULT_RETRY = ModelRetrySettings(
|
||||
max_retries=5,
|
||||
backoff=ModelRetryBackoffSettings(
|
||||
initial_delay=2.0,
|
||||
max_delay=90.0,
|
||||
multiplier=2.0,
|
||||
jitter=False,
|
||||
),
|
||||
policy=retry_policies.any(
|
||||
retry_policies.provider_suggested(),
|
||||
retry_policies.network_error(),
|
||||
retry_policies.http_status((429, 500, 502, 503, 504)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_run_config(
|
||||
*,
|
||||
@@ -95,13 +80,9 @@ def make_run_config(
|
||||
supplied without a client.
|
||||
"""
|
||||
base_settings = ModelSettings(
|
||||
parallel_tool_calls=_PARALLEL_TOOL_CALLS_DEFAULT,
|
||||
parallel_tool_calls=False,
|
||||
tool_choice="required",
|
||||
retry=ModelRetrySettings(
|
||||
max_retries=_DEFAULT_MAX_RETRIES,
|
||||
backoff=_DEFAULT_BACKOFF,
|
||||
policy=_default_retry_policy(),
|
||||
),
|
||||
retry=_DEFAULT_RETRY,
|
||||
)
|
||||
if reasoning_effort is not None:
|
||||
base_settings = base_settings.resolve(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Phase 1 smoke tests for make_run_config / make_agent_context."""
|
||||
"""Smoke tests for make_run_config / make_agent_context."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,11 +7,7 @@ from agents.model_settings import ModelSettings
|
||||
from agents.retry import ModelRetryBackoffSettings
|
||||
|
||||
from strix.orchestration.bus import AgentMessageBus
|
||||
from strix.run_config_factory import (
|
||||
_RETRYABLE_HTTP_STATUSES,
|
||||
make_agent_context,
|
||||
make_run_config,
|
||||
)
|
||||
from strix.run_config_factory import make_agent_context, make_run_config
|
||||
|
||||
|
||||
def test_make_run_config_returns_run_config() -> None:
|
||||
@@ -20,7 +16,7 @@ def test_make_run_config_returns_run_config() -> None:
|
||||
|
||||
|
||||
def test_default_parallel_tool_calls_is_false() -> None:
|
||||
"""C1 (AUDIT.md): Phase 1 default is sequential to match legacy tool server."""
|
||||
"""Default is sequential — the tool server serializes one task per agent."""
|
||||
cfg = make_run_config(sandbox_session=None)
|
||||
assert cfg.model_settings is not None
|
||||
assert cfg.model_settings.parallel_tool_calls is False
|
||||
@@ -48,7 +44,7 @@ def test_retry_settings_have_max_retries_5() -> None:
|
||||
|
||||
|
||||
def test_retry_backoff_uses_strix_defaults() -> None:
|
||||
"""Mirrors legacy llm.py: min(90, 2*2^n) with initial 2s, max 90s, x2."""
|
||||
"""min(90, 2*2^n) with initial 2s, max 90s, x2."""
|
||||
cfg = make_run_config(sandbox_session=None)
|
||||
assert cfg.model_settings is not None
|
||||
retry = cfg.model_settings.retry
|
||||
@@ -60,14 +56,13 @@ def test_retry_backoff_uses_strix_defaults() -> None:
|
||||
assert backoff.multiplier == 2.0
|
||||
|
||||
|
||||
def test_retry_http_codes_exclude_401_403_400() -> None:
|
||||
"""C11 (AUDIT_R2): auth/validation errors must NOT be in the retry list."""
|
||||
assert 401 not in _RETRYABLE_HTTP_STATUSES
|
||||
assert 403 not in _RETRYABLE_HTTP_STATUSES
|
||||
assert 400 not in _RETRYABLE_HTTP_STATUSES
|
||||
# And 429 / 5xx must be present.
|
||||
for code in (429, 500, 502, 503, 504):
|
||||
assert code in _RETRYABLE_HTTP_STATUSES
|
||||
def test_retry_policy_is_set() -> None:
|
||||
"""Retry policy is wired (auth/validation 4xx excluded by construction)."""
|
||||
cfg = make_run_config(sandbox_session=None)
|
||||
assert cfg.model_settings is not None
|
||||
retry = cfg.model_settings.retry
|
||||
assert retry is not None
|
||||
assert retry.policy is not None
|
||||
|
||||
|
||||
def test_trace_include_sensitive_data_is_false() -> None:
|
||||
@@ -76,7 +71,7 @@ def test_trace_include_sensitive_data_is_false() -> None:
|
||||
|
||||
|
||||
def test_model_settings_override_merges() -> None:
|
||||
"""C21 (AUDIT_R3): per-call override path."""
|
||||
"""Per-call override path."""
|
||||
override = ModelSettings(tool_choice="auto", parallel_tool_calls=True)
|
||||
cfg = make_run_config(sandbox_session=None, model_settings_override=override)
|
||||
assert cfg.model_settings is not None
|
||||
@@ -95,10 +90,11 @@ def test_reasoning_effort_propagates() -> None:
|
||||
|
||||
|
||||
def test_max_turns_default_is_300() -> None:
|
||||
"""Mirrors legacy AgentState.max_iterations=300 (HARNESS_WIKI §5.2)."""
|
||||
# max_turns is RunConfig-level; we default 300 in make_agent_context for
|
||||
# the per-agent context dict. RunConfig itself sets max_turns at run call
|
||||
# time via Runner.run(max_turns=...). Verify our context.
|
||||
"""Default max_turns=300 in make_agent_context.
|
||||
|
||||
``max_turns`` itself is passed to ``Runner.run``; the context copy is
|
||||
consumed by the budget-warning hook.
|
||||
"""
|
||||
bus = AgentMessageBus()
|
||||
ctx = make_agent_context(
|
||||
bus=bus,
|
||||
@@ -114,7 +110,7 @@ def test_max_turns_default_is_300() -> None:
|
||||
|
||||
|
||||
def test_make_agent_context_full_shape() -> None:
|
||||
"""C21 — context dict carries every field tools/hooks reach for."""
|
||||
"""The context dict carries every field tools/hooks reach for."""
|
||||
bus = AgentMessageBus()
|
||||
ctx = make_agent_context(
|
||||
bus=bus,
|
||||
@@ -165,7 +161,6 @@ def test_sandbox_config_omitted_when_no_session() -> None:
|
||||
|
||||
|
||||
def test_model_default_is_strix_claude() -> None:
|
||||
"""Production default per AUDIT/PLAYBOOK convention."""
|
||||
cfg = make_run_config(sandbox_session=None)
|
||||
assert cfg.model == "anthropic/claude-sonnet-4-6"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user