Google ADK
Zero-config tracing for Google ADK (Agent Development Kit) applications.
Google ADK already emits OpenTelemetry spans for agents, model calls, tools and workflows. Rhesis turns those spans into first-class traces with a single call: translate ADK’s spans into the Rhesis semantic conventions, pull prompts and completions out of ADK’s own attributes, and synthesize agent handoff spans for the Graph View. Your agent code does not change.
Installation
Install the SDK with the google-adk extra:
Quick Start
Create the RhesisClient first (it installs the OpenTelemetry tracer provider and Rhesis exporter), then call auto_instrument. After that, use ADK exactly as you normally would.
Order matters. auto_instrument() must run after RhesisClient is created. ADK emits spans as soon as any tracer provider exists, so with no Rhesis provider active there is nothing to translate: the call returns an empty list and logs a warning rather than letting untranslated spans reach the backend, where they would be rejected.
auto_instrument() with no arguments auto-detects every installed framework (including ADK). Pass "google_adk" (or "adk") to enable it explicitly. The call returns the list of frameworks that were instrumented:
What Gets Traced
Once enabled, ADK operations are captured automatically and mapped onto the Rhesis schema:
- Agents - each agent activation becomes an
ai.agent.invokespan carryingai.agent.name - Model calls - each model call becomes one
ai.llm.invokespan with model, provider, token counts (including cached and reasoning tokens), finish reason, andai.prompt/ai.completionevents carrying the real text - Tools - tool executions become
ai.tool.invokespans withai.tool.input/ai.tool.outputevents - Handoffs - agent-to-agent delegation becomes
ai.agent.handoffspans, for both ADK multi-agent mechanisms - Conversations - the run root carries the turn’s input and output, and every turn of a conversation lands in one trace rather than one trace per turn
- Workflows and nodes -
Workflow/ node orchestration and the run root becomefunction.google_adk.*spans
Operation Mapping
| ADK span | Rhesis span name |
|---|---|
invoke_agent {name} | ai.agent.invoke |
call_llm | ai.llm.invoke |
execute_tool {tool} | ai.tool.invoke |
execute_tool transfer_to_agent | ai.agent.handoff |
invocation (schema v1 run root) | function.google_adk.invocation |
invoke_workflow {name} | function.google_adk.workflow.{name} |
invoke_node {name} | function.google_adk.node.{name} |
generate_content {model} | dropped as a duplicate — see below |
Spans that don’t match a known name fall back to function.google_adk.<name> so they always pass backend validation, with the original name preserved on the gen_ai.original_span_name attribute. ADK’s workflow spans cannot use an ai.* name because the Rhesis validator reserves workflow as a forbidden ai.* domain.
Two spans per model call
ADK wraps every model call in a call_llm span and then opens generate_content {model} inside it. Both describe the same call, so only one may become ai.llm.invoke — otherwise every model call would appear twice with double-counted tokens.
Rhesis keeps call_llm (the only one carrying prompts and completions with default settings, and the one with the fuller token breakdown) and drops the inner duplicate. Because ADK parents the execute_tool spans on that inner span, the tool spans are re-pointed at call_llm so nothing orphans:
Multi-Agent Handoffs
ADK has two ways for one agent to hand work to another, and they produce structurally different traces. Rhesis draws ai.agent.handoff edges for both, so the Graph View is connected either way.
sub_agents + transfer_to_agent — the model calls ADK’s built-in transfer_to_agent tool and control moves across. The target agent’s own span is a sibling of the caller’s, so the transfer tool span is the only place the edge is observable; Rhesis translates that span itself into ai.agent.handoff rather than adding a tool row for it.
AgentTool — a whole agent registered as a tool. Rhesis keeps the ai.tool.invoke span (an AgentTool call genuinely is a tool call) and synthesizes an extra ai.agent.handoff span alongside it.
Each handoff span carries ai.agent.handoff.from and ai.agent.handoff.to. See the Multi-Agent Tracing page for the full span reference.
The deprecated SequentialAgent / LoopAgent / ParallelAgent classes also produce handoff edges, from the orchestrator to each step, because their step spans nest under the orchestrator’s own agent span. The newer Workflow API nests its steps under invoke_workflow instead, so its structure shows as span nesting rather than as edges.
Conversation Grouping
ADK opens its own root span for every turn, and OpenTelemetry gives each one a fresh trace id. Rhesis instead shows a conversation as a single trace whose roots are the individual turns. To get that, set a conversation id around each turn — the integration both stamps the run root as a conversation turn root and puts every turn on one trace:
That is the whole integration: no per-turn span to open, nothing to thread through your own loop. The conversation’s trace is the first turn’s own trace; later turns are moved onto it. The first turn is never moved, so any trace id already recorded for it still resolves.
This applies only to a standalone ADK run — a terminal chat, a script, anything where ADK’s own run span is the top of the trace and no part of Rhesis has published an id for it. Behind @endpoint or @observe, or anywhere the platform supplied a conversation trace id, Rhesis owns the trace and already joins the turns itself, so the integration leaves the ids untouched. That id is what the platform returns as the endpoint result, persists as the conversation’s trace id for the next turn, and writes its own turn record to — including the reply. Moving spans off it would separate the agent’s spans from their own reply.
Turn labelling and turn joining need different things. With no conversation id set, the turn root is still labelled with the ADK session id, so an app that reuses one session across turns is grouped in the Conversation tab. It is not joined into one trace, though: the target trace has to be known when ADK creates the run span, and ADK assigns no span attributes at that point, so only the conversation id from rhesis.telemetry.context is readable that early. Set it explicitly if you want one trace per conversation.
When the Reply Is Not the Model’s Last Message
The turn’s recorded reply is extracted from ADK’s model spans, which is right when your agent answers with model text. It cannot work when the reply comes from somewhere else — a terminal tool’s output, a template, a branch in your own code. None of that reaches an llm_response attribute, and ADK’s run span has already ended by the time your code holds the reply, so those turns show an empty reply in the Conversation tab.
If your agent answers that way, own the turn instead — see Owning the Turn:
The ADK run then nests under that span, which becomes the turn root and carries the reply your user actually saw. This is what agents/reg-advisor/ does, because its reply can come from a terminal tool or a refusal composed in Python.
A step that ADK blocks on its own max_llm_calls budget still opens a call_llm span, so it still becomes an ai.llm.invoke — with no prompt or completion and near-zero duration. Blocked steps therefore count towards the model calls you see on a trace.
Telemetry Schema Versions
ADK supports two telemetry formats, selected with ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN (1 or 2; the default is 2 on Vertex Agent Engine and 1 elsewhere). Both are supported, and Rhesis does not force either — the only difference is which span is the run root (invocation in v1, invoke_workflow {entrypoint} in v2) and the integration decides that structurally rather than by name.
Content Capture and Privacy
By default, the integration captures message content (prompts, completions, tool arguments and results) so traces show what each agent actually sent and received. It reads this from ADK’s own span attributes, which record it out of the box.
To omit message content — for example in production or regulated environments — set RHESIS_DISABLE_CONTENT_CAPTURE before initializing:
Accepted truthy values: 1, true, yes, on (case-insensitive). When disabled, spans are still exported with structural metadata (model, provider, token counts, agent names, operation types), but no prompt, completion, or tool input/output payloads are recorded.
The integration switches off all three of ADK’s overlapping content knobs, including ADK_TELEMETRY_IGNORE_RUN_CONFIG — without that one a per-request RunConfig.telemetry would outrank the environment and put prompts back on the spans. It never touches OTEL_SEMCONV_STABILITY_OPT_IN, which is a global OpenTelemetry switch that would change the behaviour of unrelated instrumentation in the same process.
Span Verbosity
To keep traces readable, the integration drops ADK infrastructure spans that carry no agent, model or tool payload: send_data, create_cache, handle_context_caching, compact_events, and execute_tool (merged) (a duplicate summary ADK emits after parallel tool calls, whose payload is placeholder text).
To keep every span, set:
They then appear under function.google_adk.*, never claiming an ai.* semantic.
Combining with Decorators
Auto-instrumentation composes with @endpoint and @observe. Wrap your entry point with @endpoint so Rhesis can run tests against it; ADK spans created inside are nested under the endpoint span, which then owns the turn-root semantics:
Use Runner.run_async, not Runner.run. The synchronous run executes the agent on a fresh thread with fresh context variables, so its spans start a new trace instead of nesting under the enclosing endpoint span, and the conversation id set via rhesis.telemetry.context is invisible to them.
Do not call google.adk.telemetry.setup.maybe_set_otel_providers(). It installs its own tracer, logger and meter providers, replacing the ones Rhesis configured, and spans will no longer reach Rhesis.
Troubleshooting
auto_instrument("google_adk") returns an empty list. Either google-adk is not installed, or no Rhesis tracer provider is active. Create RhesisClient before calling auto_instrument, and check the log — the integration says which of the two it was.
Spans are missing from the trace. ADK spans nest under whichever span is active when the run starts. If you are using the synchronous Runner.run, switch to run_async (see the warning above).
No prompts or completions. Check whether RHESIS_DISABLE_CONTENT_CAPTURE is set, or whether the app sets ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false itself.
google-adk[otel-gcp] is installed. That extra brings opentelemetry-instrumentation-google-genai, which makes ADK hand the model span to a different instrumentation scope. Traces stay complete (call_llm is still ADK’s own and still becomes ai.llm.invoke), but that library’s spans are not translated. The extra is not needed for Rhesis tracing.
Worked Example: Reg-Advisor
The repository ships a runnable multi-agent ADK demo — a regulatory coordinator that delegates to three specialists via AgentTool — wired to produce agent, LLM, tool and handoff traces in Rhesis. See agents/reg-advisor and its architecture notes .
Each of its entry points has a traced twin that wraps the plain one, which is a pattern worth copying: the untraced path stays the one under test, and turning tracing on never means editing the code that does the work.
Testing ADK Agents
Beyond tracing, Rhesis can run autonomous multi-turn conversation tests against an ADK agent with the GoogleADKTarget — see Conversation Simulation.
Disabling
Turn instrumentation off (restores the original exporter and any environment variable the integration set) with:
ADK itself has no instrumentation switch, so it keeps emitting spans after disable(). Those spans then pass through untranslated — the same state as never having enabled the integration.
Related:
- Auto-Instrumentation - zero-config tracing for every supported framework
- Multi-Agent Tracing - agent and handoff spans
- Conversation Tracing - grouping turns into conversations
- Setup - initial configuration