Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
801 changes: 477 additions & 324 deletions Cargo.lock

Large diffs are not rendered by default.

70 changes: 52 additions & 18 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,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;
Expand Down Expand Up @@ -49,26 +49,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())),
Expand Down Expand Up @@ -768,19 +787,20 @@ 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 }) => {
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);
Expand All @@ -789,7 +809,8 @@ pub async fn cli() -> Result<()> {
}
};

crate::commands::session::handle_session_export(session_identifier, output)?;
crate::commands::session::handle_session_export(session_identifier, output)
.await?;
Ok(())
}
None => {
Expand All @@ -803,9 +824,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,
Expand Down Expand Up @@ -841,6 +868,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));

Expand Down Expand Up @@ -993,9 +1021,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,
Expand Down Expand Up @@ -1047,6 +1080,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));

Expand Down Expand Up @@ -1171,7 +1205,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(),
Expand All @@ -1187,7 +1221,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,
Expand Down
19 changes: 10 additions & 9 deletions crates/goose-cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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};
Expand All @@ -11,28 +11,31 @@ 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()
// Return None since CliSession doesn't have a session file concept
None
}
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())
})
}
}
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,
Expand All @@ -56,10 +59,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");

Expand Down
3 changes: 1 addition & 2 deletions crates/goose-cli/src/commands/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
}
Expand Down
Loading