Docs

    Frameworks

    Strands Agents

    Trace Strands Agents through its OpenTelemetry spans: init() installs the tracer provider Strands records with, and its agent, model and tool spans are typed by the GenAI conventions, with a tested tutorial.

    Strands Agents records its agent loop with OpenTelemetry, on the GenAI semantic conventions. EvalKit has no Strands patch of its own: init() installs the OpenTelemetry tracer provider Strands records with, and the SDK types each Strands span by its gen_ai.operation.name. This page builds a support agent with two tools in Python, runs it once, and reads the trace.

    What you get

    • invoke_agent is an agent span named after the agent, such as invoke_agent support-bot, with the user's message as its prompt and the agent's reply as its completion. Its usage totals the model calls under it, so it carries no tokens of its own.
    • execute_tool is a tool_call span named after the tool, with tool.arguments and tool.result. A tool whose gen_ai.tool.status is error is an error span.
    • execute_event_loop_cycle is an internal span: one per turn of the agent loop, never a tool or a model call.
    • The model call Strands records, chat, is an internal span here, because EvalKit records the same request itself, through the OpenAI client, as the chat.completions.create llm_call inside it. The call is counted once.
    • The OpenAI client's HTTP requests show as POST spans. Strands' OpenAI model asks for usage in its streams, so the streamed model calls carry token counts.
    text
    invoke_agent support-bot          agent
      execute_event_loop_cycle        internal
        chat                          internal    Strands' record of the model call
          chat.completions.create     llm_call    tokens 65 in, 34 out
          POST                        http_call
        lookup_order                  tool_call
      execute_event_loop_cycle        internal
        chat                          internal
          chat.completions.create     llm_call
          POST                        http_call
        search_policy                 tool_call
      execute_event_loop_cycle        internal
        chat                          internal
          chat.completions.create     llm_call
          POST                        http_call

    Install

    bash
    pip install syntropylabs-evalkit "strands-agents[openai]"

    Set up

    python
    import os
    
    import evalkit
    
    evalkit.init(subscription_key=os.environ["EVALKIT_SUBSCRIPTION_KEY"], service_name="my-agent")
    
    # Import Strands after init(), so the tracer provider it records with is the one init() installed.
    from strands import Agent

    Where init() goes

    • Call init() once, before Strands is imported. With no tracer provider installed yet, init() installs one and sets it as OpenTelemetry's global provider, which Strands records with.
    • If you set up OpenTelemetry first, with a provider of the OpenTelemetry SDK, init() adds EvalKit's exporter to that provider instead, and your own exporters keep their spans. See OpenTelemetry and distributed tracing.
    • Strands can also send its spans without the SDK, with its own OTLP exporter pointed at the trace service's OTLP receiver. Use one path, not both, so each span arrives once.

    Tutorial: a support agent with two tools

    The agent looks an order up and checks the refund policy before it answers, so one question makes three model calls and two tool calls. Its question and tools differ from the other framework pages because the test run used a model that streams its tool calls, as Strands reads them.

    1. 1

      Install the packages

      Run the install line above.

    2. 2

      Set the keys

      Export the environment key of your Environment and your OpenAI key.

    3. 3

      Save the code

      Save it as agent.py.

    4. 4

      Run it

      python agent.py prints the agent's reply. The SDK sends the queued spans when the script exits.

    5. 5

      Open the trace

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

    Shell
    export EVALKIT_SUBSCRIPTION_KEY=tk_live_...   # Settings → This project → Environments → Copy key
    export OPENAI_API_KEY=sk-...
    agent.py
    import os
    
    import evalkit
    
    # init() runs before Strands is imported: it installs the OpenTelemetry tracer provider that
    # Strands records its agent, model and tool spans with, and sends those spans to EvalKit.
    evalkit.init(
        subscription_key=os.environ["EVALKIT_SUBSCRIPTION_KEY"],
        service_name="strands-support",
        environment="development",
    )
    
    from strands import Agent, tool  # noqa: E402
    from strands.models.openai import OpenAIModel  # noqa: E402
    
    
    @tool
    def lookup_order(order_id: str) -> dict:
        """Look up an order by its id."""
        return {"order_id": order_id, "item": "Vortex blender", "price": 89.99, "delivered": "2 October"}
    
    
    @tool
    def search_policy(query: str) -> str:
        """Search the refund policy."""
        return "Items damaged on arrival are refundable within 30 days."
    
    
    agent = Agent(
        name="support-bot",  # the agent span is named after it, and the trace lists under it
        model=OpenAIModel(model_id="gpt-4o-mini"),  # reads OPENAI_API_KEY
        tools=[lookup_order, search_policy],
        system_prompt="You are a support agent for an online shop. Look orders up and check the policy before you answer.",
        callback_handler=None,  # no console streaming; the spans carry the reply
    )
    
    with evalkit.session("conv_strands_1", user_id="u_42"):  # groups the turns of one conversation
        result = agent("My blender from order 48213 arrived cracked. I want my money back.")
    print(result)
    bash
    python agent.py

    It prints the agent's reply, for example:

    text
    Order 48213 (Vortex blender, $89.99) was delivered on 2 October. Items damaged on arrival are refundable within 30 days, so you qualify. Shall I refund $89.99 to the card you paid with?

    Tested with syntropylabs-evalkit 0.3.4, strands-agents 1.60 and openai 2.54: one trace of 15 spans.

    See the trace

    • The waterfall shows the tree under What you get.
    • Click invoke_agent support-bot to read the question and the reply, a chat.completions.create span for the messages and tokens, and lookup_order or search_policy for the tool's arguments and result.
    • Traces → Sessions lists conv_strands_1, and Traces → Agents lists support-bot.

    Sessions, users and agents

    • Run the agent inside with evalkit.session(session_id, user_id=...), as the tutorial does. Every Strands span started inside the block carries the session and user, because the SDK records the scope in force when each span starts.
    • The agent's name names the invoke_agent span and sets gen_ai.agent.name on it, and a trace's agent is its root span's name, so the trace lists under support-bot with no set_agent call.
    • EvalKit also reads the ids from span attributes: session.id, conversation.id or gen_ai.conversation.id for the session, and user.id or enduser.id for the user.

    Pitfalls

    • The chat spans have no tokens. They are Strands' records of requests the SDK recorded itself as chat.completions.create, so they are kept as internal spans and each call is counted once. Set EVALKIT_COUNT_FOREIGN_LLM=1 only when Strands is the one recording its model calls, so both copies are counted.
    • Two exports of the same spans. If you also point Strands' OTLP exporter at the trace service, each span arrives twice. Use init() or the exporter, not both.
    • 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.

    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.