feat: 子代理 todo 列表支持 — 进入子代理视图时显示子代理的待办
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
eef0d24dcd
commit
24bbd5f8c9
@ -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<CommandMetadata> {
|
||||
@ -33,25 +33,35 @@ impl CommandHandler for ListTodosCommandHandler {
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_cmd: Command,
|
||||
cmd: Command,
|
||||
ctx: CommandContext,
|
||||
) -> Result<CommandResponse, CommandError> {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +74,11 @@ pub enum Command {
|
||||
/// 列出所有技能
|
||||
ListSkills,
|
||||
/// 列出当前 Todo 列表
|
||||
ListTodos,
|
||||
ListTodos {
|
||||
/// 子代理的 task_id(进入子代理视图时传入)
|
||||
#[serde(default)]
|
||||
task_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<String, String>,
|
||||
store: Arc<SessionStore>,
|
||||
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<crate::storage::TodoRecord> = 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<SubagentCatalog>,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
store: Arc<SessionStore>,
|
||||
}
|
||||
|
||||
impl DefaultSubAgentRuntime {
|
||||
@ -184,6 +238,7 @@ impl DefaultSubAgentRuntime {
|
||||
provider_config: LLMProviderConfig,
|
||||
catalog: Arc<SubagentCatalog>,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
store: Arc<SessionStore>,
|
||||
) -> 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(),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -390,6 +390,7 @@ export interface ListSkillsCommand {
|
||||
|
||||
export interface ListTodosCommand {
|
||||
type: 'list_todos'
|
||||
task_id?: string
|
||||
}
|
||||
|
||||
export type Command =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user