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:
parent
d802534abe
commit
606fcbcd29
@ -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) => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -214,6 +214,8 @@ pub enum WsOutbound {
|
||||
topic_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
parent_task_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tool_call_id: Option<String>,
|
||||
},
|
||||
#[serde(rename = "session_established")]
|
||||
SessionEstablished { session_id: String },
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -185,6 +185,7 @@ mod tests {
|
||||
nesting_depth: 0,
|
||||
task_id: None,
|
||||
parent_task_id: None,
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -415,6 +415,7 @@ mod tests {
|
||||
nesting_depth: 0,
|
||||
task_id: None,
|
||||
parent_task_id: None,
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,8 @@ pub struct ToolContext {
|
||||
pub task_id: Option<String>,
|
||||
/// 父任务 ID(仅子/孙智能体有值,用于构建任务层级)
|
||||
pub parent_task_id: Option<String>,
|
||||
/// 当前工具调用的 ID(由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件)
|
||||
pub tool_call_id: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@ -176,6 +176,10 @@ export function useChat(): UseChatReturn {
|
||||
const selectedTopicRef = useRef<string | null>(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<Map<string, string>>(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
|
||||
|
||||
@ -102,6 +102,7 @@ export interface TaskStarted {
|
||||
subagent_type: string
|
||||
topic_id?: string
|
||||
parent_task_id?: string
|
||||
tool_call_id?: string
|
||||
}
|
||||
|
||||
export interface SessionEstablished {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user