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
14 changes: 14 additions & 0 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <available_skills> 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:
Expand Down
8 changes: 7 additions & 1 deletion hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <available_skills> 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.
Expand Down Expand Up @@ -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,
}

# =============================================================================
Expand Down
44 changes: 31 additions & 13 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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])

Expand Down
8 changes: 8 additions & 0 deletions tests/agent/test_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions tests/hermes_cli/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
50 changes: 50 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/test_browser_camofox_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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