Agent-to-Agent Communication: Delegation and Consensus
# Multi-agent systems need structured protocols for agents to delegate tasks, validate each other's work, and resolve conflicts. Learn the communication patterns that prevent cascading failures and deadlocks.
When agents communicate ad-hoc — just passing text strings between themselves — you get misunderstandings, cascading failures, and impossible-to-debug errors. Structured agent-to-agent communication protocols prevent these problems by making expectations explicit.
The Fundamental Problem
Agents talking to agents face the same problems as microservices talking to each other, plus one more: the content of messages is natural language that can be ambiguous.
Agent A: "Please analyze the codebase"
Agent B: "Done. Here's my analysis: [500 words]"
Agent A: ... (what does "analyze" mean? was it comprehensive?)
Without explicit schemas for requests and responses, agents constantly misunderstand each other.
A2A Message Schema
Define explicit schemas for inter-agent communication:
// Base message types for all agent communications
interface A2ARequest {
requestId: string;
fromAgent: string;
toAgent: string;
timestamp: number;
type: "delegate" | "query" | "validate" | "cancel";
priority: "high" | "normal" | "low";
deadline?: number; // Unix ms timestamp
context: string; // task context the target needs
payload: unknown;
}
interface A2AResponse {
requestId: string; // matches the request
fromAgent: string;
toAgent: string;
timestamp: number;
status: "success" | "failure" | "partial" | "delegated_further";
result?: unknown;
error?: string;
confidence: number; // 0–1: how confident is the agent in its result
completionNote?: string; // human-readable summary
}
// Specific payload types
interface DelegateTaskPayload {
task: string;
expectedOutputSchema: object; // JSON Schema
maxCostUSD?: number;
requiredCapabilities: string[]; // what this sub-agent must be able to do
}
interface ValidationRequestPayload {
toValidate: unknown;
validationCriteria: string[];
referenceDocuments?: string[];
}
Structured Delegation
Orchestrators delegate to sub-agents with explicit expectations:
class AgentOrchestrator {
private agents: Map<string, SubAgent> = new Map();
async delegate<T>(
targetAgentName: string,
task: string,
outputSchema: z.ZodSchema<T>,
options: {
context?: string;
maxCostUSD?: number;
deadline?: number;
} = {}
): Promise<T> {
const agent = this.agents.get(targetAgentName);
if (!agent) throw new Error(`Agent ${targetAgentName} not registered`);
const request: A2ARequest = {
requestId: crypto.randomUUID(),
fromAgent: "orchestrator",
toAgent: targetAgentName,
timestamp: Date.now(),
type: "delegate",
priority: "normal",
deadline: options.deadline,
context: options.context ?? "",
payload: {
task,
expectedOutputSchema: zodToJsonSchema(outputSchema),
maxCostUSD: options.maxCostUSD,
} as DelegateTaskPayload,
};
const response = await agent.handle(request);
if (response.status === "failure") {
throw new Error(`Agent ${targetAgentName} failed: ${response.error}`);
}
// Validate response against expected schema
return outputSchema.parse(response.result);
}
}
Sub-Agent Implementation
class SubAgent {
constructor(
private readonly name: string,
private readonly capabilities: string[],
private readonly systemPrompt: string
) {}
async handle(request: A2ARequest): Promise<A2AResponse> {
const base = {
requestId: request.requestId,
fromAgent: this.name,
toAgent: request.fromAgent,
timestamp: Date.now(),
};
if (request.type === "delegate") {
return this.handleDelegation(request, base);
}
if (request.type === "validate") {
return this.handleValidation(request, base);
}
return { ...base, status: "failure", error: `Unknown request type: ${request.type}`, confidence: 0 };
}
private async handleDelegation(
request: A2ARequest,
base: Partial<A2AResponse>
): Promise<A2AResponse> {
const { task, expectedOutputSchema, maxCostUSD } = request.payload as DelegateTaskPayload;
// Check deadline
if (request.deadline && request.deadline < Date.now() + 5000) {
return { ...base, status: "failure", error: "Insufficient time to complete task", confidence: 0 } as A2AResponse;
}
try {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
system: `${this.systemPrompt}
Return your response as JSON matching this schema:
${JSON.stringify(expectedOutputSchema, null, 2)}`,
messages: [
request.context ? { role: "user" as const, content: `Context: ${request.context}` } : null,
{ role: "user" as const, content: task },
].filter(Boolean) as Anthropic.MessageParam[],
});
const result = JSON.parse(response.content[0].text);
const confidence = this.estimateConfidence(response.content[0].text);
return {
...base,
status: "success",
result,
confidence,
completionNote: `Completed by ${this.name}`,
} as A2AResponse;
} catch (err: any) {
return { ...base, status: "failure", error: err.message, confidence: 0 } as A2AResponse;
}
}
private estimateConfidence(output: string): number {
// Simple heuristic: longer, more structured output = higher confidence
const hasStructure = /\{[\s\S]+\}/.test(output);
const hasContent = output.length > 100;
return hasStructure && hasContent ? 0.85 : 0.5;
}
}
Consensus Protocol for High-Stakes Decisions
When accuracy is critical, use majority voting among multiple agents:
async function agentConsensus<T>(
agents: SubAgent[],
task: string,
schema: z.ZodSchema<T>,
threshold = 0.67 // 2/3 agreement required
): Promise<{ result: T; confidence: number; agreement: number }> {
// Get responses from all agents in parallel
const responses = await Promise.allSettled(
agents.map((agent) =>
orchestrator.delegate(agent.name, task, schema)
)
);
const successful = responses
.filter((r): r is PromiseFulfilledResult<T> => r.status === "fulfilled")
.map((r) => r.value);
if (successful.length === 0) {
throw new Error("All agents failed to produce a result");
}
// Find consensus by comparing outputs
const clusters = clusterSimilarResponses(successful);
const largestCluster = clusters.sort((a, b) => b.length - a.length)[0];
const agreement = largestCluster.length / agents.length;
if (agreement < threshold) {
throw new Error(`No consensus: highest agreement ${(agreement * 100).toFixed(0)}% < ${(threshold * 100).toFixed(0)}%`);
}
// Return representative from largest cluster
return {
result: largestCluster[0],
confidence: agreement,
agreement,
};
}
function clusterSimilarResponses<T>(responses: T[]): T[][] {
// Simple clustering: exact JSON match
const clusters: Map<string, T[]> = new Map();
for (const r of responses) {
const key = JSON.stringify(r);
clusters.set(key, [...(clusters.get(key) ?? []), r]);
}
return [...clusters.values()];
}
Preventing Deadlocks
In multi-agent systems, circular delegation causes deadlocks (A waits for B waits for A):
class DelegationTracker {
private inProgressDelegations: Map<string, Set<string>> = new Map();
canDelegate(fromAgent: string, toAgent: string, requestId: string): boolean {
// Check for circular dependency
const visited = new Set<string>([fromAgent]);
let current = toAgent;
while (current) {
if (visited.has(current)) return false; // cycle detected
visited.add(current);
// Find what 'current' is waiting for
const waiting = this.inProgressDelegations.get(current);
if (!waiting || waiting.size === 0) break;
current = [...waiting][0]; // follow the chain
}
return true;
}
track(fromAgent: string, toAgent: string) {
if (!this.inProgressDelegations.has(fromAgent)) {
this.inProgressDelegations.set(fromAgent, new Set());
}
this.inProgressDelegations.get(fromAgent)!.add(toAgent);
}
release(fromAgent: string, toAgent: string) {
this.inProgressDelegations.get(fromAgent)?.delete(toAgent);
}
}
Practical Example: Code Review Pipeline
async function codeReviewPipeline(code: string): Promise<ReviewResult> {
const tracker = new DelegationTracker();
// Delegate to specialist agents in parallel
const [security, performance, correctness] = await Promise.allSettled([
orchestrator.delegate("security-agent", `Review for security issues: ${code}`, SecurityReviewSchema),
orchestrator.delegate("performance-agent", `Review for performance: ${code}`, PerformanceReviewSchema),
orchestrator.delegate("correctness-agent", `Review for correctness: ${code}`, CorrectnessReviewSchema),
]);
// Aggregate results
const reviews = [security, performance, correctness]
.filter((r): r is PromiseFulfilledResult<any> => r.status === "fulfilled")
.map((r) => r.value);
// Final synthesis by orchestrator
const synthesis = await orchestrator.delegate(
"synthesis-agent",
`Combine these code reviews into a unified report: ${JSON.stringify(reviews)}`,
FinalReviewSchema,
{ context: `Original code: ${code}` }
);
return synthesis;
}
FAQ
Should agents communicate directly or through a message bus? Direct for tight orchestrator-subagent relationships. Message bus (Redis pub/sub, NATS) for loosely coupled agents that discover each other dynamically. Start direct; add a bus when you need event-driven patterns.
How do I handle an agent that takes too long?
Set explicit deadlines in the request. Implement timeout-aware completion: if a sub-agent sees it can't complete by the deadline, it should return a partial result with status: "partial" rather than failing silently.
What happens when sub-agents disagree? Use the consensus protocol for high-stakes decisions. For lower-stakes: take the highest-confidence response. Log all disagreements for analysis — patterns indicate unclear task definitions.
Can an agent re-delegate to another agent?
Yes — with deadlock detection active. Always pass the requestId chain so cycles can be detected. Limit re-delegation depth (max 3 levels) to prevent explosion.
How do I test agent-to-agent interactions? Mock sub-agents with deterministic responses. Test the orchestrator logic with known sub-agent behaviors. Test sub-agents in isolation with standardized request fixtures. Integration test with real sub-agents on a small subset of representative tasks.