数据截至 (上游 commit 63392db20d55)
工具执行循环 — agent 调用前端函数的核心机制
这是 CopilotKit 最核心的机制,也是「agent-ui」这个 area 的灵魂:agent 说要调一个函数 → 这个函数在用户浏览器里跑 → 结果回灌给 agent → agent 接着想。本章把这条循环一步步拆开。
1. 它要解决的小问题
普通的「后端工具」由 agent 框架自己执行。但 CopilotKit 的卖点是前端工具:工具的 handler 要在用户的浏览器里跑(因为它要改 DOM、读页面状态、调用只有前端有 token 的 API)。
于是问题变成:agent 在远端决定「调用 setTheme(dark)」,这个决定通过 SSE 流回前端,前端得
- 认出「这是一次工具调用」,
- 找到本地注册的同名工具,
- 跑它的 handler,
- 把返回值变成一条
tool消息塞进对话, - 再把整个对话发回 agent,让它基于结果继续生成。
第 5 步是精髓:工具结果必须回灌,否则 agent 永远不知道它的工具调用成功了没。这整套就是 RunHandler 干的(run-handler.ts)。
2. 直觉:一次「问答 + 干活」的来回
用一个简化模型先建立直觉(不是源码,只为讲清楚循环形状):
# 示意,非源码——把核心循环的形状演出来
def run_until_no_more_tools(agent):
while True:
new_messages = agent.run() # 发请求,收 SSE,累积消息
needs_followup = False
for msg in new_messages:
for call in msg.tool_calls:
if not already_has_result(call):
result = run_frontend_handler(call) # 在浏览器里跑
agent.insert_tool_message(call, result)
needs_followup = True
if not needs_followup:
break # 没有工具要跑了,结束
# 否则:带着工具结果再跑一次,让 agent 看到结果
真实实现不是 while,而是递归调用 runAgent——但形状一样。重点看:有工具跑过 → 必须再跑一轮。
3. 主线:runAgent → processAgentResult
core.runAgent({ agent }) core.ts:1122 → run-handler.ts:289
│
├─ applyHeadersToAgent / detachActiveRun (清场)
├─ 顶层才建 AbortController + 拦截 agent.abortRun()
├─ agent.runAgent({ tools, context, forwardedProps }) ← 发 HTTP,收 SSE
│ tools = buildFrontendTools(agentId)
│ context = getContextForAgent(agentId)
└─ processAgentResult({ runAgentResult, agent }) run-handler.ts:383
扫 newMessages 里每个 assistant 的 toolCalls:
├─ 已有对应 tool 结果? → 跳过
├─ 查到具名工具 → executeSpecificTool
└─ 没查到但有 "*" → executeWildcardTool
若任一工具 needsFollowUp 且未中止:
waitForPendingFrameworkUpdates() ← 让一拍给 React
return runAgent({ agent }) ← 递归续跑
发请求时打包的三样东西(run-handler.ts:528-541):前端工具清单、按当前 agent 过滤后的上下文、forwardedProps(新版还可在续跑之外把 runId 钉在 wire 上,保证外部 tracing 看到的是同一个逻辑 run)。工具清单由 buildFrontendTools 生成——它把注册的工具转成 AG-UI 的 Tool 形状(name/description/JSON-schema 参数),并过滤掉 available === false 的和「绑定了别的 agentId」的(run-handler.ts:1236)。