diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 4f916cf..248d94d 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -21,7 +21,7 @@ use std::time::Instant; const MAX_TOOL_RESULT_CHARS: usize = 16_000; /// Minimum characters to keep when truncating const TRUNCATION_SUFFIX_LEN: usize = 200; -const MEMORY_TOOL_USAGE_SYSTEM_PROMPT: &str = "你可以在处理任务过程中使用长期记忆工具。读取记忆时,优先使用 memory_search:当用户的任务描述缺少相关指代,上下文存在模糊不清,执行任务需要用户长期偏好、稳定事实、历史决策、持续任务上下文时,先 search;已知 namespace/key 时可用 get;需要浏览最近记忆时可用 list。写入或修改记忆时,再使用 memory_manage。仅在遇到高价值且未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务/项目上下文、明确决策。不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。写入时优先使用规范 namespace:preferences、profile、tasks、decisions,并优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。检索时应提供 queries 数组,尽量同时放入中文关键词、英文别名,以及可能的 snake_case memory_key 词,例如 queries=['email', '邮件', 'email_folder_preference']。"; +const MEMORY_TOOL_USAGE_SYSTEM_PROMPT: &str = "在绝大多数请求开始时,你都应先使用长期记忆检索工具 memory_search 来召回相关上下文,然后再决定如何回答或是否需要写入记忆。默认流程是:先用 memory_search(action='search');只有在你已经明确知道 namespace 和 key 时才改用 get;只有在需要浏览最近几条记忆时才用 list。即使用户没有明确提到“记忆”或“偏好”,只要请求可能与用户长期偏好、稳定事实、历史决策、持续任务或项目上下文有关,就应先搜记忆。仅以下少数情况可跳过记忆搜索:纯寒暄、一次性简单计算、完全不依赖用户历史的直接事实问答。写入或修改记忆时,再使用 memory_manage。仅在遇到高价值且未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务/项目上下文、明确决策。不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。写入时优先使用规范 namespace:preferences、profile、tasks、decisions,并优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。检索时应提供 queries 数组,尽量同时放入中文关键词、英文别名,以及可能的 snake_case memory_key 词,例如 queries=['email', '邮件', 'email_folder_preference']。如果你决定跳过记忆搜索,应先确认当前请求确实属于上述少数例外,而不是因为你忘了检索。"; const PENDING_USER_ACTION_MARKER: &str = "__PICOBOT_PENDING_USER_ACTION__"; const DEFAULT_PENDING_ASSISTANT_MESSAGE: &str = "工具已经启动并进入等待用户操作的状态。请先完成外部操作,完成后直接告诉我继续。"; diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 3b8bc22..5510264 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -80,7 +80,7 @@ async fn handle_socket(ws: WebSocket, state: Arc) { return; } - let runtime_session_id = session.lock().await.id; + let runtime_session_id = session.lock().await.id.to_string(); let mut current_session_id = initial_record.id.clone(); tracing::info!(runtime_session_id = %runtime_session_id, session_id = %current_session_id, "CLI session established"); @@ -95,7 +95,7 @@ async fn handle_socket(ws: WebSocket, state: Arc) { let (mut ws_sender, mut ws_receiver) = ws.split(); let mut receiver = receiver; - let session_id_for_sender = runtime_session_id; + let session_id_for_sender = runtime_session_id.clone(); tokio::spawn(async move { while let Some(msg) = receiver.recv().await { if let Ok(text) = serialize_outbound(&msg) { @@ -114,7 +114,13 @@ async fn handle_socket(ws: WebSocket, state: Arc) { let text = text.to_string(); match parse_inbound(&text) { Ok(inbound) => { - if let Err(e) = handle_inbound(&state, &session, &mut current_session_id, inbound).await { + if let Err(e) = handle_inbound( + &state, + &session, + &runtime_session_id, + &mut current_session_id, + inbound, + ).await { tracing::warn!(error = %e, session_id = %current_session_id, "Failed to handle inbound message"); let _ = session .lock() @@ -232,12 +238,14 @@ fn should_display_message_to_user(show_tool_results: bool, message: &ChatMessage async fn handle_inbound( state: &Arc, session: &Arc>, + runtime_session_id: &str, current_session_id: &mut String, inbound: WsInbound, ) -> Result<(), crate::agent::AgentError> { match inbound { - WsInbound::UserInput { content, chat_id, .. } => { + WsInbound::UserInput { content, chat_id, sender_id, .. } => { let chat_id = chat_id.unwrap_or_else(|| current_session_id.clone()); + let sender_id = resolve_ws_sender_id(sender_id.as_deref(), runtime_session_id); let mut session_guard = session.lock().await; session_guard.ensure_persistent_session(&chat_id)?; @@ -280,7 +288,7 @@ async fn handle_inbound( show_tool_results: state.config.gateway.show_tool_results, }); let agent = session_guard - .create_agent(&chat_id, None, Some(&user_message_id))? + .create_agent(&chat_id, Some(&sender_id), Some(&user_message_id))? .with_emitted_message_handler(live_emitter); match agent.process(history).await { Ok(result) => { @@ -434,10 +442,23 @@ async fn handle_inbound( } } +fn resolve_ws_sender_id(sender_id: Option<&str>, runtime_session_id: &str) -> String { + sender_id + .map(str::trim) + .filter(|sender_id| !sender_id.is_empty()) + .map(ToOwned::to_owned) + .unwrap_or_else(|| runtime_session_id.to_string()) +} + #[cfg(test)] mod tests { use crate::agent::EmittedMessageHandler; - use super::{WsToolCallEmitter, should_display_message_to_user, ws_outbound_from_chat_message}; + use super::{ + WsToolCallEmitter, + resolve_ws_sender_id, + should_display_message_to_user, + ws_outbound_from_chat_message, + }; use crate::bus::ChatMessage; use crate::bus::message::ToolMessageState; use crate::providers::ToolCall; @@ -528,6 +549,18 @@ mod tests { assert!(should_display_message_to_user(true, &completed)); } + #[test] + fn test_resolve_ws_sender_id_prefers_inbound_sender() { + assert_eq!(resolve_ws_sender_id(Some("user-42"), "runtime-1"), "user-42"); + assert_eq!(resolve_ws_sender_id(Some(" user-42 "), "runtime-1"), "user-42"); + } + + #[test] + fn test_resolve_ws_sender_id_falls_back_to_runtime_session_id() { + assert_eq!(resolve_ws_sender_id(None, "runtime-1"), "runtime-1"); + assert_eq!(resolve_ws_sender_id(Some(" "), "runtime-1"), "runtime-1"); + } + #[tokio::test] async fn test_ws_tool_call_emitter_hides_completed_tool_results_when_disabled() { let (sender, mut receiver) = mpsc::channel(4); diff --git a/src/tools/memory_manage.rs b/src/tools/memory_manage.rs index 223af36..4df41cd 100644 --- a/src/tools/memory_manage.rs +++ b/src/tools/memory_manage.rs @@ -23,7 +23,7 @@ impl Tool for MemoryManageTool { } fn description(&self) -> &str { - "Create, update, or delete long-term user memories stored in SQLite. Supports actions: put, update, delete. Use memory_search for all retrieval, including search, get, and list. Memories are scoped to the current channel and sender, and record the originating session/message when available." + "Create, update, or delete long-term user memories stored in SQLite. Supports actions: put, update, delete. Use memory_search as the default retrieval path before answering most requests, and use memory_search for all retrieval actions including search, get, and list. Only call this tool when you have determined that a high-value long-term memory should be created, overwritten, updated, or deleted. Memories are scoped to the current channel and sender, and record the originating session/message when available." } fn parameters_schema(&self) -> serde_json::Value { diff --git a/src/tools/memory_search.rs b/src/tools/memory_search.rs index 4f500df..7abc904 100644 --- a/src/tools/memory_search.rs +++ b/src/tools/memory_search.rs @@ -23,7 +23,7 @@ impl Tool for MemorySearchTool { } fn description(&self) -> &str { - "Search and read long-term user memories stored in SQLite. Use this tool when you need prior preferences, stable facts, historical decisions, or ongoing task context. This tool is read-only and supports three actions: search for multi-keyword recall, get for exact namespace/key lookup, and list for browsing recent memories. Prefer this tool over memory_manage when you only need to retrieve memory." + "Search and read long-term user memories stored in SQLite. This is the default entry point for memory retrieval and should usually be the first memory tool you call at the start of a request, unless the request is clearly a simple greeting, a one-off calculation, or a direct fact question that does not depend on user history. Use it to recall prior preferences, stable facts, historical decisions, and ongoing task context. This tool is read-only and supports three actions: search for multi-keyword recall, get for exact namespace/key lookup, and list for browsing recent memories. Prefer this tool over memory_manage whenever you only need to retrieve memory." } fn parameters_schema(&self) -> serde_json::Value {