From 606fcbcd290816b8ffeeb0a6c44434f345f81372 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Mon, 22 Jun 2026 10:22:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=B3=A8=E5=85=A5=E5=B9=B6?= =?UTF-8?q?=E4=BC=A0=E9=80=92=20tool=5Fcall=5Fid=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B8=8E=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=B2=BE=E5=87=86=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 agent_loop 执行上下文中注入 tool_call_id,确保执行时传递该字段 - 在 agent_factory、ws、todo_read、todo_write 等多处构造对象时添加 tool_call_id 字段初始化 - 扩展协议定义及序列化,支持 tool_call_id 字段传递 - 在工具调用任务运行时传递 tool_call_id,便于事件追踪和层级关联 - 在前端 useChat hook 增加 tool_call_id 关联逻辑,实现 task_started 事件精准匹配和跳转 - 增加 pendingTaskNavs 缓存处理,解决 task_started 事件先于 tool_call 到达的顺序问题 - 调整消息渲染逻辑,根据 tool_call_id 进行优先匹配,提升用户交互体验 --- src/agent/agent_loop.rs | 9 ++++- src/gateway/agent_factory.rs | 1 + src/gateway/ws.rs | 1 + src/protocol/mod.rs | 2 ++ src/protocol/ws_adapter.rs | 1 + src/tools/task/runtime.rs | 6 ++++ src/tools/todo_read.rs | 1 + src/tools/todo_write.rs | 1 + src/tools/traits.rs | 2 ++ web/src/hooks/useChat.ts | 68 ++++++++++++++++++++++++++++++++++-- web/src/types/protocol.ts | 1 + 11 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 21b866c..5c0ee7f 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -1416,7 +1416,14 @@ impl AgentLoop { }; match tool - .execute_with_context(&self.tool_context, normalized_arguments.clone()) + .execute_with_context( + &{ + let mut ctx = self.tool_context.clone(); + ctx.tool_call_id = Some(tool_call.id.clone()); + ctx + }, + normalized_arguments.clone(), + ) .await { Ok(result) => { diff --git a/src/gateway/agent_factory.rs b/src/gateway/agent_factory.rs index cabd8ef..43922fe 100644 --- a/src/gateway/agent_factory.rs +++ b/src/gateway/agent_factory.rs @@ -82,6 +82,7 @@ impl AgentFactory { nesting_depth: 0, task_id: None, parent_task_id: None, + tool_call_id: None, }); // 如果有取消信号接收端,注入 Agent if let Some(token) = request.cancel_token { diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 7390f64..84ca3d8 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -681,6 +681,7 @@ async fn send_topic_history( subagent_type: task.subagent_type.clone(), topic_id: Some(topic_id.to_string()), parent_task_id: None, + tool_call_id: None, }) .await; } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index a640957..a467e29 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -214,6 +214,8 @@ pub enum WsOutbound { topic_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] parent_task_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + tool_call_id: Option, }, #[serde(rename = "session_established")] SessionEstablished { session_id: String }, diff --git a/src/protocol/ws_adapter.rs b/src/protocol/ws_adapter.rs index 043c9d4..b9743d5 100644 --- a/src/protocol/ws_adapter.rs +++ b/src/protocol/ws_adapter.rs @@ -174,6 +174,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(), topic_id: message.metadata.get("topic_id").cloned(), parent_task_id: message.metadata.get("parent_task_id").cloned(), + tool_call_id: message.metadata.get("tool_call_id").cloned(), }], OutboundEventKind::StreamDelta => vec![WsOutbound::StreamDelta { id: message.tool_call_id.clone().unwrap_or_default(), diff --git a/src/tools/task/runtime.rs b/src/tools/task/runtime.rs index 5269b4f..9e836ec 100644 --- a/src/tools/task/runtime.rs +++ b/src/tools/task/runtime.rs @@ -351,6 +351,7 @@ impl DefaultSubAgentRuntime { nesting_depth: parent_nesting_depth + 1, task_id: Some(session.id.clone()), parent_task_id, + tool_call_id: None, }); // 如果有 MessageBus,附加实时广播 emitter @@ -543,6 +544,11 @@ impl SubAgentRuntime for DefaultSubAgentRuntime { metadata.insert("parent_task_id".to_string(), ptid.clone()); } + // 传递 tool_call_id,前端据此精确匹配创建此任务的 tool_call + if let Some(ref tcid) = parent_context.tool_call_id { + metadata.insert("tool_call_id".to_string(), tcid.clone()); + } + let event = OutboundMessage { channel: session.parent_channel_name.clone(), chat_id: session.parent_chat_id.clone(), diff --git a/src/tools/todo_read.rs b/src/tools/todo_read.rs index 1219568..018a6d3 100644 --- a/src/tools/todo_read.rs +++ b/src/tools/todo_read.rs @@ -185,6 +185,7 @@ mod tests { nesting_depth: 0, task_id: None, parent_task_id: None, + tool_call_id: None, } } diff --git a/src/tools/todo_write.rs b/src/tools/todo_write.rs index 8e0608c..fca31bf 100644 --- a/src/tools/todo_write.rs +++ b/src/tools/todo_write.rs @@ -415,6 +415,7 @@ mod tests { nesting_depth: 0, task_id: None, parent_task_id: None, + tool_call_id: None, } } diff --git a/src/tools/traits.rs b/src/tools/traits.rs index 3483050..5bf0457 100644 --- a/src/tools/traits.rs +++ b/src/tools/traits.rs @@ -24,6 +24,8 @@ pub struct ToolContext { pub task_id: Option, /// 父任务 ID(仅子/孙智能体有值,用于构建任务层级) pub parent_task_id: Option, + /// 当前工具调用的 ID(由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件) + pub tool_call_id: Option, } #[async_trait] diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 69f5614..6dfae90 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -176,6 +176,10 @@ export function useChat(): UseChatReturn { const selectedTopicRef = useRef(null) const pendingNewTopicRef = useRef(false) + // Pending task navigations: tool_call_id -> task_id + // Used when task_started arrives before the tool_call is in the sub-agent view + const pendingTaskNavsRef = useRef>(new Map()) + // Ref to send commands from within handleServerMessage (set by App.tsx) const sendMessageRef = useRef<((msg: WsInbound) => boolean) | null>(null) const setSendMessage = useCallback((fn: (msg: WsInbound) => boolean) => { @@ -391,14 +395,30 @@ export function useChat(): UseChatReturn { if (message.type === 'task_started') { const msg = message as TaskStarted if (msg.parent_task_id === currentSubAgentView.taskId) { + let matched = false setSubAgentStack((prev) => { if (prev.length === 0) return prev const top = prev[prev.length - 1] const updatedMessages = [...top.messages] + + // 优先:按 tool_call_id 精确匹配 + if (msg.tool_call_id) { + const idx = updatedMessages.findIndex(m => + m.toolCallId === msg.tool_call_id && m.type === 'tool_call' && m.toolName === 'task') + if (idx >= 0 && !updatedMessages[idx].navigateToTaskId) { + updatedMessages[idx] = { ...updatedMessages[idx], navigateToTaskId: msg.task_id } + matched = true + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: updatedMessages } + return newStack + } + } + // 回退:backward-search (兼容无 tool_call_id 的旧版本) for (let i = updatedMessages.length - 1; i >= 0; i--) { const m = updatedMessages[i] if (m.type === 'tool_call' && m.toolName === 'task' && !m.navigateToTaskId) { updatedMessages[i] = { ...m, navigateToTaskId: msg.task_id } + matched = true break } } @@ -406,6 +426,11 @@ export function useChat(): UseChatReturn { newStack[newStack.length - 1] = { ...top, messages: updatedMessages } return newStack }) + if (!matched) { + // tool_call 尚未到达,存储 pending navigation 等后续 tool_call 到达时回填 + const key = msg.tool_call_id || `fallback:${msg.task_id}` + pendingTaskNavsRef.current.set(key, msg.task_id) + } return } } @@ -416,6 +441,33 @@ export function useChat(): UseChatReturn { const msgSubagentTaskId = getSubagentTaskId(message) if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) { appendToSubAgentViewMessage(message) + + // 检查 pending navigation:当 task tool_call 到达时,回填之前未匹配的 navigateToTaskId + if (message.type === 'tool_call') { + const tc = message as ToolCall + if (tc.tool_name === 'task' && tc.tool_call_id) { + const key = tc.tool_call_id + const pendingTaskId = pendingTaskNavsRef.current.get(key) + if (pendingTaskId) { + pendingTaskNavsRef.current.delete(key) + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const updatedMessages = [...top.messages] + const idx = updatedMessages.findIndex(m => + m.toolCallId === tc.tool_call_id && m.type === 'tool_call') + if (idx >= 0) { + updatedMessages[idx] = { ...updatedMessages[idx], navigateToTaskId: pendingTaskId } + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: updatedMessages } + return newStack + } + return prev + }) + } + } + } + // 子代理 todo_write 完成后自动刷新待办列表 if (message.type === 'tool_result' && (message as ToolResult).tool_name === 'todo_write') { const refreshCmd = requestSubAgentTodoList(currentSubAgentView.taskId) @@ -459,10 +511,22 @@ export function useChat(): UseChatReturn { // 设置 navigateToTaskId,让用户可以点击查看实时进度 setMessages((prev) => { - console.log('[useChat] task_started searching messages for task tool_call, total messages:', prev.length) + console.log('[useChat] task_started searching messages for task tool_call, total messages:', prev.length, 'tool_call_id:', msg.tool_call_id) + // 优先:按 tool_call_id 精确匹配 + if (msg.tool_call_id) { + const idx = prev.findIndex(m => + m.toolCallId === msg.tool_call_id && m.type === 'tool_call' && m.toolName === 'task') + if (idx >= 0 && !prev[idx].navigateToTaskId) { + console.log('[useChat] task_started EXACT MATCH at index', idx, 'task_id:', msg.task_id) + const updated = [...prev] + updated[idx] = { ...updated[idx], navigateToTaskId: msg.task_id } + return updated + } + } + // 回退:backward-search (兼容无 tool_call_id 的旧版本) for (let i = prev.length - 1; i >= 0; i--) { if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].navigateToTaskId) { - console.log('[useChat] task_started SET navigateToTaskId at index', i, 'task_id:', msg.task_id) + console.log('[useChat] task_started BACKWARD MATCH at index', i, 'task_id:', msg.task_id) const updated = [...prev] updated[i] = { ...updated[i], navigateToTaskId: msg.task_id } return updated diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 776cd3b..b7eca3f 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -102,6 +102,7 @@ export interface TaskStarted { subagent_type: string topic_id?: string parent_task_id?: string + tool_call_id?: string } export interface SessionEstablished {