From 24bbd5f8c9a9b2eff7e77e7853d79dc85031c101 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Fri, 12 Jun 2026 18:01:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AD=90=E4=BB=A3=E7=90=86=20todo=20?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=94=AF=E6=8C=81=20=E2=80=94=20=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E5=AD=90=E4=BB=A3=E7=90=86=E8=A7=86=E5=9B=BE=E6=97=B6?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=AD=90=E4=BB=A3=E7=90=86=E7=9A=84=E5=BE=85?= =?UTF-8?q?=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SubAgentEmitter 添加 todo_write 持久化(照搬 BusToolCallEmitter 模式) - DefaultSubAgentRuntime 加 store 字段,透传给 emitter - Command::ListTodos 加 task_id 参数 - list_todos handler: 当 task_id 存在时,scope_key = sub:{parent}:{task_id} - 前端: 子代理视图下自动带 task_id 请求子代理的 todo Co-Authored-By: Claude Opus 4.8 --- src/command/handlers/list_todos.rs | 42 +++++++++++++-------- src/command/mod.rs | 8 +++- src/gateway/runtime.rs | 1 + src/tools/task/runtime.rs | 60 +++++++++++++++++++++++++++++- web/src/App.tsx | 7 +++- web/src/hooks/useChat.ts | 6 +++ web/src/types/protocol.ts | 1 + 7 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/command/handlers/list_todos.rs b/src/command/handlers/list_todos.rs index 535b21d..7b8d2aa 100644 --- a/src/command/handlers/list_todos.rs +++ b/src/command/handlers/list_todos.rs @@ -20,7 +20,7 @@ impl ListTodosCommandHandler { #[async_trait] impl CommandHandler for ListTodosCommandHandler { fn can_handle(&self, cmd: &Command) -> bool { - matches!(cmd, Command::ListTodos) + matches!(cmd, Command::ListTodos { .. }) } fn metadata(&self) -> Option { @@ -33,25 +33,35 @@ impl CommandHandler for ListTodosCommandHandler { async fn handle( &self, - _cmd: Command, + cmd: Command, ctx: CommandContext, ) -> Result { - // scope_key = topic_id.unwrap_or(session_id) - let scope_key = ctx - .topic_id - .as_deref() - .filter(|t| !t.is_empty()) - .or(ctx.session_id.as_deref()) - .ok_or_else(|| { - CommandError::new( - "MISSING_CONTEXT", - "Cannot list todos: no session_id or topic_id in command context", - ) - })?; + let task_id = match cmd { + Command::ListTodos { task_id } => task_id, + _ => None, + }; + + // 子代理:scope_key = sub:{parent_session_id}:{task_id} + // 主代理:scope_key = topic_id.unwrap_or(session_id) + let scope_key = if let (Some(tid), Some(parent_sid)) = (task_id.as_deref(), ctx.session_id.as_deref()) { + format!("sub:{}:{}", parent_sid, tid) + } else { + ctx.topic_id + .as_deref() + .filter(|t| !t.is_empty()) + .or(ctx.session_id.as_deref()) + .ok_or_else(|| { + CommandError::new( + "MISSING_CONTEXT", + "Cannot list todos: no session_id or topic_id in command context", + ) + })? + .to_string() + }; let records = self .store - .list_todos(scope_key) + .list_todos(&scope_key) .map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?; tracing::info!( @@ -77,6 +87,6 @@ impl CommandHandler for ListTodosCommandHandler { Ok(CommandResponse::success(ctx.request_id) .with_metadata("todos", &todos_json) - .with_metadata("todos_scope_key", scope_key)) + .with_metadata("todos_scope_key", &scope_key)) } } diff --git a/src/command/mod.rs b/src/command/mod.rs index 1831a29..90e3544 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -74,7 +74,11 @@ pub enum Command { /// 列出所有技能 ListSkills, /// 列出当前 Todo 列表 - ListTodos, + ListTodos { + /// 子代理的 task_id(进入子代理视图时传入) + #[serde(default)] + task_id: Option, + }, } impl Command { @@ -102,7 +106,7 @@ impl Command { Command::UpdateMemory { .. } => "update_memory", Command::DeleteMemory { .. } => "delete_memory", Command::ListSkills => "list_skills", - Command::ListTodos => "list_todos", + Command::ListTodos { .. } => "list_todos", } } } diff --git a/src/gateway/runtime.rs b/src/gateway/runtime.rs index 43a77e7..ec5d112 100644 --- a/src/gateway/runtime.rs +++ b/src/gateway/runtime.rs @@ -201,6 +201,7 @@ pub(crate) fn build_session_manager_with_sender( provider_config.clone(), catalog, bus.clone(), + store.clone(), )); (factory.with_subagent_runtime(subagent_runtime), task_repository) diff --git a/src/tools/task/runtime.rs b/src/tools/task/runtime.rs index 6b5bfa4..8489d75 100644 --- a/src/tools/task/runtime.rs +++ b/src/tools/task/runtime.rs @@ -12,7 +12,7 @@ use crate::bus::ChatMessage; use crate::bus::message::{OutboundMessage, OutboundEventKind}; use crate::bus::MessageBus; use crate::config::{LLMProviderConfig, SubagentsConfig}; -use crate::storage::ConversationRepository; +use crate::storage::{ConversationRepository, SessionStore}; use crate::tools::{ToolContext, ToolRegistry}; use super::error::TaskError; @@ -105,6 +105,8 @@ struct SubAgentEmitter { channel_name: String, chat_id: String, metadata: HashMap, + store: Arc, + sub_session_id: String, } #[async_trait] @@ -151,6 +153,57 @@ impl EmittedMessageHandler for SubAgentEmitter { ); } } + + // 拦截 todo_write 结果:持久化到 SQLite(子代理用 session_id 作为 scope_key) + if message.tool_name.as_deref() == Some("todo_write") { + self.persist_todo_write_result(&message); + } + } +} + +impl SubAgentEmitter { + fn persist_todo_write_result(&self, message: &ChatMessage) { + let parsed: serde_json::Value = match serde_json::from_str(&message.content) { + Ok(v) => v, + Err(_) => return, + }; + + let Some(todos_array) = parsed.get("current_todos").and_then(|v| v.as_array()) else { + return; + }; + + let scope_key = &self.sub_session_id; + + let records: Vec = todos_array + .iter() + .filter_map(|item| { + Some(crate::storage::TodoRecord { + id: item.get("id")?.as_str()?.to_string(), + scope_key: scope_key.clone(), + session_id: scope_key.clone(), + topic_id: None, + content: item.get("content")?.as_str()?.to_string(), + status: item.get("status")?.as_str()?.to_string(), + priority: item.get("priority")?.as_str()?.to_string(), + created_at: item.get("created_at")?.as_i64()?, + updated_at: item.get("updated_at")?.as_i64()?, + }) + }) + .collect(); + + if records.is_empty() { + return; + } + + tracing::info!( + scope_key = %scope_key, + todo_count = records.len(), + "SubAgentEmitter: persisting todo_write result" + ); + + if let Err(e) = self.store.replace_todos(scope_key, &records) { + tracing::warn!(error = %e, %scope_key, "Failed to persist sub-agent todo list"); + } } } @@ -173,6 +226,7 @@ pub struct DefaultSubAgentRuntime { /// 子代理定义目录(内置 + 自定义) catalog: Arc, bus: Option>, + store: Arc, } impl DefaultSubAgentRuntime { @@ -184,6 +238,7 @@ impl DefaultSubAgentRuntime { provider_config: LLMProviderConfig, catalog: Arc, bus: Option>, + store: Arc, ) -> Self { Self { config, @@ -193,6 +248,7 @@ impl DefaultSubAgentRuntime { provider_config, catalog, bus, + store, } } @@ -258,6 +314,8 @@ impl DefaultSubAgentRuntime { channel_name: session.parent_channel_name.clone(), chat_id: session.parent_chat_id.clone(), metadata, + store: self.store.clone(), + sub_session_id: session.session_id.clone(), }, self.conversation_repository.clone(), session.session_id.clone(), diff --git a/web/src/App.tsx b/web/src/App.tsx index 23ce1d0..f0a3e33 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -45,6 +45,7 @@ function App() { requestSkillList, todos, requestTodoList, + requestSubAgentTodoList, // 定时任务 schedulerJobs, sidebarTab, @@ -342,10 +343,12 @@ function App() { const key = `${selectedTopic ?? ''}|${subAgentView?.taskId ?? ''}` if (key === prevTodoTriggerRef.current) return prevTodoTriggerRef.current = key - const todoCmd = requestTodoList() + const todoCmd = subAgentView?.taskId + ? requestSubAgentTodoList(subAgentView.taskId) + : requestTodoList() handleCommand(todoCmd) sendMessage({ type: 'command', payload: JSON.stringify(todoCmd) }) - }, [status, selectedTopic, subAgentView, handleCommand, sendMessage, requestTodoList]) + }, [status, selectedTopic, subAgentView, handleCommand, sendMessage, requestTodoList, requestSubAgentTodoList]) const handleRefreshMemories = useCallback(() => { const cmd = requestMemoryList() diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 6c51562..8fe6cef 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -97,6 +97,7 @@ interface UseChatReturn { // Todo 状态 todos: TodoItemSummary[] requestTodoList: () => Command + requestSubAgentTodoList: (subTaskId: string) => Command // 定时任务状态 schedulerJobs: SchedulerJobSummary[] @@ -766,6 +767,10 @@ export function useChat(): UseChatReturn { return { type: 'list_todos' } }, []) + const requestSubAgentTodoList = useCallback((subTaskId: string): Command => { + return { type: 'list_todos', task_id: subTaskId } + }, []) + // 定时任务方法 const requestSchedulerJobList = useCallback((): Command => { return { type: 'list_scheduler_jobs' } @@ -856,6 +861,7 @@ export function useChat(): UseChatReturn { requestSkillList, todos, requestTodoList, + requestSubAgentTodoList, schedulerJobs, sidebarTab, setSidebarTab, diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 1b6b708..ea28ac4 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -390,6 +390,7 @@ export interface ListSkillsCommand { export interface ListTodosCommand { type: 'list_todos' + task_id?: string } export type Command =