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 {