Google ADK on Swarmd
swarmd-google-adk wraps Google ADK so
an LlmAgent can live on Swarmd: authenticated, discovering its sub-agents and
MCP tools, served over A2A, and refreshing itself when grants change.
Three helpers do all of it.
Assumes you’ve finished Setup. Ten minutes on
Core concepts will make the rest of this page obvious
rather than magical — but it isn’t required.
Step 1 — Install
swarmd-sdk, google-adk[a2a,extensions], a2a-sdk, mcp, and
litellm. Use a virtualenv — the version bounds are
deliberately tight.
Step 2 — Add your LLM to .env
On top of the credentials from Setup, pick a model
provider:
- Through the Swarmd gateway (recommended)
- Direct to OpenAI
.env
Step 3 — Write the agent
The whole file:main.py
Your docstring is the tool spec. ADK sends the function’s name, signature,
and docstring to the model as the tool definition. A vague docstring is a tool
the model won’t use correctly. Describe the arguments.
Step 4 — Run it
Seeing 'runtime not configured — standalone mode' instead?
Seeing 'runtime not configured — standalone mode' instead?
create_runtime() only configures when all four of SWARMD_AGENT_ID,
SWARMD_CLIENT_SECRET, SWARMD_BASE_URL and SWARMD_TOKEN_URL are set. Miss
one and you get standalone mode: the agent runs, but discovers nothing.It’s a deliberate escape hatch for offline development, and the single most
common cause of “why is my catalogue empty?”. Check all four.Step 5 — Talk to it
Dissected: create_runtime()
SwarmDRuntime and, if the four platform env vars are present,
calls runtime.configure(...) with them — plus SWARMD_WEBHOOK_SECRET if set.
That single call builds:
- A
SwarmDClientfor registry calls. - An
McpClientfor MCP discovery and invocation. - Two token manager proxies —
swarmd:apifor platform calls,mcp:callfor the MCP relay. See two tokens, not one.
Dissected: create_llm_agent()
LlmAgent — pass it around, wrap it, inspect it
like any other. Five things happen on the way there.
1
Picks the LLM provider
Checked in order:
SWARMD_LLM_GATEWAY_IDset → LiteLLM is pointed at{SWARMD_BASE_URL}/llm/v1/{gateway_id}, wrapped in anAsyncOpenAIwhose HTTP client mints a fresh Swarmd bearer on every request and evicts the cached token on a401. No provider key needed.OPENAI_API_KEYset → LiteLLM goes direct to OpenAI.- Neither →
ValueError.
The degraded-boot case
The degraded-boot case
If
SWARMD_LLM_GATEWAY_ID is set but the runtime is unconfigured — the
credentials haven’t been issued yet on a brand-new agent’s first deploy — the
helper prints a WARN and builds a placeholder LlmAgent instead of crashing.That’s on purpose: the pod boots, the readiness probe passes, registry
discovery succeeds, and the agent serves its card. Live LLM calls would fail
in that state, but a fresh agent registers as PRIVATE and shouldn’t be taking
traffic yet. The next deploy with real credentials builds a properly routed
agent.Without that guard the pod would CrashLoopBackOff, Helm would roll back, and
the deploy would go red — over an agent that didn’t need an LLM call to serve
its card.2
Discovers sub-agents
Calls
fetch_remote_agents(runtime), which pulls
GET /registry/v1/agents/{id}/subscriptions and wraps each result in a
PollingRemoteA2aAgent.The polling is the point. ADK’s stock RemoteA2aAgent returns whatever the
sub-agent replied with — including a non-terminal working state, which the
parent LLM can do nothing useful with. PollingRemoteA2aAgent detects
non-terminal states and polls tasks/get through the relay (every 5s, up to
600s) until the task is terminal, then surfaces the real answer as a normal
ADK Event.Each wrapped agent gets an authenticated ClientFactory: a JSON-RPC transport
with a token-injecting interceptor and a response hook that adopts the relay’s
correlation ID for the rest of the chain.3
Discovers MCP servers
Calls
fetch_mcp_tools(runtime), which lists your grants and builds one ADK
McpToolset per server, pointed at the relay’s proxy
(/relay/v1/mcp-servers/{id}/mcp).Two details worth knowing:- Auth goes through a custom
httpx_client_factory, not ADK’sheader_provider. ADK skipsheader_providerwhen the context isNone, which is exactly the case for the catalogue-build request it issues at boot — so the boot request would go out unauthenticated, the relay would401, and the toolset would be silently dropped. The client factory fires on every request, including that one. - Tools are namespaced with
mcp_tool_namespace(server)so a server called"GitLab - swarmd.ai"can’t produce a tool name OpenAI rejects with400.
4
Applies sampling config
If
generate_content_config is None and OPENAI_TEMPERATURE is set, it
becomes GenerateContentConfig(temperature=...). Pass
generate_content_config= explicitly to control the rest.tool_choice= forwards OpenAI’s tool-calling policy to every LLM turn:5
Composes the catalogue
tools=list(your_tools) + mcp_toolsets, sub_agents=remote_agents.Discovery failures here are non-fatal and logged — an agent that can’t reach
the registry at boot still starts with its local tools rather than refusing to
run.Dissected: serve()
1
Mounts the A2A protocol
Builds the agent card with ADK’s
AgentCardBuilder, serves it at
/.well-known/agent-card.json, and attaches A2AStarletteApplication —
which owns message/send, message/stream, tasks/get, tasks/cancel and
the push-notification config endpoints. A2aAgentExecutor routes those calls
into your LlmAgent.2
Persists tasks and sessions
SQLite at
/tmp/{agent_name}_tasks.db and /tmp/{agent_name}_sessions.db.3
Adds correlation propagation
CorrelationIdMiddleware captures inbound X-Correlation-Id into a
ContextVar; every outbound client copies it forward. One user request, one
trace id, across every hop.4
Mounts /admin
POST /admin/configure, POST /admin/refresh, POST /admin/webhook,
GET /admin/status./admin/webhook is the platform-driven kick: Swarmd POSTs a signed payload
when a subscription, MCP grant, or lifecycle event fires; the SDK verifies the
HMAC against SWARMD_WEBHOOK_SECRET and calls the refresh handler.Swarmd derives the webhook URL from your agent card URL by replacing
/.well-known/agent-card.json with /admin/webhook — mounting at /admin
is what makes that line up.5
Refreshes in place
The refresh callback re-runs
fetch_remote_agents and fetch_mcp_tools, then
swaps agent.sub_agents and agent.tools.Your local Python tools survive: serve() snapshots them at startup by
filtering out anything that’s a BaseToolset, and rebuilds the combined list
from that snapshot each time. No restart when subscriptions change.6
Starts uvicorn
On
HOST:PORT (default 0.0.0.0:8080) at LOG_LEVEL (default info).Going off the rails
Everything is exported, so you can drop a level wherever you need to:Custom polling interval or wait cap
Custom polling interval or wait cap
a2a_client_factory, it builds an authenticated one from the active
runtime.Hand-built LlmAgent
Hand-built LlmAgent
Use the discovery functions for the lists, then assemble the agent yourself
with whatever callbacks, structured output, or planner config you need:
Your own server, keeping the admin surface
Your own server, keeping the admin surface
create_admin_app
needs from you.Next
Migrating an existing ADK agent
Diff-shaped guide: what to delete from your current
main.py.Reference
Every helper, argument, and environment variable.
LangChain
The same three helpers, for LangGraph.
Troubleshooting
Empty catalogues, 403s on MCP, webhooks that never fire.
