mirror of
https://github.com/usestrix/strix.git
synced 2026-08-21 10:48:59 +02:00
fix: adopt a changed STRIX_LLM when a parked agent wakes and surface guardrail errors in the TUI
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user