Skip to content

Trace the effective tools and handoffs for each model turn #4626

Description

@dgenio

Please read this first

  • Have you read the docs? Yes.
  • Have you searched for related issues and PRs? Yes.

Related but distinct:

This issue is narrower: preserve the effective model-visible capability set for each individual model turn in tracing.

Describe the feature

The SDK already resolves the effective tools and handoffs immediately before a model call and writes them to the current AgentSpanData:

https://github.com/openai/openai-agents-python/blob/main/src/agents/run_internal/run_loop.py#L2098-L2114

That is useful, but one agent span can cover multiple model turns. AgentSpanData.tools and .handoffs are mutable fields, so if runtime policy changes the effective capability set between turns, each update replaces the previous values.

As a result, after the run it may be impossible to answer a production-debugging question precisely:

What tools and handoffs did this model invocation actually see?

This matters when FunctionTool.is_enabled, Handoff.is_enabled, or MCP tool_filter depend on request/run context such as tenant, workflow state, feature flags, experiment allocation, or state changed by an earlier tool call.

Why TurnSpanData looks like a natural fit

Current main already has optional task/turn tracing. TurnSpanData represents one agent loop turn, but currently carries only the turn number, agent name, usage, and metadata:

https://github.com/openai/openai-agents-python/blob/main/src/agents/tracing/span_data.py

Conceptually, the trace could preserve:

agent span: Agent A
  turn 1: tools=[search, lookup] handoffs=[billing]
  turn 2: tools=[lookup, submit] handoffs=[]

The agent span can keep its current behavior for compatibility; when turn spans are enabled, each turn span could additionally snapshot the already-resolved tool and handoff names for that model invocation.

I am not attached to TurnSpanData specifically if maintainers prefer the generation/response span or another existing model-invocation boundary. The important property is that the snapshot is associated with the exact model invocation rather than being overwritten later on a longer-lived agent span.

Suggested semantics

  1. Record the capability set after dynamic enablement/filtering and tool-name collision resolution, i.e. the same effective names that are about to be sent to the model.
  2. Preserve one immutable snapshot per model turn/invocation.
  3. Record names/identifiers only, matching the information already emitted on AgentSpanData; do not add tool arguments, schemas, credentials, or authorization data.
  4. Keep this explicitly observational: a visible tool or handoff does not imply that a particular invocation is authorized.
  5. Preserve existing tracing behavior when task/turn spans are disabled.

Reproduction-shaped case

A single agent runs for two turns. A dynamic is_enabled policy exposes draft_invoice initially. After the first tool call changes workflow state, the next model turn exposes send_invoice instead.

Expected trace evidence:

turn 1 -> [draft_invoice]
turn 2 -> [send_invoice]

The final AgentSpanData.tools may still contain the latest set, but the trace retains both historical snapshots.

A regression test could use one agent with a context-dependent tool/handoff predicate, force two model turns, and assert that the exported turn spans contain two distinct effective capability sets while the existing agent-span behavior remains unchanged.

Non-goals

  • no new capability-policy abstraction;
  • no authorization framework;
  • no tool registry;
  • no raw tool schemas/arguments in tracing;
  • no change to how capabilities are selected.

This is only about preserving the capability surface that the SDK has already resolved for each model invocation.

If this fits the tracing model, I would be happy to help with a focused implementation/tests around the preferred span.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions