CSE Roadmap

Common tier · about 6 hours · 6 parts

Part 4 of 6 · 10 min read

Acting for someone: delegation

Covers: Acting for someone: delegation; An agent behind a tool and more.

Acting for someone: delegation

The concierge agent receives a request from a user, picks an agent from the cards it trusts, and forwards the request. It is the smallest example of what makes agent systems different: three things travel along the chain, the request, the answer and the user's identity.

AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpmYTQxNWZjMS00YmUyLTRmMTgtOTZhMi05MWI0ODhmOWQ3YjAAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaNGE2JCcZDM+ifR1VsHpxvUAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo1MjI4MzU0Zi1mMWYyLTQ1MzgtOTFiNi1hN2QxN2E1NmFiOTFscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo63fP1xOcuzgO1bFrgCyQ9gAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggwhJOFobi6vEQpypsDGdsteNSkp52iMW15FIZcq3DZYSkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaFVGUTbIXnEqGdIKL50XAR4AAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCC4F/A11WtNQm1s3PVbHXojy12PQrkVNOGJvxomy4YLLGRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBigZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmZhNDE1ZmMxLTRiZTItNGYxOC05NmEyLTkxYjQ4OGY5ZDdiMC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjk4N2RmYjNkLTkwNDctNDBmZi1iMzAxLTZmYjA2YmZkZWRlN3JjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCDCEk4WhuLq8RCnKmwMZ2y141KSnnaIxbXkUhlyrcNlhKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggLDxKOYrRf8bfo/7TIYdn0to4d+FQon4UX0YDuJC1v16iY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggwlsrb9gtD2uz5YwM/D/dJ8Zq8GWW3bLqJKoN0yOcV/p0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQP4QxOO1bIfpuwvBvz7dq4efvYbvPaI1q8Vj/Ken3viaD7zxoF3qcoou/0PEWwdYY6PChLrHAbDBLMMHgeFRavY=Three ways a concierge can speak to another agent for a useraliceconciergevenue agentpassthroughforward the user's token→ downstream refuses it: wrong audience (401)serviceone token for everybody→ every task is owned by the concierge; users cannot tell apartexchangenew token: user + actor + narrow audience→ tasks owned by alice and bob; bob cannot read alice'sRFC 8693-style exchange is the only row where the downstream agent can both enforce who the user is and see who acted for them.
Three ways a concierge can speak to another agent for a user.
Mode What the concierge sends What the venue agent sees Result in the example
Pass-through the token it received a token issued for someone else FAILED with 401: the audience check rejects it. (If the venue forgot that check, it would succeed, which is the worse outcome.)
Service token one token of its own for everyone one principal, concierge-svc works, but all tasks are owned by the concierge; Alice cannot read her own task, and a holder of that token sees both users' tasks
Exchange a new token for that audience, that user, a narrower scope, naming the concierge as the actor principal alice or bob, actor concierge.example works; tasks are owned by the right user and Bob cannot read Alice's

The exchange is in the style of RFC 8693 (token exchange). The stand-in service checks that the incoming token was issued for the concierge, refuses a wider scope than the original, and never issues a token that outlives the one it received (tests for each). A token is for one audience: an exchanged venue token is rejected by the catering agent.

Holding a user's token in a background task is a risk

A delegating agent that keeps going after the user has left is holding credentials with nobody watching. Short lifetimes, narrow scopes and the actor claim (so a log can say who acted for whom) are the defences; none of them is automatic.

Loops and depth

Agents that can call agents can call each other. With no protection, the example's experiment made 41 delegations before I stopped it at a cap of 40. A path check (refuse if your own name is already in the path, carried in message metadata) stopped it at 3; a hop limit alone stopped it at 5. The hop limit is a backstop, not a fix: it still permits four wasted calls. The concierge reports REJECTED with the reason in both cases, rather than failing silently.

What the concierge cannot do

It cannot pass AUTH_REQUIRED upward: if the venue agent needs the person's permission, the concierge turns that into FAILED. That is a real gap, and it is an exercise. It also relays an INPUT_REQUIRED question to its own caller and sends the answer to the same remote task, which a test confirms with one remote task and not two.

An agent behind a tool

Many hosts will meet A2A through MCP: somebody wraps a remote agent as one tool. The wrapper decides what the model gets to see. I wrote two and ran five situations through a scripted caller that can read a status field but not intent from prose.

Situation Naive wrapper Careful wrapper Plain A2A client
Plain answer ok ok ok
The agent asks a question fails ok ok
The agent needs permission fails ok ok
The agent refuses ok (by luck) ok ok
Long job (20 s, tool limit 30 s) fails: timed out ok in 9 calls ok in 11 calls

The naive wrapper blocks until the task settles and returns the words with isError false. A question looks like an answer; a permission request looks like an answer; and a job longer than the caller's timeout loses the task id completely. It passes the refusal case only because the agent's sentence says "only plans events up to 2000", not because the wrapper told anyone it was a refusal.

The careful wrapper keeps the state in structuredContent, marks what really failed with isError, says what to do next in words (Call this tool again with task_id=...), tells the model not to guess credentials and to hand a link to the person, and never blocks past its own limit: after 25 virtual seconds it returns working and the task id.

if st == M.INPUT_REQUIRED:
    return _text(f"The agent needs more information: {words} Call this tool again with "
                 f"task_id={t['id']} and request set to your answer.", False,
                 {"status": "needs_input", "taskId": t["id"], "question": words})

A caveat that matters for how you read the table: a real language model might guess correctly more often than my script, for instance by noticing that the naive wrapper's output ends with a question mark. The table says which design gives any reader something to act on, not how often a real model succeeds. Hard exercise 2 asks you to measure it.

Text from another agent is data

A remote agent's artifact can contain instructions, whether by malice or because it summarised a web page that did. In the experiment the rogue agent returns "SYSTEM NOTE TO THE ASSISTANT: ..." with a link. A naive planner that treats it as a command would send the user's token to the attacker's address. The careful planner shows the text as quoted data and flags the instruction-like wording. This is the same prompt-injection problem as in earlier modules, now with a trust boundary that is easy to forget because the other side is "an agent".

The suite: 41 checks, each tied to a rule

The conformance suite (conformance.py) is a list of functions with an id, the rule it comes from, and a title. They run against a server built from make_world(bugs).

Group Checks What they pin
Discovery (D) 3 card valid at the well-known path, ETag and 304, every listed interface answers
Authentication (A) 6 no token and the challenge, wrong audience, missing scope, another user's task looks absent, listing is scoped, joining someone's context
Version (V) 2 a missing version is treated as 0.3 and refused; an unsupported one is refused and the supported ones listed
Send (S) 11 blocking vs returnImmediately, direct message answers, question and answer on one task, duplicate message ids, server-made task ids, context mismatch, finished tasks refuse messages, malformed messages, media types, no speaking in the agent's voice
Tasks (T) 4 history length, list order and paging, cancel and release, a finished task never moves
Streaming (ST) 6 task then ordered updates, subscribe starts with a snapshot, two streams agree, close at an interrupted state, message-only streams, refusal when streaming is off
Push (PN) 6 ordered delivery with the credential, internal address refused at registration, name that later resolves inside blocked at delivery, secret not echoed, cleanup, capability checked
Extended card (E) 2 premium skill only for the right scope; an agent without one says so
Equivalence (Q) 1 both bindings agree

Does the suite work? Plant defects

Module 6's lesson applies again: a test is only worth what it would catch. I planted 26 realistic defects, each a switch in the server (a missing version check, a task id accepted from the client, a stream that drops its last event, a webhook sent to a private address, and so on), and wrote down before running which checks I expected to fail.

  • On the correct server: 0 failures of 41.
  • 24 of 26 defects were caught, and for 24 the checks that failed were exactly the ones I predicted.
  • Two predictions were wrong, both by being too narrow: blocking_returns_early also failed T2 (list order is by status-change time, not creation time, so a half-finished task moves in the order), and stream_drops_last_event also failed ST3. Neither was a missed defect; I had under-counted the effects.
  • Two defects escaped: working_message_dropped (a message sent to a working task is ignored instead of recorded) and no_push_retry (a failing webhook is not retried). Both are in the code as honest holes. A real check for the first needs to define what the agent should do with the message; for the second it needs a receiver that fails on purpose and a count of attempts. The test file asserts that both escape, so if someone closes the hole the test reminds them to update the counts.

Why 'predicted' matters

If you write down which checks should fail before you run the defect, a surprise tells you something about your understanding of the system, not only about the tests. Both of my surprises were small effects I had not thought through.

What the example does not do

  • No language model anywhere. The agents are scripts. The conclusions about what a model would do are mine, and the tool-wrapper table is the weakest on that point.
  • No gRPC. The third binding is named, not built.
  • HS256 stands in for public-key signatures. It shows the checks; it does not show key distribution.
  • No real discovery. The router ranks cards that are handed to it; the specification leaves registries and curation out of scope, and so do I.
  • The concierge cannot pass an authorization request upward (exercise Medium 3).
  • Time is virtual, so there is no real clock skew, no real timeouts, and no concurrency.
  • My choices where the specification is silent: the transition table, the notification token header, redaction of push secrets on reads, appending messages to working tasks, InvalidParams for another user's context id, and the HTTP status of a JSON-RPC version error.
  • The specification is young. I read it on 24 October 2026; check it before you rely on a detail.

A checklist for "ready to expose an agent"

  • [ ] The card validates, is public, supports conditional requests, and is signed if clients are expected to rely on it.
  • [ ] Every operation checks the caller's identity and audience; another person's task is "not found".
  • [ ] Task ids and context ids are server-generated, and a context belongs to one principal.
  • [ ] A final task stays final; follow-ups are new tasks.
  • [ ] Push URLs are checked against private ranges once, and the checked address is the one used.
  • [ ] Secrets are accepted but never echoed.
  • [ ] A hop limit and a path check protect against loops.
  • [ ] Output from other agents is data, not commands.
  • [ ] The suite has planted defects and you know which ones escape.

Check yourself

An Agent Card says the agent handles payments and is run by your bank. How much of that can a client rely on? (Almost none, unless the card is signed by a key you provisioned. A card is a claim, not a fact.)

For the curious A2A and MCP overlap

The slogan that MCP is for tools and A2A is for agents hides that an agent can sit behind a tool and a tool can be described as an agent. The useful differences are a task that outlives a request, the two pauses (a question, a missing credential), and how the user identity is carried to the other agent.

What you will use daily at work

Practice What it looks like
Task ids A long job returns an id you can poll, stream, or be notified about.
Delegation Pass the user identity on, not the calling agent own.
Conformance tests A suite tied to the protocol rules, checked by planting defects.

Draft: confirm whether your organisation uses A2A at all or only MCP.