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
+14 -33
View File
@@ -28,43 +28,28 @@ 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
#: Default ``max_turns`` callers should pass to ``Runner.run``.
STRIX_DEFAULT_MAX_TURNS = 300
# 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(
# 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,
)
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(
),
policy=retry_policies.any(
retry_policies.provider_suggested(),
retry_policies.network_error(),
retry_policies.http_status(_RETRYABLE_HTTP_STATUSES),
retry_policies.http_status((429, 500, 502, 503, 504)),
),
)
#: Default ``max_turns`` callers should pass to ``Runner.run``.
STRIX_DEFAULT_MAX_TURNS = 300
def make_run_config(
*,
sandbox_session: BaseSandboxSession | None,
@@ -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(
+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)
+18 -23
View File
@@ -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"