跳到主要内容

CopilotKitCore — 前端中枢与子系统

本章讲「前端大脑」的解剖:它管哪些状态、切成哪几个器官、怎么对外通知变化。读完你就能在脑子里画出 Core 的内部结构,后面几章都挂在这张图上。

1. 它要解决的小问题

前端需要一个单一的真相源来回答这些问题:现在有哪些 agent?注册了哪些前端工具?页面给了 agent 哪些上下文?谁在监听这些变化?用户发消息时该怎么跑 agent、怎么执行工具?

如果把这些全塞进一个几千行的类,会变成不可测的泥球。CopilotKit 的做法是:CopilotKitCore门面(facade),真正的活切给 6 个 delegate。

2. 顶层结构

CopilotKitCore (门面 / packages/core/src/core/core.ts)
对外:runAgent / addTool / addContext / subscribe / getAgent …(几乎都一行转发)

┌───────┬─────────┼──────────┬───────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼
AgentRegistry RunHandler ContextStore StateManager SuggestionEngine ThreadStoreRegistry
管 agent 跑/续跑+ 存页面 按 run 记 生成对话 持久化线程
/info 拉取 工具循环 上下文 state/消息 建议 注册表

这 6 个 delegate 在构造函数里一次性 new 好并初始化(core.ts:395-406)。注意每个 delegate 的构造都收到 this(Core 本身),这样它们能回访 Core:

// 真实源码 packages/core/src/core/core.ts:395
this.agentRegistry = new AgentRegistry(this);
this.contextStore = new ContextStore(this);
this.suggestionEngine = new SuggestionEngine(this);
this.runHandler = new RunHandler(this);
this.stateManager = new StateManager(this);
this.threadStoreRegistry = new ThreadStoreRegistry(this);

门面方法的典型形态是「一行转发」,例如 addContext / runAgent / getAgent 全是把参数丢给对应 delegate(core.ts:810core.ts:1122core.ts:783)。这意味着:想搞懂某个能力,去看它的 delegate,而不是 Core 本身。

3. delegate 怎么回访 Core:friends 接口

这里有个巧妙处。delegate 经常需要回访 Core 的「半私有」能力——比如 RunHandler 要 notifySubscribers(广播)、要读 properties / headers、要 buildFrontendTools。如果把这些设成 public,公共 API 就被污染了;设成 private 则 delegate 够不着。

CopilotKit 的解法:定义一个内部接口 CopilotKitCoreFriendsAccess,delegate 通过 this.core as unknown as CopilotKitCoreFriendsAccess 拿到这组受控能力(core.ts:293)。

// 真实源码 packages/core/src/core/run-handler.ts:132
private get _internal(): CopilotKitCoreFriendsAccess {
return this.core as unknown as CopilotKitCoreFriendsAccess;
}

这相当于 C++ 的 friend:把「谁能访问什么」显式写成一份契约,而不是靠 public 全开。emitError / notifySubscribers / waitForPendingFrameworkUpdates 等都在这个接口里(core.ts:295-335)。

4. 订阅者模型:Core 怎么对外说「我变了」

Core 不直接知道 React。它对外只做一件事:变化时通知订阅者。订阅者是一组可选回调,定义在 CopilotKitCoreSubscriber(core.ts:120):

回调何时触发
onAgentsChangedagent 列表变了(runtime /info 回来、注册/注销)
onContextChanged上下文增删
onToolExecutionStart / onToolExecutionEnd前端工具开始/结束执行
onError任何错误(带 code 枚举)
onHeadersChanged / onPropertiesChanged配置变更
onSuggestionsChanged对话建议

subscribe() 把回调塞进一个 Set,返回一个 unsubscribe 句柄(core.ts:892)。广播由 notifySubscribers 完成——它对每个订阅者并发调用、单个订阅者抛错只 console.error 不影响其他人(core.ts:486):

// 真实源码 packages/core/src/core/core.ts:490
await Promise.all(
Array.from(this.subscribers).map(async (subscriber) => {
try { await handler(subscriber); }
catch (error) { console.error(errorMessage, error); }
}),
);

React 层(第 4 章)就是靠 subscribe 把 Core 的变化转成 forceUpdate() 重渲染。

5. 一个更细的订阅:订阅单个 agent,带节流

上面的 subscribe 订阅的是「Core 级」变化。还有一个更精细的:subscribeToAgentWithOptions,订阅某个 agent 的消息/状态/run 生命周期(core.ts:913)。它是 useAgent 重渲染的来源。

两个值得记住的设计:

  • 只放行一小撮回调。 只接受 onMessagesChanged / onStateChanged + 四个 run 生命周期回调(白名单 SUBSCRIBE_TO_AGENT_KEYS,core.ts:215),AG-UI 的事件级回调(带 stopPropagation 语义)被刻意排除——因为节流/错误包裹无法安全地中介那种返回值。传了不支持的 key 会被丢弃并 warn(core.ts:1010)。
  • 节流是「leading + trailing 共享窗口」。 流式时 onMessagesChanged 会高频触发;开了 throttleMs 后,第一帧立即出、窗口内的更新合并、窗口末尾补发最后一帧。onMessagesChangedonStateChanged 共用一个 Throttler(core.ts:1063),而 run 生命周期回调永不节流、立即触发(core.ts:1069)。

6. 一个易错点:thread store 的自动注销

Core 在构造时给自己 subscribe 了一个 onAgentsChanged,用来在 agent 消失时清理它的 thread store 和 StateManager 订阅(core.ts:421)。这里藏着一个「先后顺序」的坑,代码里有大段注释解释:

runtime 的 agent 是异步拉来的,第一条 onAgentsChanged({ agents: {} })(空)会先于「真正的 agent 被合并进来」触发。如果不加判断,这条空通知会把某个消费者(如 useThreads)刚注册的 store 直接拆掉。

解法:用 previousAgentIds 记住上一次的 agent 集合,只注销「上次有、这次没有」的 id(core.ts:443-457)。构造时还得用 dev-only agents 给 previousAgentIds 播种,否则首次非空通知会被误判成「新增」(core.ts:418)。

这类「异步通知顺序」的防御散落在整个 codebase,是 CopilotKit 工程量的一大来源。

7. StateManager:为什么要「按 run」记状态

StateManager 订阅每个 agent 的底层 AG-UI 事件,把 state 快照和消息按 agentId → threadId → runId 三层 Map 存起来(state-manager.ts:19)。为什么不直接存「当前 state」?因为生成式 UI 经常要回看某条历史消息当时的状态(例如某个工具调用渲染时依赖那一刻的 agent state)。getStateByRun / getRunIdForMessage 就是给渲染器查这个的(state-manager.ts:160)。

它还处理一个棘手的边界:同一个订阅可能先收到旧 run 的 RUN_FINISHED、又收到新 run 的 RUN_STARTED(pipeline 还没切),此时用 runFinished 标志检测出来、给新 run 生成一个全新 runId,避免两个 run 的状态撞在同一个 key 上(state-manager.ts:87)。

8. 代码地图

主题文件符号
门面 + delegate 装配packages/core/src/core/core.tsCopilotKitCore(构造函数 core.ts:378)
friends 内部接口packages/core/src/core/core.tsCopilotKitCoreFriendsAccess
订阅者类型packages/core/src/core/core.tsCopilotKitCoreSubscribersubscribe
广播packages/core/src/core/core.tsnotifySubscribersemitError
单 agent 订阅 + 节流packages/core/src/core/core.tssubscribeToAgentWithOptionsSUBSCRIBE_TO_AGENT_KEYS
thread store 自动注销packages/core/src/core/core.ts构造里的 onAgentsChangedpreviousAgentIds
错误码枚举packages/core/src/core/core.tsCopilotKitCoreErrorCode
按 run 记状态packages/core/src/core/state-manager.tsStateManagergetStateByRunsubscribeToAgent
上下文存储packages/core/src/core/context-store.tsContextStoregetContextForAgent