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 c4801c41..8472cc8a 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 2f021f94..486df8ae 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 67cb59b2..313de96b 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 7db69db5..f6847e46 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 282df3f4..54cf8c77 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 f1e0c796..d920dc5e 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 93427540..5c9a9503 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 33ac9f0d..fb712cc3 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 5805917f..252ba404 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 465b9d35..5ca05fa5 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 5c2751b9..c5cce0f1 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 20bbf79c..cd301e50 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 25e69576..9ecb7fdd 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 12045786..3b586b79 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 80c8702e..e48541ec 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 794ae59f..71227fb3 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 fe29ffdf..ee1ad911 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 1cf15fdb..cbca5a4b 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 4c270949..ad7d4d7f 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 5f3a2b55..0d94effa 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' })[] {