Skip to main content

ADR-0015 — Agent-to-Agent (A2A) Communication Model

  • Status: Proposed (under review — proposal shared with Ashik for input)
  • Date: 2026-06-08
  • Deciders: Vaisakh, Ashik
  • Related: ADR-0012 (agnostic workforce-governance middleware / adopt Google A2A protocol), PRD #623 (Temporal adoption), #1127 (workflow cost rollup)

Context

We need to settle how work flows between Quad (the per-human copilot/orchestrator) and agents, and between agents. The flow: the human talks to Quad; Quad gathers requirements, creates a Task (the unit of work and the unit of accounting), and the work is carried out by one or more agents — often through a reusable workflow (e.g. the dev SDLC pipeline architect → coding → testing → devops). We want per-unit-of-work effort/cost accounting, and we want it to scale.

The open question was: when work starts, should an agent call another agent directly (synchronous), or should work be placed on a queue that the right agent picks up, completes, and hands off via the same mechanism?

First-principles analysis (reasoned independent of the current implementation): the only capability a direct call provides is synchronous blocking — Agent A staying resident while it waits on Agent B. Carrying a message, returning a result, and passing context are all served equally by a queue. And blocking-on-a-peer is precisely what forces an agent to stay resident (and, for LLM agents, to keep burning compute). Every agent-to-agent use case — sub-query, multi-turn clarification, streaming, shared artifact, callback — collapses to "emit message → checkpoint to the Context Engine → stand down → resume on reply," which is strictly cheaper because the waiting agent releases its compute. The Context Engine is the enabler: a durable continuation/memory store that lets an ephemeral agent wake, rehydrate the context for this work-item, finish, and stand down.

Decision

No direct synchronous agent-to-agent communication. All agent work — dispatch, handoff, sub-queries, and results — flows through a message bus, mediated by a thin orchestrator. The only synchronous/persistent connections are at non-agent edges: human ↔ Quad (a live stream) and agent ↔ tool / MCP / data-store (request-response to non-agent services).

The model has five parts:

  1. Message bus / work-queues — role/capability-keyed queues. The only substrate for inter-agent communication. Interim: Redis Streams consumer-groups (at-least-once, idempotency keys, DLQ) behind a Temporal-shaped dispatcher interface. Future: Temporal task-queues (PRD #623) — a backend swap, not a redesign.
  2. Agent worker harness — stateless; consumes a message, hydrates context from the Context Engine, executes, emits a result/next message. Suspend/resume replaces blocking sub-calls: an agent needing input from another emits a query message with a continuation token, checkpoints, and stands down; on reply the orchestrator re-dispatches and the harness rehydrates and resumes. Deployment: warm pool now; scale-to-zero is a per-role deployment knob (min-replicas / idle-timeout), NOT an architecture choice — the same code runs either way, so any role can be flipped to scale-to-zero later purely for cost.
  3. Thin always-on Coordinator (deterministic, non-LLM, cheap to keep warm) — the single choke-point. Owns the Task + WorkflowRun state machine; runs Governance.Check (the cascade); writes the hash-chained audit; accrues cost/effort per Task; decides and publishes the next hop per the workflow definition; manages approval-gate pause/resume. LLM agents scale freely; only this small brain stays up.
  4. Context Engine — the durable context/continuation/memory store. Hydrates per work-item; checkpoints on suspend; rehydrates on resume. The reason ephemeral/event-triggered agents are possible.
  5. Quad (human edge) — gathers requirements, creates the Task, hands it to the Coordinator, and streams status back to the human (the one genuinely live session). Quad does not call agents directly; it publishes to the control plane.

Decision rule (one line): agents never call agents directly — everything is async-on-bus, mediated by the Coordinator. (Tools, data stores, and the human stream are not agents and remain synchronous.)

Scope of this decision: intra-tenant only for now; adopt the Google A2A-protocol envelope shape internally (per ADR-0012); cross-org interop is a later add.

Consequences

Positive

  • Scalable — stateless workers behind queues scale horizontally with natural backpressure.
  • Efficient accounting — the Coordinator accrues cost / elapsed-time / tokens per hop onto the Task, so "effort per unit of work" falls out directly (BillingService.RecordUsage, GetUsageByAgent/Model already meter; finish #1127 for the rollup).
  • Governable — a single choke-point keeps the governance cascade + hash-chained audit centralized (the core moat) rather than scattered across every consumer.
  • Compute-efficient (optional) — because agents are event-triggered functions that checkpoint and stand down, scale-to-zero is available per role with no rearchitecting.
  • Easy — reuses the existing workflow engine, governance cascade, audit chain, and Context Engine; the hard durability (retries, timeouts, saga, survive-restart) is deferred to Temporal rather than hand-built on raw pub/sub.

Negative / trade-offs

  • Per-hop latency — queue + optional cold-start on every hop. Irrelevant for background work; for interactive roles it is hidden by a warm pool. (Founder choice: run warm now.)
  • A coordinator must exist — a small always-on deterministic service (not an LLM, so cheap), and it is a logical single point that must be made HA.
  • Existing delegation must change — today's delegate_to_agent (internal/runtime/subagent/*) pauses the parent but keeps it resident; this becomes emit-query + checkpoint + resume so the delegating agent truly releases compute.

Alternatives considered

  • Direct synchronous A2A (RPC between agents). Rejected: its only benefit is blocking, which forces agents to stay resident and scatters governance/accounting across callers.
  • Free-form agent-to-agent pub/sub choreography (agents choose their own successors). Rejected: loses the central governance/audit/accounting choke-point, makes ordering/branching/recovery implicit, and forces hand-built saga logic.

Implementation outline

  1. This ADR (the decision).
  2. Message bus — role-keyed Redis Streams consumer-groups; idempotency + DLQ; Temporal-shaped dispatcher interface.
  3. Coordinator service (deterministic) — Task + WorkflowRun state machine; Governance.Check
    • approval-gate pause/resume + hash-chain audit + cost accrual + next-hop routing. Builds on internal/workflow/engine.go, internal/governance/cascade.go, audit/v1.
  4. Agent worker harness — message-triggered; Context-Engine hydrate; emit-query + checkpoint
    • resume continuation. Warm-pool deployment; scale-to-zero knob per role.
  5. Context Engine continuation store — checkpoint/rehydrate per work-item + continuation tokens.
  6. Quad router (internal/quad) — requirements → create Task → hand to Coordinator → stream status; remove any direct agent-call path.
  7. Task spine + cost rollup (finish #1127) — Task as per-instance accounting unit linked to its run(s).
  8. Convert delegate_to_agent (internal/runtime/subagent/*) to emit + suspend-with-checkpoint
    • resume.
  9. Later — Temporal migration (#623); cross-org A2A interop (ADR-0012).

Verification

Model the dev SDLC task (architect → coding → testing → devops): Quad creates a Task → Coordinator instantiates the WorkflowRun → each stage is published to a role-queue, consumed by a worker, governed + audited + cost-accrued at the Coordinator, with an approval gate before the deploy step. Demonstrate the suspend/resume sub-query (the coding agent needs an answer from the architect agent → emits a query + checkpoints + stands down to zero compute → resumes on reply). Acceptance: the same workflow definition runs two different Tasks end-to-end, both fully governed/audited/cost-accounted; AuditService.VerifyChain covers every hop; flipping a role from warm to scale-to-zero changes only deployment config, not code.