feat(agent): 注入并传递 tool_call_id 实现任务与工具调用精准关联

- 在 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 进行优先匹配,提升用户交互体验
This commit is contained in:
oudecheng 2026-06-22 10:22:36 +08:00
parent d802534abe
commit 606fcbcd29
11 changed files with 90 additions and 3 deletions

View File

@ -1416,7 +1416,14 @@ impl AgentLoop {
}; };
match tool 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 .await
{ {
Ok(result) => { Ok(result) => {

View File

@ -82,6 +82,7 @@ impl AgentFactory {
nesting_depth: 0, nesting_depth: 0,
task_id: None, task_id: None,
parent_task_id: None, parent_task_id: None,
tool_call_id: None,
}); });
// 如果有取消信号接收端,注入 Agent // 如果有取消信号接收端,注入 Agent
if let Some(token) = request.cancel_token { if let Some(token) = request.cancel_token {

View File

@ -681,6 +681,7 @@ async fn send_topic_history(
subagent_type: task.subagent_type.clone(), subagent_type: task.subagent_type.clone(),
topic_id: Some(topic_id.to_string()), topic_id: Some(topic_id.to_string()),
parent_task_id: None, parent_task_id: None,
tool_call_id: None,
}) })
.await; .await;
} }

View File

@ -214,6 +214,8 @@ pub enum WsOutbound {
topic_id: Option<String>, topic_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
parent_task_id: Option<String>, parent_task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
}, },
#[serde(rename = "session_established")] #[serde(rename = "session_established")]
SessionEstablished { session_id: String }, SessionEstablished { session_id: String },

View File

@ -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(), subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(),
topic_id: message.metadata.get("topic_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(),
parent_task_id: message.metadata.get("parent_task_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 { OutboundEventKind::StreamDelta => vec![WsOutbound::StreamDelta {
id: message.tool_call_id.clone().unwrap_or_default(), id: message.tool_call_id.clone().unwrap_or_default(),

View File

@ -351,6 +351,7 @@ impl DefaultSubAgentRuntime {
nesting_depth: parent_nesting_depth + 1, nesting_depth: parent_nesting_depth + 1,
task_id: Some(session.id.clone()), task_id: Some(session.id.clone()),
parent_task_id, parent_task_id,
tool_call_id: None,
}); });
// 如果有 MessageBus附加实时广播 emitter // 如果有 MessageBus附加实时广播 emitter
@ -543,6 +544,11 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
metadata.insert("parent_task_id".to_string(), ptid.clone()); 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 { let event = OutboundMessage {
channel: session.parent_channel_name.clone(), channel: session.parent_channel_name.clone(),
chat_id: session.parent_chat_id.clone(), chat_id: session.parent_chat_id.clone(),

View File

@ -185,6 +185,7 @@ mod tests {
nesting_depth: 0, nesting_depth: 0,
task_id: None, task_id: None,
parent_task_id: None, parent_task_id: None,
tool_call_id: None,
} }
} }

View File

@ -415,6 +415,7 @@ mod tests {
nesting_depth: 0, nesting_depth: 0,
task_id: None, task_id: None,
parent_task_id: None, parent_task_id: None,
tool_call_id: None,
} }
} }

View File

@ -24,6 +24,8 @@ pub struct ToolContext {
pub task_id: Option<String>, pub task_id: Option<String>,
/// 父任务 ID仅子/孙智能体有值,用于构建任务层级) /// 父任务 ID仅子/孙智能体有值,用于构建任务层级)
pub parent_task_id: Option<String>, pub parent_task_id: Option<String>,
/// 当前工具调用的 ID由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件)
pub tool_call_id: Option<String>,
} }
#[async_trait] #[async_trait]

View File

@ -176,6 +176,10 @@ export function useChat(): UseChatReturn {
const selectedTopicRef = useRef<string | null>(null) const selectedTopicRef = useRef<string | null>(null)
const pendingNewTopicRef = useRef(false) 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<Map<string, string>>(new Map())
// Ref to send commands from within handleServerMessage (set by App.tsx) // Ref to send commands from within handleServerMessage (set by App.tsx)
const sendMessageRef = useRef<((msg: WsInbound) => boolean) | null>(null) const sendMessageRef = useRef<((msg: WsInbound) => boolean) | null>(null)
const setSendMessage = useCallback((fn: (msg: WsInbound) => boolean) => { const setSendMessage = useCallback((fn: (msg: WsInbound) => boolean) => {
@ -391,14 +395,30 @@ export function useChat(): UseChatReturn {
if (message.type === 'task_started') { if (message.type === 'task_started') {
const msg = message as TaskStarted const msg = message as TaskStarted
if (msg.parent_task_id === currentSubAgentView.taskId) { if (msg.parent_task_id === currentSubAgentView.taskId) {
let matched = false
setSubAgentStack((prev) => { setSubAgentStack((prev) => {
if (prev.length === 0) return prev if (prev.length === 0) return prev
const top = prev[prev.length - 1] const top = prev[prev.length - 1]
const updatedMessages = [...top.messages] 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--) { for (let i = updatedMessages.length - 1; i >= 0; i--) {
const m = updatedMessages[i] const m = updatedMessages[i]
if (m.type === 'tool_call' && m.toolName === 'task' && !m.navigateToTaskId) { if (m.type === 'tool_call' && m.toolName === 'task' && !m.navigateToTaskId) {
updatedMessages[i] = { ...m, navigateToTaskId: msg.task_id } updatedMessages[i] = { ...m, navigateToTaskId: msg.task_id }
matched = true
break break
} }
} }
@ -406,6 +426,11 @@ export function useChat(): UseChatReturn {
newStack[newStack.length - 1] = { ...top, messages: updatedMessages } newStack[newStack.length - 1] = { ...top, messages: updatedMessages }
return newStack 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 return
} }
} }
@ -416,6 +441,33 @@ export function useChat(): UseChatReturn {
const msgSubagentTaskId = getSubagentTaskId(message) const msgSubagentTaskId = getSubagentTaskId(message)
if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) { if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) {
appendToSubAgentViewMessage(message) 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 完成后自动刷新待办列表 // 子代理 todo_write 完成后自动刷新待办列表
if (message.type === 'tool_result' && (message as ToolResult).tool_name === 'todo_write') { if (message.type === 'tool_result' && (message as ToolResult).tool_name === 'todo_write') {
const refreshCmd = requestSubAgentTodoList(currentSubAgentView.taskId) const refreshCmd = requestSubAgentTodoList(currentSubAgentView.taskId)
@ -459,10 +511,22 @@ export function useChat(): UseChatReturn {
// 设置 navigateToTaskId让用户可以点击查看实时进度 // 设置 navigateToTaskId让用户可以点击查看实时进度
setMessages((prev) => { 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--) { for (let i = prev.length - 1; i >= 0; i--) {
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].navigateToTaskId) { 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] const updated = [...prev]
updated[i] = { ...updated[i], navigateToTaskId: msg.task_id } updated[i] = { ...updated[i], navigateToTaskId: msg.task_id }
return updated return updated

View File

@ -102,6 +102,7 @@ export interface TaskStarted {
subagent_type: string subagent_type: string
topic_id?: string topic_id?: string
parent_task_id?: string parent_task_id?: string
tool_call_id?: string
} }
export interface SessionEstablished { export interface SessionEstablished {