数据截至 (上游 commit 63392db20d55)
ProxiedCopilotRuntimeAgent — 远端 agent 在前端的替身
本章讲「前端怎么跟跑在别处的 agent 说话」。Core 手里拿到的 agent 对象,其实是个替身——你调它的
runAgent(),它在背后发 HTTP、收 SSE。读完你会知道一次 agent 运行是怎么变成网络请求的。
1. 它要解决的小问题
Core 的运行循环(第 3 章)只想跟一个统一的 AbstractAgent 接口打交道:agent.runAgent(...)、agent.messages、agent.subscribe(...)。但真正的 agent 跑在服务端的 CopilotRuntime 里。需要一个前端对象,长得像 agent、但把每次调用翻译成网络请求,并把流回来的事件累积成 messages。
这个对象就是 ProxiedCopilotRuntimeAgent(agent.ts:100),它继承自外部依赖 @ag-ui/client 的 HttpAgent。
2. 谁来造它:AgentRegistry 与 /info 握手
替身不是你 new 的,是 AgentRegistry 在连接 runtime 时造的。流程:
setRuntimeUrl(url)
└─> updateRuntimeConnection() agent-registry.ts:381
├─ fetchRuntimeInfo() ──HTTP──> runtime 的 /info
│ 回:{ agents: {…}, version, mode, intelligence, … }
└─ 对每个 agent id:
若已有同 id 实例 → 复用(只重涂 headers/credentials)
否则 → new ProxiedCopilotRuntimeAgent(...)
最后 notifyAgentsChanged() → 触发 onAgentsChanged
复用已有实例是关键设计(agent-registry.ts:444)。每次重连(header 变了、transport 变了、/info 重新结算)都会重跑这段;若每次都 new 新实例,会丢掉这个 agent 攒的 messages/threadId/订阅,而且下游(如 useAgent 的 memo)按实例身份做 key,换实例 = 把已渲染的对话卸载重挂。所以同 id 一律复用,只重新应用 registry 拥有的东西(headers/credentials):
// 真实源码 packages/core/src/core/agent-registry.ts:450
if (existing instanceof ProxiedCopilotRuntimeAgent) {
this.applyHeadersToAgent(existing);
this.applyCredentialsToAgent(existing);
return [id, existing];
}
3. 三种传输:rest / single / auto
替身支持三种把请求发出去的方式,由 transport 字段决定(agent.ts:108):
| transport | run 的 URL / 方式 | 适用 |
|---|---|---|
rest | POST {runtimeUrl}/agent/<id>/run,SSE 回 | 标准 Express/Hono 多路由 runtime |
single | POST {runtimeUrl},body 是 { method, params, body } 信封 | 只暴露一个端点的环境(如某些 serverless) |
auto | 先试 REST 的 GET /info,2xx 就用 rest,否则回落 single | 默认,自动探测 |
run URL 在构造时就根据 transport 定死(agent.ts:122),因为 REST 的 run URL 要烤进父类 HttpAgent。auto 的探测在 fetchRuntimeInfoAutoDetect 里:只把 2xx 当成有效 REST,404/405/500 都回落单端点(agent.ts:574)。探测成功后会把 transport 改写成具体值,后续请求直接走它。
单端点模式靠 createSingleRouteRequestInit 把原本的 REST body 包进一个 { method, params, body } 信封再 POST(agent.ts:615)。
4. 一次 run 在替身里走哪条路
@ag-ui/client 的基类会调替身覆写的 run(input)(agent.ts:363)。它按运行模式分叉:
run(input)
├─ runtimeMode === "intelligence" ? → #runViaDelegate (委托给 WebSocket 代理)
└─ 否则 → #runViaHttp
├─ single → 包信封 POST 单端点
└─ rest → super.run(input)(父类 HttpAgent 发 /agent/<id>/run)
两条 HTTP 路径都用 runHttpRequest + transformHttpEventStream(来自 @ag-ui/client)把 SSE 字节流转成 AG-UI 事件 Observable,再包一层 withAbortErrorHandling——把 Zod 校验错误和 Abort 错误吞成空流(agent.ts:67),这样导航中断、流中途取消不会炸成未捕获异常。
5. Intelligence 模式:委托 + 桥接
当 runtime 报告自己是 Intelligence 模式(CopilotKit 的企业级实时能力,走 WebSocket),替身不自己发 HTTP,而是创建一个委托 agent(IntelligenceAgent,走 intelligence.wsUrl)并把活全转给它(agent.ts:661)。
难点是:UI 订阅的是替身,但消息实际在委托上累积。解法是 connectAgent 里架一座「桥」(agent.ts:293):
// 真实源码 packages/core/src/agent.ts:293(节选)
const bridgeSub = delegate.subscribe({
onMessagesChanged: () => { this.setMessages([...delegate.messages]); },
onStateChanged: () => { this.setState({ ...delegate.state }); },
onRunInitialized: () => { this.isRunning = true; },
onRunFinalized: () => { this.isRunning = false; },
});
桥接订阅先于转发给 UI 的订 阅注册,这样替身的 messages 在 UI 重渲染读它之前就已同步好——否则 UI 会读到滞后的替身消息(注释 agent.ts:289 详述)。运行结束在 finally 里把桥拆掉、强制 isRunning = false 兜底(agent.ts:333)。
委托的同步统一走 syncDelegate:把替身的 agentId(用的是路由 id)、threadId、messages、state、headers 全拷给委托(agent.ts:678)。
6. 路由 id vs 注册 id:一个细节
替身有两个 id 概念(agent.ts:97):
agentId—— 本地注册 id,Core 的订阅簿记、useAgent都用它。runtimeAgentId—— 路由 id,出站 HTTP 请求实际打到 runtime 上的哪个 agent。
大多数时候两者相同。但 registerProxiedAgent(agent-registry.ts:249)允许「把多个前端 agent 挂到同一个 runtime agent」——比如两个聊天窗 chat-1/chat-2 都代理到 default。出站用 routedAgentId()(agent.ts:162),它优先返回 runtimeAgentId,两者都没有就抛错(防止生成 /agent//run 这种畸形 URL)。runtimeAgentId 故意设成 readonly,因为 REST run URL 构造时已定死,事后改它会让 URL 和 routedAgentId() 脱节。
7. 中止:stopAgent 发的是一个独立请求
点「停止」时,stopAgent 既中止本地运行循环,又调 agent.abortRun()(core.ts:1314)。对替身,abortRun 会额外发一个 stop HTTP 请求告诉 runtime 停下:REST 是 POST /agent/<id>/stop/<threadId>,单端点是 { method: "agent/stop" } 信封(agent.ts:195)。Intelligence 模式则转给委托的 abortRun 再 detach 自己的 pipeline。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 替身本体 | packages/core/src/agent.ts | ProxiedCopilotRuntimeAgent |
| run 分叉 | packages/core/src/agent.ts | run、#runViaHttp、#runViaDelegate |
| 单端点信封 | packages/core/src/agent.ts | createSingleRouteRequestInit |
| auto 探测 | packages/core/src/agent.ts | fetchRuntimeInfoAutoDetect |
| 路由 id | packages/core/src/agent.ts | routedAgentId、runtimeAgentId |
| Intelligence 桥接 | packages/core/src/agent.ts | connectAgent(bridge)、syncDelegate、createIntelligenceDelegate |
| 中止 | packages/core/src/agent.ts | abortRun |
| 造替身 / 复用 | packages/core/src/core/agent-registry.ts | updateRuntimeConnection、registerProxiedAgent |
| 拉 /info | packages/core/src/core/agent-registry.ts | fetchRuntimeInfo、fetchRuntimeInfoAutoDetect |