CopilotKit Runtime 中 AgentRunner 怎么选:InMemory、Sqlite 与 Intelligence
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
搭建 CopilotKit Runtime 时,每个 runtime 都把 agent 执行和会话持久化委托给一个AgentRunner。runner 负责把POST /agent/:id/run变成一条 AG-UI 事件流,记住 thread 以便POST /agent/:id/connect重新挂接,并在需要时停止运行。选择(或子类化)runner 决定了对话状态存放在哪里:进程内存、本地 SQLite 文件,还是 CopilotKit Intelligence 平台。这篇文章给出三者的适用条件、配置写法和验证方式,覆盖从本地开发到单实例上线再到多实例扩展的选型路径。
AgentRunner 是什么、由谁承担
AgentRunner是一个抽象类,有四个方法,与 runtime 的 HTTP 路由一一对应(详见 AgentRunner and persistence):
import type { Observable } from "rxjs"; import type { BaseEvent } from "@ag-ui/client"; abstract class AgentRunner { // 启动一次 run,返回 AG-UI 事件流 abstract run(request: AgentRunnerRunRequest): Observable<BaseEvent>; // 重新挂接到已有 thread 的流(reconnect / 刷新页面) abstract connect(request: AgentRunnerConnectRequest): Observable<BaseEvent>; // 该 thread 当前是否有活跃的 run? abstract isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean>; // 停止该 thread 上正在进行的 run abstract stop(request: AgentRunnerStopRequest): Promise<boolean | undefined>; }run接收threadId、克隆后的agent、AG-UIRunAgentInput以及persistedInputMessages;connect接收threadId(外加可选的 headers 和joinCode)。thread 背后的存储由 runner 自己持有。
不传runner时,runtime 默认使用InMemoryAgentRunner。runner在CopilotRuntime上只配置一次,对它的每个 agent 生效:同一个 runner 处理所有已注册 agent 的run、connect和stop。
按部署形态选 runner
文档给出的内置 runner 及用途如下:
| Runner | 导入来源 | 适用场景 |
|---|---|---|
InMemoryAgentRunner | @copilotkit/runtime/v2 | v2 默认 runner。thread 保存在进程内存中,用于本地开发、单实例部署,或作为扩展基类 |
SqliteAgentRunner | @copilotkit/sqlite-runner | 第一方文件级持久化 runner。thread 运行记录写入 SQLite 文件,单实例重启后历史仍在。需要better-sqlite3peer 依赖和一个真实(非:memory:)的dbPath |
IntelligenceAgentRunner | @copilotkit/runtime/v2 | 为 CopilotKit Intelligence 提供持久化 thread、跨实例持久化和 threads/history 功能。在 Intelligence runtime 上自动使用 |
TelemetryAgentRunner | @copilotkit/runtime | 旧版包装行为。根 runtime 在开启遥测时会在 runner 外组合遥测层;@copilotkit/runtime/v2不这样做 |
据此可以按三条主路径决策:
- 本地开发或单实例、不要求重启后保留历史:保持默认的
InMemoryAgentRunner。 - 单实例生产部署,要求重启后历史仍在:换成第一方
SqliteAgentRunner。 - 多实例水平扩展:使用 CopilotKit Intelligence 的
IntelligenceAgentRunner,或者自己提供一个基于共享数据存储的 runner(文档建议的子类化路径见后文)。
InMemoryAgentRunner的历史在重启时丢失、进程运行期间有界(见下文边界配置),且不在多实例间共享。这三点就是它不能作为多实例方案的依据。
配置 InMemoryAgentRunner 并给内存历史上界
最小配置(以 Next.js App Router 为例):
import { CopilotRuntime, BuiltInAgent, InMemoryAgentRunner } from "@copilotkit/runtime/v2"; const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }) }, // 显式声明,省略时也是这个默认值: runner: new InMemoryAgentRunner(), });InMemoryAgentRunner把每个 thread 的运行历史放在一个进程级全局 store 中。该 store默认就有上界,长生命周期服务会驱逐旧历史而不是把 Node.js 堆撑爆。当默认值不匹配你的负载时,通过构造函数传入限制:
import { CopilotRuntime, BuiltInAgent, InMemoryAgentRunner } from "@copilotkit/runtime/v2"; const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }) }, runner: new InMemoryAgentRunner({ maxThreads: 200, maxRunsPerThread: 50, maxBytes: 128 * 1024 ** 2, // 128 MiB }), });| 选项 | 默认值 | 约束对象 |
|---|---|---|
maxThreads | 1000 | 保留的不同 thread 数量。超限时整个丢弃最近最少使用的 thread |
maxRunsPerThread | 100 | 每个 thread 保留的 run 数,按最旧优先驱逐。设为Infinity(或0)会关闭该上限——这是唯一的按线程边界(maxBytes只驱逐其他 thread),单个热 thread 会无限增长,文档建议改为一个较大的有限值 |
maxBytes | 536870912(512 MiB) | 所有 thread 保留历史的近似总大小,是主要防线;两个计数是次要的保险上限 |
哪个边界先触发哪个生效。两条规则保证驱逐安全:
- 有活跃或仍在收尾的 run 的 thread 永远不会被驱逐,即使这意味着暂时超限。
maxBytes是跨 thread 的上限:它驱逐其他最近最少使用的 thread,从不裁剪刚结束 run 的 thread。单个热 thread 只受maxRunsPerThread约束,不受maxBytes约束。
驱逐有两种形式,移除的内容和影响范围不同:
- 整 thread 驱逐:整体丢弃最近最少使用的 thread——所有 run、事件和该 thread 的消息快照。
maxThreads和maxBytes都会触发它。被驱逐的 thread 不再出现在GET /threads中,后续connect()也没有任何内容可回放。 - run 上限裁剪(
maxRunsPerThread):只丢弃单个超上限 thread 的最旧 run,thread 本身保留。该 thread 仍出现在GET /threads中、保留原始创建时间,最新消息快照和最新 run 都在——只有被裁剪 run 的事件消失了,后续connect()回放剩余部分。
任一形式触发时都会打一条单行警告,然后保持安静。该警告每个 store 只锁存一次(不是每次驱逐一次),所以频繁的驱逐不会刷爆日志;锁存只在 store 被清空(clearThreads()/POST /threads/clear)后复位,之后最多再触发一次警告。把它当作"正在发生驱逐"的信号,而不是逐条丢弃的审计记录。
驱逐还会削弱该 thread 上的消息去重。run()通过扫描仍持有的 run 来剥离输入中已见过的消息;一旦 thread 超过maxRunsPerThread且最旧 run 被丢弃,只存在于被驱逐 run 中的消息就不再被识别为"已见过",后续connect()或run()可能再次呈现它,客户端可能短暂显示一条已见过的历史消息。这是显示层面的现象,不是数据损坏。如果某个 thread 绝不允许旧消息重新出现,应迁移到持久化 runner,或把maxRunsPerThread调大为有限值——不要设为Infinity(或0),那会移除唯一的按线程边界,让单个长生命周期 thread 增长到堆耗尽。
一个必须注意的坑:这些限制是进程全局的。所有InMemoryAgentRunner共享同一个进程内存 store,限制也一样——最后一个传入限制的 runner 生效于所有内存 thread,会静默覆盖之前 runner 设置的边界。文档明确提示不要指望日志来发现这个问题:覆盖警告只在传入限制的 runner 之后又出现另一个传入不同限制的runner 时才触发;一个用默认值、第二个传自定义限制的常见场景属于首次显式覆盖,不产生日志。一个进程里只配置一套一致的限制。
设置上界防止的是崩溃,不等于持久化。如果不能接受历史丢失,就切换到下面的持久化后端。
配置 SqliteAgentRunner 让历史在重启后保留
SqliteAgentRunner来自@copilotkit/sqlite-runner,把 thread 运行记录持久化到 SQLite 文件,单实例重启后历史仍在。使用前提有两个:安装better-sqlite3peer 依赖(包声明的版本约束是^12.2.0),并提供一个真实的、非:memory:的dbPath:
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2"; import { SqliteAgentRunner } from "@copilotkit/sqlite-runner"; const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }) }, runner: new SqliteAgentRunner({ dbPath: "./data/threads.db" }), });dbPath: "./data/threads.db"是文档给出的示例路径,指向一个可写目录下的真实文件。@copilotkit/sqlite-runner要求 Node>=18。
接入 CopilotKit Intelligence
需要跨水平扩展实例共享历史时,文档给的路径是 Intelligence 的IntelligenceAgentRunner(在 Intelligence runtime 上自动使用),或者自建共享数据存储 runner。接入托管平台的接线步骤见 Connect your runtime to Intelligence:
准备项目 API key。用 CLI:
npx copilotkit login npx copilotkit project selectproject select会把项目级 key 写入.env的CPK_INTELLIGENCE_API_KEY。该 key 是服务端机密,不要加NEXT_PUBLIC_或VITE_前缀。构造
CopilotKitIntelligence客户端并作为intelligence传给 runtime。runtime 从你传入的客户端读取 key,不是从环境读:import { CopilotRuntime, CopilotKitIntelligence, createCopilotRuntimeHandler, } from "@copilotkit/runtime/v2"; const intelligence = new CopilotKitIntelligence({ // apiUrl 和 wsUrl 默认指向托管平台,不要设置 apiKey: process.env.CPK_INTELLIGENCE_API_KEY!, }); const runtime = new CopilotRuntime({ agents, intelligence, // Threads 按用户隔离,否则所有访客共享同一份历史 identifyUser: (request) => ({ id: request.headers.get("x-user-id") ?? "anonymous", name: request.headers.get("x-user-name") ?? "Anonymous", }), }); export const { GET, POST } = createCopilotRuntimeHandler({ runtime });apiKey是唯一必填字段,key 本身限定项目范围,不需要另传组织或 project id。agents是你的 agent 注册对象,需自行替换为实际应用中的注册内容。如果是自托管部署,
apiUrl和wsUrl必须一起覆盖或都不覆盖——API 面和实时面部署在不同主机上,websocket URL 无法从 API URL 推导,只设一个会让另一面仍指向托管主机。传裸 websocket 基址即可:客户端自己追加/runner和/client,并给每个 REST 调用加/api前缀;传apiUrl: ".../api"会产生/api/api/threads这样的双前缀。
验证 runner 是否按预期工作
文档给出的检查方式按 runner 分别对应:
- InMemory / Sqlite:用
GET /threads查看当前保留的 thread。被整 thread 驱逐的 thread 会从这里消失;被maxRunsPerThread裁剪的 thread 仍在列表中且保留原始创建时间。刷新或断线后用POST /agent/:id/connect重新挂接,能回放的内容就是 runner 实际保留的历史——这正是验证持久化与否最直接的行为差异(重启后 InMemory 为空,Sqlite 仍能回放)。 - Intelligence:编译通过和聊天能回复都证明不了 Intelligence 在工作——runtime 在 SSE 模式下不读 key 也能做到这些。文档要求的验证方式是从产品侧确认:打开云托管 dashboard,在应用里发一条消息,应该出现一个 thread;没有出现就说明 runtime 根本没有连到平台,实际仍跑在 SSE 模式。对应的排查表:
| 现象 | 原因 |
|---|---|
| 聊天正常,dashboard 里没有 thread | intelligence没有传给CopilotRuntime,runtime 处于 SSE 模式 |
| 首个请求出现不透明的鉴权错误 | CPK_INTELLIGENCE_API_KEY为空或属于别的项目 |
socket 停在connecting,随后报 "did not settle in time" | 只覆盖了wsUrl,或把它指向了 API 主机 |
请求日志里出现/api/api/... | apiUrl带了/api后缀 |
并发 run 与自定义 runner 的边界
当你的 UX 允许用户快速发送追问、或卡住的 run 需要被顶替时,可以对并发的run()选择 supersede 语义(默认行为是对同一 thread 的并发run()抛Thread already running):
import { InMemoryAgentRunner } from "@copilotkit/runtime/v2"; const runner = new InMemoryAgentRunner({ onConcurrentRun: "supersede" });| 取值 | 行为 |
|---|---|
"throw"(默认) | 同一 thread 上的并发run()抛出Thread already running |
"supersede" | 进行中的 run 被中止(走stop()同一条路径),新 run 开始;被顶替 run 的部分输出被丢弃,不写入历史 |
与内存限制不同,onConcurrentRun是按 runner的,只作用于你传入它的那个 runner。
需要共享数据存储或外部记忆层时,最常见的定制方式是子类化InMemoryAgentRunner,只覆盖需要的方法、其余调用super:
import { InMemoryAgentRunner } from "@copilotkit/runtime/v2"; export class MyRunner extends InMemoryAgentRunner { override run(request: Parameters<InMemoryAgentRunner["run"]>[0]) { // 在此持久化 request.threadId / input,然后委托 return super.run(request); } override connect(request: Parameters<InMemoryAgentRunner["connect"]>[0]) { // 重新挂接前先从自己的存储恢复 thread return super.connect(request); } }这里有一个文档明确要求的边界情况:如果connect()可能被调用在 runner 从未见过的 thread 上(例如首次页面加载时的新 thread id),必须显式处理,否则POST /agent/:id/connect会在用户发消息之前返回 404 或报错。完整的生产级示例可参考仓库中的 AWS AgentCore 集成(AWS AgentCore 文档 指向的/deploy/agentcore页面)。
限制小结
InMemoryAgentRunner的历史重启即失、进程内不跨实例共享;上界配置防止内存耗尽但不提供持久化。- 内存限制是进程全局的,多个传限 runner 时"最后一个生效",且该场景通常无日志。
SqliteAgentRunner解决单实例重启问题,依赖better-sqlite3和真实dbPath;跨实例仍需 Intelligence 或自建共享存储 runner。TelemetryAgentRunner属于@copilotkit/runtime根入口的旧版组合行为,@copilotkit/runtime/v2不做这件事——新项目按 v2 方式配置 runner 和遥测。
下一步若需了解每个 runner 方法对应的路由细节,见 Runtime HTTP endpoints;运行时整体配置见 Copilot Runtime。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考