Sessions required (#5548)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
+21
-10
@@ -19,6 +19,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_bench::bench_config::BenchRunConfig;
|
||||
use goose_bench::runners::bench_runner::BenchRunner;
|
||||
@@ -86,9 +87,12 @@ async fn get_or_create_session_id(
|
||||
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
|
||||
Ok(Some(session_id))
|
||||
} else {
|
||||
let session =
|
||||
SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string())
|
||||
.await?;
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir()?,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::User,
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(session.id))
|
||||
};
|
||||
};
|
||||
@@ -105,8 +109,12 @@ async fn get_or_create_session_id(
|
||||
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?;
|
||||
Ok(Some(session_id))
|
||||
} else {
|
||||
let session =
|
||||
SessionManager::create_session(std::env::current_dir()?, name.clone()).await?;
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir()?,
|
||||
name.clone(),
|
||||
SessionType::User,
|
||||
)
|
||||
.await?;
|
||||
|
||||
SessionManager::update_session(&session.id)
|
||||
.user_provided_name(name)
|
||||
@@ -123,9 +131,12 @@ async fn get_or_create_session_id(
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))?;
|
||||
Ok(Some(session_id))
|
||||
} else {
|
||||
let session =
|
||||
SessionManager::create_session(std::env::current_dir()?, "CLI Session".to_string())
|
||||
.await?;
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir()?,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::User,
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(session.id))
|
||||
}
|
||||
}
|
||||
@@ -977,7 +988,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
let exit_type = if result.is_ok() { "normal" } else { "error" };
|
||||
|
||||
let (total_tokens, message_count) = session
|
||||
.get_metadata()
|
||||
.get_session()
|
||||
.await
|
||||
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
|
||||
.unwrap_or((0, 0));
|
||||
@@ -1198,7 +1209,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
let exit_type = if result.is_ok() { "normal" } else { "error" };
|
||||
|
||||
let (total_tokens, message_count) = session
|
||||
.get_metadata()
|
||||
.get_session()
|
||||
.await
|
||||
.map(|m| (m.total_tokens.unwrap_or(0), m.message_count))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
@@ -3,11 +3,13 @@ use agent_client_protocol::{
|
||||
ToolCallContent,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use goose::agents::Agent;
|
||||
use goose::agents::{Agent, SessionConfig};
|
||||
use goose::config::{get_all_extensions, Config};
|
||||
use goose::conversation::message::{Message, MessageContent};
|
||||
use goose::conversation::Conversation;
|
||||
use goose::providers::create;
|
||||
use goose::session::session_manager::SessionType;
|
||||
use goose::session::SessionManager;
|
||||
use rmcp::model::{RawContent, ResourceContents};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
@@ -19,17 +21,15 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
/// Represents a single goose session for ACP
|
||||
struct GooseSession {
|
||||
struct GooseAcpSession {
|
||||
messages: Conversation,
|
||||
tool_call_ids: HashMap<String, String>, // Maps internal tool IDs to ACP tool call IDs
|
||||
cancel_token: Option<CancellationToken>, // Active cancellation token for prompt processing
|
||||
}
|
||||
|
||||
/// goose ACP Agent implementation that connects to real goose agents
|
||||
struct GooseAcpAgent {
|
||||
session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>,
|
||||
sessions: Arc<Mutex<HashMap<String, GooseSession>>>,
|
||||
session_update_tx: mpsc::UnboundedSender<(SessionNotification, oneshot::Sender<()>)>,
|
||||
sessions: Arc<Mutex<HashMap<String, GooseAcpSession>>>,
|
||||
agent: Agent, // Shared agent instance
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ impl GooseAcpAgent {
|
||||
async fn new(
|
||||
session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>,
|
||||
) -> Result<Self> {
|
||||
// Load config and create provider
|
||||
let config = Config::global();
|
||||
|
||||
let provider_name: String = config
|
||||
@@ -217,7 +216,7 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
content_item: &MessageContent,
|
||||
session_id: &acp::SessionId,
|
||||
session: &mut GooseSession,
|
||||
session: &mut GooseAcpSession,
|
||||
) -> Result<(), acp::Error> {
|
||||
match content_item {
|
||||
MessageContent::Text(text) => {
|
||||
@@ -273,7 +272,7 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
tool_request: &goose::conversation::message::ToolRequest,
|
||||
session_id: &acp::SessionId,
|
||||
session: &mut GooseSession,
|
||||
session: &mut GooseAcpSession,
|
||||
) -> Result<(), acp::Error> {
|
||||
// Generate ACP tool call ID and track mapping
|
||||
let acp_tool_id = format!("tool_{}", uuid::Uuid::new_v4());
|
||||
@@ -341,7 +340,7 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
tool_response: &goose::conversation::message::ToolResponse,
|
||||
session_id: &acp::SessionId,
|
||||
session: &mut GooseSession,
|
||||
session: &mut GooseAcpSession,
|
||||
) -> Result<(), acp::Error> {
|
||||
// Look up the ACP tool call ID
|
||||
if let Some(acp_tool_id) = session.tool_call_ids.get(&tool_response.id) {
|
||||
@@ -496,7 +495,7 @@ impl acp::Agent for GooseAcpAgent {
|
||||
// Generate a unique session ID
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let session = GooseSession {
|
||||
let session = GooseAcpSession {
|
||||
messages: Conversation::new_unvalidated(Vec::new()),
|
||||
tool_call_ids: HashMap::new(),
|
||||
cancel_token: None,
|
||||
@@ -544,30 +543,26 @@ impl acp::Agent for GooseAcpAgent {
|
||||
// Create and store cancellation token for this prompt
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
// Convert ACP prompt to Goose message
|
||||
let user_message = self.convert_acp_prompt_to_message(args.prompt);
|
||||
|
||||
// Prepare for agent reply
|
||||
let messages = {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let session = sessions
|
||||
.get_mut(&session_id)
|
||||
.ok_or_else(acp::Error::invalid_params)?;
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
"ACP Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add message to conversation
|
||||
session.messages.push(user_message);
|
||||
|
||||
// Store cancellation token
|
||||
session.cancel_token = Some(cancel_token.clone());
|
||||
|
||||
// Clone what we need for the reply call
|
||||
session.messages.clone()
|
||||
let session_config = SessionConfig {
|
||||
id: session.id.clone(),
|
||||
schedule_id: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
// Get agent's reply through the Goose agent
|
||||
let mut stream = self
|
||||
.agent
|
||||
.reply(messages, None, Some(cancel_token.clone()))
|
||||
.reply(user_message, session_config, Some(cancel_token.clone()))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error getting agent reply: {}", e);
|
||||
|
||||
@@ -26,9 +26,7 @@ impl BenchBaseSession for CliSession {
|
||||
}
|
||||
|
||||
fn get_session_id(&self) -> anyhow::Result<String> {
|
||||
self.session_id()
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("No session ID available"))
|
||||
Ok(self.session_id().to_string())
|
||||
}
|
||||
}
|
||||
pub async fn agent_generator(
|
||||
|
||||
@@ -98,7 +98,6 @@ pub async fn handle_schedule_add(
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
execution_mode: Some("background".to_string()), // Default to background for CLI
|
||||
};
|
||||
|
||||
let scheduler_storage_path =
|
||||
|
||||
@@ -15,6 +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 serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -226,6 +227,7 @@ 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(),
|
||||
SessionType::User,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
@@ -467,21 +469,16 @@ async fn process_message_streaming(
|
||||
|
||||
let session = SessionManager::get_session(&session_id, true).await?;
|
||||
let mut messages = session.conversation.unwrap_or_default();
|
||||
messages.push(user_message);
|
||||
messages.push(user_message.clone());
|
||||
|
||||
let session_config = SessionConfig {
|
||||
id: session.id.clone(),
|
||||
working_dir: session.working_dir,
|
||||
schedule_id: None,
|
||||
execution_mode: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
match agent
|
||||
.reply(messages.clone(), Some(session_config), None)
|
||||
.await
|
||||
{
|
||||
match agent.reply(user_message, session_config, None).await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
|
||||
@@ -9,8 +9,10 @@ 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 std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -190,7 +192,6 @@ where
|
||||
)
|
||||
};
|
||||
|
||||
// Generate messages using the provider
|
||||
let messages = vec![message_generator(&*provider_arc)];
|
||||
|
||||
let mock_client = weather_client();
|
||||
@@ -218,11 +219,17 @@ where
|
||||
.update_provider(provider_arc as Arc<dyn goose::providers::base::Provider>)
|
||||
.await?;
|
||||
|
||||
let mut session = CliSession::new(agent, None, false, None, None, None, None).await;
|
||||
let session = SessionManager::create_session(
|
||||
PathBuf::default(),
|
||||
"scenario-runner".to_string(),
|
||||
SessionType::Hidden,
|
||||
)
|
||||
.await?;
|
||||
let mut cli_session = CliSession::new(agent, session.id, false, None, None, None, None).await;
|
||||
|
||||
let mut error = None;
|
||||
for message in &messages {
|
||||
if let Err(e) = session
|
||||
if let Err(e) = cli_session
|
||||
.process_message(message.clone(), CancellationToken::default())
|
||||
.await
|
||||
{
|
||||
@@ -230,7 +237,7 @@ where
|
||||
break;
|
||||
}
|
||||
}
|
||||
let updated_messages = session.message_history();
|
||||
let updated_messages = cli_session.message_history();
|
||||
|
||||
if let Some(ref err_msg) = error {
|
||||
if err_msg.contains("No recorded response found") {
|
||||
@@ -249,7 +256,7 @@ where
|
||||
|
||||
validator(&result)?;
|
||||
|
||||
drop(session);
|
||||
drop(cli_session);
|
||||
|
||||
if let Some(provider) = provider_for_saving {
|
||||
if result.error.is_none() {
|
||||
|
||||
@@ -11,6 +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 rustyline::EditMode;
|
||||
@@ -25,7 +26,7 @@ use tokio::task::JoinSet;
|
||||
/// including session identification, extension configuration, and debug settings.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SessionBuilderConfig {
|
||||
/// Optional session ID for resuming or identifying an existing session
|
||||
/// Session id, optional need to deduce from context
|
||||
pub session_id: Option<String>,
|
||||
/// Whether to resume an existing session
|
||||
pub resume: bool,
|
||||
@@ -132,8 +133,14 @@ async fn offer_extension_debugging_help(
|
||||
}
|
||||
}
|
||||
|
||||
// Create the debugging session
|
||||
let mut debug_session = CliSession::new(debug_agent, None, false, None, None, None, None).await;
|
||||
let session = SessionManager::create_session(
|
||||
std::env::current_dir()?,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
)
|
||||
.await?;
|
||||
let mut debug_session =
|
||||
CliSession::new(debug_agent, session.id, false, None, None, None, None).await;
|
||||
|
||||
// Process the debugging request
|
||||
println!("{}", style("Analyzing the extension failure...").yellow());
|
||||
@@ -278,12 +285,20 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let session_id: Option<String> = if session_config.no_session {
|
||||
None
|
||||
let session_id: String = if session_config.no_session {
|
||||
let working_dir = std::env::current_dir().expect("Could not get working directory");
|
||||
let session = SessionManager::create_session(
|
||||
working_dir,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
)
|
||||
.await
|
||||
.expect("Could not create session");
|
||||
session.id
|
||||
} else if session_config.resume {
|
||||
if let Some(session_id) = session_config.session_id {
|
||||
match SessionManager::get_session(&session_id, false).await {
|
||||
Ok(_) => Some(session_id),
|
||||
Ok(_) => session_id,
|
||||
Err(_) => {
|
||||
output::render_error(&format!(
|
||||
"Cannot resume session {} - no such session exists",
|
||||
@@ -294,7 +309,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
}
|
||||
} else {
|
||||
match SessionManager::list_sessions().await {
|
||||
Ok(sessions) if !sessions.is_empty() => Some(sessions[0].id.clone()),
|
||||
Ok(sessions) if !sessions.is_empty() => sessions[0].id.clone(),
|
||||
_ => {
|
||||
output::render_error("Cannot resume - no previous sessions found");
|
||||
process::exit(1);
|
||||
@@ -302,46 +317,44 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session_config.session_id
|
||||
session_config.session_id.unwrap()
|
||||
};
|
||||
|
||||
agent
|
||||
.extension_manager
|
||||
.set_context(PlatformExtensionContext {
|
||||
session_id: session_id.clone(),
|
||||
session_id: Some(session_id.clone()),
|
||||
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
|
||||
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
|
||||
})
|
||||
.await;
|
||||
|
||||
if session_config.resume {
|
||||
if let Some(session_id) = session_id.as_ref() {
|
||||
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 session = 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 {
|
||||
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()))
|
||||
let current_workdir =
|
||||
std::env::current_dir().expect("Failed to get current working directory");
|
||||
if current_workdir != session.working_dir {
|
||||
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(session.working_dir.display()).cyan(), style(current_workdir.display()).cyan()))
|
||||
.initial_value(true)
|
||||
.interact().expect("Failed to get user input");
|
||||
|
||||
if change_workdir {
|
||||
if !metadata.working_dir.exists() {
|
||||
output::render_error(&format!(
|
||||
"Cannot switch to original working directory - {} no longer exists",
|
||||
style(metadata.working_dir.display()).cyan()
|
||||
));
|
||||
} else if let Err(e) = std::env::set_current_dir(&metadata.working_dir) {
|
||||
output::render_error(&format!(
|
||||
"Failed to switch to original working directory: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
if change_workdir {
|
||||
if !session.working_dir.exists() {
|
||||
output::render_error(&format!(
|
||||
"Cannot switch to original working directory - {} no longer exists",
|
||||
style(session.working_dir.display()).cyan()
|
||||
));
|
||||
} else if let Err(e) = std::env::set_current_dir(&session.working_dir) {
|
||||
output::render_error(&format!(
|
||||
"Failed to switch to original working directory: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,22 +367,18 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
agent.disable_router_for_recipe().await;
|
||||
extensions.into_iter().collect()
|
||||
} else if session_config.resume {
|
||||
if let Some(session_id) = session_id.as_ref() {
|
||||
match SessionManager::get_session(session_id, false).await {
|
||||
Ok(session_data) => {
|
||||
if let Some(saved_state) =
|
||||
EnabledExtensionsState::from_extension_data(&session_data.extension_data)
|
||||
{
|
||||
check_missing_extensions_or_exit(&saved_state.extensions);
|
||||
saved_state.extensions
|
||||
} else {
|
||||
get_enabled_extensions()
|
||||
}
|
||||
match SessionManager::get_session(&session_id, false).await {
|
||||
Ok(session_data) => {
|
||||
if let Some(saved_state) =
|
||||
EnabledExtensionsState::from_extension_data(&session_data.extension_data)
|
||||
{
|
||||
check_missing_extensions_or_exit(&saved_state.extensions);
|
||||
saved_state.extensions
|
||||
} else {
|
||||
get_enabled_extensions()
|
||||
}
|
||||
_ => get_enabled_extensions(),
|
||||
}
|
||||
} else {
|
||||
get_enabled_extensions()
|
||||
_ => get_enabled_extensions(),
|
||||
}
|
||||
} else {
|
||||
get_enabled_extensions()
|
||||
@@ -560,23 +569,19 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(session_id) = session_id.as_ref() {
|
||||
let session_config_for_save = SessionConfig {
|
||||
id: session_id.clone(),
|
||||
working_dir: std::env::current_dir().unwrap_or_default(),
|
||||
schedule_id: None,
|
||||
execution_mode: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
let session_config_for_save = SessionConfig {
|
||||
id: session_id.clone(),
|
||||
schedule_id: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
if let Err(e) = session
|
||||
.agent
|
||||
.save_extension_state(&session_config_for_save)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save initial extension state: {}", e);
|
||||
}
|
||||
if let Err(e) = session
|
||||
.agent
|
||||
.save_extension_state(&session_config_for_save)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save initial extension state: {}", e);
|
||||
}
|
||||
|
||||
// Add CLI-specific system prompt extension
|
||||
@@ -603,7 +608,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
session_config.resume,
|
||||
&provider_name,
|
||||
&model_name,
|
||||
&session_id,
|
||||
&Some(session_id),
|
||||
Some(&provider_for_display),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub enum RunMode {
|
||||
pub struct CliSession {
|
||||
agent: Agent,
|
||||
messages: Conversation,
|
||||
session_id: Option<String>,
|
||||
session_id: String,
|
||||
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
||||
debug: bool,
|
||||
run_mode: RunMode,
|
||||
@@ -122,21 +122,17 @@ pub async fn classify_planner_response(
|
||||
impl CliSession {
|
||||
pub async fn new(
|
||||
agent: Agent,
|
||||
session_id: Option<String>,
|
||||
session_id: 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_id) = &session_id {
|
||||
SessionManager::get_session(session_id, true)
|
||||
.await
|
||||
.map(|session| session.conversation.unwrap_or_default())
|
||||
.unwrap()
|
||||
} else {
|
||||
Conversation::new_unvalidated(Vec::new())
|
||||
};
|
||||
let messages = SessionManager::get_session(&session_id, true)
|
||||
.await
|
||||
.map(|session| session.conversation.unwrap_or_default())
|
||||
.unwrap();
|
||||
|
||||
CliSession {
|
||||
agent,
|
||||
@@ -152,8 +148,8 @@ impl CliSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> Option<&String> {
|
||||
self.session_id.as_ref()
|
||||
pub fn session_id(&self) -> &String {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Add a stdio extension to the session
|
||||
@@ -359,9 +355,6 @@ impl CliSession {
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let cancel_token = cancel_token.clone();
|
||||
|
||||
// TODO(Douwe): Make sure we generate the description here still:
|
||||
|
||||
self.push_message(message);
|
||||
self.process_agent_response(false, cancel_token).await?;
|
||||
Ok(())
|
||||
@@ -443,7 +436,7 @@ impl CliSession {
|
||||
// Track the current directory and last instruction in projects.json
|
||||
if let Err(e) = crate::project_tracker::update_project_tracker(
|
||||
Some(&content),
|
||||
self.session_id.as_deref(),
|
||||
Some(&self.session_id),
|
||||
) {
|
||||
eprintln!("Warning: Failed to update project tracker with instruction: {}", e);
|
||||
}
|
||||
@@ -583,16 +576,14 @@ impl CliSession {
|
||||
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;
|
||||
}
|
||||
if let Err(e) = SessionManager::replace_conversation(
|
||||
&self.session_id,
|
||||
&Conversation::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
output::render_error(&format!("Failed to clear session: {}", e));
|
||||
continue;
|
||||
}
|
||||
|
||||
self.messages.clear();
|
||||
@@ -671,9 +662,10 @@ impl CliSession {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = &self.session_id {
|
||||
println!("Closing session. Session ID: {}", console::style(id).cyan());
|
||||
}
|
||||
println!(
|
||||
"Closing session. Session ID: {}",
|
||||
console::style(&self.session_id).cyan()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -768,18 +760,20 @@ impl CliSession {
|
||||
) -> Result<()> {
|
||||
let cancel_token_clone = cancel_token.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(),
|
||||
let session_config = SessionConfig {
|
||||
id: self.session_id.clone(),
|
||||
schedule_id: self.scheduled_job_id.clone(),
|
||||
execution_mode: None,
|
||||
max_turns: self.max_turns,
|
||||
retry_config: self.retry_config.clone(),
|
||||
});
|
||||
};
|
||||
let user_message = self
|
||||
.messages
|
||||
.last()
|
||||
.ok_or_else(|| anyhow::anyhow!("No user message"))?;
|
||||
let mut stream = self
|
||||
.agent
|
||||
.reply(
|
||||
self.messages.clone(),
|
||||
user_message.clone(),
|
||||
session_config.clone(),
|
||||
Some(cancel_token.clone()),
|
||||
)
|
||||
@@ -1224,16 +1218,13 @@ impl CliSession {
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn get_metadata(&self) -> Result<goose::session::Session> {
|
||||
match &self.session_id {
|
||||
Some(id) => SessionManager::get_session(id, false).await,
|
||||
None => Err(anyhow::anyhow!("No session available")),
|
||||
}
|
||||
pub async fn get_session(&self) -> Result<goose::session::Session> {
|
||||
SessionManager::get_session(&self.session_id, false).await
|
||||
}
|
||||
|
||||
// Get the session's total token usage
|
||||
pub async fn get_total_token_usage(&self) -> Result<Option<i32>> {
|
||||
let metadata = self.get_metadata().await?;
|
||||
let metadata = self.get_session().await?;
|
||||
Ok(metadata.total_tokens)
|
||||
}
|
||||
|
||||
@@ -1265,7 +1256,7 @@ impl CliSession {
|
||||
}
|
||||
}
|
||||
|
||||
match self.get_metadata().await {
|
||||
match self.get_session().await {
|
||||
Ok(metadata) => {
|
||||
let total_tokens = metadata.total_tokens.unwrap_or(0) as usize;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use goose::config::ExtensionEntry;
|
||||
use goose::conversation::Conversation;
|
||||
use goose::permission::permission_confirmation::PrincipalType;
|
||||
use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType};
|
||||
use goose::session::{Session, SessionInsights};
|
||||
use goose::session::{Session, SessionInsights, SessionType};
|
||||
use rmcp::model::{
|
||||
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
|
||||
RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ResourceContents, Role,
|
||||
@@ -444,6 +444,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
ModelInfo,
|
||||
Session,
|
||||
SessionInsights,
|
||||
SessionType,
|
||||
Conversation,
|
||||
IconSchema,
|
||||
goose::session::extension_data::ExtensionData,
|
||||
|
||||
@@ -18,6 +18,7 @@ use goose::prompt_template::render_global_file;
|
||||
use goose::providers::{create, create_with_named_model};
|
||||
use goose::recipe::Recipe;
|
||||
use goose::recipe_deeplink;
|
||||
use goose::session::session_manager::SessionType;
|
||||
use goose::session::{Session, SessionManager};
|
||||
use goose::{
|
||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||
@@ -137,15 +138,16 @@ 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)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to create session: {}", err);
|
||||
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);
|
||||
ErrorResponse {
|
||||
message: format!("Failed to create session: {}", err),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(recipe) = original_recipe {
|
||||
SessionManager::update_session(&session.id)
|
||||
|
||||
@@ -257,17 +257,30 @@ pub async fn reply(
|
||||
|
||||
let session_config = SessionConfig {
|
||||
id: session_id.clone(),
|
||||
working_dir: session.working_dir.clone(),
|
||||
schedule_id: session.schedule_id.clone(),
|
||||
execution_mode: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
let user_message = match messages.last() {
|
||||
Some(msg) => msg,
|
||||
_ => {
|
||||
let _ = stream_event(
|
||||
MessageEvent::Error {
|
||||
error: "Reply started with empty messages".to_string(),
|
||||
},
|
||||
&task_tx,
|
||||
&task_cancel,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut stream = match agent
|
||||
.reply(
|
||||
messages.clone(),
|
||||
Some(session_config.clone()),
|
||||
user_message.clone(),
|
||||
session_config,
|
||||
Some(task_cancel.clone()),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -115,7 +115,6 @@ async fn create_schedule(
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
execution_mode: req.execution_mode.or(Some("background".to_string())), // Default to background
|
||||
};
|
||||
scheduler
|
||||
.add_scheduled_job(job.clone())
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use dotenvy::dotenv;
|
||||
use futures::StreamExt;
|
||||
use goose::agents::{Agent, AgentEvent, ExtensionConfig};
|
||||
use goose::agents::{Agent, AgentEvent, ExtensionConfig, SessionConfig};
|
||||
use goose::config::{DEFAULT_EXTENSION_DESCRIPTION, DEFAULT_EXTENSION_TIMEOUT};
|
||||
use goose::conversation::message::Message;
|
||||
use goose::conversation::Conversation;
|
||||
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 std::path::PathBuf;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -32,11 +34,29 @@ async fn main() {
|
||||
println!(" {}", extension);
|
||||
}
|
||||
|
||||
let conversation = Conversation::new(vec![Message::user()
|
||||
.with_text("can you summarize the readme.md in this dir using just a haiku?")])
|
||||
.unwrap();
|
||||
let session = SessionManager::create_session(
|
||||
PathBuf::default(),
|
||||
"max-turn-test".to_string(),
|
||||
SessionType::Hidden,
|
||||
)
|
||||
.await
|
||||
.expect("session manager creation failed");
|
||||
|
||||
let session_config = SessionConfig {
|
||||
id: session.id,
|
||||
schedule_id: None,
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
let user_message = Message::user()
|
||||
.with_text("can you summarize the readme.md in this dir using just a haiku?");
|
||||
|
||||
let mut stream = agent
|
||||
.reply(user_message, session_config, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut stream = agent.reply(conversation, None, None).await.unwrap();
|
||||
while let Some(Ok(AgentEvent::Message(message))) = stream.next().await {
|
||||
println!("{}", serde_json::to_string_pretty(&message).unwrap());
|
||||
println!("\n");
|
||||
|
||||
+104
-182
@@ -61,7 +61,7 @@ use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DEC
|
||||
use crate::agents::subagent_task_config::TaskConfig;
|
||||
use crate::conversation::message::{Message, MessageContent, SystemNotificationType, ToolRequest};
|
||||
use crate::session::extension_data::{EnabledExtensionsState, ExtensionState};
|
||||
use crate::session::SessionManager;
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation...";
|
||||
@@ -75,7 +75,6 @@ pub struct ReplyContext {
|
||||
pub system_prompt: String,
|
||||
pub goose_mode: GooseMode,
|
||||
pub initial_messages: Vec<Message>,
|
||||
pub config: &'static Config,
|
||||
}
|
||||
|
||||
pub struct ToolCategorizeResult {
|
||||
@@ -219,16 +218,20 @@ impl Agent {
|
||||
self.retry_manager.get_attempts().await
|
||||
}
|
||||
|
||||
/// Handle retry logic for the agent reply loop
|
||||
async fn handle_retry_logic(
|
||||
&self,
|
||||
messages: &mut Conversation,
|
||||
session: &Option<SessionConfig>,
|
||||
session_config: &SessionConfig,
|
||||
initial_messages: &[Message],
|
||||
) -> Result<bool> {
|
||||
let result = self
|
||||
.retry_manager
|
||||
.handle_retry_logic(messages, session, initial_messages, &self.final_output_tool)
|
||||
.handle_retry_logic(
|
||||
messages,
|
||||
session_config,
|
||||
initial_messages,
|
||||
&self.final_output_tool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
@@ -242,7 +245,6 @@ impl Agent {
|
||||
async fn prepare_reply_context(
|
||||
&self,
|
||||
unfixed_conversation: Conversation,
|
||||
session: &Option<SessionConfig>,
|
||||
) -> Result<ReplyContext> {
|
||||
let unfixed_messages = unfixed_conversation.messages().clone();
|
||||
let (conversation, issues) = fix_conversation(unfixed_conversation.clone());
|
||||
@@ -260,9 +262,8 @@ impl Agent {
|
||||
let config = Config::global();
|
||||
|
||||
let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?;
|
||||
let goose_mode = Self::determine_goose_mode(session.as_ref(), config);
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
// Update permission inspector mode to match the session mode
|
||||
self.tool_inspection_manager
|
||||
.update_permission_inspector_mode(goose_mode)
|
||||
.await;
|
||||
@@ -274,7 +275,6 @@ impl Agent {
|
||||
system_prompt,
|
||||
goose_mode,
|
||||
initial_messages,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ impl Agent {
|
||||
permission_check_result: &PermissionCheckResult,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
session: &Session,
|
||||
) -> Result<Vec<(String, ToolStream)>> {
|
||||
let mut tool_futures: Vec<(String, ToolStream)> = Vec::new();
|
||||
|
||||
@@ -311,7 +311,7 @@ impl Agent {
|
||||
tool_call,
|
||||
request.id.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
session,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -392,7 +392,7 @@ impl Agent {
|
||||
tool_call: CallToolRequestParam,
|
||||
request_id: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
session: &Session,
|
||||
) -> (String, Result<ToolCallResult, ErrorData>) {
|
||||
if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME {
|
||||
let arguments = tool_call
|
||||
@@ -451,17 +451,13 @@ impl Agent {
|
||||
);
|
||||
}
|
||||
};
|
||||
let (parent_session_id, parent_working_dir) = match session.as_ref() {
|
||||
Some(s) => (Some(s.id.clone()), s.working_dir.clone()),
|
||||
None => (None, std::env::current_dir().unwrap_or_default()),
|
||||
};
|
||||
|
||||
// Get extensions from the agent's runtime state rather than global config
|
||||
// This ensures subagents inherit extensions that were dynamically enabled by the parent
|
||||
let extensions = self.get_extension_configs().await;
|
||||
|
||||
let task_config =
|
||||
TaskConfig::new(provider, parent_session_id, parent_working_dir, extensions);
|
||||
TaskConfig::new(provider, &session.id, &session.working_dir, extensions);
|
||||
|
||||
let arguments = match tool_call.arguments.clone() {
|
||||
Some(args) => Value::Object(args),
|
||||
@@ -731,117 +727,110 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, unfixed_conversation, session), fields(user_message))]
|
||||
#[instrument(skip(self, user_message, session_config), fields(user_message))]
|
||||
pub async fn reply(
|
||||
&self,
|
||||
unfixed_conversation: Conversation,
|
||||
session: Option<SessionConfig>,
|
||||
user_message: Message,
|
||||
session_config: SessionConfig,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
|
||||
let is_manual_compact = unfixed_conversation.messages().last().is_some_and(|msg| {
|
||||
msg.content.iter().any(|c| {
|
||||
if let MessageContent::Text(text) = c {
|
||||
text.text.trim() == MANUAL_COMPACT_TRIGGER
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
let is_manual_compact = user_message.content.iter().any(|c| {
|
||||
if let MessageContent::Text(text) = c {
|
||||
text.text.trim() == MANUAL_COMPACT_TRIGGER
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !is_manual_compact {
|
||||
let session_metadata = if let Some(session_config) = &session {
|
||||
SessionManager::get_session(&session_config.id, false)
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SessionManager::add_message(&session_config.id, &user_message).await?;
|
||||
let session = SessionManager::get_session(&session_config.id, true).await?;
|
||||
|
||||
let needs_auto_compact = crate::context_mgmt::check_if_compaction_needed(
|
||||
self,
|
||||
&unfixed_conversation,
|
||||
None,
|
||||
session_metadata.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let conversation = session
|
||||
.conversation
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Session {} has no conversation", session_config.id))?;
|
||||
|
||||
if !needs_auto_compact {
|
||||
return self
|
||||
.reply_internal(unfixed_conversation, session, cancel_token)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
let needs_auto_compact =
|
||||
crate::context_mgmt::check_if_compaction_needed(self, &conversation, None, &session)
|
||||
.await?;
|
||||
|
||||
let conversation_to_compact = unfixed_conversation.clone();
|
||||
let conversation_to_compact = conversation.clone();
|
||||
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
if !is_manual_compact {
|
||||
let config = crate::config::Config::global();
|
||||
let threshold = config
|
||||
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
|
||||
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
|
||||
let threshold_percentage = (threshold * 100.0) as u32;
|
||||
let final_conversation = if !needs_auto_compact {
|
||||
conversation
|
||||
} else {
|
||||
if !is_manual_compact {
|
||||
let config = crate::config::Config::global();
|
||||
let threshold = config
|
||||
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
|
||||
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
|
||||
let threshold_percentage = (threshold * 100.0) as u32;
|
||||
|
||||
let inline_msg = format!(
|
||||
"Exceeded auto-compact threshold of {}%. Performing auto-compaction...",
|
||||
threshold_percentage
|
||||
);
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
inline_msg,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::ThinkingMessage,
|
||||
COMPACTION_THINKING_TEXT,
|
||||
)
|
||||
);
|
||||
|
||||
match crate::context_mgmt::compact_messages(self, &conversation_to_compact, false).await {
|
||||
Ok((compacted_conversation, summarization_usage)) => {
|
||||
if let Some(session_to_store) = &session {
|
||||
SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?;
|
||||
Self::update_session_metrics(session_to_store, &summarization_usage, true).await?;
|
||||
}
|
||||
|
||||
yield AgentEvent::HistoryReplaced(compacted_conversation.clone());
|
||||
let inline_msg = format!(
|
||||
"Exceeded auto-compact threshold of {}%. Performing auto-compaction...",
|
||||
threshold_percentage
|
||||
);
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
"Compaction complete",
|
||||
inline_msg,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if !is_manual_compact {
|
||||
let mut reply_stream = self.reply_internal(compacted_conversation, session, cancel_token).await?;
|
||||
while let Some(event) = reply_stream.next().await {
|
||||
yield event?;
|
||||
}
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::ThinkingMessage,
|
||||
COMPACTION_THINKING_TEXT,
|
||||
)
|
||||
);
|
||||
|
||||
match crate::context_mgmt::compact_messages(self, &conversation_to_compact, false).await {
|
||||
Ok((compacted_conversation, summarization_usage)) => {
|
||||
SessionManager::replace_conversation(&session_config.id, &compacted_conversation).await?;
|
||||
Self::update_session_metrics(&session_config, &summarization_usage, true).await?;
|
||||
|
||||
yield AgentEvent::HistoryReplaced(compacted_conversation.clone());
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
"Compaction complete",
|
||||
)
|
||||
);
|
||||
|
||||
compacted_conversation
|
||||
}
|
||||
Err(e) => {
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_text(
|
||||
format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session")
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
yield AgentEvent::Message(Message::assistant().with_text(
|
||||
format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session")
|
||||
));
|
||||
};
|
||||
|
||||
if !is_manual_compact {
|
||||
let mut reply_stream = self.reply_internal(final_conversation, session_config, session, cancel_token).await?;
|
||||
while let Some(event) = reply_stream.next().await {
|
||||
yield event?;
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Main reply method that handles the actual agent processing
|
||||
async fn reply_internal(
|
||||
&self,
|
||||
conversation: Conversation,
|
||||
session: Option<SessionConfig>,
|
||||
session_config: SessionConfig,
|
||||
session: Session,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
|
||||
let context = self.prepare_reply_context(conversation, &session).await?;
|
||||
let context = self.prepare_reply_context(conversation).await?;
|
||||
let ReplyContext {
|
||||
mut conversation,
|
||||
mut tools,
|
||||
@@ -849,66 +838,22 @@ impl Agent {
|
||||
mut system_prompt,
|
||||
goose_mode,
|
||||
initial_messages,
|
||||
config,
|
||||
} = context;
|
||||
let reply_span = tracing::Span::current();
|
||||
self.reset_retry_attempts().await;
|
||||
|
||||
// This will need further refactoring. In the ideal world we pass the new message into
|
||||
// reply and load the existing conversation. Until we get to that point, fetch the conversation
|
||||
// so far and append the last (user) message that the caller already added.
|
||||
if let Some(session_config) = &session {
|
||||
let stored_conversation = SessionManager::get_session(&session_config.id, true)
|
||||
.await?
|
||||
.conversation
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Session {} has no conversation", session_config.id)
|
||||
})?;
|
||||
|
||||
match conversation.len().cmp(&stored_conversation.len()) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
if conversation != stored_conversation {
|
||||
warn!("Session messages mismatch - replacing with incoming");
|
||||
SessionManager::replace_conversation(&session_config.id, &conversation)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Greater
|
||||
if conversation.len() == stored_conversation.len() + 1 =>
|
||||
{
|
||||
let last_message = conversation.last().unwrap();
|
||||
if let Some(content) = last_message.content.first().and_then(|c| c.as_text()) {
|
||||
debug!("user_message" = &content);
|
||||
}
|
||||
SessionManager::add_message(&session_config.id, last_message).await?;
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Unexpected session state: stored={}, incoming={}. Replacing.",
|
||||
stored_conversation.len(),
|
||||
conversation.len()
|
||||
);
|
||||
SessionManager::replace_conversation(&session_config.id, &conversation).await?;
|
||||
}
|
||||
let provider = self.provider().await?;
|
||||
let session_id = session_config.id.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await {
|
||||
warn!("Failed to generate session description: {}", e);
|
||||
}
|
||||
let provider = self.provider().await?;
|
||||
let session_id = session_config.id.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await {
|
||||
warn!("Failed to generate session description: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
let _ = reply_span.enter();
|
||||
let mut turns_taken = 0u32;
|
||||
let max_turns = session
|
||||
.as_ref()
|
||||
.and_then(|s| s.max_turns)
|
||||
.unwrap_or_else(|| {
|
||||
config.get_param("GOOSE_MAX_TURNS").unwrap_or(DEFAULT_MAX_TURNS)
|
||||
});
|
||||
let max_turns = session_config.max_turns.unwrap_or(DEFAULT_MAX_TURNS);
|
||||
|
||||
loop {
|
||||
if is_token_cancelled(&cancel_token) {
|
||||
@@ -989,11 +934,8 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Record usage for the session
|
||||
if let Some(ref session_config) = &session {
|
||||
if let Some(ref usage) = usage {
|
||||
Self::update_session_metrics(session_config, usage, false).await?;
|
||||
}
|
||||
if let Some(ref usage) = usage {
|
||||
Self::update_session_metrics(&session_config, usage, false).await?;
|
||||
}
|
||||
|
||||
if let Some(response) = response {
|
||||
@@ -1078,18 +1020,17 @@ impl Agent {
|
||||
&permission_check_result,
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
&session,
|
||||
).await?;
|
||||
|
||||
let tool_futures_arc = Arc::new(Mutex::new(tool_futures));
|
||||
|
||||
// Process tools requiring approval
|
||||
let mut tool_approval_stream = self.handle_approval_tool_requests(
|
||||
&permission_check_result.needs_approval,
|
||||
tool_futures_arc.clone(),
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
&session,
|
||||
&inspection_results,
|
||||
);
|
||||
|
||||
@@ -1136,10 +1077,8 @@ impl Agent {
|
||||
}
|
||||
|
||||
if all_install_successful && !enable_extension_request_ids.is_empty() {
|
||||
if let Some(ref session_config) = session {
|
||||
if let Err(e) = self.save_extension_state(session_config).await {
|
||||
warn!("Failed to save extension state after runtime changes: {}", e);
|
||||
}
|
||||
if let Err(e) = self.save_extension_state(&session_config).await {
|
||||
warn!("Failed to save extension state after runtime changes: {}", e);
|
||||
}
|
||||
tools_updated = true;
|
||||
}
|
||||
@@ -1168,14 +1107,10 @@ impl Agent {
|
||||
|
||||
match crate::context_mgmt::compact_messages(self, &conversation, true).await {
|
||||
Ok((compacted_conversation, usage)) => {
|
||||
if let Some(session_to_store) = &session {
|
||||
SessionManager::replace_conversation(&session_to_store.id, &compacted_conversation).await?;
|
||||
Self::update_session_metrics(session_to_store, &usage, true).await?;
|
||||
}
|
||||
|
||||
SessionManager::replace_conversation(&session_config.id, &compacted_conversation).await?;
|
||||
Self::update_session_metrics(&session_config, &usage, true).await?;
|
||||
conversation = compacted_conversation;
|
||||
did_recovery_compact_this_iteration = true;
|
||||
|
||||
yield AgentEvent::HistoryReplaced(conversation.clone());
|
||||
continue;
|
||||
}
|
||||
@@ -1221,7 +1156,7 @@ impl Agent {
|
||||
} else if did_recovery_compact_this_iteration {
|
||||
// Avoid setting exit_chat; continue from last user message in the conversation
|
||||
} else {
|
||||
match self.handle_retry_logic(&mut conversation, &session, &initial_messages).await {
|
||||
match self.handle_retry_logic(&mut conversation, &session_config, &initial_messages).await {
|
||||
Ok(should_retry) => {
|
||||
if should_retry {
|
||||
info!("Retry logic triggered, restarting agent loop");
|
||||
@@ -1242,10 +1177,8 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(session_config) = &session {
|
||||
for msg in &messages_to_add {
|
||||
SessionManager::add_message(&session_config.id, msg).await?;
|
||||
}
|
||||
for msg in &messages_to_add {
|
||||
SessionManager::add_message(&session_config.id, msg).await?;
|
||||
}
|
||||
conversation.extend(messages_to_add);
|
||||
if exit_chat {
|
||||
@@ -1257,17 +1190,6 @@ impl Agent {
|
||||
}))
|
||||
}
|
||||
|
||||
fn determine_goose_mode(session: Option<&SessionConfig>, config: &Config) -> GooseMode {
|
||||
let mode = session.and_then(|s| s.execution_mode.as_deref());
|
||||
|
||||
match mode {
|
||||
Some("foreground") => GooseMode::Chat,
|
||||
Some("background") => GooseMode::Auto,
|
||||
_ => config.get_goose_mode().unwrap_or(GooseMode::Auto),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the system prompt with one line of additional instruction
|
||||
pub async fn extend_system_prompt(&self, instruction: String) {
|
||||
let mut prompt_manager = self.prompt_manager.lock().await;
|
||||
prompt_manager.add_system_prompt_extra(instruction);
|
||||
|
||||
@@ -33,7 +33,6 @@ pub fn manage_schedule_tool() -> Tool {
|
||||
"job_id": {"type": "string", "description": "Job identifier for operations on existing jobs"},
|
||||
"recipe_path": {"type": "string", "description": "Path to recipe file for create action"},
|
||||
"cron_expression": {"type": "string", "description": "A cron expression for create action. Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats. 5-field expressions are automatically converted to 6-field by prepending '0' for seconds."},
|
||||
"execution_mode": {"type": "string", "description": "Execution mode for create action: 'foreground' or 'background'", "enum": ["foreground", "background"], "default": "background"},
|
||||
"limit": {"type": "integer", "description": "Limit for sessions list", "default": 50},
|
||||
"session_id": {"type": "string", "description": "Session identifier for session_content action"}
|
||||
}
|
||||
|
||||
@@ -108,18 +108,13 @@ impl RetryManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle retry logic for the agent reply loop
|
||||
pub async fn handle_retry_logic(
|
||||
&self,
|
||||
messages: &mut Conversation,
|
||||
session: &Option<SessionConfig>,
|
||||
session_config: &SessionConfig,
|
||||
initial_messages: &[Message],
|
||||
final_output_tool: &Arc<Mutex<Option<crate::agents::final_output_tool::FinalOutputTool>>>,
|
||||
) -> Result<RetryResult> {
|
||||
let Some(session_config) = session else {
|
||||
return Ok(RetryResult::Skipped);
|
||||
};
|
||||
|
||||
let Some(retry_config) = &session_config.retry_config else {
|
||||
return Ok(RetryResult::Skipped);
|
||||
};
|
||||
|
||||
@@ -186,7 +186,6 @@ impl Agent {
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
execution_mode: Some(execution_mode.to_string()),
|
||||
};
|
||||
|
||||
match scheduler.add_scheduled_job(job).await {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::session::session_manager::SessionType;
|
||||
use crate::{
|
||||
agents::{
|
||||
extension::PlatformExtensionContext, subagent_task_config::TaskConfig, Agent, AgentEvent,
|
||||
SessionConfig,
|
||||
},
|
||||
agents::{subagent_task_config::TaskConfig, AgentEvent, SessionConfig},
|
||||
conversation::{message::Message, Conversation},
|
||||
execution::manager::AgentManager,
|
||||
session::SessionManager,
|
||||
@@ -10,8 +8,8 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use rmcp::model::{ErrorCode, ErrorData};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::{future::Future, sync::Arc};
|
||||
use tracing::debug;
|
||||
|
||||
/// Standalone function to run a complete subagent task with output options
|
||||
@@ -104,34 +102,18 @@ fn get_agent_messages(
|
||||
.map_err(|e| anyhow!("Failed to create AgentManager: {}", e))?;
|
||||
let parent_session_id = task_config.parent_session_id;
|
||||
let working_dir = task_config.parent_working_dir;
|
||||
let (agent, session_id) = match parent_session_id {
|
||||
Some(parent_session_id) => {
|
||||
let session = SessionManager::create_session(
|
||||
working_dir.clone(),
|
||||
format!("Subagent task for: {}", parent_session_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create a session for sub agent: {}", e))?;
|
||||
let session = SessionManager::create_session(
|
||||
working_dir.clone(),
|
||||
format!("Subagent task for: {}", parent_session_id),
|
||||
SessionType::SubAgent,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create a session for sub agent: {}", e))?;
|
||||
|
||||
let agent = agent_manager
|
||||
.get_or_create_agent(session.id.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get sub agent session file path: {}", e))?;
|
||||
(agent, Some(session.id))
|
||||
}
|
||||
None => {
|
||||
let agent = Arc::new(Agent::new());
|
||||
agent
|
||||
.extension_manager
|
||||
.set_context(PlatformExtensionContext {
|
||||
session_id: None,
|
||||
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
|
||||
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
|
||||
})
|
||||
.await;
|
||||
(agent, None)
|
||||
}
|
||||
};
|
||||
let agent = agent_manager
|
||||
.get_or_create_agent(session.id.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get sub agent session file path: {}", e))?;
|
||||
|
||||
agent
|
||||
.update_provider(task_config.provider)
|
||||
@@ -148,28 +130,18 @@ fn get_agent_messages(
|
||||
}
|
||||
}
|
||||
|
||||
let mut conversation =
|
||||
Conversation::new_unvalidated(
|
||||
vec![Message::user().with_text(text_instruction.clone())],
|
||||
);
|
||||
let session_config = if let Some(session_id) = session_id {
|
||||
Some(SessionConfig {
|
||||
id: session_id,
|
||||
working_dir,
|
||||
schedule_id: None,
|
||||
execution_mode: None,
|
||||
max_turns: task_config.max_turns.map(|v| v as u32),
|
||||
retry_config: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
let user_message = Message::user().with_text(text_instruction);
|
||||
let mut conversation = Conversation::new_unvalidated(vec![user_message.clone()]);
|
||||
|
||||
let session_config = SessionConfig {
|
||||
id: session.id.clone(),
|
||||
schedule_id: None,
|
||||
max_turns: task_config.max_turns.map(|v| v as u32),
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
let session_id = session_config.as_ref().map(|s| s.id.clone());
|
||||
let mut stream = crate::session_context::with_session_id(session_id, async {
|
||||
agent
|
||||
.reply(conversation.clone(), session_config, None)
|
||||
.await
|
||||
let mut stream = crate::session_context::with_session_id(Some(session.id.clone()), async {
|
||||
agent.reply(user_message, session_config, None).await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::agents::ExtensionConfig;
|
||||
use crate::providers::base::Provider;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Default maximum number of turns for task execution
|
||||
@@ -15,7 +15,7 @@ pub const GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR: &str = "GOOSE_SUBAGENT_MAX_TURNS";
|
||||
#[derive(Clone)]
|
||||
pub struct TaskConfig {
|
||||
pub provider: Arc<dyn Provider>,
|
||||
pub parent_session_id: Option<String>,
|
||||
pub parent_session_id: String,
|
||||
pub parent_working_dir: PathBuf,
|
||||
pub extensions: Vec<ExtensionConfig>,
|
||||
pub max_turns: Option<usize>,
|
||||
@@ -34,17 +34,16 @@ impl fmt::Debug for TaskConfig {
|
||||
}
|
||||
|
||||
impl TaskConfig {
|
||||
/// Create a new TaskConfig with all required dependencies
|
||||
pub fn new(
|
||||
provider: Arc<dyn Provider>,
|
||||
parent_session_id: Option<String>,
|
||||
parent_working_dir: PathBuf,
|
||||
parent_session_id: &str,
|
||||
parent_working_dir: &Path,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
parent_session_id,
|
||||
parent_working_dir,
|
||||
parent_session_id: parent_session_id.to_owned(),
|
||||
parent_working_dir: parent_working_dir.to_owned(),
|
||||
extensions,
|
||||
max_turns: Some(
|
||||
env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR)
|
||||
|
||||
@@ -29,8 +29,9 @@ impl From<ToolResult<Vec<Content>>> for ToolCallResult {
|
||||
}
|
||||
|
||||
use super::agent::{tool_stream, ToolStream};
|
||||
use crate::agents::{Agent, SessionConfig};
|
||||
use crate::agents::Agent;
|
||||
use crate::conversation::message::{Message, ToolRequest};
|
||||
use crate::session::Session;
|
||||
use crate::tool_inspection::get_security_finding_id_from_results;
|
||||
|
||||
pub const DECLINED_RESPONSE: &str = "The user has declined to run this tool. \
|
||||
@@ -53,7 +54,7 @@ impl Agent {
|
||||
tool_futures: Arc<Mutex<Vec<(String, ToolStream)>>>,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
session: &'a Session,
|
||||
inspection_results: &'a [crate::tool_inspection::InspectionResult],
|
||||
) -> BoxStream<'a, anyhow::Result<Message>> {
|
||||
try_stream! {
|
||||
@@ -93,7 +94,7 @@ impl Agent {
|
||||
}
|
||||
|
||||
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session.clone()).await;
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session).await;
|
||||
let mut futures = tool_futures.lock().await;
|
||||
|
||||
futures.push((req_id, match tool_result {
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::mcp_utils::ToolResult;
|
||||
use crate::providers::base::Provider;
|
||||
use rmcp::model::{Content, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use utoipa::ToSchema;
|
||||
@@ -84,14 +83,10 @@ pub struct FrontendTool {
|
||||
/// Session configuration for an agent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionConfig {
|
||||
/// Unique identifier for the session
|
||||
/// Identifier of the underlying Session
|
||||
pub id: String,
|
||||
/// Working directory for the session
|
||||
pub working_dir: PathBuf,
|
||||
/// ID of the schedule that triggered this session, if any
|
||||
pub schedule_id: Option<String>,
|
||||
/// Execution mode for scheduled jobs: "foreground" or "background"
|
||||
pub execution_mode: Option<String>,
|
||||
/// Maximum number of turns (iterations) allowed without user input
|
||||
pub max_turns: Option<u32>,
|
||||
/// Retry configuration for automated validation and recovery
|
||||
|
||||
@@ -145,11 +145,10 @@ pub async fn check_if_compaction_needed(
|
||||
agent: &Agent,
|
||||
conversation: &Conversation,
|
||||
threshold_override: Option<f64>,
|
||||
session_metadata: Option<&crate::session::Session>,
|
||||
session: &crate::session::Session,
|
||||
) -> Result<bool> {
|
||||
let messages = conversation.messages();
|
||||
let config = Config::global();
|
||||
// TODO(Douwe): check the default here; it seems to reset to 0.3 sometimes
|
||||
let threshold = threshold_override.unwrap_or_else(|| {
|
||||
config
|
||||
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
|
||||
@@ -159,7 +158,7 @@ pub async fn check_if_compaction_needed(
|
||||
let provider = agent.provider().await?;
|
||||
let context_limit = provider.get_model_config().context_limit();
|
||||
|
||||
let (current_tokens, token_source) = match session_metadata.and_then(|m| m.total_tokens) {
|
||||
let (current_tokens, token_source) = match session.total_tokens {
|
||||
Some(tokens) => (tokens as usize, "session metadata"),
|
||||
None => {
|
||||
let token_counter = create_token_counter()
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::providers::base::Provider as GooseProvider; // Alias to avoid conflic
|
||||
use crate::providers::create;
|
||||
use crate::recipe::Recipe;
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
use crate::session::session_manager::SessionType;
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
// Track running tasks with their abort handles
|
||||
@@ -152,8 +153,6 @@ pub struct ScheduledJob {
|
||||
pub current_session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub process_start_time: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub execution_mode: Option<String>, // "foreground" or "background"
|
||||
}
|
||||
|
||||
async fn persist_jobs_from_arc(
|
||||
@@ -1160,8 +1159,6 @@ async fn run_scheduled_job_internal(
|
||||
});
|
||||
}
|
||||
tracing::info!("Agent configured with provider for job '{}'", job.id);
|
||||
let execution_mode = job.execution_mode.as_deref().unwrap_or("background");
|
||||
tracing::info!("Job '{}' running in {} mode", job.id, execution_mode);
|
||||
|
||||
let current_dir = match std::env::current_dir() {
|
||||
Ok(cd) => cd,
|
||||
@@ -1173,10 +1170,10 @@ async fn run_scheduled_job_internal(
|
||||
}
|
||||
};
|
||||
|
||||
// Create session upfront
|
||||
let session = match SessionManager::create_session(
|
||||
current_dir.clone(),
|
||||
format!("Scheduled job: {}", job.id),
|
||||
SessionType::Scheduled,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1204,23 +1201,19 @@ async fn run_scheduled_job_internal(
|
||||
.or(recipe.instructions.as_ref())
|
||||
.unwrap();
|
||||
|
||||
let mut conversation =
|
||||
Conversation::new_unvalidated(vec![Message::user().with_text(prompt_text.clone())]);
|
||||
let user_message = Message::user().with_text(prompt_text);
|
||||
let mut conversation = Conversation::new_unvalidated(vec![user_message.clone()]);
|
||||
|
||||
let session_config = SessionConfig {
|
||||
id: session.id.clone(),
|
||||
working_dir: current_dir.clone(),
|
||||
schedule_id: Some(job.id.clone()),
|
||||
execution_mode: job.execution_mode.clone(),
|
||||
max_turns: None,
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
let session_id = Some(session_config.id.clone());
|
||||
match crate::session_context::with_session_id(session_id, async {
|
||||
agent
|
||||
.reply(conversation.clone(), Some(session_config.clone()), None)
|
||||
.await
|
||||
agent.reply(user_message, session_config, None).await
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -1455,7 +1448,6 @@ mod tests {
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
execution_mode: Some("background".to_string()), // Default for test
|
||||
};
|
||||
|
||||
let mock_model_config = ModelConfig::new_or_fail("test_model");
|
||||
|
||||
@@ -6,4 +6,4 @@ pub mod session_manager;
|
||||
|
||||
pub use diagnostics::generate_diagnostics;
|
||||
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
|
||||
pub use session_manager::{Session, SessionInsights, SessionManager};
|
||||
pub use session_manager::{Session, SessionInsights, SessionManager, SessionType};
|
||||
|
||||
@@ -18,7 +18,47 @@ use tokio::sync::OnceCell;
|
||||
use tracing::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const CURRENT_SCHEMA_VERSION: i32 = 4;
|
||||
const CURRENT_SCHEMA_VERSION: i32 = 5;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SessionType {
|
||||
User,
|
||||
Scheduled,
|
||||
SubAgent,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Default for SessionType {
|
||||
fn default() -> Self {
|
||||
Self::User
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SessionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SessionType::User => write!(f, "user"),
|
||||
SessionType::SubAgent => write!(f, "sub_agent"),
|
||||
SessionType::Hidden => write!(f, "hidden"),
|
||||
SessionType::Scheduled => write!(f, "scheduled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SessionType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"user" => Ok(SessionType::User),
|
||||
"sub_agent" => Ok(SessionType::SubAgent),
|
||||
"hidden" => Ok(SessionType::Hidden),
|
||||
"scheduled" => Ok(SessionType::Scheduled),
|
||||
_ => Err(anyhow::anyhow!("Invalid session type: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SESSION_STORAGE: OnceCell<Arc<SessionStorage>> = OnceCell::const_new();
|
||||
|
||||
@@ -27,11 +67,12 @@ pub struct Session {
|
||||
pub id: String,
|
||||
#[schema(value_type = String)]
|
||||
pub working_dir: PathBuf,
|
||||
// Allow importing session exports from before 'description' was renamed to 'name'
|
||||
#[serde(alias = "description")]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub user_set_name: bool,
|
||||
#[serde(default)]
|
||||
pub session_type: SessionType,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub extension_data: ExtensionData,
|
||||
@@ -52,6 +93,7 @@ pub struct SessionUpdateBuilder {
|
||||
session_id: String,
|
||||
name: Option<String>,
|
||||
user_set_name: Option<bool>,
|
||||
session_type: Option<SessionType>,
|
||||
working_dir: Option<PathBuf>,
|
||||
extension_data: Option<ExtensionData>,
|
||||
total_tokens: Option<Option<i32>>,
|
||||
@@ -78,6 +120,7 @@ impl SessionUpdateBuilder {
|
||||
session_id,
|
||||
name: None,
|
||||
user_set_name: None,
|
||||
session_type: None,
|
||||
working_dir: None,
|
||||
extension_data: None,
|
||||
total_tokens: None,
|
||||
@@ -110,6 +153,11 @@ impl SessionUpdateBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn session_type(mut self, session_type: SessionType) -> Self {
|
||||
self.session_type = Some(session_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn working_dir(mut self, working_dir: PathBuf) -> Self {
|
||||
self.working_dir = Some(working_dir);
|
||||
self
|
||||
@@ -183,10 +231,14 @@ impl SessionManager {
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
pub async fn create_session(working_dir: PathBuf, name: String) -> Result<Session> {
|
||||
pub async fn create_session(
|
||||
working_dir: PathBuf,
|
||||
name: String,
|
||||
session_type: SessionType,
|
||||
) -> Result<Session> {
|
||||
Self::instance()
|
||||
.await?
|
||||
.create_session(working_dir, name)
|
||||
.create_session(working_dir, name, session_type)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -306,6 +358,7 @@ impl Default for Session {
|
||||
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
name: String::new(),
|
||||
user_set_name: false,
|
||||
session_type: SessionType::default(),
|
||||
created_at: Default::default(),
|
||||
updated_at: Default::default(),
|
||||
extension_data: ExtensionData::default(),
|
||||
@@ -353,11 +406,17 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
|
||||
|
||||
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();
|
||||
|
||||
Ok(Session {
|
||||
id: row.try_get("id")?,
|
||||
working_dir: PathBuf::from(row.try_get::<String, _>("working_dir")?),
|
||||
name,
|
||||
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")?)
|
||||
@@ -446,6 +505,7 @@ impl SessionStorage {
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
user_set_name BOOLEAN DEFAULT FALSE,
|
||||
session_type TEXT NOT NULL DEFAULT 'user',
|
||||
working_dir TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -491,6 +551,9 @@ impl SessionStorage {
|
||||
sqlx::query("CREATE INDEX idx_sessions_updated ON sessions(updated_at DESC)")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
sqlx::query("CREATE INDEX idx_sessions_type ON sessions(session_type)")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
@@ -553,31 +616,32 @@ impl SessionStorage {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO sessions (
|
||||
id, name, user_set_name, working_dir, created_at, updated_at, extension_data,
|
||||
id, name, user_set_name, session_type, working_dir, created_at, updated_at, extension_data,
|
||||
total_tokens, input_tokens, output_tokens,
|
||||
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
|
||||
schedule_id, recipe_json, user_recipe_values_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&session.id)
|
||||
.bind(&session.name)
|
||||
.bind(session.user_set_name)
|
||||
.bind(session.working_dir.to_string_lossy().as_ref())
|
||||
.bind(session.created_at)
|
||||
.bind(session.updated_at)
|
||||
.bind(serde_json::to_string(&session.extension_data)?)
|
||||
.bind(session.total_tokens)
|
||||
.bind(session.input_tokens)
|
||||
.bind(session.output_tokens)
|
||||
.bind(session.accumulated_total_tokens)
|
||||
.bind(session.accumulated_input_tokens)
|
||||
.bind(session.accumulated_output_tokens)
|
||||
.bind(&session.schedule_id)
|
||||
.bind(recipe_json)
|
||||
.bind(user_recipe_values_json)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
.bind(&session.id)
|
||||
.bind(&session.name)
|
||||
.bind(session.user_set_name)
|
||||
.bind(session.session_type.to_string())
|
||||
.bind(session.working_dir.to_string_lossy().as_ref())
|
||||
.bind(session.created_at)
|
||||
.bind(session.updated_at)
|
||||
.bind(serde_json::to_string(&session.extension_data)?)
|
||||
.bind(session.total_tokens)
|
||||
.bind(session.input_tokens)
|
||||
.bind(session.output_tokens)
|
||||
.bind(session.accumulated_total_tokens)
|
||||
.bind(session.accumulated_input_tokens)
|
||||
.bind(session.accumulated_output_tokens)
|
||||
.bind(&session.schedule_id)
|
||||
.bind(recipe_json)
|
||||
.bind(user_recipe_values_json)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
if let Some(conversation) = &session.conversation {
|
||||
self.replace_conversation(&session.id, conversation).await?;
|
||||
@@ -687,6 +751,19 @@ impl SessionStorage {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
}
|
||||
5 => {
|
||||
sqlx::query(
|
||||
r#"
|
||||
ALTER TABLE sessions ADD COLUMN session_type TEXT NOT NULL DEFAULT 'user'
|
||||
"#,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query("CREATE INDEX idx_sessions_type ON sessions(session_type)")
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!("Unknown migration version: {}", version);
|
||||
}
|
||||
@@ -695,11 +772,16 @@ impl SessionStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_session(&self, working_dir: PathBuf, name: String) -> Result<Session> {
|
||||
async fn create_session(
|
||||
&self,
|
||||
working_dir: PathBuf,
|
||||
name: String,
|
||||
session_type: SessionType,
|
||||
) -> Result<Session> {
|
||||
let today = chrono::Utc::now().format("%Y%m%d").to_string();
|
||||
Ok(sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO sessions (id, name, user_set_name, working_dir, extension_data)
|
||||
INSERT INTO sessions (id, name, user_set_name, session_type, working_dir, extension_data)
|
||||
VALUES (
|
||||
? || '_' || CAST(COALESCE((
|
||||
SELECT MAX(CAST(SUBSTR(id, 10) AS INTEGER))
|
||||
@@ -709,23 +791,25 @@ impl SessionStorage {
|
||||
?,
|
||||
FALSE,
|
||||
?,
|
||||
?,
|
||||
'{}'
|
||||
)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(&today)
|
||||
.bind(&today)
|
||||
.bind(&name)
|
||||
.bind(working_dir.to_string_lossy().as_ref())
|
||||
.fetch_one(&self.pool)
|
||||
.await?)
|
||||
.bind(&today)
|
||||
.bind(&today)
|
||||
.bind(&name)
|
||||
.bind(session_type.to_string())
|
||||
.bind(working_dir.to_string_lossy().as_ref())
|
||||
.fetch_one(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn get_session(&self, id: &str, include_messages: bool) -> Result<Session> {
|
||||
let mut session = sqlx::query_as::<_, Session>(
|
||||
r#"
|
||||
SELECT id, working_dir, name, description, user_set_name, created_at, updated_at, extension_data,
|
||||
SELECT id, working_dir, name, description, user_set_name, session_type, created_at, updated_at, extension_data,
|
||||
total_tokens, input_tokens, output_tokens,
|
||||
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
|
||||
schedule_id, recipe_json, user_recipe_values_json
|
||||
@@ -733,10 +817,10 @@ impl SessionStorage {
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
|
||||
|
||||
if include_messages {
|
||||
let conv = self.get_conversation(&session.id).await?;
|
||||
@@ -773,6 +857,7 @@ impl SessionStorage {
|
||||
|
||||
add_update!(builder.name, "name");
|
||||
add_update!(builder.user_set_name, "user_set_name");
|
||||
add_update!(builder.session_type, "session_type");
|
||||
add_update!(builder.working_dir, "working_dir");
|
||||
add_update!(builder.extension_data, "extension_data");
|
||||
add_update!(builder.total_tokens, "total_tokens");
|
||||
@@ -803,6 +888,9 @@ impl SessionStorage {
|
||||
if let Some(user_set_name) = builder.user_set_name {
|
||||
q = q.bind(user_set_name);
|
||||
}
|
||||
if let Some(session_type) = builder.session_type {
|
||||
q = q.bind(session_type.to_string());
|
||||
}
|
||||
if let Some(wd) = builder.working_dir {
|
||||
q = q.bind(wd.to_string_lossy().to_string());
|
||||
}
|
||||
@@ -872,7 +960,6 @@ impl SessionStorage {
|
||||
|
||||
let mut message = Message::new(role, created_timestamp, content);
|
||||
message.metadata = metadata;
|
||||
// TODO(Douwe): make id required
|
||||
message = message.with_id(format!("msg_{}_{}", session_id, idx));
|
||||
messages.push(message);
|
||||
}
|
||||
@@ -942,20 +1029,21 @@ impl SessionStorage {
|
||||
async fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||
sqlx::query_as::<_, Session>(
|
||||
r#"
|
||||
SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.created_at, s.updated_at, s.extension_data,
|
||||
SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.session_type, s.created_at, s.updated_at, s.extension_data,
|
||||
s.total_tokens, s.input_tokens, s.output_tokens,
|
||||
s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens,
|
||||
s.schedule_id, s.recipe_json, s.user_recipe_values_json,
|
||||
COUNT(m.id) as message_count
|
||||
FROM sessions s
|
||||
INNER JOIN messages m ON s.id = m.session_id
|
||||
WHERE s.session_type = 'user' OR s.session_type = 'scheduled'
|
||||
GROUP BY s.id
|
||||
ORDER BY s.updated_at DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn delete_session(&self, session_id: &str) -> Result<()> {
|
||||
@@ -1008,7 +1096,11 @@ impl SessionStorage {
|
||||
let import: Session = serde_json::from_str(json)?;
|
||||
|
||||
let session = self
|
||||
.create_session(import.working_dir.clone(), import.name.clone())
|
||||
.create_session(
|
||||
import.working_dir.clone(),
|
||||
import.name.clone(),
|
||||
import.session_type,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut builder = SessionUpdateBuilder::new(session.id.clone())
|
||||
@@ -1084,7 +1176,7 @@ mod tests {
|
||||
let description = format!("Test session {}", i);
|
||||
|
||||
let session = session_storage
|
||||
.create_session(working_dir.clone(), description)
|
||||
.create_session(working_dir.clone(), description, SessionType::User)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1176,7 +1268,11 @@ mod tests {
|
||||
let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap());
|
||||
|
||||
let original = storage
|
||||
.create_session(PathBuf::from("/tmp/test"), DESCRIPTION.to_string())
|
||||
.create_session(
|
||||
PathBuf::from("/tmp/test"),
|
||||
DESCRIPTION.to_string(),
|
||||
SessionType::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
+403
-1329
File diff suppressed because it is too large
Load Diff
@@ -1,833 +0,0 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use rmcp::model::{CallToolRequestParam, ErrorCode};
|
||||
use rmcp::object;
|
||||
use serde_json::json;
|
||||
|
||||
use goose::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME;
|
||||
mod test_support;
|
||||
use test_support::{
|
||||
create_temp_recipe, create_test_session_metadata, MockBehavior, ScheduleToolTestBuilder,
|
||||
};
|
||||
|
||||
// Test all actions of the scheduler platform tool
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_list_action() {
|
||||
// Create a test builder with existing jobs
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.with_existing_job("job2", "0 0 * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test list action
|
||||
let arguments = json!({
|
||||
"action": "list"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content.text.contains("Scheduled Jobs:"));
|
||||
assert!(text_content.text.contains("job1"));
|
||||
assert!(text_content.text.contains("job2"));
|
||||
} else {
|
||||
panic!("Expected text content");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"list_scheduled_jobs".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_list_action_empty() {
|
||||
// Create a test builder with no jobs
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test list action
|
||||
let arguments = json!({
|
||||
"action": "list"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content.text.contains("Scheduled Jobs:"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"list_scheduled_jobs".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_list_action_error() {
|
||||
// Create a test builder with a list error
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_scheduler_behavior(
|
||||
"list_scheduled_jobs",
|
||||
MockBehavior::InternalError("Database error".to_string()),
|
||||
)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test list action
|
||||
let arguments = json!({
|
||||
"action": "list"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
|
||||
assert!(err.message.contains("Failed to list jobs"));
|
||||
assert!(err.message.contains("Database error"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"list_scheduled_jobs".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_create_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Create a temporary recipe file
|
||||
let temp_recipe = create_temp_recipe(true, "json");
|
||||
|
||||
// Test create action
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"recipe_path": temp_recipe.path.to_str().unwrap(),
|
||||
"cron_expression": "*/5 * * * * *"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Successfully created scheduled job"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"add_scheduled_job".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_create_action_missing_params() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test create action with missing recipe_path
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"cron_expression": "*/5 * * * * *"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
|
||||
assert!(err.message.contains("Missing 'recipe_path' parameter"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Test create action with missing cron_expression
|
||||
let temp_recipe = create_temp_recipe(true, "json");
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"recipe_path": temp_recipe.path.to_str().unwrap()
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
|
||||
assert!(err.message.contains("Missing 'cron_expression' parameter"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_create_action_nonexistent_recipe() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test create action with nonexistent recipe
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"recipe_path": "/nonexistent/recipe.json",
|
||||
"cron_expression": "*/5 * * * * *"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
|
||||
assert!(err.message.contains("Recipe file not found"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_create_action_invalid_recipe() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Create an invalid recipe file
|
||||
let temp_recipe = create_temp_recipe(false, "json");
|
||||
|
||||
// Test create action with invalid recipe
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"recipe_path": temp_recipe.path.to_str().unwrap(),
|
||||
"cron_expression": "*/5 * * * * *"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
|
||||
assert!(err.message.contains("Invalid JSON recipe"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_create_action_scheduler_error() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_scheduler_behavior(
|
||||
"add_scheduled_job",
|
||||
MockBehavior::AlreadyExists("job1".to_string()),
|
||||
)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Create a temporary recipe file
|
||||
let temp_recipe = create_temp_recipe(true, "json");
|
||||
|
||||
// Test create action
|
||||
let arguments = json!({
|
||||
"action": "create",
|
||||
"recipe_path": temp_recipe.path.to_str().unwrap(),
|
||||
"cron_expression": "*/5 * * * * *"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
|
||||
assert!(err.message.contains("Failed to create job"));
|
||||
assert!(err.message.contains("job1"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"add_scheduled_job".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_run_now_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test run_now action
|
||||
let arguments = json!({
|
||||
"action": "run_now",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Successfully started job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"run_now".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_run_now_action_missing_job_id() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test run_now action with missing job_id
|
||||
let arguments = json!({
|
||||
"action": "run_now"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
|
||||
assert!(err.message.contains("Missing 'job_id' parameter"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_run_now_action_nonexistent_job() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_scheduler_behavior("run_now", MockBehavior::NotFound("nonexistent".to_string()))
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test run_now action with nonexistent job
|
||||
let arguments = json!({
|
||||
"action": "run_now",
|
||||
"job_id": "nonexistent"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Failed to run job"));
|
||||
assert!(err.message.contains("nonexistent"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"run_now".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_pause_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test pause action
|
||||
let arguments = json!({
|
||||
"action": "pause",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content.text.contains("Successfully paused job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"pause_schedule".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_pause_action_missing_job_id() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test pause action with missing job_id
|
||||
let arguments = json!({
|
||||
"action": "pause"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Missing 'job_id' parameter"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_pause_action_running_job() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_scheduler_behavior(
|
||||
"pause_schedule",
|
||||
MockBehavior::JobCurrentlyRunning("job1".to_string()),
|
||||
)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test pause action with a running job
|
||||
let arguments = json!({
|
||||
"action": "pause",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Failed to pause job"));
|
||||
assert!(err.message.contains("job1"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"pause_schedule".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_unpause_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test unpause action
|
||||
let arguments = json!({
|
||||
"action": "unpause",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Successfully unpaused job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"unpause_schedule".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_delete_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test delete action
|
||||
let arguments = json!({
|
||||
"action": "delete",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Successfully deleted job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"remove_scheduled_job".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_kill_action() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.with_running_job("job1")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test kill action
|
||||
let arguments = json!({
|
||||
"action": "kill",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Successfully killed running job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"kill_running_job".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_kill_action_not_running() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test kill action with a job that's not running
|
||||
let arguments = json!({
|
||||
"action": "kill",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Failed to kill job"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"kill_running_job".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_inspect_action_running() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.with_running_job("job1")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test inspect action
|
||||
let arguments = json!({
|
||||
"action": "inspect",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Job 'job1' is currently running"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"get_running_job_info".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_inspect_action_not_running() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test inspect action with a job that's not running
|
||||
let arguments = json!({
|
||||
"action": "inspect",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("Job 'job1' is not currently running"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"get_running_job_info".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_sessions_action() {
|
||||
// Create test session metadata
|
||||
let sessions = vec![
|
||||
(
|
||||
"1234567890_session1".to_string(),
|
||||
create_test_session_metadata(5, "/tmp"),
|
||||
),
|
||||
(
|
||||
"0987654321_session2".to_string(),
|
||||
create_test_session_metadata(10, "/home"),
|
||||
),
|
||||
];
|
||||
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.with_sessions_data("job1", sessions)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test sessions action
|
||||
let arguments = json!({
|
||||
"action": "sessions",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content.text.contains("Sessions for job 'job1'"));
|
||||
assert!(text_content.text.contains("session1"));
|
||||
assert!(text_content.text.contains("session2"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"sessions".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_sessions_action_with_limit() {
|
||||
// Create test session metadata
|
||||
let sessions = vec![
|
||||
(
|
||||
"1234567890_session1".to_string(),
|
||||
create_test_session_metadata(5, "/tmp"),
|
||||
),
|
||||
(
|
||||
"0987654321_session2".to_string(),
|
||||
create_test_session_metadata(10, "/home"),
|
||||
),
|
||||
(
|
||||
"5555555555_session3".to_string(),
|
||||
create_test_session_metadata(15, "/usr"),
|
||||
),
|
||||
];
|
||||
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.with_sessions_data("job1", sessions)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test sessions action with limit
|
||||
let arguments = json!({
|
||||
"action": "sessions",
|
||||
"job_id": "job1",
|
||||
"limit": 2
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"sessions".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_sessions_action_empty() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test sessions action with no sessions
|
||||
let arguments = json!({
|
||||
"action": "sessions",
|
||||
"job_id": "job1"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = result.unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
if let Some(text_content) = content[0].as_text() {
|
||||
assert!(text_content
|
||||
.text
|
||||
.contains("No sessions found for job 'job1'"));
|
||||
}
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"sessions".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_session_content_action_missing_session_id() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test session_content action with missing session_id
|
||||
let arguments = json!({
|
||||
"action": "session_content"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Missing 'session_id' parameter"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_unknown_action() {
|
||||
let (agent, _) = ScheduleToolTestBuilder::new().build().await;
|
||||
|
||||
// Test unknown action
|
||||
let arguments = json!({
|
||||
"action": "unknown_action"
|
||||
});
|
||||
|
||||
let result = agent
|
||||
.handle_schedule_management(arguments, "test_req".to_string())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.message.contains("Unknown action"));
|
||||
} else {
|
||||
panic!("Expected ExecutionError");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schedule_tool_dispatch() {
|
||||
let (agent, scheduler) = ScheduleToolTestBuilder::new()
|
||||
.with_existing_job("job1", "*/5 * * * * *")
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
// Test that the tool is properly dispatched through dispatch_tool_call
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: PLATFORM_MANAGE_SCHEDULE_TOOL_NAME.into(),
|
||||
arguments: Some(object!({
|
||||
"action": "list"
|
||||
})),
|
||||
};
|
||||
|
||||
let (request_id, result) = agent
|
||||
.dispatch_tool_call(tool_call, "test_dispatch".to_string(), None, None)
|
||||
.await;
|
||||
assert_eq!(request_id, "test_dispatch");
|
||||
assert!(result.is_ok());
|
||||
|
||||
let tool_result = result.unwrap();
|
||||
// The result should be a future that resolves to the tool output
|
||||
let output = tool_result.result.await;
|
||||
assert!(output.is_ok());
|
||||
|
||||
// Verify the scheduler was called
|
||||
let calls = scheduler.get_calls().await;
|
||||
assert!(calls.contains(&"list_scheduled_jobs".to_string()));
|
||||
}
|
||||
@@ -345,7 +345,6 @@ impl ScheduleToolTestBuilder {
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
execution_mode: Some("background".to_string()),
|
||||
};
|
||||
{
|
||||
let mut jobs = self.scheduler.jobs.lock().await;
|
||||
@@ -397,5 +396,6 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
|
||||
conversation: None,
|
||||
message_count,
|
||||
user_recipe_values: None,
|
||||
session_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -4127,10 +4127,6 @@
|
||||
"currently_running": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"execution_mode": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4226,6 +4222,9 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"session_type": {
|
||||
"$ref": "#/components/schemas/SessionType"
|
||||
},
|
||||
"total_tokens": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
@@ -4344,6 +4343,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SessionType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user",
|
||||
"scheduled",
|
||||
"sub_agent",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"SessionsQuery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+30
-10
@@ -328,6 +328,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -697,6 +698,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -743,6 +745,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -1182,6 +1185,7 @@
|
||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
@@ -2731,6 +2735,7 @@
|
||||
"integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^3.0.1",
|
||||
"@inquirer/confirm": "^4.0.1",
|
||||
@@ -5960,8 +5965,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -6250,6 +6254,7 @@
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -6291,6 +6296,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -6301,6 +6307,7 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -6447,6 +6454,7 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -6848,6 +6856,7 @@
|
||||
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -7097,6 +7106,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7736,6 +7746,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -9071,8 +9082,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
@@ -9130,6 +9140,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
@@ -10145,6 +10156,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10604,6 +10616,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
@@ -12819,6 +12832,7 @@
|
||||
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||
"cssstyle": "^5.3.1",
|
||||
@@ -14043,7 +14057,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -14064,6 +14077,7 @@
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
@@ -16438,6 +16452,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -16526,7 +16541,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -16542,7 +16556,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -16859,6 +16872,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -16868,6 +16882,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -16889,8 +16904,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
@@ -17839,6 +17853,7 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -18832,7 +18847,8 @@
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -19451,6 +19467,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -19836,6 +19853,7 @@
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -19949,6 +19967,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -20654,6 +20673,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -667,7 +667,6 @@ export type ScheduledJob = {
|
||||
cron: string;
|
||||
current_session_id?: string | null;
|
||||
currently_running?: boolean;
|
||||
execution_mode?: string | null;
|
||||
id: string;
|
||||
last_run?: string | null;
|
||||
paused?: boolean;
|
||||
@@ -689,6 +688,7 @@ export type Session = {
|
||||
output_tokens?: number | null;
|
||||
recipe?: Recipe | null;
|
||||
schedule_id?: string | null;
|
||||
session_type?: SessionType;
|
||||
total_tokens?: number | null;
|
||||
updated_at: string;
|
||||
user_recipe_values?: {
|
||||
@@ -725,6 +725,8 @@ export type SessionListResponse = {
|
||||
sessions: Array<Session>;
|
||||
};
|
||||
|
||||
export type SessionType = 'user' | 'scheduled' | 'sub_agent' | 'hidden';
|
||||
|
||||
export type SessionsQuery = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user