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
|
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) => {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user