« Phase 03 · Warmup · Track Overview
Lab 01 — A2A Delegation & a Protocol-Agnostic Core
The problem
Group Compliance runs a sanctions-screening agent. Wholesale runs a payment-investigation agent. The investigation needs the screening. Neither team wants to own the other's code, and neither system can hold the other's data.
That is delegation, and it is a different problem from tool calling. The work is long-running (minutes, sometimes a human approval), it produces artifacts rather than a return value, it can be cancelled, it may need to ask you something halfway through, and — the part that matters most in a bank — it crosses an organizational boundary carrying an identity chain.
A2A specifies the wire format for all of that. It specifies none of the admission control: how deep delegation may go, whether a cycle is forming, whether that agent may see this tenant's data, whether it is cleared for this classification. You build both halves, plus the adapter layer that keeps your kernel from learning any protocol's vocabulary.
What you build
| # | Component | What it does |
|---|---|---|
| 1 | Part, Message, Artifact | the exchange model — text, files and structured data; artifacts as durable outputs distinct from the conversation |
| 2 | TaskState, TASK_TRANSITIONS, advance | the long-running task lifecycle, including input-required and auth-required, with terminal states absorbing |
| 3 | AgentCard, Skill, AgentDirectory | discovery documents and skill-tag search, filtered by tenant and classification before listing |
| 4 | check_delegation | the admission control A2A does not specify: depth, cycles, tenant, classification, acceptable auth |
| 5 | A2AServer | message/send, message/stream, tasks/get, tasks/cancel, push-notification config |
| 6 | A2AClient | the delegating side, which owns the identity chain |
| 7 | PushNotificationConfig + host allow-list | the callback mechanism, and the SSRF it invites |
| 8 | InternalTask + A2A/ACP adapters | the protocol-agnostic core, proven by a lossless round-trip |
Key concepts
| Concept | Where | Why it matters |
|---|---|---|
| Task ≠ tool call | Task | long-running, cancellable, artifact-producing, and able to ask you a question |
context_id | Task.context_id | groups several delegated tasks into one investigation for the audit record |
| Chain from the caller | message_stream | the callee cannot forge its own position in the chain |
| Depth and cycle limits | check_delegation | unbounded delegation is the multi-agent equivalent of an infinite loop, with a bill |
| Classification flows downhill | check_delegation | you may not delegate restricted data to an agent cleared for internal |
| Denied ⇒ no task | message_stream | a refused delegation must leave no state behind |
| Generator handler | AgentHandler | one agent implementation serves both send and stream |
| Callback allow-list | set_push_config | a caller-supplied URL your server fetches is SSRF by construction |
| Internal vocabulary | InternalTask | the kernel never learns a protocol's words, so a revision is an adapter change |
| Declared lossiness | INTERNAL_TO_ACP_STATUS | ACP has no rejected; the collapse is pinned by a test, not discovered in production |
Files
| File | Role |
|---|---|
| lab.py | your implementation |
| solution.py | reference; python solution.py runs an eight-part session |
| test_lab.py | 56 tests |
| requirements.txt | pytest |
Run
pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py
Success criteria
-
All 56 tests green against your
lab.py. -
submitted → completedis illegal;working → completedis legal. -
input-required → rejectedis illegal (rejection is an admission decision, not a mid-flight one). - Every non-terminal state has a path to a terminal state.
-
to_json()emits exactly ten keys, with no tenant, owner or classification. -
Delegation depth is bounded exclusively: a chain of 3 is allowed at
max_depth=4, a chain of 4 is not. -
A denied delegation leaves
server.tasksempty. -
delegation_chainon the created task iscaller.chain + (caller.agent_id,). - A push config with an off-list host, or with no token, is refused.
-
Every
TaskStateround-trips through the internal vocabulary. -
An
InternalTaskwith no output round-trips through ACP unchanged.
How this maps to the real stack
| This lab | The real thing | What we simplified |
|---|---|---|
A2AServer / A2AClient | the A2A protocol over JSON-RPC (and gRPC/REST bindings), with SSE for streaming | no transport, no authentication of the connection |
AgentCard | the published /.well-known/agent-card.json, plus authenticated extended cards | ours is an object, not an HTTP resource; no signature |
TaskState | the A2A task lifecycle | faithful in shape; real implementations also carry richer status metadata |
message/stream | SSE with TaskStatusUpdateEvent / TaskArtifactUpdateEvent | ours is a generator; no reconnection or event ids |
PushNotificationConfig | the real config method, with webhook auth (JWT/bearer) | we validate the host and the token's presence, not its signature |
check_delegation | nothing in the protocol — your control plane | this is the point of the lab |
AgentDirectory | a private agent registry; hyperscaler catalogues (Azure AI Foundry, Bedrock, Agent Engine) | no storage, no approvals |
| ACP adapter | the real ACP REST shape | ours captures the envelope and status vocabulary, not the full spec |
Honest limits. No transport, no connection authentication (the token exchange that makes the
identity chain real is Phase 08), no retry or
timeout semantics, no partial-artifact streaming with append, and no negotiation of input/output
modes.
Extensions
- Streaming artifacts. Implement
append=Truechunking so a long report streams. Then make the client assemble chunks correctly when one arrives out of order. - Real identity. Replace
CallerContextwith a verified JWT carrying anactchain (RFC 8693). Havecheck_delegationread the chain from the token rather than a parameter, and watch how much of the lab's trust model tightens. - Timeouts and compensation. Give a delegated task a deadline; on expiry, cancel it and run a compensating action. Then ask what "cancelled" means for a callee that already moved money.
- Federated directories. Two directories in two trust domains, with an explicit federation agreement. Which fields must be re-verified rather than trusted?
- Card signing. Sign agent cards and verify on discovery, so a rogue endpoint cannot claim to be the sanctions agent.
- A third adapter. Add an adapter for a hyperscaler fabric's task shape and confirm
InternalTaskstill needs no changes. If it does, your core was not protocol-agnostic.
Interview / resume bullets
- "Built the bank's agent-to-agent delegation layer on A2A: agent cards with skill-based discovery, the long-running task lifecycle with input-required and cancellation, artifacts as first-class outputs, and push notifications with a callback allow-list."
- "Added the admission control A2A leaves undefined — bounded delegation depth, cycle detection, tenant and data-classification checks, and a minimum authentication scheme — so a multi-agent flow cannot recurse, loop, or carry restricted data to an uncleared agent."
- "Kept the kernel protocol-agnostic: an internal task model with A2A and ACP adapters at the edge, verified by a lossless round-trip test, so a protocol revision is an adapter change rather than a data migration."
- "Made the delegation chain non-forgeable by deriving it from the caller's verified context rather than the message body, so every hop of a multi-agent flow is attributable in the audit record."