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.
| Evidence | Value |
|---|---|
pyproject.toml package name | hermes-workspace |
| Its own description | "An internal agent workspace with Hermes, Modal sandboxes, and native app connections." |
| Its Hermes pin | 7968c72a3cb80beaae51948378944dd6e3423b96 — in our own git history |
| Its patch file | sandbox/HERMES_PATCHES.md — patches it applies to a Hermes checkout |
| Our revision | 3b0dc776b6 |
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:
| File | Size | What |
|---|---|---|
adapter.py | 49 KB | the inbound A2A v1.0 server |
protocol.py | 21 KB | Agent Card, JSON-RPC framing, task store, persistence |
security.py | 10 KB | auth, identity, injection filters, redaction, audit |
tools.py | 19 KB | outbound 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/subscribereattaches to a running task's stream - Live-session injection — "inbound tasks route through the normal
MessageEvent→handle_messagepath keyed by the A2AcontextId, 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:
| Field | Live value |
|---|---|
name | hermes-LAB |
version | 1.0.0 |
url | http://127.0.0.1:9900/ |
skills | 35 — 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:
| Option | Consequence |
|---|---|
| Use Hermes' A2A plugin | 0 new dependency; spec-correct; already enabled; already security-hardened; but it is Hermes' binding, not the SDK's |
Install a2a-sdk and build our own | matches 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 state | todo · blocked · review · done · archived |
|---|---|
| Lifecycle verbs | create · complete · block · unblock · request_review · request_changes · heartbeat · comment · link · attach · schedule |
| Liveness | kanban_heartbeat + tasks.last_heartbeat_at |
| Stale worker | DEFAULT_CLAIM_HEARTBEAT_MAX_STALE_SECONDS = 60 * 60 (1 hour) |
| Dispatcher | gateway/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:
| Mechanism | Number | Where |
|---|---|---|
| A2A orphaned-task watchdog | 300 s (5 min) | adapter.py:38 _MIN_ORPHAN_TIMEOUT = 300 |
| Watchdog sweep period | 60 s | adapter.py:38 _WATCHDOG_INTERVAL = 60 |
| Kanban stale worker | 3600 s (1 h) | kanban_db.py:288 |
| Boot lock stale | 600 s | boot_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:26importsextract_skill_conditions,extract_skill_description,get_skill_search_rootsagent/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 Stage | Plan said | Reality |
|---|---|---|
| 2 — A2A transport | install a2a-sdk, implement AgentExecutor, prove with Inspector | Hermes has a working A2A v1.0 server, already enabled. Adopt it, or justify duplicating it |
| 3 — task contract | write states.yaml, wire the mapping, invent the stuck rule | kanban has 5 states, heartbeats, a 1 h stale rule, a dispatcher watchdog. The stuck rule needs a choice, not an invention |
| 4 — skills | build the load model | Hermes has progressive disclosure + a curator |
| 5 — memory | design the model | Hermes 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
| Capability | Why it matters to V3 |
|---|---|
a2a_orchestrate(capability, message, mode) — fan-out to every peer advertising a capability, all/first/best | this is the orchestrator, built — and V2 retired its orchestrator as a duplicate |
Anti-loop: A2A_MAX_PINGPONG_TURNS, default 5, hard max 20 | runaway 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 only | fail-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 leaving | V2 nearly leaked a live key |
Audit log — append-only ~/.hermes/a2a_audit.jsonl | evidence |
| Rate limiting per authenticated identity | abuse control |
Trust gate — A2A_TRUSTED_PEERS | allowlist |
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-sdkconflict (§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.
E-commerce study — one document
Young, 2026-10-13: "consolidate into a single document SolOSV3 Docs 01-research: saleor-study.md, saleor-commercial-review.md. Review and study nextjs-commerce and Medusa."
Hermes as the Brain — what we studied, what we enabled, what is still open
Young, 2026-10-11: "Utilise and optimise Hermes for a better Brain... Review, study and enable it these feature on Hermes."

