From b4785b5f88b8b414044af8e7d701b42351a2d18a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 10 May 2025 21:43:27 -0700 Subject: [PATCH] feat: include "reasoning" messages in Rust TUI (#892) As shown in the screenshot, we now include reasoning messages from the model in the TUI under the heading "codex reasoning": ![image](https://github.com/user-attachments/assets/d8eb3dc3-2f9f-4e95-847e-d24b421249a8) To ensure these are visible by default when using `o4-mini`, this also changes the default value for `summary` (formerly `generate_summary`, which is deprecated in favor of `summary` according to the docs) from unset to `"auto"`. --- codex-rs/core/src/client.rs | 3 ++- codex-rs/core/src/client_common.rs | 14 +++++++++++++- codex-rs/core/src/codex.rs | 13 +++++++++++++ codex-rs/core/src/models.rs | 10 ++++++++++ codex-rs/core/src/protocol.rs | 5 +++++ codex-rs/core/src/rollout.rs | 2 +- codex-rs/tui/src/chatwidget.rs | 4 ++++ codex-rs/tui/src/conversation_history_widget.rs | 4 ++++ codex-rs/tui/src/history_cell.rs | 13 +++++++++++++ 9 files changed, 65 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 5f4f2a1c..6dd20aaa 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -26,6 +26,7 @@ use crate::client_common::Prompt; use crate::client_common::Reasoning; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; +use crate::client_common::Summary; use crate::error::CodexErr; use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; @@ -173,7 +174,7 @@ impl ModelClient { parallel_tool_calls: false, reasoning: Some(Reasoning { effort: "high", - generate_summary: None, + summary: Some(Summary::Auto), }), previous_response_id: prompt.prev_id.clone(), store: prompt.store, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 514b6b60..fcdac71d 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -36,7 +36,19 @@ pub enum ResponseEvent { pub(crate) struct Reasoning { pub(crate) effort: &'static str, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) generate_summary: Option, + pub(crate) summary: Option, +} + +/// A summary of the reasoning performed by the model. This can be useful for +/// debugging and understanding the model's reasoning process. +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum Summary { + Auto, + #[allow(dead_code)] // Will go away once this is configurable. + Concise, + #[allow(dead_code)] // Will go away once this is configurable. + Detailed, } #[derive(Debug, Serialize)] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6366d30c..bf08c10f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -49,6 +49,7 @@ use crate::mcp_connection_manager::try_parse_fully_qualified_tool_name; use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; use crate::models::FunctionCallOutputPayload; +use crate::models::ReasoningItemReasoningSummary; use crate::models::ResponseInputItem; use crate::models::ResponseItem; use crate::models::ShellToolCallParams; @@ -934,6 +935,18 @@ async fn handle_response_item( } } } + ResponseItem::Reasoning { id: _, summary } => { + for item in summary { + let text = match item { + ReasoningItemReasoningSummary::SummaryText { text } => text, + }; + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoning { text }, + }; + sess.tx_event.send(event).await.ok(); + } + } ResponseItem::FunctionCall { name, arguments, diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index fad5a318..a8817cf7 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -33,6 +33,10 @@ pub enum ResponseItem { role: String, content: Vec, }, + Reasoning { + id: String, + summary: Vec, + }, FunctionCall { name: String, // The Responses API returns the function call arguments as a *string* that contains @@ -67,6 +71,12 @@ impl From for ResponseItem { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ReasoningItemReasoningSummary { + SummaryText { text: String }, +} + impl From> for ResponseInputItem { fn from(items: Vec) -> Self { Self::Message { diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 131ccb7a..1069a904 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -317,6 +317,11 @@ pub enum EventMsg { message: String, }, + /// Reasoning event from agent. + AgentReasoning { + text: String, + }, + /// Ack the client's configure message. SessionConfigured { /// Tell the client what model is being queried. diff --git a/codex-rs/core/src/rollout.rs b/codex-rs/core/src/rollout.rs index 0038dfa6..2a45222a 100644 --- a/codex-rs/core/src/rollout.rs +++ b/codex-rs/core/src/rollout.rs @@ -114,7 +114,7 @@ impl RolloutRecorder { ResponseItem::Message { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::FunctionCallOutput { .. } => {} - ResponseItem::Other => { + ResponseItem::Reasoning { .. } | ResponseItem::Other => { // These should never be serialized. continue; } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 53bb24b8..b6d0f73c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -225,6 +225,10 @@ impl ChatWidget<'_> { self.conversation_history.add_agent_message(message); self.request_redraw()?; } + EventMsg::AgentReasoning { text } => { + self.conversation_history.add_agent_reasoning(text); + self.request_redraw()?; + } EventMsg::TaskStarted => { self.bottom_pane.set_task_running(true)?; self.request_redraw()?; diff --git a/codex-rs/tui/src/conversation_history_widget.rs b/codex-rs/tui/src/conversation_history_widget.rs index e3bb9121..70e7b6c4 100644 --- a/codex-rs/tui/src/conversation_history_widget.rs +++ b/codex-rs/tui/src/conversation_history_widget.rs @@ -174,6 +174,10 @@ impl ConversationHistoryWidget { self.add_to_history(HistoryCell::new_agent_message(message)); } + pub fn add_agent_reasoning(&mut self, text: String) { + self.add_to_history(HistoryCell::new_agent_reasoning(text)); + } + pub fn add_background_event(&mut self, message: String) { self.add_to_history(HistoryCell::new_background_event(message)); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 53035a98..c3003b2b 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -41,6 +41,9 @@ pub(crate) enum HistoryCell { /// Message from the agent. AgentMessage { lines: Vec> }, + /// Reasoning event from the agent. + AgentReasoning { lines: Vec> }, + /// An exec tool call that has not finished yet. ActiveExecCommand { call_id: String, @@ -134,6 +137,15 @@ impl HistoryCell { HistoryCell::AgentMessage { lines } } + pub(crate) fn new_agent_reasoning(text: String) -> Self { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("codex reasoning".magenta().italic())); + append_markdown(&text, &mut lines); + lines.push(Line::from("")); + + HistoryCell::AgentReasoning { lines } + } + pub(crate) fn new_active_exec_command(call_id: String, command: Vec) -> Self { let command_escaped = escape_command(&command); let start = Instant::now(); @@ -363,6 +375,7 @@ impl HistoryCell { HistoryCell::WelcomeMessage { lines, .. } | HistoryCell::UserPrompt { lines, .. } | HistoryCell::AgentMessage { lines, .. } + | HistoryCell::AgentReasoning { lines, .. } | HistoryCell::BackgroundEvent { lines, .. } | HistoryCell::ErrorEvent { lines, .. } | HistoryCell::SessionInfo { lines, .. }