diff --git a/run_agent.py b/run_agent.py index 973f0d95d72ea..f8cf4682cc452 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1792,7 +1792,7 @@ def __init__( tool_names = sorted(self.valid_tool_names) if not self.quiet_mode: print(f"🛠️ Loaded {len(self.tools)} tools: {', '.join(tool_names)}") - + # Show filtering info if applied if enabled_toolsets: print(f" ✅ Enabled toolsets: {', '.join(enabled_toolsets)}") @@ -1800,6 +1800,16 @@ def __init__( print(f" ❌ Disabled toolsets: {', '.join(disabled_toolsets)}") elif not self.quiet_mode: print("🛠️ No tools loaded (all tools filtered out or unavailable)") + + # Kanban worker/orchestrator lifecycle guidance is session-static: + # the dispatcher decides at spawn time whether this process is a + # kanban worker (kanban_show tool is present iff HERMES_KANBAN_TASK + # is set — see tools/kanban_tools.py:54). Resolving the ~835-token + # block once here avoids re-running the membership test + reference + # on every system-prompt rebuild (init + each context compression). + self._kanban_worker_guidance = ( + KANBAN_GUIDANCE if "kanban_show" in self.valid_tool_names else "" + ) # Check tool requirements if self.tools and not self.quiet_mode: @@ -5783,9 +5793,9 @@ def _build_system_prompt_parts(self, system_message: str = None) -> Dict[str, st # Kanban worker/orchestrator lifecycle — only present when the # dispatcher spawned this process (kanban_show check_fn gates on # HERMES_KANBAN_TASK env var). Normal chat sessions never see - # this block. - if "kanban_show" in self.valid_tool_names: - tool_guidance.append(KANBAN_GUIDANCE) + # this block. Resolved once at __init__ (see _kanban_worker_guidance). + if self._kanban_worker_guidance: + tool_guidance.append(self._kanban_worker_guidance) if tool_guidance: stable_parts.append(" ".join(tool_guidance)) diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index c31ae6f08bb92..d390e9a9a01b4 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -917,6 +917,70 @@ def test_kanban_guidance_prompt_size_bounded(monkeypatch, tmp_path): ) +def test_kanban_guidance_resolved_once_at_init(monkeypatch, tmp_path): + """Regression: the kanban-worker classification must be resolved once + at __init__ and cached on ``self._kanban_worker_guidance``, not re-checked + against ``valid_tool_names`` on every system-prompt rebuild. + + The system prompt is rebuilt on every context-compression event; the + KANBAN_GUIDANCE block is session-static (the dispatcher decides worker + vs. non-worker once at spawn time), so the membership test belongs in + __init__ — not in the hot path. This test asserts the cache exists + and is the source of truth. + """ + monkeypatch.setenv("HERMES_KANBAN_TASK", "t_fake") + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + from pathlib import Path as _P + monkeypatch.setattr(_P, "home", lambda: tmp_path) + + from run_agent import AIAgent + from agent.prompt_builder import KANBAN_GUIDANCE + + a = AIAgent( + api_key="test", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + # 1. Cache attribute exists and equals KANBAN_GUIDANCE for workers. + assert hasattr(a, "_kanban_worker_guidance") + assert a._kanban_worker_guidance == KANBAN_GUIDANCE + + # 2. The system-prompt build consumes the cache, not the live tool set. + # If we remove ``kanban_show`` from valid_tool_names AFTER init, the + # cached guidance still drives the prompt — proving the membership + # test isn't re-run per build. + a.valid_tool_names = a.valid_tool_names - {"kanban_show"} + prompt = a._build_system_prompt() + assert "Kanban task execution protocol" in prompt + + +def test_kanban_guidance_cache_empty_for_normal_chat(monkeypatch, tmp_path): + """Normal chat sessions (no HERMES_KANBAN_TASK) must have an empty + ``_kanban_worker_guidance`` cache so we never accidentally inject the + worker protocol into a non-worker prompt.""" + monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + from pathlib import Path as _P + monkeypatch.setattr(_P, "home", lambda: tmp_path) + + from run_agent import AIAgent + a = AIAgent( + api_key="test", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + assert a._kanban_worker_guidance == "" + + # --------------------------------------------------------------------------- # Worker task-ownership enforcement (regression tests for #19534) # ---------------------------------------------------------------------------