« Phase 03 · Track Overview

Warmup — A2A, ACP & Multi-Agent Interop, From Zero

Assumes Phase 02 (MCP, JSON-RPC, registries). Assumes nothing about A2A, ACP, agent cards, or why delegation is a different problem from tool calling.


Table of Contents


1. Why tool calling is not enough

The obvious first reaction to "agent A needs agent B" is: expose B as a tool. Wrap it in an MCP server, give it a schema, done.

That works when B is fast and stateless. It breaks in five specific ways when B is a real agent:

PropertyTool callDelegated task
Durationsub-second to secondsminutes to hours; may park for a human
Resulta return valueone or more artifacts with identity and provenance
Interactionone-shotthe callee can ask a question (input-required)
Controlruns to completioncancellable by the caller or a supervisor
Boundaryinside your trust domainanother team, another tenant, sometimes another vendor

Force a two-hour task into a tool call and you get: a connection timeout, work that completed but whose result you lost, no way to cancel it, no way for the callee to ask a clarifying question, and no record of what was produced.

So the protocol needs a task as a first-class, addressable, long-lived object — not a request. Everything else in A2A follows from that one decision.

The other half of the argument is organizational. A tool is something you own and expose; an agent is something another team operates. Delegation is a contract between two owners, which is why A2A's discovery document reads like a service description rather than a function signature.

2. A2A's object model

2.1 The Agent Card

The Agent Card is A2A's discovery document: a JSON object describing an agent's identity, endpoint, capabilities, skills and authentication requirements. Conventionally published at a well-known path so it can be fetched before any interaction.

{
  "protocolVersion": "0.3",
  "name": "sanctions-screening-agent",
  "description": "Screens counterparties against sanctions and watch lists.",
  "url": "https://agents.bank.ae/sanctions",
  "version": "3.1.0",
  "capabilities": {"streaming": true, "pushNotifications": true},
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "securitySchemes": ["oauth2"],
  "skills": [{
    "id": "screen",
    "name": "Screen a counterparty",
    "description": "Screen a legal entity or individual against SDN, UN and local lists.",
    "tags": ["sanctions", "compliance", "screening"],
    "examples": ["Screen Acme Trading FZE"]
  }]
}

Three things to notice, each of which is a design decision you inherit:

Skills are the unit of discovery, not the agent. An agent may do several things; a caller searches for a capability. Tags are how that search works in practice, which makes tag hygiene a platform concern — ungoverned tags produce an unsearchable directory within a year.

securitySchemes is part of discovery. You learn how to authenticate before you connect. That is what lets a caller refuse an agent that only offers an API key, as the lab's NO_ACCEPTABLE_AUTH check does.

The card is prompt surface. Its description and its skills' descriptions go into your model's context so it can decide whom to delegate to — exactly like an MCP tool description, with exactly the same injection implications, now across an organizational boundary. A hostile card is a persistent prompt injection, and your registry should pin the text you reviewed rather than trusting whatever the endpoint serves today.

And what should not be in a published card: your tenant model, your data classifications, your owner metadata. Those are the consumer's control model, not the producer's advertisement. The lab keeps them on AgentCard as platform fields and omits them from to_json().

2.2 Task, Message, Part, Artifact

Task ─┬─ task_id, context_id
      ├─ status: {state, message, timestamp}
      ├─ history: [Message, ...]          the conversation
      └─ artifacts: [Artifact, ...]       the outputs

Message ─┬─ role: user | agent            "user" is the CALLER — often another agent
         └─ parts: [Part, ...]

Part ── kind: text | file | data          data = STRUCTURED result, not prose

Artifact ─┬─ artifact_id, name
          └─ parts: [Part, ...]

Why role: "user" for a calling agent — the vocabulary is inherited from chat, and it is mildly confusing until you read it as "the party being served." In a delegation, the calling agent occupies the user role. Say it out loud once and it stops being confusing.

Why data parts matter. This is the single most important part kind for a bank and the one that separates agent-to-agent from chat. A screening result is {"matches": 1, "top_score": 0.83, "list": "SDN", "recommendation": "manual_review"} — a typed object the calling agent can branch on deterministically. If the only channel were prose, the caller would have to parse a paragraph produced by a language model, which reintroduces non-determinism at exactly the boundary you were trying to make reliable.

Why artifacts are separate from history. The conversation is how the work was negotiated; the artifact is what was produced. An auditor asks for the screening report, not the chat. Keeping them distinct means the evidence pack (Phase 15) has something to point at, and it means a caller can consume the result without replaying a dialogue.

2.3 The context id

taskId identifies one unit of work; contextId groups related ones.

An investigation that delegates screening, credit risk and policy interpretation produces three tasks, one context. That grouping is what makes the audit story coherent: "on 12 March, investigation ctx-88 delegated three tasks across two organizations and produced these four artifacts."

It is also the join key across your own systems — the trace, the cost record, the audit log and the evidence pack all carry it. Choosing it deliberately in Phase 03 is cheap; retrofitting a correlation id across three organizations is not.

3. The task lifecycle

3.1 The states, justified

StateMeaningTerminal
submittedreceived, not yet startedno
workingin progressno
input-requiredthe callee needs something from the callerno
auth-requiredthe callee needs additional authorizationno
completedfinished successfullyyes
canceledstopped by requestyes
failedstopped by erroryes
rejectedthe callee refused to startyes

rejected is worth a note: it is distinct from failed because "I will not do this" and "I tried and broke" are different rows in every incident report and every capacity analysis. A high rejection rate means a discovery or policy problem; a high failure rate means an engineering one.

3.2 Why submitted cannot complete

The lab makes submitted → completed illegal. It looks like it forbids a legitimate fast path.

What it actually forbids is a task reporting success without a recorded working state — which means the execution chain has a completion with no work in it. In a bank, "we have a completed task and no evidence of what was done" is a finding. The cost of the rule is one status transition; the benefit is that every completed task has a middle.

3.3 input-required vs auth-required

Both park the task. They differ in who must act and what the caller does next:

  • input-required — the callee needs information: which jurisdiction, which date range. The caller (or its user) supplies it in a message on the same task. Ordinary business flow.
  • auth-required — the callee needs authorization: a step-up, a consent, a credential with a scope the current one lacks. The caller must obtain a new credential, not send text.

Collapsing them into one state is a common shortcut and it costs you the ability to route the two cases differently — one goes to the user, one goes to the identity layer. It also loses a metric you want: a rising auth-required rate means your scoping is wrong somewhere.

Note the lab makes auth-required → rejected legal but input-required → rejected illegal. Rejection is an admission decision; once a callee is asking clarifying questions it has already admitted the task, and refusing then should be a failed, not a rejected.

4. The three interaction modes

ModeShapeUse when
message/sendblocking; returns the final taskfast work, and simple callers
message/streamSSE stream of status and artifact updates, then the final taskinteractive work where partial progress matters
Push notificationsthe callee POSTs to a caller-supplied webhook on state changework that outlives any connection — hours, or overnight

The lab implements all three over one generator-shaped handler, which is the design worth stealing: an agent author writes one function that yields events, and the platform serves it blocking or streaming without the author writing it twice. message/send is literally "drain the stream and return the last event."

Push notifications are the mode that matters most in a bank, because approval flows are measured in hours. They are also the one with a security problem, next.

5. What A2A does not specify

This is the section to memorize. A2A defines how to delegate. Whether you may is yours.

5.1 Delegation depth

Agent A delegates to B, which delegates to C, which delegates to D. Nothing in the protocol stops this, and each hop multiplies cost and latency while dividing accountability.

The control is a maximum chain depth, enforced at admission, with the chain carried in the request. The lab uses 4. The right number is small: each hop is a full agent run, so at per-hop success p, an n-deep chain succeeds with \( p^n \) — the Phase 00 arithmetic, now across organizations where you cannot even see the failures.

5.2 Cycles

Agent A delegates to B; B, doing its job honestly, delegates to A. Neither is misbehaving. The result is an infinite loop distributed across two owners, each seeing only its own half, each billing tokens.

The control is cycle detection on the chain: if the target already appears in the chain, refuse. This is why the chain must be carried and verified, not reconstructed — the only place that can see the whole cycle is the request itself.

The subtle case: A → B → A' where A' is a different instance of A. Identify agents by their registered identity, not their endpoint, or you will detect nothing.

5.3 Tenancy and classification

Two checks that look like the same thing and are not:

  • Tenancy — may this caller talk to this agent at all? A Retail agent should not be able to delegate to a Wholesale-only service, regardless of what data is involved.
  • Classification — may this data go to that agent? An agent cleared for internal must not receive a restricted task even if both are in the same tenant.

The second is the one people miss, and it is where information barriers live. Classification flows downhill only: a restricted task may be delegated to a restricted-cleared agent, never to a less-cleared one. The lab enforces exactly this comparison.

5.4 The identity chain

The most important undefined piece. When A delegates to B on behalf of user U, B must know:

  • who the user is (for entitlement checks against U's own permissions),
  • which agent is acting (for KYA and attribution),
  • the full chain (so the last hop can enforce on the whole path, not just its caller).

And the rule the lab encodes: the chain is built from the caller's verified context, never from the message body. A callee that can assert its own position in the chain can erase a hop — and the hop it erases will be the one you needed. In the lab this is one line:

delegation_chain = caller.delegation_chain + (caller.agent_id,)

Making that real requires a token that carries the chain and can be verified at each hop — RFC 8693 token exchange with the act claim, which is Phase 08. This phase builds the shape; that phase makes it unforgeable.

6. Push notifications and the SSRF

The caller supplies a URL. The callee's server will make an HTTP request to it. That is server-side request forgery by construction — a feature that, unguarded, lets any caller point your server at any address it likes, including your own internal network and cloud metadata endpoints.

Three controls, and you need all three:

  1. Allow-list the callback host. Not a deny-list; an allow-list. In a bank the set of legal callback hosts is small and known.
  2. Require an authenticated callback. The config carries a token the receiver validates, so a stray POST to the webhook is not accepted as a status update. The lab refuses a config with no token.
  3. Egress control on the callee's network path, so even a mistake cannot reach an internal address. That is Phase 13.

There is a fourth consideration that is not security but reliability: the callback is a delivery attempt, so it needs retries with backoff, and the receiver needs idempotency — the same status update may arrive twice. That is the Phase 10 discipline applied to a webhook.

7. ACP and the protocol-agnostic core

ACP (Agent Communication Protocol) is a REST-shaped sibling: agents expose HTTP endpoints, work is a run, messages are multipart, and execution can be synchronous or asynchronous. Its vocabulary differs (run not task, created/in-progress/completed not submitted/working/completed), but the concepts line up almost one-to-one.

Which raises the actual architectural question: which one does your kernel store?

The answer is neither.

   A2A  ──adapter──┐
                   ├──►  InternalTask  ──►  the kernel, the store, the audit record
   ACP  ──adapter──┤
                   │
   fabric X ───────┘

The kernel has its own vocabulary (queued, running, awaiting_input, awaiting_auth, succeeded, cancelled, failed, rejected), chosen for its needs. Each protocol maps onto it at the edge. Three consequences:

  1. A protocol revision is an adapter change, not a data migration. Given that both A2A and ACP are young and moving, this is not a hypothetical.
  2. Supporting a third protocol is a new adapter, not a new state machine.
  3. Lossy mappings become explicit. ACP has no rejected and no separate auth state, so the lab declares rejected → failed and awaiting_auth → awaiting — and pins both with a test. That is the important part: an undeclared lossy mapping is a bug you discover in production; a declared one is a documented limitation you can reason about.

The test that proves the design works is the round-trip: A2A → internal → ACP → internal must be identity. If it is not, your internal model has leaked a protocol's assumptions.

8. Interoperating with hyperscaler fabrics

The JD names three: Azure AI Foundry agents, AWS Bedrock Agents / AgentCore, and Google ADK (with Agent Engine). Each hosts agents and each has its own notion of a session, an invocation and a result.

Interoperability means two directions, and they have different difficulties:

  • Fronting them — your platform delegates to a fabric-hosted agent. Comparatively easy: write an adapter, map the states, done.
  • Being fronted by them — a fabric-hosted agent delegates to your platform. Harder, because now their identity model has to survive the boundary into yours, and their notion of "the user" may be a service principal with no delegation chain at all.

The real integration risk is therefore not the wire format. It is that identity and policy degrade at the boundary. A fabric that calls you with a workload credential and no user context has erased the chain, and your action gateway will (correctly) refuse anything that needs a user.

So the design rule for this phase: at every protocol boundary, assert what identity you require, and refuse rather than degrade. An adapter that quietly substitutes a service account for a missing user identity has converted an interoperability gap into an audit finding.

9. Multi-agent topologies, and when not to

Four shapes you will be asked about:

TopologyShapeFitsWatch
Supervisor / workerone orchestrator delegates to specialistsmost enterprise cases; clear accountabilitythe supervisor becomes a bottleneck and a single point of failure
Peer-to-peerany agent may delegate to any othergenuinely decentralized organizationscycles, depth, and nobody owning the outcome
Pipelinefixed sequence, each stage delegating onwardwell-understood workflowsit is usually a workflow engine wearing a costume
Blackboardagents read/write a shared contextexploratory workconcurrency, and an audit trail nobody can read

And the question that should come first: should this be multi-agent at all?

The honest answer is often no. Multi-agent buys you organizational separation — different teams, different data, different compliance boundaries — and costs you reliability (\( p^n \) across hops), latency (a full agent run per hop), cost (each hop has its own context), and debuggability (a trace that spans owners).

So the rule: delegate across an ownership boundary, not across a task boundary. Sanctions screening is a different team with different data and its own approvals — delegate. "Summarize then translate" is two steps of one job — do not spawn an agent for each; that is a function call wearing a protocol.

10. Lab walkthrough

Work Lab 01 in this order.

  1. Part and its constructors (§2.2). Validate per kind; copy the mapping in data_part — an aliased dict makes a frozen dataclass mutable.
  2. Message.text() — text parts only.
  3. TASK_TRANSITIONS and advance (§3). Fill from the comment. Run the terminal and trap tests first.
  4. AgentCard.to_json (§2.1). Exactly ten keys; platform metadata omitted.
  5. classification_rank, AgentDirectory (§5.3). register refuses duplicates; discover filters before ranking and never returns the caller.
  6. check_delegation (§5). Return all denials, not the first. The depth boundary is >=, so a chain of 3 passes at max_depth=4.
  7. _host_of — strip scheme, path and userinfo.
  8. A2AServer.set_status / add_artifact (§3, §4). set_status also appends to pushed when a config exists.
  9. message_stream (§4, §5.4). Check delegation first — a denied delegation must leave server.tasks empty. Build the chain from the caller.
  10. message_send — drain the stream, return the last Task.
  11. tasks_get / tasks_cancel / set_push_config (§6). Four distinct refusals in set_push_config.
  12. A2AClientdelegate, delegate_streaming, reply.
  13. The four mapping tables and three adapters (§7). INTERNAL_TO_A2A_STATE must be a true inverse; INTERNAL_TO_ACP_STATUS is deliberately not injective. Then chase the round-trip test until it passes — including for an empty task.

11. Success criteria

Without the guide open:

  • Give five structural differences between a tool call and a delegated task.
  • Draw the A2A lifecycle; justify rejected vs failed, and input-required vs auth-required.
  • Say what an Agent Card contains and what must not be published in one.
  • Explain why role: "user" is the calling agent.
  • Explain why data parts matter more than text parts for agent-to-agent.
  • Explain contextId and name three systems it joins.
  • List five things A2A does not specify, with the control for each.
  • Explain why the delegation chain must be derived from verified context.
  • Explain the push-notification SSRF and its three controls.
  • Argue for a protocol-agnostic core and describe what a declared lossy mapping obliges you to do.
  • State the rule for when to use multi-agent at all.

12. Common mistakes

Modelling a delegated task as a tool call. Timeouts, lost work, no cancellation, no clarification.

Trusting the chain in the message. A callee can erase a hop.

No depth limit. \( p^n \) across organizations, with a bill.

No cycle detection. Two honest agents, one infinite loop, no owner.

Checking tenancy but not classification. Restricted data reaches an internal-cleared agent inside the same tenant.

Accepting any callback URL. SSRF, pointed at your metadata endpoint.

A callback with no token. Anyone who guesses the URL can drive your task's state.

Publishing platform metadata in the card. Your control model, advertised.

Trusting the card the endpoint serves today. It is prompt surface; pin the reviewed text.

Storing A2A objects in the kernel. The next revision is a data migration.

An undeclared lossy mapping. You find out when an auditor asks why a rejected task shows as failed.

Multi-agent for a two-step task. A function call wearing a protocol.

13. Interview Q&A

Q: Why do you need A2A when you already have MCP?

A: "They answer different questions. MCP is 'what tools do I have' — invoke, get a value back, inside my trust domain, in under a second. A2A is 'who else can do this and how do I hand it to them' — and delegated work differs structurally in five ways: it's long-running, sometimes hours if it parks for a human; it produces artifacts with their own identity rather than a return value; the callee can talk back with input-required or auth-required; it's cancellable; and it crosses an organizational boundary, so identity, tenancy and data classification all become explicit. If you force a two-hour task into a tool call you get a connection timeout, work that completed but whose result you lost, and no way to cancel it. The other half of the argument is organizational: a tool is something I own and expose; an agent is something another team operates, so delegation is a contract between two owners."

Q: What does A2A leave to you?

A: "The entire admission layer, which in a bank is the whole risk. Delegation depth — nothing stops A→B→C→D, and each hop is a full agent run, so success is p^n across organizations where I can't even see the failures. Cycle detection — A delegates to B, B honestly delegates back to A, and now there's an infinite loop across two owners each seeing half of it. Tenancy — may this caller talk to that agent at all. Classification — may this data go there; classification flows downhill only, and that's where information barriers live. Acceptable authentication — I refuse an agent that only offers an API key. And the identity chain, which has to be derived from the caller's verified context rather than asserted in the message, because a callee that can state its own position in the chain can erase a hop, and it'll be the interesting one."

Q: How do you support A2A, ACP and three hyperscaler fabrics without a rewrite every quarter?

A: "The kernel speaks an internal task model and every protocol is an edge adapter. My vocabulary is queued/running/awaiting_input/awaiting_auth/succeeded/cancelled/failed/rejected, chosen for my needs, and A2A's states and ACP's statuses map onto it. Three consequences: a protocol revision is an adapter change rather than a data migration, which matters because both specs are young and moving; a third protocol is a new adapter, not a new state machine; and lossy mappings become explicit — ACP has no rejected and no separate auth state, so I declare rejected→failed and awaiting_auth→awaiting and pin both with a test. An undeclared lossy mapping is something you discover in production when an auditor asks why a rejected task shows as failed. The test that proves it works is a round-trip: A2A → internal → ACP → internal has to be identity, and if it isn't, my internal model has leaked someone's assumptions."

Q: What's the real risk in interoperating with a hyperscaler agent fabric?

A: "Not the wire format — that's an adapter. It's that identity and policy degrade at the boundary. Fronting a fabric-hosted agent is easy: I delegate out, I map states. Being fronted is hard: a fabric calls me with a workload credential and no user context, so the delegation chain is already erased before my action gateway sees it, and anything requiring a user entitlement now has no user. The design rule I'd hold is: at every protocol boundary, assert what identity you require and refuse rather than degrade. The tempting shortcut is to substitute a service account for the missing user, and that converts an interoperability gap into an audit finding — you'd be recording that 'the platform' moved money, which is not an answer an examiner accepts."

Q: Push notifications — anything you'd push back on?

A: "Yes, it's an SSRF by construction: the caller supplies a URL and my server fetches it. Three controls, all of them needed. Allow-list the callback host — not a deny-list, an allow-list, because in a bank the legal set is small and known. Require a token in the config that the receiver validates, so a stray POST can't drive a task's state — I'd refuse a config without one. And egress control on the network path so a mistake can't reach an internal address or the cloud metadata endpoint. There's also a reliability half people forget: a callback is a delivery attempt, so it needs retries with backoff and the receiver needs to be idempotent, because the same status update will arrive twice."

Q: When would you not use multi-agent?

A: "Most of the time. Multi-agent buys organizational separation — different teams, different data, different compliance boundaries — and it costs reliability, latency, tokens and debuggability. Every hop is a full agent run, so p^n applies across owners, and the trace now spans systems I don't control. My rule is: delegate across an ownership boundary, not across a task boundary. Sanctions screening is a different team with different data and its own approvals — delegate. 'Summarize then translate' is two steps of one job, and spawning an agent for each is a function call wearing a protocol. When someone proposes a five-agent design for a linear workflow, I ask which two teams own which parts; if the answer is 'one team', it's a workflow."

14. References

  • A2A (Agent2Agent)a2a-protocol.org: the specification, the Agent Card schema, the task lifecycle, streaming and push notifications. Donated to the Linux Foundation in 2025; read the current version and note the protocolVersion field.
  • ACP (Agent Communication Protocol)agentcommunicationprotocol.dev: REST shape, runs, multipart messages, sync/async execution.
  • MCPmodelcontextprotocol.io, for the contrast.
  • OWASP Top 10 for LLM ApplicationsExcessive Agency covers unbounded delegation directly.
  • RFC 8693 (OAuth 2.0 Token Exchange) — the act claim, which is what makes a delegation chain unforgeable; built in Phase 08.
  • Hyperscaler fabrics — Azure AI Foundry Agent Service; AWS Bedrock Agents and AgentCore (Runtime, Gateway, Identity, Memory); Google ADK and Agent Engine. Read each one's session and identity model, which is where the interop risk is.
  • Newman, Building Microservices, 2nd ed. — sagas, choreography vs orchestration, and the ownership argument that maps directly onto supervisor-vs-peer-to-peer topologies.