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
+69 -19
View File
@@ -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,