Skip to content

Commit 23412e2

Browse files
authored
Replace compaction notifications with system notifications (#5218)
1 parent 755e9f8 commit 23412e2

File tree

31 files changed

+349
-1059
lines changed

31 files changed

+349
-1059
lines changed

crates/goose-cli/src/session/export.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,8 @@ pub fn message_to_markdown(message: &Message, export_all_content: bool) -> Strin
369369
md.push_str("**Thinking:**\n");
370370
md.push_str("> *Thinking was redacted*\n\n");
371371
}
372-
MessageContent::ConversationCompacted(summarization) => {
373-
md.push_str(&format!("*{}*\n\n", summarization.msg));
372+
MessageContent::SystemNotification(notification) => {
373+
md.push_str(&format!("*{}*\n\n", notification.msg));
374374
}
375375
_ => {
376376
md.push_str(

crates/goose-cli/src/session/output.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,18 @@ pub fn render_message(message: &Message, debug: bool) {
185185
println!("\n{}", style("Thinking:").dim().italic());
186186
print_markdown("Thinking was redacted", theme);
187187
}
188-
MessageContent::ConversationCompacted(summarization) => {
189-
println!("\n{}", style(&summarization.msg).yellow());
188+
MessageContent::SystemNotification(notification) => {
189+
use goose::conversation::message::SystemNotificationType;
190+
191+
match notification.notification_type {
192+
SystemNotificationType::ThinkingMessage => {
193+
show_thinking();
194+
set_thinking_message(&notification.msg);
195+
}
196+
SystemNotificationType::InlineMessage => {
197+
println!("\n{}", style(&notification.msg).yellow());
198+
}
199+
}
190200
}
191201
_ => {
192202
println!("WARNING: Message content type could not be rendered");

crates/goose-server/src/openapi.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ use goose::config::declarative_providers::{
1818
DeclarativeProviderConfig, LoadedProvider, ProviderEngine,
1919
};
2020
use goose::conversation::message::{
21-
ConversationCompacted, FrontendToolRequest, Message, MessageContent, MessageMetadata,
22-
RedactedThinkingContent, ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse,
21+
FrontendToolRequest, Message, MessageContent, MessageMetadata, RedactedThinkingContent,
22+
SystemNotificationContent, SystemNotificationType, ThinkingContent, ToolConfirmationRequest,
23+
ToolRequest, ToolResponse,
2324
};
2425

2526
use utoipa::openapi::schema::{
@@ -350,7 +351,6 @@ derive_utoipa!(Icon as IconSchema);
350351
super::routes::agent::update_router_tool_selector,
351352
super::routes::reply::confirm_permission,
352353
super::routes::reply::reply,
353-
super::routes::context::manage_context,
354354
super::routes::session::list_sessions,
355355
super::routes::session::get_session,
356356
super::routes::session::get_session_insights,
@@ -393,8 +393,6 @@ derive_utoipa!(Icon as IconSchema);
393393
super::routes::config_management::UpdateCustomProviderRequest,
394394
super::routes::reply::PermissionConfirmationRequest,
395395
super::routes::reply::ChatRequest,
396-
super::routes::context::ContextManageRequest,
397-
super::routes::context::ContextManageResponse,
398396
super::routes::session::ImportSessionRequest,
399397
super::routes::session::SessionListResponse,
400398
super::routes::session::UpdateSessionDescriptionRequest,
@@ -420,7 +418,8 @@ derive_utoipa!(Icon as IconSchema);
420418
RedactedThinkingContent,
421419
FrontendToolRequest,
422420
ResourceContentsSchema,
423-
ConversationCompacted,
421+
SystemNotificationType,
422+
SystemNotificationContent,
424423
JsonObjectSchema,
425424
RoleSchema,
426425
ProviderMetadata,

crates/goose-server/src/routes/context.rs

Lines changed: 0 additions & 68 deletions
This file was deleted.

crates/goose-server/src/routes/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pub mod agent;
22
pub mod audio;
33
pub mod config_management;
4-
pub mod context;
54
pub mod errors;
65
pub mod extension;
76
pub mod recipe;
@@ -23,7 +22,6 @@ pub fn configure(state: Arc<crate::state::AppState>) -> Router {
2322
.merge(reply::routes(state.clone()))
2423
.merge(agent::routes(state.clone()))
2524
.merge(audio::routes(state.clone()))
26-
.merge(context::routes(state.clone()))
2725
.merge(extension::routes(state.clone()))
2826
.merge(config_management::routes(state.clone()))
2927
.merge(recipe::routes(state.clone()))

crates/goose/src/agents/agent.rs

Lines changed: 103 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ use super::model_selector::autopilot::AutoPilot;
5959
use super::platform_tools;
6060
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
6161
use crate::agents::subagent_task_config::TaskConfig;
62-
use crate::conversation::message::{Message, ToolRequest};
62+
use crate::conversation::message::{Message, MessageContent, SystemNotificationType, ToolRequest};
6363
use crate::session::extension_data::{EnabledExtensionsState, ExtensionState};
6464
use crate::session::SessionManager;
6565

6666
const DEFAULT_MAX_TURNS: u32 = 1000;
67+
const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation...";
68+
const MANUAL_COMPACT_TRIGGER: &str = "Please compact this conversation";
6769

6870
/// Context needed for the reply function
6971
pub struct ReplyContext {
@@ -745,77 +747,99 @@ impl Agent {
745747
session: Option<SessionConfig>,
746748
cancel_token: Option<CancellationToken>,
747749
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
748-
// Try to get session metadata for more accurate token counts
749-
let session_metadata = if let Some(session_config) = &session {
750-
SessionManager::get_session(&session_config.id, false)
751-
.await
752-
.ok()
753-
} else {
754-
None
755-
};
756-
757-
let check_result = crate::context_mgmt::check_if_compaction_needed(
758-
self,
759-
&unfixed_conversation,
760-
None,
761-
session_metadata.as_ref(),
762-
)
763-
.await;
750+
let is_manual_compact = unfixed_conversation.messages().last().is_some_and(|msg| {
751+
msg.content.iter().any(|c| {
752+
if let MessageContent::Text(text) = c {
753+
text.text.trim() == MANUAL_COMPACT_TRIGGER
754+
} else {
755+
false
756+
}
757+
})
758+
});
764759

765-
let (did_compact, compacted_conversation, compaction_error) = match check_result {
766-
// TODO(dkatz): send a notification that we are starting compaction here.
767-
Ok(true) => {
768-
match crate::context_mgmt::compact_messages(self, &unfixed_conversation, false)
760+
if !is_manual_compact {
761+
let session_metadata = if let Some(session_config) = &session {
762+
SessionManager::get_session(&session_config.id, false)
769763
.await
770-
{
771-
Ok((conversation, _token_counts, _summarization_usage)) => {
772-
(true, conversation, None)
773-
}
774-
Err(e) => (false, unfixed_conversation.clone(), Some(e)),
775-
}
764+
.ok()
765+
} else {
766+
None
767+
};
768+
769+
let needs_auto_compact = crate::context_mgmt::check_if_compaction_needed(
770+
self,
771+
&unfixed_conversation,
772+
None,
773+
session_metadata.as_ref(),
774+
)
775+
.await?;
776+
777+
if !needs_auto_compact {
778+
return self
779+
.reply_internal(unfixed_conversation, session, cancel_token)
780+
.await;
776781
}
777-
Ok(false) => (false, unfixed_conversation, None),
778-
Err(e) => (false, unfixed_conversation.clone(), Some(e)),
779-
};
782+
}
780783

781-
if did_compact {
782-
// Get threshold from config to include in message
783-
let config = crate::config::Config::global();
784-
let threshold = config
785-
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
786-
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
787-
let threshold_percentage = (threshold * 100.0) as u32;
788-
789-
let compaction_msg = format!(
790-
"Exceeded auto-compact threshold of {}%. Context has been summarized and reduced.\n\n",
791-
threshold_percentage
792-
);
784+
let conversation_to_compact = unfixed_conversation.clone();
785+
786+
Ok(Box::pin(async_stream::try_stream! {
787+
if !is_manual_compact {
788+
let config = crate::config::Config::global();
789+
let threshold = config
790+
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
791+
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
792+
let threshold_percentage = (threshold * 100.0) as u32;
793+
794+
let inline_msg = format!(
795+
"Exceeded auto-compact threshold of {}%. Performing auto-compaction...",
796+
threshold_percentage
797+
);
793798

794-
Ok(Box::pin(async_stream::try_stream! {
795-
// TODO(Douwe): send this before we actually compact:
796799
yield AgentEvent::Message(
797-
Message::assistant().with_conversation_compacted(compaction_msg)
800+
Message::assistant().with_system_notification(
801+
SystemNotificationType::InlineMessage,
802+
inline_msg,
803+
)
798804
);
799-
yield AgentEvent::HistoryReplaced(compacted_conversation.clone());
800-
if let Some(session_to_store) = &session {
801-
SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?
802-
}
805+
}
806+
807+
yield AgentEvent::Message(
808+
Message::assistant().with_system_notification(
809+
SystemNotificationType::ThinkingMessage,
810+
COMPACTION_THINKING_TEXT,
811+
)
812+
);
813+
814+
match crate::context_mgmt::compact_messages(self, &conversation_to_compact, false).await {
815+
Ok((compacted_conversation, _token_counts, _summarization_usage)) => {
816+
if let Some(session_to_store) = &session {
817+
SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?;
818+
}
819+
820+
yield AgentEvent::HistoryReplaced(compacted_conversation.clone());
803821

804-
let mut reply_stream = self.reply_internal(compacted_conversation, session, cancel_token).await?;
805-
while let Some(event) = reply_stream.next().await {
806-
yield event?;
822+
yield AgentEvent::Message(
823+
Message::assistant().with_system_notification(
824+
SystemNotificationType::InlineMessage,
825+
"Compaction complete",
826+
)
827+
);
828+
829+
if !is_manual_compact {
830+
let mut reply_stream = self.reply_internal(compacted_conversation, session, cancel_token).await?;
831+
while let Some(event) = reply_stream.next().await {
832+
yield event?;
833+
}
834+
}
807835
}
808-
}))
809-
} else if let Some(error) = compaction_error {
810-
Ok(Box::pin(async_stream::try_stream! {
811-
yield AgentEvent::Message(Message::assistant().with_text(
812-
format!("Ran into this error trying to auto-compact: {error}.\n\nPlease try again or create a new session")
813-
));
814-
}))
815-
} else {
816-
self.reply_internal(compacted_conversation, session, cancel_token)
817-
.await
818-
}
836+
Err(e) => {
837+
yield AgentEvent::Message(Message::assistant().with_text(
838+
format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session")
839+
));
840+
}
841+
}
842+
}))
819843
}
820844

821845
/// Main reply method that handles the actual agent processing
@@ -1138,23 +1162,29 @@ impl Agent {
11381162
}
11391163
}
11401164
Err(ProviderError::ContextLengthExceeded(_error_msg)) => {
1141-
info!("Context length exceeded, attempting compaction");
1165+
yield AgentEvent::Message(
1166+
Message::assistant().with_system_notification(
1167+
SystemNotificationType::InlineMessage,
1168+
"Context limit reached. Compacting to continue conversation...",
1169+
)
1170+
);
1171+
yield AgentEvent::Message(
1172+
Message::assistant().with_system_notification(
1173+
SystemNotificationType::ThinkingMessage,
1174+
COMPACTION_THINKING_TEXT,
1175+
)
1176+
);
11421177

1143-
// TODO(dkatz): send a notification that we are starting compaction here.
11441178
match crate::context_mgmt::compact_messages(self, &conversation, true).await {
11451179
Ok((compacted_conversation, _token_counts, _usage)) => {
1180+
if let Some(session_to_store) = &session {
1181+
SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?
1182+
}
1183+
11461184
conversation = compacted_conversation;
11471185
did_recovery_compact_this_iteration = true;
11481186

1149-
yield AgentEvent::Message(
1150-
Message::assistant().with_conversation_compacted(
1151-
"Context limit reached. Conversation has been automatically compacted to continue."
1152-
)
1153-
);
11541187
yield AgentEvent::HistoryReplaced(conversation.clone());
1155-
if let Some(session_to_store) = &session {
1156-
SessionManager::replace_conversation(&session_to_store.id, &conversation).await?
1157-
}
11581188
continue;
11591189
}
11601190
Err(e) => {

crates/goose/src/context_mgmt/mod.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,6 @@ pub async fn compact_messages(
9393
final_token_counts.push(0);
9494
}
9595

96-
// Add the compaction marker (user_visible=true, agent_visible=false)
97-
let compaction_marker = Message::assistant()
98-
.with_conversation_compacted("Conversation compacted and summarized")
99-
.with_metadata(MessageMetadata::user_only());
100-
let compaction_marker_tokens: usize = 0; // Not counted since agent_visible=false
101-
final_messages.push(compaction_marker);
102-
final_token_counts.push(compaction_marker_tokens);
103-
10496
// Add the summary message (agent_visible=true, user_visible=false)
10597
let summary_msg = summary_message.with_metadata(MessageMetadata::agent_only());
10698
// For token counting purposes, we use the output tokens (the actual summary content)
@@ -281,7 +273,9 @@ fn format_message_for_compacting(msg: &Message) -> String {
281273
}
282274
MessageContent::Thinking(thinking) => format!("thinking: {}", thinking.thinking),
283275
MessageContent::RedactedThinking(_) => "redacted_thinking".to_string(),
284-
MessageContent::ConversationCompacted(compact) => format!("compacted: {}", compact.msg),
276+
MessageContent::SystemNotification(notification) => {
277+
format!("system_notification: {}", notification.msg)
278+
}
285279
})
286280
.collect();
287281

0 commit comments

Comments
 (0)