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
LLMspan) with model, tokens, cost and time to first token. Responses API calls are all namedResponse. - Tool calls. Each tool you wrap with observe becomes a
TOOLspan. Without an explicit name it is calledexecute_toolplus the tool name, for exampleexecute_tool account_status, andgen_ai.tool.namecarries the function’s name. - Nesting. The
TOOLspan parents under the run through ambient context. If you wrap the entrypoint aroundRunner.run()as anAGENTspan, the tool spans and generations all sit beneath it. - Which agent ran the tool. A
TOOLspan also carriesgen_ai.agent.name, set to the innermost enclosingAGENTspan opened withobserve, 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
- 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- Wrap the tool body and the entrypoint. Put
@function_tooloutermost andobserveon 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)-
Set the tool’s identity when it should differ from the function name. Pass
tool_nametoobserveto controlgen_ai.tool.name. If you pass only a spannameon aTOOLspan, that name doubles as the tool name and the SDK warns, so settool_namewhen the two differ. -
Control what each tool span records.
observestores the arguments as JSON ininput.valueand the return value inoutput.value, truncated at 32 KB. Passcapture_input=Falseorcapture_output=Falsefor a tool that handles sensitive data. -
Run one request that calls a tool, then open the trace in the console and look for the root
AGENTspan, theLLMgenerations and your namedTOOLspans.
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.typeset 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
observewrapper per agent entry, or use the framework’sRunHooksto 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.