TAI BUI
Back to curriculum
BuildTypeScript

Communication Protocols

Agents that can't speak the same language aren't a team. They're strangers shouting into the void.

TIME: ~120 minutesPREREQUISITES: Phase 14 (Agent Engineering), Lesson 16.01 (Why Multi-Agent)

Learning Objectives

  • Implement MCP tool discovery and invocation so agents can use tools exposed by external servers
  • Build an A2A agent card and task endpoint that allows one agent to delegate work to another over HTTP
  • Compare MCP (tool access), A2A (agent-to-agent), ACP (enterprise audit), and ANP (decentralized trust) and explain which protocol solves which problem
  • Wire multiple protocols together in a single system where agents discover tools via MCP and delegate tasks via A2A

The Problem

You split your system into multiple agents: a researcher, a coder, a reviewer. Each is good at its own job, and now they have to talk to each other.

The obvious first attempt is to pass strings around. The researcher returns a blob of text and the coder parses it however it can. That works until the coder misreads a research summary, or two agents deadlock waiting on each other, or you need agents built by different teams to collaborate. At that point "just pass strings" falls apart.

Without a shared contract for how agents exchange information, a multi-agent system is fragile, hard to audit, and stuck at the handful of agents you personally wrote.

The ecosystem has answered with four protocols, each covering a different slice of the problem:

  • MCP for tool access
  • A2A for agent-to-agent collaboration
  • ACP for enterprise auditability
  • ANP for decentralized identity and trust

This lesson reads the real wire formats from each spec, builds working implementations, and connects the protocols into one system.

Warm-up0/4
1. In an agent loop, what does the harness do after the model requests a tool call?Warm-up
A model box proposing search(query) to a harness box, with a dashed box asking what the harness does next.
2. Two agents each wait for the other to reply first, and neither ever makes progress. What is this failure called?Warm-up
Agent A and agent B each marked waiting for a reply, with dashed arrows pointing at each other both ways, and a dashed box asking what this is called.
3. A web server answers your request with HTTP status 202. What does that usually mean?Warm-up
A client sending POST /jobs to a server and getting back 202, with a dashed box asking what 202 means.
4. In a public-key digital signature, which key creates the signature and which key checks it?Warm-up
An Alice box holding a key pair sending a document and a signature to a Bob box that already has Alice's public key, with a dashed box asking which key did which.
Check: The Problem0/2
1. Why do multi-agent systems need structured communication protocols instead of passing plain strings between agents?Explain
A researcher sending a paragraph of prose to a coder, with two different guesses at what it means branching off, and a dashed box asking what a shared contract would change.
Typed schematic of the lesson's own text and code, not captured output.
2. A researcher agent sends a free-text summary and the coder agent keeps misreading it. Which fix addresses the root cause?Analyze
A researcher sending a free-text summary to a coder, with two different guesses at its meaning branching off, and a dashed box asking for the fix that addresses the root cause.
Typed schematic of the lesson's own text and code, not captured output.

The Concept

The Protocol Landscape

Think of these four protocols as layers, each addressing a different question:

Mermaid diagram

They aren't competitors. Each one answers a different question at a different level.

MCP (Recap)

MCP is covered in depth in Phase 13. Quick recap: MCP standardizes how an LLM connects to external tools and data sources. It's a client-server protocol where the agent (client) discovers and calls tools exposed by a server.

Mermaid diagram

MCP is agent-to-tool communication. It doesn't help agents talk to each other.

A2A (Agent2Agent Protocol)

Created by: Google (now under Linux Foundation as lf.a2a.v1) Spec version: 1.0.0 Problem: How do autonomous agents collaborate, negotiate, and delegate tasks to each other?

A2A is the protocol for peer-to-peer agent collaboration. Where MCP connects an agent to tools, A2A connects an agent to other agents. Each agent publishes an Agent Card at a well-known URL, and other agents discover, negotiate with, and delegate tasks to it.

How A2A Works

Mermaid diagram

The Real Agent Card

Here is what an A2A Agent Card looks like. It is served at GET /.well-known/agent-card.json:

json
{
  "name": "Research Agent",
  "description": "Searches documentation and summarizes findings",
  "version": "1.0.0",
  "supportedInterfaces": [
    {
      "url": "https://research-agent.example.com/a2a/v1",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "https://research-agent.example.com/a2a/rest",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
  "provider": {
    "organization": "Your Company",
    "url": "https://example.com"
  },
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "web-research",
      "name": "Web Research",
      "description": "Searches the web and synthesizes findings",
      "tags": ["research", "search", "summarization"],
      "examples": ["Research the latest changes in React 19"]
    },
    {
      "id": "doc-analysis",
      "name": "Documentation Analysis",
      "description": "Reads and analyzes technical documentation",
      "tags": ["docs", "analysis"],
      "inputModes": ["text/plain", "application/pdf"],
      "outputModes": ["application/json"]
    }
  ],
  "securitySchemes": {
    "bearer": {
      "httpAuthSecurityScheme": {
        "scheme": "Bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "security": [{ "bearer": [] }]
}

Key things to notice:

  • Skills are what an agent can do. Each has an ID, tags, and supported input/output MIME types. This is how a client agent decides whether this remote agent can handle its request.
  • supportedInterfaces lists multiple protocol bindings. A single agent can speak JSON-RPC, REST, and gRPC simultaneously.
  • Security is built into the card. The client knows what auth it needs before making a single request.

Task Lifecycle

Tasks are the core unit of work in A2A. They move through defined states:

Mermaid diagram

All 8 states (the spec also defines UNSPECIFIED as a sentinel, omitted here):

StateTerminal?Meaning
TASK_STATE_SUBMITTEDNoAcknowledged, not yet processing
TASK_STATE_WORKINGNoActively being processed
TASK_STATE_INPUT_REQUIREDNoAgent needs more info from client
TASK_STATE_AUTH_REQUIREDNoAuthentication needed
TASK_STATE_COMPLETEDYesFinished successfully
TASK_STATE_FAILEDYesFinished with error
TASK_STATE_CANCELEDYesCanceled before completion
TASK_STATE_REJECTEDYesAgent declined the task

Once a task reaches a terminal state, it's immutable. No further messages. Follow-ups create a new task within the same contextId.

Wire Format

A2A uses JSON-RPC 2.0. Here's what a real message exchange looks like:

Client sends a task:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-001",
      "role": "ROLE_USER",
      "parts": [{ "text": "Research React 19 compiler features" }]
    },
    "configuration": {
      "acceptedOutputModes": ["text/plain", "application/json"],
      "historyLength": 10
    }
  }
}

Agent responds with a task:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "id": "task-abc-123",
      "contextId": "ctx-xyz-789",
      "status": {
        "state": "TASK_STATE_COMPLETED",
        "timestamp": "2026-03-27T10:30:00Z"
      },
      "artifacts": [
        {
          "artifactId": "art-001",
          "name": "research-results",
          "parts": [{
            "data": {
              "findings": [
                "React 19 compiler auto-memoizes components",
                "No more manual useMemo/useCallback needed",
                "Compiler runs at build time, not runtime"
              ]
            },
            "mediaType": "application/json"
          }]
        }
      ]
    }
  }
}

Streaming via SSE:

text
POST /message:stream HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0
 
data: {"task":{"id":"task-123","status":{"state":"TASK_STATE_WORKING"}}}
 
data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_WORKING","message":{"role":"ROLE_AGENT","parts":[{"text":"Searching documentation..."}]}}}}
 
data: {"artifactUpdate":{"taskId":"task-123","artifact":{"artifactId":"art-1","parts":[{"text":"partial findings..."}]},"append":true,"lastChunk":false}}
 
data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_COMPLETED"}}}

ACP (Agent Communication Protocol)

Created by: IBM / BeeAI Spec version: 0.2.0 (OpenAPI 3.1.1) Status: Merging into A2A under the Linux Foundation Problem: How do agents communicate with full auditability, session continuity, and trajectory tracking?

ACP is the enterprise protocol. Despite what many summaries claim, ACP does not use JSON-LD. It's a plain REST/JSON API defined via OpenAPI. Its distinguishing piece is TrajectoryMetadata: an agent response can carry a detailed log of the reasoning steps and tool calls that produced it.

Mermaid diagram

Agent Discovery in ACP

ACP defines four discovery methods:

Mermaid diagram

The AgentManifest is simpler than A2A's Agent Card:

json
{
  "name": "summarizer",
  "description": "Summarizes documents with source citations",
  "input_content_types": ["text/plain", "application/pdf"],
  "output_content_types": ["text/plain", "application/json"],
  "metadata": {
    "tags": ["summarization", "RAG"],
    "framework": "BeeAI",
    "capabilities": [
      {
        "name": "Document Summarization",
        "description": "Condenses long documents into key points"
      }
    ],
    "recommended_models": ["llama3.3:70b-instruct-fp16"],
    "license": "Apache-2.0",
    "programming_language": "Python"
  }
}

Run Lifecycle

ACP uses "Runs" instead of "Tasks". A Run is an agent execution with three modes:

ModeBehavior
syncBlocking. Response contains the complete result.
asyncReturns 202 immediately. Poll GET /runs/{id} for status.
streamSSE stream. Events fire as the agent works.

Mermaid diagram

TrajectoryMetadata (The Audit Trail)

This is ACP's key differentiator. A message part can include metadata showing what the agent did:

json
{
  "role": "agent/researcher",
  "parts": [
    {
      "content_type": "text/plain",
      "content": "The weather in San Francisco is 72F and sunny.",
      "metadata": {
        "kind": "trajectory",
        "message": "I need to check the weather for this location",
        "tool_name": "weather_api",
        "tool_input": { "location": "San Francisco, CA" },
        "tool_output": { "temperature": 72, "condition": "sunny" }
      }
    }
  ]
}

For regulated industries this is the feature that matters. An answer arrives with the chain of reasoning behind it: which tools were called, what inputs went in, what outputs came back.

ACP also supports CitationMetadata for source attribution:

json
{
  "kind": "citation",
  "start_index": 0,
  "end_index": 47,
  "url": "https://weather.gov/sf",
  "title": "NWS San Francisco Forecast"
}

ANP (Agent Network Protocol)

Created by: Open-source community (founded by GaoWei Chang) Repo: github.com/agent-network-protocol/AgentNetworkProtocol Problem: How do agents from different organizations trust each other without a central authority?

ANP is the decentralized identity protocol. It builds trust using W3C Decentralized Identifiers (DIDs) and end-to-end encryption. With A2A you find agents through known endpoints. ANP lets an agent prove who it is cryptographically.

ANP has three layers:

Mermaid diagram

DID Documents (Real Structure)

ANP uses a custom DID method called did:wba (Web-Based Agent). The DID did:wba:example.com:user:alice resolves to https://example.com/user/alice/did.json:

json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/jws-2020/v1",
    "https://w3id.org/security/suites/secp256k1-2019/v1"
  ],
  "id": "did:wba:example.com:user:alice",
  "verificationMethod": [
    {
      "id": "did:wba:example.com:user:alice#key-1",
      "type": "EcdsaSecp256k1VerificationKey2019",
      "controller": "did:wba:example.com:user:alice",
      "publicKeyJwk": {
        "crv": "secp256k1",
        "x": "NtngWpJUr-rlNNbs0u-Aa8e16OwSJu6UiFf0Rdo1oJ4",
        "y": "qN1jKupJlFsPFc1UkWinqljv4YE0mq_Ickwnjgasvmo",
        "kty": "EC"
      }
    },
    {
      "id": "did:wba:example.com:user:alice#key-x25519-1",
      "type": "X25519KeyAgreementKey2019",
      "controller": "did:wba:example.com:user:alice",
      "publicKeyMultibase": "z9hFgmPVfmBZwRvFEyniQDBkz9LmV7gDEqytWyGZLmDXE"
    }
  ],
  "authentication": [
    "did:wba:example.com:user:alice#key-1"
  ],
  "keyAgreement": [
    "did:wba:example.com:user:alice#key-x25519-1"
  ],
  "humanAuthorization": [
    "did:wba:example.com:user:alice#key-1"
  ],
  "service": [
    {
      "id": "did:wba:example.com:user:alice#agent-description",
      "type": "AgentDescription",
      "serviceEndpoint": "https://example.com/agents/alice/ad.json"
    }
  ]
}

Key things to notice:

  • Key separation is enforced. Signing keys (secp256k1) are separate from encryption keys (X25519).
  • humanAuthorization is unique to ANP. These keys require explicit human approval (biometric, password, HSM) before use. High-risk operations like fund transfers go through this path.
  • keyAgreement keys are used for HPKE end-to-end encryption (RFC 9180).
  • The service section links to the Agent Description document.

How Trust Works in ANP

ANP does not use a web-of-trust or endorsement graph. Trust is bilateral and verified per-interaction:

Mermaid diagram

Trust comes from three sources:

  1. Domain-level TLS verifies the DID document host
  2. DID cryptographic signatures verify the agent's identity
  3. Principle of least trust grants only minimum permissions

There's no gossip-based trust propagation or PageRank scoring. You verify each agent directly through its DID.

Meta-Protocol Negotiation

This is ANP's most novel feature. When two agents from different ecosystems meet, they don't need pre-agreed data formats. They negotiate in natural language:

json
{
  "action": "protocolNegotiation",
  "sequenceId": 0,
  "candidateProtocols": "I can communicate using:\n1. JSON-RPC with hotel booking schema\n2. REST with OpenAPI 3.1 spec\n3. Natural language over HTTP",
  "modificationSummary": "Initial proposal",
  "status": "negotiating"
}

Mermaid diagram

The agents go back and forth (max 10 rounds) until they agree on a format, then dynamically generate code to handle it. Status values: negotiating, rejected, accepted, timeout.

So two agents that have never met can work out how to communicate without a pre-defined shared schema.

Comparison

MCPA2AACPANP
Created byAnthropicGoogle / Linux FoundationIBM / BeeAICommunity
Spec formatJSON-RPCJSON-RPC / REST / gRPCOpenAPI 3.1 (REST)JSON-RPC
Primary useAgent to ToolAgent to AgentAgent to AgentAgent to Agent
DiscoveryTool listing/.well-known/agent-card.jsonGET /agents, /.well-known/agent.yml/.well-known/agent-descriptions, DID service endpoints
IdentityImplicit (local)Security schemes (OAuth, mTLS)Server-levelW3C DID (did:wba) with E2EE
Audit trailN/ABasic (task history)TrajectoryMetadata (tool calls, reasoning)Not formally specified
State machineN/A8 task states7 run statesN/A
StreamingN/ASSESSETransport-agnostic
Unique featureTool schemasAgent Cards + SkillsTrajectory audit trailMeta-protocol negotiation
Best forTools & dataDynamic collaborationRegulated industriesCross-org trust
StatusStableStable (v1.0)Merging into A2AActive development

How They Work Together

You don't have to pick one. A realistic enterprise system uses several:

Mermaid diagram

  • MCP connects each agent to its tools
  • A2A handles collaboration between agents (internal and external)
  • ACP wraps responses in trajectory metadata for auditability
  • ANP provides identity verification for agents you don't control
Check: The Concept0/22
1. What is MCP (Model Context Protocol)?Define
An agent client listing a server's tools with tools/list, getting a list back, then calling a tool with tools/call and getting a result, with a dashed box asking what this exchange is.
Typed schematic of the lesson's own text and code, not captured output.
2. What is A2A (the Agent2Agent protocol)?Define
Agent 1 fetching agent 2's card at a well-known URL, getting back a card with skills, then sending a task and getting back task status and artifacts, with a dashed box asking what this exchange is.
Typed schematic of the lesson's own text and code, not captured output.
3. What is ACP (Agent Communication Protocol)?Define
A client posting to /runs on an agent and getting back a response with a trajectory metadata field, next to an agent's published manifest listing its input and output content types, with a dashed box asking what this is.
Manifest fields from the lesson's AgentManifest example.
4. What is ANP (Agent Network Protocol)?Define
Agent A fetching a DID document from agent B's own domain and checking a signature against the public keys inside it, with a note that no central authority is involved.
5. In A2A, what is an Agent Card and why does it matter?Define
A caller fetching the research-agent's card at a well-known URL and getting back its name, url and a skills list, with a dashed box asking why this matters to the caller.
Card excerpt is the lesson's research-agent example.
6. What is a DID in the context of ANP?Define
A did:wba identifier with an arrow down to a URL on the same domain, next to a dashed box for the kind of document it resolves to, with a dashed box asking what this identifier is called.
Identifier is the lesson's did:wba:example.com:user:alice example.
7. What does ACP's TrajectoryMetadata attach to an agent's response?Define
A response part with role agent/researcher whose content is a weather report, with a metadata block of kind trajectory holding a message, a tool name, tool input and tool output, and a dashed box asking what this metadata is for.
Exact fields from the lesson's TrajectoryMetadata example.
8. What is a meta-protocol in ANP?Define
Agent X and agent Y with a real protocolNegotiation JSON message passed between them listing candidate protocols, with a dashed box asking what this kind of exchange is called.
JSON is the lesson's own protocolNegotiation example.
9. What is a Task in A2A?Define
A Task object with fields id, contextId, status holding a state and a timestamp, artifacts and history, with a dashed box asking what this object is.
Fields from the lesson's Task type.
10. What is the difference between MCP and A2A?Explain
Two lanes, one agent box connected to a server box and another agent box connected to a second agent box, with a dashed box asking the difference between the two lanes.
Typed schematic of the lesson's own text and code, not captured output.
11. Why does an Agent Card list skills with tags and input and output MIME types?Explain
One skill entry from an Agent Card with an id, tags and input and output modes, with a dashed box asking why an Agent Card lists tags and MIME types for each skill.
Skill fields from the lesson's Real Agent Card example.
12. Why can MCP alone not cover one agent delegating a task to another agent?Explain
An agent client and a server exchanging tools/list and tools/call, with a dashed red box for agent-to-agent delegation marked missing from this exchange, and a question asking why MCP alone cannot cover delegation.
Typed schematic of the lesson's own text and code, not captured output.
13. How does ACP differ from A2A in its design goals?Explain
A client posting a run to an agent over ACP next to one agent delegating a task to a peer agent over A2A, with a note that each protocol's focus is intentionally different.
Typed schematic of the lesson's own text and code, not captured output.
14. In ANP's DID document, why are authentication keys kept separate from keyAgreement keys?Explain
An ANP DID document excerpt with two verification methods, one Ecdsa and one X25519, and separate authentication and keyAgreement lists, with a dashed box asking why the two key types are kept separate.
Fields from the lesson's DID Documents example.
15. What does the humanAuthorization entry in an ANP DID document add?Explain
An ANP DID document excerpt showing authentication, keyAgreement and humanAuthorization lists all naming the same key, with a dashed box asking what humanAuthorization adds.
Fields from the lesson's DID Documents example.
16. What happens when two agents from different ecosystems meet under ANP's meta-protocol negotiation?Explain
Agent X and agent Y trading two rounds of a status negotiating message, with a dashed box asking the cap on rounds and what happens after it.
Typed schematic of the lesson's own text and code, not captured output.
17. How does ANP establish trust between agents during normal operation?Explain
Agent A about to interact with agent B, with three dashed question boxes along the arrow between them, and a note asking how agent A decides to trust agent B per interaction.
Typed schematic of the lesson's own text and code, not captured output.
18. Which set contains only terminal A2A task states, meaning states after which the task is immutable?Explain
A table of the eight A2A task states with a Terminal column full of question marks.
All eight states from the lesson's Task Lifecycle table.
19. ACP's Run lifecycle defines three execution modes. What are they?Explain
A table of three ACP run behaviors with the Mode column full of question marks: one blocking, one returning 202 and polling, one streaming over SSE.
Behaviors are exact text from the lesson's Run Lifecycle table.
20. Your enterprise agents must call tools, collaborate, leave audit records, and also work with a partner's agents you do not control. Which protocol covers which need?Design
Four needs, call tools, collaborate with peers, keep audit records, and trust a partner's agent, each above a dashed question box, with a question asking which protocol covers each.
Typed schematic of the lesson's own text and code, not captured output.
21. A partner company's agent you have never met will send you delegated work, there is no central registry, and you worry about impersonation. Which addition helps most?Design
A partner's agent you have never met delegating a task to you, with a note worrying about impersonation and no central registry, and a dashed box asking what addition helps most.
22. Two agents from different ecosystems share no data schema, and you do not want to write an adapter for every pair. Which idea fits, and what is the catch?Design
Agent A on schema X and agent B on schema Y with no shared format between them, and a dashed box asking which idea fits and what its catch is.

Build It

Step 1: Core Message Types

A multi-agent system starts with a message format. These types map to what the real protocols use:

typescript
import crypto from "node:crypto";
 
type MessageRole = "user" | "agent";
 
type MessagePart =
  | { kind: "text"; text: string }
  | { kind: "data"; data: unknown; mediaType: string }
  | { kind: "file"; name: string; url: string; mediaType: string };
 
type TrajectoryEntry = {
  reasoning: string;
  toolName?: string;
  toolInput?: unknown;
  toolOutput?: unknown;
  timestamp: number;
};
 
type AgentMessage = {
  id: string;
  role: MessageRole;
  parts: MessagePart[];
  trajectory?: TrajectoryEntry[];
  replyTo?: string;
  timestamp: number;
};
 
function createMessage(
  role: MessageRole,
  parts: MessagePart[],
  replyTo?: string
): AgentMessage {
  return {
    id: crypto.randomUUID(),
    role,
    parts,
    replyTo,
    timestamp: Date.now(),
  };
}
 
function textMessage(role: MessageRole, text: string): AgentMessage {
  return createMessage(role, [{ kind: "text", text }]);
}

Notice: MessagePart is multimodal (text, structured data, files) just like the real A2A and ACP specs. TrajectoryEntry captures the reasoning chain, matching ACP's TrajectoryMetadata.

Step 2: A2A Agent Card and Registry

Build agent discovery that matches the real A2A spec:

typescript
type Skill = {
  id: string;
  name: string;
  description: string;
  tags: string[];
  inputModes: string[];
  outputModes: string[];
};
 
type AgentCard = {
  name: string;
  description: string;
  version: string;
  url: string;
  capabilities: {
    streaming: boolean;
    pushNotifications: boolean;
  };
  defaultInputModes: string[];
  defaultOutputModes: string[];
  skills: Skill[];
};
 
class AgentRegistry {
  private cards: Map<string, AgentCard> = new Map();
 
  register(card: AgentCard) {
    this.cards.set(card.name, card);
  }
 
  discoverBySkillTag(tag: string): AgentCard[] {
    return [...this.cards.values()].filter((card) =>
      card.skills.some((skill) => skill.tags.includes(tag))
    );
  }
 
  discoverByInputMode(mimeType: string): AgentCard[] {
    return [...this.cards.values()].filter(
      (card) =>
        card.defaultInputModes.includes(mimeType) ||
        card.skills.some((skill) => skill.inputModes.includes(mimeType))
    );
  }
 
  resolve(name: string): AgentCard | undefined {
    return this.cards.get(name);
  }
 
  listAll(): AgentCard[] {
    return [...this.cards.values()];
  }
}

This is richer than a name-to-capability map. You can find agents by skill tag, by input MIME type, or by name, which is what the A2A spec supports.

Step 3: A2A Task Lifecycle

Build the full task state machine:

typescript
type TaskState =
  | "submitted"
  | "working"
  | "input-required"
  | "auth-required"
  | "completed"
  | "failed"
  | "canceled"
  | "rejected";
 
const TERMINAL_STATES: TaskState[] = [
  "completed",
  "failed",
  "canceled",
  "rejected",
];
 
type TaskStatus = {
  state: TaskState;
  message?: AgentMessage;
  timestamp: number;
};
 
type Artifact = {
  id: string;
  name: string;
  parts: MessagePart[];
};
 
type Task = {
  id: string;
  contextId: string;
  status: TaskStatus;
  artifacts: Artifact[];
  history: AgentMessage[];
};
 
type TaskEvent =
  | { kind: "statusUpdate"; taskId: string; status: TaskStatus }
  | {
      kind: "artifactUpdate";
      taskId: string;
      artifact: Artifact;
      append: boolean;
      lastChunk: boolean;
    };
 
type TaskHandler = (
  task: Task,
  message: AgentMessage
) => AsyncGenerator<TaskEvent>;
 
class TaskManager {
  private tasks: Map<string, Task> = new Map();
  private handlers: Map<string, TaskHandler> = new Map();
  private listeners: Map<string, ((event: TaskEvent) => void)[]> = new Map();
 
  registerHandler(agentName: string, handler: TaskHandler) {
    this.handlers.set(agentName, handler);
  }
 
  subscribe(taskId: string, listener: (event: TaskEvent) => void) {
    const existing = this.listeners.get(taskId) ?? [];
    existing.push(listener);
    this.listeners.set(taskId, existing);
  }
 
  async sendMessage(
    agentName: string,
    message: AgentMessage,
    contextId?: string
  ): Promise<Task> {
    const handler = this.handlers.get(agentName);
    if (!handler) {
      const task = this.createTask(contextId);
      task.status = {
        state: "rejected",
        timestamp: Date.now(),
        message: textMessage("agent", `No handler for ${agentName}`),
      };
      return task;
    }
 
    const task = this.createTask(contextId);
    task.history.push(message);
    task.status = { state: "submitted", timestamp: Date.now() };
 
    this.processTask(task, handler, message).catch((err) => {
      task.status = {
        state: "failed",
        timestamp: Date.now(),
        message: textMessage("agent", String(err)),
      };
    });
    return task;
  }
 
  getTask(taskId: string): Task | undefined {
    return this.tasks.get(taskId);
  }
 
  cancelTask(taskId: string): boolean {
    const task = this.tasks.get(taskId);
    if (!task || TERMINAL_STATES.includes(task.status.state)) return false;
    task.status = { state: "canceled", timestamp: Date.now() };
    this.emit(taskId, {
      kind: "statusUpdate",
      taskId,
      status: task.status,
    });
    return true;
  }
 
  private createTask(contextId?: string): Task {
    const task: Task = {
      id: crypto.randomUUID(),
      contextId: contextId ?? crypto.randomUUID(),
      status: { state: "submitted", timestamp: Date.now() },
      artifacts: [],
      history: [],
    };
    this.tasks.set(task.id, task);
    return task;
  }
 
  private async processTask(
    task: Task,
    handler: TaskHandler,
    message: AgentMessage
  ) {
    task.status = { state: "working", timestamp: Date.now() };
    this.emit(task.id, {
      kind: "statusUpdate",
      taskId: task.id,
      status: task.status,
    });
 
    try {
      for await (const event of handler(task, message)) {
        if (TERMINAL_STATES.includes(task.status.state)) break;
 
        if (event.kind === "statusUpdate") {
          task.status = event.status;
        }
        if (event.kind === "artifactUpdate") {
          const existing = task.artifacts.find(
            (a) => a.id === event.artifact.id
          );
          if (existing && event.append) {
            existing.parts.push(...event.artifact.parts);
          } else {
            task.artifacts.push(event.artifact);
          }
        }
        this.emit(task.id, event);
      }
    } catch (err) {
      task.status = {
        state: "failed",
        timestamp: Date.now(),
        message: textMessage("agent", String(err)),
      };
      this.emit(task.id, {
        kind: "statusUpdate",
        taskId: task.id,
        status: task.status,
      });
    }
  }
 
  private emit(taskId: string, event: TaskEvent) {
    for (const listener of this.listeners.get(taskId) ?? []) {
      listener(event);
    }
  }
}

This implements the real A2A task lifecycle: submitted, working, input-required, terminal states. Handlers are async generators that yield events (status updates and artifact chunks) matching the SSE streaming model.

Step 4: ACP-Style Audit Trail

Wrap communication with trajectory tracking:

typescript
type AuditEntry = {
  runId: string;
  agentName: string;
  input: AgentMessage[];
  output: AgentMessage[];
  trajectory: TrajectoryEntry[];
  status: "created" | "in-progress" | "completed" | "failed" | "awaiting";
  startedAt: number;
  completedAt?: number;
  sessionId?: string;
};
 
class AuditableRunner {
  private log: AuditEntry[] = [];
  private handlers: Map<
    string,
    (input: AgentMessage[]) => Promise<{
      output: AgentMessage[];
      trajectory: TrajectoryEntry[];
    }>
  > = new Map();
 
  registerAgent(
    name: string,
    handler: (input: AgentMessage[]) => Promise<{
      output: AgentMessage[];
      trajectory: TrajectoryEntry[];
    }>
  ) {
    this.handlers.set(name, handler);
  }
 
  async run(
    agentName: string,
    input: AgentMessage[],
    sessionId?: string
  ): Promise<AuditEntry> {
    const entry: AuditEntry = {
      runId: crypto.randomUUID(),
      agentName,
      input: structuredClone(input),
      output: [],
      trajectory: [],
      status: "created",
      startedAt: Date.now(),
      sessionId,
    };
    this.log.push(entry);
 
    const handler = this.handlers.get(agentName);
    if (!handler) {
      entry.status = "failed";
      return entry;
    }
 
    entry.status = "in-progress";
    try {
      const result = await handler(input);
      entry.output = structuredClone(result.output);
      entry.trajectory = structuredClone(result.trajectory);
      entry.status = "completed";
      entry.completedAt = Date.now();
    } catch (err) {
      entry.status = "failed";
      entry.trajectory.push({
        reasoning: `Error: ${String(err)}`,
        timestamp: Date.now(),
      });
      entry.completedAt = Date.now();
    }
    return entry;
  }
 
  getFullAuditLog(): AuditEntry[] {
    return structuredClone(this.log);
  }
 
  getAuditLogForAgent(agentName: string): AuditEntry[] {
    return structuredClone(
      this.log.filter((e) => e.agentName === agentName)
    );
  }
 
  getAuditLogForSession(sessionId: string): AuditEntry[] {
    return structuredClone(
      this.log.filter((e) => e.sessionId === sessionId)
    );
  }
 
  getTrajectoryForRun(runId: string): TrajectoryEntry[] {
    const entry = this.log.find((e) => e.runId === runId);
    return entry ? structuredClone(entry.trajectory) : [];
  }
}

Each execution produces an audit entry: what went in, what came out, and the trajectory of tool calls and reasoning steps in between. You can query by agent, by session, or by individual run.

Step 5: ANP-Style Identity Verification

Build DID-based identity and verification:

typescript
type VerificationMethod = {
  id: string;
  type: string;
  controller: string;
  publicKeyDer: string;
};
 
type DIDDocument = {
  id: string;
  verificationMethod: VerificationMethod[];
  authentication: string[];
  keyAgreement: string[];
  humanAuthorization: string[];
  service: { id: string; type: string; serviceEndpoint: string }[];
};
 
type AgentIdentity = {
  did: string;
  document: DIDDocument;
  privateKey: crypto.KeyObject;
  publicKey: crypto.KeyObject;
};
 
class IdentityRegistry {
  private documents: Map<string, DIDDocument> = new Map();
 
  publish(doc: DIDDocument) {
    this.documents.set(doc.id, doc);
  }
 
  resolve(did: string): DIDDocument | undefined {
    return this.documents.get(did);
  }
 
  verify(did: string, signature: string, payload: string): boolean {
    const doc = this.documents.get(did);
    if (!doc) return false;
 
    const authKeyIds = doc.authentication;
    const authKeys = doc.verificationMethod.filter((vm) =>
      authKeyIds.includes(vm.id)
    );
 
    for (const key of authKeys) {
      const publicKey = crypto.createPublicKey({
        key: Buffer.from(key.publicKeyDer, "base64"),
        format: "der",
        type: "spki",
      });
      const isValid = crypto.verify(
        null,
        Buffer.from(payload),
        publicKey,
        Buffer.from(signature, "hex")
      );
      if (isValid) return true;
    }
    return false;
  }
 
  requiresHumanAuth(did: string, operationKeyId: string): boolean {
    const doc = this.documents.get(did);
    if (!doc) return false;
    return doc.humanAuthorization.includes(operationKeyId);
  }
}
 
function createIdentity(domain: string, agentName: string): AgentIdentity {
  const did = `did:wba:${domain}:agent:${agentName}`;
  const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
 
  const publicKeyDer = publicKey
    .export({ format: "der", type: "spki" })
    .toString("base64");
 
  const keyId = `${did}#key-1`;
  const encKeyId = `${did}#key-x25519-1`;
 
  const document: DIDDocument = {
    id: did,
    verificationMethod: [
      {
        id: keyId,
        type: "Ed25519VerificationKey2020",
        controller: did,
        publicKeyDer,
      },
      {
        id: encKeyId,
        type: "X25519KeyAgreementKey2019",
        controller: did,
        publicKeyDer,
      },
    ],
    authentication: [keyId],
    keyAgreement: [encKeyId],
    humanAuthorization: [],
    service: [
      {
        id: `${did}#agent-description`,
        type: "AgentDescription",
        serviceEndpoint: `https://${domain}/agents/${agentName}/ad.json`,
      },
    ],
  };
 
  return { did, document, privateKey, publicKey };
}
 
function signPayload(identity: AgentIdentity, payload: string): string {
  return crypto
    .sign(null, Buffer.from(payload), identity.privateKey)
    .toString("hex");
}

The shape follows ANP's identity model: a DID document with separate authentication, key agreement, and human authorization fields. The IdentityRegistry simulates DID resolution (in production that is an HTTP fetch to the agent's domain).

Step 6: Protocol Gateway

Connect all four protocols into a unified system:

Mermaid diagram

typescript
class ProtocolGateway {
  private registry: AgentRegistry;
  private taskManager: TaskManager;
  private auditRunner: AuditableRunner;
  private identityRegistry: IdentityRegistry;
 
  constructor(
    registry: AgentRegistry,
    taskManager: TaskManager,
    auditRunner: AuditableRunner,
    identityRegistry: IdentityRegistry
  ) {
    this.registry = registry;
    this.taskManager = taskManager;
    this.auditRunner = auditRunner;
    this.identityRegistry = identityRegistry;
  }
 
  async delegateTask(
    fromDid: string,
    signature: string,
    targetAgent: string,
    message: AgentMessage,
    sessionId?: string
  ): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
    if (!this.identityRegistry.verify(fromDid, signature, message.id)) {
      return { error: "Identity verification failed" };
    }
 
    const card = this.registry.resolve(targetAgent);
    if (!card) {
      return { error: `Agent ${targetAgent} not found in registry` };
    }
 
    const audit = await this.auditRunner.run(
      targetAgent,
      [message],
      sessionId
    );
    const task = await this.taskManager.sendMessage(targetAgent, message);
 
    return { task, audit };
  }
 
  discoverAndDelegate(
    fromDid: string,
    signature: string,
    skillTag: string,
    message: AgentMessage
  ): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
    const candidates = this.registry.discoverBySkillTag(skillTag);
    if (candidates.length === 0) {
      return Promise.resolve({
        error: `No agents found with skill tag: ${skillTag}`,
      });
    }
    return this.delegateTask(
      fromDid,
      signature,
      candidates[0].name,
      message
    );
  }
}

One call to the gateway does four things:

  1. ANP: Verifies the caller's identity via DID signature
  2. A2A: Discovers the target agent and checks capabilities
  3. ACP: Wraps the execution in an audit trail with trajectory
  4. A2A: Creates a task with full lifecycle tracking

Step 7: Wire It All Together

typescript
async function protocolDemo() {
  const registry = new AgentRegistry();
  registry.register({
    name: "researcher",
    description: "Searches and summarizes findings",
    version: "1.0.0",
    url: "https://researcher.local/a2a/v1",
    capabilities: { streaming: true, pushNotifications: false },
    defaultInputModes: ["text/plain"],
    defaultOutputModes: ["text/plain", "application/json"],
    skills: [
      {
        id: "web-research",
        name: "Web Research",
        description: "Searches the web",
        tags: ["research", "search", "summarization"],
        inputModes: ["text/plain"],
        outputModes: ["application/json"],
      },
    ],
  });
  registry.register({
    name: "coder",
    description: "Writes code from specs",
    version: "1.0.0",
    url: "https://coder.local/a2a/v1",
    capabilities: { streaming: false, pushNotifications: false },
    defaultInputModes: ["text/plain", "application/json"],
    defaultOutputModes: ["text/plain"],
    skills: [
      {
        id: "code-gen",
        name: "Code Generation",
        description: "Generates code",
        tags: ["coding", "generation"],
        inputModes: ["text/plain", "application/json"],
        outputModes: ["text/plain"],
      },
    ],
  });
 
  const taskManager = new TaskManager();
  const auditRunner = new AuditableRunner();
 
  const researchTrajectory: TrajectoryEntry[] = [];
 
  taskManager.registerHandler(
    "researcher",
    async function* (task, message) {
      yield {
        kind: "statusUpdate" as const,
        taskId: task.id,
        status: { state: "working" as const, timestamp: Date.now() },
      };
 
      researchTrajectory.push({
        reasoning: "Searching for React 19 documentation",
        toolName: "web_search",
        toolInput: { query: "React 19 compiler features" },
        toolOutput: {
          results: ["react.dev/blog/react-19", "github.com/react/react"],
        },
        timestamp: Date.now(),
      });
 
      researchTrajectory.push({
        reasoning: "Extracting key findings from search results",
        toolName: "doc_analysis",
        toolInput: { url: "react.dev/blog/react-19" },
        toolOutput: {
          summary:
            "React 19 compiler auto-memoizes, no manual useMemo needed",
        },
        timestamp: Date.now(),
      });
 
      yield {
        kind: "artifactUpdate" as const,
        taskId: task.id,
        artifact: {
          id: crypto.randomUUID(),
          name: "research-results",
          parts: [
            {
              kind: "data" as const,
              data: {
                findings: [
                  "React 19 compiler auto-memoizes components",
                  "No more manual useMemo/useCallback needed",
                  "Compiler runs at build time, not runtime",
                ],
                sources: ["react.dev/blog/react-19"],
              },
              mediaType: "application/json",
            },
          ],
        },
        append: false,
        lastChunk: true,
      };
 
      yield {
        kind: "statusUpdate" as const,
        taskId: task.id,
        status: { state: "completed" as const, timestamp: Date.now() },
      };
    }
  );
 
  auditRunner.registerAgent("researcher", async () => ({
    output: [
      textMessage("agent", "React 19 compiler auto-memoizes components"),
    ],
    trajectory: researchTrajectory,
  }));
 
  const identityRegistry = new IdentityRegistry();
 
  const coderIdentity = createIdentity("coder.local", "coder");
  const researcherIdentity = createIdentity("researcher.local", "researcher");
 
  identityRegistry.publish(coderIdentity.document);
  identityRegistry.publish(researcherIdentity.document);
 
  const gateway = new ProtocolGateway(
    registry,
    taskManager,
    auditRunner,
    identityRegistry
  );
 
  console.log("=== Protocol Demo ===\n");
 
  console.log("1. Agent Discovery (A2A)");
  const researchAgents = registry.discoverBySkillTag("research");
  console.log(
    `   Found ${researchAgents.length} agent(s):`,
    researchAgents.map((a) => a.name)
  );
 
  console.log("\n2. Identity Verification (ANP)");
  const message = textMessage("user", "Research React 19 compiler features");
  const signature = signPayload(coderIdentity, message.id);
  const verified = identityRegistry.verify(
    coderIdentity.did,
    signature,
    message.id
  );
  console.log(`   Coder DID: ${coderIdentity.did}`);
  console.log(`   Signature verified: ${verified}`);
 
  console.log("\n3. Task Delegation (A2A + ACP + ANP)");
  const result = await gateway.delegateTask(
    coderIdentity.did,
    signature,
    "researcher",
    message,
    "session-001"
  );
 
  if ("error" in result) {
    console.log(`   Error: ${result.error}`);
    return;
  }
 
  console.log(`   Task ID: ${result.task.id}`);
  console.log(`   Task state: ${result.task.status.state}`);
  console.log(`   Artifacts: ${result.task.artifacts.length}`);
 
  console.log("\n4. Audit Trail (ACP)");
  console.log(`   Run ID: ${result.audit.runId}`);
  console.log(`   Status: ${result.audit.status}`);
  console.log(`   Trajectory steps: ${result.audit.trajectory.length}`);
  for (const step of result.audit.trajectory) {
    console.log(`     - ${step.reasoning}`);
    if (step.toolName) {
      console.log(`       Tool: ${step.toolName}`);
    }
  }
 
  console.log("\n5. Full Audit Log");
  const fullLog = auditRunner.getFullAuditLog();
  console.log(`   Total runs: ${fullLog.length}`);
  for (const entry of fullLog) {
    const duration = entry.completedAt
      ? `${entry.completedAt - entry.startedAt}ms`
      : "in-progress";
    console.log(`   ${entry.agentName}: ${entry.status} (${duration})`);
  }
}
 
protocolDemo().catch((err) => {
  console.error("Protocol demo failed:", err);
  process.exitCode = 1;
});
Check: Build It0/11
1. In the Build It `ProtocolGateway.delegateTask`, which four things happen in one call?Explain
The real delegateTask method with four numbered steps marked in comments, and a dashed box asking what each step does.
Code from the lesson's Build It, Step 6.
2. With the researcher and coder cards from Step 7 registered, what does `registry.discoverBySkillTag("research")` return?Apply
The tags on the researcher and coder cards from main.ts, next to a call to discoverBySkillTag('research'), with a dashed box asking what it returns.
Tags are from the lesson's Build It, Step 7.
3. With the same two cards, what does `registry.discoverByInputMode("application/json")` return?Apply
The default input modes on the researcher and coder cards from main.ts, next to a call to discoverByInputMode('application/json'), with a dashed box asking what it returns.
Input modes are from the lesson's Build It, Step 7.
4. In Build It, `taskManager.sendMessage("planner", msg)` is called but no handler was registered for "planner". What comes back?Apply
A note that handlers are registered for researcher and coder but not planner, next to a call sendMessage('planner', msg), with a dashed box asking what comes back.
Handler setup from the lesson's Build It, Step 7.
5. The coder signs `message.id`, but `delegateTask` receives a signature made over a different string. What does it return?Apply
A signature created over one string, used in a call to delegateTask that checks a different string, with a dashed box asking what delegateTask returns.
Code mirrors the lesson's delegateTask and signPayload.
6. As written, what do steps 3 and 4 of `protocolDemo` print for the delegated task and the audit entry?Apply
The delegateTask call and the two console.log lines from protocolDemo, with a dashed box asking what they print.
Code is from main.ts, Step 7.
7. An agent handler yields a completed event and then yields more artifacts. How does the Build It `TaskManager` guard against this?Apply
The processTask loop that checks TERMINAL_STATES before applying each event, next to a scenario where a handler yields completed and then yields another artifact, with a dashed box asking what happens to the late artifact.
Code is the lesson's own TaskManager.processTask.
8. A task has already reached `completed`. What does `taskManager.cancelTask(taskId)` do?Apply
A task whose status state is completed, next to a call to cancelTask on it, with a dashed box asking the result.
Matches the lesson's TaskManager.cancelTask.
9. The audit entry for a delegation shows `completed` with zero trajectory steps, although the researcher handler records two. What explains it?Analyze
An AuditEntry marked completed with 0 trajectory entries, next to a note that the researcher handler pushes 2 entries into researchTrajectory, with a dashed box asking what explains the mismatch.
Typed schematic of the lesson's own text and code, not captured output.
10. A reviewer finds that the audit entry for a delegation does not describe the task being tracked. Which design flaw in the gateway causes this?Analyze
The two separate calls inside delegateTask, one to auditRunner.run and one to taskManager.sendMessage, both naming the same targetAgent, with a dashed box asking the design flaw this causes.
Typed schematic of the lesson's own text and code, not captured output.
11. An attacker intercepts a delegation and swaps the message text but keeps the same `message.id` and signature. What does the gateway's verification conclude?Analyze
A signature made over message.id, then checked against a tampered copy of the message that keeps the same id but different text, with a dashed box asking what verify concludes.
Code mirrors the lesson's delegateTask and signPayload.

What Goes Wrong

Protocols cover the happy path. Here is what breaks in production.

Schema drift. Agent A publishes an Agent Card advertising application/json output, then the JSON schema changes between versions. Agent B parses the old format and gets garbage. Fix: version your skills and output schemas. The A2A spec supports version on Agent Cards for this reason.

State machine violations. An agent handler yields a completed event, then tries to yield more artifacts. The task is immutable, so your code either drops the updates silently or throws. Fix: check terminal state before applying an event. The TaskManager above does this with the break after a terminal state.

Trust resolution failures. Agent A tries to verify Agent B's DID, but B's domain is down and the DID document can't be fetched. Do you fail open and accept an unverified agent, or fail closed and reject it? ANP recommends failing closed, following the principle of least trust.

Trajectory bloat. ACP trajectory logging is powerful but expensive. An agent that makes 200 tool calls per run produces a very large audit entry. Fix: log trajectory at configurable verbosity. Record tool names and IO for compliance, and skip reasoning steps for workloads that aren't regulated.

Discovery thundering herd. 50 agents all query GET /agents at once on startup. Fix: cache Agent Cards with a TTL, stagger discovery intervals, or use push-based registration instead of polling.

Check: What Goes Wrong0/4
1. What is trajectory bloat, and what fix is suggested?Explain
An agent making 200 tool calls in one run feeding one audit entry, with a bar showing 200 calls logged, and a question asking what this problem is called and what fix is suggested.
The 200 calls figure is the lesson's own example.
2. An Agent Card advertises a JSON output format that changes between versions, and consumers now parse garbage. What is this called and what is the fix?Analyze
Two versions of the same Agent Card with different output fields, and a note that the consumer still parses the older shape, with a question asking what this is called and how the lesson suggests fixing it.
3. Agent A cannot fetch Agent B's DID document because B's domain is down. What does ANP recommend?Analyze
Agent A failing to fetch agent B's DID document because B's domain is down and the request times out, with a dashed box asking what ANP recommends.
4. Fifty agents all call GET /agents at the same moment on startup. What is this problem called, and which fix applies?Analyze
Fifty simultaneous GET /agents requests arriving at a registry server at once, with a question asking what this problem is called and which fix applies.
The 50-agent figure is the lesson's own example.

Use It

Real Implementations

A2A is the most mature. Google's official spec is open-source under the Linux Foundation. SDKs for Python and TypeScript. If your agents need dynamic discovery and collaboration, start here.

ACP is merging into A2A. IBM's BeeAI project created ACP as a REST-first alternative, but the trajectory metadata concept is being absorbed into the A2A ecosystem. Keep the ACP patterns (trajectory logging, run lifecycle) even if A2A is your transport.

ANP is the most experimental. The community repo has a Python SDK (AgentConnect). The meta-protocol negotiation idea is novel, and worth watching for cross-organizational agent deployments.

MCP is already covered in Phase 13. If you want agents to use tools, MCP is the standard.

Picking the Right Protocol

Mermaid diagram

Check: Use It0/2
1. How do the four protocols compare in real-world maturity?Explain
Four boxes named MCP, A2A, ACP and ANP, each with a dashed question mark box under it, and a note asking how mature each one is in real-world use.
Typed schematic of the lesson's own text and code, not captured output.
2. A bank builds all its agents in house, and regulators want to see which tools each delegated task called and why. What fits best?Design
A bank's in-house agents delegating a task, with a regulator asking which tools were called and why, and a dashed box asking what fits best.

Ship It

This lesson produces:

  • code/main.ts: the A2A, ACP, and ANP patterns wired together through one gateway
  • outputs/prompt-protocol-selector.md: a prompt that helps you choose protocols for your system
Author's Note

I'd run main.ts before reading any further, and read the output with some suspicion. The demo prints the delegated task as working with zero artifacts, and the audit entry as completed with zero trajectory steps. That is the code behaving as written. sendMessage starts processTask without awaiting it, so the task you get back is still in flight. The audit runner executes first, and its handler returns researchTrajectory before the task handler has pushed anything into it.

The gateway also runs the researcher twice, once through AuditableRunner and once through TaskManager. They are two independent executions, so the audit entry describes a run that isn't the task you are tracking. My first fix would be to have the task handler produce the trajectory and make the gateway wait on the task before it reports back. Exercise 2 is a good place to start, since a streaming audit trail forces the same question.

The identity code is a sketch of the shape, not of ANP's cryptography. createIdentity generates a single Ed25519 key pair and puts the same public key bytes in both verification method entries, one of them labelled X25519. humanAuthorization is an empty array, and delegateTask verifies a signature over message.id only, so a valid signature says who sent a message ID and nothing about its content. I'd use IdentityRegistry to learn where verification sits in the flow, and sign the full payload before trusting it with anything real.

Exercises

  1. Multi-hop task delegation. Extend the TaskManager so an agent handler can delegate subtasks to other agents. The researcher receives a task, delegates "search" and "summarize" subtasks to two specialist agents, waits for both to complete, then merges the results into its own artifacts.

  2. Streaming audit trail. Modify the AuditableRunner to support streaming mode. Instead of waiting for the full result, yield AuditEntry updates in real-time as trajectory entries are added. Use an async generator that produces audit snapshots.

  3. DID rotation. Add key rotation to the IdentityRegistry. An agent should be able to publish a new DID document with updated keys while maintaining a previousDid reference. Verifiers should accept signatures from both the current and previous key during a grace period.

  4. Protocol negotiation. Implement ANP's meta-protocol concept. Two agents exchange protocolNegotiation messages with candidate formats (e.g., "I can speak JSON-RPC" vs "I prefer REST"). After max 3 rounds, they agree on a format or timeout. The agreed format determines which TaskManager or AuditableRunner they use.

  5. Rate-limited discovery. Add a RateLimitedRegistry wrapper that caches Agent Card lookups with a configurable TTL and limits discovery queries per agent per second. Simulate a thundering herd of 100 agents discovering each other on startup and measure the difference.

Key Terms

TermWhat people sayWhat it actually means
MCP"The protocol for AI tools"A client-server protocol for agents to discover and use tools. Agent-to-tool, not agent-to-agent.
A2A"Google's agent protocol"A peer-to-peer protocol for agent collaboration under the Linux Foundation. Discovery via Agent Cards, 8-state task lifecycle, streaming via SSE. Supports JSON-RPC, REST, and gRPC bindings.
ACP"Enterprise agent messaging"IBM/BeeAI's REST API for agent runs with TrajectoryMetadata: every response carries the full chain of reasoning and tool calls. Merging into A2A.
ANP"Decentralized agent identity"A community protocol using did:wba (DID) for cryptographic identity, HPKE for E2EE, and AI-powered meta-protocol negotiation for agents that have never seen each other.
Agent Card"An agent's business card"A JSON document at /.well-known/agent-card.json describing skills, supported MIME types, security schemes, and protocol bindings.
DID"Decentralized ID"W3C standard for cryptographically verifiable identities hosted on the agent's own domain. ANP uses did:wba method.
TrajectoryMetadata"The audit receipt"ACP's mechanism for attaching reasoning steps, tool calls, and their inputs/outputs to every agent response.
Meta-protocol"Agents negotiating how to talk"ANP's approach where agents use natural language to dynamically agree on data formats, then generate code to handle them.
Task"A unit of work"A2A's stateful object tracking work from submission through completion. Immutable once terminal.

Further Reading

Code

Ships With