Agent-to-agent
Agent-to-agent (A2A) is the surface one Covenant agent uses to send a task to another, get the result back, and reconstruct a task graph across many such exchanges. The wire types are small; the storage and routing are intentionally pluggable.
Wire types
A task is a request from one agent to another. A result is the response. Tasks form a tree via parent so an orchestrator can fan a root intent across child agents and reconstruct the result graph.
A2ATask {
id: uuid,
sender: AgentId,
recipient: AgentId,
intent_text: "do the thing",
parent: uuid | null,
deadline_ms: u64 | null
}
A2ATaskResult {
task_id: uuid, // matches A2ATask.id
status: "ok" | "error" | "partial",
content: [ Content ], // same Content blocks MCP uses
error_message: string | null
}Mailbox
The Mailbox trait abstracts the queue between agents. The daemon holds one mailbox; agents send and receive through the daemon's IPC or HTTP surface.
trait Mailbox {
async fn send_task(&self, task: A2ATask) -> Result<()>;
async fn recv_task(&self) -> Result<A2ATask>;
async fn try_recv_task(&self) -> Result<Option<A2ATask>>;
async fn recent_tasks(&self, limit: usize) -> Result<Vec<A2ATask>>;
async fn send_result(&self, result: A2ATaskResult) -> Result<()>;
async fn recv_result(&self) -> Result<A2ATaskResult>;
async fn try_recv_result(&self) -> Result<Option<A2ATaskResult>>;
async fn recent_results(&self, limit: usize) -> Result<Vec<A2ATaskResult>>;
}The blocking recv_* variants suit in-process agents that idle on a long-lived connection; the non-blocking try_recv_* variants suit RPC-style callers that prefer to poll over a single round-trip; the non-consuming recent_* variants suit operator dashboards that need to inspect the queue without draining it.
Daemon-mediated flow
POST /a2a/tasks # body: A2ATask JSON
→ 200 { "kind": "a2a_task_queued", "task_id": "uuid" }
GET /a2a/tasks/next # consumes the next queued task
→ 200 { "kind": "a2a_task_opt", "task": { ... } | null }
GET /a2a/tasks/recent?limit=N # non-consuming snapshot
→ 200 { "kind": "a2a_tasks", "tasks": [ ... ] }
POST /a2a/results # body: A2ATaskResult JSON
→ 200 { "kind": "a2a_result_posted", "task_id": "uuid" }
GET /a2a/results/next # consumes the next queued result
→ 200 { "kind": "a2a_result_opt", "result": { ... } | null }
GET /a2a/results/recent?limit=N # non-consuming snapshot
→ 200 { "kind": "a2a_results", "results": [ ... ] }Equivalent IPC variants exist: SendA2ATask, TryRecvA2ATask, RecentA2ATasks, PostA2AResult, TryRecvA2AResult, RecentA2AResults. See Local IPC for the full request/ response shapes.
Capability gating
Both write paths are gated by capability tokens, audited via the standard CapabilityCheck event:
SendA2ATaskrequiresa2a.send.<recipient.display>. The audit row carries scope ida2a-send:<recipient>.PostA2AResultrequiresa2a.respond.<sender.display>, wheresenderis the original sender of the task identified byresult.task_id. The daemon looks the sender up via the mailbox; results whosetask_idwas never dispatched through this daemon are rejected before the capability check, so the attacker cannot probe for granted caps with arbitrary task ids. The audit row carries scope ida2a-respond:<task_id>.
Read paths (TryRecv*, Recent*) are not gated. Drain operations on the operator's own daemon are treated as a local-trust action.
Orchestration patterns
Fan-out
An orchestrator receives a root intent, generates several child A2ATask envelopes (each with parent = root_intent.id), sends them via POST /a2a/tasks, and polls GET /a2a/results/next until it has results for every dispatched child.
Pipeline
Two agents form a producer-consumer pipeline. The producer sends tasks tagged for the consumer's recipient; the consumer pulls them off the mailbox via recv_task and posts results back.
Implementation notes
- Persistence. The default mailbox is in memory. A daemon restart drops every queued task and result. A disk-backed mailbox is on the roadmap.
- Routing. The default mailbox is global FIFO — every
recv_taskcaller pulls from the same queue regardless ofrecipient. Per-recipient routing is on the roadmap. - Authentication. Both write paths are gated by capability tokens (
a2a.send.<recipient>anda2a.respond.<sender>) checked against the daemon's local identity. The cap is not yet bound to the calling HTTP/IPC peer — closing that gap requires per-call peer authentication, which is a separate piece of work. - Sender record. The mailbox keeps a permanent record of the sender for every dispatched task so that respond capabilities can be sender-scoped even after the task has been recv'd. For long-running daemons this map grows unboundedly; a TTL or LRU policy lands with the disk-backed mailbox.
Related
- Concepts — agents in context.
- MCP integration — the companion surface for tools.