With the OpenAI Agents SDK, set modelSettings: { reasoning: { effort: "medium", summary: "auto" } } on the agent to get reasoning summaries back. When you stream the run, each one arrives as a reasoning_item_created run item event. Forward those to the browser as steps of a thinking line with a live timer, and mark it done when the first output_text_delta arrives, so it folds to "Thought for 9s" above the answer.
How the Agents SDK exposes reasoning#
The SDK's Models guide lists reasoning in ModelSettings: an effort, and a summary of auto, concise or detailed that controls how much reasoning summary the model returns. The Streaming guide lists reasoning_item_created among the run item events, and shows output_text_delta as the raw model event that carries answer text.
So the order on the wire is the order the UI needs: reasoning items while the model thinks, then text deltas once it answers. The reasoning item carries the summary text, not the model's raw thinking; show it as short steps, not a transcript.
From events to the thinking line#
| Moment | Agents SDK event | Thinking state props |
|---|---|---|
| Request sent | Before the first event | label, startedAt |
| A summary arrives | reasoning_item_created, text in item.rawItem.content |
A new step, active; the previous one done |
| The answer starts | raw_model_stream_event with data.type === "output_text_delta" |
done; the timer freezes, the line folds |
| The model returns no summary | No reasoning items | label and timer only |
| Answer text streams | More output_text_delta |
StreamingText with the text so far |
The code#
The route asks for summaries and forwards three kinds of line: a reasoning step, the answer starting, and answer text.
import { Agent, run } from "@openai/agents";
export type ThinkEvent = { type: "reasoning"; text: string } | { type: "answer"; delta: string } | { type: "end" };
const agent = new Agent({
name: "Analyst",
instructions: "Answer questions about the revenue data.",
model: "gpt-5.5",
modelSettings: { reasoning: { effort: "medium", summary: "auto" } },
});
export async function POST(req: Request) {
const { question }: { question: string } = await req.json();
const result = await run(agent, question, { stream: true });
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
const send = (event: ThinkEvent) => controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
for await (const event of result) {
if (event.type === "run_item_stream_event" && event.name === "reasoning_item_created" && event.item.rawItem.type === "reasoning") {
const text = event.item.rawItem.content.map((part) => part.text).join(" ").trim();
if (text) send({ type: "reasoning", text });
}
if (event.type === "raw_model_stream_event" && event.data.type === "output_text_delta") send({ type: "answer", delta: event.data.delta });
}
await result.completed;
send({ type: "end" });
controller.close();
},
});
return new Response(body, { headers: { "Content-Type": "application/x-ndjson" } });
}The client grows the steps, folds the line on the first answer text, and streams the answer under it.
"use client";
import { useState } from "react";
import { StreamingText } from "@/components/pixilui/streaming-text";
import { ThinkingState, type ThinkingStateStep } from "@/components/pixilui/thinking-state";
import type { ThinkEvent } from "../app/api/think/route";
export function AnalystAnswer() {
const [steps, setSteps] = useState<ThinkingStateStep[]>([]);
const [answer, setAnswer] = useState("");
const [streaming, setStreaming] = useState(false);
const [startedAt, setStartedAt] = useState<number>();
const apply = (event: ThinkEvent) => {
if (event.type === "reasoning")
setSteps((prev) => [...prev.map((s) => ({ ...s, status: "done" as const })), { id: String(prev.length), text: event.text.split(/(?<=[.!?])\s/)[0]!.slice(0, 90), status: "active" }]);
if (event.type === "answer") setAnswer((so) => so + event.delta);
if (event.type === "end") setStreaming(false);
};
async function ask() {
setSteps([]);
setAnswer("");
setStreaming(true);
setStartedAt(Date.now());
const response = await fetch("/api/think", { method: "POST", body: JSON.stringify({ question: "Why did net revenue drop 6% in October?" }) });
const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
const lines = (buffer + value).split("\n");
buffer = lines.pop() ?? "";
lines.filter(Boolean).forEach((line) => apply(JSON.parse(line) as ThinkEvent));
}
setStreaming(false);
}
return (
<section aria-live="polite">
<button type="button" onClick={ask} aria-disabled={streaming}>
Explain the October drop
</button>
{startedAt ? <ThinkingState label="Looking into October revenue" steps={steps} startedAt={startedAt} done={answer.length > 0} /> : null}
{answer ? <StreamingText text={answer} streaming={streaming} /> : null}
</section>
);
}Install both with npx shadcn@latest add @pixilui/thinking-state @pixilui/streaming-text.
What the thinking line adds#
- A labeled wait from the first moment, before any summary arrives.
- Steps that check off. Each new summary becomes the active step; the one before it draws its check.
- A timer that freezes and folds. The first answer text ends the thinking; the line becomes "Thought for 9s".
- Reduced motion. No moving light and no rise; steps change at once.
When the framework already ships a UI#
The TypeScript Agents SDK's guides list no React components; its streaming examples print to the terminal. ChatKit, OpenAI's chat UI with React bindings, lists chain-of-thought visualizations among its features and can sit in front of an agent you host; if ChatKit renders your chat, use its reasoning display. The thinking line is for answers shown in your own screens.
When not to use this#
- Models without reasoning. If you set no
reasoningor the model does not reason, show the label and timer only, or nothing for fast replies. - Showing the summary in full. If people need to read the whole summary, render it as text under a disclosure, not as clipped steps.
- Tool-heavy runs. When the agent calls tools between thoughts, use a run timeline for the steps.
Questions#
How do I get reasoning summaries from the OpenAI Agents SDK?
Set modelSettings.reasoning.summary on the agent to auto, concise or detailed, with a reasoning model. Summaries then arrive as reasoning items; in a streamed run, as reasoning_item_created events.
How do I know when the model stopped thinking?
The first raw_model_stream_event whose data.type is output_text_delta is the first token of the answer. Mark the thinking line done there; the time since you sent the request is the thinking time.
Can I show the model's raw chain of thought?
The reasoning item in this flow carries summary text, which is what summary asks for. Show that, and say it is a summary if you show it in full.
Related#
- How to show an AI model's reasoning or thinking state: what a thinking line needs, framework aside.
- Reasoning UI with the Vercel AI SDK: the same line from reasoning parts.
- Agent run timeline with the OpenAI Agents SDK: when the run is more than thinking.
Sources#
- OpenAI Agents SDK (TypeScript): Models, read 4 November 2026
- OpenAI Agents SDK (TypeScript): Streaming, read 4 November 2026
- OpenAI: ChatKit, read 4 November 2026