Young's Mind
01 Research

Hermes As A Subsystem

Consolidated. This note's findings now live in ../MIN-STUDIES.md §3, 8, 9. The original is kept for its measurements; do not re-derive from it.

Study — Hermes, and what Moyai is built on

Young, 2026-10-10: "What study Hermes as well to learn from it. Moyai built on Hermes?"

Direct answer to the question asked

Yes. Moyai is literally built on Hermes. Not "inspired by" — built on.

EvidenceValue
pyproject.toml package namehermes-workspace
Its own description"An internal agent workspace with Hermes, Modal sandboxes, and native app connections."
Its Hermes pin7968c72a3cb80beaae51948378944dd6e3423b96 — in our own git history
Its patch filesandbox/HERMES_PATCHES.md — patches it applies to a Hermes checkout
Our revision3b0dc776b6

So Moyai is a wrapper around the same framework this session runs inside. Their contribution is the workspace around Hermes — sandboxes, a web app, a broker, a gateway. The agent loop is ours already.


1. The finding that changes the plan

I studied our own Hermes source, and it already ships — natively — most of what V3's Stages 2, 3 and 4 propose to build.

1.1 Hermes ships A2A v1.0 natively

plugins/platforms/a2a/ — a complete, spec-conformant A2A v1.0 implementation:

FileSizeWhat
adapter.py49 KBthe inbound A2A v1.0 server
protocol.py21 KBAgent Card, JSON-RPC framing, task store, persistence
security.py10 KBauth, identity, injection filters, redaction, audit
tools.py19 KBoutbound client tools

And it is already enabled in our config: platforms.a2a.enabled: true.

What it implements — verified in the source:

  • Agent Card at GET /.well-known/agent-card.json (canonical v1.0 path), built dynamically from the live tool registry
  • JSON-RPC 2.0 with the full method set: SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask, SubscribeToTask, and push-config CRUD
  • SSE streaming (message/stream)
  • Push notifications, HMAC-SHA256 signed, SSRF-guarded
  • A task store — every task queryable via tasks/get/tasks/list, last 500 retained, tasks/subscribe reattaches to a running task's stream
  • Live-session injection — "inbound tasks route through the normal MessageEvent → handle_message path keyed by the A2A contextId, so the agent that answers is the same one serving the user — full memory/context, not a clone"
  • Persistence outside compaction — conversations in ~/.hermes/a2a_conversations/<context>.jsonl, "compaction and restarts can't lose them"

1.2 🔴 It serves BOTH method dialects, and the states are spec-correct

The declaration Sol found — cited to the line, and it is the value V2 got wrong:

# plugins/platforms/a2a/protocol.py:22  — measured in our checkout
PROTOCOL_VERSION = "1.0"

and the card builder at line 64 emits it as {"url": ..., "protocolBinding": "JSONRPC", "protocolVersion": PROTOCOL_VERSION}.

V2 hand-rolled an A2A server that declared PROTOCOL_VERSION = "1.0.0" while serving v0.3 method names. The correct declaration was sitting in a directory we never grepped.

This is the defect V2 died of, already solved:

# adapter.py:47 — (adapter handler, v1.0 PascalCase method, *legacy slash aliases still accepted)
_METHOD_TABLE = (
    ("_rpc_message_send", "SendMessage", "message/send"),
    ("_rpc_tasks_get",    "GetTask",     "tasks/get"),
    ...

And the state names it serves:

TASK_STATE_CANCELED        ← ONE L. Spec-correct.
TASK_STATE_INPUT_REQUIRED  ← underscore. Spec-correct.
TASK_STATE_SUBMITTED · WORKING · COMPLETED · FAILED · REJECTED

Compare V2: cancelled (two Ls), input_required, auth_required. All three wrong on the wire. Hermes gets all three right.

1.2a ✅ PROVEN LIVE — not read, run

The A2A server is running on this host right now. I probed it:

GET http://127.0.0.1:9900/.well-known/agent-card.json   → HTTP 200

The live card:

FieldLive value
namehermes-LAB
version1.0.0
urlhttp://127.0.0.1:9900/
skills35 — built dynamically from the tool registry (a2a, browser, browser-cdp, catalog, clarify, …)

And the dialect claim, proven on the wire — the same request in both spellings:

POST / {"method":"tasks/get", ...}   → {"error":{"code":-32001,"message":"task not found: no-such-task"}}
POST / {"method":"GetTask",   ...}   → {"error":{"code":-32001,"message":"task not found: no-such-task"}}

Both accepted. Identical response. -32001 is the spec's reserved TaskNotFound code — not a made-up error.

So this is not a documentation claim. A spec-conformant A2A v1.0 endpoint, accepting both dialects, with a dynamically-built 35-skill card, is live on LAB right now — while V3's plan proposes to build one.

Port note: the plugin serves 9900; a2a_fleet assigns 8301–8313. 8301 and 8311 both returned HTTP 000 (nothing listening — the V2 fleet is off, as intended). So there is no conflict today, and the two port schemes are distinct.

1.3 ⚠️ It does NOT use a2a-sdk — and our plan says to install it

README.md:6 — "no a2a-sdk dependency."DESIGN.md:146 — "a2a-sdk / gRPC + HTTP+JSON bindings. Only the JSONRPC binding is served; the card advertises exactly that."

Our PLAN.md Stage 2 step 2.1 says: "Install a2a-sdk; import a2a succeeds."

That is now a conflict that needs a ruling, and it is a real one:

OptionConsequence
Use Hermes' A2A plugin0 new dependency; spec-correct; already enabled; already security-hardened; but it is Hermes' binding, not the SDK's
Install a2a-sdk and build our ownmatches the plan; the official SDK; but duplicates a working implementation and adds a 26-package dependency

I recommend the first. V3's spine is A2A — and we already own a working, spec-conformant, security-hardened A2A v1.0 implementation. Building a second one is the exact mistake the fork discussion was about.

But this is Sol's lane (A2A / SDK / executor) and it is his call to make.

1.4 Hermes kanban is Young's task contract, already built

Young asked for: store information of task · assign to agent · completion status · brain Unit can follow up if agent got stuck or blocked.

hermes_cli/kanban_*.py + tools/kanban_tools.py — 30+ modules, a durable SQLite task board:

Card statetodo · blocked · review · done · archived
Lifecycle verbscreate · complete · block · unblock · request_review · request_changes · heartbeat · comment · link · attach · schedule
Livenesskanban_heartbeat + tasks.last_heartbeat_at
Stale workerDEFAULT_CLAIM_HEARTBEAT_MAX_STALE_SECONDS = 60 * 60 (1 hour)
Dispatchergateway/kanban_watchers_dispatcher.py — a watchdog that reads the board
Safety"a stale or unbound worker must not terminate a run a live successor owns"

That last rule is the separation-of-duties enforcement, in production.

1.5 The stuck rule — Sol said nobody had a number. Hermes has three.

Sol's §4.1: "how long before an agent is considered stuck? Without a duration, 'stuck' is a judgement call each agent makes differently."

Measured in Hermes:

MechanismNumberWhere
A2A orphaned-task watchdog300 s (5 min)adapter.py:38 _MIN_ORPHAN_TIMEOUT = 300
Watchdog sweep period60 sadapter.py:38 _WATCHDOG_INTERVAL = 60
Kanban stale worker3600 s (1 h)kanban_db.py:288
Boot lock stale600 sboot_bootstrap.py:22

And the orphan handler is exactly the honest version:

def fail_orphans(self, timeout_seconds: float = 300, ...):
    ... complete(tid, STATE_FAILED, "[task orphaned — no reply produced]")

A task that stops is a state, not an absence — and the state names the cause.

1.6 Skills: Hermes has progressive disclosure, and a curator

Moyai's load model is Hermes' own pattern. Verified in our source:

  • agent/prompt_builder.py:26 imports extract_skill_conditions, extract_skill_description, get_skill_search_roots
  • agent/skill_utils.py:392 — get_skill_search_roots() with tiers (TIER_LOCAL, project, etc.)
  • tools/skills_tool_plugin.py:15 — "Anthropic-recommended progressive-disclosure limits"

And a curator — agent/curator.py, hermes_cli/curator.py: "background maintenance for agent-created skills — usage tracking, staleness, archival, and LLM-driven review." That is skill lifecycle management, built.

1.7 Memory is two-tier and bounded

agent/memory_manager.py + agent/memory_provider.py — the Memory Provider ABC (pluggable backends), with MEMORY.md (2,200 chars) and USER.md (1,375 chars).

Sol's §4.2 — "nobody has designed the memory model" — has a working reference in the framework we run. And note the failure mode they guard: "Don't point two agent processes at the same Hermes home directory... two writers sharing one home will compound each other's entries into state neither of them (nor you) authored."That is the multi-writer defect, documented.


2. So: what does this do to the plan?

V3's plan assumed we build the spine. Hermes ships the spine.

V3 StagePlan saidReality
2 — A2A transportinstall a2a-sdk, implement AgentExecutor, prove with InspectorHermes has a working A2A v1.0 server, already enabled. Adopt it, or justify duplicating it
3 — task contractwrite states.yaml, wire the mapping, invent the stuck rulekanban has 5 states, heartbeats, a 1 h stale rule, a dispatcher watchdog. The stuck rule needs a choice, not an invention
4 — skillsbuild the load modelHermes has progressive disclosure + a curator
5 — memorydesign the modelHermes has the two-tier model + a provider ABC

This does not mean the plan is wrong. It means three stages change from "build" to "adopt, then prove we adopted it correctly."

The stages that genuinely remain ours:

  • Stage 0 — the root and the prover. Still first, still ours.
  • Stage 1 — the registry. Still ours — Hermes has no single-source registry with derived projections, and that was V2's worst live defect.
  • Stage 6 — cost and retention. Still ours — Hermes has no GO/ZEN.
  • Stage 7 — the executor. Still ours — but now it is HarnessDefinition over Hermes' own harness pattern, which is cleaner.

3. What Hermes gives us that we did not know to ask for

CapabilityWhy it matters to V3
a2a_orchestrate(capability, message, mode) — fan-out to every peer advertising a capability, all/first/bestthis is the orchestrator, built — and V2 retired its orchestrator as a duplicate
Anti-loop: A2A_MAX_PINGPONG_TURNS, default 5, hard max 20runaway agent↔agent ping-pong is a real failure mode we never designed for
Peer identity via A2A_PEER_TOKENS="alice:tok1,bob:tok2" — "nothing in the request body can assert identity"that is the credential rule, implemented
Bind safety — no token ⇒ 127.0.0.1 onlyfail-safe by default
Injection filters — all inbound text defanged, "remote peers can never reach operator slash commands"the prompt-injection boundary
Outbound redaction — credential-shaped strings scrubbed before leavingV2 nearly leaked a live key
Audit log — append-only ~/.hermes/a2a_audit.jsonlevidence
Rate limiting per authenticated identityabuse control
Trust gate — A2A_TRUSTED_PEERSallowlist
Honest scope note — "True task abort: tasks/cancel marks the task canceled and drops the reply, but cannot abort the live session's in-flight turn"they name the limit — the honesty rule

That last one is the culture we want. They wrote down what their implementation does not do.


4. The uncomfortable summary

We spent a day designing an A2A-native agent system. The framework we run inside already is one.

  • A2A v1.0, spec-correct, both dialects, hardened — built
  • A durable task board with states, heartbeats and a stuck rule — built
  • Progressive-disclosure skills with a curator — built
  • A two-tier bounded memory with pluggable providers — built

And Moyai — the repo Young asked about — is a wrapper around this same framework, named hermes-workspace.

What this is not: a reason to stop. What it is: the same lesson as the fork question, one layer down. Our design work was not wasted — but three stages should change from "build" to "adopt and prove."

The one thing that stays exactly as planned: Stage 0. The instrument that tells whether a control is in force. Because every finding in this document was found by reading source — and none of it was found by the system telling us.


5. Not established

  • The A2A endpoint IS live and WAS probed (§1.2a) — card fetched, both dialects exercised on the wire. But no task has been sent (SendMessage), so the task lifecycle, the store and the streaming path are read from source only.
  • kanban has not been run. States, verbs and the stale number are read from source, not exercised.
  • The a2a-sdk conflict (§1.3) is Sol's call — I found it, I did not resolve it.
  • Port coexistence is now measured, not assumed: plugin on 9900, V2 roster on 8301–8313 (nothing listening). Distinct schemes, no conflict today.
  • The A2A plugin's security controls were read, not tested — no injection filter, redaction or rate limit was exercised.
Copyright © 2026