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);
|
||||
|
||||
Reference in New Issue
Block a user