Session manager (#4648)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -42,8 +42,6 @@ use crate::providers::errors::ProviderError;
|
||||
use crate::recipe::{Author, Recipe, Response, Settings, SubRecipe};
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
use crate::security::security_inspector::SecurityInspector;
|
||||
use crate::session;
|
||||
use crate::session::extension_data::ExtensionState;
|
||||
use crate::tool_inspection::ToolInspectionManager;
|
||||
use crate::tool_monitor::RepetitionInspector;
|
||||
use crate::utils::is_token_cancelled;
|
||||
@@ -55,7 +53,7 @@ use rmcp::model::{
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use super::final_output_tool::FinalOutputTool;
|
||||
use super::model_selector::autopilot::AutoPilot;
|
||||
@@ -66,12 +64,14 @@ use crate::agents::todo_tools::{
|
||||
todo_read_tool, todo_write_tool, TODO_READ_TOOL_NAME, TODO_WRITE_TOOL_NAME,
|
||||
};
|
||||
use crate::conversation::message::{Message, ToolRequest};
|
||||
use crate::session::extension_data::ExtensionState;
|
||||
use crate::session::{extension_data, SessionManager};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
|
||||
/// Context needed for the reply function
|
||||
pub struct ReplyContext {
|
||||
pub messages: Conversation,
|
||||
pub conversation: Conversation,
|
||||
pub tools: Vec<Tool>,
|
||||
pub toolshim_tools: Vec<Tool>,
|
||||
pub system_prompt: String,
|
||||
@@ -268,7 +268,7 @@ impl Agent {
|
||||
.await;
|
||||
|
||||
Ok(ReplyContext {
|
||||
messages: conversation,
|
||||
conversation,
|
||||
tools,
|
||||
toolshim_tools,
|
||||
system_prompt,
|
||||
@@ -506,17 +506,12 @@ impl Agent {
|
||||
)))
|
||||
} else if tool_call.name == TODO_READ_TOOL_NAME {
|
||||
// Handle task planner read tool
|
||||
let session_file_path = if let Some(session_config) = session {
|
||||
session::storage::get_path(session_config.id.clone()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let todo_content = if let Some(path) = session_file_path {
|
||||
session::storage::read_metadata(&path)
|
||||
let todo_content = if let Some(session_config) = session {
|
||||
SessionManager::get_session(&session_config.id, false)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|m| {
|
||||
session::TodoState::from_extension_data(&m.extension_data)
|
||||
.and_then(|metadata| {
|
||||
extension_data::TodoState::from_extension_data(&metadata.extension_data)
|
||||
.map(|state| state.content)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@@ -551,43 +546,39 @@ impl Agent {
|
||||
None,
|
||||
)))
|
||||
} else if let Some(session_config) = session {
|
||||
// Update session metadata with new TODO content
|
||||
match session::storage::get_path(session_config.id.clone()) {
|
||||
Ok(path) => match session::storage::read_metadata(&path) {
|
||||
Ok(mut metadata) => {
|
||||
let todo_state = session::TodoState::new(content);
|
||||
todo_state
|
||||
.to_extension_data(&mut metadata.extension_data)
|
||||
.ok();
|
||||
|
||||
let path_clone = path.clone();
|
||||
let metadata_clone = metadata.clone();
|
||||
let update_result = tokio::task::spawn(async move {
|
||||
session::storage::update_metadata(&path_clone, &metadata_clone)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
match update_result {
|
||||
Ok(Ok(_)) => ToolCallResult::from(Ok(vec![Content::text(
|
||||
format!("Updated ({} chars)", char_count),
|
||||
)])),
|
||||
_ => ToolCallResult::from(Err(ErrorData::new(
|
||||
match SessionManager::get_session(&session_config.id, false).await {
|
||||
Ok(mut session) => {
|
||||
let todo_state = extension_data::TodoState::new(content);
|
||||
if todo_state
|
||||
.to_extension_data(&mut session.extension_data)
|
||||
.is_ok()
|
||||
{
|
||||
match SessionManager::update_session(&session_config.id)
|
||||
.extension_data(session.extension_data)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(_) => ToolCallResult::from(Ok(vec![Content::text(format!(
|
||||
"Updated ({} chars)",
|
||||
char_count
|
||||
))])),
|
||||
Err(_) => ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to update session metadata".to_string(),
|
||||
None,
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to serialize TODO state".to_string(),
|
||||
None,
|
||||
)))
|
||||
}
|
||||
Err(_) => ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to read session metadata".to_string(),
|
||||
None,
|
||||
))),
|
||||
},
|
||||
}
|
||||
Err(_) => ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to get session path".to_string(),
|
||||
"Failed to read session metadata".to_string(),
|
||||
None,
|
||||
))),
|
||||
}
|
||||
@@ -911,10 +902,9 @@ impl Agent {
|
||||
> {
|
||||
// Try to get session metadata for more accurate token counts
|
||||
let session_metadata = if let Some(session_config) = session {
|
||||
match session::storage::get_path(session_config.id.clone()) {
|
||||
Ok(session_file_path) => session::storage::read_metadata(&session_file_path).ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
SessionManager::get_session(&session_config.id, false)
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -960,7 +950,7 @@ impl Agent {
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
|
||||
// Handle auto-compaction before processing
|
||||
let (messages, compaction_msg, _summarization_usage) = match self
|
||||
let (conversation, compaction_msg, _summarization_usage) = match self
|
||||
.handle_auto_compaction(unfixed_conversation.messages(), &session)
|
||||
.await?
|
||||
{
|
||||
@@ -969,7 +959,7 @@ impl Agent {
|
||||
let context = self
|
||||
.prepare_reply_context(unfixed_conversation, &session)
|
||||
.await?;
|
||||
(context.messages, None, None)
|
||||
(context.conversation, None, None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -977,10 +967,13 @@ impl Agent {
|
||||
if let Some(compaction_msg) = compaction_msg {
|
||||
return Ok(Box::pin(async_stream::try_stream! {
|
||||
yield AgentEvent::Message(Message::assistant().with_summarization_requested(compaction_msg));
|
||||
yield AgentEvent::HistoryReplaced(messages.messages().clone());
|
||||
yield AgentEvent::HistoryReplaced(conversation.messages().clone());
|
||||
if let Some(session_to_store) = &session {
|
||||
SessionManager::replace_conversation(&session_to_store.id, &conversation).await?
|
||||
}
|
||||
|
||||
// Continue with normal reply processing using compacted messages
|
||||
let mut reply_stream = self.reply_internal(messages, session, cancel_token).await?;
|
||||
let mut reply_stream = self.reply_internal(conversation, session, cancel_token).await?;
|
||||
while let Some(event) = reply_stream.next().await {
|
||||
yield event?;
|
||||
}
|
||||
@@ -988,19 +981,20 @@ impl Agent {
|
||||
}
|
||||
|
||||
// No compaction needed, proceed with normal processing
|
||||
self.reply_internal(messages, session, cancel_token).await
|
||||
self.reply_internal(conversation, session, cancel_token)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Main reply method that handles the actual agent processing
|
||||
async fn reply_internal(
|
||||
&self,
|
||||
messages: Conversation,
|
||||
conversation: Conversation,
|
||||
session: Option<SessionConfig>,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
|
||||
let context = self.prepare_reply_context(messages, &session).await?;
|
||||
let context = self.prepare_reply_context(conversation, &session).await?;
|
||||
let ReplyContext {
|
||||
mut messages,
|
||||
mut conversation,
|
||||
mut tools,
|
||||
mut toolshim_tools,
|
||||
mut system_prompt,
|
||||
@@ -1011,12 +1005,52 @@ impl Agent {
|
||||
let reply_span = tracing::Span::current();
|
||||
self.reset_retry_attempts().await;
|
||||
|
||||
if let Some(content) = messages
|
||||
.last()
|
||||
.and_then(|msg| msg.content.first())
|
||||
.and_then(|c| c.as_text())
|
||||
{
|
||||
debug!("user_message" = &content);
|
||||
// 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_description(&session_id, provider).await
|
||||
{
|
||||
warn!("Failed to generate session description: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
@@ -1054,7 +1088,7 @@ impl Agent {
|
||||
|
||||
{
|
||||
let mut autopilot = self.autopilot.lock().await;
|
||||
if let Some((new_provider, role, model)) = autopilot.check_for_switch(&messages, self.provider().await?).await? {
|
||||
if let Some((new_provider, role, model)) = autopilot.check_for_switch(&conversation, self.provider().await?).await? {
|
||||
debug!("AutoPilot switching to {} role with model {}", role, model);
|
||||
self.update_provider(new_provider).await?;
|
||||
|
||||
@@ -1065,17 +1099,16 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut stream = Self::stream_response_from_provider(
|
||||
self.provider().await?,
|
||||
&system_prompt,
|
||||
messages.messages(),
|
||||
conversation.messages(),
|
||||
&tools,
|
||||
&toolshim_tools,
|
||||
).await?;
|
||||
|
||||
let mut added_message = false;
|
||||
let mut messages_to_add = Vec::new();
|
||||
let mut no_tools_called = true;
|
||||
let mut messages_to_add = Conversation::default();
|
||||
let mut tools_updated = false;
|
||||
|
||||
while let Some(next) = stream.next().await {
|
||||
@@ -1109,12 +1142,12 @@ 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, messages.len())
|
||||
.await?;
|
||||
Self::update_session_metrics(session_config, usage).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(response) = response {
|
||||
messages_to_add.push(response.clone());
|
||||
let ToolCategorizeResult {
|
||||
frontend_requests,
|
||||
remaining_requests,
|
||||
@@ -1161,7 +1194,7 @@ impl Agent {
|
||||
let inspection_results = self.tool_inspection_manager
|
||||
.inspect_tools(
|
||||
&remaining_requests,
|
||||
messages.messages(),
|
||||
conversation.messages(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1260,25 +1293,26 @@ impl Agent {
|
||||
let final_message_tool_resp = message_tool_response.lock().await.clone();
|
||||
yield AgentEvent::Message(final_message_tool_resp.clone());
|
||||
|
||||
added_message = true;
|
||||
messages_to_add.push(response);
|
||||
no_tools_called = false;
|
||||
messages_to_add.push(final_message_tool_resp);
|
||||
}
|
||||
}
|
||||
Err(ProviderError::ContextLengthExceeded(error_msg)) => {
|
||||
info!("Context length exceeded, attempting compaction");
|
||||
|
||||
match auto_compact::perform_compaction(self, messages.messages()).await {
|
||||
match auto_compact::perform_compaction(self, conversation.messages()).await {
|
||||
Ok(compact_result) => {
|
||||
messages = compact_result.messages;
|
||||
conversation = compact_result.messages;
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_summarization_requested(
|
||||
"Context limit reached. Conversation has been automatically compacted to continue."
|
||||
)
|
||||
);
|
||||
yield AgentEvent::HistoryReplaced(messages.messages().to_vec());
|
||||
|
||||
yield AgentEvent::HistoryReplaced(conversation.messages().to_vec());
|
||||
if let Some(session_to_store) = &session {
|
||||
SessionManager::replace_conversation(&session_to_store.id, &conversation).await?
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -1301,40 +1335,49 @@ impl Agent {
|
||||
if tools_updated {
|
||||
(tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?;
|
||||
}
|
||||
if !added_message {
|
||||
let mut exit_chat = false;
|
||||
if no_tools_called {
|
||||
if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() {
|
||||
if final_output_tool.final_output.is_none() {
|
||||
tracing::warn!("Final output tool has not been called yet. Continuing agent loop.");
|
||||
warn!("Final output tool has not been called yet. Continuing agent loop.");
|
||||
let message = Message::user().with_text(FINAL_OUTPUT_CONTINUATION_MESSAGE);
|
||||
messages_to_add.push(message.clone());
|
||||
yield AgentEvent::Message(message);
|
||||
messages.extend(messages_to_add);
|
||||
continue
|
||||
} else {
|
||||
let message = Message::assistant().with_text(final_output_tool.final_output.clone().unwrap());
|
||||
messages_to_add.push(message.clone());
|
||||
yield AgentEvent::Message(message);
|
||||
exit_chat = true;
|
||||
}
|
||||
}
|
||||
|
||||
match self.handle_retry_logic(&mut messages, &session, &initial_messages).await {
|
||||
Ok(should_retry) => {
|
||||
if should_retry {
|
||||
info!("Retry logic triggered, restarting agent loop");
|
||||
continue;
|
||||
} else {
|
||||
match self.handle_retry_logic(&mut conversation, &session, &initial_messages).await {
|
||||
Ok(should_retry) => {
|
||||
if should_retry {
|
||||
info!("Retry logic triggered, restarting agent loop");
|
||||
} else {
|
||||
exit_chat = true;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Retry logic failed: {}", e);
|
||||
yield AgentEvent::Message(Message::assistant().with_text(
|
||||
format!("Retry logic encountered an error: {}", e)
|
||||
));
|
||||
exit_chat = true;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Retry logic failed: {}", e);
|
||||
yield AgentEvent::Message(Message::assistant().with_text(
|
||||
format!("Retry logic encountered an error: {}", e)
|
||||
));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
messages.extend(messages_to_add);
|
||||
if let Some(session_config) = &session {
|
||||
for msg in &messages_to_add {
|
||||
SessionManager::add_message(&session_config.id, msg).await?;
|
||||
}
|
||||
}
|
||||
conversation.extend(messages_to_add);
|
||||
if exit_chat {
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::providers::toolshim::{
|
||||
modify_system_prompt_for_tool_json, OllamaInterpreter,
|
||||
};
|
||||
|
||||
use crate::session;
|
||||
use crate::session::SessionManager;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
async fn toolshim_postprocess(
|
||||
@@ -276,23 +276,9 @@ impl Agent {
|
||||
pub(crate) async fn update_session_metrics(
|
||||
session_config: &crate::agents::types::SessionConfig,
|
||||
usage: &ProviderUsage,
|
||||
messages_length: usize,
|
||||
) -> Result<()> {
|
||||
let session_file_path = match session::storage::get_path(session_config.id.clone()) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("Failed to get session file path: {}", e));
|
||||
}
|
||||
};
|
||||
let mut metadata = session::storage::read_metadata(&session_file_path)?;
|
||||
|
||||
metadata.schedule_id = session_config.schedule_id.clone();
|
||||
|
||||
metadata.total_tokens = usage.usage.total_tokens;
|
||||
metadata.input_tokens = usage.usage.input_tokens;
|
||||
metadata.output_tokens = usage.usage.output_tokens;
|
||||
|
||||
metadata.message_count = messages_length + 1;
|
||||
let session_id = session_config.id.as_str();
|
||||
let session = SessionManager::get_session(session_id, false).await?;
|
||||
|
||||
let accumulate = |a: Option<i32>, b: Option<i32>| -> Option<i32> {
|
||||
match (a, b) {
|
||||
@@ -300,16 +286,24 @@ impl Agent {
|
||||
_ => a.or(b),
|
||||
}
|
||||
};
|
||||
metadata.accumulated_total_tokens =
|
||||
accumulate(metadata.accumulated_total_tokens, usage.usage.total_tokens);
|
||||
metadata.accumulated_input_tokens =
|
||||
accumulate(metadata.accumulated_input_tokens, usage.usage.input_tokens);
|
||||
metadata.accumulated_output_tokens = accumulate(
|
||||
metadata.accumulated_output_tokens,
|
||||
usage.usage.output_tokens,
|
||||
);
|
||||
|
||||
session::storage::update_metadata(&session_file_path, &metadata).await?;
|
||||
let accumulated_total =
|
||||
accumulate(session.accumulated_total_tokens, usage.usage.total_tokens);
|
||||
let accumulated_input =
|
||||
accumulate(session.accumulated_input_tokens, usage.usage.input_tokens);
|
||||
let accumulated_output =
|
||||
accumulate(session.accumulated_output_tokens, usage.usage.output_tokens);
|
||||
|
||||
SessionManager::update_session(session_id)
|
||||
.schedule_id(session_config.schedule_id.clone())
|
||||
.total_tokens(usage.usage.total_tokens)
|
||||
.input_tokens(usage.usage.input_tokens)
|
||||
.output_tokens(usage.usage.output_tokens)
|
||||
.accumulated_total_tokens(accumulated_total)
|
||||
.accumulated_input_tokens(accumulated_input)
|
||||
.accumulated_output_tokens(accumulated_output)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -421,12 +421,12 @@ impl Agent {
|
||||
} else {
|
||||
let sessions_info: Vec<String> = sessions
|
||||
.into_iter()
|
||||
.map(|(session_name, metadata)| {
|
||||
.map(|(session_name, session)| {
|
||||
format!(
|
||||
"- Session: {} (Messages: {}, Working Dir: {})",
|
||||
session_name,
|
||||
metadata.message_count,
|
||||
metadata.working_dir.display()
|
||||
session.conversation.unwrap_or_default().len(),
|
||||
session.working_dir.display()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -462,55 +462,19 @@ impl Agent {
|
||||
)
|
||||
})?;
|
||||
|
||||
// Get the session file path
|
||||
let session_path = match crate::session::storage::get_path(
|
||||
crate::session::storage::Identifier::Name(session_id.to_string()),
|
||||
) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
return Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Invalid session ID '{}': {}", session_id, e),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if session file exists
|
||||
if !session_path.exists() {
|
||||
return Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Session '{}' not found", session_id),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
// Read session metadata
|
||||
let metadata = match crate::session::storage::read_metadata(&session_path) {
|
||||
let session = match crate::session::SessionManager::get_session(session_id, true).await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(e) => {
|
||||
return Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to read session metadata: {}", e),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Read session messages
|
||||
let messages = match crate::session::storage::read_messages(&session_path) {
|
||||
Ok(messages) => messages,
|
||||
Err(e) => {
|
||||
return Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to read session messages: {}", e),
|
||||
format!("Failed to read session for '{}': {}", session_id, e),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Format the response with metadata and messages
|
||||
let metadata_json = match serde_json::to_string_pretty(&metadata) {
|
||||
let metadata_json = match serde_json::to_string_pretty(&session) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
return Err(ErrorData::new(
|
||||
@@ -521,20 +485,9 @@ impl Agent {
|
||||
}
|
||||
};
|
||||
|
||||
let messages_json = match serde_json::to_string_pretty(&messages) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
return Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to serialize messages: {}", e),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(vec![Content::text(format!(
|
||||
"Session '{}' Content:\n\nMetadata:\n{}\n\nMessages:\n{}",
|
||||
session_id, metadata_json, messages_json
|
||||
"Session '{}' Content:\n\nSession:\n{}",
|
||||
session_id, metadata_json
|
||||
))])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::session;
|
||||
use mcp_core::ToolResult;
|
||||
use rmcp::model::{Content, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -82,7 +81,7 @@ pub struct FrontendTool {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionConfig {
|
||||
/// Unique identifier for the session
|
||||
pub id: session::Identifier,
|
||||
pub id: String,
|
||||
/// Working directory for the session
|
||||
pub working_dir: PathBuf,
|
||||
/// ID of the schedule that triggered this session, if any
|
||||
|
||||
Reference in New Issue
Block a user