Session manager (#4648)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2025-09-26 14:56:45 -04:00
committed by GitHub
parent d0aa14a1ea
commit dc292883b7
61 changed files with 2895 additions and 5572 deletions
+14 -12
View File
@@ -1,38 +1,42 @@
use crate::session::build_session;
use crate::session::SessionBuilderConfig;
use crate::{logging, session, Session};
use crate::{logging, CliSession};
use async_trait::async_trait;
use goose::conversation::Conversation;
use goose_bench::bench_session::{BenchAgent, BenchBaseSession};
use goose_bench::eval_suites::ExtensionRequirements;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
// allow session obj to be used in benchmarking
#[async_trait]
impl BenchBaseSession for Session {
impl BenchBaseSession for CliSession {
async fn headless(&mut self, message: String) -> anyhow::Result<()> {
self.headless(message).await
}
fn session_file(&self) -> Option<PathBuf> {
self.session_file()
}
fn message_history(&self) -> Conversation {
self.message_history()
}
fn get_total_token_usage(&self) -> anyhow::Result<Option<i32>> {
self.get_total_token_usage()
// Since the trait requires sync but the session method is async,
// we need to block on the async call
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(self.get_total_token_usage())
})
}
fn get_session_id(&self) -> anyhow::Result<String> {
self.session_id()
.cloned()
.ok_or_else(|| anyhow::anyhow!("No session ID available"))
}
}
pub async fn agent_generator(
requirements: ExtensionRequirements,
session_id: String,
) -> BenchAgent {
let identifier = Some(session::Identifier::Name(session_id));
let base_session = build_session(SessionBuilderConfig {
identifier,
session_id: Some(session_id),
resume: false,
no_session: false,
extensions: requirements.external,
@@ -56,10 +60,8 @@ pub async fn agent_generator(
})
.await;
// package session obj into benchmark-compatible struct
let bench_agent = BenchAgent::new(Box::new(base_session));
// Initialize logging with error capture
let errors = Some(Arc::new(Mutex::new(bench_agent.get_errors().await)));
logging::setup_logging(Some("bench"), errors).expect("Failed to initialize logging");
+1 -2
View File
@@ -219,11 +219,10 @@ pub async fn handle_schedule_sessions(id: String, limit: Option<u32>) -> Result<
// sessions is now Vec<(String, SessionMetadata)>
for (session_name, metadata) in sessions {
println!(
" - Session ID: {}, Working Dir: {}, Description: \"{}\", Messages: {}, Schedule ID: {:?}",
" - Session ID: {}, Working Dir: {}, Description: \"{}\", Schedule ID: {:?}",
session_name, // Display the session_name as Session ID
metadata.working_dir.display(),
metadata.description,
metadata.message_count,
metadata.schedule_id.as_deref().unwrap_or("N/A")
);
}
+70 -109
View File
@@ -1,19 +1,19 @@
use crate::session::message_to_markdown;
use anyhow::{Context, Result};
use cliclack::{confirm, multiselect, select};
use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder};
use goose::session::{self, Identifier};
use goose::session::{Session, SessionManager};
use goose::utils::safe_truncate;
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
const TRUNCATED_DESC_LENGTH: usize = 60;
pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> {
pub async fn remove_sessions(sessions: Vec<Session>) -> Result<()> {
println!("The following sessions will be removed:");
for session in &sessions {
println!("- {}", session.id);
println!("- {} {}", session.id, session.description);
}
let should_delete = confirm("Are you sure you want to delete these sessions?")
@@ -22,8 +22,7 @@ pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> {
if should_delete {
for session in sessions {
fs::remove_file(session.path.clone())
.with_context(|| format!("Failed to remove session file '{}'", session.path))?;
SessionManager::delete_session(&session.id).await?;
println!("Session `{}` removed.", session.id);
}
} else {
@@ -33,7 +32,7 @@ pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> {
Ok(())
}
fn prompt_interactive_session_removal(sessions: &[SessionInfo]) -> Result<Vec<SessionInfo>> {
fn prompt_interactive_session_removal(sessions: &[Session]) -> Result<Vec<Session>> {
if sessions.is_empty() {
println!("No sessions to delete.");
return Ok(vec![]);
@@ -43,16 +42,16 @@ fn prompt_interactive_session_removal(sessions: &[SessionInfo]) -> Result<Vec<Se
"Select sessions to delete (use spacebar, Enter to confirm, Ctrl+C to cancel):",
);
let display_map: std::collections::HashMap<String, SessionInfo> = sessions
let display_map: std::collections::HashMap<String, Session> = sessions
.iter()
.map(|s| {
let desc = if s.metadata.description.is_empty() {
let desc = if s.description.is_empty() {
"(no description)"
} else {
&s.metadata.description
&s.description
};
let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH);
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
let display_text = format!("{} - {} ({})", s.updated_at, truncated_desc, s.id);
(display_text, s.clone())
})
.collect();
@@ -63,7 +62,7 @@ fn prompt_interactive_session_removal(sessions: &[SessionInfo]) -> Result<Vec<Se
let selected_display_texts: Vec<String> = selector.interact()?;
let selected_sessions: Vec<SessionInfo> = selected_display_texts
let selected_sessions: Vec<Session> = selected_display_texts
.into_iter()
.filter_map(|text| display_map.get(&text).cloned())
.collect();
@@ -71,8 +70,8 @@ fn prompt_interactive_session_removal(sessions: &[SessionInfo]) -> Result<Vec<Se
Ok(selected_sessions)
}
pub fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -> Result<()> {
let all_sessions = match get_valid_sorted_sessions(SortOrder::Descending) {
pub async fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -> Result<()> {
let all_sessions = match SessionManager::list_sessions().await {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to retrieve sessions: {:?}", e);
@@ -80,7 +79,7 @@ pub fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -
}
};
let matched_sessions: Vec<SessionInfo>;
let matched_sessions: Vec<Session>;
if let Some(id_val) = id {
if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) {
@@ -112,23 +111,16 @@ pub fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -
return Ok(());
}
remove_sessions(matched_sessions)
remove_sessions(matched_sessions).await
}
pub fn handle_session_list(verbose: bool, format: String, ascending: bool) -> Result<()> {
let sort_order = if ascending {
SortOrder::Ascending
pub async fn handle_session_list(verbose: bool, format: String, ascending: bool) -> Result<()> {
let mut sessions = SessionManager::list_sessions().await?;
if ascending {
sessions.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
} else {
SortOrder::Descending
};
let sessions = match get_valid_sorted_sessions(sort_order) {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(anyhow::anyhow!("Failed to list sessions"));
}
};
sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
}
match format.as_str() {
"json" => {
@@ -138,27 +130,18 @@ pub fn handle_session_list(verbose: bool, format: String, ascending: bool) -> Re
if sessions.is_empty() {
println!("No sessions found");
return Ok(());
} else {
println!("Available sessions:");
for SessionInfo {
id,
path,
metadata,
modified,
} in sessions
{
let description = if metadata.description.is_empty() {
"(none)"
} else {
&metadata.description
};
let output = format!("{} - {} - {}", id, description, modified);
if verbose {
println!(" {}", output);
println!(" Path: {}", path);
} else {
println!("{}", output);
}
}
println!("Available sessions:");
for session in sessions {
let output = format!(
"{} - {} - {}",
session.id, session.description, session.updated_at
);
if verbose {
println!(" {}", output);
} else {
println!("{}", output);
}
}
}
@@ -166,68 +149,55 @@ pub fn handle_session_list(verbose: bool, format: String, ascending: bool) -> Re
Ok(())
}
/// Export a session to Markdown without creating a full Session object
///
/// This function directly reads messages from the session file and converts them to Markdown
/// without creating an Agent or prompting about working directories.
pub fn handle_session_export(identifier: Identifier, output_path: Option<PathBuf>) -> Result<()> {
// Get the session file path
let session_file_path = match goose::session::get_path(identifier.clone()) {
Ok(path) => path,
pub async fn handle_session_export(
session_id: String,
output_path: Option<PathBuf>,
format: String,
) -> Result<()> {
let session = match SessionManager::get_session(&session_id, true).await {
Ok(session) => session,
Err(e) => {
return Err(anyhow::anyhow!("Invalid session identifier: {}", e));
return Err(anyhow::anyhow!(
"Session '{}' not found or failed to read: {}",
session_id,
e
));
}
};
if !session_file_path.exists() {
return Err(anyhow::anyhow!(
"Session file not found (expected path: {})",
session_file_path.display()
));
}
// Read messages directly without using Session
let messages = match goose::session::read_messages(&session_file_path) {
Ok(msgs) => msgs,
Err(e) => {
return Err(anyhow::anyhow!("Failed to read session messages: {}", e));
let output = match format.as_str() {
"json" => serde_json::to_string_pretty(&session)?,
"yaml" => serde_yaml::to_string(&session)?,
"markdown" => {
let conversation = session
.conversation
.ok_or_else(|| anyhow::anyhow!("Session has no messages"))?;
export_session_to_markdown(conversation.messages().to_vec(), &session.description)
}
_ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
};
// Generate the markdown content using the export functionality
let markdown =
export_session_to_markdown(messages.messages().clone(), &session_file_path, None);
// Output the markdown
if let Some(output) = output_path {
fs::write(&output, markdown)
.with_context(|| format!("Failed to write to output file: {}", output.display()))?;
println!("Session exported to {}", output.display());
if let Some(output_path) = output_path {
fs::write(&output_path, output).with_context(|| {
format!("Failed to write to output file: {}", output_path.display())
})?;
println!("Session exported to {}", output_path.display());
} else {
println!("{}", markdown);
println!("{}", output);
}
Ok(())
}
/// Convert a list of messages to markdown format for session export
///
/// This function handles the formatting of a complete session including headers,
/// message organization, and proper tool request/response pairing.
fn export_session_to_markdown(
messages: Vec<goose::conversation::message::Message>,
session_file: &Path,
session_name_override: Option<&str>,
session_name: &String,
) -> String {
let mut markdown_output = String::new();
let session_name = session_name_override.unwrap_or_else(|| {
session_file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Unnamed Session")
});
markdown_output.push_str(&format!("# Session Export: {}\n\n", session_name));
if messages.is_empty() {
@@ -293,15 +263,8 @@ fn export_session_to_markdown(
/// Prompt the user to interactively select a session
///
/// Shows a list of available sessions and lets the user select one
pub fn prompt_interactive_session_selection() -> Result<session::Identifier> {
// Get sessions sorted by modification date (newest first)
let sessions = match get_valid_sorted_sessions(SortOrder::Descending) {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(anyhow::anyhow!("Failed to list sessions"));
}
};
pub async fn prompt_interactive_session_selection() -> Result<String> {
let sessions = SessionManager::list_sessions().await?;
if sessions.is_empty() {
return Err(anyhow::anyhow!("No sessions found"));
@@ -311,19 +274,17 @@ pub fn prompt_interactive_session_selection() -> Result<session::Identifier> {
let mut selector = select("Select a session to export:");
// Map to display text
let display_map: std::collections::HashMap<String, SessionInfo> = sessions
let display_map: std::collections::HashMap<String, Session> = sessions
.iter()
.map(|s| {
let desc = if s.metadata.description.is_empty() {
let desc = if s.description.is_empty() {
"(no description)"
} else {
&s.metadata.description
&s.description
};
let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH);
// Truncate description if too long
let truncated_desc = safe_truncate(desc, 40);
let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id);
let display_text = format!("{} - {} ({})", s.updated_at, truncated_desc, s.id);
(display_text, s.clone())
})
.collect();
@@ -346,7 +307,7 @@ pub fn prompt_interactive_session_selection() -> Result<session::Identifier> {
// Retrieve the selected session
if let Some(session) = display_map.get(&selected_display_text) {
Ok(goose::session::Identifier::Name(session.id.clone()))
Ok(session.id.clone())
} else {
Err(anyhow::anyhow!("Invalid selection"))
}
+54 -183
View File
@@ -8,24 +8,25 @@ use axum::{
routing::get,
Json, Router,
};
use goose::session::SessionManager;
use webbrowser;
use futures::{sink::SinkExt, stream::StreamExt};
use goose::agents::{Agent, AgentEvent};
use goose::conversation::message::Message as GooseMessage;
use goose::conversation::Conversation;
use goose::session;
use axum::response::Redirect;
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::{Mutex, RwLock};
use tower_http::cors::{Any, CorsLayer};
use tracing::error;
type SessionStore = Arc<RwLock<std::collections::HashMap<String, Arc<Mutex<Conversation>>>>>;
type CancellationStore = Arc<RwLock<std::collections::HashMap<String, tokio::task::AbortHandle>>>;
#[derive(Clone)]
struct AppState {
agent: Arc<Agent>,
sessions: SessionStore,
cancellations: CancellationStore,
}
@@ -123,7 +124,6 @@ pub async fn handle_web(port: u16, host: String, open: bool) -> Result<()> {
let state = AppState {
agent: Arc::new(agent),
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
cancellations: Arc::new(RwLock::new(std::collections::HashMap::new())),
};
@@ -169,8 +169,15 @@ pub async fn handle_web(port: u16, host: String, open: bool) -> Result<()> {
Ok(())
}
async fn serve_index() -> Html<&'static str> {
Html(include_str!("../../static/index.html"))
async fn serve_index() -> Result<Redirect, (http::StatusCode, String)> {
let session = SessionManager::create_session(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Web session".to_string(),
)
.await
.map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
Ok(Redirect::to(&format!("/session/{}", session.id)))
}
async fn serve_session(
@@ -222,23 +229,19 @@ async fn health_check() -> Json<serde_json::Value> {
}
async fn list_sessions() -> Json<serde_json::Value> {
match session::list_sessions() {
match SessionManager::list_sessions().await {
Ok(sessions) => {
let session_info: Vec<serde_json::Value> = sessions
.into_iter()
.filter_map(|(name, path)| {
session::read_metadata(&path).ok().map(|metadata| {
serde_json::json!({
"name": name,
"path": path,
"description": metadata.description,
"message_count": metadata.message_count,
"working_dir": metadata.working_dir
})
})
})
.collect();
let mut session_info = Vec::new();
for session in sessions {
session_info.push(serde_json::json!({
"name": session.id,
"path": session.id,
"description": session.description,
"message_count": session.message_count,
"working_dir": session.working_dir
}));
}
Json(serde_json::json!({
"sessions": session_info
}))
@@ -251,30 +254,14 @@ async fn list_sessions() -> Json<serde_json::Value> {
async fn get_session(
axum::extract::Path(session_id): axum::extract::Path<String>,
) -> Json<serde_json::Value> {
let session_file = match session::get_path(session::Identifier::Name(session_id)) {
Ok(path) => path,
Err(e) => {
return Json(serde_json::json!({
"error": format!("Invalid session ID: {}", e)
}));
}
};
let error_response = |e: Box<dyn std::error::Error>| {
Json(serde_json::json!({
match SessionManager::get_session(&session_id, true).await {
Ok(session) => Json(serde_json::json!({
"metadata": session,
"messages": session.conversation.unwrap_or_default().messages()
})),
Err(e) => Json(serde_json::json!({
"error": e.to_string()
}))
};
match session::read_messages(&session_file) {
Ok(messages) => match session::read_metadata(&session_file) {
Ok(metadata) => Json(serde_json::json!({
"metadata": metadata,
"messages": messages
})),
Err(e) => error_response(e.into()),
},
Err(e) => error_response(e.into()),
})),
}
}
@@ -299,46 +286,14 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
session_id,
..
}) => {
// Get session file path from session_id
let session_file = match session::get_path(session::Identifier::Name(
session_id.clone(),
)) {
Ok(path) => path,
Err(e) => {
tracing::error!("Failed to get session path: {}", e);
continue;
}
};
// Get or create session in memory (for fast access during processing)
let session_messages = {
let sessions = state.sessions.read().await;
if let Some(session) = sessions.get(&session_id) {
session.clone()
} else {
drop(sessions);
let mut sessions = state.sessions.write().await;
// Load existing messages from JSONL file if it exists
let existing_messages =
session::read_messages(&session_file).unwrap_or_default();
let new_session = Arc::new(Mutex::new(existing_messages));
sessions.insert(session_id.clone(), new_session.clone());
new_session
}
};
// Clone sender for async processing
let sender_clone = sender.clone();
let agent = state.agent.clone();
let session_id_clone = session_id.clone();
// Process message in a separate task to allow streaming
let task_handle = tokio::spawn(async move {
let result = process_message_streaming(
&agent,
session_messages,
session_file,
session_id_clone,
content,
sender_clone,
)
@@ -349,25 +304,21 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
}
});
// Store the abort handle
{
let mut cancellations = state.cancellations.write().await;
cancellations
.insert(session_id.clone(), task_handle.abort_handle());
}
// Wait for task completion and handle abort
// Handle task completion and cleanup
let sender_for_abort = sender.clone();
let session_id_for_cleanup = session_id.clone();
let cancellations_for_cleanup = state.cancellations.clone();
tokio::spawn(async move {
match task_handle.await {
Ok(_) => {
// Task completed normally
}
Ok(_) => {}
Err(e) if e.is_cancelled() => {
// Task was aborted
let mut sender = sender_for_abort.lock().await;
let _ = sender
.send(Message::Text(
@@ -387,11 +338,8 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
}
}
// Clean up cancellation token
{
let mut cancellations = cancellations_for_cleanup.write().await;
cancellations.remove(&session_id_for_cleanup);
}
let mut cancellations = cancellations_for_cleanup.write().await;
cancellations.remove(&session_id_for_cleanup);
});
}
Ok(WebSocketMessage::Cancel { session_id }) => {
@@ -436,27 +384,16 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
async fn process_message_streaming(
agent: &Agent,
session_messages: Arc<Mutex<Conversation>>,
session_file: std::path::PathBuf,
session_id: String,
content: String,
sender: Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
) -> Result<()> {
use futures::StreamExt;
use goose::agents::SessionConfig;
use goose::conversation::message::MessageContent;
use goose::session;
// Create a user message
let user_message = GooseMessage::user().with_text(content.clone());
// Messages will be auto-compacted in agent.reply() if needed
let messages: Conversation = {
let mut session_msgs = session_messages.lock().await;
session_msgs.push(user_message.clone());
session_msgs.clone()
};
// Persist messages to JSONL file with provider for automatic description generation
let provider = agent.provider().await;
if provider.is_err() {
let error_msg = "I'm not properly configured yet. Please configure a provider through the CLI first using `goose configure`.".to_string();
@@ -475,19 +412,13 @@ async fn process_message_streaming(
return Ok(());
}
let provider = provider.unwrap();
let working_dir = Some(std::env::current_dir()?);
session::persist_messages(
&session_file,
&messages,
Some(provider.clone()),
working_dir.clone(),
)
.await?;
let session = SessionManager::get_session(&session_id, true).await?;
let mut messages = session.conversation.unwrap_or_default();
messages.push(user_message);
let session_config = SessionConfig {
id: session::Identifier::Path(session_file.clone()),
working_dir: std::env::current_dir()?,
id: session.id.clone(),
working_dir: session.working_dir,
schedule_id: None,
execution_mode: None,
max_turns: None,
@@ -502,29 +433,11 @@ async fn process_message_streaming(
while let Some(result) = stream.next().await {
match result {
Ok(AgentEvent::Message(message)) => {
// Add message to our session
{
let mut session_msgs = session_messages.lock().await;
session_msgs.push(message.clone());
}
SessionManager::add_message(&session_id, &message).await?;
// Persist messages to JSONL file (no provider needed for assistant messages)
let current_messages = {
let session_msgs = session_messages.lock().await;
session_msgs.clone()
};
session::persist_messages(
&session_file,
&current_messages,
None,
working_dir.clone(),
)
.await?;
// Handle different message content types
for content in &message.content {
match content {
MessageContent::Text(text) => {
// Send the text response
let mut sender = sender.lock().await;
let _ = sender
.send(Message::Text(
@@ -539,7 +452,6 @@ async fn process_message_streaming(
.await;
}
MessageContent::ToolRequest(req) => {
// Send tool request notification
let mut sender = sender.lock().await;
if let Ok(tool_call) = &req.tool_call {
let _ = sender
@@ -557,13 +469,8 @@ async fn process_message_streaming(
.await;
}
}
MessageContent::ToolResponse(_resp) => {
// Tool responses are already included in the complete message stream
// and will be persisted to session history. No need to send separate
// WebSocket messages as this would cause duplicates.
}
MessageContent::ToolResponse(_resp) => {}
MessageContent::ToolConfirmationRequest(confirmation) => {
// Send tool confirmation request
let mut sender = sender.lock().await;
let _ = sender
.send(Message::Text(
@@ -580,8 +487,6 @@ async fn process_message_streaming(
))
.await;
// For now, auto-approve in web mode
// TODO: Implement proper confirmation UI
agent.handle_confirmation(
confirmation.id.clone(),
goose::permission::PermissionConfirmation {
@@ -591,7 +496,6 @@ async fn process_message_streaming(
).await;
}
MessageContent::Thinking(thinking) => {
// Send thinking indicator
let mut sender = sender.lock().await;
let _ = sender
.send(Message::Text(
@@ -604,7 +508,6 @@ async fn process_message_streaming(
.await;
}
MessageContent::ContextLengthExceeded(msg) => {
// Send context exceeded notification
let mut sender = sender.lock().await;
let _ = sender
.send(Message::Text(
@@ -618,55 +521,27 @@ async fn process_message_streaming(
))
.await;
// For now, auto-summarize in web mode
// TODO: Implement proper UI for context handling
let (summarized_messages, _, _) =
agent.summarize_context(messages.messages()).await?;
{
let mut session_msgs = session_messages.lock().await;
*session_msgs = summarized_messages;
}
}
_ => {
// Handle other message types as needed
SessionManager::replace_conversation(
&session_id,
&summarized_messages,
)
.await?;
}
_ => {}
}
}
}
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 = Conversation::new_unvalidated(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,
&current_messages,
None, // No provider needed for persisting
working_dir.clone(),
)
.await
{
error!("Failed to persist compacted messages: {}", e);
}
Ok(AgentEvent::HistoryReplaced(_new_messages)) => {
tracing::info!("History replaced, compacting happened in reply");
}
Ok(AgentEvent::McpNotification(_notification)) => {
// Handle MCP notifications if needed
// For now, we'll just log them
tracing::info!("Received MCP notification in web interface");
}
Ok(AgentEvent::ModelChange { model, mode }) => {
// Log model change
tracing::info!("Model changed to {} in {} mode", model, mode);
}
Err(e) => {
error!("Error in message stream: {}", e);
let mut sender = sender.lock().await;
@@ -699,7 +574,6 @@ async fn process_message_streaming(
}
}
// Send completion message
let mut sender = sender.lock().await;
let _ = sender
.send(Message::Text(
@@ -713,6 +587,3 @@ async fn process_message_streaming(
Ok(())
}
// Add webbrowser dependency for opening browser
use webbrowser;