diff --git a/strix/config/__init__.py b/strix/config/__init__.py index eaef5f07..f21fdab6 100644 --- a/strix/config/__init__.py +++ b/strix/config/__init__.py @@ -15,7 +15,6 @@ from strix.config.loader import ( apply_config_override, load_settings, persist_current, - reload_settings, ) from strix.config.settings import ( ContextSettings, @@ -39,5 +38,4 @@ __all__ = [ "apply_config_override", "load_settings", "persist_current", - "reload_settings", ] diff --git a/strix/config/loader.py b/strix/config/loader.py index 4bc5de8f..5b940760 100644 --- a/strix/config/loader.py +++ b/strix/config/loader.py @@ -45,13 +45,6 @@ def load_settings() -> Settings: return _cached -def reload_settings() -> Settings: - """Invalidate the cache and re-resolve settings from env + JSON file.""" - global _cached # noqa: PLW0603 - _cached = None - return load_settings() - - def apply_config_override(path: Path) -> None: """Switch the JSON source to ``path`` and invalidate the cache.""" global _override, _cached # noqa: PLW0603 diff --git a/strix/core/execution.py b/strix/core/execution.py index a87118cd..fec727eb 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import contextlib -import dataclasses import logging import uuid from collections.abc import Callable @@ -22,14 +21,12 @@ from openai import ( RateLimitError, ) -from strix.config import codex, reload_settings -from strix.config.models import configure_sdk_model_defaults from strix.core.hooks import ( BudgetExceededError, BudgetPausedError, SubagentBudgetReservedError, ) -from strix.core.inputs import child_initial_input, make_model_settings +from strix.core.inputs import child_initial_input from strix.core.sessions import ( enforce_image_budget, open_agent_session, @@ -61,31 +58,6 @@ def _run_config_model(run_config: RunConfig) -> str | None: return run_config.model if isinstance(run_config.model, str) else None -def refresh_run_config_model(run_config: RunConfig) -> RunConfig: - """Adopt a changed configured model before re-running a woken agent.""" - current_model = _run_config_model(run_config) - if current_model is None: - return run_config - try: - settings = reload_settings() - except Exception: - logger.exception("settings reload failed; keeping model %s", current_model) - return run_config - new_model = (settings.llm.model or "").strip() - if not new_model or new_model == current_model: - return run_config - configure_sdk_model_defaults(settings) - model_settings = make_model_settings( - settings.llm.reasoning_effort, - model_name=new_model, - force_required_tool_choice=settings.llm.force_required_tool_choice, - request_timeout=settings.llm.timeout, - prompt_cache=settings.llm.prompt_cache, - ) - logger.info("model switched on wake: %s -> %s", current_model, new_model) - return dataclasses.replace(run_config, model=new_model, model_settings=model_settings) - - def _agent_instructions(agent: Any) -> str: instructions = getattr(agent, "instructions", None) return instructions if isinstance(instructions, str) else "" @@ -116,12 +88,6 @@ async def _compact_session( ) -_GUARDRAIL_PARK_ERROR = ( - "Blocked by the model's content guardrail (flagged as a possible cybersecurity risk). " - "Switch STRIX_LLM to a model that isn't blocked (update it in ~/.strix/cli-config.json " - "and message this agent, or resume the scan with a new STRIX_LLM)." -) - _TRANSIENT_MODEL_STATUS_CODES = frozenset({408, 500, 502, 503, 504}) _MAX_TRANSIENT_MODEL_RETRIES = 4 _TRANSIENT_MODEL_RETRY_BASE_DELAY_S = 2.0 @@ -232,7 +198,6 @@ async def run_agent_loop( raise SubagentBudgetReservedError("scan reached the sub-agent budget reserve") await coordinator.consume_pending(agent_id) - run_config = refresh_run_config_model(run_config) with contextlib.suppress(BudgetPausedError): result = await _run_cycle( agent, @@ -339,7 +304,6 @@ async def respawn_subagents( if coordinator.parent_of.get(aid) is None or aid == root_id: continue md["_restored_status"] = status - md["_restored_error"] = coordinator.errors.get(aid) candidates.append( ( aid, @@ -352,8 +316,7 @@ async def respawn_subagents( for child_id, name, parent_id, md in candidates: try: restored_status = str(md.get("_restored_status") or "running") - recoverable_park = restored_status == "waiting" and bool(md.get("_restored_error")) - start_parked = interactive and restored_status != "running" and not recoverable_park + start_parked = interactive and restored_status != "running" if start_parked: logger.warning( @@ -609,10 +572,6 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 if session is not None: input_data = [] continue - if codex.is_content_guardrail_error(exc): - return await _handle_content_guardrail( - coordinator, agent_id, exc, interactive=interactive - ) if not interactive: raise if isinstance(exc, MaxTurnsExceeded): @@ -630,22 +589,6 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 return stream -async def _handle_content_guardrail( - coordinator: AgentCoordinator, - agent_id: str, - exc: BaseException, - *, - interactive: bool, -) -> RunResultBase | None: - logger.warning("agent %s blocked by the model's content guardrail: %s", agent_id, exc) - if interactive: - await coordinator.set_status(agent_id, "waiting", error=_GUARDRAIL_PARK_ERROR) - return None - await coordinator.set_status(agent_id, "failed", error=_GUARDRAIL_PARK_ERROR) - await _notify_parent_on_terminal(coordinator, agent_id, "failed") - return None - - async def _settle_run_result( coordinator: AgentCoordinator, agent_id: str, diff --git a/strix/core/runner.py b/strix/core/runner.py index c5f51b15..fdb432d0 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -376,12 +376,6 @@ async def run_strix_scan( async with coordinator._lock: root_status = coordinator.statuses.get(root_id) - root_error = coordinator.errors.get(root_id) - - root_recoverable_park = root_status == "waiting" and bool(root_error) - root_start_parked = bool( - interactive and is_resume and root_status != "running" and not root_recoverable_park - ) result = await run_agent_loop( agent=root_agent, @@ -393,7 +387,7 @@ async def run_strix_scan( agent_id=root_id, interactive=interactive, session=root_session, - start_parked=root_start_parked, + start_parked=bool(interactive and is_resume and root_status != "running"), event_sink=event_sink, hooks=hooks, ) diff --git a/tests/test_execution.py b/tests/test_execution.py index 70a0e73f..a16880a4 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -6,21 +6,15 @@ import asyncio import contextlib import json from typing import Any -from unittest.mock import MagicMock import pytest -from agents import RunConfig from agents.memory import SQLiteSession from agents.tool_context import ToolContext -from strix.config import codex -from strix.core import execution from strix.core.agents import AgentCoordinator from strix.core.execution import ( - _handle_content_guardrail, _notify_parent_on_terminal, _notify_root_on_budget_reserve, - respawn_subagents, ) from strix.tools.finish.tool import finish_scan @@ -501,116 +495,3 @@ async def test_terminal_notice_does_not_cancel_parent_stream(tmp_path: Any) -> N assert stream.cancelled is False assert coordinator.pending_counts.get("root", 0) > 0 session.close() - - -@pytest.mark.asyncio -async def test_guardrail_interactive_parks_agent_wakeable(tmp_path: Any) -> None: - coordinator = AgentCoordinator() - await coordinator.register("root", "strix", parent_id=None) - await coordinator.register("child", "recon", parent_id="root") - exc = codex.CodexContentGuardrailError("chatgpt/gpt-5.6-sol") - - result = await _handle_content_guardrail(coordinator, "child", exc, interactive=True) - - assert result is None - assert coordinator.statuses["child"] == "waiting" - assert "STRIX_LLM" in coordinator.errors["child"] - - waiter = asyncio.create_task(coordinator.wait_for_message("child")) - await asyncio.sleep(0) - assert not waiter.done() - session = SQLiteSession("child", tmp_path / "agents.db") - await coordinator.attach_runtime("child", session=session) - await coordinator.send("child", {"from": "user", "content": "switched model, resume"}) - await asyncio.wait_for(waiter, timeout=1.0) - session.close() - - -@pytest.mark.asyncio -async def test_guardrail_noninteractive_fails_only_blocked_agent(tmp_path: Any) -> None: - coordinator = AgentCoordinator() - await coordinator.register("root", "strix", parent_id=None) - await coordinator.register("child", "recon", parent_id="root") - session = SQLiteSession("root", tmp_path / "agents.db") - await coordinator.attach_runtime("root", session=session) - exc = codex.CodexContentGuardrailError("chatgpt/gpt-5.6-sol") - - result = await _handle_content_guardrail(coordinator, "child", exc, interactive=False) - - assert result is None - assert coordinator.statuses["child"] == "failed" - assert "STRIX_LLM" in coordinator.errors["child"] - assert coordinator.statuses["root"] == "running" - assert coordinator.pending_counts.get("root", 0) > 0 - session.close() - - -@pytest.mark.asyncio -async def test_resume_revives_guardrail_parked_child_but_not_plain_waiting( - tmp_path: Any, monkeypatch: pytest.MonkeyPatch -) -> None: - coordinator = AgentCoordinator() - await coordinator.register("root", "strix", parent_id=None) - await coordinator.register("blocked", "recon", parent_id="root") - await coordinator.register("peer_waiter", "recon", parent_id="root") - await coordinator.set_status("blocked", "waiting", error="STRIX_LLM guardrail") - await coordinator.set_status("peer_waiter", "waiting") - - parked: dict[str, bool] = {} - - async def _fake_start_child_runner(**kwargs: Any) -> None: - parked[kwargs["child_id"]] = bool(kwargs["start_parked"]) - - monkeypatch.setattr(execution, "_start_child_runner", _fake_start_child_runner) - - await respawn_subagents( - coordinator=coordinator, - factory=lambda **_kwargs: object(), - agents_db_path=tmp_path / "agents.db", - sessions_to_close=[], - run_config=MagicMock(), - max_turns=10, - interactive=True, - parent_ctx={"agent_id": "root", "parent_id": None}, - root_id="root", - ) - - assert parked["blocked"] is False - assert parked["peer_waiter"] is True - - -class _StubLlmSettings: - def __init__(self, model: str) -> None: - self.model = model - self.reasoning_effort = None - self.force_required_tool_choice = False - self.timeout = None - self.prompt_cache = False - - -class _StubSettings: - def __init__(self, model: str) -> None: - self.llm = _StubLlmSettings(model) - - -def test_refresh_run_config_model_adopts_changed_model( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(execution, "reload_settings", lambda: _StubSettings("openai/gpt-5")) - monkeypatch.setattr(execution, "configure_sdk_model_defaults", lambda _settings: None) - run_config = RunConfig(model="chatgpt/gpt-5.6-sol") - - refreshed = execution.refresh_run_config_model(run_config) - - assert refreshed is not run_config - assert refreshed.model == "openai/gpt-5" - assert run_config.model == "chatgpt/gpt-5.6-sol" - - -def test_refresh_run_config_model_keeps_unchanged_model( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(execution, "reload_settings", lambda: _StubSettings("chatgpt/gpt-5.6-sol")) - run_config = RunConfig(model="chatgpt/gpt-5.6-sol") - - assert execution.refresh_run_config_model(run_config) is run_config