From 62cfa13afc1f87a24ab902ce25f73df819bf0a46 Mon Sep 17 00:00:00 2001 From: Jeff Bridges Date: Sat, 18 Apr 2026 13:58:24 +0700 Subject: [PATCH] fix: reduce gateway context burn from skills catalog injection - add skills.system_prompt_mode config (auto/full/minimal/off) - default auto to minimal on gateway platforms, full on CLI - add compact minimal skills prompt builder - add regression tests for gateway auto/minimal + full override - bump config schema to 18 and update version-pinned tests --- agent/prompt_builder.py | 14 +++++++ hermes_cli/config.py | 8 +++- run_agent.py | 44 ++++++++++++++------ tests/agent/test_prompt_builder.py | 8 ++++ tests/hermes_cli/test_config.py | 4 +- tests/run_agent/test_run_agent.py | 50 +++++++++++++++++++++++ tests/tools/test_browser_camofox_state.py | 2 +- 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 558a57888047..d6559240f53a 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -800,6 +800,20 @@ def build_skills_system_prompt( return result +def build_minimal_skills_system_prompt() -> str: + """Build a tiny skills guidance block without enumerating the full catalog. + + Used for token-sensitive environments (e.g. messaging gateways) where + shipping the full index every turn is expensive. + """ + return ( + "## Skills\n" + "Use skills when relevant, but do NOT assume the catalog from memory. " + "If a task might match a reusable workflow, call skills_list first, then " + "load the best match with skill_view(name) before executing." + ) + + def build_nous_subscription_prompt(valid_tool_names: "set[str] | None" = None) -> str: """Build a compact Nous subscription capability block for the system prompt.""" try: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 64a5bd1a9bdb..f55a10a8fb42 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -617,6 +617,12 @@ def _ensure_hermes_home_managed(home: Path): # always goes to ~/.hermes/skills/. "skills": { "external_dirs": [], # e.g. ["~/.agents/skills", "/shared/team-skills"] + # Skills block size in system prompt: + # - auto: full index on CLI, minimal block on gateway platforms + # - full: full index every turn + # - minimal: tiny guidance block (use skills_list/skill_view as needed) + # - off: do not inject skills guidance + "system_prompt_mode": "auto", }, # Honcho AI-native memory -- reads ~/.honcho/config.json as single source of truth. @@ -700,7 +706,7 @@ def _ensure_hermes_home_managed(home: Path): }, # Config schema version - bump this when adding new required fields - "_config_version": 17, + "_config_version": 18, } # ============================================================================= diff --git a/run_agent.py b/run_agent.py index 5005153b3b3c..0fa27371cbf6 100644 --- a/run_agent.py +++ b/run_agent.py @@ -94,7 +94,7 @@ from agent.context_compressor import ContextCompressor from agent.subdirectory_hints import SubdirectoryHintTracker from agent.prompt_caching import apply_anthropic_cache_control -from agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt, build_environment_hints, load_soul_md, TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, DEVELOPER_ROLE_MODELS, GOOGLE_MODEL_OPERATIONAL_GUIDANCE, OPENAI_MODEL_EXECUTION_GUIDANCE +from agent.prompt_builder import build_skills_system_prompt, build_minimal_skills_system_prompt, build_context_files_prompt, build_environment_hints, load_soul_md, TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, DEVELOPER_ROLE_MODELS, GOOGLE_MODEL_OPERATIONAL_GUIDANCE, OPENAI_MODEL_EXECUTION_GUIDANCE from agent.usage_pricing import estimate_usage_cost, normalize_usage from agent.display import ( KawaiiSpinner, build_tool_preview as _build_tool_preview, @@ -1232,11 +1232,17 @@ def __init__( if _tname: self.valid_tool_names.add(_tname) - # Skills config: nudge interval for skill creation reminders + # Skills config: nudge interval + system-prompt index mode self._skill_nudge_interval = 10 + # Modes: full|minimal|off|auto + # auto => full on CLI/unknown, minimal on messaging gateways. + self._skills_system_prompt_mode = "auto" try: skills_config = _agent_cfg.get("skills", {}) self._skill_nudge_interval = int(skills_config.get("creation_nudge_interval", 10)) + _mode = str(skills_config.get("system_prompt_mode", "auto") or "auto").strip().lower() + if _mode in {"full", "minimal", "off", "auto"}: + self._skills_system_prompt_mode = _mode except Exception: pass @@ -3221,21 +3227,34 @@ def _build_system_prompt(self, system_message: str = None) -> str: except Exception: pass + platform_key = (self.platform or "").lower().strip() + has_skills_tools = any(name in self.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage']) if has_skills_tools: - avail_toolsets = { - toolset - for toolset in ( - get_toolset_for_tool(tool_name) for tool_name in self.valid_tool_names + skills_mode = self._skills_system_prompt_mode + if skills_mode == "auto": + # Lean default on gateways; full catalog on CLI/unspecified platform. + skills_mode = "full" if platform_key in {"", "cli"} else "minimal" + + if skills_mode == "full": + avail_toolsets = { + toolset + for toolset in ( + get_toolset_for_tool(tool_name) for tool_name in self.valid_tool_names + ) + if toolset + } + skills_prompt = build_skills_system_prompt( + available_tools=self.valid_tool_names, + available_toolsets=avail_toolsets, ) - if toolset - } - skills_prompt = build_skills_system_prompt( - available_tools=self.valid_tool_names, - available_toolsets=avail_toolsets, - ) + elif skills_mode == "minimal": + skills_prompt = build_minimal_skills_system_prompt() + else: # off + skills_prompt = "" else: skills_prompt = "" + if skills_prompt: prompt_parts.append(skills_prompt) @@ -3279,7 +3298,6 @@ def _build_system_prompt(self, system_message: str = None) -> str: if _env_hints: prompt_parts.append(_env_hints) - platform_key = (self.platform or "").lower().strip() if platform_key in PLATFORM_HINTS: prompt_parts.append(PLATFORM_HINTS[platform_key]) diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 5a222cc38bb0..936aaa919b58 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -16,6 +16,7 @@ _find_git_root, _strip_yaml_frontmatter, build_skills_system_prompt, + build_minimal_skills_system_prompt, build_nous_subscription_prompt, build_context_files_prompt, build_environment_hints, @@ -240,6 +241,13 @@ def guarded_import(name, globals=None, locals=None, fromlist=(), level=0): # ========================================================================= +def test_build_minimal_skills_system_prompt_is_compact(): + result = build_minimal_skills_system_prompt() + assert "skills_list" in result + assert "skill_view" in result + assert "available_skills" not in result + + class TestBuildSkillsSystemPrompt: @pytest.fixture(autouse=True) def _clear_skills_cache(self): diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 9f77bb4c863c..9851b96bb5f9 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -459,7 +459,7 @@ def test_v11_upgrade_moves_custom_providers_into_providers(self, tmp_path): migrate_config(interactive=False, quiet=True) raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) - assert raw["_config_version"] == 17 + assert raw["_config_version"] == 18 assert raw["providers"]["openai-direct"] == { "api": "https://api.openai.com/v1", "api_key": "test-key", @@ -606,6 +606,6 @@ def test_migrate_to_v15_adds_interim_assistant_message_gate(self, tmp_path): migrate_config(interactive=False, quiet=True) raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) - assert raw["_config_version"] == 17 + assert raw["_config_version"] == 18 assert raw["display"]["tool_progress"] == "off" assert raw["display"]["interim_assistant_messages"] is True diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 568077fd7b55..33b916ee169b 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -705,6 +705,56 @@ def test_skills_prompt_derives_available_toolsets_from_loaded_tools(self): assert mock_skills.call_args.kwargs["available_tools"] == set(toolset_map) assert mock_skills.call_args.kwargs["available_toolsets"] == {"web", "skills"} + def test_skills_prompt_auto_uses_minimal_on_gateway_platforms(self): + tools = _make_tool_defs("skills_list", "skill_view", "skill_manage") + + with ( + patch("run_agent.get_tool_definitions", return_value=tools), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.build_skills_system_prompt", return_value="FULL_SKILLS") as mock_full, + patch("run_agent.build_minimal_skills_system_prompt", return_value="MIN_SKILLS") as mock_min, + patch("run_agent.OpenAI"), + patch("hermes_cli.config.load_config", return_value={"skills": {"system_prompt_mode": "auto"}}), + ): + agent = AIAgent( + api_key="test-k...7890", + quiet_mode=True, + platform="slack", + skip_context_files=True, + skip_memory=True, + ) + prompt = agent._build_system_prompt() + + assert "MIN_SKILLS" in prompt + assert "FULL_SKILLS" not in prompt + mock_min.assert_called_once() + mock_full.assert_not_called() + + def test_skills_prompt_full_mode_forces_full_catalog_on_gateway(self): + tools = _make_tool_defs("skills_list", "skill_view", "skill_manage") + + with ( + patch("run_agent.get_tool_definitions", return_value=tools), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.build_skills_system_prompt", return_value="FULL_SKILLS") as mock_full, + patch("run_agent.build_minimal_skills_system_prompt", return_value="MIN_SKILLS") as mock_min, + patch("run_agent.OpenAI"), + patch("hermes_cli.config.load_config", return_value={"skills": {"system_prompt_mode": "full"}}), + ): + agent = AIAgent( + api_key="test-k...7890", + quiet_mode=True, + platform="slack", + skip_context_files=True, + skip_memory=True, + ) + prompt = agent._build_system_prompt() + + assert "FULL_SKILLS" in prompt + assert "MIN_SKILLS" not in prompt + mock_full.assert_called_once() + mock_min.assert_not_called() + class TestToolUseEnforcementConfig: """Tests for the agent.tool_use_enforcement config option.""" diff --git a/tests/tools/test_browser_camofox_state.py b/tests/tools/test_browser_camofox_state.py index 475e8c2d02cc..05f679efeecc 100644 --- a/tests/tools/test_browser_camofox_state.py +++ b/tests/tools/test_browser_camofox_state.py @@ -64,4 +64,4 @@ def test_config_version_matches_current_schema(self): # The current schema version is tracked globally; unrelated default # options may bump it after browser defaults are added. - assert DEFAULT_CONFIG["_config_version"] == 17 + assert DEFAULT_CONFIG["_config_version"] == 18