A2A Agent Interoperability
A2A makes agents discoverable and callable across process, team, runtime, and vendor boundaries.
Source and downloads
Intent
A2A makes agents discoverable and callable across process, team, runtime, and vendor boundaries. Use it when one agent needs to call another agent as a remote collaborator through an explicit protocol contract.
This is the agent version of service-to-service communication. The point is not to let agents chat freely. The point is to let one bounded agent request work from another bounded agent with identity, schema validation, authorization, idempotency, progress, refusal, cancellation, traceability, and versioning.
Microservice communication patterns still apply. REST, gRPC, MCP, and A2A are different protocol shapes, but the engineering concerns are familiar: contracts, identity, authorization, retries, timeouts, idempotency, observability, ownership, and backward compatibility.
Use When
- Agents are owned by different services, teams, runtimes, or vendors.
- A caller must discover what a remote agent can do before sending work.
- Task state must survive asynchronous progress, refusal, error, timeout, retry, or cancellation.
- The caller and callee need a shared contract for ownership, task lifecycle, and result shape.
- Cross-agent communication needs TLS, OAuth or OIDC scopes, trace IDs, and audit records.
Avoid When
- Both agents are simple functions inside one process.
- The interaction is only a local tool call with a typed input and output.
- You cannot authenticate callers, validate messages, or trace handoffs.
- The remote agent has no stable owner, capability contract, or versioning policy.
- A deterministic workflow would be clearer than adding another autonomous collaborator.
Architecture
Use this diagram to read A2A Agent Interoperability as a system boundary, not only a code shape. The key ownership question is: the protocol or capability boundary owns schemas, permissions, invocation records, and response validation.
System Shape
- Capability boundary: callers discover capabilities from an agent card instead of relying on stale assumptions.
- Message boundary: every task request has a schema, trace ID, message ID, idempotency key, source agent, target agent, tenant, audience, scopes, and timeout.
- Identity boundary: callers are authenticated through service identity and delegated credentials.
- Authorization boundary: scopes, tenant, audience, capability, policy, and risk are checked before the remote agent starts work.
- Ownership boundary: every task has a current owner, status, stop reason, and cancellation path.
- Observability boundary: handoffs, retries, refusals, timeouts, approvals, and results are trace events, not hidden chat messages.
Core Protocol
- Fetch the remote agent card: capability, schema, version, scopes, owner, timeout, and lifecycle states.
- Build a task request envelope from current goal, state, tenant, trace ID, idempotency key, and allowed delegation budget.
- Validate the message schema before transport.
- Authenticate caller and verify audience, scopes, tenant, capability, and policy.
- Deliver the request to the remote agent only after authorization succeeds.
- Emit progress, refusal, error, timeout, cancellation, or result events with the same trace ID.
- Validate the response schema and ownership state before the caller consumes the result.
- Record the handoff, decision, latency, cost, status, owner, and stop reason.
- Convert repeated failures, unsafe delegation, or protocol mismatches into eval cases.
Implementation Notes
- Messages should be validated against schemas before delivery.
- A2A messages should carry correlation fields, not rely on logs to reconstruct a handoff later.
- Use TLS for remote transport and mTLS where service identity matters.
- Use OAuth or OIDC scopes for delegated authority.
- Treat refusals, timeouts, and cancellation as normal protocol outcomes, not exceptions.
- Include idempotency keys or task IDs so retries do not duplicate work.
- Add authorization before crossing a trust boundary.
- Keep agent card versions and message schema versions backward compatible.
- Avoid delegation loops by tracking owner, parent task, delegation depth, and stop reason.
Message Envelope
type AgentMessageEnvelope = {
traceId: string;
messageId: string;
idempotencyKey: string;
fromAgent: string;
toAgent: string;
tenantId: string;
capability: string;
auth: {
audience: string;
scopes: string[];
};
timeoutMs: number;
payload: Record<string, unknown>;
};
Authorization Check
function validateA2AEnvelope(input: {
envelope: AgentMessageEnvelope;
requiredAudience: string;
requiredScope: string;
targetAgent: string;
seenIdempotencyKeys: Set<string>;
}) {
const { envelope, requiredAudience, requiredScope, targetAgent, seenIdempotencyKeys } = input;
if (!envelope.traceId || !envelope.messageId || !envelope.idempotencyKey) {
return { decision: 'deny', reason: 'missing_correlation_fields' };
}
if (envelope.toAgent !== targetAgent || envelope.auth.audience !== targetAgent) {
return { decision: 'deny', reason: 'wrong_audience' };
}
if (envelope.auth.audience !== requiredAudience) {
return { decision: 'deny', reason: 'wrong_required_audience' };
}
if (!envelope.auth.scopes.includes(requiredScope)) {
return { decision: 'deny', reason: 'missing_scope' };
}
if (seenIdempotencyKeys.has(envelope.idempotencyKey)) {
return { decision: 'deny', reason: 'duplicate_message' };
}
return { decision: 'allow', reason: 'accepted' };
}
The exact envelope can change by protocol, but the boundary should remain: identify the caller, identify the target, validate authority, preserve correlation, and stop duplicate work.
Failure Modes
- Treating a remote agent like a local function and ignoring latency, refusal, timeout, or cancellation.
- Sending unvalidated natural language blobs instead of typed task messages.
- Missing capability discovery, causing callers to rely on stale assumptions.
- No trace ID across progress, result, and error messages.
- No idempotency key, so retries duplicate work.
- Wrong audience or missing scope is accepted because the remote agent trusts the caller by name.
- Agents delegate the same task back and forth with no owner or delegation budget.
- The remote agent changes its schema without versioning.
- A fallback path bypasses authorization or observability.
Evaluation Strategy
Test the protocol, not only the happy-path collaboration.
- Test valid calls with correct audience, scope, tenant, schema, trace ID, and idempotency key.
- Test wrong audience, missing scope, schema mismatch, missing trace ID, and duplicate message.
- Test refusal as a valid result.
- Test timeout and cancellation behavior.
- Test delegation loop detection.
- Test unsafe capability requests.
- Test backward-compatible schema evolution.
- Test that every handoff emits trace and ownership events.
Measure schema-validity rate, authorization false allows, refusal handling, timeout recovery, duplicate-message detection, delegation-loop rate, trace completeness, and owner-at-failure coverage.
Production Checklist
- Publish an agent card with capabilities, schemas, scopes, owner, version, and lifecycle states.
- Validate every request and response against versioned schemas.
- Require TLS for remote transport and mTLS for service identity where appropriate.
- Validate OAuth or OIDC audience, scopes, subject, tenant, and expiry before execution.
- Require trace ID, message ID, task ID, idempotency key, source agent, and target agent.
- Treat refusal, cancellation, timeout, and approval wait as first-class outcomes.
- Track current owner and delegation depth to prevent task bouncing.
- Add timeouts, retries, and cancellation semantics before production.
- Record handoffs, authorization decisions, progress, results, and stop reasons in traces.
- Convert protocol failures and unsafe delegation into eval fixtures.
Run the Example
npm run a2a:test
npm run a2a:run
Code Walkthrough
Read the excerpt as the smallest executable expression of the pattern. The surrounding chapter explains the design constraints; the code shows where those constraints become concrete interfaces, state, validation, or control flow.
Source Code
These excerpts show the implementation shape. The complete code is available in the download bundle and repository source.
agent-to-agent-communication-pattern/src/run_demo.ts
import { BusMemory } from './bus_memory.ts';
import { AgentA } from './agent_a.ts';
import { AgentB } from './agent_b.ts';
async function run() {
const bus = new BusMemory();
const a = new AgentA(bus);
const b = new AgentB(bus);
a.start();
b.start();
a.handshake();
a.requestTask('t1', 'sum', { a: 2, b: 5 });
}
run();
agent-to-agent-communication-pattern/src/agent_a.ts
import { BusMemory, A2A_SCHEMA } from './bus_memory.ts';
import type { Msg } from './bus_memory.ts';
import Ajv from 'ajv';
import crypto from 'node:crypto';
const ajv = new Ajv({ allErrors: true, strict: true });
const validateResponse = ajv.compile((A2A_SCHEMA as any).properties.TaskResponse);
export class AgentA {
private bus: BusMemory;
private traceId = crypto.randomUUID();
constructor(bus: BusMemory) { this.bus = bus; }
start() {
// listen for responses
this.bus.subscribe('TaskResponse', (m: Msg) => {
if (!validateResponse(m.payload)) {
console.error('Invalid TaskResponse', validateResponse.errors);
return;
}
console.log('AgentA received response:', m.payload);
});
}
handshake() {
this.bus.publish({ type: 'Handshake', payload: { version: '1.0', capabilities: ['tasks', 'cancel'] } });
}
requestTask(id: string, task_type: string, input: any) {
this.bus.publish({
type: 'TaskRequest',
payload: {
id,
task_type,
input,
meta: {
trace_id: this.traceId,
message_id: crypto.randomUUID(),
idempotency_key: `task:${id}`,
from_agent: 'agent-a',
to_agent: 'agent-b',
tenant_id: 'tenant_a',
auth: {
audience: 'agent-b',
scopes: ['task:sum']
},
timeout_ms: 30000,
ts: Date.now()
}
}
});
}
cancel(id: string, reason: string) {
this.bus.publish({ type: 'Cancel', payload: { id, reason } });
}
}
agent-to-agent-communication-pattern/src/agent_b.ts
import { BusMemory, A2A_SCHEMA } from './bus_memory.ts';
import type { Msg } from './bus_memory.ts';
import Ajv from 'ajv';
const ajv = new Ajv({ allErrors: true, strict: true });
const validateReq = ajv.compile((A2A_SCHEMA as any).properties.TaskRequest);
export class AgentB {
private bus: BusMemory;
private handshakeAckSent = false;
private seenIdempotencyKeys = new Set<string>();
constructor(bus: BusMemory) { this.bus = bus; }
start() {
this.bus.subscribe('Handshake', () => {
if (this.handshakeAckSent) return;
this.handshakeAckSent = true;
this.bus.publish({ type: 'Handshake', payload: { version: '1.0', capabilities: ['tasks'] } });
});
this.bus.subscribe('TaskRequest', (m: Msg) => {
if (!validateReq(m.payload)) return;
const payload = m.payload as any;
const { id, task_type, input } = payload;
const authorization = this.authorize(payload);
if (!authorization.allowed) {
this.bus.publish({
type: 'TaskResponse',
payload: { id, status: 'refused', error: authorization.reason }
});
return;
}
this.seenIdempotencyKeys.add(payload.meta.idempotency_key);
if (task_type !== 'sum') {
this.bus.publish({ type: 'TaskResponse', payload: { id, status: 'refused', error: 'unsupported_task' } });
return;
}
// progress
this.bus.publish({ type: 'Progress', payload: { id, stage: 'start', pct: 10, message: 'starting' } });
const a = input?.a;
const b = input?.b;
if (typeof a !== 'number' || typeof b !== 'number') {
this.bus.publish({ type: 'TaskResponse', payload: { id, status: 'error', error: 'invalid_input' } });
return;
}
// compute safely
const sum = a + b;
this.bus.publish({ type: 'Progress', payload: { id, stage: 'compute', pct: 60 } });
this.bus.publish({ type: 'TaskResponse', payload: { id, status: 'success', output: { sum } } });
});
this.bus.subscribe('Cancel', (m: Msg) => {
console.log('AgentB cancel received:', m.payload);
});
}
private authorize(payload: any) {
const meta = payload.meta;
if (meta.to_agent !== 'agent-b' || meta.auth.audience !== 'agent-b') {
return { allowed: false, reason: 'wrong_audience' };
}
if (!meta.auth.scopes.includes('task:sum')) {
return { allowed: false, reason: 'missing_scope' };
}
if (meta.tenant_id !== 'tenant_a') {
return { allowed: false, reason: 'tenant_boundary' };
}
if (this.seenIdempotencyKeys.has(meta.idempotency_key)) {
return { allowed: false, reason: 'duplicate_message' };
}
return { allowed: true, reason: 'authorized' };
}
}
Download
The download bundle contains the current agent-to-agent-communication-pattern/ folder from this repository.