跳到主要内容

ProxiedCopilotRuntimeAgent — 远端 agent 在前端的替身

本章讲「前端怎么跟跑在别处的 agent 说话」。Core 手里拿到的 agent 对象,其实是个替身——你调它的 runAgent(),它在背后发 HTTP、收 SSE。读完你会知道一次 agent 运行是怎么变成网络请求的。

1. 它要解决的小问题

Core 的运行循环(第 3 章)只想跟一个统一的 AbstractAgent 接口打交道:agent.runAgent(...)agent.messagesagent.subscribe(...)。但真正的 agent 跑在服务端的 CopilotRuntime 里。需要一个前端对象,长得像 agent、但把每次调用翻译成网络请求,并把流回来的事件累积成 messages

这个对象就是 ProxiedCopilotRuntimeAgent(agent.ts:100),它继承自外部依赖 @ag-ui/clientHttpAgent

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):

transportrun 的 URL / 方式适用
restPOST {runtimeUrl}/agent/<id>/run,SSE 回标准 Express/Hono 多路由 runtime
singlePOST {runtimeUrl},body 是 { method, params, body } 信封只暴露一个端点的环境(如某些 serverless)
auto先试 REST 的 GET /info,2xx 就用 rest,否则回落 single默认,自动探测

run URL 在构造时就根据 transport 定死(agent.ts:122),因为 REST 的 run URL 要烤进父类 HttpAgentauto 的探测在 fetchRuntimeInfoAutoDetect 里:只把 2xx 当成有效 REST,404/405/500 都回落单端点(agent.ts:534)。探测成功后会把 transport 改写成具体值,后续请求直接走它。

单端点模式靠 createSingleRouteRequestInit 把原本的 REST body 包进一个 { method, params, body } 信封再 POST(agent.ts:575)。

4. 一次 run 在替身里走哪条路

@ag-ui/client 的基类会调替身覆写的 run(input)(agent.ts:346)。它按运行模式分叉:

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:621)。

难点是:UI 订阅的是替身,但消息实际在委托上累积。解法是 connectAgent 里架一座「桥」(agent.ts:285):

// 真实源码 packages/core/src/agent.ts:285(节选)
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:281 详述)。运行结束在 finally 里把桥拆掉、强制 isRunning = false 兜底(agent.ts:325)。

委托的同步统一走 syncDelegate:把替身的 agentId(用的是路由 id)、threadIdmessagesstate、headers 全拷给委托(agent.ts:638)。

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:1117)。对替身,abortRun额外发一个 stop HTTP 请求告诉 runtime 停下:REST 是 POST /agent/<id>/stop/<threadId>,单端点是 { method: "agent/stop" } 信封(agent.ts:187)。Intelligence 模式则转给委托的 abortRun 再 detach 自己的 pipeline。

8. 代码地图

主题文件符号
替身本体packages/core/src/agent.tsProxiedCopilotRuntimeAgent
run 分叉packages/core/src/agent.tsrun#runViaHttp#runViaDelegate
单端点信封packages/core/src/agent.tscreateSingleRouteRequestInit
auto 探测packages/core/src/agent.tsfetchRuntimeInfoAutoDetect
路由 idpackages/core/src/agent.tsroutedAgentIdruntimeAgentId
Intelligence 桥接packages/core/src/agent.tsconnectAgent(bridge)、syncDelegatecreateIntelligenceDelegate
中止packages/core/src/agent.tsabortRun
造替身 / 复用packages/core/src/core/agent-registry.tsupdateRuntimeConnectionregisterProxiedAgent
拉 /infopackages/core/src/core/agent-registry.tsfetchRuntimeInfofetchRuntimeInfoAutoDetect