From 7ec6b4bdce47a6119549d0e651b2cc213940fb66 Mon Sep 17 00:00:00 2001 From: David Katz Date: Tue, 2 Sep 2025 17:04:05 -0400 Subject: [PATCH 01/12] pass 1 --- crates/goose/src/agents/reply_parts.rs | 1 + crates/goose/src/context_mgmt/auto_compact.rs | 31 +++- crates/goose/src/conversation/message.rs | 138 ++++++++++++++++++ crates/goose/src/providers/base.rs | 23 ++- crates/goose/src/providers/claude_code.rs | 2 + crates/goose/src/providers/cursor_agent.rs | 3 + crates/goose/src/providers/formats/openai.rs | 2 + 7 files changed, 194 insertions(+), 6 deletions(-) diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 14cbc7888e50..c165f9ce3efb 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -267,6 +267,7 @@ impl Agent { role: response.role.clone(), created: response.created, content: filtered_content, + metadata: response.metadata.clone(), }; // Categorize tool requests diff --git a/crates/goose/src/context_mgmt/auto_compact.rs b/crates/goose/src/context_mgmt/auto_compact.rs index b5dbf46fc516..94203716b855 100644 --- a/crates/goose/src/context_mgmt/auto_compact.rs +++ b/crates/goose/src/context_mgmt/auto_compact.rs @@ -177,17 +177,42 @@ pub async fn check_and_compact_messages( }; // Perform the compaction on messages excluding the preserved user message - let (mut compacted_messages, _, summarization_usage) = + let (summary_messages, _, summarization_usage) = agent.summarize_context(messages_to_compact).await?; + // Create the final message list with updated visibility metadata: + // 1. Original messages before summary become user_visible but not agent_visible + // 2. Summary message becomes agent_visible but not user_visible + // 3. Messages after summary remain both user_visible and agent_visible + + let mut final_messages = Vec::new(); + + // Add all original messages before compaction with updated visibility + // (user_visible=true, agent_visible=false) + for mut msg in messages_to_compact.iter().cloned() { + msg.metadata.user_visible = true; + msg.metadata.agent_visible = false; + final_messages.push(msg); + } + + // Add the summary message(s) with updated visibility + // (user_visible=false, agent_visible=true) + for msg in summary_messages.messages().iter().cloned() { + let mut updated_msg = msg; + updated_msg.metadata.user_visible = false; + updated_msg.metadata.agent_visible = true; + final_messages.push(updated_msg); + } + // Add back the preserved user message if it exists + // (keeps default visibility: both true) if let Some(user_message) = preserved_user_message { - compacted_messages.push(user_message); + final_messages.push(user_message); } Ok(AutoCompactResult { compacted: true, - messages: compacted_messages, + messages: Conversation::new_unvalidated(final_messages), summarization_usage, }) } diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 107b88b70e27..06c46ec536f3 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -364,6 +364,31 @@ impl From for Message { } } +#[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize)] +/// Metadata for message visibility +#[serde(rename_all = "camelCase")] +pub struct MessageMetadata { + /// Whether the message should be visible to the user in the UI + #[serde(default = "default_true")] + pub user_visible: bool, + /// Whether the message should be included in the agent's context window + #[serde(default = "default_true")] + pub agent_visible: bool, +} + +impl Default for MessageMetadata { + fn default() -> Self { + MessageMetadata { + user_visible: true, + agent_visible: true, + } + } +} + +fn default_true() -> bool { + true +} + #[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize)] /// A message to or from an LLM #[serde(rename_all = "camelCase")] @@ -374,6 +399,8 @@ pub struct Message { pub created: i64, #[serde(deserialize_with = "deserialize_sanitized_content")] pub content: Vec, + #[serde(default)] + pub metadata: MessageMetadata, } impl fmt::Debug for Message { @@ -400,6 +427,7 @@ impl Message { role, created, content, + metadata: MessageMetadata::default(), } } pub fn debug(&self) -> String { @@ -413,6 +441,7 @@ impl Message { role: Role::User, created: Utc::now().timestamp(), content: Vec::new(), + metadata: MessageMetadata::default(), } } @@ -423,6 +452,7 @@ impl Message { role: Role::Assistant, created: Utc::now().timestamp(), content: Vec::new(), + metadata: MessageMetadata::default(), } } @@ -587,6 +617,37 @@ impl Message { pub fn with_summarization_requested>(self, msg: S) -> Self { self.with_content(MessageContent::summarization_requested(msg)) } + + /// Set the visibility metadata for the message + pub fn with_visibility(mut self, user_visible: bool, agent_visible: bool) -> Self { + self.metadata.user_visible = user_visible; + self.metadata.agent_visible = agent_visible; + self + } + + /// Mark the message as only visible to the user (not the agent) + pub fn user_only(mut self) -> Self { + self.metadata.user_visible = true; + self.metadata.agent_visible = false; + self + } + + /// Mark the message as only visible to the agent (not the user) + pub fn agent_only(mut self) -> Self { + self.metadata.user_visible = false; + self.metadata.agent_visible = true; + self + } + + /// Check if the message is visible to the user + pub fn is_user_visible(&self) -> bool { + self.metadata.user_visible + } + + /// Check if the message is visible to the agent + pub fn is_agent_visible(&self) -> bool { + self.metadata.agent_visible + } } #[cfg(test)] @@ -932,4 +993,81 @@ mod tests { assert_eq!(message.as_concat_text(), "Hello world δΈ–η•Œ 🌍"); } + + #[test] + fn test_message_metadata_defaults() { + let message = Message::user().with_text("Test"); + + // By default, messages should be both user and agent visible + assert!(message.is_user_visible()); + assert!(message.is_agent_visible()); + } + + #[test] + fn test_message_visibility_methods() { + // Test user_only + let user_only_msg = Message::user().with_text("User only").user_only(); + assert!(user_only_msg.is_user_visible()); + assert!(!user_only_msg.is_agent_visible()); + + // Test agent_only + let agent_only_msg = Message::assistant().with_text("Agent only").agent_only(); + assert!(!agent_only_msg.is_user_visible()); + assert!(agent_only_msg.is_agent_visible()); + + // Test with_visibility + let custom_msg = Message::user() + .with_text("Custom visibility") + .with_visibility(false, true); + assert!(!custom_msg.is_user_visible()); + assert!(custom_msg.is_agent_visible()); + } + + #[test] + fn test_message_metadata_serialization() { + let message = Message::user() + .with_text("Test message") + .with_visibility(false, true); + + let json_str = serde_json::to_string(&message).unwrap(); + let value: Value = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(value["metadata"]["userVisible"], false); + assert_eq!(value["metadata"]["agentVisible"], true); + } + + #[test] + fn test_message_metadata_deserialization() { + // Test with explicit metadata + let json_with_metadata = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "text", + "text": "Test" + }], + "metadata": { + "userVisible": false, + "agentVisible": true + } + }"#; + + let message: Message = serde_json::from_str(json_with_metadata).unwrap(); + assert!(!message.is_user_visible()); + assert!(message.is_agent_visible()); + + // Test without metadata (should use defaults) + let json_without_metadata = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "text", + "text": "Test" + }] + }"#; + + let message: Message = serde_json::from_str(json_without_metadata).unwrap(); + assert!(message.is_user_visible()); + assert!(message.is_agent_visible()); + } } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index a71bc2113a29..0a8bff82606a 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -328,6 +328,7 @@ pub trait Provider: Send + Sync { ) -> Result<(Message, ProviderUsage), ProviderError>; // Default implementation: use the provider's configured model + // This method filters messages to only include agent_visible ones async fn complete( &self, system: &str, @@ -335,11 +336,20 @@ pub trait Provider: Send + Sync { tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { let model_config = self.get_model_config(); - self.complete_with_model(&model_config, system, messages, tools) + + // Filter messages to only include agent_visible ones + let agent_visible_messages: Vec = messages + .iter() + .filter(|m| m.is_agent_visible()) + .cloned() + .collect(); + + self.complete_with_model(&model_config, system, &agent_visible_messages, tools) .await } // Check if a fast model is configured, otherwise fall back to regular model + // This method filters messages to only include agent_visible ones async fn complete_fast( &self, system: &str, @@ -349,8 +359,15 @@ pub trait Provider: Send + Sync { let model_config = self.get_model_config(); let fast_config = model_config.use_fast_model(); + // Filter messages to only include agent_visible ones + let agent_visible_messages: Vec = messages + .iter() + .filter(|m| m.is_agent_visible()) + .cloned() + .collect(); + match self - .complete_with_model(&fast_config, system, messages, tools) + .complete_with_model(&fast_config, system, &agent_visible_messages, tools) .await { Ok(result) => Ok(result), @@ -362,7 +379,7 @@ pub trait Provider: Send + Sync { e, model_config.model_name ); - self.complete_with_model(&model_config, system, messages, tools) + self.complete_with_model(&model_config, system, &agent_visible_messages, tools) .await } else { Err(e) diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 9ef2950c272c..81f8e695ed25 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -287,6 +287,7 @@ impl ClaudeCodeProvider { role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: message_content, + metadata: crate::conversation::message::MessageMetadata::default(), }; Ok((response_message, usage)) @@ -438,6 +439,7 @@ impl ClaudeCodeProvider { role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: vec![MessageContent::text(description.clone())], + metadata: crate::conversation::message::MessageMetadata::default(), }; let usage = Usage::default(); diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index bbce315f648b..56cdd63dda12 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -219,6 +219,7 @@ impl CursorAgentProvider { role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: message_content, + metadata: crate::conversation::message::MessageMetadata::default(), }; let usage = Usage::default(); @@ -238,6 +239,7 @@ impl CursorAgentProvider { role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: message_content, + metadata: crate::conversation::message::MessageMetadata::default(), }; let usage = Usage::default(); @@ -371,6 +373,7 @@ impl CursorAgentProvider { role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: vec![MessageContent::text(description.clone())], + metadata: crate::conversation::message::MessageMetadata::default(), }; let usage = Usage::default(); diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 3ff4b712a867..3423543816f3 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -540,6 +540,7 @@ where role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: contents, + metadata: crate::conversation::message::MessageMetadata::default(), }), usage, ) @@ -550,6 +551,7 @@ where role: Role::Assistant, created: chrono::Utc::now().timestamp(), content: vec![MessageContent::text(text)], + metadata: crate::conversation::message::MessageMetadata::default(), }), if chunk.choices[0].finish_reason.is_some() { usage From d4b9b5ca41b6f95a6c169e9001328f3414cad907 Mon Sep 17 00:00:00 2001 From: David Katz Date: Thu, 4 Sep 2025 11:28:55 -0400 Subject: [PATCH 02/12] swap constructors --- crates/goose/src/agents/reply_parts.rs | 17 +++++---- crates/goose/src/providers/claude_code.rs | 24 ++++++------- crates/goose/src/providers/cursor_agent.rs | 36 ++++++++----------- crates/goose/src/providers/formats/openai.rs | 38 ++++++++++++-------- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index c165f9ce3efb..4ed8bc1a342d 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -262,13 +262,16 @@ impl Agent { } } - let filtered_message = Message { - id: response.id.clone(), - role: response.role.clone(), - created: response.created, - content: filtered_content, - metadata: response.metadata.clone(), - }; + let mut filtered_message = Message::new( + response.role.clone(), + response.created, + filtered_content, + ); + + // Preserve the ID if it exists + if let Some(id) = response.id.clone() { + filtered_message = filtered_message.with_id(id); + } // Categorize tool requests let mut frontend_requests = Vec::new(); diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 81f8e695ed25..b474432b3607 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -282,13 +282,11 @@ impl ClaudeCodeProvider { let message_content = vec![MessageContent::text(combined_text)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - metadata: crate::conversation::message::MessageMetadata::default(), - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); Ok((response_message, usage)) } @@ -434,13 +432,11 @@ impl ClaudeCodeProvider { println!("================================"); } - let message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(description.clone())], - metadata: crate::conversation::message::MessageMetadata::default(), - }; + let message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(description.clone())], + ); let usage = Usage::default(); diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 56cdd63dda12..a3f0d12858db 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -214,13 +214,11 @@ impl CursorAgentProvider { }; let message_content = vec![MessageContent::text(text_content)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - metadata: crate::conversation::message::MessageMetadata::default(), - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); let usage = Usage::default(); @@ -234,13 +232,11 @@ impl CursorAgentProvider { let response_text = lines.join("\n"); let message_content = vec![MessageContent::text(response_text)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - metadata: crate::conversation::message::MessageMetadata::default(), - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); let usage = Usage::default(); Ok((response_message, usage)) @@ -368,13 +364,11 @@ impl CursorAgentProvider { println!("================================"); } - let message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(description.clone())], - metadata: crate::conversation::message::MessageMetadata::default(), - }; + let message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(description.clone())], + ); let usage = Usage::default(); diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 3423543816f3..9e60b1d358fe 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -534,25 +534,35 @@ where } } + let mut msg = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + contents, + ); + + // Add ID if present + if let Some(id) = chunk.id { + msg = msg.with_id(id); + } + yield ( - Some(Message { - id: chunk.id, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: contents, - metadata: crate::conversation::message::MessageMetadata::default(), - }), + Some(msg), usage, ) } else if let Some(text) = &chunk.choices[0].delta.content { + let mut msg = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(text)], + ); + + // Add ID if present + if let Some(id) = chunk.id { + msg = msg.with_id(id); + } + yield ( - Some(Message { - id: chunk.id, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(text)], - metadata: crate::conversation::message::MessageMetadata::default(), - }), + Some(msg), if chunk.choices[0].finish_reason.is_some() { usage } else { From 0e2c186d9230442ba5c26df5bb17fc82bf31558f Mon Sep 17 00:00:00 2001 From: David Katz Date: Thu, 4 Sep 2025 12:36:23 -0400 Subject: [PATCH 03/12] client loading --- crates/goose-server/src/routes/context.rs | 7 ++++++- crates/goose-server/src/routes/reply.rs | 6 +++++- crates/goose-server/src/routes/session.rs | 10 +++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/goose-server/src/routes/context.rs b/crates/goose-server/src/routes/context.rs index e64f3fa6b635..f2303f2d8a9e 100644 --- a/crates/goose-server/src/routes/context.rs +++ b/crates/goose-server/src/routes/context.rs @@ -74,7 +74,12 @@ async fn manage_context( } Ok(Json(ContextManageResponse { - messages: processed_messages.messages().clone(), + messages: processed_messages + .messages() + .iter() + .filter(|m| m.is_user_visible()) + .cloned() + .collect(), token_counts, })) } diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 0b75a913777e..6c8050b63fc4 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -296,7 +296,11 @@ async fn reply_handler( } all_messages.push(message.clone()); - stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; + + // Only send message to client if it's user_visible + if message.is_user_visible() { + stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; + } } Ok(Some(Ok(AgentEvent::HistoryReplaced(new_messages)))) => { // Replace the message history with the compacted messages diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index f9afb0e41b8c..8dfe3fb90408 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -134,10 +134,18 @@ async fn get_session_history( } }; + // Filter messages to only include user_visible ones + let user_visible_messages: Vec = messages + .messages() + .iter() + .filter(|m| m.is_user_visible()) + .cloned() + .collect(); + Ok(Json(SessionHistoryResponse { session_id, metadata, - messages: messages.messages().clone(), + messages: user_visible_messages, })) } From 16efe594f83d2324cd7d384e825e6ec95208e29c Mon Sep 17 00:00:00 2001 From: David Katz Date: Thu, 4 Sep 2025 13:01:53 -0400 Subject: [PATCH 04/12] Remove client-side message visibility complexity - Remove display and sendToLLM fields from TypeScript Message interface - Remove ancestor messages concept (redundant with server-side metadata) - Update convertApiMessageToFrontendMessage to not set visibility flags - Fix all test files to remove references to display/sendToLLM - Trust server-side filtering as single source of truth for message visibility - Simplify client to just display all messages it receives from server --- ui/desktop/src/components/BaseChat.tsx | 12 +- ui/desktop/src/components/ChatInput.tsx | 4 +- .../context_management/ContextManager.tsx | 54 ++----- .../__tests__/CompactionMarker.test.tsx | 10 -- .../__tests__/ContextManager.test.tsx | 132 +++--------------- .../components/context_management/index.ts | 10 +- .../sessions/SessionHistoryView.tsx | 2 +- ui/desktop/src/hooks/useChatEngine.ts | 12 +- ui/desktop/src/hooks/useMessageStream.ts | 24 +--- ui/desktop/src/sessions.ts | 2 +- ui/desktop/src/types/message.ts | 2 - 11 files changed, 44 insertions(+), 220 deletions(-) diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index 096320a3f0de..02f4e348184e 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -125,7 +125,6 @@ function BaseChatContent({ const { messages, filteredMessages, - setAncestorMessages, append, chatState, error, @@ -196,14 +195,13 @@ function BaseChatContent({ console.log('Switching from recipe:', previousTitle, 'to:', newTitle); setHasStartedUsingRecipe(false); setMessages([]); - setAncestorMessages([]); } else if (isInitialRecipeLoad) { setHasStartedUsingRecipe(false); } else if (hasExistingConversation) { setHasStartedUsingRecipe(true); } } - }, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages, setAncestorMessages]); + }, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages]); // Handle recipe auto-execution useEffect(() => { @@ -397,12 +395,7 @@ function BaseChatContent({ onClick={async () => { clearError(); - await handleManualCompaction( - messages, - setMessages, - append, - setAncestorMessages - ); + await handleManualCompaction(messages, setMessages, append); }} > Summarize Conversation @@ -473,7 +466,6 @@ function BaseChatContent({ recipeAccepted={recipeAccepted} initialPrompt={initialPrompt} autoSubmit={autoSubmit} - setAncestorMessages={setAncestorMessages} append={append} {...customChatInputProps} /> diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index dba2f0080046..4ac9bb559439 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -84,7 +84,6 @@ interface ChatInputProps { recipeAccepted?: boolean; initialPrompt?: string; autoSubmit: boolean; - setAncestorMessages?: (messages: Message[]) => void; append?: (message: Message) => void; } @@ -110,7 +109,6 @@ export default function ChatInput({ initialPrompt, autoSubmit = false, append, - setAncestorMessages, }: ChatInputProps) { const [_value, setValue] = useState(initialValue); const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback @@ -532,7 +530,7 @@ export default function ChatInput({ // Hide the alert popup by dispatching a custom event that the popover can listen to // Importantly, this leaves the alert so the dot still shows up, but hides the popover window.dispatchEvent(new CustomEvent('hide-alert-popover')); - handleManualCompaction(messages, setMessages, append, setAncestorMessages); + handleManualCompaction(messages, setMessages, append); }, compactIcon: , }); diff --git a/ui/desktop/src/components/context_management/ContextManager.tsx b/ui/desktop/src/components/context_management/ContextManager.tsx index d2f660eeb558..f95e396cb395 100644 --- a/ui/desktop/src/components/context_management/ContextManager.tsx +++ b/ui/desktop/src/components/context_management/ContextManager.tsx @@ -12,14 +12,12 @@ interface ContextManagerActions { handleAutoCompaction: ( messages: Message[], setMessages: (messages: Message[]) => void, - append: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append: (message: Message) => void ) => Promise; handleManualCompaction: ( messages: Message[], setMessages: (messages: Message[]) => void, - append?: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append?: (message: Message) => void ) => Promise; hasCompactionMarker: (message: Message) => boolean; } @@ -39,8 +37,7 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( messages: Message[], setMessages: (messages: Message[]) => void, append: (message: Message) => void, - isManual: boolean = false, - setAncestorMessages?: (messages: Message[]) => void + isManual: boolean = false ) => { setIsCompacting(true); setCompactionError(null); @@ -53,29 +50,10 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( }); // Convert API messages to frontend messages - const convertedMessages = summaryResponse.messages.map((apiMessage) => { - const isCompactionMarker = apiMessage.content.some( - (content) => content.type === 'summarizationRequested' - ); - - if (isCompactionMarker) { - // show to user but not model - return convertApiMessageToFrontendMessage(apiMessage, true, false); - } - - // show to model but not user - return convertApiMessageToFrontendMessage(apiMessage, false, true); - }); - - // Store the original messages as ancestor messages so they can still be scrolled to - if (setAncestorMessages) { - const ancestorMessages = messages.map((msg) => ({ - ...msg, - display: msg.display === false ? false : true, - sendToLLM: false, - })); - setAncestorMessages(ancestorMessages); - } + // The server now handles all visibility - we just display what we receive + const convertedMessages = summaryResponse.messages.map((apiMessage) => + convertApiMessageToFrontendMessage(apiMessage) + ); // Replace messages with the server-provided messages setMessages(convertedMessages); @@ -109,8 +87,6 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( msg: 'Compaction failed. Please try again or start a new session.', }, ], - display: true, - sendToLLM: false, }; setMessages([...messages, errorMarker]); @@ -124,10 +100,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( async ( messages: Message[], setMessages: (messages: Message[]) => void, - append: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append: (message: Message) => void ) => { - await performCompaction(messages, setMessages, append, false, setAncestorMessages); + await performCompaction(messages, setMessages, append, false); }, [performCompaction] ); @@ -136,16 +111,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( async ( messages: Message[], setMessages: (messages: Message[]) => void, - append?: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append?: (message: Message) => void ) => { - await performCompaction( - messages, - setMessages, - append || (() => {}), - true, - setAncestorMessages - ); + await performCompaction(messages, setMessages, append || (() => {}), true); }, [performCompaction] ); diff --git a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx index 5c1cce1a8646..8e1691ba24b9 100644 --- a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx +++ b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx @@ -10,8 +10,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [{ type: 'text', text: 'Regular message' }], - display: true, - sendToLLM: false, }; render(); @@ -28,8 +26,6 @@ describe('CompactionMarker', () => { { type: 'text', text: 'Some other content' }, { type: 'summarizationRequested', msg: 'Custom compaction message' }, ], - display: true, - sendToLLM: false, }; render(); @@ -43,8 +39,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [], - display: true, - sendToLLM: false, }; render(); @@ -58,8 +52,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [{ type: 'summarizationRequested', msg: '' }], - display: true, - sendToLLM: false, }; render(); @@ -75,8 +67,6 @@ describe('CompactionMarker', () => { created: 1000, // eslint-disable-next-line @typescript-eslint/no-explicit-any content: [{ type: 'summarizationRequested' } as any], - display: true, - sendToLLM: false, }; render(); diff --git a/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx b/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx index 20f5152c1a6b..453d8cfa5a36 100644 --- a/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx +++ b/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx @@ -23,16 +23,12 @@ describe('ContextManager', () => { role: 'user', created: 1000, content: [{ type: 'text', text: 'Hello' }], - display: true, - sendToLLM: true, }, { id: '2', role: 'assistant', created: 2000, content: [{ type: 'text', text: 'Hi there!' }], - display: true, - sendToLLM: true, }, ]; @@ -41,13 +37,10 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'text', text: 'This is a summary of the conversation.' }], - display: false, - sendToLLM: true, }; const mockSetMessages = vi.fn(); const mockAppend = vi.fn(); - const mockSetAncestorMessages = vi.fn(); beforeEach(() => { vi.clearAllMocks(); @@ -84,8 +77,6 @@ describe('ContextManager', () => { role: 'assistant', created: 1000, content: [{ type: 'summarizationRequested', msg: 'Compaction marker' }], - display: true, - sendToLLM: false, }; expect(result.current.hasCompactionMarker(messageWithMarker)).toBe(true); @@ -98,8 +89,6 @@ describe('ContextManager', () => { role: 'user', created: 1000, content: [{ type: 'text', text: 'Hello' }], - display: true, - sendToLLM: true, }; expect(result.current.hasCompactionMarker(regularMessage)).toBe(false); @@ -115,8 +104,6 @@ describe('ContextManager', () => { { type: 'text', text: 'Some text' }, { type: 'summarizationRequested', msg: 'Compaction marker' }, ], - display: true, - sendToLLM: false, }; expect(result.current.hasCompactionMarker(mixedMessage)).toBe(true); @@ -156,8 +143,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -170,25 +155,18 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; // Mock the conversion function to return different messages based on call order mockConvertApiMessageToFrontendMessage - .mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker (display: true, sendToLLM: false) - .mockReturnValueOnce(mockSummaryMessage) // Second call - summary (display: false, sendToLLM: true) - .mockReturnValueOnce(mockContinuationMessage); // Third call - continuation (display: false, sendToLLM: true) + .mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker + .mockReturnValueOnce(mockSummaryMessage) // Second call - summary + .mockReturnValueOnce(mockContinuationMessage); // Third call - continuation const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(mockManageContextFromBackend).toHaveBeenCalledWith({ @@ -203,17 +181,13 @@ describe('ContextManager', () => { content: [ { type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }, ], - }), - true, // display: true - false // sendToLLM: false + }) ); expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ content: [{ type: 'text', text: 'Summary content' }], - }), - false, // display: false - true // sendToLLM: true + }) ); expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith( 3, @@ -224,24 +198,7 @@ describe('ContextManager', () => { text: expect.stringContaining('The previous message contains a summary'), }, ], - }), - false, // display: false - true // sendToLLM: true - ); - - expect(mockSetAncestorMessages).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: '1', - display: true, - sendToLLM: false, - }), - expect.objectContaining({ - id: '2', - display: true, - sendToLLM: false, - }), - ]) + }) ); // Expect setMessages to be called with all 3 converted messages @@ -268,12 +225,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(result.current.compactionError).toBe('Backend error'); @@ -304,12 +256,7 @@ describe('ContextManager', () => { // Start compaction act(() => { - result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); // Should be compacting @@ -336,8 +283,8 @@ describe('ContextManager', () => { // Should no longer be compacting expect(result.current.isCompacting).toBe(false); }); + it('preserves display: false for ancestor messages', async () => { - // Backend returns no new messages; we're validating ancestor behavior only mockManageContextFromBackend.mockResolvedValue({ messages: [], tokenCounts: [] }); const hiddenMessage: Message = { @@ -345,8 +292,6 @@ describe('ContextManager', () => { role: 'user', created: 1500, content: [{ type: 'text', text: 'Secret' }], - display: false, - sendToLLM: true, }; const visibleMessage: Message = { @@ -354,8 +299,6 @@ describe('ContextManager', () => { role: 'assistant', created: 1600, content: [{ type: 'text', text: 'Public' }], - display: true, - sendToLLM: true, }; const messages: Message[] = [hiddenMessage, visibleMessage]; @@ -363,21 +306,9 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - messages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(messages, mockSetMessages, mockAppend); }); - expect(mockSetAncestorMessages).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ id: 'hidden-1', display: false, sendToLLM: false }), - expect.objectContaining({ id: 'visible-1', display: true, sendToLLM: false }), - ]) - ); - // No server messages -> setMessages called with empty list expect(mockSetMessages).toHaveBeenCalledWith([]); expect(mockAppend).not.toHaveBeenCalled(); @@ -416,8 +347,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -430,8 +359,6 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage @@ -442,12 +369,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleManualCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(mockManageContextFromBackend).toHaveBeenCalledWith({ @@ -490,8 +412,7 @@ describe('ContextManager', () => { await result.current.handleManualCompaction( mockMessages, mockSetMessages, - undefined, // No append function - mockSetAncestorMessages + undefined // No append function ); }); @@ -538,8 +459,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -552,8 +471,6 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage @@ -564,12 +481,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleManualCompaction( - mockMessages, - mockSetMessages, - mockAppend, // Provide append function - mockSetAncestorMessages - ); + await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend); }); // Verify all three messages are set @@ -596,12 +508,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(result.current.compactionError).toBe('Unknown error during compaction'); @@ -625,8 +532,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'toolResponse', id: 'test', toolResult: { status: 'success' } }], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage.mockReturnValue(mockMessageWithoutText); @@ -634,12 +539,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); // Should complete without error even if content is not text diff --git a/ui/desktop/src/components/context_management/index.ts b/ui/desktop/src/components/context_management/index.ts index 1f656adbf4c3..68ddbb8cf013 100644 --- a/ui/desktop/src/components/context_management/index.ts +++ b/ui/desktop/src/components/context_management/index.ts @@ -50,14 +50,8 @@ export async function manageContextFromBackend({ } // Function to convert API Message to frontend Message -export function convertApiMessageToFrontendMessage( - apiMessage: ApiMessage, - display?: boolean, - sendToLLM?: boolean -): FrontendMessage { +export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage { return { - display: display ?? true, - sendToLLM: sendToLLM ?? true, id: generateId(), role: apiMessage.role as Role, created: apiMessage.created ?? Math.floor(Date.now() / 1000), @@ -149,7 +143,5 @@ export function createSummarizationRequestMessage( msg: requestMessage, }, ], - sendToLLM: false, - display: true, }; } diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index 713f97d0d81d..8c7abf3a5c4e 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -45,7 +45,7 @@ const isUserMessage = (message: Message): boolean => { }; const filterMessagesForDisplay = (messages: Message[]): Message[] => { - return messages.filter((message) => message.display ?? true); + return messages; }; interface SessionHistoryViewProps { diff --git a/ui/desktop/src/hooks/useChatEngine.ts b/ui/desktop/src/hooks/useChatEngine.ts index 8c53f0a40d36..b4bd746e1f73 100644 --- a/ui/desktop/src/hooks/useChatEngine.ts +++ b/ui/desktop/src/hooks/useChatEngine.ts @@ -45,7 +45,6 @@ export const useChatEngine = ({ }: UseChatEngineProps) => { const [lastInteractionTime, setLastInteractionTime] = useState(Date.now()); const [sessionTokenCount, setSessionTokenCount] = useState(0); - const [ancestorMessages, setAncestorMessages] = useState([]); const [sessionInputTokens, setSessionInputTokens] = useState(0); const [sessionOutputTokens, setSessionOutputTokens] = useState(0); const [localInputTokens, setLocalInputTokens] = useState(0); @@ -362,8 +361,6 @@ export const useChatEngine = ({ // Create tool responses for all interrupted tool requests let responseMessage: Message = { - display: true, - sendToLLM: true, role: 'user', created: Date.now(), content: [], @@ -390,11 +387,12 @@ export const useChatEngine = ({ } }, [stop, messages, _setInput, setMessages, stopPowerSaveBlocker, enableLocalStorage]); + // Since server now handles all filtering, we just use messages directly const filteredMessages = useMemo(() => { - return [...ancestorMessages, ...messages].filter((message) => message.display ?? true); - }, [ancestorMessages, messages]); + return messages; + }, [messages]); - // Generate command history from filtered messages + // Generate command history from messages const commandHistory = useMemo(() => { return filteredMessages .reduce((history, message) => { @@ -454,8 +452,6 @@ export const useChatEngine = ({ // Core message data messages, filteredMessages, - ancestorMessages, - setAncestorMessages, // Message stream controls append, diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts index c6e01d170caa..bc5c07dd793e 100644 --- a/ui/desktop/src/hooks/useMessageStream.ts +++ b/ui/desktop/src/hooks/useMessageStream.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react'; import useSWR from 'swr'; -import { createUserMessage, hasCompletedToolCalls, Message } from '../types/message'; +import { createUserMessage, hasCompletedToolCalls, Message, Role } from '../types/message'; import { getSessionHistory } from '../api'; import { ChatState } from '../types/chatState'; @@ -286,19 +286,12 @@ export function useMessageStream({ mutateChatState(ChatState.Streaming); // Create a new message object with the properties preserved or defaulted - const newMessage = { + const newMessage: Message = { ...parsedEvent.message, - // Ensure the message has an ID - if not provided, generate one - id: parsedEvent.message.id || generateMessageId(), - // Only set to true if it's undefined (preserve false values) - display: - parsedEvent.message.display === undefined - ? true - : parsedEvent.message.display, - sendToLLM: - parsedEvent.message.sendToLLM === undefined - ? true - : parsedEvent.message.sendToLLM, + id: parsedEvent.message.id || undefined, + role: parsedEvent.message.role as Role, + created: parsedEvent.message.created || Date.now(), + content: parsedEvent.message.content || [], }; // Update messages with the new message @@ -435,9 +428,6 @@ export function useMessageStream({ const abortController = new AbortController(); abortControllerRef.current = abortController; - // Filter out messages where sendToLLM is explicitly false - const filteredMessages = requestMessages.filter((message) => message.sendToLLM !== false); - // Send request to the server const response = await fetch(api, { method: 'POST', @@ -447,7 +437,7 @@ export function useMessageStream({ ...extraMetadataRef.current.headers, }, body: JSON.stringify({ - messages: filteredMessages, + messages: requestMessages, ...extraMetadataRef.current.body, }), signal: abortController.signal, diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts index ef560a372276..bad941126196 100644 --- a/ui/desktop/src/sessions.ts +++ b/ui/desktop/src/sessions.ts @@ -119,7 +119,7 @@ export async function fetchSessionDetails(sessionId: string): Promise - convertApiMessageToFrontendMessage(message, true, true) + convertApiMessageToFrontendMessage(message) ), // slight diffs between backend and frontend Message obj }; } catch (error) { diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 3c45bc75d7ad..578636d9c152 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -108,8 +108,6 @@ export interface Message { role: Role; created: number; content: MessageContent[]; - display?: boolean; - sendToLLM?: boolean; } // Helper functions to create messages From e8c45e8f09325b83a0724db4208750ebab72ebdf Mon Sep 17 00:00:00 2001 From: David Katz Date: Thu, 4 Sep 2025 16:48:04 -0400 Subject: [PATCH 05/12] fix assertion --- crates/goose/src/context_mgmt/auto_compact.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/goose/src/context_mgmt/auto_compact.rs b/crates/goose/src/context_mgmt/auto_compact.rs index 94203716b855..f46cce92bcf6 100644 --- a/crates/goose/src/context_mgmt/auto_compact.rs +++ b/crates/goose/src/context_mgmt/auto_compact.rs @@ -459,8 +459,9 @@ mod tests { ); } - // Should have fewer messages (summarized) - assert!(result.messages.len() <= messages.len()); + // After visibility implementation, we keep all messages plus summary + // Original messages become user_visible only, summary becomes agent_visible only + assert!(result.messages.len() > messages.len()); } #[tokio::test] @@ -645,8 +646,9 @@ mod tests { // Verify the compacted messages are returned assert!(!result.messages.is_empty()); - // Should have fewer messages after compaction - assert!(result.messages.len() <= messages.len()); + // After visibility implementation, we keep all messages plus summary + // Original messages become user_visible only, summary becomes agent_visible only + assert!(result.messages.len() > messages.len()); } #[tokio::test] From c0010a8741594441aa9f9d5443467eb73d70ffa1 Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 5 Sep 2025 13:32:34 -0400 Subject: [PATCH 06/12] merge conflicts --- crates/goose-server/src/routes/reply.rs | 2 +- crates/goose/src/agents/reply_parts.rs | 9 +++------ crates/goose/src/context_mgmt/auto_compact.rs | 8 ++++---- crates/goose/src/conversation/message.rs | 2 +- crates/goose/src/providers/base.rs | 4 ++-- crates/goose/src/providers/formats/openai.rs | 8 ++++---- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 92c0299e6b5d..79ba00c72200 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -313,7 +313,7 @@ async fn reply_handler( } all_messages.push(message.clone()); - + // Only send message to client if it's user_visible if message.is_user_visible() { stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index e06636fb8aa4..d6ae3805dc51 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -268,12 +268,9 @@ impl Agent { } } - let mut filtered_message = Message::new( - response.role.clone(), - response.created, - filtered_content, - ); - + let mut filtered_message = + Message::new(response.role.clone(), response.created, filtered_content); + // Preserve the ID if it exists if let Some(id) = response.id.clone() { filtered_message = filtered_message.with_id(id); diff --git a/crates/goose/src/context_mgmt/auto_compact.rs b/crates/goose/src/context_mgmt/auto_compact.rs index 65069ec1846a..406343c960e8 100644 --- a/crates/goose/src/context_mgmt/auto_compact.rs +++ b/crates/goose/src/context_mgmt/auto_compact.rs @@ -228,9 +228,9 @@ pub async fn check_and_compact_messages( // 1. Original messages before summary become user_visible but not agent_visible // 2. Summary message becomes agent_visible but not user_visible // 3. Messages after summary remain both user_visible and agent_visible - + let mut final_messages = Vec::new(); - + // Add all original messages before compaction with updated visibility // (user_visible=true, agent_visible=false) for mut msg in messages_to_compact.iter().cloned() { @@ -238,7 +238,7 @@ pub async fn check_and_compact_messages( msg.metadata.agent_visible = false; final_messages.push(msg); } - + // Add the summary message(s) with updated visibility // (user_visible=false, agent_visible=true) for msg in summary_messages.messages().iter().cloned() { @@ -247,7 +247,7 @@ pub async fn check_and_compact_messages( updated_msg.metadata.agent_visible = true; final_messages.push(updated_msg); } - + // Add back the preserved user message if it exists // (keeps default visibility: both true) if let Some(user_message) = preserved_user_message { diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 813fae94ea86..2236a7db4382 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -1018,7 +1018,7 @@ mod tests { #[test] fn test_message_metadata_defaults() { let message = Message::user().with_text("Test"); - + // By default, messages should be both user and agent visible assert!(message.is_user_visible()); assert!(message.is_agent_visible()); diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 0a8bff82606a..2c17d452f817 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -336,14 +336,14 @@ pub trait Provider: Send + Sync { tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { let model_config = self.get_model_config(); - + // Filter messages to only include agent_visible ones let agent_visible_messages: Vec = messages .iter() .filter(|m| m.is_agent_visible()) .cloned() .collect(); - + self.complete_with_model(&model_config, system, &agent_visible_messages, tools) .await } diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 9e60b1d358fe..897293109e73 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -539,12 +539,12 @@ where chrono::Utc::now().timestamp(), contents, ); - + // Add ID if present if let Some(id) = chunk.id { msg = msg.with_id(id); } - + yield ( Some(msg), usage, @@ -555,12 +555,12 @@ where chrono::Utc::now().timestamp(), vec![MessageContent::text(text)], ); - + // Add ID if present if let Some(id) = chunk.id { msg = msg.with_id(id); } - + yield ( Some(msg), if chunk.choices[0].finish_reason.is_some() { From 4ec1992c43ef37f74ae9643938b0de6782979196 Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 5 Sep 2025 16:29:09 -0400 Subject: [PATCH 07/12] mostly fix visibility backend --- crates/goose/src/agents/context.rs | 82 +++++++++++-------- crates/goose/src/context_mgmt/auto_compact.rs | 31 +------ 2 files changed, 54 insertions(+), 59 deletions(-) diff --git a/crates/goose/src/agents/context.rs b/crates/goose/src/agents/context.rs index 1a6cd5b64a5c..5b821083764e 100644 --- a/crates/goose/src/agents/context.rs +++ b/crates/goose/src/agents/context.rs @@ -64,17 +64,8 @@ impl Agent { let provider = self.provider().await?; let summary_result = summarize_messages(provider.clone(), messages).await?; - let (mut new_messages, mut new_token_counts, summarization_usage) = match summary_result { - Some((summary_message, provider_usage)) => { - // For token counting purposes, we use the output tokens (the actual summary content) - // since that's what will be in the context going forward - let total_tokens = provider_usage.usage.output_tokens.unwrap_or(0) as usize; - ( - vec![summary_message], - vec![total_tokens], - Some(provider_usage), - ) - } + let (summary_message, summarization_usage) = match summary_result { + Some((summary_message, provider_usage)) => (summary_message, Some(provider_usage)), None => { // No summary was generated (empty input) tracing::warn!("Summarization failed. Returning empty messages."); @@ -82,30 +73,57 @@ impl Agent { } }; - // Add an assistant message to the summarized messages to ensure the assistant's response is included in the context. - if new_messages.len() == 1 { - let compaction_marker = Message::assistant() - .with_summarization_requested("Conversation compacted and summarized"); - let compaction_marker_tokens: usize = 8; - - // Insert the marker before the summary message - new_messages.insert(0, compaction_marker); - new_token_counts.insert(0, compaction_marker_tokens); - - // Add an assistant message to continue the conversation - let assistant_message = Message::assistant().with_text(" - The previous message contains a summary that was prepared because a context limit was reached. - Do not mention that you read a summary or that conversation summarization occurred - Just continue the conversation naturally based on the summarized context - "); - let assistant_message_tokens: usize = 41; - new_messages.push(assistant_message); - new_token_counts.push(assistant_message_tokens); + // Create the final message list with updated visibility metadata: + // 1. Original messages become user_visible but not agent_visible + // 2. Summary message becomes agent_visible but not user_visible + // 3. Assistant messages to continue the conversation remain both user_visible and agent_visible + + let mut final_messages = Vec::new(); + let mut final_token_counts = Vec::new(); + + // Add all original messages with updated visibility (user_visible=true, agent_visible=false) + for mut msg in messages.iter().cloned() { + msg.metadata.user_visible = true; + msg.metadata.agent_visible = false; + final_messages.push(msg); + // Token count doesn't matter for agent_visible=false messages, but we'll use 0 + final_token_counts.push(0); } + // Add the compaction marker (agent_visible=true, user_visible=false) + let compaction_marker = Message::assistant() + .with_summarization_requested("Conversation compacted and summarized") + .agent_only(); + let compaction_marker_tokens: usize = 8; + final_messages.push(compaction_marker); + final_token_counts.push(compaction_marker_tokens); + + // Add the summary message (agent_visible=true, user_visible=false) + let mut summary_msg = summary_message; + summary_msg.metadata.user_visible = false; + summary_msg.metadata.agent_visible = true; + // For token counting purposes, we use the output tokens (the actual summary content) + // since that's what will be in the context going forward + let summary_tokens = summarization_usage + .as_ref() + .and_then(|usage| usage.usage.output_tokens) + .unwrap_or(0) as usize; + final_messages.push(summary_msg); + final_token_counts.push(summary_tokens); + + // Add an assistant message to continue the conversation (both user_visible and agent_visible) + let assistant_message = Message::assistant().with_text(" + The previous message contains a summary that was prepared because a context limit was reached. + Do not mention that you read a summary or that conversation summarization occurred + Just continue the conversation naturally based on the summarized context + "); + let assistant_message_tokens: usize = 41; + final_messages.push(assistant_message); + final_token_counts.push(assistant_message_tokens); + Ok(( - Conversation::new_unvalidated(new_messages), - new_token_counts, + Conversation::new_unvalidated(final_messages), + final_token_counts, summarization_usage, )) } diff --git a/crates/goose/src/context_mgmt/auto_compact.rs b/crates/goose/src/context_mgmt/auto_compact.rs index 406343c960e8..a86f7d5a07ca 100644 --- a/crates/goose/src/context_mgmt/auto_compact.rs +++ b/crates/goose/src/context_mgmt/auto_compact.rs @@ -221,42 +221,19 @@ pub async fn check_and_compact_messages( }; // Perform the compaction on messages excluding the preserved user message - let (summary_messages, _, summarization_usage) = + // The summarize_context method already handles the visibility properly + let (mut summary_messages, _, summarization_usage) = agent.summarize_context(messages_to_compact).await?; - // Create the final message list with updated visibility metadata: - // 1. Original messages before summary become user_visible but not agent_visible - // 2. Summary message becomes agent_visible but not user_visible - // 3. Messages after summary remain both user_visible and agent_visible - - let mut final_messages = Vec::new(); - - // Add all original messages before compaction with updated visibility - // (user_visible=true, agent_visible=false) - for mut msg in messages_to_compact.iter().cloned() { - msg.metadata.user_visible = true; - msg.metadata.agent_visible = false; - final_messages.push(msg); - } - - // Add the summary message(s) with updated visibility - // (user_visible=false, agent_visible=true) - for msg in summary_messages.messages().iter().cloned() { - let mut updated_msg = msg; - updated_msg.metadata.user_visible = false; - updated_msg.metadata.agent_visible = true; - final_messages.push(updated_msg); - } - // Add back the preserved user message if it exists // (keeps default visibility: both true) if let Some(user_message) = preserved_user_message { - final_messages.push(user_message); + summary_messages.push(user_message); } Ok(AutoCompactResult { compacted: true, - messages: Conversation::new_unvalidated(final_messages), + messages: summary_messages, summarization_usage, }) } From 166c4391b2c2eee5c75e12d564f36c022ebe24af Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 5 Sep 2025 17:16:33 -0400 Subject: [PATCH 08/12] set metadata --- crates/goose/src/agents/context.rs | 20 ++++----- crates/goose/src/conversation/message.rs | 52 +++++++++++++++++++++++- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/crates/goose/src/agents/context.rs b/crates/goose/src/agents/context.rs index 5b821083764e..ed5247cddd4d 100644 --- a/crates/goose/src/agents/context.rs +++ b/crates/goose/src/agents/context.rs @@ -1,6 +1,6 @@ use anyhow::Ok; -use crate::conversation::message::Message; +use crate::conversation::message::{Message, MessageMetadata}; use crate::conversation::Conversation; use crate::token_counter::create_async_token_counter; @@ -90,11 +90,11 @@ impl Agent { final_token_counts.push(0); } - // Add the compaction marker (agent_visible=true, user_visible=false) + // Add the compaction marker (user_visible=true, agent_visible=false) let compaction_marker = Message::assistant() .with_summarization_requested("Conversation compacted and summarized") - .agent_only(); - let compaction_marker_tokens: usize = 8; + .with_metadata(MessageMetadata::user_only()); + let compaction_marker_tokens: usize = 0; // Not counted since agent_visible=false final_messages.push(compaction_marker); final_token_counts.push(compaction_marker_tokens); @@ -111,13 +111,11 @@ impl Agent { final_messages.push(summary_msg); final_token_counts.push(summary_tokens); - // Add an assistant message to continue the conversation (both user_visible and agent_visible) - let assistant_message = Message::assistant().with_text(" - The previous message contains a summary that was prepared because a context limit was reached. - Do not mention that you read a summary or that conversation summarization occurred - Just continue the conversation naturally based on the summarized context - "); - let assistant_message_tokens: usize = 41; + // Add an assistant message to continue the conversation (agent_visible=true, user_visible=false) + let assistant_message = Message::assistant() + .with_text("The previous message contains a summary that was prepared because a context limit was reached.") + .with_metadata(MessageMetadata::agent_only()); + let assistant_message_tokens: usize = 0; // Not counted since it's for agent context only final_messages.push(assistant_message); final_token_counts.push(assistant_message_tokens); diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 2236a7db4382..1537fd174dc1 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -394,6 +394,24 @@ impl Default for MessageMetadata { } } +impl MessageMetadata { + /// Create metadata for messages visible only to the agent + pub fn agent_only() -> Self { + MessageMetadata { + user_visible: false, + agent_visible: true, + } + } + + /// Create metadata for messages visible only to the user + pub fn user_only() -> Self { + MessageMetadata { + user_visible: true, + agent_visible: false, + } + } +} + fn default_true() -> bool { true } @@ -635,6 +653,12 @@ impl Message { self } + /// Set the entire metadata for the message + pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self { + self.metadata = metadata; + self + } + /// Mark the message as only visible to the user (not the agent) pub fn user_only(mut self) -> Self { self.metadata.user_visible = true; @@ -662,7 +686,7 @@ impl Message { #[cfg(test)] mod tests { - use crate::conversation::message::{Message, MessageContent}; + use crate::conversation::message::{Message, MessageContent, MessageMetadata}; use crate::conversation::*; use mcp_core::ToolCall; use rmcp::model::{ @@ -1091,4 +1115,30 @@ mod tests { assert!(message.is_user_visible()); assert!(message.is_agent_visible()); } + + #[test] + fn test_message_metadata_static_methods() { + // Test MessageMetadata::agent_only() + let agent_only_metadata = MessageMetadata::agent_only(); + assert!(!agent_only_metadata.user_visible); + assert!(agent_only_metadata.agent_visible); + + // Test MessageMetadata::user_only() + let user_only_metadata = MessageMetadata::user_only(); + assert!(user_only_metadata.user_visible); + assert!(!user_only_metadata.agent_visible); + + // Test using them with messages + let agent_msg = Message::assistant() + .with_text("Agent only message") + .with_metadata(MessageMetadata::agent_only()); + assert!(!agent_msg.is_user_visible()); + assert!(agent_msg.is_agent_visible()); + + let user_msg = Message::user() + .with_text("User only message") + .with_metadata(MessageMetadata::user_only()); + assert!(user_msg.is_user_visible()); + assert!(!user_msg.is_agent_visible()); + } } From 9ac31ac32dddf53e8f81fb2bcaf904312a454bef Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 5 Sep 2025 17:37:48 -0400 Subject: [PATCH 09/12] fix ui launch bugs --- crates/goose-server/src/openapi.rs | 3 +- crates/goose/src/agents/context.rs | 14 ++--- crates/goose/src/conversation/message.rs | 75 +++++++++++++++++++++++- ui/desktop/openapi.json | 17 ++++++ ui/desktop/src/api/types.gen.ts | 15 +++++ 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index d78746f4d775..edb61f2bd50e 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -14,7 +14,7 @@ use rmcp::model::{ use utoipa::{OpenApi, ToSchema}; use goose::conversation::message::{ - ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, RedactedThinkingContent, + ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, MessageMetadata, RedactedThinkingContent, SummarizationRequested, ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse, }; use utoipa::openapi::schema::{ @@ -418,6 +418,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::session::SessionHistoryResponse, Message, MessageContent, + MessageMetadata, ContentSchema, EmbeddedResourceSchema, ImageContentSchema, diff --git a/crates/goose/src/agents/context.rs b/crates/goose/src/agents/context.rs index ed5247cddd4d..e97795dd6140 100644 --- a/crates/goose/src/agents/context.rs +++ b/crates/goose/src/agents/context.rs @@ -81,11 +81,11 @@ impl Agent { let mut final_messages = Vec::new(); let mut final_token_counts = Vec::new(); - // Add all original messages with updated visibility (user_visible=true, agent_visible=false) - for mut msg in messages.iter().cloned() { - msg.metadata.user_visible = true; - msg.metadata.agent_visible = false; - final_messages.push(msg); + // Add all original messages with updated visibility (preserve user_visible, set agent_visible=false) + for msg in messages.iter().cloned() { + let updated_metadata = msg.metadata.with_agent_invisible(); + let updated_msg = msg.with_metadata(updated_metadata); + final_messages.push(updated_msg); // Token count doesn't matter for agent_visible=false messages, but we'll use 0 final_token_counts.push(0); } @@ -99,9 +99,7 @@ impl Agent { final_token_counts.push(compaction_marker_tokens); // Add the summary message (agent_visible=true, user_visible=false) - let mut summary_msg = summary_message; - summary_msg.metadata.user_visible = false; - summary_msg.metadata.agent_visible = true; + let summary_msg = summary_message.with_metadata(MessageMetadata::agent_only()); // For token counting purposes, we use the output tokens (the actual summary content) // since that's what will be in the context going forward let summary_tokens = summarization_usage diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 1537fd174dc1..82f41a10b501 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -373,7 +373,7 @@ impl From for Message { } } -#[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Clone, Copy, PartialEq, Serialize, Deserialize)] /// Metadata for message visibility #[serde(rename_all = "camelCase")] pub struct MessageMetadata { @@ -410,6 +410,38 @@ impl MessageMetadata { agent_visible: false, } } + + /// Create metadata for messages visible to neither user nor agent (archived) + pub fn invisible() -> Self { + MessageMetadata { + user_visible: false, + agent_visible: false, + } + } + + /// Return a copy with agent_visible set to false + pub fn with_agent_invisible(mut self) -> Self { + self.agent_visible = false; + self + } + + /// Return a copy with user_visible set to false + pub fn with_user_invisible(mut self) -> Self { + self.user_visible = false; + self + } + + /// Return a copy with agent_visible set to true + pub fn with_agent_visible(mut self) -> Self { + self.agent_visible = true; + self + } + + /// Return a copy with user_visible set to true + pub fn with_user_visible(mut self) -> Self { + self.user_visible = true; + self + } } fn default_true() -> bool { @@ -1128,6 +1160,11 @@ mod tests { assert!(user_only_metadata.user_visible); assert!(!user_only_metadata.agent_visible); + // Test MessageMetadata::invisible() + let invisible_metadata = MessageMetadata::invisible(); + assert!(!invisible_metadata.user_visible); + assert!(!invisible_metadata.agent_visible); + // Test using them with messages let agent_msg = Message::assistant() .with_text("Agent only message") @@ -1140,5 +1177,41 @@ mod tests { .with_metadata(MessageMetadata::user_only()); assert!(user_msg.is_user_visible()); assert!(!user_msg.is_agent_visible()); + + let invisible_msg = Message::user() + .with_text("Invisible message") + .with_metadata(MessageMetadata::invisible()); + assert!(!invisible_msg.is_user_visible()); + assert!(!invisible_msg.is_agent_visible()); + } + + #[test] + fn test_message_metadata_builder_methods() { + // Test with_agent_invisible + let metadata = MessageMetadata::default().with_agent_invisible(); + assert!(metadata.user_visible); + assert!(!metadata.agent_visible); + + // Test with_user_invisible + let metadata = MessageMetadata::default().with_user_invisible(); + assert!(!metadata.user_visible); + assert!(metadata.agent_visible); + + // Test with_agent_visible + let metadata = MessageMetadata::invisible().with_agent_visible(); + assert!(!metadata.user_visible); + assert!(metadata.agent_visible); + + // Test with_user_visible + let metadata = MessageMetadata::invisible().with_user_visible(); + assert!(metadata.user_visible); + assert!(!metadata.agent_visible); + + // Test chaining + let metadata = MessageMetadata::invisible() + .with_user_visible() + .with_agent_visible(); + assert!(metadata.user_visible); + assert!(metadata.agent_visible); } } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index ad35b702dbbe..7fd363736e7d 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -2502,6 +2502,9 @@ "type": "string", "nullable": true }, + "metadata": { + "$ref": "#/components/schemas/MessageMetadata" + }, "role": { "$ref": "#/components/schemas/Role" } @@ -2725,6 +2728,20 @@ "propertyName": "type" } }, + "MessageMetadata": { + "type": "object", + "description": "Metadata for message visibility", + "properties": { + "agentVisible": { + "type": "boolean", + "description": "Whether the message should be included in the agent's context window" + }, + "userVisible": { + "type": "boolean", + "description": "Whether the message should be visible to the user in the UI" + } + } + }, "ModelInfo": { "type": "object", "description": "Information about a model's capabilities", diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 6fdcc63854c2..0d20203bf447 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -360,6 +360,7 @@ export type Message = { content: Array; created?: number; id?: string | null; + metadata?: MessageMetadata; role: Role; }; @@ -388,6 +389,20 @@ export type MessageContent = (TextContent & { type: 'summarizationRequested'; }); +/** + * Metadata for message visibility + */ +export type MessageMetadata = { + /** + * Whether the message should be included in the agent's context window + */ + agentVisible?: boolean; + /** + * Whether the message should be visible to the user in the UI + */ + userVisible?: boolean; +}; + /** * Information about a model's capabilities */ From e9af3f9948a50ceaa6c072716431923f0777be5a Mon Sep 17 00:00:00 2001 From: David Katz Date: Mon, 8 Sep 2025 15:09:20 -0400 Subject: [PATCH 10/12] fmt --- crates/goose-server/src/openapi.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index edb61f2bd50e..097fff3d11fb 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -14,8 +14,9 @@ use rmcp::model::{ use utoipa::{OpenApi, ToSchema}; use goose::conversation::message::{ - ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, MessageMetadata, RedactedThinkingContent, - SummarizationRequested, ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse, + ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, MessageMetadata, + RedactedThinkingContent, SummarizationRequested, ThinkingContent, ToolConfirmationRequest, + ToolRequest, ToolResponse, }; use utoipa::openapi::schema::{ AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema, From c20974249cdbfbc085bfe5dc11152ff8b3349004 Mon Sep 17 00:00:00 2001 From: David Katz Date: Tue, 9 Sep 2025 14:56:35 -0400 Subject: [PATCH 11/12] rm mut --- crates/goose/src/conversation/message.rs | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 82f41a10b501..db445b2e9b60 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -420,27 +420,35 @@ impl MessageMetadata { } /// Return a copy with agent_visible set to false - pub fn with_agent_invisible(mut self) -> Self { - self.agent_visible = false; - self + pub fn with_agent_invisible(self) -> Self { + Self { + agent_visible: false, + ..self + } } /// Return a copy with user_visible set to false - pub fn with_user_invisible(mut self) -> Self { - self.user_visible = false; - self + pub fn with_user_invisible(self) -> Self { + Self { + user_visible: false, + ..self + } } /// Return a copy with agent_visible set to true - pub fn with_agent_visible(mut self) -> Self { - self.agent_visible = true; - self + pub fn with_agent_visible(self) -> Self { + Self { + agent_visible: true, + ..self + } } /// Return a copy with user_visible set to true - pub fn with_user_visible(mut self) -> Self { - self.user_visible = true; - self + pub fn with_user_visible(self) -> Self { + Self { + user_visible: true, + ..self + } } } From 8e2df94b550a3778f6a5d572f57ecc44664ace3b Mon Sep 17 00:00:00 2001 From: David Katz Date: Wed, 10 Sep 2025 13:19:37 -0400 Subject: [PATCH 12/12] dont mention summary is back --- crates/goose/src/agents/context.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/agents/context.rs b/crates/goose/src/agents/context.rs index e97795dd6140..8b594bbc0ab5 100644 --- a/crates/goose/src/agents/context.rs +++ b/crates/goose/src/agents/context.rs @@ -110,9 +110,11 @@ impl Agent { final_token_counts.push(summary_tokens); // Add an assistant message to continue the conversation (agent_visible=true, user_visible=false) - let assistant_message = Message::assistant() - .with_text("The previous message contains a summary that was prepared because a context limit was reached.") - .with_metadata(MessageMetadata::agent_only()); + let assistant_message = Message::assistant().with_text(" + The previous message contains a summary that was prepared because a context limit was reached. + Do not mention that you read a summary or that conversation summarization occurred + Just continue the conversation naturally based on the summarized context + ").with_metadata(MessageMetadata::agent_only()); let assistant_message_tokens: usize = 0; // Not counted since it's for agent context only final_messages.push(assistant_message); final_token_counts.push(assistant_message_tokens);