fix: adopt a changed STRIX_LLM when a parked agent wakes and surface guardrail errors in the TUI

This commit is contained in:
Ahmed Allam
2026-07-27 23:44:29 +00:00
parent e037d8d727
commit 1814a63de0
6 changed files with 86 additions and 6 deletions
+2
View File
@@ -15,6 +15,7 @@ from strix.config.loader import (
apply_config_override,
load_settings,
persist_current,
reload_settings,
)
from strix.config.settings import (
ContextSettings,
@@ -38,4 +39,5 @@ __all__ = [
"apply_config_override",
"load_settings",
"persist_current",
"reload_settings",
]
+7
View File
@@ -45,6 +45,13 @@ 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
+32 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
import dataclasses
import logging
import uuid
from collections.abc import Callable
@@ -21,13 +22,14 @@ from openai import (
RateLimitError,
)
from strix.config import codex
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
from strix.core.inputs import child_initial_input, make_model_settings
from strix.core.sessions import (
enforce_image_budget,
open_agent_session,
@@ -59,6 +61,31 @@ 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 ""
@@ -91,7 +118,8 @@ async def _compact_session(
_GUARDRAIL_PARK_ERROR = (
"Blocked by the model's content guardrail (flagged as a possible cybersecurity risk). "
"Set STRIX_LLM to a model that isn't blocked and resume the scan to continue."
"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})
@@ -204,6 +232,7 @@ 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,
+6 -2
View File
@@ -1040,9 +1040,9 @@ class StrixTUIApp(App): # type: ignore[misc]
name=names.get(agent_id, agent_id),
parent_id=parent_of.get(agent_id),
status=status,
error_message=error,
error_message=error or "",
)
if status in {"failed", "crashed"} and error:
if status in {"failed", "crashed", "waiting"} and error:
if agent_id not in self._error_noted_agents:
self._error_noted_agents.add(agent_id)
self.live_view.record_agent_error(agent_id, error)
@@ -1292,6 +1292,10 @@ class StrixTUIApp(App): # type: ignore[misc]
text.append("Send a message to continue", style="dim")
keymap = keymap_styled([("ctrl-q", "quit")])
else:
error_msg = agent_data.get("error_message") or ""
if error_msg:
text.append(error_msg, style="red")
text.append(" \u00b7 ", style="dim")
text.append("Send message to resume", style="dim")
return (text, keymap, False)
+1 -1
View File
@@ -82,7 +82,7 @@ class TuiLiveView:
current["parent_id"] = parent_id
if status is not None:
current["status"] = status
if error_message:
if error_message is not None:
current["error_message"] = error_message
current["updated_at"] = now
+38
View File
@@ -9,6 +9,7 @@ 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
@@ -576,3 +577,40 @@ async def test_resume_revives_guardrail_parked_child_but_not_plain_waiting(
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