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]
|
#[async_trait]
|
||||||
impl CommandHandler for ListTodosCommandHandler {
|
impl CommandHandler for ListTodosCommandHandler {
|
||||||
fn can_handle(&self, cmd: &Command) -> bool {
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
matches!(cmd, Command::ListTodos)
|
matches!(cmd, Command::ListTodos { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&self) -> Option<CommandMetadata> {
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
@ -33,25 +33,35 @@ impl CommandHandler for ListTodosCommandHandler {
|
|||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
_cmd: Command,
|
cmd: Command,
|
||||||
ctx: CommandContext,
|
ctx: CommandContext,
|
||||||
) -> Result<CommandResponse, CommandError> {
|
) -> Result<CommandResponse, CommandError> {
|
||||||
// scope_key = topic_id.unwrap_or(session_id)
|
let task_id = match cmd {
|
||||||
let scope_key = ctx
|
Command::ListTodos { task_id } => task_id,
|
||||||
.topic_id
|
_ => None,
|
||||||
.as_deref()
|
};
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.or(ctx.session_id.as_deref())
|
// 子代理:scope_key = sub:{parent_session_id}:{task_id}
|
||||||
.ok_or_else(|| {
|
// 主代理:scope_key = topic_id.unwrap_or(session_id)
|
||||||
CommandError::new(
|
let scope_key = if let (Some(tid), Some(parent_sid)) = (task_id.as_deref(), ctx.session_id.as_deref()) {
|
||||||
"MISSING_CONTEXT",
|
format!("sub:{}:{}", parent_sid, tid)
|
||||||
"Cannot list todos: no session_id or topic_id in command context",
|
} 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
|
let records = self
|
||||||
.store
|
.store
|
||||||
.list_todos(scope_key)
|
.list_todos(&scope_key)
|
||||||
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
|
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@ -77,6 +87,6 @@ impl CommandHandler for ListTodosCommandHandler {
|
|||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id)
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
.with_metadata("todos", &todos_json)
|
.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,
|
ListSkills,
|
||||||
/// 列出当前 Todo 列表
|
/// 列出当前 Todo 列表
|
||||||
ListTodos,
|
ListTodos {
|
||||||
|
/// 子代理的 task_id(进入子代理视图时传入)
|
||||||
|
#[serde(default)]
|
||||||
|
task_id: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@ -102,7 +106,7 @@ impl Command {
|
|||||||
Command::UpdateMemory { .. } => "update_memory",
|
Command::UpdateMemory { .. } => "update_memory",
|
||||||
Command::DeleteMemory { .. } => "delete_memory",
|
Command::DeleteMemory { .. } => "delete_memory",
|
||||||
Command::ListSkills => "list_skills",
|
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(),
|
provider_config.clone(),
|
||||||
catalog,
|
catalog,
|
||||||
bus.clone(),
|
bus.clone(),
|
||||||
|
store.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
(factory.with_subagent_runtime(subagent_runtime), task_repository)
|
(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::message::{OutboundMessage, OutboundEventKind};
|
||||||
use crate::bus::MessageBus;
|
use crate::bus::MessageBus;
|
||||||
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
||||||
use crate::storage::ConversationRepository;
|
use crate::storage::{ConversationRepository, SessionStore};
|
||||||
use crate::tools::{ToolContext, ToolRegistry};
|
use crate::tools::{ToolContext, ToolRegistry};
|
||||||
|
|
||||||
use super::error::TaskError;
|
use super::error::TaskError;
|
||||||
@ -105,6 +105,8 @@ struct SubAgentEmitter {
|
|||||||
channel_name: String,
|
channel_name: String,
|
||||||
chat_id: String,
|
chat_id: String,
|
||||||
metadata: HashMap<String, String>,
|
metadata: HashMap<String, String>,
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
sub_session_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[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>,
|
catalog: Arc<SubagentCatalog>,
|
||||||
bus: Option<Arc<MessageBus>>,
|
bus: Option<Arc<MessageBus>>,
|
||||||
|
store: Arc<SessionStore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultSubAgentRuntime {
|
impl DefaultSubAgentRuntime {
|
||||||
@ -184,6 +238,7 @@ impl DefaultSubAgentRuntime {
|
|||||||
provider_config: LLMProviderConfig,
|
provider_config: LLMProviderConfig,
|
||||||
catalog: Arc<SubagentCatalog>,
|
catalog: Arc<SubagentCatalog>,
|
||||||
bus: Option<Arc<MessageBus>>,
|
bus: Option<Arc<MessageBus>>,
|
||||||
|
store: Arc<SessionStore>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
@ -193,6 +248,7 @@ impl DefaultSubAgentRuntime {
|
|||||||
provider_config,
|
provider_config,
|
||||||
catalog,
|
catalog,
|
||||||
bus,
|
bus,
|
||||||
|
store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +314,8 @@ impl DefaultSubAgentRuntime {
|
|||||||
channel_name: session.parent_channel_name.clone(),
|
channel_name: session.parent_channel_name.clone(),
|
||||||
chat_id: session.parent_chat_id.clone(),
|
chat_id: session.parent_chat_id.clone(),
|
||||||
metadata,
|
metadata,
|
||||||
|
store: self.store.clone(),
|
||||||
|
sub_session_id: session.session_id.clone(),
|
||||||
},
|
},
|
||||||
self.conversation_repository.clone(),
|
self.conversation_repository.clone(),
|
||||||
session.session_id.clone(),
|
session.session_id.clone(),
|
||||||
|
|||||||
@ -45,6 +45,7 @@ function App() {
|
|||||||
requestSkillList,
|
requestSkillList,
|
||||||
todos,
|
todos,
|
||||||
requestTodoList,
|
requestTodoList,
|
||||||
|
requestSubAgentTodoList,
|
||||||
// 定时任务
|
// 定时任务
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
@ -342,10 +343,12 @@ function App() {
|
|||||||
const key = `${selectedTopic ?? ''}|${subAgentView?.taskId ?? ''}`
|
const key = `${selectedTopic ?? ''}|${subAgentView?.taskId ?? ''}`
|
||||||
if (key === prevTodoTriggerRef.current) return
|
if (key === prevTodoTriggerRef.current) return
|
||||||
prevTodoTriggerRef.current = key
|
prevTodoTriggerRef.current = key
|
||||||
const todoCmd = requestTodoList()
|
const todoCmd = subAgentView?.taskId
|
||||||
|
? requestSubAgentTodoList(subAgentView.taskId)
|
||||||
|
: requestTodoList()
|
||||||
handleCommand(todoCmd)
|
handleCommand(todoCmd)
|
||||||
sendMessage({ type: 'command', payload: JSON.stringify(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 handleRefreshMemories = useCallback(() => {
|
||||||
const cmd = requestMemoryList()
|
const cmd = requestMemoryList()
|
||||||
|
|||||||
@ -97,6 +97,7 @@ interface UseChatReturn {
|
|||||||
// Todo 状态
|
// Todo 状态
|
||||||
todos: TodoItemSummary[]
|
todos: TodoItemSummary[]
|
||||||
requestTodoList: () => Command
|
requestTodoList: () => Command
|
||||||
|
requestSubAgentTodoList: (subTaskId: string) => Command
|
||||||
|
|
||||||
// 定时任务状态
|
// 定时任务状态
|
||||||
schedulerJobs: SchedulerJobSummary[]
|
schedulerJobs: SchedulerJobSummary[]
|
||||||
@ -766,6 +767,10 @@ export function useChat(): UseChatReturn {
|
|||||||
return { type: 'list_todos' }
|
return { type: 'list_todos' }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const requestSubAgentTodoList = useCallback((subTaskId: string): Command => {
|
||||||
|
return { type: 'list_todos', task_id: subTaskId }
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 定时任务方法
|
// 定时任务方法
|
||||||
const requestSchedulerJobList = useCallback((): Command => {
|
const requestSchedulerJobList = useCallback((): Command => {
|
||||||
return { type: 'list_scheduler_jobs' }
|
return { type: 'list_scheduler_jobs' }
|
||||||
@ -856,6 +861,7 @@ export function useChat(): UseChatReturn {
|
|||||||
requestSkillList,
|
requestSkillList,
|
||||||
todos,
|
todos,
|
||||||
requestTodoList,
|
requestTodoList,
|
||||||
|
requestSubAgentTodoList,
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
setSidebarTab,
|
setSidebarTab,
|
||||||
|
|||||||
@ -390,6 +390,7 @@ export interface ListSkillsCommand {
|
|||||||
|
|
||||||
export interface ListTodosCommand {
|
export interface ListTodosCommand {
|
||||||
type: 'list_todos'
|
type: 'list_todos'
|
||||||
|
task_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Command =
|
export type Command =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user