Skip to main content

A2A (Agent-to-Agent)

A2A is the open Agent2Agent protocol (v1.0, stewarded by the Linux Foundation) for communication between independent AI agents. The Hermes A2A plugin works in both directions: your agent can call other A2A agents as tools, and other agents can send tasks to your Hermes over HTTP.

It interoperates with any A2A-compliant peer — another Hermes, LangChain, CrewAI, Google ADK agents, or anything built on the official a2a-sdk.

When to use A2A

  • Hermes ↔ Hermes across machines — let your desktop agent hand tasks to a Hermes on a server, or vice versa, each with its own memory, tools, and credentials.
  • Delegating to specialist agents — a peer that advertises web_search/research/coding skills on its Agent Card can be discovered and called mid-conversation.
  • Being a callable service — expose your Hermes so other frameworks' agents can send it tasks.

When you want multiple agents on the same machine, prefer delegation (in-process subagents) or the kanban board (durable multi-profile work queue) — A2A is for crossing process/machine/framework boundaries.

Enable

hermes gateway setup # pick A2A

Or in ~/.hermes/config.yaml:

gateway:
platforms:
a2a:
enabled: true
extra:
port: 9900

The outbound client tools ship as the a2a toolset, off by default — enable it with hermes tools.

Outbound: calling other agents

With the a2a toolset enabled, the agent gets:

ToolWhat it does
a2a_discover(url)Fetch and summarize a peer's Agent Card
a2a_call(agent, message, context_id?)Send a task, get the reply; multi-turn via context_id
a2a_list()Configured peers, saved conversations, metrics
a2a_history(context_id)Recall a persisted A2A conversation
a2a_orchestrate(capability, message, mode?)Fan a task out to every peer advertising a capability (all / first / best)

Configure known peers in config.yaml:

a2a_agents:
researcher:
url: "http://research-box.local:9900"
auth: { type: bearer, token: "..." }
timeout: 120
capabilities: [web_search, research]

Then just ask: "Ask the researcher agent to summarize today's arXiv postings." Direct URLs work too — a2a_call accepts any A2A endpoint.

Inbound: being callable

With the platform enabled, Hermes serves:

  • Agent Card at GET /.well-known/agent-card.json (canonical v1.0 path; the legacy agent.json also answers) — advertises your agent's name, skills (derived from enabled toolsets), and auth requirements.
  • JSON-RPC 2.0 at POST / — canonical v1.0 methods (SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask, SubscribeToTask, push-notification config CRUD) plus the pre-1.0 path-style aliases (message/send, …).
  • SSE streaming for SendStreamingMessage, with spec-correct JSON-RPC-enveloped frames.
  • Push notifications (webhooks) for long-running tasks, HMAC-SHA256 signed.

Inbound tasks are injected into a live gateway session — the same agent, memory, and tools that serve your other channels — and the final reply is returned to the caller as the task result. Conversations are keyed by the A2A contextId, so a peer can hold a multi-turn exchange.

Interoperability is verified against the official Python a2a-sdk (card resolution, SendMessage, streaming).

Security model

Secure by default; every widening step is explicit:

  • No token ⇒ localhost only. The server binds 127.0.0.1. Remote exposure requires a bearer token and an explicit A2A_HOST.
  • Per-peer tokensA2A_PEER_TOKENS="alice:tok1,bob:tok2" gives each peer its own credential; the authenticated name drives rate limiting, trust, and audit.
  • Prompt-injection filtering — inbound text is filtered and framed as untrusted peer input. Remote peers cannot invoke operator slash commands.
  • Outbound redaction — credential-shaped strings (API keys, JWTs, tokens) are scrubbed from replies.
  • Audit log — every exchange appends to ~/.hermes/a2a_audit.jsonl.
  • Anti-loop — per-context turn caps stop two agents ping-ponging forever.

Configuration reference

Env varDefaultMeaning
A2A_PEER_TOKENS(unset)Per-peer credentials name:token,… (preferred)
A2A_BEARER_TOKEN(unset)Shared token; identity falls back to caller IP
A2A_HOST127.0.0.1Bind host — only widens when a token is set
A2A_PORT9900Inbound port
A2A_AGENT_NAMEhostname-derivedName on the Agent Card
A2A_PUBLIC_URL(unset)Routable URL advertised on the card (reverse proxies / k8s)
A2A_TRUSTED_PEERS(unset)Allow-list of authenticated identities
A2A_ALLOW_ALL_USERSfalseAllow any authenticated peer (dev only)
A2A_RATE_LIMIT60Requests/minute per identity
A2A_MAX_PINGPONG_TURNS5Anti-loop turn cap per context (max 20)
A2A_REPLY_TIMEOUT300Seconds to wait for the agent's reply
A2A_PUSH_SECRETbearer tokenHMAC secret for push-notification signing
A2A_ADVERTISED_TOOLSETSall registeredRestrict which skills appear on the Agent Card

Behind a reverse proxy or Kubernetes Service, set A2A_PUBLIC_URL (or rely on X-Forwarded-Host/X-Forwarded-Proto) so the Agent Card advertises a URL peers can actually call back.

Quick test

# From another machine / agent:
curl http://your-host:9900/.well-known/agent-card.json

curl -X POST http://your-host:9900/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage",
"params":{"message":{"messageId":"m1","role":"ROLE_USER",
"parts":[{"text":"What tools do you have?"}]}}}'

Troubleshooting

  • Peers can't reach the card URL — the card was advertising your bind address; set A2A_PUBLIC_URL to the externally routable URL.
  • 401 Unauthorized — token mismatch; check A2A_PEER_TOKENS/A2A_BEARER_TOKEN on the server and the peer's auth: block.
  • Server won't bind non-localhost — by design: set a bearer token first, then A2A_HOST=0.0.0.0.
  • Replies time out on long tasks — raise A2A_REPLY_TIMEOUT, or have the caller register a push-notification config and poll GetTask.