Agent-to-agent
Agent-to-agent (A2A) is the surface through which one Covenant agent dispatches a task to another, receives a result, and reconstructs a task graph across many such exchanges. The wire types are minimal; storage and routing are 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,
idempotency: {
duplicate_safety: "unsafe" | "idempotent",
key: string
} | 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_for(&self, recipient) -> Result<Option<A2ATask>>;
async fn recent_tasks(&self, limit: usize) -> Result<Vec<A2ATask>>;
async fn task_queue(&self, limit: usize) -> Result<Vec<A2ATaskQueueEntry>>;
async fn repair_task(&self, request) -> Result<A2ARepairOutcome>;
async fn send_result(&self, result: A2ATaskResult) -> Result<()>;
async fn recv_result(&self) -> Result<A2ATaskResult>;
async fn try_recv_result_for(&self, peer) -> Result<Option<A2ATaskResult>>;
async fn recent_results(&self, limit: usize) -> Result<Vec<A2ATaskResult>>;
}The blocking recv_* variants are appropriate for in-process agents on long-lived connections. The non-blocking try_recv_* variants are appropriate for RPC-style callers that poll over a single round-trip. The non-consuming recent_* and task_queue variants support operator dashboards that inspect the queue without draining it.
A received task becomes an explicit in_flight lease. Leases survive daemon restart and are not automatically redelivered; operators inspect them through the queue-status surface and repair them explicitly when needed. The CLI status surface also has a one-object JSON mode for supervisors that need a stable queue snapshot.
Lease repair
Repair is explicit and operator-controlled. The mailbox supports two repair commands for in-flight tasks:
requeuereturns a leased task toqueued, preserves the last attempt counter, and requires an explicit duplicate-work posture:idempotentoroperator_accepted.force_errorclears the lease and posts an error result for the original sender to drain.
Both commands require a non-empty reason and may include the observed lease_id as a guard against repairing a newer lease than the operator inspected. Daemon, HTTP, and CLI exposure exists for manual repair paths; automatic background retry remains disabled.
Tasks may carry optional idempotency metadata: duplicate safety plus a stable key. Missing metadata is treated as unsafe. The daemon persists and validates the metadata. An explicit retry-stale scan can requeue only stale idempotent tasks when the operator passes --enable; skipped tasks stay visible in the report. See A2A idempotency policy.
Daemon-mediated flow
POST /a2a/tasks # body: A2ATask JSON
→ 200 { "kind": "a2a_task_queued", "task_id": "uuid" }
GET /a2a/tasks/next # leases 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 # drains 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": [ ... ] }
GET /a2a/queue?limit=N # queued tasks, in-flight leases, pending results
→ 200 { "kind": "a2a_queue", "tasks": [ ... ], "results": [ ... ] }covenant a2a status --min-lease-age-ms 300000 --json
→ { "kind": "a2a_status", "limit": 10, "min_lease_age_ms": 300000, "tasks": [ ... ], "results": [ ... ] }Equivalent IPC variants exist: SendA2ATask, TryRecvA2ATask, RecentA2ATasks, PostA2AResult, TryRecvA2AResult, RecentA2AResults, A2AQueue. 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>.
Non-empty A2A scopes are enforced at dispatch. peer_pubkey_b58 pins the counterparty, task_id pins one task, lease_id pins manual repair to one in-flight lease, and duplicate_risk pins requeue posture.
Read paths (TryRecv*, Recent*) are not gated. Drain operations on the operator's own daemon are treated as local-trust actions.
Orchestration patterns
Fan-out
An orchestrator receives a root intent, generates a set of child A2ATask envelopes (each with parent = root_intent.id), submits them via POST /a2a/tasks, and polls GET /a2a/results/next until results have been received for every dispatched child.
Pipeline
Two agents form a producer-consumer pipeline. The producer submits tasks tagged for the consumer's recipient; the consumer dequeues them via recv_task and posts results back.
Implementation notes
- Persistence. The daemon uses an append-only JSONL mailbox. Queued tasks, in-flight leases, sender lookup state, and pending results replay after restart.
- Routing. Peer-scoped receives only drain tasks addressed to the authenticated recipient. Global FIFO receives remain available for in-process orchestrators.
- Authentication. Both write paths are gated by capability tokens (
a2a.send.<recipient>anda2a.respond.<sender>) checked against the daemon's local identity. The capability is not yet bound to the calling HTTP/IPC peer; per-call peer authentication is tracked separately. - Retry posture. A2A delivery avoids automatic redelivery of leased tasks after restart. The explicit retry gate and opt-in scheduler can requeue stale in-flight tasks only when an operator enables bounded policy and the task carries idempotent duplicate-safety metadata.
Related
- Concepts — agents in context.
- MCP integration — the companion surface for tools.