From a762fe1000e9f31f461e2bd367fb229ab4b21f85 Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 14:36:28 +0100 Subject: [PATCH] Improve the formatting of tool calls, show thinking, treat Reasoning and Thinking as the same thing (sorry Kant) (#7626) Co-authored-by: Claude Opus 4.6 --- crates/goose-cli/src/session/output.rs | 9 +- crates/goose-server/src/openapi.rs | 6 +- crates/goose/src/agents/agent.rs | 2 +- crates/goose/src/context_mgmt/mod.rs | 1 - crates/goose/src/conversation/message.rs | 91 ++++++--- .../goose/src/providers/formats/anthropic.rs | 174 +++++++++--------- crates/goose/src/providers/formats/bedrock.rs | 5 - .../goose/src/providers/formats/databricks.rs | 4 - crates/goose/src/providers/formats/google.rs | 24 +-- crates/goose/src/providers/formats/openai.rs | 28 ++- .../src/providers/formats/openai_responses.rs | 18 +- .../goose/src/providers/formats/snowflake.rs | 4 - ui/desktop/openapi.json | 32 ---- ui/desktop/src/api/index.ts | 2 +- ui/desktop/src/api/types.gen.ts | 6 - ui/desktop/src/components/GooseMessage.tsx | 28 +-- .../src/components/ToolCallArguments.tsx | 92 ++++----- .../src/components/ToolCallWithResponse.tsx | 21 +-- .../sessions/SessionViewComponents.tsx | 19 +- ui/desktop/src/hooks/useChatStream.ts | 15 ++ ui/desktop/src/types/message.ts | 10 +- 21 files changed, 262 insertions(+), 329 deletions(-) diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index c4801c4182..8472cc8a6a 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -240,7 +240,6 @@ pub fn render_message(message: &Message, debug: bool) { println!("Image: [data: {}, type: {}]", image.data, image.mime_type); } MessageContent::Thinking(t) => render_thinking(&t.thinking, theme), - MessageContent::Reasoning(r) => render_thinking(&r.text, theme), MessageContent::RedactedThinking(_) => { println!("\n{}", style("Thinking:").dim().italic()); print_markdown("Thinking was redacted", theme); @@ -280,10 +279,7 @@ pub fn render_message_streaming( let theme = get_theme(); for content in &message.content { - if !matches!( - content, - MessageContent::Thinking(_) | MessageContent::Reasoning(_) - ) { + if !matches!(content, MessageContent::Thinking(_)) { *thinking_header_shown = false; } @@ -322,9 +318,6 @@ pub fn render_message_streaming( MessageContent::Thinking(t) => { render_thinking_streaming(&t.thinking, buffer, thinking_header_shown, theme); } - MessageContent::Reasoning(r) => { - render_thinking_streaming(&r.text, buffer, thinking_header_shown, theme); - } MessageContent::RedactedThinking(_) => { flush_markdown_buffer(buffer, theme); println!("\n{}", style("Thinking:").dim().italic()); diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 2f021f94fc..486df8ae4d 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -21,9 +21,8 @@ use goose::config::declarative_providers::{ }; use goose::conversation::message::{ ActionRequired, ActionRequiredData, FrontendToolRequest, Message, MessageContent, - MessageMetadata, ReasoningContent, RedactedThinkingContent, SystemNotificationContent, - SystemNotificationType, ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest, - ToolResponse, + MessageMetadata, RedactedThinkingContent, SystemNotificationContent, SystemNotificationType, + ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest, ToolResponse, }; use crate::routes::recipe_utils::RecipeManifest; @@ -552,7 +551,6 @@ derive_utoipa!(Icon as IconSchema); ActionRequiredData, ThinkingContent, RedactedThinkingContent, - ReasoningContent, FrontendToolRequest, ResourceContentsSchema, SystemNotificationType, diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 67cb59b26a..313de96b44 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1444,7 +1444,7 @@ impl Agent { // Collect reasoning content to attach to tool request messages let reasoning_content: Vec = response.content.iter() - .filter(|c| matches!(c, MessageContent::Reasoning(_))) + .filter(|c| matches!(c, MessageContent::Thinking(_))) .cloned() .collect(); diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index 7db69db59a..f6847e460c 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -403,7 +403,6 @@ fn format_message_for_compacting(msg: &Message) -> String { MessageContent::SystemNotification(notification) => { Some(format!("system_notification: {}", notification.msg)) } - MessageContent::Reasoning(_) => None, }) .collect(); diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 282df3f49e..54cf8c771e 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -26,13 +26,31 @@ where { use serde::de::Error; - let mut raw: Vec = Vec::deserialize(deserializer)?; + let raw: Vec = Vec::deserialize(deserializer)?; - // Filter out old "conversationCompacted" messages from pre-14.0 - raw.retain(|item| item.get("type").and_then(|v| v.as_str()) != Some("conversationCompacted")); + let mut migrated = Vec::with_capacity(raw.len()); + for item in raw { + match item.get("type").and_then(|v| v.as_str()) { + // Filter out old "conversationCompacted" messages from pre-14.0 + Some("conversationCompacted") => {} + // Migrate old "reasoning" content to "thinking". Invalid legacy reasoning + // blocks are dropped so they don't fail deserialization. + Some("reasoning") => { + if let Some(text) = item.get("text").and_then(|v| v.as_str()) { + migrated.push(serde_json::json!({ + "type": "thinking", + "thinking": text, + "signature": "" + })); + } + } + _ => migrated.push(item), + } + } - let mut content: Vec = serde_json::from_value(serde_json::Value::Array(raw)) - .map_err(|e| Error::custom(format!("Failed to deserialize MessageContent: {}", e)))?; + let mut content: Vec = + serde_json::from_value(serde_json::Value::Array(migrated)) + .map_err(|e| Error::custom(format!("Failed to deserialize MessageContent: {}", e)))?; for message_content in &mut content { if let MessageContent::Text(text_content) = message_content { @@ -176,11 +194,6 @@ pub struct SystemNotificationContent { pub data: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct ReasoningContent { - pub text: String, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] /// Content passed inside a message, which can be both simple content and tool content #[serde(tag = "type", rename_all = "camelCase")] @@ -195,7 +208,6 @@ pub enum MessageContent { Thinking(ThinkingContent), RedactedThinking(RedactedThinkingContent), SystemNotification(SystemNotificationContent), - Reasoning(ReasoningContent), } impl fmt::Display for MessageContent { @@ -237,7 +249,6 @@ impl fmt::Display for MessageContent { MessageContent::SystemNotification(r) => { write!(f, "[SystemNotification: {}]", r.msg) } - MessageContent::Reasoning(r) => write!(f, "[Reasoning: {}]", r.text), } } } @@ -450,10 +461,6 @@ impl MessageContent { }) } - pub fn reasoning>(text: S) -> Self { - MessageContent::Reasoning(ReasoningContent { text: text.into() }) - } - pub fn as_system_notification(&self) -> Option<&SystemNotificationContent> { if let MessageContent::SystemNotification(ref notification) = self { Some(notification) @@ -525,14 +532,6 @@ impl MessageContent { _ => None, } } - - /// Get the reasoning content if this is a ReasoningContent variant - pub fn as_reasoning(&self) -> Option<&ReasoningContent> { - match self { - MessageContent::Reasoning(reasoning) => Some(reasoning), - _ => None, - } - } } impl From for MessageContent { @@ -1109,6 +1108,50 @@ mod tests { } } + #[test] + fn test_deserialization_migrates_reasoning_to_thinking() { + let json = serde_json::json!({ + "role": "assistant", + "created": 1740171566, + "content": [ + { "type": "reasoning", "text": "step by step" }, + { "type": "text", "text": "final answer" } + ], + "metadata": { "agentVisible": true, "userVisible": true } + }); + + let message: Message = serde_json::from_value(json).unwrap(); + assert_eq!(message.content.len(), 2); + + let MessageContent::Thinking(thinking) = &message.content[0] else { + panic!("Expected Thinking content"); + }; + assert_eq!(thinking.thinking, "step by step"); + assert!(thinking.signature.is_empty()); + } + + #[test] + fn test_deserialization_drops_invalid_reasoning_blocks() { + let json = serde_json::json!({ + "role": "assistant", + "created": 1740171566, + "content": [ + { "type": "reasoning" }, + { "type": "reasoning", "text": 42 }, + { "type": "text", "text": "still here" } + ], + "metadata": { "agentVisible": true, "userVisible": true } + }); + + let message: Message = serde_json::from_value(json).unwrap(); + assert_eq!(message.content.len(), 1); + + let MessageContent::Text(text) = &message.content[0] else { + panic!("Expected Text content"); + }; + assert_eq!(text.text, "still here"); + } + #[test] fn test_from_prompt_message_text() { let prompt_content = PromptMessageContent::Text { diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index f1e0c796ac..d920dc5e76 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -171,11 +171,13 @@ pub fn format_messages(messages: &[Message]) -> Vec { // Skip } MessageContent::Thinking(thinking) => { - content.push(json!({ - TYPE_FIELD: THINKING_TYPE, - THINKING_TYPE: thinking.thinking, - SIGNATURE_FIELD: thinking.signature - })); + if !thinking.signature.is_empty() { + content.push(json!({ + TYPE_FIELD: THINKING_TYPE, + THINKING_TYPE: thinking.thinking, + SIGNATURE_FIELD: thinking.signature + })); + } } MessageContent::RedactedThinking(redacted) => { content.push(json!({ @@ -196,10 +198,6 @@ pub fn format_messages(messages: &[Message]) -> Vec { })); } } - MessageContent::Reasoning(_reasoning) => { - // Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek) - // Anthropic doesn't use this format, so skip it - } } } @@ -563,8 +561,11 @@ where try_stream! { let mut accumulated_text = String::new(); + let mut accumulated_thinking = String::new(); + let mut accumulated_thinking_signature = String::new(); let mut accumulated_tool_calls: std::collections::HashMap = std::collections::HashMap::new(); let mut current_tool_id: Option = None; + let mut current_block_type: Option = None; let mut final_usage: Option = None; let mut message_id: Option = None; @@ -619,13 +620,33 @@ where "content_block_start" => { // A new content block started if let Some(content_block) = event.data.get("content_block") { - if content_block.get("type") == Some(&json!("tool_use")) { - if let Some(id) = content_block.get("id").and_then(|v| v.as_str()) { - current_tool_id = Some(id.to_string()); - if let Some(name) = content_block.get("name").and_then(|v| v.as_str()) { - accumulated_tool_calls.insert(id.to_string(), (name.to_string(), String::new())); + let block_type = content_block.get("type").and_then(|v| v.as_str()).unwrap_or(""); + current_block_type = Some(block_type.to_string()); + match block_type { + "tool_use" => { + if let Some(id) = content_block.get("id").and_then(|v| v.as_str()) { + current_tool_id = Some(id.to_string()); + if let Some(name) = content_block.get("name").and_then(|v| v.as_str()) { + accumulated_tool_calls.insert(id.to_string(), (name.to_string(), String::new())); + } } } + THINKING_TYPE => { + accumulated_thinking.clear(); + } + REDACTED_THINKING_TYPE => { + // Yield redacted thinking immediately — there are no deltas for it + if let Some(data) = content_block.get("data").and_then(|v| v.as_str()) { + let mut message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::redacted_thinking(data)], + ); + message.id = message_id.clone(); + yield (Some(message), None); + } + } + _ => {} } } continue; @@ -646,6 +667,20 @@ where message.id = message_id.clone(); yield (Some(message), None); } + } else if delta.get("type") == Some(&json!("thinking_delta")) { + // Thinking content delta — stream incrementally for real-time UI + if let Some(thinking) = delta.get("thinking").and_then(|v| v.as_str()) { + accumulated_thinking.push_str(thinking); + + // Yield partial thinking (no signature yet) for live display + let mut message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::thinking(thinking, "")], + ); + message.id = message_id.clone(); + yield (Some(message), None); + } } else if delta.get("type") == Some(&json!("input_json_delta")) { // Tool input delta if let Some(tool_id) = ¤t_tool_id { @@ -655,12 +690,34 @@ where } } } + } else if delta.get("type") == Some(&json!("signature_delta")) { + // Signature for a thinking block + if let Some(sig) = delta.get("signature").and_then(|v| v.as_str()) { + accumulated_thinking_signature.push_str(sig); + } } } continue; } "content_block_stop" => { // Content block finished + if current_block_type.as_deref() == Some(THINKING_TYPE) && !accumulated_thinking.is_empty() { + // Yield the complete thinking block with signature for session storage + let mut message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::thinking( + std::mem::take(&mut accumulated_thinking), + std::mem::take(&mut accumulated_thinking_signature), + )], + ); + message.id = message_id.clone(); + yield (Some(message), None); + current_block_type = None; + continue; + } + current_block_type = None; + if let Some(tool_id) = current_tool_id.take() { // Tool call finished, yield complete tool call if let Some((name, args)) = accumulated_tool_calls.remove(&tool_id) { @@ -863,80 +920,6 @@ mod tests { Ok(()) } - #[test] - fn test_parse_thinking_response() -> Result<()> { - let response = json!({ - "id": "msg_456", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "This is a step-by-step thought process...", - "signature": "EuYBCkQYAiJAVbJNBoH7HQiDcMwwAMhWqNyoe4G2xHRprK8ICM8gZzu16i7Se4EiEbmlKqNH1GtwcX1BMK6iLu8bxWn5wPVIFBIMnptdlVal7ZX5iNPFGgwWjX+BntcEOHky4HciMFVef7FpQeqnuiL1Xt7J4OLHZSyu4tcr809AxAbclcJ5dm1xE5gZrUO+/v60cnJM2ipQp4B8/3eHI03KSV6bZR/vMrBSYCV+aa/f5KHX2cRtLGp/Ba+3Tk/efbsg01WSduwAIbR4coVrZLnGJXNyVTFW/Be2kLy/ECZnx8cqvU3oQOg=" - }, - { - "type": "redacted_thinking", - "data": "EmwKAhgBEgy3va3pzix/LafPsn4aDFIT2Xlxh0L5L8rLVyIwxtE3rAFBa8cr3qpP" - }, - { - "type": "text", - "text": "I've analyzed the problem and here's the solution." - } - ], - "model": "claude-3-7-sonnet-20250219", - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 10, - "output_tokens": 45, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - } - }); - - let message = response_to_message(&response)?; - let usage = get_usage(&response)?; - - assert_eq!(message.content.len(), 3); - - if let MessageContent::Thinking(thinking) = &message.content[0] { - assert_eq!( - thinking.thinking, - "This is a step-by-step thought process..." - ); - assert!(thinking - .signature - .starts_with("EuYBCkQYAiJAVbJNBoH7HQiDcMwwAMhWqNyoe4G2xHRprK8ICM8g")); - } else { - panic!("Expected Thinking content at index 0"); - } - - if let MessageContent::RedactedThinking(redacted) = &message.content[1] { - assert_eq!( - redacted.data, - "EmwKAhgBEgy3va3pzix/LafPsn4aDFIT2Xlxh0L5L8rLVyIwxtE3rAFBa8cr3qpP" - ); - } else { - panic!("Expected RedactedThinking content at index 1"); - } - - if let MessageContent::Text(text) = &message.content[2] { - assert_eq!( - text.text, - "I've analyzed the problem and here's the solution." - ); - } else { - panic!("Expected Text content at index 2"); - } - - assert_eq!(usage.input_tokens, Some(10)); - assert_eq!(usage.output_tokens, Some(45)); - assert_eq!(usage.total_tokens, Some(55)); - - Ok(()) - } - #[test] fn test_message_to_anthropic_spec() { let messages = vec![ @@ -957,6 +940,21 @@ mod tests { assert_eq!(spec[2]["content"][0]["text"], "How are you?"); } + #[test] + fn test_message_to_anthropic_spec_skips_unsigned_thinking() { + let messages = vec![ + Message::assistant().with_content(MessageContent::thinking("internal", "")), + Message::assistant().with_text("Hi there"), + ]; + + let spec = format_messages(&messages); + + assert_eq!(spec.len(), 1); + assert_eq!(spec[0]["role"], "assistant"); + assert_eq!(spec[0]["content"][0]["type"], "text"); + assert_eq!(spec[0]["content"][0]["text"], "Hi there"); + } + #[test] fn test_tools_to_anthropic_spec() { let tools = vec![ diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index 9342754020..5c9a950314 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -125,11 +125,6 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result { - // Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek) - // Bedrock doesn't use this format, so skip - bedrock::ContentBlock::Text("".to_string()) - } }) } diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index 33ac9f0d67..fb712cc343 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -206,10 +206,6 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec {} - MessageContent::Reasoning(_reasoning) => { - // Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek) - // Databricks doesn't use this format, so skip - } } } diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index 5805917fa7..252ba404c9 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -255,7 +255,7 @@ fn process_response_part_impl( if is_thought { match signature { Some(sig) => Some(MessageContent::thinking(text.to_string(), sig.to_string())), - None => Some(MessageContent::reasoning(text.to_string())), + None => Some(MessageContent::thinking(text.to_string(), "")), } } else { Some(MessageContent::text(text.to_string())) @@ -1050,14 +1050,14 @@ mod tests { } #[test] - fn test_thought_without_signature_maps_to_reasoning() { + fn test_thought_without_signature_maps_to_thinking() { let response = google_response(vec![json!({ "text": "Working through options...", "thought": true })]); let native = response_to_message(response).unwrap(); assert_eq!(native.content.len(), 1); - assert!(native.content[0].as_reasoning().is_some()); + assert!(native.content[0].as_thinking().is_some()); } #[test] @@ -1188,30 +1188,28 @@ mod tests { async fn test_streaming_with_thought_signature() { use futures::StreamExt; - async fn collect_streaming_text(raw: &str) -> (String, usize, usize) { + async fn collect_streaming_text(raw: &str) -> (String, usize) { let lines: Vec> = raw.lines().map(|l| Ok(l.to_string())).collect(); let stream = Box::pin(futures::stream::iter(lines)); let mut msg_stream = std::pin::pin!(response_to_streaming_message(stream)); let mut text = String::new(); let mut thinking = 0usize; - let mut reasoning = 0usize; while let Some(Ok((message, _))) = msg_stream.next().await { if let Some(msg) = message { for c in &msg.content { match c { MessageContent::Text(t) => text.push_str(&t.text), MessageContent::Thinking(_) => thinking += 1, - MessageContent::Reasoning(_) => reasoning += 1, _ => {} } } } } - (text, thinking, reasoning) + (text, thinking) } - let (text, thinking, reasoning) = collect_streaming_text(concat!( + let (text, thinking) = collect_streaming_text(concat!( r#"data: {"candidates": [{"content": {"role": "model", "#, r#""parts": [{"text": "Hello", "thoughtSignature": "sig1"}]}}], "#, r#""modelVersion": "gemini-3-flash-preview"}"#, @@ -1221,10 +1219,9 @@ mod tests { )) .await; assert_eq!(thinking, 0); - assert_eq!(reasoning, 0); assert_eq!(text, "Hello world"); - let (text, thinking, reasoning) = collect_streaming_text(concat!( + let (text, thinking) = collect_streaming_text(concat!( r#"data: {"candidates": [{"content": {"role": "model", "#, r#""parts": [{"text": "SECURITY.md: Project"}]}}], "#, r#""modelVersion": "gemini-3-flash-preview"}"#, @@ -1235,10 +1232,9 @@ mod tests { )) .await; assert_eq!(thinking, 0); - assert_eq!(reasoning, 0); assert_eq!(text, "SECURITY.md: Project policies.\n\nRead it?"); - let (text, thinking, reasoning) = collect_streaming_text(concat!( + let (text, thinking) = collect_streaming_text(concat!( r#"data: {"candidates": [{"content": {"role": "model", "#, r#""parts": [{"text": "one "}]}}], "modelVersion": "gemini-3-flash-preview"}"#, "\n", @@ -1250,10 +1246,9 @@ mod tests { )) .await; assert_eq!(thinking, 0); - assert_eq!(reasoning, 0); assert_eq!(text, "one two three"); - let (text, thinking, reasoning) = collect_streaming_text(concat!( + let (text, thinking) = collect_streaming_text(concat!( r#"data: {"candidates": [{"content": {"role": "model", "#, r#""parts": [{"text": "internal chain", "thought": true, "thoughtSignature": "sig4"}]}}]}"#, "\n", @@ -1262,7 +1257,6 @@ mod tests { )) .await; assert_eq!(thinking, 1); - assert_eq!(reasoning, 0); assert_eq!(text, "visible"); } diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 465b9d356c..5ca05fa592 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -105,20 +105,15 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< } } } - MessageContent::Thinking(_) => { - // Thinking blocks are not directly used in OpenAI format - continue; + MessageContent::Thinking(t) => { + reasoning_text.push_str(&t.thinking); } MessageContent::RedactedThinking(_) => { - // Redacted thinking blocks are not directly used in OpenAI format continue; } MessageContent::SystemNotification(_) => { continue; } - MessageContent::Reasoning(r) => { - reasoning_text.push_str(&r.text); - } MessageContent::ToolRequest(request) => match &request.tool_call { Ok(tool_call) => { let sanitized_name = sanitize_function_name(&tool_call.name); @@ -346,7 +341,7 @@ pub fn response_to_message(response: &Value) -> anyhow::Result { if let Some(reasoning_content) = reasoning_value { if let Some(reasoning_str) = reasoning_content.as_str() { if !reasoning_str.is_empty() { - content.push(MessageContent::reasoning(reasoning_str)); + content.push(MessageContent::thinking(reasoning_str, "")); } } } @@ -646,7 +641,7 @@ where let mut contents = Vec::new(); if !accumulated_reasoning_content.is_empty() { - contents.push(MessageContent::reasoning(&accumulated_reasoning_content)); + contents.push(MessageContent::thinking(&accumulated_reasoning_content, "")); accumulated_reasoning_content.clear(); } let mut sorted_indices: Vec<_> = tool_call_data.keys().cloned().collect(); @@ -706,7 +701,7 @@ where if let Some(reasoning) = &chunk.choices[0].delta.reasoning_content { if !reasoning.is_empty() { - content.push(MessageContent::reasoning(reasoning)); + content.push(MessageContent::thinking(reasoning, "")); } } @@ -1837,11 +1832,11 @@ data: [DONE]"#; let message = response_to_message(&response)?; assert_eq!(message.content.len(), 2); - // First should be reasoning content - if let MessageContent::Reasoning(reasoning) = &message.content[0] { - assert_eq!(reasoning.text, "Let me think about this step by step..."); + // First should be thinking content (reasoning is mapped to thinking) + if let MessageContent::Thinking(thinking) = &message.content[0] { + assert_eq!(thinking.thinking, "Let me think about this step by step..."); } else { - panic!("Expected Reasoning content"); + panic!("Expected Thinking content, got {:?}", message.content[0]); } // Second should be text content @@ -1858,7 +1853,10 @@ data: [DONE]"#; fn test_format_messages_with_reasoning_content() -> anyhow::Result<()> { // Test that reasoning_content is properly included in formatted messages let mut message = Message::assistant() - .with_content(MessageContent::reasoning("Thinking through the problem...")) + .with_content(MessageContent::thinking( + "Thinking through the problem...", + "", + )) .with_text("The result is 42"); // Add a tool call to test that reasoning_content works with tool calls diff --git a/crates/goose/src/providers/formats/openai_responses.rs b/crates/goose/src/providers/formats/openai_responses.rs index 5c2751b925..c5cce0f117 100644 --- a/crates/goose/src/providers/formats/openai_responses.rs +++ b/crates/goose/src/providers/formats/openai_responses.rs @@ -41,7 +41,7 @@ fn reasoning_from_summary(summary: &[SummaryText]) -> Option { if text.is_empty() { None } else { - Some(MessageContent::reasoning(text)) + Some(MessageContent::thinking(text, "")) } } @@ -881,10 +881,10 @@ mod tests { let message = responses_api_to_message(&response)?; - let reasoning = message.content.iter().find_map(|c| c.as_reasoning()); - assert!(reasoning.is_some(), "should contain reasoning content"); + let thinking = message.content.iter().find_map(|c| c.as_thinking()); + assert!(thinking.is_some(), "should contain thinking content"); assert_eq!( - reasoning.unwrap().text, + thinking.unwrap().thinking, "Thinking about the question...\nThe answer is straightforward." ); @@ -938,7 +938,7 @@ mod tests { let messages = responses_api_to_streaming_message(response_stream); futures::pin_mut!(messages); - let mut reasoning_parts = Vec::new(); + let mut thinking_parts = Vec::new(); let mut text_parts = Vec::new(); while let Some(item) = messages.next().await { @@ -946,7 +946,7 @@ mod tests { if let Some(msg) = message { for content in msg.content { match &content { - MessageContent::Reasoning(r) => reasoning_parts.push(r.text.clone()), + MessageContent::Thinking(t) => thinking_parts.push(t.thinking.clone()), MessageContent::Text(t) => text_parts.push(t.text.clone()), _ => {} } @@ -955,10 +955,10 @@ mod tests { } assert!( - !reasoning_parts.is_empty(), - "should capture reasoning from stream" + !thinking_parts.is_empty(), + "should capture thinking from stream" ); - assert_eq!(reasoning_parts.join(""), "Let me think step by step."); + assert_eq!(thinking_parts.join(""), "Let me think step by step."); assert!(text_parts.concat().contains("Paris.")); Ok(()) diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index 20bbf79cbb..cd301e50e4 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -65,10 +65,6 @@ pub fn format_messages(messages: &[Message]) -> Vec { MessageContent::FrontendToolRequest(_tool_request) => { // Skip frontend tool requests } - MessageContent::Reasoning(_reasoning) => { - // Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek) - // Snowflake doesn't use this format, so skip - } } } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 25e69576ee..9ecb7fdd5e 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -5876,27 +5876,6 @@ } } ] - }, - { - "allOf": [ - { - "$ref": "#/components/schemas/ReasoningContent" - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "reasoning" - ] - } - } - } - ] } ], "description": "Content passed inside a message, which can be both simple content and tool content", @@ -6863,17 +6842,6 @@ } } }, - "ReasoningContent": { - "type": "object", - "required": [ - "text" - ], - "properties": { - "text": { - "type": "string" - } - } - }, "Recipe": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 1204578660..3b586b7982 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 80c8702eb6..e48541ec1f 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -670,8 +670,6 @@ export type MessageContent = (TextContent & { type: 'redactedThinking'; }) | (SystemNotificationContent & { type: 'systemNotification'; -}) | (ReasoningContent & { - type: 'reasoning'; }); export type MessageEvent = { @@ -1009,10 +1007,6 @@ export type ReadResourceResponse = { uri: string; }; -export type ReasoningContent = { - text: string; -}; - export type Recipe = { activities?: Array | null; author?: Author | null; diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index 794ae59fcb..71227fb377 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -5,7 +5,7 @@ import MarkdownContent from './MarkdownContent'; import ToolCallWithResponse from './ToolCallWithResponse'; import { getTextAndImageContent, - getReasoningContent, + getThinkingContent, getToolRequests, getToolResponses, getToolConfirmationContent, @@ -48,7 +48,7 @@ export default function GooseMessage({ const contentRef = useRef(null); let { textContent, imagePaths } = getTextAndImageContent(message); - const reasoningContent = getReasoningContent(message); + const thinkingContent = getThinkingContent(message); const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => { const regex = /([\s\S]*?)<\/think>/i; @@ -131,26 +131,16 @@ export default function GooseMessage({ return (
- {reasoningContent && ( -
- - Show reasoning - -
- -
-
+ {thinkingContent && ( +
+ +
)} {cotText && ( -
- - Show thinking - -
- -
-
+
+ +
)} {(displayText.trim() || imagePaths.length > 0) && ( diff --git a/ui/desktop/src/components/ToolCallArguments.tsx b/ui/desktop/src/components/ToolCallArguments.tsx index fe29ffdf7c..ee1ad911e5 100644 --- a/ui/desktop/src/components/ToolCallArguments.tsx +++ b/ui/desktop/src/components/ToolCallArguments.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import MarkdownContent from './MarkdownContent'; import Expand from './ui/Expand'; export type ToolCallArgumentValue = @@ -14,6 +13,12 @@ interface ToolCallArgumentsProps { args: Record; } +function formatValue(value: ToolCallArgumentValue): string { + if (typeof value === 'string') return value; + if (typeof value === 'object' && value !== null) return JSON.stringify(value, null, 2); + return String(value); +} + export function ToolCallArguments({ args }: ToolCallArgumentsProps) { const [expandedKeys, setExpandedKeys] = useState>({}); @@ -22,46 +27,33 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) { }; const renderValue = (key: string, value: ToolCallArgumentValue) => { - if (typeof value === 'string') { - const needsExpansion = value.length > 60; - const isExpanded = expandedKeys[key]; + const text = formatValue(value).trim(); + const needsExpansion = text.length > 60 || text.includes('\n'); + const isExpanded = expandedKeys[key]; - if (!needsExpansion) { - return ( -
-
- {key} - {value} -
-
- ); - } - - return ( -
-
- -
- {isExpanded ? ( -
- -
- ) : ( - - )} + return ( +
+
+ +
+ {isExpanded ? ( +
+                {text}
+              
+ ) : ( + + )} + {needsExpansion && ( -
+ )}
- ); - } - - // Handle non-string values (arrays, objects, etc.) - const content = Array.isArray(value) - ? value.map((item, index) => `${index + 1}. ${JSON.stringify(item)}`).join('\n') - : typeof value === 'object' && value !== null - ? JSON.stringify(value, null, 2) - : String(value); - - return ( -
-
- {key} -
-            {content}
-          
-
); }; diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index 1cf15fdbd6..cbca5a4b7d 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -866,7 +866,7 @@ interface ToolResultViewProps { isStartExpanded: boolean; } -function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewProps) { +function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) { const hasText = (c: ContentBlock): c is ContentBlock & { text: string } => 'text' in c && typeof (c as Record).text === 'string'; @@ -879,18 +879,6 @@ function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewPro const hasResource = (c: ContentBlock): c is ContentBlock & { resource: unknown } => 'resource' in c; - const wrapMarkdown = (text: string): string => { - if ( - ['code_execution__list_functions', 'code_execution__get_function_details'].includes( - toolCall.name - ) - ) { - return '```typescript\n' + text + '\n```'; - } else { - return text; - } - }; - return ( Output} @@ -898,10 +886,9 @@ function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewPro >
{hasText(result) && ( - +
+            {result.text.trim()}
+          
)} {hasImage(result) && ( = ({ messages .map((message, index) => { const { textContent, imagePaths } = getTextAndImageContent(message); - const reasoningContent = getReasoningContent(message); + const thinkingContent = getThinkingContent(message); // Get tool requests from the message const toolRequests = message.content @@ -121,16 +121,11 @@ export const SessionMessages: React.FC = ({
- {/* Reasoning content */} - {reasoningContent && ( -
- - Show reasoning - -
- -
-
+ {/* Thinking content */} + {thinkingContent && ( +
+ +
)} {/* Text content */} diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 4c270949a4..ad7d4d7fc2 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -187,6 +187,21 @@ function pushMessage(currentMessages: Message[], incomingMsg: Message): Message[ incomingMsg.content.length === 1 ) { lastContent.text += newContent.text; + } else if ( + lastContent?.type === 'thinking' && + newContent?.type === 'thinking' && + incomingMsg.content.length === 1 && + 'thinking' in lastContent && + 'thinking' in newContent + ) { + // For thinking blocks: if the new block has a signature, it's the complete + // block from content_block_stop — replace entirely. Otherwise append the delta. + if ('signature' in newContent && newContent.signature) { + lastContent.thinking = newContent.thinking; + lastContent.signature = newContent.signature; + } else { + lastContent.thinking += newContent.thinking; + } } else { lastMsg.content.push(...incomingMsg.content); } diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 5f3a2b552d..0d94effa82 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -97,16 +97,16 @@ export function getTextAndImageContent(message: Message): { return { textContent, imagePaths }; } -export function getReasoningContent(message: Message): string | null { - const reasoningContents = message.content - .filter((content) => content.type === 'reasoning') +export function getThinkingContent(message: Message): string | null { + const thinkingContents = message.content + .filter((content) => content.type === 'thinking') .map((content) => { - if ('text' in content) return content.text; + if ('thinking' in content) return content.thinking; return ''; }) .filter((text) => text.length > 0); - return reasoningContents.length > 0 ? reasoningContents.join('') : null; + return thinkingContents.length > 0 ? thinkingContents.join('') : null; } export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {