How do I trace a research agent built with the OpenAI Agents SDK?
Rius traces a research agent built with the OpenAI Agents SDK by capturing model calls automatically and adding observe wrappers for the run, each stage and each tool. Every model call arrives as a generation with model, tokens and cost. A retrieval span records the index you searched and the documents it returned. Setting a session and a user groups the traces by conversation and by person.
How it works
The framework drives the OpenAI client underneath, so the openai integration captures its model calls with no framework-specific code. Each call becomes an LLM generation. The agent loop, tool runs and handoffs happen inside the framework, so you add that structure yourself. The OpenAI Agents SDK integration is Python only. See OpenAI Agents SDK for its full coverage, and Integrations for the other frameworks.
A research agent usually needs four things on top of the model calls:
- A run root. Wrap the entrypoint with
observeandSpanKind.AGENT. Every other span nests under it. - A span per stage. Wrap each stage, such as planning and investigating, with a bare
observe. Each becomes aCHAINspan named after the function. - A retrieval span. Open a
RETRIEVERspan around each search withdata_source_idandtop_k. It is namedretrievalplus the data source id, andset_retrieved_documentsrecords document ids and scores, not text. - A
TOOLspan per tool. UseobservewithSpanKind.TOOLon the tool body. The Tool kind in the Spans tab and the Tool Loop alert only see spans of that kind, and a bareobservemakes aCHAINspan they miss.
The framework’s own tracing uploads to the OpenAI platform and is independent of OpenTelemetry. Call set_tracing_disabled(True) if Rius should be the single telemetry path.
Set it up
-
Install the SDK with the
openaiextra and the framework. OpenAI Agents SDK covers setup in full.pip install "glassflow-rius[openai]" openai-agents -
Initialize Rius and turn off the framework’s own tracing. Then write the search tool with a retrieval span inside it. Put
@function_tooloutermost andobserveon the function body, because the framework reads the function signature for its tool schema.import rius from agents import Agent, Runner, function_tool, set_tracing_disabled from rius import SpanKind rius.init(api_key="ri_...", service_name="research-agent") set_tracing_disabled(True) @function_tool @rius.observe(kind=SpanKind.TOOL) def search_papers(query: str) -> str: """Search the paper index.""" with rius.start_as_current_span( kind=SpanKind.RETRIEVER, data_source_id="papers", top_k=5 ) as obs: obs.set_input(query) docs = search(query) obs.set_retrieved_documents([{"id": d.id, "score": d.score} for d in docs]) return format_docs(docs) -
Define the agents and wrap each stage and the run. Use underscores in agent names, because hyphens are rewritten in the generated handoff tool name and the framework warns on every run.
planner = Agent(name="planner", instructions="Break the question into sub-questions.", model="gpt-5-mini") researcher = Agent( name="researcher", instructions="Answer with the search tool and cite sources.", tools=[search_papers], model="gpt-5-mini", ) @rius.observe def plan(question: str) -> str: return str(Runner.run_sync(planner, question).final_output) @rius.observe def investigate(plan_text: str) -> str: return str(Runner.run_sync(researcher, plan_text).final_output) @rius.observe(name="research-run", kind=SpanKind.AGENT) def run_research(question: str) -> str: return investigate(plan(question)) -
Scope the session and the user around each run. Use your conversation id as the session, so every turn of one conversation groups together. Use an opaque user id rather than an email.
with rius.session(conversation_id): with rius.user(request.user_id): report = run_research(question)Both scopes stamp
session.idanduser.idon every span started inside them, including the spans the integration creates. See Tracing your code for the span options, sessions and users. -
Run one question that searches, then open the trace. You should see the
research-runroot, aCHAINspan per stage,LLMgenerations, theTOOLspan and theRETRIEVERspan under it.
What you get
- One trace per question, shaped like the research itself: the run root, then
planandinvestigate, with each model call, tool call and search nested where it happened. - Model, tokens and cost on every model call, with time to first token where the integration records it. Cost is a list-price estimate that Rius computes, so you never send it.
- Each search shows its index, the number of documents requested and the ids and scores of those returned.
- Traces grouped into sessions by conversation and attributed to users, so you can follow one person across several research questions.
- Tool Loop and Tool kind filtering that count your search tool, because it carries the
TOOLkind.
Two blind spots to plan around. A handoff produces no span of its own and shows only as consecutive generations from different agents. Model calls are all named Response, so the span name does not say which agent spoke. Add an observe wrapper at each agent entry if you need those boundaries.
When something else fits
- If your research agent is not built on the OpenAI Agents SDK, check Integrations for the framework you use.
- For TypeScript, this framework has no integration. Trace its calls with manual generations and
observefrom Tracing your code.