[SL_CORE]
< CD ..||6 min read

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.

[multi-agent][ai-agents][architecture][protocols]

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.