Skip to Content
Rius

How do I trace tool calls in the OpenAI Agents SDK?

Rius traces tool calls in the OpenAI Agents SDK when you wrap each tool body with observe and kind TOOL, which gives each call its own span under the run. The framework runs tools internally, so Rius does not see them without that one decorator. Model calls arrive on their own as generations with tokens and cost, and the tool spans nest next to them under your root agent span. This works in the Python SDK.

How it works

The OpenAI Agents SDK drives the OpenAI client underneath, so the openai instrumentation captures every model call without framework-specific code. Tool executions happen inside the framework, and auto-instrumentation only sees the client calls that come out of it. A tool therefore needs a wrapper of your own.

  • Model calls. Each one is a generation (an LLM span) with model, tokens, cost and time to first token. Responses API calls are all named Response.
  • Tool calls. Each tool you wrap with observe becomes a TOOL span. Without an explicit name it is called execute_tool plus the tool name, for example execute_tool account_status, and gen_ai.tool.name carries the function’s name.
  • Nesting. The TOOL span parents under the run through ambient context. If you wrap the entrypoint around Runner.run() as an AGENT span, the tool spans and generations all sit beneath it.
  • Which agent ran the tool. A TOOL span also carries gen_ai.agent.name, set to the innermost enclosing AGENT span opened with observe, or to the configured agent name.
  • Handoffs. A handoff produces no span of its own. You see it as consecutive generations that belong to different agents.

Set it up

  1. Install the SDK with the OpenAI extra and the framework. Full setup is on the OpenAI Agents SDK integration page.
pip install "glassflow-rius[openai]" openai-agents
  1. Wrap the tool body and the entrypoint. Put @function_tool outermost and observe on the function body, because the framework reads the function signature for its tool schema. Turning off the framework’s own tracing keeps Rius as the single telemetry path.
import rius from agents import Agent, Runner, function_tool, set_tracing_disabled from rius import SpanKind rius.init(api_key="ri_...", service_name="my-agent") set_tracing_disabled(True) @function_tool @rius.observe(kind=SpanKind.TOOL) def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) support_agent = Agent( name="support_agent", instructions="Answer support questions; check accounts with the tool.", tools=[account_status], model="gpt-5-mini", ) @rius.observe(name="handle-ticket", kind=SpanKind.AGENT) def handle_ticket(ticket: str) -> str: result = Runner.run_sync(support_agent, ticket) return str(result.final_output)
  1. Set the tool’s identity when it should differ from the function name. Pass tool_name to observe to control gen_ai.tool.name. If you pass only a span name on a TOOL span, that name doubles as the tool name and the SDK warns, so set tool_name when the two differ.

  2. Control what each tool span records. observe stores the arguments as JSON in input.value and the return value in output.value, truncated at 32 KB. Pass capture_input=False or capture_output=False for a tool that handles sensitive data.

  3. Run one request that calls a tool, then open the trace in the console and look for the root AGENT span, the LLM generations and your named TOOL spans.

What you get

A run that calls a tool looks like this:

handle-ticket AGENT 9.8s ├─ Response LLM 2.3s triage decides to hand off ├─ Response LLM 2.8s billing agent decides to call the tool ├─ execute_tool account_status TOOL 0ms └─ Response LLM 4.6s billing agent answers
  • A separate span for every tool call, with its input, output and duration, sitting between the generation that asked for it and the generation that used the result.
  • Tool failures recorded on the span with ERROR status and error.type set to the exception class. The exception is always re-raised, so your agent’s behavior does not change.
  • Cost and tokens on every generation, so you can see what each step around a tool call spent.
  • Grouped turns, if you scope each run with rius.session() and reuse your conversation or ticket id.

Streamed runs carry token counts only when usage is enabled on the underlying client. The Integrations overview covers that note and the rest of the support matrix.

When something else fits

  • If handoff boundaries matter, add an observe wrapper per agent entry, or use the framework’s RunHooks to open spans at agent boundaries, since handoffs get no span by default.
  • If you work in TypeScript, there is no OpenAI Agents SDK integration today. Trace those calls with manual spans and generations from the tracing guide.
  • If agent names contain hyphens, switch to underscores. The framework rewrites hyphens in the generated handoff tool name and warns on every run.

Still stuck? Get help

Last updated on