Nicer API

This commit is contained in:
Jack Amadeo
2025-12-29 15:36:24 -05:00
parent 3007cad226
commit 2aa8c560e7
15 changed files with 171 additions and 126 deletions
+1 -5
View File
@@ -26,8 +26,7 @@ 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::{build_session, SessionBuilderConfig, SessionSettings};
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use goose_bench::bench_config::BenchRunConfig;
use goose_bench::runners::bench_runner::BenchRunner;
use goose_bench::runners::eval_runner::EvalRunner;
@@ -97,7 +96,6 @@ async fn get_or_create_session_id(
std::env::current_dir()?,
"CLI Session".to_string(),
SessionType::User,
None,
)
.await?;
Ok(Some(session.id))
@@ -120,7 +118,6 @@ async fn get_or_create_session_id(
std::env::current_dir()?,
name.clone(),
SessionType::User,
None,
)
.await?;
@@ -143,7 +140,6 @@ async fn get_or_create_session_id(
std::env::current_dir()?,
"CLI Session".to_string(),
SessionType::User,
None,
)
.await?;
Ok(Some(session.id))
+1 -4
View File
@@ -9,8 +9,7 @@ use goose::conversation::message::{Message, MessageContent};
use goose::conversation::Conversation;
use goose::mcp_utils::ToolResult;
use goose::providers::create;
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role};
use std::collections::{HashMap, HashSet};
use std::fs;
@@ -230,7 +229,6 @@ impl GooseAcpAgent {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"ACP Session".to_string(),
SessionType::Hidden,
None,
)
.await?;
@@ -571,7 +569,6 @@ impl acp::Agent for GooseAcpAgent {
std::env::current_dir().unwrap_or_default(),
"ACP Session".to_string(), // just an initial name - may be replaced by maybe_update_name
SessionType::User,
None,
)
.await?;
@@ -1456,7 +1456,6 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Tool Permission Configuration".to_string(),
SessionType::Hidden,
None,
)
.await?;
-1
View File
@@ -135,7 +135,6 @@ pub async fn handle_term_init(
working_dir,
"Goose Term Session".to_string(),
SessionType::Terminal,
None,
)
.await?;
+1 -4
View File
@@ -15,8 +15,7 @@ use base64::Engine;
use futures::{sink::SinkExt, stream::StreamExt};
use goose::agents::{Agent, AgentEvent};
use goose::conversation::message::Message as GooseMessage;
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{net::SocketAddr, sync::Arc};
@@ -157,7 +156,6 @@ pub async fn handle_web(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Web Agent Initialization".to_string(),
SessionType::Hidden,
None,
)
.await?;
@@ -246,7 +244,6 @@ async fn serve_index(uri: Uri) -> Result<Redirect, (http::StatusCode, String)> {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Web session".to_string(),
SessionType::User,
None,
)
.await
.map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
@@ -9,8 +9,7 @@ use anyhow::Result;
use goose::agents::Agent;
use goose::model::ModelConfig;
use goose::providers::{create, testprovider::TestProvider};
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -221,7 +220,6 @@ where
PathBuf::default(),
"scenario-runner".to_string(),
SessionType::Hidden,
None,
)
.await?;
+1 -5
View File
@@ -11,9 +11,7 @@ use goose::providers::create;
use goose::recipe::{Response, SubRecipe};
use goose::agents::extension::PlatformExtensionContext;
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{EnabledExtensionsState, ExtensionState};
use goose::session::{EnabledExtensionsState, ExtensionState, SessionManager, SessionType};
use rustyline::EditMode;
use std::collections::HashSet;
use std::process;
@@ -154,7 +152,6 @@ async fn offer_extension_debugging_help(
std::env::current_dir()?,
"CLI Session".to_string(),
SessionType::Hidden,
None,
)
.await?;
@@ -345,7 +342,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
working_dir,
"CLI Session".to_string(),
SessionType::Hidden,
None,
)
.await
.expect("Could not create session");
+15 -13
View File
@@ -19,8 +19,7 @@ use goose::prompt_template::render_global_file;
use goose::providers::create;
use goose::recipe::Recipe;
use goose::recipe_deeplink;
use goose::session::session_manager::SessionType;
use goose::session::{Session, SessionManager};
use goose::session::{Session, SessionManager, SessionType};
use goose::{
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
config::permission::PermissionLevel,
@@ -175,17 +174,20 @@ async fn start_agent(
let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1;
let name = format!("New session {}", counter);
let mut session =
SessionManager::create_session(PathBuf::from(&working_dir), name, SessionType::User, None)
.await
.map_err(|err| {
error!("Failed to create session: {}", err);
goose::posthog::emit_error("session_create_failed", &err.to_string());
ErrorResponse {
message: format!("Failed to create session: {}", err),
status: StatusCode::BAD_REQUEST,
}
})?;
let mut session = SessionManager::create_session(
PathBuf::from(&working_dir),
name,
SessionType::User,
)
.await
.map_err(|err| {
error!("Failed to create session: {}", err);
goose::posthog::emit_error("session_create_failed", &err.to_string());
ErrorResponse {
message: format!("Failed to create session: {}", err),
status: StatusCode::BAD_REQUEST,
}
})?;
if let Some(recipe) = original_recipe {
SessionManager::update_session(&session.id)
+1 -3
View File
@@ -5,8 +5,7 @@ use goose::config::{DEFAULT_EXTENSION_DESCRIPTION, DEFAULT_EXTENSION_TIMEOUT};
use goose::conversation::message::Message;
use goose::providers::create_with_named_model;
use goose::providers::databricks::DATABRICKS_DEFAULT_MODEL;
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use std::path::PathBuf;
#[tokio::main]
@@ -21,7 +20,6 @@ async fn main() -> anyhow::Result<()> {
PathBuf::default(),
"max-turn-test".to_string(),
SessionType::Hidden,
None,
)
.await?;
+5 -9
View File
@@ -433,7 +433,7 @@ impl Agent {
session: &Session,
) -> (String, Result<ToolCallResult, ErrorData>) {
// Prevent subagents from creating other subagents
if session.session_type == SessionType::SubAgent && tool_call.name == SUBAGENT_TOOL_NAME {
if matches!(session.session_type, SessionType::SubAgent { .. }) && tool_call.name == SUBAGENT_TOOL_NAME {
return (
request_id,
Err(ErrorData::new(
@@ -657,14 +657,10 @@ impl Agent {
return false;
}
if let Some(ref session_id) = self.extension_manager.get_context().await.session_id {
if matches!(
SessionManager::get_session(session_id, false)
.await
.ok()
.map(|session| session.session_type),
Some(SessionType::SubAgent)
) {
return false;
if let Ok(session) = SessionManager::get_session(session_id, false).await {
if matches!(session.session_type, SessionType::SubAgent { .. }) {
return false;
}
}
}
!self
+1 -4
View File
@@ -18,8 +18,6 @@ use crate::providers::toolshim::{
use crate::agents::code_execution_extension::EXTENSION_NAME as CODE_EXECUTION_EXTENSION;
use crate::session::SessionManager;
#[cfg(test)]
use crate::session::SessionType;
use rmcp::model::Tool;
fn coerce_value(s: &str, schema: &Value) -> Value {
@@ -444,8 +442,7 @@ mod tests {
let session = SessionManager::create_session(
std::path::PathBuf::default(),
"test-prepare-tools".to_string(),
SessionType::Hidden,
None,
crate::session::SessionType::Hidden,
)
.await?;
+4 -3
View File
@@ -16,7 +16,7 @@ use crate::providers;
use crate::recipe::build_recipe::build_recipe_from_template;
use crate::recipe::local_recipes::load_local_recipe_file;
use crate::recipe::{Recipe, SubRecipe};
use crate::session::SessionManager;
use crate::session::{SessionManager, SessionType};
pub const SUBAGENT_TOOL_NAME: &str = "subagent";
@@ -245,8 +245,9 @@ async fn execute_subagent(
let session = SessionManager::create_session(
working_dir,
"Subagent task".to_string(),
crate::session::session_manager::SessionType::SubAgent,
Some(task_config.parent_session_id.clone()),
SessionType::SubAgent {
parent_session_id: task_config.parent_session_id.clone(),
},
)
.await
.map_err(|e| ErrorData {
+1 -3
View File
@@ -22,8 +22,7 @@ use crate::posthog;
use crate::providers::create;
use crate::recipe::Recipe;
use crate::scheduler_trait::SchedulerTrait;
use crate::session::session_manager::SessionType;
use crate::session::{Session, SessionManager};
use crate::session::{Session, SessionManager, SessionType};
type RunningTasksMap = HashMap<String, CancellationToken>;
type JobsMap = HashMap<String, (JobId, ScheduledJob)>;
@@ -744,7 +743,6 @@ async fn execute_job(
std::env::current_dir()?,
format!("Scheduled job: {}", job.id),
SessionType::Scheduled,
None,
)
.await?;
+137 -65
View File
@@ -23,12 +23,11 @@ pub const CURRENT_SCHEMA_VERSION: i32 = 7;
pub const SESSIONS_FOLDER: &str = "sessions";
pub const DB_NAME: &str = "sessions.db";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionType {
User,
Scheduled,
SubAgent,
SubAgent { parent_session_id: String },
Hidden,
Terminal,
}
@@ -39,33 +38,109 @@ impl Default for SessionType {
}
}
impl std::fmt::Display for SessionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl SessionType {
/// Get the type string for database storage
pub fn type_string(&self) -> &str {
match self {
SessionType::User => write!(f, "user"),
SessionType::SubAgent => write!(f, "sub_agent"),
SessionType::Hidden => write!(f, "hidden"),
SessionType::Scheduled => write!(f, "scheduled"),
SessionType::Terminal => write!(f, "terminal"),
SessionType::User => "user",
SessionType::SubAgent { .. } => "sub_agent",
SessionType::Hidden => "hidden",
SessionType::Scheduled => "scheduled",
SessionType::Terminal => "terminal",
}
}
/// Extract parent_session_id if this is a SubAgent
pub fn parent_session_id(&self) -> Option<&str> {
match self {
SessionType::SubAgent { parent_session_id } => Some(parent_session_id),
_ => None,
}
}
/// Reconstruct SessionType from database columns
pub fn from_db(type_str: &str, parent_id: Option<String>) -> Result<Self> {
match type_str {
"user" => Ok(SessionType::User),
"sub_agent" => Ok(SessionType::SubAgent {
parent_session_id: parent_id.ok_or_else(|| {
anyhow::anyhow!("SubAgent session type requires parent_session_id")
})?,
}),
"hidden" => Ok(SessionType::Hidden),
"scheduled" => Ok(SessionType::Scheduled),
"terminal" => Ok(SessionType::Terminal),
_ => Err(anyhow::anyhow!("Invalid session type: {}", type_str)),
}
}
}
impl std::str::FromStr for SessionType {
type Err = anyhow::Error;
impl std::fmt::Display for SessionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.type_string())
}
}
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
// Custom serialization to maintain JSON compatibility (serialize as simple string)
impl Serialize for SessionType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.type_string())
}
}
// Custom deserialization - for JSON, we don't have parent_id, so we use a placeholder
// The actual parent_id will come from the Session.parent_session_id field
impl<'de> Deserialize<'de> for SessionType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"user" => Ok(SessionType::User),
"sub_agent" => Ok(SessionType::SubAgent),
"sub_agent" => {
// When deserializing from JSON, we don't have the parent_id yet
// It will be populated from Session.parent_session_id field
Ok(SessionType::SubAgent {
parent_session_id: String::new(),
})
}
"hidden" => Ok(SessionType::Hidden),
"scheduled" => Ok(SessionType::Scheduled),
"terminal" => Ok(SessionType::Terminal),
_ => Err(anyhow::anyhow!("Invalid session type: {}", s)),
_ => Err(serde::de::Error::custom(format!(
"Invalid session type: {}",
s
))),
}
}
}
// Manual ToSchema implementation for SessionType since it serializes as a string
impl utoipa::ToSchema<'_> for SessionType {
fn schema() -> (
&'static str,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
) {
(
"SessionType",
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::SchemaType::String)
.enum_values(Some(vec![
"user",
"scheduled",
"sub_agent",
"hidden",
"terminal",
]))
.into(),
)
}
}
static SESSION_STORAGE: OnceCell<Arc<SessionStorage>> = OnceCell::const_new();
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -273,8 +348,8 @@ impl SessionManager {
working_dir: PathBuf,
name: String,
session_type: SessionType,
parent_session_id: Option<String>,
) -> Result<Session> {
let parent_session_id = session_type.parent_session_id().map(String::from);
Self::instance()
.await?
.create_session(working_dir, name, session_type, parent_session_id)
@@ -445,47 +520,35 @@ impl Session {
}
impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> sqlx::Result<Self> {
use sqlx::Row;
let recipe_json: Option<String> = row.try_get("recipe_json")?;
let recipe = recipe_json.and_then(|json| serde_json::from_str(&json).ok());
let session_type_str: String = row.try_get("session_type")?;
let parent_session_id: Option<String> = row.try_get("parent_session_id")?;
let user_recipe_values_json: Option<String> = row.try_get("user_recipe_values_json")?;
let user_recipe_values =
user_recipe_values_json.and_then(|json| serde_json::from_str(&json).ok());
let model_config_json: Option<String> = row.try_get("model_config_json").ok().flatten();
let model_config = model_config_json.and_then(|json| serde_json::from_str(&json).ok());
let parent_session_id: Option<String> = row.try_get("parent_session_id").ok().flatten();
let name: String = {
let name_val: String = row.try_get("name").unwrap_or_default();
if !name_val.is_empty() {
name_val
} else {
row.try_get("description").unwrap_or_default()
}
};
let user_set_name = row.try_get("user_set_name").unwrap_or(false);
let session_type_str: String = row
.try_get("session_type")
.unwrap_or_else(|_| "user".to_string());
let session_type = session_type_str.parse().unwrap_or_default();
let session_type = SessionType::from_db(&session_type_str, parent_session_id.clone())
.map_err(|e| sqlx::Error::ColumnDecode {
index: "session_type".to_string(),
source: Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
)),
})?;
Ok(Session {
id: row.try_get("id")?,
working_dir: PathBuf::from(row.try_get::<String, _>("working_dir")?),
name,
user_set_name,
working_dir: row.try_get::<String, _>("working_dir")?.into(),
name: row
.try_get::<Option<String>, _>("name")?
.or_else(|| row.try_get("description").ok())
.unwrap_or_default(),
user_set_name: row.try_get("user_set_name")?,
session_type,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
extension_data: serde_json::from_str(&row.try_get::<String, _>("extension_data")?)
.unwrap_or_default(),
extension_data: row.try_get::<String, _>("extension_data").and_then(|s| {
serde_json::from_str(&s).map_err(|e| sqlx::Error::Decode(Box::new(e)))
})?,
total_tokens: row.try_get("total_tokens")?,
input_tokens: row.try_get("input_tokens")?,
output_tokens: row.try_get("output_tokens")?,
@@ -493,12 +556,18 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
accumulated_input_tokens: row.try_get("accumulated_input_tokens")?,
accumulated_output_tokens: row.try_get("accumulated_output_tokens")?,
schedule_id: row.try_get("schedule_id")?,
recipe,
user_recipe_values,
conversation: None,
message_count: row.try_get("message_count").unwrap_or(0) as usize,
provider_name: row.try_get("provider_name").ok().flatten(),
model_config,
recipe: row
.try_get::<Option<String>, _>("recipe_json")?
.and_then(|s| serde_json::from_str(&s).ok()),
user_recipe_values: row
.try_get::<Option<String>, _>("user_recipe_values_json")?
.and_then(|s| serde_json::from_str(&s).ok()),
conversation: None, // Populated separately
message_count: row.try_get::<i64, _>("message_count").unwrap_or(0) as usize,
provider_name: row.try_get("provider_name")?,
model_config: row
.try_get::<Option<String>, _>("model_config_json")?
.and_then(|s| serde_json::from_str(&s).ok()),
parent_session_id,
})
}
@@ -1271,7 +1340,9 @@ impl SessionStorage {
fn build_recursive_export<'a>(
&'a self,
session: Session,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<RecursiveSessionExport>> + Send + 'a>> {
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<RecursiveSessionExport>> + Send + 'a>,
> {
Box::pin(async move {
// Query for all sub-agent sessions
let subagent_sessions = sqlx::query_as::<_, Session>(
@@ -1374,12 +1445,7 @@ impl SessionStorage {
// Import parent session first
let parent_session = self
.import_session_with_parent(
recursive_import.session,
None,
&mut id_mapping,
&mut tx,
)
.import_session_with_parent(recursive_import.session, None, &mut id_mapping, &mut tx)
.await?;
// Import all sub-agents recursively
@@ -1825,7 +1891,9 @@ mod tests {
.create_session(
PathBuf::from("/tmp/test"),
"Subagent session".to_string(),
SessionType::SubAgent,
SessionType::SubAgent {
parent_session_id: parent.id.clone(),
},
Some(parent.id.clone()),
)
.await
@@ -1878,7 +1946,9 @@ mod tests {
.create_session(
PathBuf::from("/tmp/test"),
"Subagent 1".to_string(),
SessionType::SubAgent,
SessionType::SubAgent {
parent_session_id: parent.id.clone(),
},
Some(parent.id.clone()),
)
.await
@@ -1902,7 +1972,9 @@ mod tests {
.create_session(
PathBuf::from("/tmp/test"),
"Subagent 2".to_string(),
SessionType::SubAgent,
SessionType::SubAgent {
parent_session_id: parent.id.clone(),
},
Some(parent.id.clone()),
)
.await
+2 -3
View File
@@ -302,8 +302,8 @@ mod tests {
use goose::model::ModelConfig;
use goose::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage};
use goose::providers::errors::ProviderError;
use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use rmcp::model::{CallToolRequestParam, Tool};
use rmcp::object;
use std::path::PathBuf;
@@ -379,7 +379,6 @@ mod tests {
PathBuf::default(),
"max-turn-test".to_string(),
SessionType::Hidden,
None,
)
.await?;