Session manager (#4648)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
+69
-19
@@ -18,8 +18,8 @@ use crate::commands::schedule::{
|
||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
|
||||
use crate::session;
|
||||
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
||||
use goose::session::SessionManager;
|
||||
use goose_bench::bench_config::BenchRunConfig;
|
||||
use goose_bench::runners::bench_runner::BenchRunner;
|
||||
use goose_bench::runners::eval_runner::EvalRunner;
|
||||
@@ -48,26 +48,45 @@ struct Identifier {
|
||||
)]
|
||||
name: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long = "session-id",
|
||||
value_name = "SESSION_ID",
|
||||
help = "Session ID (e.g., '20250921_143022')",
|
||||
long_help = "Specify a session ID directly. When used with --resume, will resume this specific session if it exists."
|
||||
)]
|
||||
session_id: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
help = "Path for the chat session (e.g., './playground.jsonl')",
|
||||
long_help = "Specify a path for your chat session. When used with --resume, will resume this specific session if it exists."
|
||||
help = "Legacy: Path for the chat session",
|
||||
long_help = "Legacy parameter for backward compatibility. Extracts session ID from the file path (e.g., '/path/to/20250325_200615.
|
||||
jsonl' -> '20250325_200615')."
|
||||
)]
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn extract_identifier(identifier: Identifier) -> session::Identifier {
|
||||
if let Some(name) = identifier.name {
|
||||
session::Identifier::Name(name)
|
||||
async fn get_session_id(identifier: Identifier) -> Result<String> {
|
||||
if let Some(session_id) = identifier.session_id {
|
||||
Ok(session_id)
|
||||
} else if let Some(name) = identifier.name {
|
||||
let sessions = SessionManager::list_sessions().await?;
|
||||
|
||||
sessions
|
||||
.into_iter()
|
||||
.find(|s| s.description == name)
|
||||
.map(|s| s.id)
|
||||
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))
|
||||
} else if let Some(path) = identifier.path {
|
||||
session::Identifier::Path(path)
|
||||
path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_val(s: &str) -> Result<(String, String), String> {
|
||||
match s.split_once('=') {
|
||||
Some((key, value)) => Ok((key.to_string(), value.to_string())),
|
||||
@@ -121,6 +140,14 @@ enum SessionCommand {
|
||||
long_help = "Path to save the exported Markdown. If not provided, output will be sent to stdout"
|
||||
)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
long = "format",
|
||||
value_name = "FORMAT",
|
||||
help = "Output format (markdown, json, yaml)",
|
||||
default_value = "markdown"
|
||||
)]
|
||||
format: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -768,19 +795,24 @@ pub async fn cli() -> Result<()> {
|
||||
format,
|
||||
ascending,
|
||||
}) => {
|
||||
handle_session_list(verbose, format, ascending)?;
|
||||
handle_session_list(verbose, format, ascending).await?;
|
||||
Ok(())
|
||||
}
|
||||
Some(SessionCommand::Remove { id, regex }) => {
|
||||
handle_session_remove(id, regex)?;
|
||||
handle_session_remove(id, regex).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(SessionCommand::Export { identifier, output }) => {
|
||||
Some(SessionCommand::Export {
|
||||
identifier,
|
||||
output,
|
||||
format,
|
||||
}) => {
|
||||
let session_identifier = if let Some(id) = identifier {
|
||||
extract_identifier(id)
|
||||
get_session_id(id).await?
|
||||
} else {
|
||||
// If no identifier is provided, prompt for interactive selection
|
||||
match crate::commands::session::prompt_interactive_session_selection() {
|
||||
match crate::commands::session::prompt_interactive_session_selection().await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
@@ -789,7 +821,12 @@ pub async fn cli() -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
crate::commands::session::handle_session_export(session_identifier, output)?;
|
||||
crate::commands::session::handle_session_export(
|
||||
session_identifier,
|
||||
output,
|
||||
format,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
@@ -803,9 +840,15 @@ pub async fn cli() -> Result<()> {
|
||||
"Session started"
|
||||
);
|
||||
|
||||
let session_id = if let Some(id) = identifier {
|
||||
Some(get_session_id(id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Run session command by default
|
||||
let mut session: crate::Session = build_session(SessionBuilderConfig {
|
||||
identifier: identifier.map(extract_identifier),
|
||||
let mut session: crate::CliSession = build_session(SessionBuilderConfig {
|
||||
session_id,
|
||||
resume,
|
||||
no_session: false,
|
||||
extensions,
|
||||
@@ -841,6 +884,7 @@ pub async fn cli() -> Result<()> {
|
||||
|
||||
let (total_tokens, message_count) = session
|
||||
.get_metadata()
|
||||
.await
|
||||
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
@@ -994,9 +1038,14 @@ pub async fn cli() -> Result<()> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let session_id = if let Some(id) = identifier {
|
||||
Some(get_session_id(id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
identifier: identifier.map(extract_identifier),
|
||||
session_id,
|
||||
resume,
|
||||
no_session,
|
||||
extensions,
|
||||
@@ -1048,6 +1097,7 @@ pub async fn cli() -> Result<()> {
|
||||
|
||||
let (total_tokens, message_count) = session
|
||||
.get_metadata()
|
||||
.await
|
||||
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
@@ -1172,7 +1222,7 @@ pub async fn cli() -> Result<()> {
|
||||
} else {
|
||||
// Run session command by default
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
identifier: None,
|
||||
session_id: None,
|
||||
resume: false,
|
||||
no_session: false,
|
||||
extensions: Vec::new(),
|
||||
@@ -1188,7 +1238,7 @@ pub async fn cli() -> Result<()> {
|
||||
max_tool_repetitions: None,
|
||||
max_turns: None,
|
||||
scheduled_job_id: None,
|
||||
interactive: true, // Default case is always interactive
|
||||
interactive: true,
|
||||
quiet: false,
|
||||
sub_recipes: None,
|
||||
final_output_response: None,
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
¤t_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,
|
||||
¤t_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;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod session;
|
||||
pub mod signal;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use session::Session;
|
||||
pub use session::CliSession;
|
||||
|
||||
pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
|
||||
top_level_domain: "Block".to_string(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use goose::conversation::Conversation;
|
||||
use crate::scenario_tests::message_generator::MessageGenerator;
|
||||
use crate::scenario_tests::mock_client::weather_client;
|
||||
use crate::scenario_tests::provider_configs::{get_provider_configs, ProviderConfig};
|
||||
use crate::session::Session;
|
||||
use crate::session::CliSession;
|
||||
use anyhow::Result;
|
||||
use goose::agents::Agent;
|
||||
use goose::model::ModelConfig;
|
||||
@@ -218,7 +218,7 @@ where
|
||||
.update_provider(provider_arc as Arc<dyn goose::providers::base::Provider>)
|
||||
.await?;
|
||||
|
||||
let mut session = Session::new(agent, None, false, None, None, None, None);
|
||||
let mut session = CliSession::new(agent, None, false, None, None, None, None);
|
||||
|
||||
let mut error = None;
|
||||
for message in &messages {
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
use super::output;
|
||||
use super::CliSession;
|
||||
use console::style;
|
||||
use goose::agents::types::RetryConfig;
|
||||
use goose::agents::Agent;
|
||||
use goose::config::{Config, ExtensionConfig, ExtensionConfigManager};
|
||||
use goose::providers::create;
|
||||
use goose::recipe::{Response, SubRecipe};
|
||||
use goose::session;
|
||||
use goose::session::Identifier;
|
||||
|
||||
use goose::session::SessionManager;
|
||||
use rustyline::EditMode;
|
||||
use std::collections::HashSet;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::output;
|
||||
use super::Session;
|
||||
|
||||
/// Configuration for building a new Goose session
|
||||
///
|
||||
/// This struct contains all the parameters needed to create a new session,
|
||||
/// including session identification, extension configuration, and debug settings.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SessionBuilderConfig {
|
||||
/// Optional identifier for the session (name or path)
|
||||
pub identifier: Option<Identifier>,
|
||||
/// Optional identifier for the session
|
||||
pub session_id: Option<String>,
|
||||
/// Whether to resume an existing session
|
||||
pub resume: bool,
|
||||
/// Whether to run without a session file
|
||||
@@ -129,20 +128,8 @@ async fn offer_extension_debugging_help(
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary session file for this debugging session
|
||||
let temp_session_file =
|
||||
std::env::temp_dir().join(format!("goose_debug_extension_{}.jsonl", extension_name));
|
||||
|
||||
// Create the debugging session
|
||||
let mut debug_session = Session::new(
|
||||
debug_agent,
|
||||
Some(temp_session_file.clone()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut debug_session = CliSession::new(debug_agent, None, false, None, None, None, None);
|
||||
|
||||
// Process the debugging request
|
||||
println!("{}", style("Analyzing the extension failure...").yellow());
|
||||
@@ -160,10 +147,6 @@ async fn offer_extension_debugging_help(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the temporary session file
|
||||
let _ = std::fs::remove_file(temp_session_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -174,7 +157,7 @@ pub struct SessionSettings {
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
|
||||
pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
// Load config and get provider/model
|
||||
let config = Config::global();
|
||||
|
||||
@@ -257,68 +240,64 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
|
||||
});
|
||||
|
||||
// Handle session file resolution and resuming
|
||||
let session_file: Option<std::path::PathBuf> = if session_config.no_session {
|
||||
let session_id: Option<String> = if session_config.no_session {
|
||||
None
|
||||
} else if session_config.resume {
|
||||
if let Some(identifier) = session_config.identifier {
|
||||
let session_file = match session::get_path(identifier) {
|
||||
Err(e) => {
|
||||
output::render_error(&format!("Invalid session identifier: {}", e));
|
||||
if let Some(session_id) = session_config.session_id {
|
||||
match SessionManager::get_session(&session_id, false).await {
|
||||
Ok(_) => Some(session_id),
|
||||
Err(_) => {
|
||||
output::render_error(&format!(
|
||||
"Cannot resume session {} - no such session exists",
|
||||
style(&session_id).cyan()
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
Ok(path) => path,
|
||||
};
|
||||
if !session_file.exists() {
|
||||
output::render_error(&format!(
|
||||
"Cannot resume session {} - no such session exists",
|
||||
style(session_file.display()).cyan()
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
Some(session_file)
|
||||
} else {
|
||||
// Try to resume most recent session
|
||||
match session::get_most_recent_session() {
|
||||
Ok(file) => Some(file),
|
||||
match SessionManager::list_sessions().await {
|
||||
Ok(sessions) => {
|
||||
if sessions.is_empty() {
|
||||
output::render_error("Cannot resume - no previous sessions found");
|
||||
process::exit(1);
|
||||
}
|
||||
Some(sessions[0].id.clone())
|
||||
}
|
||||
Err(_) => {
|
||||
output::render_error("Cannot resume - no previous sessions found");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(session_id) = session_config.session_id {
|
||||
Some(session_id)
|
||||
} else {
|
||||
// Create new session with provided name/path or generated name
|
||||
let id = match session_config.identifier {
|
||||
Some(identifier) => identifier,
|
||||
None => Identifier::Name(session::generate_session_id()),
|
||||
};
|
||||
|
||||
// Just get the path - file will be created when needed
|
||||
match session::get_path(id) {
|
||||
Ok(path) => Some(path),
|
||||
Err(e) => {
|
||||
output::render_error(&format!("Failed to create session path: {}", e));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir().unwrap(),
|
||||
"CLI Session".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
Some(session.id)
|
||||
};
|
||||
|
||||
if session_config.resume {
|
||||
if let Some(session_file) = session_file.as_ref() {
|
||||
// Read the session metadata
|
||||
let metadata = session::read_metadata(session_file).unwrap_or_else(|e| {
|
||||
output::render_error(&format!("Failed to read session metadata: {}", e));
|
||||
process::exit(1);
|
||||
});
|
||||
if let Some(session_id) = session_id.as_ref() {
|
||||
// Read the session metadata from database
|
||||
let metadata = SessionManager::get_session(session_id, false)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
output::render_error(&format!("Failed to read session metadata: {}", e));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let current_workdir =
|
||||
std::env::current_dir().expect("Failed to get current working directory");
|
||||
if current_workdir != metadata.working_dir {
|
||||
// Ask user if they want to change the working directory
|
||||
let change_workdir = cliclack::confirm(format!("{} The original working directory of this session was set to {}. Your current directory is {}. Do you want to switch back to the original working directory?", style("WARNING:").yellow(), style(metadata.working_dir.display()).cyan(), style(current_workdir.display()).cyan()))
|
||||
.initial_value(true)
|
||||
.interact().expect("Failed to get user input");
|
||||
.initial_value(true)
|
||||
.interact().expect("Failed to get user input");
|
||||
|
||||
if change_workdir {
|
||||
if !metadata.working_dir.exists() {
|
||||
@@ -417,9 +396,9 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
|
||||
});
|
||||
|
||||
// Create new session
|
||||
let mut session = Session::new(
|
||||
let mut session = CliSession::new(
|
||||
Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")),
|
||||
session_file.clone(),
|
||||
session_id.clone(),
|
||||
session_config.debug,
|
||||
session_config.scheduled_job_id.clone(),
|
||||
session_config.max_turns,
|
||||
@@ -586,7 +565,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
|
||||
session_config.resume,
|
||||
&provider_name,
|
||||
&model_name,
|
||||
&session_file,
|
||||
&session_id,
|
||||
Some(&provider_for_display),
|
||||
);
|
||||
}
|
||||
@@ -600,7 +579,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_builder_config_creation() {
|
||||
let config = SessionBuilderConfig {
|
||||
identifier: Some(Identifier::Name("test".to_string())),
|
||||
session_id: Some("test".to_string()),
|
||||
resume: false,
|
||||
no_session: false,
|
||||
extensions: vec!["echo test".to_string()],
|
||||
@@ -639,7 +618,7 @@ mod tests {
|
||||
fn test_session_builder_config_default() {
|
||||
let config = SessionBuilderConfig::default();
|
||||
|
||||
assert!(config.identifier.is_none());
|
||||
assert!(config.session_id.is_none());
|
||||
assert!(!config.resume);
|
||||
assert!(!config.no_session);
|
||||
assert!(config.extensions.is_empty());
|
||||
|
||||
@@ -21,7 +21,6 @@ use goose::permission::permission_confirmation::PrincipalType;
|
||||
use goose::permission::Permission;
|
||||
use goose::permission::PermissionConfirmation;
|
||||
use goose::providers::base::Provider;
|
||||
pub use goose::session::Identifier;
|
||||
use goose::utils::safe_truncate;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -39,6 +38,7 @@ use rmcp::model::ServerNotification;
|
||||
use rmcp::model::{ErrorCode, ErrorData};
|
||||
|
||||
use goose::conversation::message::{Message, MessageContent};
|
||||
use goose::session::SessionManager;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rustyline::EditMode;
|
||||
use serde_json::Value;
|
||||
@@ -54,13 +54,12 @@ pub enum RunMode {
|
||||
Plan,
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub struct CliSession {
|
||||
agent: Agent,
|
||||
messages: Conversation,
|
||||
session_file: Option<PathBuf>,
|
||||
// Cache for completion data - using std::sync for thread safety without async
|
||||
session_id: Option<String>,
|
||||
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
||||
debug: bool, // New field for debug mode
|
||||
debug: bool,
|
||||
run_mode: RunMode,
|
||||
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
|
||||
max_turns: Option<u32>,
|
||||
@@ -111,8 +110,6 @@ pub async fn classify_planner_response(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// println!("classify_planner_response: {result:?}\n"); // TODO: remove
|
||||
|
||||
let predicted = result.as_concat_text();
|
||||
if predicted.to_lowercase().contains("plan") {
|
||||
Ok(PlannerResponseType::Plan)
|
||||
@@ -121,30 +118,33 @@ pub async fn classify_planner_response(
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
impl CliSession {
|
||||
pub fn new(
|
||||
agent: Agent,
|
||||
session_file: Option<PathBuf>,
|
||||
session_id: Option<String>,
|
||||
debug: bool,
|
||||
scheduled_job_id: Option<String>,
|
||||
max_turns: Option<u32>,
|
||||
edit_mode: Option<EditMode>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
) -> Self {
|
||||
let messages = if let Some(session_file) = &session_file {
|
||||
session::read_messages(session_file).unwrap_or_else(|e| {
|
||||
eprintln!("Warning: Failed to load message history: {}", e);
|
||||
Conversation::new_unvalidated(Vec::new())
|
||||
let messages = if let Some(session_id) = &session_id {
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
SessionManager::get_session(session_id, true)
|
||||
.await
|
||||
.map(|session| session.conversation.unwrap_or_default())
|
||||
.unwrap()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Don't try to read messages if we're not saving sessions
|
||||
Conversation::new_unvalidated(Vec::new())
|
||||
};
|
||||
|
||||
Session {
|
||||
CliSession {
|
||||
agent,
|
||||
messages,
|
||||
session_file,
|
||||
session_id,
|
||||
completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())),
|
||||
debug,
|
||||
run_mode: RunMode::Normal,
|
||||
@@ -155,13 +155,15 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to summarize context messages
|
||||
pub fn session_id(&self) -> Option<&String> {
|
||||
self.session_id.as_ref()
|
||||
}
|
||||
|
||||
async fn summarize_context_messages(
|
||||
messages: &mut Conversation,
|
||||
agent: &Agent,
|
||||
message_suffix: &str,
|
||||
) -> Result<()> {
|
||||
// Summarize messages to fit within context length
|
||||
let (summarized_messages, _, _) = agent.summarize_context(messages.messages()).await?;
|
||||
let msg = format!("Context maxed out\n{}\n{}", "-".repeat(50), message_suffix);
|
||||
output::render_text(&msg, Some(Color::Yellow), true);
|
||||
@@ -179,7 +181,6 @@ impl Session {
|
||||
let mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
||||
let mut envs = HashMap::new();
|
||||
|
||||
// Parse environment variables (format: KEY=value)
|
||||
while let Some(part) = parts.first() {
|
||||
if !part.contains('=') {
|
||||
break;
|
||||
@@ -194,7 +195,6 @@ impl Session {
|
||||
}
|
||||
|
||||
let cmd = parts.remove(0).to_string();
|
||||
// Generate a random name for the ephemeral extension
|
||||
let name: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
@@ -374,46 +374,10 @@ impl Session {
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let cancel_token = cancel_token.clone();
|
||||
let message_text = message.as_concat_text();
|
||||
|
||||
// TODO(Douwe): Make sure we generate the description here still:
|
||||
|
||||
self.push_message(message);
|
||||
// Get the provider from the agent for description generation
|
||||
let provider = self.agent.provider().await?;
|
||||
|
||||
// Persist messages with provider for automatic description generation
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = Some(
|
||||
std::env::current_dir().expect("failed to get current session working directory"),
|
||||
);
|
||||
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
Some(provider),
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Track the current directory and last instruction in projects.json
|
||||
let session_id = self
|
||||
.session_file
|
||||
.as_ref()
|
||||
.and_then(|p| p.file_stem())
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if let Err(e) = crate::project_tracker::update_project_tracker(
|
||||
Some(&message_text),
|
||||
session_id.as_deref(),
|
||||
) {
|
||||
eprintln!(
|
||||
"Warning: Failed to update project tracker with instruction: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
self.process_agent_response(false, cancel_token).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -493,35 +457,14 @@ impl Session {
|
||||
self.push_message(Message::user().with_text(&content));
|
||||
|
||||
// Track the current directory and last instruction in projects.json
|
||||
let session_id = self
|
||||
.session_file
|
||||
.as_ref()
|
||||
.and_then(|p| p.file_stem())
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if let Err(e) = crate::project_tracker::update_project_tracker(
|
||||
Some(&content),
|
||||
session_id.as_deref(),
|
||||
self.session_id.as_deref(),
|
||||
) {
|
||||
eprintln!("Warning: Failed to update project tracker with instruction: {}", e);
|
||||
}
|
||||
|
||||
let provider = self.agent.provider().await?;
|
||||
|
||||
// Persist messages with provider for automatic description generation
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = Some(std::env::current_dir().unwrap_or_default());
|
||||
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
Some(provider),
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let _provider = self.agent.provider().await?;
|
||||
|
||||
output::show_thinking();
|
||||
let start_time = Instant::now();
|
||||
@@ -659,16 +602,25 @@ impl Session {
|
||||
input::InputResult::Clear => {
|
||||
save_history(&mut editor);
|
||||
|
||||
if let Some(session_id) = &self.session_id {
|
||||
if let Err(e) = SessionManager::replace_conversation(
|
||||
session_id,
|
||||
&Conversation::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
output::render_error(&format!("Failed to clear session: {}", e));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.messages.clear();
|
||||
tracing::info!("Chat context cleared by user.");
|
||||
output::render_message(
|
||||
&Message::assistant().with_text("Chat context cleared."),
|
||||
self.debug,
|
||||
);
|
||||
if let Some(file) = self.session_file.as_ref().filter(|f| f.exists()) {
|
||||
std::fs::remove_file(file)?;
|
||||
std::fs::File::create(file)?;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
input::InputResult::PromptCommand(opts) => {
|
||||
@@ -729,44 +681,30 @@ impl Session {
|
||||
output::show_thinking();
|
||||
|
||||
// Get the provider for summarization
|
||||
let provider = self.agent.provider().await?;
|
||||
let _provider = self.agent.provider().await?;
|
||||
|
||||
// Call the summarize_context method which uses the summarize_messages function
|
||||
// Call the summarize_context method
|
||||
let (summarized_messages, _token_counts, summarization_usage) = self
|
||||
.agent
|
||||
.summarize_context(self.messages.messages())
|
||||
.await?;
|
||||
|
||||
// Update the session messages with the summarized ones
|
||||
self.messages = summarized_messages;
|
||||
self.messages = summarized_messages.clone();
|
||||
|
||||
// Persist the summarized messages and update session metadata with new token counts
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
Some(provider),
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
// Persist the summarized messages and update session metadata
|
||||
if let Some(session_id) = &self.session_id {
|
||||
// Replace all messages with the summarized version
|
||||
SessionManager::replace_conversation(session_id, &summarized_messages)
|
||||
.await?;
|
||||
|
||||
// Update session metadata with the new token counts from summarization
|
||||
if let Some(usage) = summarization_usage {
|
||||
let session_file_path = session::storage::get_path(
|
||||
session::storage::Identifier::Path(session_file.to_path_buf()),
|
||||
)?;
|
||||
let mut metadata =
|
||||
session::storage::read_metadata(&session_file_path)?;
|
||||
let session =
|
||||
SessionManager::get_session(session_id, false).await?;
|
||||
|
||||
// Update token counts with the summarization usage
|
||||
// Use output tokens as total since that's what's actually in the context going forward
|
||||
let summary_tokens = usage.usage.output_tokens.unwrap_or(0);
|
||||
metadata.total_tokens = Some(summary_tokens);
|
||||
metadata.input_tokens = None; // Clear input tokens since we now have a summary
|
||||
metadata.output_tokens = Some(summary_tokens);
|
||||
metadata.message_count = self.messages.len();
|
||||
|
||||
// Update accumulated tokens (add the summarization cost)
|
||||
let accumulate = |a: Option<i32>, b: Option<i32>| -> Option<i32> {
|
||||
@@ -775,20 +713,28 @@ impl Session {
|
||||
_ => a.or(b),
|
||||
}
|
||||
};
|
||||
metadata.accumulated_total_tokens = accumulate(
|
||||
metadata.accumulated_total_tokens,
|
||||
|
||||
let accumulated_total = accumulate(
|
||||
session.accumulated_total_tokens,
|
||||
usage.usage.total_tokens,
|
||||
);
|
||||
metadata.accumulated_input_tokens = accumulate(
|
||||
metadata.accumulated_input_tokens,
|
||||
let accumulated_input = accumulate(
|
||||
session.accumulated_input_tokens,
|
||||
usage.usage.input_tokens,
|
||||
);
|
||||
metadata.accumulated_output_tokens = accumulate(
|
||||
metadata.accumulated_output_tokens,
|
||||
let accumulated_output = accumulate(
|
||||
session.accumulated_output_tokens,
|
||||
usage.usage.output_tokens,
|
||||
);
|
||||
|
||||
session::storage::update_metadata(&session_file_path, &metadata)
|
||||
SessionManager::update_session(session_id)
|
||||
.total_tokens(Some(summary_tokens))
|
||||
.input_tokens(None)
|
||||
.output_tokens(Some(summary_tokens))
|
||||
.accumulated_total_tokens(accumulated_total)
|
||||
.accumulated_input_tokens(accumulated_input)
|
||||
.accumulated_output_tokens(accumulated_output)
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -808,19 +754,10 @@ impl Session {
|
||||
} else {
|
||||
println!("{}", console::style("Summarization cancelled.").yellow());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nClosing session.{}",
|
||||
self.session_file
|
||||
.as_ref()
|
||||
.map(|p| format!(" Recorded to {}", p.display()))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -919,16 +856,13 @@ impl Session {
|
||||
) -> Result<()> {
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
|
||||
let session_config = self.session_file.as_ref().map(|s| {
|
||||
let session_id = session::Identifier::Path(s.clone());
|
||||
SessionConfig {
|
||||
id: session_id.clone(),
|
||||
working_dir: std::env::current_dir().unwrap_or_default(),
|
||||
schedule_id: self.scheduled_job_id.clone(),
|
||||
execution_mode: None,
|
||||
max_turns: self.max_turns,
|
||||
retry_config: self.retry_config.clone(),
|
||||
}
|
||||
let session_config = self.session_id.as_ref().map(|session_id| SessionConfig {
|
||||
id: session_id.clone(),
|
||||
working_dir: std::env::current_dir().unwrap_or_default(),
|
||||
schedule_id: self.scheduled_job_id.clone(),
|
||||
execution_mode: None,
|
||||
max_turns: self.max_turns,
|
||||
retry_config: self.retry_config.clone(),
|
||||
});
|
||||
let mut stream = self
|
||||
.agent
|
||||
@@ -998,17 +932,6 @@ impl Session {
|
||||
Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None })
|
||||
));
|
||||
self.messages.push(response_message);
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
None,
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
cancel_token_clone.cancel();
|
||||
drop(stream);
|
||||
break;
|
||||
@@ -1140,22 +1063,8 @@ impl Session {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.messages.push(message.clone());
|
||||
|
||||
// No need to update description on assistant messages
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
None,
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if interactive {output::hide_thinking()};
|
||||
let _ = progress_bars.hide();
|
||||
output::render_message(&message, self.debug);
|
||||
@@ -1267,26 +1176,10 @@ impl Session {
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(Ok(AgentEvent::HistoryReplaced(new_messages))) => {
|
||||
// Replace the session's message history with the compacted messages
|
||||
self.messages = Conversation::new_unvalidated(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 })) => {
|
||||
Some(Ok(AgentEvent::HistoryReplaced(new_messages))) => {
|
||||
self.messages = Conversation::new_unvalidated(new_messages.clone());
|
||||
}
|
||||
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
|
||||
// Log model change if in debug mode
|
||||
if self.debug {
|
||||
eprintln!("Model changed to {} in {} mode", model, mode);
|
||||
@@ -1308,20 +1201,8 @@ impl Session {
|
||||
// Try auto-compaction first - keep the stream alive!
|
||||
if let Ok(compact_result) = goose::context_mgmt::auto_compact::perform_compaction(&self.agent, self.messages.messages()).await {
|
||||
self.messages = compact_result.messages;
|
||||
|
||||
// Persist the compacted messages
|
||||
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);
|
||||
}
|
||||
if let Some(session_id) = &self.session_id {
|
||||
SessionManager::replace_conversation(session_id, &self.messages).await?;
|
||||
}
|
||||
|
||||
output::render_text(
|
||||
@@ -1465,40 +1346,13 @@ impl Session {
|
||||
}),
|
||||
));
|
||||
}
|
||||
// TODO(Douwe): update also db
|
||||
self.push_message(response_message);
|
||||
|
||||
// No need for description update here
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
None,
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let prompt = format!(
|
||||
"The existing call to {} was interrupted. How would you like to proceed?",
|
||||
last_tool_name
|
||||
);
|
||||
self.push_message(Message::assistant().with_text(&prompt));
|
||||
|
||||
// No need for description update here
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
None,
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
output::render_message(&Message::assistant().with_text(&prompt), self.debug);
|
||||
} else {
|
||||
// An interruption occurred outside of a tool request-response.
|
||||
@@ -1509,20 +1363,6 @@ impl Session {
|
||||
// Interruption occurred after a tool had completed but not assistant reply
|
||||
let prompt = "The tool calling loop was interrupted. How would you like to proceed?";
|
||||
self.push_message(Message::assistant().with_text(prompt));
|
||||
|
||||
// No need for description update here
|
||||
if let Some(session_file) = &self.session_file {
|
||||
let working_dir = std::env::current_dir().ok();
|
||||
session::persist_messages_with_schedule_id(
|
||||
session_file,
|
||||
&self.messages,
|
||||
None,
|
||||
self.scheduled_job_id.clone(),
|
||||
working_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
output::render_message(
|
||||
&Message::assistant().with_text(prompt),
|
||||
self.debug,
|
||||
@@ -1545,10 +1385,6 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn session_file(&self) -> Option<PathBuf> {
|
||||
self.session_file.clone()
|
||||
}
|
||||
|
||||
/// Update the completion cache with fresh data
|
||||
/// This should be called before the interactive session starts
|
||||
pub async fn update_completion_cache(&mut self) -> Result<()> {
|
||||
@@ -1619,17 +1455,16 @@ impl Session {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_metadata(&self) -> Result<session::SessionMetadata> {
|
||||
if !self.session_file.as_ref().is_some_and(|f| f.exists()) {
|
||||
return Err(anyhow::anyhow!("Session file does not exist"));
|
||||
pub async fn get_metadata(&self) -> Result<session::Session> {
|
||||
match &self.session_id {
|
||||
Some(id) => SessionManager::get_session(id, false).await,
|
||||
None => Err(anyhow::anyhow!("No session available")),
|
||||
}
|
||||
|
||||
session::read_metadata(self.session_file.as_ref().unwrap())
|
||||
}
|
||||
|
||||
// Get the session's total token usage
|
||||
pub fn get_total_token_usage(&self) -> Result<Option<i32>> {
|
||||
let metadata = self.get_metadata()?;
|
||||
pub async fn get_total_token_usage(&self) -> Result<Option<i32>> {
|
||||
let metadata = self.get_metadata().await?;
|
||||
Ok(metadata.total_tokens)
|
||||
}
|
||||
|
||||
@@ -1661,7 +1496,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
match self.get_metadata() {
|
||||
match self.get_metadata().await {
|
||||
Ok(metadata) => {
|
||||
let total_tokens = metadata.total_tokens.unwrap_or(0) as usize;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use serde_json::Value;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -685,12 +685,12 @@ pub fn display_session_info(
|
||||
resume: bool,
|
||||
provider: &str,
|
||||
model: &str,
|
||||
session_file: &Option<PathBuf>,
|
||||
session_id: &Option<String>,
|
||||
provider_instance: Option<&Arc<dyn goose::providers::base::Provider>>,
|
||||
) {
|
||||
let start_session_msg = if resume {
|
||||
"resuming session |"
|
||||
} else if session_file.is_none() {
|
||||
} else if session_id.is_none() {
|
||||
"running without session |"
|
||||
} else {
|
||||
"starting session |"
|
||||
@@ -732,14 +732,6 @@ pub fn display_session_info(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(session_file) = session_file {
|
||||
println!(
|
||||
" {} {}",
|
||||
style("logging to").dim(),
|
||||
style(session_file.display()).dim().cyan(),
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
" {} {}",
|
||||
style("working directory:").dim(),
|
||||
|
||||
Reference in New Issue
Block a user