Docs

    Frameworks

    Google ADK

    Trace Google Agent Development Kit agents in Python: each agent run, its model calls and its tool calls in one trace, with ADK's session id as the conversation.

    With init() called before ADK is imported, every agent run is a span of its own, and each model call and tool call it makes nests underneath. There is nothing to add to the agent.

    What you get

    • A run_agent span for each run of an agent, named in gen_ai.agent.name after the agent's name, with the user's message in function.args, and the reply and the tool calls of the run in function.result. The trace lists under that agent in Traces → Agents.
    • A tool_call span for each tool the agent runs, named after the tool, with gen_ai.tool.call.arguments, gen_ai.tool.call.result and gen_ai.tool.description. The SDK adds them through ADK's before and after tool callbacks: its own runs first before the tool and last after it, so callbacks of your own keep working.
    • An llm_call span for each model call, from the integration of the client ADK calls the model with: litellm.acompletion when the agent's model is a LiteLlm, as in the tutorial. A Gemini model goes through the Google Gen AI client, which the SDK traces too; this guide did not run one.
    • ADK's own OpenTelemetry spans are left out, because they repeat what these spans record.
    The trace of one turn
    run_agent                 function_call   gen_ai.agent.name = support_bot
      litellm.acompletion     llm_call        the model asks for lookup_order
      lookup_order            tool_call       arguments and result
      litellm.acompletion     llm_call        the model answers
    POST                      http_call       the model client's request, at the top of the trace
    POST                      http_call

    Install

    bash
    pip install syntropylabs-evalkit google-adk litellm

    Set up

    python
    import os
    
    import evalkit
    
    evalkit.init(subscription_key=os.environ["EVALKIT_SUBSCRIPTION_KEY"], service_name="adk-support")
    
    from google.adk.agents import LlmAgent  # import ADK after init()

    Where init() goes

    • Before google.adk is imported. init() patches ADK's agent class and the model clients, so call it at the top of the module that starts your app, then import ADK.
    • Create the runner after init(). The patches sit on ADK's classes, so agents built later are covered without any change.

    Tutorial: a support agent with one tool

    1. 1

      Install and set the keys

      Install the packages above and export the environment key and your model key. The agent reaches OpenAI through LiteLLM, which reads OPENAI_API_KEY.

    2. 2

      Save the agent

      Save agent.py below. It runs one turn: the model asks for lookup_order, ADK runs the tool, and the model answers.

    3. 3

      Run it

      Run python agent.py. It prints the answer, and the SDK sends the spans when the script exits.

    4. 4

      Open the trace

      In Traces, open the newest trace of the adk-support service.

    bash
    export EVALKIT_SUBSCRIPTION_KEY=tk_live_...   # Settings → This project → Environments → Copy key
    export OPENAI_API_KEY=sk-...
    agent.py
    import asyncio
    import os
    
    import evalkit
    
    # init() runs before ADK is imported, so its agents, tool callbacks and model clients are patched.
    evalkit.init(
        subscription_key=os.environ["EVALKIT_SUBSCRIPTION_KEY"],
        service_name="adk-support",
        environment="development",
    )
    
    from google.adk.agents import LlmAgent  # noqa: E402
    from google.adk.models.lite_llm import LiteLlm  # noqa: E402
    from google.adk.runners import InMemoryRunner  # noqa: E402
    from google.genai import types  # noqa: E402
    
    
    def lookup_order(query: str) -> dict:
        """Find an order from the customer's words."""
        return {"order_id": "1001", "status": "shipped", "arrives": "Thursday"}
    
    
    agent = LlmAgent(
        name="support_bot",  # the run_agent span and the trace's agent take this name
        model=LiteLlm(model="openai/gpt-4o-mini"),  # through LiteLLM, which reads OPENAI_API_KEY
        instruction="You are a support agent for an online shop. Use lookup_order for questions about an order.",
        tools=[lookup_order],
    )
    runner = InMemoryRunner(agent=agent, app_name="support")
    
    
    async def main() -> None:
        session = await runner.session_service.create_session(app_name="support", user_id="u_42")
        message = types.Content(role="user", parts=[types.Part(text="Where is my order 1001?")])
        with evalkit.session(session.id, user_id="u_42"):  # ADK's own session id groups the turns
            async for event in runner.run_async(user_id="u_42", session_id=session.id, new_message=message):
                if event.is_final_response() and event.content and event.content.parts:
                    print(event.content.parts[0].text)
    
    
    asyncio.run(main())
    bash
    python agent.py
    # Order 1001 shipped yesterday and arrives on Thursday.

    Tested with google-adk 2.12, LiteLLM 1.105 and Python 3.13.

    See the trace

    • The trace's root is run_agent, and its agent is support_bot. Click lookup_order to read the arguments the model chose and what the tool returned.
    • Traces → Sessions lists the conversation under ADK's session id, with user u_42.
    • A second trace holds one GET span: LiteLLM downloads its model price list when it is imported. It belongs to no session.

    Sessions, users and agents

    • Open evalkit.session(session.id, user_id=...) around run_async, as the tutorial does, with ADK's own session id, so the turns of one ADK session are one conversation in Traces → Sessions.
    • The agent name comes from the agent's name. A sub-agent run is a run_agent span of its own, named after that sub-agent.

    Pitfalls

    • The model calls are missing. The client the model goes through was imported before init(), or it is not one the SDK traces. Import ADK after init(); see What is traced automatically for the clients covered.
    • Log lines become spans; your log output stays as it was. Each record that propagates to the root logger is recorded as a log span, as a handler there would record it: at the level of the logger that wrote it, which is the root level, WARNING, unless that logger or your app sets another. init() changes no handler, formatter or level, so your console output stays as it was. Every record carries trace_id and span_id, so a format of your own can show them, as in %(asctime)s %(levelname)s [%(trace_id)s] %(name)s: %(message)s. On 0.3.4 and earlier, init() changes the format of the root logger's console handlers and sets the root level to INFO; configure logging after init() there.
    • TypeScript. The TypeScript SDK patches @google/adk too (its Runner and agents), but this guide covers Python only.

    Safe by design

    • Spans are queued in memory and sent from a background thread, so a request never waits for the trace service.
    • If the trace service is down or unreachable, your app answers as before. The SDK retries each export 3 times, logs evalkit: export failed after 3 retries as a warning and drops those spans.
    • If the key is wrong, the trace service refuses the spans. After 3 refusals in a row the SDK logs one warning and stops exporting until the process restarts; your app keeps answering.
    • A streamed model reply reaches your code chunk by chunk, unchanged. The SDK reads the same chunks on the side and writes the span when the stream ends.
    • Tracing never breaks the call it records. An error inside the SDK is logged at debug level and the call runs as if untraced, and an exception from the provider reaches your code unchanged.
    • A failed export never raises in your code, and evalkit.flush() returns normally too. At exit the SDK sends what is queued within 3 seconds in all, whether or not the trace service answers, so an outage never holds up a deploy.

    Next steps

    EvalKit is built by SyntropyLabs. Published on PyPI and npm.