CopilotKit React 的 Agent 访问层:useAgent 与 useAgentContext 完整接入与避坑指南
CopilotKit React 的 Agent 访问层useAgent 与 useAgentContext 完整接入与避坑指南【免费下载链接】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 仓库中packages/react-core/skills/react-core/references/agent-access.md的实战要点结合copilotkit/react-core/v2的源码实现系统讲解两个互补的 Agent 访问 HookuseAgent命令式获取 Agent 实例订阅消息、状态与运行状态变更与useAgentContext声明式将应用状态推送给每一次 Agent 运行。读完本文你将掌握 Agent 的注册解析、线程隔离、消息发送、运行中止与上下文注入的完整写法并避开文档与源码中标注的 7 类高频陷阱。useAgent与useAgentContext建立在copilotkit/provider-setup之上Provider 通过GET {runtimeUrl}/info把运行时登记的 Agent 同步进同一份注册表useAgent从这份注册表中读取 Agent而useAgentContext注入的上下文则会被runAgent携带到每一次运行中。Setup最小可运行的 Agent 接入示例所有 v2 的 Hook 都必须从copilotkit/react-core/v2子路径导入包根路径导入的是 v1 入口无法与 v2 Hook 协同且所在组件必须带有use client指令并渲染在CopilotKitProvider 内部。use client; import { useAgent, useAgentContext, UseAgentUpdate, } from copilotkit/react-core/v2; import { useMemo } from react; export function ChatDriver({ route, userId, }: { route: string; userId: string; }) { const { agent } useAgent({ agentId: default, threadId: main, updates: [ UseAgentUpdate.OnMessagesChanged, UseAgentUpdate.OnRunStatusChanged, ], throttleMs: 100, }); const context useMemo(() ({ route, userId }), [route, userId]); useAgentContext({ description: app context, value: context }); return ( div {agent.isRunning ? …thinking : idle} — {agent.messages.length}{ } messages /div ); }这个示例同时覆盖了两个 Hook 的核心用法useAgent以agentId: default绑定注册表中的共享 Agent并订阅两类更新useAgentContext把{ route, userId }序列化后注册到全局上下文存储所有 Agent 运行都会携带这份上下文。Hook 参数与类型约束源码级解析useAgent的入参类型在packages/react-core/src/v2/hooks/use-agent.tsx#L25-L140中被严格定义为恰好两种合法形态介于两者之间的组合全部是编译错误形态用法语义绑定共享 AgentuseAgent()或useAgent({ agentId })绑定注册表中agentId对应的共享实例线程threadId取自周围CopilotChatConfiguration或 Agent 自动生成的 UUID绑定私有 Agent 到线程useAgent({ agentId, runtimeAgentId, threadId })三个参数缺一不可注册一个本地agentId的代理 Agent出站路由到runtimeAgentId并把threadId钉在私有实例上也就是说useAgent({ agentId, threadId })、useAgent({ agentId, runtimeAgentId })、useAgent({ runtimeAgentId, threadId })都是编译错误。原因在于仅凭agentId解析出的 Agent 是共享单例把线程直接写上去会让两个useAgent调用互相覆盖线程而注册私有代理的唯一目的就是钉线程没有线程则与直接绑定共享 Agent 无异。在类型无法覆盖的调用方纯 JS、as any等处源码还提供了运行时兜底校验抛出明确的错误信息见use-agent.tsx#L160-L198。agentId的解析优先级为显式传入的agentId→ 外层CopilotChatConfiguration的agentId→ Provider 的全局默认 →DEFAULT_AGENT_IDdefault这保证了在CopilotChat agentId...子树内调用useAgent()不会错误解析到default见use-agent.tsx#L205-L208。UseAgentUpdate可订阅的更新类型UseAgentUpdate枚举在use-agent.tsx#L13-L17中定义共三种可订阅更新OnMessagesChanged消息数组变化时触发OnStateChangedAgent 状态变化时触发OnRunStatusChanged运行生命周期状态变化时触发。未传updates时默认订阅全部三种ALL_UPDATES见use-agent.tsx#L19-L23。从源码看订阅器会把对应回调接到subscribeToAgentWithOptions上并统一走微任务批处理forceUpdate机制——同一 tick 内多次通知如OnStateChanged与OnRunStatusChanged同时触发会合并为一次 React 重渲染避免流式输出过程中的滚动跳动问题见use-agent.tsx#L372-L426。Core Patterns六个核心操作模式1. 发送消息并流式接收响应useAgent返回的agent本身就是可用的 Agent 实例配合useCopilotKit()暴露的copilotkit.runAgent({ agent })即可发起运行const { agent } useAgent({ agentId: default }); const { copilotkit } useCopilotKit(); async function ask(text: string) { agent.addMessage({ id: crypto.randomUUID(), role: user, content: text }); await copilotkit.runAgent({ agent }); }消息必须通过agent.addMessage(...)添加而不是直接 push 数组原因详见下文常见误区第 3 条。runAgent的底层实现在packages/core/src/core/core.ts#L1341-L1344最终由RunHandler.runAgent编排运行并携带当前已注册的上下文。2. 只订阅运行状态以减少重渲染高频流式更新会带来大量重渲染如果页面只关心是否正在运行只订阅OnRunStatusChanged即可const { agent } useAgent({ agentId: default, updates: [UseAgentUpdate.OnRunStatusChanged], }); const isRunning agent.isRunning;注意这里的关键点useAgent返回的是{ agent, isReady }而isRunning活在agent本身之上。订阅OnRunStatusChanged会在该值翻转时强制重渲染所以直接读取agent.isRunning也能保持实时。从源码看该枚举同时绑定了onRunInitialized、onRunFinalized、onRunFailed以及协议级的onRunErrorEvent四个回调见use-agent.tsx#L405-L412因此运行开始、结束、失败都会触发刷新。3. 把应用状态共享给每一次 Agent 运行全局上下文const value useMemo( () ({ cartItems: cart.items, currentRoute: router.pathname }), [cart.items, router.pathname], ); useAgentContext({ description: user cart route, value });useAgentContext的实现在packages/react-core/src/v2/hooks/use-agent-context.tsx内部先用useMemo把 value 序列化为字符串非字符串值走JSON.stringify然后在useLayoutEffect中调用copilotkit.addContext({ description, value })注册并在卸载时removeContext。注册后的条目进入核心的ContextStore见packages/core/src/core/context-store.ts在每次运行组装上下文时通过getContextForAgent取出、剥离开放元数据后随协议发送见packages/core/src/core/run-handler.ts#L466。4. 中止当前运行const { agent } useAgent({ agentId: default }); button onClick{() agent.abortRun()}Stop/button;abortRun()直接作用在 Agent 实例上与线程绑定无关任何拿到agent的地方都能调用。5. 等真实 Agent 就绪后再挂接订阅const { agent, isReady } useAgent({ agentId: default }); useEffect(() { if (!isReady) return; // provisional stand-in — dont attach yet const sub agent.subscribe({ onRunStartedEvent: handleRunStarted }); return () sub.unsubscribe(); }, [agent, isReady]);这是临时占位实例机制的典型配套写法。在运行时/info同步完成之前agent是一个ProxiedCopilotRuntimeAgent占位实例runtimeMode: pending。源码保证它永远是完整构造的AbstractAgent调用任何方法都安全但它随后会被真正的 Agent替换agent引用发生改变任何以旧实例为 key 的东西订阅、缓存、effect都会随之失效。isReady就是用来区分占位与真实的开关见use-agent.tsx#L256-L370与返回值注释use-agent.tsx#L467-L484。值得一提的边界行为如果运行时处于Error状态如/info拉取失败useAgent不会抛异常而是同样返回占位实例让onError处理器有机会触发、应用保持存活而不是让整个 React 树崩溃见use-agent.tsx#L327-L345。6.补充私有代理与线程绑定当应用需要多个前端 Agent 挂在同一个运行时 Agent 上时使用三参数形态注册私有代理useAgent({ agentId: chat-1, runtimeAgentId: default, threadId: t1 });源码中该形态会调用copilotkit.registerProxiedAgent({ agentId, runtimeAgentId })注册代理并在 effect 卸载时unregisterStrictMode 安全注册完成后通过registeredProxyAgent状态驱动 Hook 从占位实例确定性切换到真实代理见use-agent.tsx#L237-L254。显式传入的threadId还会被同步写到 Agent 上保证/agent/run、/agent/connect、/agent/stop三个协议端点都指向同一个线程而非 Agent 自动生成的随机 UUID见use-agent.tsx#L444-L465。Common Mistakes七类高频陷阱与正确写法CRITICAL — 自定义AbstractAgent.clone()返回this错误class MyAgent extends AbstractAgent { clone() { return this; // wrong — same instance is reused across threads } }正确class MyAgent extends AbstractAgent { clone() { const next new MyAgent(this.config); next.state { ...this.state }; return next; } }useAgent调用source.clone()为每个线程构建独立克隆若克隆与原实例是同一个对象会抛出clone() must return a new, independent object。这条校验正是为了守卫线程隔离克隆共享同一实例会让不同线程读写同一份状态造成相互污染。仓库中的测试替身也严格遵循这一契约——例如packages/react-core/src/v2/__tests__/utils/test-helpers.tsx#L69-L80中的clone()显式new出一个新实例并复制字段并注释说明clone() 契约必须返回独立对象。HIGH — 未用isReady守卫就从agent派生应用状态错误const { agent } useAgent({ agentId: default }); // Correlation map for matching responses back to the row that asked. const pending useRef(new Mapstring, string()); useEffect(() { pending.current new Map(); // re-runs when agent is swapped const sub agent.subscribe({ onRunFinishedEvent: resolvePending }); return () sub.unsubscribe(); }, [agent]);正确const { agent, isReady } useAgent({ agentId: default }); // Owned by the component, not by the agent — survives the swap. const pending useRef(new Mapstring, string()); useEffect(() { if (!isReady) return; const sub agent.subscribe({ onRunFinishedEvent: resolvePending }); return () sub.unsubscribe(); }, [agent, isReady]);agent引用在每次挂载期间恰好改变一次——当/info解析完成、占位实例被替换为真实实例时。任何依赖数组含agent的 effect 都会在这一刻重跑因此放在 effect 内部初始化的应用状态会在首次交互中途被静默重置。典型症状是把关联映射表 / 进行中的请求记录 / ref这类组件自有簿记放到了agent依赖之后。这个 bug 在配置了 CopilotKit Intelligence 时最致命许可证校验与线程端点发现会把占位窗口拉长到用户首次操作之后而纯 SSE 模式下窗口通常在有人交互前就关闭了这就是为什么它在纯 OSS 开发中无法复现。正确原则永远不要把组件级簿记放在agent依赖之后在 ref 本身中初始化让 effect 只负责管理订阅。HIGH — 直接修改agent.messages错误agent.messages.push({ id, role: user, content: hi });正确agent.addMessage({ id: crypto.randomUUID(), role: user, content: hi }); // or: agent.setMessages([...agent.messages, newMessage]);AG-UI 协议通过addMessage/setMessages触发onMessagesChanged订阅者。直接改数组会绕过订阅系统UI 永远不会重渲染。测试代码中也能看到setMessages被频繁用于线程切换等场景如packages/core/src/__tests__/core-connect-thread-switch.test.ts中的setMessagesSpy断言。HIGH — 通过useAgentContext注册不可序列化值错误useAgentContext({ description: user, value: { name: Alice, lastLogin: new Date(), onLogout: () logout(), // dropped silently }, });正确useAgentContext({ description: user, value: { name: Alice, lastLogin: new Date().toISOString() }, });useAgentContext会把 value 交给JSON.stringify见use-agent-context.tsx#L30-L35函数被静默丢弃、Date被强转成 ISO 字符串Agent 还得自己解析、循环引用直接抛异常。AgentContextInput.value的类型JsonSerializable本身就只允许字符串/数字/布尔/null/数组/对象见use-agent-context.tsx#L7-L24这是类型层面的第一道约束。MEDIUM — 以为生命周期回调也受节流控制错误useAgent({ agentId: default, throttleMs: 300, // expecting onRunInitialized / onRunFinalized / onRunFailed to also be throttled });正确// Only OnMessagesChanged / OnStateChanged / OnRunStatusChanged are throttled. // Lifecycle callbacks always fire immediately — handle them synchronously. useAgent({ agentId: default, throttleMs: 300 });throttleMs只作用于UseAgentUpdate枚举中的三类订阅更新生命周期回调一律立即触发。源码注释明确说明该值采用leading trailing 共享窗口模式窗口内首次更新立即触发、后续更新合并、窗口结束的 trailing 定时器兜底发出最近一次解析优先级为throttleMs ?? provider defaultThrottleMs ?? 0显式传0可在 Provider 设置了非零默认值时强制关闭节流见use-agent.tsx#L25-L50。MEDIUM — 上下文 value 身份不稳定错误useAgentContext({ description: cart, value: { items: cart.items } });正确const value useMemo(() ({ items: cart.items }), [cart.items]); useAgentContext({ description: cart, value });每次渲染新建对象字面量会让useAgentContext内部用于序列化的useMemo永远失效依赖是 value 引用导致核心上下文存储中反复 remove/re-add产生抖动。用useMemo稳定引用即可见use-agent-context.tsx#L30-L35。MEDIUM — 以为useAgentContext/copilotkit.addContext支持按 Agent 隔离错误useAgentContext({ agentId: research, description: paper list, value }); // or the imperative form: copilotkit.addContext({ description: paper list, value: JSON.stringify(value), agentId: research, });正确// Context is global — every agent run sees every registered entry. useAgentContext({ description: paper list, value }); // When only one agent should key off a value, branch inside its prompt // or tool logic instead of trying to scope the context entry.上下文是有意全局化的不存在按 Agent 隔离的 HookuseAgentContext没有agentId参数copilotkit.addContext也只解构{ description, value }传进去的agentId会被静默丢弃。请把上下文当作每个 Agent 都能看到的世界的状态。补充一个重要的边界事实来自仓库核心而非 Hook 层核心层ContextStore内部其实支持可选agentIds作用域字段ScopedContext见context-store.ts#L11-L13getContextForAgent会过滤出当前 Agent 可见的条目并在发送前剥离开放元数据见context-store.ts#L52-L59。相关测试见packages/core/src/__tests__/core-context-injection.test.ts其中也覆盖了空agentIds数组意味着上下文不发给任何 Agent的边界。但这条通道并没有暴露给useAgentContext与 v2 的useCopilotKit客户端包装——在 Hook 层试图传agentId依然会被丢弃。MEDIUM — 两个组件使用相同(agentId, threadId)却期望隔离错误function A() { const { agent } useAgent({ agentId: default, threadId: t1 }); } function B() { const { agent } useAgent({ agentId: default, threadId: t1 }); }正确function A() { useAgent({ agentId: default, threadId: a }); } function B() { useAgent({ agentId: default, threadId: b }); }按线程的克隆体缓存在模块级WeakMap中key 为(registryAgent, threadId)。两个消费相同(agentId, threadId)的组件观察到的是同一份状态。需要隔离时给每个界面面分配独立的threadId或使用前文的三参数私有代理形态。总结两条黄金法则回顾整个agent-access.md与对应源码可以把所有要点浓缩为两条判断准则agent是会换人的/info解析完成前它是占位实例解析后引用被替换。所有订阅、缓存、簿记要么以isReady守卫要么放在组件自己的 ref 里绝不放在依赖agent的 effect 中。上下文是全局的消息只能走 APIuseAgentContext注入的状态所有 Agent 都看得到不要指望按 Agent 隔离消息增改一律走addMessage/setMessages直接改数组等于绕过订阅系统。掌握了这两个 Hook 与七类陷阱你就可以在 CopilotKit 之上构建出线程安全、状态可预测、重渲染可控的 Agent 交互界面。更深层的注册表、/info同步与传输协商机制可继续阅读 provider-setup.md 与 threads.md 这两份同目录参考资料。【免费下载链接】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),仅供参考