Reasoning Messages

Customize how reasoning (thinking) tokens from models like o1, o3, and o4-mini are displayed.

/** * Reasoning agents: emit AG-UI REASONING_MESSAGE_* events. * * Mirrors the Python siblings `agents/reasoning_agent.py` and * `agents/reasoning_chain_agent.py`. * * Why a reasoning model plus the Responses API: the OpenAI Responses API * streams `response.reasoning_summary_text.delta` items only for native * reasoning models (gpt-5, o3, o4-mini and friends). The Strands bridge * translates those into AG-UI REASONING_MESSAGE_* events with * `role: "reasoning"`, which the frontend renders through the * `reasoningMessage` slot. gpt-4o emits no reasoning items, so the showcase's * default chat-completions model would never light the slot up. * * `buildReasoningAgent` is tool-free and serves reasoning-default and * reasoning-custom. `buildReasoningChainAgent` adds the four mock tools the * tool-rendering-reasoning-chain demo paints per-tool renderers for; each pill * drives a chained pair of calls, so the agent has to own every tool in the * chain to reach the closing narration. */import { Agent, tool } from "@strands-agents/sdk";import { z } from "zod";import { StrandsAgent } from "@ag-ui/aws-strands";import { createModel } from "./model-factory";export const REASONING_MODEL = process.env.OPENAI_REASONING_MODEL ?? "gpt-5.4";/** Responses-API model that streams reasoning summaries on every turn. */function reasoningModel() {  return createModel({    openaiApi: "responses",    reasoning: true,    openaiModelId: REASONING_MODEL,  });}const REASONING_SYSTEM_PROMPT =  "You are a helpful assistant. For each user question, first think " +  "step-by-step about the approach, then give a concise answer.";/** Tool-free agent backing reasoning-default and reasoning-custom. */export async function buildReasoningAgent(): Promise<StrandsAgent> {  return new StrandsAgent({    agent: new Agent({      model: await reasoningModel(),      systemPrompt: REASONING_SYSTEM_PROMPT,      tools: [],    }),    name: "reasoning",    description:      "Strands agent that streams reasoning summaries alongside its answer",  });}const getWeather = tool({  name: "get_weather",  description: "Get the current weather for a given location.",  inputSchema: z.object({    location: z.string().describe("City or airport to report on."),  }),  callback: ({ location }) => ({    city: location,    temperature: 68,    humidity: 55,    wind_speed: 10,    conditions: "Sunny",  }),});const searchFlights = tool({  name: "search_flights",  description:    "Search mock flights from an origin airport to a destination airport.",  inputSchema: z.object({    origin: z.string().describe("Origin airport code."),    destination: z.string().describe("Destination airport code."),  }),  callback: ({ origin, destination }) => ({    origin,    destination,    flights: [      {        airline: "United",        flight: "UA231",        depart: "08:15",        arrive: "16:45",        price_usd: 348,      },      {        airline: "Delta",        flight: "DL412",        depart: "11:20",        arrive: "19:55",        price_usd: 312,      },      {        airline: "JetBlue",        flight: "B6722",        depart: "17:05",        arrive: "01:30",        price_usd: 289,      },    ],  }),});const getStockPrice = tool({  name: "get_stock_price",  description: "Get a mock current price for a stock ticker.",  inputSchema: z.object({    ticker: z.string().describe("Ticker symbol to quote."),    price_usd: z.number().optional().describe("Optional scripted price."),    change_pct: z      .number()      .optional()      .describe("Optional scripted percentage change."),  }),  // The optional arguments let the model (or an aimock fixture) script a  // deterministic quote: when supplied they are echoed back verbatim.  callback: ({ ticker, price_usd, change_pct }) => ({    ticker: ticker.toUpperCase(),    price_usd:      price_usd !== undefined        ? Math.round(price_usd * 100) / 100        : Math.round((100 + Math.random() * 400) * 100) / 100,    change_pct:      change_pct !== undefined        ? Math.round(change_pct * 100) / 100        : Math.round((Math.random() < 0.5 ? -1 : 1) * Math.random() * 300) /          100,  }),});const rollDice = tool({  name: "roll_dice",  description: "Roll a single die with the given number of sides.",  inputSchema: z.object({    sides: z.number().default(6).describe("Number of faces on the die."),  }),  callback: ({ sides }) => ({    sides,    result: 1 + Math.floor(Math.random() * Math.max(2, sides)),  }),});const REASONING_CHAIN_SYSTEM_PROMPT = `You are a helpful travel & lifestyle concierge with mock tools for weather, flights, stock prices, and dice rolls -- they all return fake data, so call them liberally.Your habit is to CHAIN tools when one answer naturally invites another. For a single user question, call at least TWO tools in succession when the topic allows, then compose your final reply. Default chains:  - 'What's the weather in <city>?' -> call get_weather(<city>), then call search_flights(origin='SFO', destination=<city>) so the user also sees how to get there.  - 'How is <ticker> doing?' -> call get_stock_price(<ticker>), then call get_stock_price on a comparable ticker (e.g. 'MSFT' or 'GOOGL') so the user can compare.  - 'Roll a 20-sided die' -> call roll_dice(sides=20), then call roll_dice again with a different number of sides so the user sees a contrast.  - 'Find flights from <a> to <b>' -> call search_flights(a, b), then call get_weather(<b>) for the destination.Only skip chaining when the user has clearly asked for a single, atomic answer and more tool calls would feel intrusive. Never fabricate data that a tool could provide.`;/** Agent backing tool-rendering-reasoning-chain. */export async function buildReasoningChainAgent(): Promise<StrandsAgent> {  return new StrandsAgent({    agent: new Agent({      model: await reasoningModel(),      systemPrompt: REASONING_CHAIN_SYSTEM_PROMPT,      tools: [getWeather, searchFlights, getStockPrice, rollDice],    }),    name: "reasoning_chain",    description:      "Strands agent that chains mock tools while streaming reasoning summaries",  });}

Some models (like OpenAI's o1, o3, and o4-mini) emit reasoning tokens: internal "thinking" traces that show the model's chain-of-thought before it produces a final answer. CopilotKit surfaces these tokens automatically with a collapsible Reasoning Message card.

Default Behavior#

When reasoning events arrive from the agent, CopilotKit renders them inside a built-in card that:

  • Shows a "Thinking…" label with a pulsating indicator while the model is reasoning.
  • Expands automatically so you can follow the model's thought process in real-time.
  • Collapses and switches to "Thought for X seconds" once reasoning finishes.
  • Renders the reasoning content as Markdown.
  • Includes a chevron toggle so users can re-expand and review the reasoning at any time.

No extra configuration is needed; if your model emits reasoning tokens, the card appears automatically.

The only requirement is connecting your agent to CopilotKit; no extra props or configuration needed:

page.tsx
const AGENT_ID = "reasoning-default";export default function ReasoningDefaultDemo() {  return (    <CopilotKit runtimeUrl="/api/copilotkit" agent={AGENT_ID}>      <div className="flex justify-center items-center h-screen w-full">        <div className="h-full w-full max-w-4xl">          <Chat />        </div>      </div>    </CopilotKit>  );}function Chat() {  useReasoningDefaultSuggestions();  return <CopilotChat agentId={AGENT_ID} className="h-full rounded-2xl" />;}

Customizing the Reasoning Message#

The reasoning message is composed of three sub-components that can each be replaced independently via slot props:

Sub-componentSlot propDescription
HeaderheaderThe clickable bar with the brain icon, label, and chevron
ContentcontentViewThe reasoning text area (Markdown)
ToggletoggleThe expand/collapse animation wrapper

You pass custom sub-components through the messageView prop on CopilotChat, CopilotPopup, or CopilotSidebar:

<CopilotChat
  messageView={{
    reasoningMessage: {
      header: CustomHeader,
      contentView: CustomContent,
    },
  }}
/>

Custom Header#

Replace the header to change the icon, label text, or styling. The header receives these props:

PropTypeDescription
isOpenbooleanWhether the content panel is currently expanded
labelstring"Thinking…" while streaming, "Thought for X seconds" after
hasContentbooleanWhether any reasoning text has been received
isStreamingbooleanWhether reasoning is actively streaming
onClick() => voidToggle handler (only present when hasContent is true)
import { CopilotChat } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";

function CustomHeader({
  isOpen,
  label,
  hasContent,
  isStreaming,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
  isOpen?: boolean;
  label?: string;
  hasContent?: boolean;
  isStreaming?: boolean;
}) {
  return (
    <button
      className="flex w-full items-center gap-2 px-3 py-2 text-sm font-medium"
      {...props}
    >
      {isStreaming ? "🧠" : "💡"}
      <span>{label}</span>
      {hasContent && (
        <span className="ml-auto text-xs">{isOpen ? "Hide" : "Show"}</span>
      )}
    </button>
  );
}

<CopilotChat
  messageView={{
    reasoningMessage: { header: CustomHeader },
  }}
/>

Custom Content#

Replace the content area to change how reasoning text is displayed:

PropTypeDescription
isStreamingbooleanWhether reasoning tokens are still arriving
hasContentbooleanWhether any reasoning text has been received
childrenstringThe raw reasoning text
function CustomContent({
  isStreaming,
  hasContent,
  children,
  ...props
}: React.HTMLAttributes<HTMLDivElement> & {
  isStreaming?: boolean;
  hasContent?: boolean;
}) {
  if (!hasContent && !isStreaming) return null;

  return (
    <div className="px-4 pb-3 text-sm text-gray-500 font-mono" {...props}>
      {children}
      {isStreaming && <span className="animate-pulse ml-1">▊</span>}
    </div>
  );
}

<CopilotChat
  messageView={{
    reasoningMessage: { contentView: CustomContent },
  }}
/>

Fully Custom Reasoning Message#

For complete control over the entire reasoning card, pass a component instead of slot props. Your component receives the same top-level props as the built-in one:

PropTypeDescription
messageReasoningMessageThe reasoning message object (.content holds the text)
messagesMessage[]All messages in the conversation
isRunningbooleanWhether the agent is currently running
/** * Reasoning agents: emit AG-UI REASONING_MESSAGE_* events. * * Mirrors the Python siblings `agents/reasoning_agent.py` and * `agents/reasoning_chain_agent.py`. * * Why a reasoning model plus the Responses API: the OpenAI Responses API * streams `response.reasoning_summary_text.delta` items only for native * reasoning models (gpt-5, o3, o4-mini and friends). The Strands bridge * translates those into AG-UI REASONING_MESSAGE_* events with * `role: "reasoning"`, which the frontend renders through the * `reasoningMessage` slot. gpt-4o emits no reasoning items, so the showcase's * default chat-completions model would never light the slot up. * * `buildReasoningAgent` is tool-free and serves reasoning-default and * reasoning-custom. `buildReasoningChainAgent` adds the four mock tools the * tool-rendering-reasoning-chain demo paints per-tool renderers for; each pill * drives a chained pair of calls, so the agent has to own every tool in the * chain to reach the closing narration. */import { Agent, tool } from "@strands-agents/sdk";import { z } from "zod";import { StrandsAgent } from "@ag-ui/aws-strands";import { createModel } from "./model-factory";export const REASONING_MODEL = process.env.OPENAI_REASONING_MODEL ?? "gpt-5.4";/** Responses-API model that streams reasoning summaries on every turn. */function reasoningModel() {  return createModel({    openaiApi: "responses",    reasoning: true,    openaiModelId: REASONING_MODEL,  });}const REASONING_SYSTEM_PROMPT =  "You are a helpful assistant. For each user question, first think " +  "step-by-step about the approach, then give a concise answer.";/** Tool-free agent backing reasoning-default and reasoning-custom. */export async function buildReasoningAgent(): Promise<StrandsAgent> {  return new StrandsAgent({    agent: new Agent({      model: await reasoningModel(),      systemPrompt: REASONING_SYSTEM_PROMPT,      tools: [],    }),    name: "reasoning",    description:      "Strands agent that streams reasoning summaries alongside its answer",  });}const getWeather = tool({  name: "get_weather",  description: "Get the current weather for a given location.",  inputSchema: z.object({    location: z.string().describe("City or airport to report on."),  }),  callback: ({ location }) => ({    city: location,    temperature: 68,    humidity: 55,    wind_speed: 10,    conditions: "Sunny",  }),});const searchFlights = tool({  name: "search_flights",  description:    "Search mock flights from an origin airport to a destination airport.",  inputSchema: z.object({    origin: z.string().describe("Origin airport code."),    destination: z.string().describe("Destination airport code."),  }),  callback: ({ origin, destination }) => ({    origin,    destination,    flights: [      {        airline: "United",        flight: "UA231",        depart: "08:15",        arrive: "16:45",        price_usd: 348,      },      {        airline: "Delta",        flight: "DL412",        depart: "11:20",        arrive: "19:55",        price_usd: 312,      },      {        airline: "JetBlue",        flight: "B6722",        depart: "17:05",        arrive: "01:30",        price_usd: 289,      },    ],  }),});const getStockPrice = tool({  name: "get_stock_price",  description: "Get a mock current price for a stock ticker.",  inputSchema: z.object({    ticker: z.string().describe("Ticker symbol to quote."),    price_usd: z.number().optional().describe("Optional scripted price."),    change_pct: z      .number()      .optional()      .describe("Optional scripted percentage change."),  }),  // The optional arguments let the model (or an aimock fixture) script a  // deterministic quote: when supplied they are echoed back verbatim.  callback: ({ ticker, price_usd, change_pct }) => ({    ticker: ticker.toUpperCase(),    price_usd:      price_usd !== undefined        ? Math.round(price_usd * 100) / 100        : Math.round((100 + Math.random() * 400) * 100) / 100,    change_pct:      change_pct !== undefined        ? Math.round(change_pct * 100) / 100        : Math.round((Math.random() < 0.5 ? -1 : 1) * Math.random() * 300) /          100,  }),});const rollDice = tool({  name: "roll_dice",  description: "Roll a single die with the given number of sides.",  inputSchema: z.object({    sides: z.number().default(6).describe("Number of faces on the die."),  }),  callback: ({ sides }) => ({    sides,    result: 1 + Math.floor(Math.random() * Math.max(2, sides)),  }),});const REASONING_CHAIN_SYSTEM_PROMPT = `You are a helpful travel & lifestyle concierge with mock tools for weather, flights, stock prices, and dice rolls -- they all return fake data, so call them liberally.Your habit is to CHAIN tools when one answer naturally invites another. For a single user question, call at least TWO tools in succession when the topic allows, then compose your final reply. Default chains:  - 'What's the weather in <city>?' -> call get_weather(<city>), then call search_flights(origin='SFO', destination=<city>) so the user also sees how to get there.  - 'How is <ticker> doing?' -> call get_stock_price(<ticker>), then call get_stock_price on a comparable ticker (e.g. 'MSFT' or 'GOOGL') so the user can compare.  - 'Roll a 20-sided die' -> call roll_dice(sides=20), then call roll_dice again with a different number of sides so the user sees a contrast.  - 'Find flights from <a> to <b>' -> call search_flights(a, b), then call get_weather(<b>) for the destination.Only skip chaining when the user has clearly asked for a single, atomic answer and more tool calls would feel intrusive. Never fabricate data that a tool could provide.`;/** Agent backing tool-rendering-reasoning-chain. */export async function buildReasoningChainAgent(): Promise<StrandsAgent> {  return new StrandsAgent({    agent: new Agent({      model: await reasoningModel(),      systemPrompt: REASONING_CHAIN_SYSTEM_PROMPT,      tools: [getWeather, searchFlights, getStockPrice, rollDice],    }),    name: "reasoning_chain",    description:      "Strands agent that chains mock tools while streaming reasoning summaries",  });}

The ReasoningBlock used above renders the reasoning as an amber-tagged inline banner, intentionally louder than the default card so the thinking chain is the focal UI of the demo. Swap in your own component to match your product's tone:

page.tsx
const AGENT_ID = "reasoning-custom";export default function ReasoningCustomDemo() {  return (    <CopilotKit runtimeUrl="/api/copilotkit" agent={AGENT_ID}>      <div className="flex justify-center items-center h-screen w-full">        <div className="h-full w-full max-w-4xl">          <Chat />        </div>      </div>    </CopilotKit>  );}function Chat() {  useReasoningCustomSuggestions();  return (    <CopilotChat      agentId={AGENT_ID}      className="h-full rounded-2xl"      messageView={{        reasoningMessage:          ReasoningBlock as unknown as typeof CopilotChatReasoningMessage,      }}    />  );}

Render-Prop Children#

The built-in CopilotChatReasoningMessage also supports a render-prop pattern for cases where you want to rearrange the built-in sub-components without reimplementing them:

import {
  CopilotChatReasoningMessage,
} from "@copilotkit/react-core/v2";
import { CopilotChat } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";

function MyReasoningLayout(props: React.ComponentProps<typeof CopilotChatReasoningMessage>) {
  return (
    <CopilotChatReasoningMessage {...props}>
      {({ header, toggle }) => (
        <div className="rounded-lg border bg-yellow-50 my-2">
          {header}
          {toggle}
        </div>
      )}
    </CopilotChatReasoningMessage>
  );
}

<CopilotChat
  messageView={{
    reasoningMessage: MyReasoningLayout,
  }}
/>

The render-prop callback receives:

PropertyDescription
headerPre-rendered header element
contentViewPre-rendered content element
togglePre-rendered expand/collapse wrapper (contains contentView)
messageThe reasoning message object
messagesAll messages
isRunningWhether the agent is running