feat(a2a): consolidated Agent-to-Agent protocol plugin (closes #514)#41711
feat(a2a): consolidated Agent-to-Agent protocol plugin (closes #514)#41711teknium1 wants to merge 3 commits into
Conversation
Single platform-adapter plugin under plugins/platforms/a2a/ — zero core edits — that supersedes the entire A2A PR/issue cluster. Built on the ctx.register_platform + ctx.register_tool surface the codebase now exposes. Outbound (a2a toolset): a2a_discover / a2a_call / a2a_list let the agent call any A2A-compliant peer over JSON-RPC message/send. Inbound (platform adapter): a stdlib http.server serves an Agent Card at /.well-known/agent.json and routes incoming tasks into the agent's LIVE gateway session (the #11025 insight) — same agent, full memory — returning the reply over A2A. Security on by default: no bearer token => 127.0.0.1-only bind; constant- time bearer auth; inbound prompt-injection filtering + untrusted-peer framing; outbound credential redaction; append-only audit log; per-context conversation persistence outside the compaction pipeline. Stdlib only (no a2a-sdk). 37 tests incl. a live HTTP round-trip (card + message/send + reply) and a bearer-auth 401 path.
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
2 |
unresolved-import |
1 |
invalid-argument-type |
1 |
First entries
tests/plugins/test_a2a_plugin.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/test_a2a_plugin.py:405: [invalid-assignment] invalid-assignment: Object of type `object` is not assignable to attribute `_message_handler` of type `((MessageEvent, /) -> Awaitable[str | None]) | None`
plugins/platforms/a2a/tools.py:313: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["description"]` on object of type `str`
plugins/platforms/a2a/protocol.py:151: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["message"]` and value of type `dict[Unknown, Unknown]` on object of type `dict[str, str]`
✅ Fixed issues: none
Unchanged: 5230 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
The a2a client tools are registered unconditionally by the plugin, but a newly-registered plugin toolset defaults to ENABLED for every platform until the user has seen it in 'hermes tools'. That force-injected 'a2a' into every agent's enabled_toolsets, leaking 3 tool schemas to all users and breaking tests that assert exact toolset membership (test_api_server_toolset::test_create_agent_respects_config_override). Add 'a2a' to _DEFAULT_OFF_TOOLSETS so it stays opt-in (user enables via 'hermes tools'), matching the spotify precedent. The inbound platform adapter is already opt-in (only instantiated when the a2a platform is enabled); this aligns the outbound client tools with the same posture.
…e alias Live Tier-3 testing (CLI agent -> a2a tools -> live peer gateway -> model) surfaced two bugs the kwarg-style unit tests masked: 1. registry.dispatch calls handlers as handler(args, **kwargs) — args is the whole dict positional. The handlers used keyword params (url=, agent=), so the dict bound to the first param and .strip() raised 'dict object has no attribute strip'. Rewrote all three handlers to take args: dict (matching the spotify/google_meet convention). Added a registry-dispatch regression test that exercises the real call path the direct-kwarg tests never hit. 2. The model repeatedly reached for agent_name= instead of agent= (6 retries before success). Accept agent_name/name and message/text/task aliases so a reasonable guess succeeds first try. Verified live: client agent discovers the peer's Agent Card, calls it, and gets the reply back (PONG round-trip confirmed on both client audit log and peer conversation log). 39 plugin tests pass.
Live validation (Tier 1–3)Ran the plugin end-to-end against real HTTP and a real model (gemini-2.5-flash on OpenRouter), in isolated Tier 1 — protocol + security loop (real HTTP, stub agent): ✓ Tier 2 — real gateway + real model: ✓ Tier 3 — CLI agent choosing the tools: ✓ Bugs the live runs caught (fixed in 2c31741)
Known design note for review (not yet addressed)A2A is synchronous request/response, but the gateway's first-contact onboarding notices (pairing code, "no home channel set") are delivered via the same |
Agent Message Bus — a complementary internal orchestration approachHi team, great work on the A2A plugin! We have been working on a related but complementary solution for local multi-agent orchestration (which the DESIGN.md explicitly lists as a different problem left for future work). Sharing our approach in case it is useful. What we built — Agent Message Bus (AMB)AMB is a pull-based internal agent-to-agent message bus — Hermes profiles (general, dev, research, study) communicate via a shared SQLite store with MCP tool access: # Every agent checks their inbox at turn start
messages = mcp_agent_message_bus_agent_read_messages()
if messages:
for msg in messages:
result = process_task(msg)
mcp_agent_message_bus_agent_mark_done(message_id=msg.id, result=result)Architecture highlights:
Why it is complementary to the A2A plugin
AMB is what lets a Study profile agent ask Dev for a file path, or Research send a status update back to General — all on the same host, with zero network config. How AMB could be packaged as a Hermes pluginFollowing your The MCP server registration pattern (from your # __init__.py
def register(ctx):
# Register MCP tools
ctx.register_tool("agent_read_messages", agent_read_messages_handler)
ctx.register_tool("agent_send_message", agent_send_message_handler)
...
# Optional: register cron jobs
ctx.register_cron("amb-watchdog", schedule="*/2 * * * *", handler=watchdog_run)Next stepsWe have the full implementation running in production across multiple Hermes profiles. If the team is interested, we would be happy to:
Happy to discuss further — either in this PR or a dedicated issue. (Full standalone repo: https://github.com/kriszmac4/a2a-communication-protocol) |
Summary
A single plugin —
plugins/platforms/a2a/— gives Hermes fullA2A (Agent-to-Agent) protocol support in both
directions with zero core edits, consolidating the entire A2A
PR/issue cluster (#514 and friends) into one cohesive, policy-correct
implementation.
Closes #514. Supersedes #4135, #11025, #14559, #4948, #4952, #17439, #23871,
#12904 and folds in requirements from #8948, #25176, #689.
Opened as a draft for review of the architecture before we polish.
Why a plugin (not core)
Every prior A2A attempt added a standalone server package (
a2a_adapter/)and/or patched
gateway/run.py+gateway/config.py. The codebase has sincegrown
ctx.register_platform()(the plugin platform-adapter API used by irc,line, teams, ntfy, simplex, …) and
ctx.register_tool(). That makes thestanding policy achievable — plugins must not touch core files — so A2A now
lives entirely under
plugins/platforms/a2a/.What it does
Outbound — client tools (
a2atoolset)a2a_discover(url)— fetch + summarize a peer's Agent Carda2a_call(agent, message, context_id?)— send a JSON-RPCmessage/sendtask, return the reply (multi-turn viacontext_id)a2a_list()— configured peers + persisted conversationsPeers from
config.yaml→a2a_agents, or a direct URL. Works with anyA2A-compliant peer (Hermes, LangChain, CrewAI, Google ADK, OpenClaw, …).
Inbound — platform adapter
http.serveron a daemon thread (no asyncio loop atregister()time — sidesteps the a2a_fleet "register outside a loop" bug class)GET /.well-known/agent.json; JSON-RPCmessage/sendatPOST /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. The reply returns throughadapter.send(), which fulfils a per-contextFuturethe HTTP request blocks on.Security (on by default)
A2A_BEARER_TOKEN⇒ bind127.0.0.1only; a token alone does not widen the bind (remote exposure needs token and explicitA2A_HOST)hmac.compare_digest)sk-…,ghp_…, JWTs, bearer tokens, emails)~/.hermes/a2a_audit.jsonl)~/.hermes/a2a_conversations/— survive context compaction and restartsRequirements traced to the cluster
protocol.build_agent_card, adapter GETtools.pyadapter._handle_inbound_tasksecurity.pyprotocol.persist_messagesecurity.resolve_bind_hostDeliberately out of scope (future)
a2a-sdk/ SSE streaming — wire format here is spec-compatible; an optional[a2a]extra can upgrade transport later without changing the contractFiles
Validation
tests/plugins/test_a2a_plugin.py: 37 passed — security (bind safety, bearer, injection, redaction, audit), protocol (Agent Card, framing, persistence), client tools (HTTP mocked), and two live HTTP round-trips: realmessage/send→ live-session injection → reply, and a bearer-auth 401 path.git diff --stattouches onlyplugins/platforms/a2a/andtests/plugins/.Infographic