Sessions required (#5548)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
+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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user