Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1792,14 +1792,24 @@ 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)}")
if disabled_toolsets:
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:
Expand Down Expand Up @@ -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))

Expand Down
64 changes: 64 additions & 0 deletions tests/tools/test_kanban_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# ---------------------------------------------------------------------------
Expand Down