Reasoning Messages
Customize how reasoning (thinking) tokens from models like o1, o3, and o4-mini are displayed.
"""AG2 agent with weather and sales tools for CopilotKit showcase.Uses AG2's ConversableAgent with AGUIStream to exposethe agent via the AG-UI protocol."""from __future__ import annotationsimport jsonimport osfrom typing import Annotated, Anyfrom autogen import ConversableAgent, LLMConfigfrom autogen.ag_ui import AGUIStreamfrom dotenv import load_dotenvload_dotenv()# Import shared tool implementationsfrom tools import ( get_weather_impl, query_data_impl, manage_sales_todos_impl, get_sales_todos_impl, schedule_meeting_impl, search_flights_impl, build_a2ui_operations_from_tool_call, RENDER_A2UI_TOOL_SCHEMA,)from tools.types import Flight# =====# Tools# =====async def get_weather( location: Annotated[str, "City name to get weather for"],) -> dict[str, str | float]: """Get current weather for a location.""" result = get_weather_impl(location) return { "city": result["city"], "temperature": result["temperature"], "feels_like": result["feels_like"], "humidity": result["humidity"], "wind_speed": result["wind_speed"], "conditions": result["conditions"], }async def query_data( query: Annotated[str, "Natural language query for financial data"],) -> list: """Query financial database for chart data.""" return query_data_impl(query)async def manage_sales_todos( todos: Annotated[list, "Complete list of sales todos"],) -> dict: """Manage the sales pipeline.""" return {"todos": manage_sales_todos_impl(todos)}async def get_sales_todos() -> list: """Get the current sales pipeline.""" return get_sales_todos_impl(None)async def schedule_meeting( reason: Annotated[str, "Reason for the meeting"],) -> dict: """Schedule a meeting with user approval.""" return schedule_meeting_impl(reason)async def search_flights( flights: Annotated[ list[dict[str, Any]], "List of flight objects to display as rich A2UI cards" ],) -> str: """Search for flights and display the results as rich cards. Return exactly 2 flights. Each flight must have: airline, airlineLogo, flightNumber, origin, destination, date (short readable format like "Tue, Mar 18" -- use near-future dates), departureTime, arrivalTime, duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"), statusColor (hex color for status dot), price (e.g. "$289"), and currency (e.g. "USD"). For airlineLogo use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128 """ typed_flights: list[Flight] = [Flight(**f) for f in flights] result = search_flights_impl(typed_flights) return json.dumps(result)async def generate_a2ui( context: Annotated[str, "Conversation context to generate UI for"],) -> str: """Generate dynamic A2UI components based on the conversation. A secondary LLM designs the UI schema and data. The result is returned as an a2ui_operations container for the middleware to detect. """ import openai client = openai.OpenAI() response = client.chat.completions.create( model="gpt-4.1", messages=[ {"role": "system", "content": context or "Generate a useful dashboard UI."}, { "role": "user", "content": "Generate a dynamic A2UI dashboard based on the conversation.", }, ], tools=[ { "type": "function", "function": RENDER_A2UI_TOOL_SCHEMA, } ], tool_choice={"type": "function", "function": {"name": "render_a2ui"}}, ) choice = response.choices[0] if choice.message.tool_calls: args = json.loads(choice.message.tool_calls[0].function.arguments) result = build_a2ui_operations_from_tool_call(args) return json.dumps(result) return json.dumps({"error": "LLM did not call render_a2ui"})# =====# Agent# =====agent = ConversableAgent( name="assistant", system_message=( "You are a helpful sales assistant. You can look up current weather " "for any city using the get_weather tool, query financial data with " "query_data, manage the sales pipeline with manage_sales_todos and " "get_sales_todos, schedule meetings with schedule_meeting, search " "flights and display rich A2UI cards with search_flights, and " "generate dynamic A2UI dashboards with generate_a2ui. " "When asked about the weather, always use the tool rather than guessing. " "Be concise and friendly in your responses." ), llm_config=LLMConfig({"model": "gpt-4o-mini", "stream": True}), human_input_mode="NEVER", # Guard against infinite tool-call loops: AG2's ConversableAgent with # human_input_mode="NEVER" will keep executing tool calls indefinitely # if the LLM keeps requesting them. Without this limit the agent floods # Railway's log stream (500 logs/sec rate-limit), becomes unresponsive # to health probes, and gets killed by the watchdog. max_consecutive_auto_reply=15, functions=[ get_weather, query_data, manage_sales_todos, get_sales_todos, schedule_meeting, search_flights, generate_a2ui, ],)# AG-UI stream wrapperstream = AGUIStream(agent)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:
<CopilotChat agentId="reasoning-default-render" 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-component | Slot prop | Description |
|---|---|---|
Header | header | The clickable bar with the brain icon, label, and chevron |
Content | contentView | The reasoning text area (Markdown) |
Toggle | toggle | The 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:
| Prop | Type | Description |
|---|---|---|
isOpen | boolean | Whether the content panel is currently expanded |
label | string | "Thinking…" while streaming, "Thought for X seconds" after |
hasContent | boolean | Whether any reasoning text has been received |
isStreaming | boolean | Whether reasoning is actively streaming |
onClick | () => void | Toggle 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:
| Prop | Type | Description |
|---|---|---|
isStreaming | boolean | Whether reasoning tokens are still arriving |
hasContent | boolean | Whether any reasoning text has been received |
children | string | The 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:
| Prop | Type | Description |
|---|---|---|
message | ReasoningMessage | The reasoning message object (.content holds the text) |
messages | Message[] | All messages in the conversation |
isRunning | boolean | Whether the agent is currently running |
"""AG2 agent with weather and sales tools for CopilotKit showcase.Uses AG2's ConversableAgent with AGUIStream to exposethe agent via the AG-UI protocol."""from __future__ import annotationsimport jsonimport osfrom typing import Annotated, Anyfrom autogen import ConversableAgent, LLMConfigfrom autogen.ag_ui import AGUIStreamfrom dotenv import load_dotenvload_dotenv()# Import shared tool implementationsfrom tools import ( get_weather_impl, query_data_impl, manage_sales_todos_impl, get_sales_todos_impl, schedule_meeting_impl, search_flights_impl, build_a2ui_operations_from_tool_call, RENDER_A2UI_TOOL_SCHEMA,)from tools.types import Flight# =====# Tools# =====async def get_weather( location: Annotated[str, "City name to get weather for"],) -> dict[str, str | float]: """Get current weather for a location.""" result = get_weather_impl(location) return { "city": result["city"], "temperature": result["temperature"], "feels_like": result["feels_like"], "humidity": result["humidity"], "wind_speed": result["wind_speed"], "conditions": result["conditions"], }async def query_data( query: Annotated[str, "Natural language query for financial data"],) -> list: """Query financial database for chart data.""" return query_data_impl(query)async def manage_sales_todos( todos: Annotated[list, "Complete list of sales todos"],) -> dict: """Manage the sales pipeline.""" return {"todos": manage_sales_todos_impl(todos)}async def get_sales_todos() -> list: """Get the current sales pipeline.""" return get_sales_todos_impl(None)async def schedule_meeting( reason: Annotated[str, "Reason for the meeting"],) -> dict: """Schedule a meeting with user approval.""" return schedule_meeting_impl(reason)async def search_flights( flights: Annotated[ list[dict[str, Any]], "List of flight objects to display as rich A2UI cards" ],) -> str: """Search for flights and display the results as rich cards. Return exactly 2 flights. Each flight must have: airline, airlineLogo, flightNumber, origin, destination, date (short readable format like "Tue, Mar 18" -- use near-future dates), departureTime, arrivalTime, duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"), statusColor (hex color for status dot), price (e.g. "$289"), and currency (e.g. "USD"). For airlineLogo use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128 """ typed_flights: list[Flight] = [Flight(**f) for f in flights] result = search_flights_impl(typed_flights) return json.dumps(result)async def generate_a2ui( context: Annotated[str, "Conversation context to generate UI for"],) -> str: """Generate dynamic A2UI components based on the conversation. A secondary LLM designs the UI schema and data. The result is returned as an a2ui_operations container for the middleware to detect. """ import openai client = openai.OpenAI() response = client.chat.completions.create( model="gpt-4.1", messages=[ {"role": "system", "content": context or "Generate a useful dashboard UI."}, { "role": "user", "content": "Generate a dynamic A2UI dashboard based on the conversation.", }, ], tools=[ { "type": "function", "function": RENDER_A2UI_TOOL_SCHEMA, } ], tool_choice={"type": "function", "function": {"name": "render_a2ui"}}, ) choice = response.choices[0] if choice.message.tool_calls: args = json.loads(choice.message.tool_calls[0].function.arguments) result = build_a2ui_operations_from_tool_call(args) return json.dumps(result) return json.dumps({"error": "LLM did not call render_a2ui"})# =====# Agent# =====agent = ConversableAgent( name="assistant", system_message=( "You are a helpful sales assistant. You can look up current weather " "for any city using the get_weather tool, query financial data with " "query_data, manage the sales pipeline with manage_sales_todos and " "get_sales_todos, schedule meetings with schedule_meeting, search " "flights and display rich A2UI cards with search_flights, and " "generate dynamic A2UI dashboards with generate_a2ui. " "When asked about the weather, always use the tool rather than guessing. " "Be concise and friendly in your responses." ), llm_config=LLMConfig({"model": "gpt-4o-mini", "stream": True}), human_input_mode="NEVER", # Guard against infinite tool-call loops: AG2's ConversableAgent with # human_input_mode="NEVER" will keep executing tool calls indefinitely # if the LLM keeps requesting them. Without this limit the agent floods # Railway's log stream (500 logs/sec rate-limit), becomes unresponsive # to health probes, and gets killed by the watchdog. max_consecutive_auto_reply=15, functions=[ get_weather, query_data, manage_sales_todos, get_sales_todos, schedule_meeting, search_flights, generate_a2ui, ],)# AG-UI stream wrapperstream = AGUIStream(agent)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:
import React from "react";import { CopilotKit, CopilotChat, CopilotChatReasoningMessage,} from "@copilotkit/react-core/v2";import { ReasoningBlock } from "./reasoning-block";// Outer layer — provider + layout chrome.export default function AgenticChatReasoningDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent="agentic-chat-reasoning"> <div data-testid="demo-agentic-chat-reasoning" className="flex justify-center items-center h-screen w-full" > <div className="h-full w-full max-w-4xl"> <Chat /> </div> </div> </CopilotKit> );}// Inner — wires a custom `reasoningMessage` slot that makes the thinking// chain visually prominent, then renders the chat.function Chat() { return ( <CopilotChat agentId="agentic-chat-reasoning" className="h-full rounded-2xl" messageView={{ reasoningMessage: ReasoningBlock 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:
| Property | Description |
|---|---|
header | Pre-rendered header element |
contentView | Pre-rendered content element |
toggle | Pre-rendered expand/collapse wrapper (contains contentView) |
message | The reasoning message object |
messages | All messages |
isRunning | Whether the agent is running |
