From 42b55818ff0c4831da363a259cfbb53aeb49cc3f Mon Sep 17 00:00:00 2001 From: kdmarble Date: Sat, 25 Apr 2026 22:13:07 -0500 Subject: [PATCH] feat(agent): route background reviews via config --- cli-config.yaml.example | 9 +++ run_agent.py | 26 +++++++- .../test_background_review_summary.py | 65 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 90d98490c5a1..fe5bd0f6794d 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -397,6 +397,15 @@ prompt_caching: # # extra_body: # # enable_thinking: false +# Background memory/skill review uses the active conversation runtime by default. +# Set this to route those non-user-visible review turns to a cheaper or isolated +# endpoint. +# +# background_review: +# provider: "" # empty/unset = inherit active conversation provider +# model: "" # empty/unset = inherit active conversation model +# max_iterations: 8 + # ============================================================================= # Persistent Memory # ============================================================================= diff --git a/run_agent.py b/run_agent.py index 1f2a0621278d..6adb1556da5d 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1593,6 +1593,26 @@ def __init__( # broad pseudo-public config object on the agent instance. self._aux_compression_context_length_config = None + self._background_review_model = None + self._background_review_provider = None + self._background_review_max_iterations = 8 + try: + _bg_review_config = _agent_cfg.get("background_review", {}) + if not isinstance(_bg_review_config, dict): + _bg_review_config = {} + _bg_review_model = _bg_review_config.get("model") + _bg_review_provider = _bg_review_config.get("provider") + if _bg_review_model: + self._background_review_model = str(_bg_review_model).strip() + if _bg_review_provider: + self._background_review_provider = str(_bg_review_provider).strip() + self._background_review_max_iterations = max( + 1, + int(_bg_review_config.get("max_iterations", 8)), + ) + except Exception: + pass + # Persistent memory (MEMORY.md + USER.md) -- loaded from disk self._memory_store = None self._memory_enabled = False @@ -3226,11 +3246,11 @@ def _run_review(): contextlib.redirect_stdout(_devnull), \ contextlib.redirect_stderr(_devnull): review_agent = AIAgent( - model=self.model, - max_iterations=8, + model=self._background_review_model or self.model, + max_iterations=self._background_review_max_iterations, quiet_mode=True, platform=self.platform, - provider=self.provider, + provider=self._background_review_provider or self.provider, parent_session_id=self.session_id, ) review_agent._memory_write_origin = "background_review" diff --git a/tests/run_agent/test_background_review_summary.py b/tests/run_agent/test_background_review_summary.py index 7401b1eb19c2..409dceb454fa 100644 --- a/tests/run_agent/test_background_review_summary.py +++ b/tests/run_agent/test_background_review_summary.py @@ -6,7 +6,10 @@ """ import json +import threading +from unittest.mock import patch +import run_agent from run_agent import AIAgent @@ -128,3 +131,65 @@ def test_removed_or_replaced_relabels_by_target(): assert "User profile updated" in actions assert "Memory updated" in actions + + +def test_spawn_background_review_uses_configured_runtime(monkeypatch): + """Background review agents can be routed away from the active model.""" + with ( + patch("run_agent.get_tool_definitions", return_value=[]), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + patch( + "hermes_cli.config.load_config", + return_value={ + "background_review": { + "model": "qwen3.6-35b-q8", + "provider": "artemis-direct", + "max_iterations": 4, + } + }, + ), + ): + agent = AIAgent( + model="qwen3.6-27b", + provider="voyager", + api_key="test-key", + base_url="http://localhost:8080/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + captured = {} + + class FakeReviewAgent: + def __init__(self, **kwargs): + captured.update(kwargs) + self._session_messages = [] + + def run_conversation(self, **kwargs): + captured["conversation"] = kwargs + + def close(self): + captured["closed"] = True + + class ImmediateThread: + def __init__(self, target, daemon, name): + self._target = target + captured["thread"] = {"daemon": daemon, "name": name} + + def start(self): + self._target() + + monkeypatch.setattr(run_agent, "AIAgent", FakeReviewAgent) + monkeypatch.setattr(threading, "Thread", ImmediateThread) + + snapshot = [{"role": "user", "content": "hello"}] + agent._spawn_background_review(snapshot, review_skills=True) + + assert captured["model"] == "qwen3.6-35b-q8" + assert captured["provider"] == "artemis-direct" + assert captured["max_iterations"] == 4 + assert captured["parent_session_id"] == agent.session_id + assert captured["conversation"]["conversation_history"] == snapshot + assert captured["closed"] is True