Skip to content
129 changes: 84 additions & 45 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,74 +322,77 @@ async fn get_or_create_session_id(

let session_manager = SessionManager::instance();

let Some(id) = identifier else {
return if resume {
let resolved_id = if resume {
let Some(id) = identifier else {
let sessions = session_manager.list_sessions().await?;
let session_id = sessions
.first()
.map(|s| s.id.clone())
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
Ok(Some(session_id))
return Ok(Some(session_id));
};

if let Some(session_id) = id.session_id {
session_id
} else if let Some(name) = id.name {
let sessions = session_manager.list_sessions().await?;
sessions
.into_iter()
.find(|s| s.name == name || s.id == name)
.map(|s| s.id)
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?
} else if let Some(path) = id.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 {
return Err(anyhow::anyhow!("Invalid identifier"));
}
} else {
let Some(id) = identifier else {
let session = session_manager
.create_session(
std::env::current_dir()?,
"CLI Session".to_string(),
SessionType::User,
)
.await?;
Ok(Some(session.id))
return Ok(Some(session.id));
};
};

if let Some(session_id) = id.session_id {
Ok(Some(session_id))
} else if let Some(name) = id.name {
if resume {
let sessions = session_manager.list_sessions().await?;
let session_id = sessions
.into_iter()
.find(|s| s.name == name || s.id == name)
.map(|s| s.id)
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?;
Ok(Some(session_id))
} else {
let session = session_manager
.create_session(std::env::current_dir()?, name.clone(), SessionType::User)
.await?;
if id.session_id.is_some() {
return Err(anyhow::anyhow!("Cannot use --session-id without --resume"));
}

let has_user_provided_name = id.name.is_some();
let name = id.name.unwrap_or_else(|| "CLI Session".to_string());
let session = session_manager
.create_session(std::env::current_dir()?, name.clone(), SessionType::User)
.await?;

if has_user_provided_name {
session_manager
.update(&session.id)
.user_provided_name(name)
.apply()
.await?;

Ok(Some(session.id))
}
} else if let Some(path) = id.path {
let session_id = 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))?;
Ok(Some(session_id))
} else {
let session = session_manager
.create_session(
std::env::current_dir()?,
"CLI Session".to_string(),
SessionType::User,
)
.await?;
Ok(Some(session.id))
}

return Ok(Some(session.id));
};

Ok(Some(resolved_id))
}

async fn lookup_session_id(identifier: Identifier) -> Result<String> {
let session_manager = SessionManager::instance();

if let Some(session_id) = identifier.session_id {
Ok(session_id)
} else if let Some(name) = identifier.name {
let session_manager = SessionManager::instance();
let sessions = session_manager.list_sessions().await?;
sessions
.into_iter()
Expand Down Expand Up @@ -722,6 +725,15 @@ enum Command {
)]
resume: bool,

/// Fork a previous session (creates new session with copied history)
#[arg(
long,
requires = "resume",
help = "Fork a previous session (creates new session with copied history)",
long_help = "Create a new session by copying all messages from a previous session. Must be used with --resume. If --name or --session-id is provided, forks that specific session. Otherwise forks the most recently used session."
)]
fork: bool,

/// Show message history when resuming
#[arg(
long,
Expand Down Expand Up @@ -1047,6 +1059,7 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> {
async fn handle_interactive_session(
identifier: Option<Identifier>,
resume: bool,
fork: bool,
history: bool,
session_opts: SessionOptions,
extension_opts: ExtensionOptions,
Expand All @@ -1056,7 +1069,13 @@ async fn handle_interactive_session(
}

let session_start = std::time::Instant::now();
let session_type = if resume { "resumed" } else { "new" };
let session_type = if fork {
"forked"
} else if resume {
"resumed"
} else {
"new"
};

tracing::info!(
counter.goose.session_starts = 1,
Expand All @@ -1076,11 +1095,21 @@ async fn handle_interactive_session(
}
}

let session_id = get_or_create_session_id(identifier, resume, false).await?;
let mut session_id = get_or_create_session_id(identifier, resume, false).await?;

if fork {
if let Some(id) = session_id {
let session_manager = SessionManager::instance();
let original = session_manager.get_session(&id, false).await?;
let copied = session_manager.copy_session(&id, original.name).await?;
session_id = Some(copied.id);
}
}

let mut session: crate::CliSession = build_session(SessionBuilderConfig {
session_id,
resume,
fork,
no_session: false,
extensions: extension_opts.extensions,
streamable_http_extensions: extension_opts.streamable_http_extensions,
Expand All @@ -1099,7 +1128,7 @@ async fn handle_interactive_session(
})
.await;

if resume && history {
if (resume || fork) && history {
session.render_message_history();
}

Expand Down Expand Up @@ -1283,6 +1312,7 @@ async fn handle_run_command(
let mut session = build_session(SessionBuilderConfig {
session_id,
resume: run_behavior.resume,
fork: false,
no_session: run_behavior.no_session,
extensions: extension_opts.extensions,
streamable_http_extensions: extension_opts.streamable_http_extensions,
Expand Down Expand Up @@ -1407,6 +1437,7 @@ async fn handle_default_session() -> Result<()> {
let mut session = build_session(SessionBuilderConfig {
session_id,
resume: false,
fork: false,
no_session: false,
extensions: Vec::new(),
streamable_http_extensions: Vec::new(),
Expand Down Expand Up @@ -1458,12 +1489,20 @@ pub async fn cli() -> anyhow::Result<()> {
command: None,
identifier,
resume,
fork,
history,
session_opts,
extension_opts,
}) => {
handle_interactive_session(identifier, resume, history, session_opts, extension_opts)
.await
handle_interactive_session(
identifier,
resume,
fork,
history,
session_opts,
extension_opts,
)
.await
}
Some(Command::Project {}) => {
handle_project_default()?;
Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub async fn agent_generator(
let base_session = build_session(SessionBuilderConfig {
session_id: Some(session_id),
resume: false,
fork: false,
no_session: false,
extensions: requirements.external,
streamable_http_extensions: requirements.streamable_http,
Expand Down
5 changes: 5 additions & 0 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub struct SessionBuilderConfig {
pub session_id: Option<String>,
/// Whether to resume an existing session
pub resume: bool,
/// Whether to fork an existing session (creates a copy of the original/existing session then resumes the copy)
pub fork: bool,
/// Whether to run without a session file
pub no_session: bool,
/// List of stdio extension commands to add
Expand Down Expand Up @@ -121,6 +123,7 @@ impl Default for SessionBuilderConfig {
SessionBuilderConfig {
session_id: None,
resume: false,
fork: false,
no_session: false,
extensions: Vec::new(),
streamable_http_extensions: Vec::new(),
Expand Down Expand Up @@ -659,6 +662,7 @@ mod tests {
let config = SessionBuilderConfig {
session_id: None,
resume: false,
fork: false,
no_session: false,
extensions: vec!["echo test".to_string()],
streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()],
Expand Down Expand Up @@ -705,6 +709,7 @@ mod tests {
assert!(config.scheduled_job_id.is_none());
assert!(!config.interactive);
assert!(!config.quiet);
assert!(!config.fork);
}

#[tokio::test]
Expand Down
7 changes: 3 additions & 4 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::session::export_session,
super::routes::session::import_session,
super::routes::session::update_session_user_recipe_values,
super::routes::session::edit_message,
super::routes::session::fork_session,
super::routes::session::get_session_extensions,
super::routes::schedule::create_schedule,
super::routes::schedule::list_schedules,
Expand Down Expand Up @@ -442,9 +442,8 @@ derive_utoipa!(Icon as IconSchema);
super::routes::session::UpdateSessionNameRequest,
super::routes::session::UpdateSessionUserRecipeValuesRequest,
super::routes::session::UpdateSessionUserRecipeValuesResponse,
super::routes::session::EditType,
super::routes::session::EditMessageRequest,
super::routes::session::EditMessageResponse,
super::routes::session::ForkRequest,
super::routes::session::ForkResponse,
super::routes::session::SessionExtensionsResponse,
Message,
MessageContent,
Expand Down
Loading
Loading