mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: introduce skills management features in AIAgent and CLI
- Added skills configuration options in cli-config.yaml.example, including a nudge interval for skill creation reminders. - Implemented skills guidance in AIAgent to prompt users to save reusable workflows after complex tasks. - Enhanced skills indexing in the prompt builder to include descriptions from SKILL.md files for better context. - Updated the agent's behavior to periodically remind users about potential skills during tool-calling iterations.
This commit is contained in:
parent
3c6750f37b
commit
db23f51bc6
4 changed files with 112 additions and 10 deletions
|
|
@ -37,6 +37,12 @@ SESSION_SEARCH_GUIDANCE = (
|
|||
"them to repeat themselves."
|
||||
)
|
||||
|
||||
SKILLS_GUIDANCE = (
|
||||
"After completing a complex task (5+ tool calls), fixing a tricky error, "
|
||||
"or discovering a non-trivial workflow, consider saving the approach as a "
|
||||
"skill with skill_manage so you can reuse it next time."
|
||||
)
|
||||
|
||||
PLATFORM_HINTS = {
|
||||
"whatsapp": (
|
||||
"You are on a text messaging communication platform, WhatsApp. "
|
||||
|
|
@ -64,11 +70,30 @@ CONTEXT_TRUNCATE_TAIL_RATIO = 0.2
|
|||
# Skills index
|
||||
# =========================================================================
|
||||
|
||||
def _read_skill_description(skill_file: Path, max_chars: int = 60) -> str:
|
||||
"""Read the description from a SKILL.md frontmatter, capped at max_chars."""
|
||||
try:
|
||||
raw = skill_file.read_text(encoding="utf-8")[:2000]
|
||||
match = re.search(
|
||||
r"^---\s*\n.*?description:\s*(.+?)\s*\n.*?^---",
|
||||
raw, re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
if match:
|
||||
desc = match.group(1).strip().strip("'\"")
|
||||
if len(desc) > max_chars:
|
||||
desc = desc[:max_chars - 3] + "..."
|
||||
return desc
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def build_skills_system_prompt() -> str:
|
||||
"""Build a compact skill index for the system prompt.
|
||||
|
||||
Scans ~/.hermes/skills/ for SKILL.md files grouped by category so the
|
||||
model can match skills at a glance without extra tool calls.
|
||||
Scans ~/.hermes/skills/ for SKILL.md files grouped by category.
|
||||
Includes per-skill descriptions from frontmatter so the model can
|
||||
match skills by meaning, not just name.
|
||||
"""
|
||||
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
||||
skills_dir = hermes_home / "skills"
|
||||
|
|
@ -76,7 +101,9 @@ def build_skills_system_prompt() -> str:
|
|||
if not skills_dir.exists():
|
||||
return ""
|
||||
|
||||
skills_by_category = {}
|
||||
# Collect skills with descriptions, grouped by category
|
||||
# Each entry: (skill_name, description)
|
||||
skills_by_category: dict[str, list[tuple[str, str]]] = {}
|
||||
for skill_file in skills_dir.rglob("SKILL.md"):
|
||||
rel_path = skill_file.relative_to(skills_dir)
|
||||
parts = rel_path.parts
|
||||
|
|
@ -86,11 +113,13 @@ def build_skills_system_prompt() -> str:
|
|||
else:
|
||||
category = "general"
|
||||
skill_name = skill_file.parent.name
|
||||
skills_by_category.setdefault(category, []).append(skill_name)
|
||||
desc = _read_skill_description(skill_file)
|
||||
skills_by_category.setdefault(category, []).append((skill_name, desc))
|
||||
|
||||
if not skills_by_category:
|
||||
return ""
|
||||
|
||||
# Read category-level descriptions from DESCRIPTION.md
|
||||
category_descriptions = {}
|
||||
for category in skills_by_category:
|
||||
desc_file = skills_dir / category / "DESCRIPTION.md"
|
||||
|
|
@ -105,13 +134,21 @@ def build_skills_system_prompt() -> str:
|
|||
|
||||
index_lines = []
|
||||
for category in sorted(skills_by_category.keys()):
|
||||
desc = category_descriptions.get(category, "")
|
||||
names = ", ".join(sorted(set(skills_by_category[category])))
|
||||
if desc:
|
||||
index_lines.append(f" {category}: {desc}")
|
||||
cat_desc = category_descriptions.get(category, "")
|
||||
if cat_desc:
|
||||
index_lines.append(f" {category}: {cat_desc}")
|
||||
else:
|
||||
index_lines.append(f" {category}:")
|
||||
index_lines.append(f" skills: {names}")
|
||||
# Deduplicate and sort skills within each category
|
||||
seen = set()
|
||||
for name, desc in sorted(skills_by_category[category], key=lambda x: x[0]):
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
if desc:
|
||||
index_lines.append(f" - {name}: {desc}")
|
||||
else:
|
||||
index_lines.append(f" - {name}")
|
||||
|
||||
return (
|
||||
"## Skills (mandatory)\n"
|
||||
|
|
|
|||
|
|
@ -178,6 +178,18 @@ memory:
|
|||
# For exit/reset, only fires if the session had at least this many user turns.
|
||||
flush_min_turns: 6 # Min user turns to trigger flush on exit/reset (0 = disabled)
|
||||
|
||||
# =============================================================================
|
||||
# Skills Configuration
|
||||
# =============================================================================
|
||||
# Skills are reusable procedures the agent can load and follow. The agent can
|
||||
# also create new skills after completing complex tasks.
|
||||
#
|
||||
skills:
|
||||
# Nudge the agent to create skills after complex tasks.
|
||||
# Every N tool-calling iterations, remind the model to consider saving a skill.
|
||||
# Set to 0 to disable.
|
||||
creation_nudge_interval: 15
|
||||
|
||||
# =============================================================================
|
||||
# Agent Behavior
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -590,6 +590,35 @@ class GatewayRunner:
|
|||
session_key = f"agent:main:{source.platform.value}:" + \
|
||||
(f"dm" if source.chat_type == "dm" else f"{source.chat_type}:{source.chat_id}")
|
||||
|
||||
# Memory flush before reset: load the old transcript and let a
|
||||
# temporary agent save memories before the session is wiped.
|
||||
try:
|
||||
old_entry = self.session_store._sessions.get(session_key)
|
||||
if old_entry:
|
||||
old_history = self.session_store.load_transcript(old_entry.session_id)
|
||||
if old_history:
|
||||
from run_agent import AIAgent
|
||||
loop = asyncio.get_event_loop()
|
||||
def _do_flush():
|
||||
tmp_agent = AIAgent(
|
||||
model=os.getenv("HERMES_MODEL", "anthropic/claude-opus-4.6"),
|
||||
max_iterations=5,
|
||||
quiet_mode=True,
|
||||
enabled_toolsets=["memory"],
|
||||
session_id=old_entry.session_id,
|
||||
)
|
||||
# Build simple message list from transcript
|
||||
msgs = []
|
||||
for m in old_history:
|
||||
role = m.get("role")
|
||||
content = m.get("content")
|
||||
if role in ("user", "assistant") and content:
|
||||
msgs.append({"role": role, "content": content})
|
||||
tmp_agent.flush_memories(msgs)
|
||||
await loop.run_in_executor(None, _do_flush)
|
||||
except Exception as e:
|
||||
logger.debug("Gateway memory flush on reset failed: %s", e)
|
||||
|
||||
# Reset the session
|
||||
new_entry = self.session_store.reset_session(session_key)
|
||||
|
||||
|
|
|
|||
26
run_agent.py
26
run_agent.py
|
|
@ -60,7 +60,7 @@ from hermes_constants import OPENROUTER_BASE_URL, OPENROUTER_MODELS_URL
|
|||
# Agent internals extracted to agent/ package for modularity
|
||||
from agent.prompt_builder import (
|
||||
DEFAULT_AGENT_IDENTITY, PLATFORM_HINTS,
|
||||
MEMORY_GUIDANCE, SESSION_SEARCH_GUIDANCE,
|
||||
MEMORY_GUIDANCE, SESSION_SEARCH_GUIDANCE, SKILLS_GUIDANCE,
|
||||
)
|
||||
from agent.model_metadata import (
|
||||
fetch_model_metadata, get_model_context_length,
|
||||
|
|
@ -393,6 +393,15 @@ class AIAgent:
|
|||
except Exception:
|
||||
pass # Memory is optional -- don't break agent init
|
||||
|
||||
# Skills config: nudge interval for skill creation reminders
|
||||
self._skill_nudge_interval = 15
|
||||
try:
|
||||
from hermes_cli.config import load_config as _load_skills_config
|
||||
skills_config = _load_skills_config().get("skills", {})
|
||||
self._skill_nudge_interval = int(skills_config.get("creation_nudge_interval", 15))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Initialize context compressor for automatic context management
|
||||
# Compresses conversation when approaching model's context limit
|
||||
# Configuration via environment variables (can be set in .env or cli-config.yaml)
|
||||
|
|
@ -1040,6 +1049,8 @@ class AIAgent:
|
|||
tool_guidance.append(MEMORY_GUIDANCE)
|
||||
if "session_search" in self.valid_tool_names:
|
||||
tool_guidance.append(SESSION_SEARCH_GUIDANCE)
|
||||
if "skill_manage" in self.valid_tool_names:
|
||||
tool_guidance.append(SKILLS_GUIDANCE)
|
||||
if tool_guidance:
|
||||
prompt_parts.append(" ".join(tool_guidance))
|
||||
|
||||
|
|
@ -1658,6 +1669,19 @@ class AIAgent:
|
|||
break
|
||||
|
||||
api_call_count += 1
|
||||
|
||||
# Periodic skill creation nudge after many tool-calling iterations
|
||||
if (self._skill_nudge_interval > 0
|
||||
and api_call_count > 0
|
||||
and api_call_count % self._skill_nudge_interval == 0
|
||||
and "skill_manage" in self.valid_tool_names):
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
"[System: This task has involved many steps. "
|
||||
"If you've discovered a reusable workflow, consider saving it as a skill.]"
|
||||
),
|
||||
})
|
||||
|
||||
# Prepare messages for API call
|
||||
# If we have an ephemeral system prompt, prepend it to the messages
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue