From 6e45066041e97b9a9a48afc99562ea59e1429567 Mon Sep 17 00:00:00 2001 From: "Pip, Agent Pip" Date: Wed, 15 Jul 2026 09:55:50 -0500 Subject: [PATCH] fix(hermes): expose Agency tool parameters (#717) Co-authored-by: Michael Sitarzewski --- .github/workflows/check-tools.yml | 3 + integrations/hermes/README.md | 23 +++++++- scripts/build-hermes-plugin.py | 93 +++++++++++++++++++++---------- scripts/check-hermes-plugin.py | 82 +++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 scripts/check-hermes-plugin.py diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index 448af63c..f85b8ca9 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -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 diff --git a/integrations/hermes/README.md b/integrations/hermes/README.md index 7f6f77bc..1310cd9f 100644 --- a/integrations/hermes/README.md +++ b/integrations/hermes/README.md @@ -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. diff --git a/scripts/build-hermes-plugin.py b/scripts/build-hermes-plugin.py index 14e4bc4e..edd62a26 100644 --- a/scripts/build-hermes-plugin.py +++ b/scripts/build-hermes-plugin.py @@ -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() diff --git a/scripts/check-hermes-plugin.py b/scripts/check-hermes-plugin.py new file mode 100644 index 00000000..4ef3ce73 --- /dev/null +++ b/scripts/check-hermes-plugin.py @@ -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())