diff --git a/strix/run_config_factory.py b/strix/run_config_factory.py index 9c14f8b..5a82a28 100644 --- a/strix/run_config_factory.py +++ b/strix/run_config_factory.py @@ -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( diff --git a/strix/tools/_decorator.py b/strix/tools/_decorator.py index 944f541..634ccaf 100644 --- a/strix/tools/_decorator.py +++ b/strix/tools/_decorator.py @@ -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, diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 5a76bef..7207db2 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -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, diff --git a/strix/tools/browser/tool.py b/strix/tools/browser/tool.py index 01e6d11..7666803 100644 --- a/strix/tools/browser/tool.py +++ b/strix/tools/browser/tool.py @@ -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", diff --git a/strix/tools/file_edit/tools.py b/strix/tools/file_edit/tools.py index 4a9ae0c..e7f6646 100644 --- a/strix/tools/file_edit/tools.py +++ b/strix/tools/file_edit/tools.py @@ -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", diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py index b8ead6c..50c382d 100644 --- a/strix/tools/notes/tools.py +++ b/strix/tools/notes/tools.py @@ -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)) diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py index 518f212..3e312bb 100644 --- a/strix/tools/proxy/tools.py +++ b/strix/tools/proxy/tools.py @@ -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}), ) diff --git a/strix/tools/python/tool.py b/strix/tools/python/tool.py index 47faea2..bc99767 100644 --- a/strix/tools/python/tool.py +++ b/strix/tools/python/tool.py @@ -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", diff --git a/strix/tools/terminal/tool.py b/strix/tools/terminal/tool.py index 13b91a0..6963ef3 100644 --- a/strix/tools/terminal/tool.py +++ b/strix/tools/terminal/tool.py @@ -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", diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py index 990e3cb..74e00c8 100644 --- a/strix/tools/todo/tools.py +++ b/strix/tools/todo/tools.py @@ -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) diff --git a/tests/test_run_config_factory.py b/tests/test_run_config_factory.py index 05693e9..159ea22 100644 --- a/tests/test_run_config_factory.py +++ b/tests/test_run_config_factory.py @@ -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"