A2A federation — SwarmKit agents that hand back their record¶
The gap¶
An agent skill can call a remote agent through its A2A card (a2a-interop.md). The call returns
an answer — and nothing else. The caller's trace stops at "called agent__x, got text"; its audit
has no link to what the remote run did; its GET /usage does not count the tokens the remote
spent; and a budget on the calling node does not reach the remote at all. Across the network the
call graph, the cost and the record all go dark at the boundary (nested-call-limits.md frames the
same problem for depth and budget).
We cannot fix that for an arbitrary remote — a non-SwarmKit agent, or one we do not run, owes us
nothing. But when the callee is also SwarmKit, it already has all of it: a run id, a durable
audit journal (audit-event-journal.md), per-run token/cost totals on the job row, and a
GET /events/GET /audit surface. The gap is that none of it crosses back. This closes that,
between SwarmKit instances, over the A2A metadata channel — no protocol extension a non-SwarmKit
agent has to understand, just extra keys it ignores.
Goal¶
- Identify. A SwarmKit instance's Agent Card declares a federation extension, so adding the
agent (portal probe,
GET /api/a2a/probe) can show "this is a SwarmKit agent" and what it will hand back. - Return. A SwarmKit callee puts its
run_id, token/costusage, and an observability pointer into the task'smetadata.swarmkit. The caller reads them, records ana2a.remote_usageaudit event linking the two runs, and can pull the remote's events/audit byrun_idfrom the same endpoint with the same credential. - Budget (slice 2). The caller passes its remaining allowance in the message metadata; a SwarmKit callee installs it as the child run's budget. Advisory across the trust boundary — a SwarmKit callee honors it, anyone else ignores it, and a returned total is the callee's claim, not an audited fact on our side.
Non-goals¶
- Not a guarantee about non-SwarmKit agents, and not trust in a remote's numbers beyond "recorded
as reported". The
a2a.remote_usageevent is attributed to the remote and labelled as its claim. - Not returning the remote's audit content across the boundary. We return a pointer (
run_id+ the fact thatGET /events/GET /auditanswer there); pulling it still takes the caller's own authorization against that endpoint. A run's events can quote a document that can quote a secret; they do not get shipped in a task result. - Not a new transport. Everything rides A2A
metadata, which the spec leaves open.
Design¶
The card says it (identify)¶
build_agent_card adds an A2A extension under capabilities.extensions:
{ "uri": "https://swarmkit.dev/a2a/federation/v1",
"description": "Returns run id, token/cost usage and an observability pointer per task; honors a passed budget.",
"params": { "runtime": "1.240.0", "returns_usage": true, "returns_observability": true, "honors_budget": true } }
A non-SwarmKit client ignores extensions. fetch_card parses it into AgentCard.swarmkit
(None when absent). GET /api/a2a/probe returns is_swarmkit + runtime + the flags, and the
portal's "Add remote agent" shows a SwarmKit badge. The written skill needs no schema change:
the card is fetched and cached on first use, so the runtime knows at call time whether to expect
the rich round-trip.
The callee returns it¶
task_from_job adds to metadata.swarmkit, on a terminal task:
"run_id": "<job id>",
"usage": { "input_tokens": 1234, "output_tokens": 567, "cost_usd": 0.0189 },
"observability": { "events": true, "audit": true }
run_id is the callee's job id (also the A2A task id, but named explicitly). usage is the job
row's usage_* totals, omitted when zero/absent. observability says the caller can
GET /events?run_id=… and GET /audit at this endpoint — a hint, not the data.
The caller stitches it¶
In _call_remote, once the task is terminal, read task.raw["metadata"]["swarmkit"] and emit:
a2a.remote_usage { card, skill_id, remote_run_id, input_tokens, output_tokens, cost_usd,
endpoint, source: "reported" }
So the caller's own audit — durable, queryable by its run id — carries the link to the remote run
and its cost, attributed as the remote's report. swarmkit logs/GET /audit on the caller then
shows the cross-boundary hop, and a later GET /usage roll-up can include reported remote spend
kept distinct from locally-metered spend.
Budget forward (slice 2)¶
The caller adds message.metadata.swarmkit.budget = { max_cost_usd, max_turns,
max_wall_clock_minutes } — its remaining allowance, derived from the calling node's envelope minus
what it has spent. send_message on a SwarmKit callee reads it and installs it as the child run's
budget/circuit-breaker (the same seam _begin_run uses). This is the cross-instance half of
nested-call-limits.md: the local depth/budget context cannot cross the network, but a value on
the A2A envelope can. Enforcement is the callee's; the caller records what it asked for.
Test plan¶
- Two SwarmKit instances over an in-process ASGI transport (as
test_a2a_*do): a calleragentskill calls a callee topology; assert the returned task carriesmetadata.swarmkit.run_idandusage, and the caller's audit has ana2a.remote_usageevent naming the callee's run id. fetch_card/probe: a SwarmKit card yieldsis_swarmkit=True+ flags; a plain card yields False.- Slice 2: a budget in the message metadata reaches the callee run's tracker; a non-SwarmKit message (no budget) runs unbounded as before.
Demo plan¶
Extend the Level 20 (agents calling agents) tutorial: call one serve instance from another and show
the caller's swarmkit logs carrying the remote run id and cost. just demo-... reuses the A2A
both-ways demo workspace.