Core concepts
Every helper in the Python SDKs is a thin wrapper over one of six ideas. Learn them once here and the rest of the documentation reads as reference rather than magic. If you’d rather see code first, go build something and come back: Core SDK · Google ADK · LangChain.The shape of it
The one-sentence version: your agent never talks to a sub-agent, an MCP server, or an LLM provider directly — it talks to Swarmd, which checks the grant, forwards the call, and writes the audit row. That indirection is the whole product. It’s also why the SDK exists: without it you’d be hand-rolling OAuth2 against two different audiences and rewriting every downstream URL yourself.1. The runtime holds your identity
SwarmDRuntime is the object everything else hangs off. It owns your
credentials and the clients built from them.
configure() the runtime exposes:
The framework wrappers give you
create_runtime(), which does the same thing
from environment variables.
Why “configure later” rather than a constructor? Because credentials can
arrive after boot.
POST /admin/configure lets an operator inject them into a
running agent, and the runtime swaps its clients in place — no restart. That’s
also why token_manager is a proxy: helpers grab it once at startup and it
keeps working across a reconfigure.2. Two tokens, not one
This is the concept that trips people up most, so it’s worth the paragraph. Your agent holds one OAuth2 client credential (agent_id +
client_secret), but it exchanges that credential for two different kinds of
access token, bound to two different audiences:
They are not interchangeable. The relay’s MCP path enforces a path-scoped
audience check; present a
swarmd:api token there and you get 403, not a
helpful error about scopes.
Token handling itself is done for you: both managers cache the token, treat it
as expired 60 seconds early, refresh under a double-checked lock so concurrent
requests don’t stampede the token endpoint, and evict the cache on a 401 so
the next call re-mints instead of looping on a stale token.
3. Subscriptions are your agent’s address book
Your agent cannot call an arbitrary agent. It can only call agents it has been subscribed to — a grant an operator creates in the dashboard or via the registry API. Both ends of a grant have to be bootstrapped first. The registry rejects a subscription whose source or sink is still an agent shell with no card attached — see Setup, step 7.- The card URL is a relay URL, not the sub-agent’s real address. You never learn where the other agent actually lives. Calls go through the relay, which enforces the grant and records the hop.
- This is a live list, not config. Grants change while your agent is running. See refresh.
sub_agent, a LangChain BaseTool — so your LLM sees subscribed agents in
the same catalogue as local functions.
Keeping the list fresh without a webhook: AgentDirectory
Keeping the list fresh without a webhook: AgentDirectory
AgentDirectory polls subscriptions on a background thread and hands you an
immutable snapshot:on_change= to be told which ids were added or
removed. Use it when you want polling instead of — or as belt and braces
alongside — webhook-driven refresh.4. MCP servers are granted the same way
MCP servers work like sub-agents: an operator grants your agent access, and the SDK discovers what it has.McpClient.
Tool namespacing
MCP server names are free-form:"GitLab - swarmd.ai". Tool names, as far as
OpenAI is concerned, must match ^[a-zA-Z0-9_-]+$ — anything else fails the
whole completion with 400.
So the SDK derives a safe namespace per server:
_, and the first 8 hex digits of the server
UUID are appended so two servers whose names sanitise identically still get
distinct prefixes. Both wrappers apply this automatically — you’ll see it in
your tool names, and now you know why.
5. Polling: sub-agents that take their time
A2A tasks are asynchronous. When your agent sendsmessage/send to a
sub-agent, the response may come back in a non-terminal state:
Stock A2A clients return that non-terminal payload immediately — and an LLM
handed
{"state": "working"} will cheerfully report back “the sub-agent is
working on it” and stop. Which is useless.
Both wrappers solve this with a polling wrapper — PollingRemoteA2aAgent
(ADK) and PollingA2aTool (LangChain). On a non-terminal response they poll
tasks/get against the relay until the task reaches a terminal state, then
return the final content:
- Poll interval: 5 seconds
- Max wait: 600 seconds (10 minutes)
6. Refresh keeps the catalogue live
Your agent discovers its sub-agents and MCP tools at boot. Grants change after boot. Without something in between, you’d be restarting pods every time an operator ticks a box. The SDK’screate_admin_app() mounts four endpoints at /admin:
POST /admin/configure is the agent-side half of a credential rotation: when
POST /registry/v1/agents/{agentId}/credential hands an operator a new
clientSecret, they push it in here and the runtime swaps its clients in
place — no restart, and the agent id is unchanged either way.
Swarmd derives your webhook URL from your agent card URL — it strips
everything from
/.well-known/ onwards and appends /admin/webhook. There is
no webhookUrl field anywhere in the API. Mount the admin app at /admin
(which serve() does for you) and it lines up.The same derivation is why an agent shell never receives a webhook: until
you attach a card with PUT /registry/v1/agents/{agentId}
(Setup, step 7) there is no
card URL to derive from, so there is no target to deliver to. Push starts
working the moment the card is attached.Signature verification
Every delivery carriesX-Swarmd-Timestamp and X-Swarmd-Signature. The
signature is HMAC-SHA256(secret, "{timestamp}." + raw_body), compared in
constant time, with a ±5 minute window on the timestamp to bound replay.
create_admin_app() does this for you. If you’re serving /admin/webhook
yourself, use verify_webhook() — and pass the raw body bytes, not a
re-serialised dict:
What triggers a refresh
Four event types, delivered in theX-Swarmd-Event-Type header:
SUBSCRIPTION_CHANGED, AGENT_LIFECYCLE, TENANT_LIFECYCLE,
MCP_GRANTS_CHANGED. All four mean the same thing — something about your
world changed, re-pull — so the SDK maps them all to one refresh callback.
Unknown event types are acknowledged rather than rejected, so a new event type
on the platform can’t dead-letter your agent.
Bonus: correlation IDs
One user request can fan out across five agents and a dozen MCP calls. To make that legible in the audit log, every hop carries the sameX-Correlation-Id.
CorrelationIdMiddleware captures the inbound header into a ContextVar, and
every outbound client the SDK builds copies that var onto its requests. The
first hop in a chain adopts the id the relay generated; every subsequent hop
inherits it.
You get this for free from serve(). If you’re building your own server:
Now go build
Core SDK
Use these pieces directly, in any framework.
Google ADK
Three helpers, one running ADK agent.
LangChain
The same three helpers, for LangGraph.
