fix(hermes): expose Agency tool parameters (#717)

Co-authored-by: Michael Sitarzewski <michael@sitarzewski.com>
This commit is contained in:
Pip, Agent Pip
2026-07-15 09:55:50 -05:00
committed by GitHub
parent 00fb28a4cf
commit 6e45066041
4 changed files with 170 additions and 31 deletions
+3
View File
@@ -18,3 +18,6 @@ jobs:
run: |
chmod +x scripts/check-tools.sh
./scripts/check-tools.sh
- name: Validate generated Hermes plugin
run: python3 scripts/check-hermes-plugin.py
+21 -2
View File
@@ -3,11 +3,11 @@
Generated by `scripts/convert.sh --tool hermes`.
This integration installs one Hermes plugin named `agency-agents-router` instead
of adding 232+ generated skills to `skills.external_dirs`. Hermes sees a
of adding hundreds of generated skills to `skills.external_dirs`. Hermes sees a
small fixed tool surface at startup, while the complete Agency roster is
stored on disk in `data/agents.json` and searched/loaded lazily.
Generated agent count: 232
Generated agent count: 254
## Tools exposed to Hermes
@@ -16,6 +16,20 @@ Generated agent count: 232
- `agency_agents_load` — compose one specialist prompt for the current task.
- `agency_agents_delegate` — delegate through Hermes `delegate_task` when available.
Each tool is registered with Hermes' complete function-tool schema, including
its name, description, and JSON `parameters`. The available arguments are:
| Tool | Arguments |
| --- | --- |
| `agency_agents_search` | `query` (required), optional `division` and `limit` |
| `agency_agents_inspect` | `agent` or `slug`, optional `include_body` |
| `agency_agents_load` | `agent` or `slug`, optional `task` |
| `agency_agents_delegate` | `agent` or `slug`, `task` (required), optional `toolsets` |
A normal flow is: search by capability, take a returned `slug`, then inspect,
load, or delegate to that specialist. You can ask Hermes to do this in natural
language; direct tool calls are not required.
## Specialist usage instruction for Hermes
When a Hermes project needs Agency specialists, explicitly ask Hermes to use
@@ -58,3 +72,8 @@ ${HERMES_HOME:-~/.hermes}/plugins/agency-agents-router
It then enables `agency-agents-router` under `plugins.enabled` in the Hermes
config. It does **not** write to `skills.external_dirs`.
Restart Hermes or start a new session after installing so the plugin and its
tool schemas are loaded. If Hermes displays these tools without their documented
arguments, regenerate and reinstall the plugin from the latest Agency Agents
checkout, then restart Hermes.
+64 -29
View File
@@ -225,13 +225,17 @@ SEARCH_DESCRIPTION = (
"Swami specialist, role, discipline, or wants help choosing the right agent."
)
SEARCH_SCHEMA = {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural-language search query."},
"division": {"type": "string", "description": "Optional division filter, e.g. engineering, marketing, testing."},
"limit": {"type": "integer", "description": "Maximum results, default 8, max 25."},
"name": "agency_agents_search",
"description": SEARCH_DESCRIPTION,
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural-language search query."},
"division": {"type": "string", "description": "Optional division filter, e.g. engineering, marketing, testing."},
"limit": {"type": "integer", "description": "Maximum results, default 8, max 25."},
},
"required": ["query"],
},
"required": ["query"],
}
READ_DESCRIPTION = (
@@ -239,13 +243,17 @@ READ_DESCRIPTION = (
"and includes the full specialist instructions only when include_body is true."
)
READ_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"include_body": {"type": "boolean", "description": "Include full specialist instructions."},
"name": "agency_agents_inspect",
"description": READ_DESCRIPTION,
"parameters": {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"include_body": {"type": "boolean", "description": "Include full specialist instructions."},
},
"required": [],
},
"required": [],
}
PROMPT_DESCRIPTION = (
@@ -253,13 +261,17 @@ PROMPT_DESCRIPTION = (
"Use after agency_agents_search when you need one specialist's full context."
)
PROMPT_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "The user's task to pair with the specialist context."},
"name": "agency_agents_load",
"description": PROMPT_DESCRIPTION,
"parameters": {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "The user's task to pair with the specialist context."},
},
"required": [],
},
"required": [],
}
DELEGATE_DESCRIPTION = (
@@ -268,18 +280,22 @@ DELEGATE_DESCRIPTION = (
"specialist prompt if delegation is unavailable."
)
DELEGATE_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "Concrete task for the specialist."},
"toolsets": {
"type": "array",
"items": {"type": "string"},
"description": "Optional Hermes toolsets for the delegated worker, e.g. ['terminal','file'].",
"name": "agency_agents_delegate",
"description": DELEGATE_DESCRIPTION,
"parameters": {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "Concrete task for the specialist."},
"toolsets": {
"type": "array",
"items": {"type": "string"},
"description": "Optional Hermes toolsets for the delegated worker, e.g. ['terminal','file'].",
},
},
"required": ["task"],
},
"required": ["task"],
}
@@ -402,7 +418,7 @@ def readme(agent_count: int) -> str:
Generated by `scripts/convert.sh --tool hermes`.
This integration installs one Hermes plugin named `{PLUGIN_NAME}` instead
of adding 232+ generated skills to `skills.external_dirs`. Hermes sees a
of adding hundreds of generated skills to `skills.external_dirs`. Hermes sees a
small fixed tool surface at startup, while the complete Agency roster is
stored on disk in `data/agents.json` and searched/loaded lazily.
@@ -415,6 +431,20 @@ def readme(agent_count: int) -> str:
- `agency_agents_load` — compose one specialist prompt for the current task.
- `agency_agents_delegate` — delegate through Hermes `delegate_task` when available.
Each tool is registered with Hermes' complete function-tool schema, including
its name, description, and JSON `parameters`. The available arguments are:
| Tool | Arguments |
| --- | --- |
| `agency_agents_search` | `query` (required), optional `division` and `limit` |
| `agency_agents_inspect` | `agent` or `slug`, optional `include_body` |
| `agency_agents_load` | `agent` or `slug`, optional `task` |
| `agency_agents_delegate` | `agent` or `slug`, `task` (required), optional `toolsets` |
A normal flow is: search by capability, take a returned `slug`, then inspect,
load, or delegate to that specialist. You can ask Hermes to do this in natural
language; direct tool calls are not required.
## Specialist usage instruction for Hermes
When a Hermes project needs Agency specialists, explicitly ask Hermes to use
@@ -457,6 +487,11 @@ def readme(agent_count: int) -> str:
It then enables `{PLUGIN_NAME}` under `plugins.enabled` in the Hermes
config. It does **not** write to `skills.external_dirs`.
Restart Hermes or start a new session after installing so the plugin and its
tool schemas are loaded. If Hermes displays these tools without their documented
arguments, regenerate and reinstall the plugin from the latest Agency Agents
checkout, then restart Hermes.
"""
).lstrip()
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Validate the generated Hermes router plugin against Hermes' tool contract."""
from __future__ import annotations
import importlib.util
import json
import tempfile
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
BUILDER_PATH = REPO_ROOT / "scripts" / "build-hermes-plugin.py"
def load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise RuntimeError(f"could not load {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class RecordingContext:
def __init__(self) -> None:
self.tools: dict[str, dict[str, Any]] = {}
def register_tool(self, **kwargs: Any) -> None:
self.tools[kwargs["name"]] = kwargs
def main() -> int:
builder = load_module("agency_agents_hermes_builder", BUILDER_PATH)
with tempfile.TemporaryDirectory() as tmp:
out_dir = Path(tmp) / "hermes"
builder.build(REPO_ROOT, out_dir)
plugin = load_module(
"agency_agents_router_check",
out_dir / builder.PLUGIN_NAME / "__init__.py",
)
ctx = RecordingContext()
plugin.register(ctx)
expected_tools = {
"agency_agents_search",
"agency_agents_inspect",
"agency_agents_load",
"agency_agents_delegate",
}
assert set(ctx.tools) == expected_tools
for name, registration in ctx.tools.items():
schema = registration["schema"]
assert schema["name"] == name, f"{name}: schema name is missing"
assert schema.get("description"), f"{name}: schema description is missing"
parameters = schema.get("parameters")
assert isinstance(parameters, dict), f"{name}: schema.parameters is missing"
assert parameters.get("type") == "object", f"{name}: parameters must be an object"
assert isinstance(parameters.get("properties"), dict), f"{name}: properties are missing"
assert isinstance(parameters.get("required"), list), f"{name}: required must be a list"
search = json.loads(
ctx.tools["agency_agents_search"]["handler"]({"query": "backend architecture"})
)
assert search["success"] is True
assert search["results"], "search should return at least one specialist"
slug = search["results"][0]["slug"]
inspected = json.loads(
ctx.tools["agency_agents_inspect"]["handler"]({"slug": slug})
)
assert inspected["success"] is True
assert inspected["agent"]["slug"] == slug
print("PASSED: generated Hermes plugin schemas and routing behavior are valid.")
return 0
if __name__ == "__main__":
raise SystemExit(main())