diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs
index ba5206b4ba2e..5e85ec7956f6 100644
--- a/crates/goose-cli/src/commands/web.rs
+++ b/crates/goose-cli/src/commands/web.rs
@@ -448,8 +448,8 @@ async fn process_message_streaming(
// Create a user message
let user_message = GooseMessage::user().with_text(content.clone());
- // Get existing messages from session and add the new user message
- let mut messages = {
+ // Messages will be auto-compacted in agent.reply() if needed
+ let messages = {
let mut session_msgs = session_messages.lock().await;
session_msgs.push(user_message.clone());
session_msgs.clone()
@@ -618,7 +618,10 @@ async fn process_message_streaming(
// TODO: Implement proper UI for context handling
let (summarized_messages, _) =
agent.summarize_context(&messages).await?;
- messages = summarized_messages;
+ {
+ let mut session_msgs = session_messages.lock().await;
+ *session_msgs = summarized_messages;
+ }
}
_ => {
// Handle other message types as needed
@@ -626,6 +629,30 @@ async fn process_message_streaming(
}
}
}
+ Ok(AgentEvent::HistoryReplaced(new_messages)) => {
+ // Replace the session's message history with the compacted messages
+ {
+ let mut session_msgs = session_messages.lock().await;
+ *session_msgs = new_messages;
+ }
+
+ // Persist the updated messages to the JSONL file
+ let current_messages = {
+ let session_msgs = session_messages.lock().await;
+ session_msgs.clone()
+ };
+
+ if let Err(e) = session::persist_messages(
+ &session_file,
+ ¤t_messages,
+ None, // No provider needed for persisting
+ working_dir.clone(),
+ )
+ .await
+ {
+ error!("Failed to persist compacted messages: {}", e);
+ }
+ }
Ok(AgentEvent::McpNotification(_notification)) => {
// Handle MCP notifications if needed
// For now, we'll just log them
diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs
index d094082368e3..cfbf0bcd6ec6 100644
--- a/crates/goose-cli/src/session/mod.rs
+++ b/crates/goose-cli/src/session/mod.rs
@@ -841,6 +841,7 @@ impl Session {
}
async fn process_agent_response(&mut self, interactive: bool) -> Result<()> {
+ // Messages will be auto-compacted in agent.reply() if needed
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
@@ -1135,6 +1136,25 @@ impl Session {
_ => (),
}
}
+ Some(Ok(AgentEvent::HistoryReplaced(new_messages))) => {
+ // Replace the session's message history with the compacted messages
+ self.messages = new_messages;
+
+ // Persist the updated messages to the session file
+ if let Some(session_file) = &self.session_file {
+ let provider = self.agent.provider().await.ok();
+ let working_dir = std::env::current_dir().ok();
+ if let Err(e) = session::persist_messages_with_schedule_id(
+ session_file,
+ &self.messages,
+ provider,
+ self.scheduled_job_id.clone(),
+ working_dir,
+ ).await {
+ eprintln!("Failed to persist compacted messages: {}", e);
+ }
+ }
+ }
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
// Log model change if in debug mode
if self.debug {
diff --git a/crates/goose-ffi/src/lib.rs b/crates/goose-ffi/src/lib.rs
index 6bcf3abe1695..367901c674d1 100644
--- a/crates/goose-ffi/src/lib.rs
+++ b/crates/goose-ffi/src/lib.rs
@@ -269,7 +269,9 @@ pub unsafe extern "C" fn goose_agent_send_message(
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
-
+ Ok(AgentEvent::HistoryReplaced(_)) => {
+ // Handle history replacement events if needed
+ }
Err(e) => {
full_response.push_str(&format!("\nError in message stream: {}", e));
}
diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs
index 12cfae3a6d9a..4a57bce8496e 100644
--- a/crates/goose-server/src/routes/reply.rs
+++ b/crates/goose-server/src/routes/reply.rs
@@ -159,8 +159,15 @@ async fn reply_handler(
retry_config: None,
};
+ // Messages will be auto-compacted in agent.reply() if needed
+ let messages_to_process = messages.clone();
+
let mut stream = match agent
- .reply(&messages, Some(session_config), Some(task_cancel.clone()))
+ .reply(
+ &messages_to_process,
+ Some(session_config),
+ Some(task_cancel.clone()),
+ )
.await
{
Ok(stream) => stream,
@@ -215,6 +222,12 @@ async fn reply_handler(
break;
}
}
+ Ok(Some(Ok(AgentEvent::HistoryReplaced(new_messages)))) => {
+ // Replace the message history with the compacted messages
+ all_messages = new_messages;
+ // Note: We don't send this as a stream event since it's an internal operation
+ // The client will see the compaction notification message that was sent before this event
+ }
Ok(Some(Ok(AgentEvent::ModelChange { model, mode }))) => {
if let Err(e) = stream_event(MessageEvent::ModelChange { model, mode }, &tx).await {
tracing::error!("Error sending model change through channel: {}", e);
diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs
index 4dfadb5d492e..6f2caa5e0056 100644
--- a/crates/goose/src/agents/agent.rs
+++ b/crates/goose/src/agents/agent.rs
@@ -29,11 +29,13 @@ use crate::agents::subagent_execution_tool::subagent_execute_task_tool::{
self, SUBAGENT_EXECUTE_TASK_TOOL_NAME,
};
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
+use crate::agents::task_tracker_tool::{TaskTracker, TASK_TRACKER_TOOL_NAME};
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::agents::tool_vectordb::generate_table_id;
use crate::agents::types::SessionConfig;
use crate::agents::types::{FrontendTool, ToolResultReceiver};
use crate::config::{Config, ExtensionConfigManager, PermissionManager};
+use crate::context_mgmt::auto_compact;
use crate::message::{push_message, Message};
use crate::permission::permission_judge::check_tool_permissions;
use crate::permission::PermissionConfirmation;
@@ -78,6 +80,7 @@ pub struct Agent {
pub(super) router_tool_selector: Mutex