ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CopilotKit 双向共享状态实战:UI 与 Agent 读写同一份 state 的完整实现(Shared State Read + Write)

CopilotKit 双向共享状态实战:UI 与 Agent 读写同一份 state 的完整实现(Shared State Read + Write) CopilotKit 双向共享状态实战UI 与 Agent 读写同一份 state 的完整实现Shared State Read Write【免费下载链接】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 仓库中的 Shared StateRead Write示例讲解如何在 React 前端与后端 Agent 之间建立双向共享状态UI 通过agent.setState(...)把偏好数据写入state.preferences后端中间件每轮把这份数据注入系统提示词从而影响模型回复同时 Agent 通过set_notes工具把笔记写入state.notes前端通过useAgent({ updates: [UseAgentUpdate.OnStateChanged] })实时订阅并重渲染侧边栏。读完本文你将掌握这一模式在前端page.tsx、运行时路由route.ts与后端 Agent 三层中的完整调用链并能在自己的 CopilotKit 应用里复刻。该示例位于showcase/integrations/claude-sdk-typescript/src/app/demos/shared-state-read-write/后端同时提供了 TypeScriptagent_server.ts与 Pythonshared_state_read_write.py两套实现。这个 Demo 展示了什么Shared StateRead WriteDemo 演示的是 UI 与 Agent 之间读、写同一份 state 对象的双向能力与纯单向的 Shared StateReadDemo 形成对比UI → Agent侧边栏表单姓名、语气、语言、兴趣通过agent.setState(...)写入state.preferences。后端中间件每一轮都会读取这份数据并将其注入系统提示词。Agent → UIAgent 的set_notes工具写入state.notes侧边栏的笔记卡片Agent Scratch pad在 Agent 每次更新时实时重渲染。往返Round-trip在侧边栏修改偏好后Agent 的下一条回复会明显随之变化——语气、语言、以及用你的名字称呼你。这正是 CopilotKit 所倡导的双向共享状态的标准形态前端写、后端既读又写、前端再重渲染见 Python 端模块 docstring 中的概括shared_state_read_write.py。如何交互启动示例后先在侧边栏编辑你的偏好填入名字、选择语气和语言、点选兴趣标签然后依次尝试以下提示Say hi and introduce yourself.打个招呼并介绍自己——验证 Agent 会用你设置的名字和语气回应。Remember that I prefer morning meetings and that I dont eat dairy.记住我喜欢上午开会、不吃乳制品——观察 Agent 调用set_notes后笔记卡片上出现新的笔记条目。Suggest a weekend plan based on my interests.根据我的兴趣推荐周末计划——验证 UI 写入的interests真正影响了 Agent 的规划内容。交互过程中可以观察两个现象Agent 的回复会随偏好实时适配每次让 Agent记住某件事侧边栏都会立刻出现对应的新笔记。这些建议提示在源码中通过useConfigureSuggestions注册suggestions.ts。前端实现useAgent 订阅与 setState 写入状态模型页面定义了双向共享状态的 TypeScript 形状page.tsxinterface RWAgentState { preferences: Preferences; // UI 写入Agent 读取 notes: string[]; // Agent 写入UI 读取 }其中Preferences的完整字段定义在 preferences-card.tsxexport interface Preferences { name: string; tone: formal | casual | playful; language: string; interests: string[]; }初始值INITIAL_PREFERENCES为name为空、tone: casual、language: English、interests: []page.tsx。读取方向订阅 Agent 状态变更页面通过useAgent订阅每一次来自 Agent 的状态变更page.tsxconst { agent } useAgent({ agentId: shared-state-read-write, updates: [UseAgentUpdate.OnStateChanged], });UseAgentUpdate.OnStateChanged使组件对 Agent 的每次状态变更都触发重渲染。随后组件从agent.state中安全地解出两份数据并为缺失字段提供回退默认值page.tsxconst agentState agent.state as RWAgentState | undefined; const preferences agentState?.preferences ?? INITIAL_PREFERENCES; const notes agentState?.notes ?? [];页面还通过useEffect在首次挂载时把初始偏好与空笔记种子化进 Agent 状态保证第一轮对话时后端就有内容可读page.tsx。写入方向setState 统一出口UI 侧的所有写入都汇入同一个agent.setState(...)调用编辑偏好表单时handlePreferencesChange写入新的preferences同时通过latestNotesRef保留 Agent 已写入的notes避免覆盖page.tsxconst handlePreferencesChange (next: Preferences) { agent.setState({ preferences: next, notes: latestNotesRef.current, // preserve what the agent has written } as RWAgentState); };点击Clear清空笔记时handleClearNotes把notes置空page.tsx。值得注意的是两个侧边栏卡片组件本身完全不知道 Agent 的存在PreferencesCard是一个普通受控表单所有修改通过onChange冒泡到父页面由父页面统一接线到agent.setStatepreferences-card.tsxNotesCard只负责渲染state.notes并把清空动作以onClear属性暴露出来notes-card.tsx。这种卡片无状态、状态接线收敛到页面层的设计让每个组件都易于测试与复用。布局与会话外壳DemoLayout把两张卡片与CopilotSidebar组合在一起侧边栏指定agentIdshared-state-read-write、默认展开并自定义了聊天输入框占位文案demo-layout.tsx。整页由CopilotKitProvider 包裹指向运行时端点/api/copilotkit并指定agentshared-state-read-writepage.tsx。前端运行时路由把 setState 转发给 AgentDemo 使用了一个专用运行时路由POST /api/copilotkit-shared-state-read-writeroute.ts它与通用的/api/copilotkit路由并存。该路由用createClaudeHttpAgent把请求代理到 Claude Agent 服务器AGENT_URL默认http://localhost:8000的/shared-state-read-write端点route.tsconst AGENT_URL process.env.AGENT_URL || http://localhost:8000; const sharedStateAgent createClaudeHttpAgent( ${AGENT_URL}/shared-state-read-write, ); const agents: Recordstring, AbstractAgent { shared-state-read-write: sharedStateAgent, default: sharedStateAgent, };随后用CopilotRuntime与createCopilotRuntimeHandler以single-route模式对外提供服务route.ts。此外该路由的异常处理遵循服务端记录完整堆栈、客户端只返回不透明 errorId的安全实践避免泄漏服务器内部路径与环境信息route.ts。值得注意的机制是agent.setState并不会直接修改后端状态而是被 AG-UI 客户端携带进下一次RunAgentInput.state。每一次对话轮次的请求体state中都包含了 UI 最新写入的preferences与notes后端拿到后据此构建本轮上下文。后端实现TypeScript 版每轮读取 preferences 并注入系统提示词路由入口读取 input.state 并组装系统提示词在 agent_server.ts 中/shared-state-read-write路由每轮从请求体中取出state.preferences与state.notes交给工具函数处理app.post( /shared-state-read-write, async (req: Request, res: Response): Promisevoid { const input req.body as RunAgentInput; const incomingState ((input as any).state as Recordstring, unknown | undefined) ?? {}; const prefs coercePreferences(incomingState.preferences); const notes Array.isArray(incomingState.notes) ? (incomingState.notes as unknown[]).filter( (n): n is string typeof n string, ) : []; await runAgenticLoop(req, res, { systemPrompt: buildSharedStateReadWriteSystemPrompt(prefs), toolSchemas: [SET_NOTES_TOOL_SCHEMA] as Anthropic.Tool[], initialState: { preferences: prefs, notes }, }); }, );关键点preferences每轮都从请求的state中重新读取而不是保存在服务器内存里所以 UI 的最新写入天然在下一次对话生效实现了UI 写 → 后端读的闭环。偏好净化与提示词构建coercePreferences负责把任意unknown输入净化为受信任的Preferences对象接受部分字段缺失并静默丢弃类型不匹配的字段防止行为不当的前端污染提示词shared-state-read-write-prompt.ts。buildPreferencesPreamble把净化后的偏好渲染成结构化文本行例如The user has shared these preferences with you: - Name: Atai - Preferred tone: casual - Preferred language: English - Interests: Travel, Cooking Tailor every response to these preferences. Address the user by name when appropriate.如果没有任何偏好字段则返回null系统提示词保持基准版本shared-state-read-write-prompt.ts。完整的系统提示词由基准文案拼接前缀生成shared-state-read-write-prompt.ts。set_notes 工具Agent 写回 state.notes后端以 tool schema 的方式暴露set_notes工具模型输入一个notes: string[]数组语义是用完整更新后的列表替换共享状态中的笔记数组并要求单条笔记短于 120 字符、每次传入完整列表而非 diffshared-state-read-write-prompt.ts。当工具被调用时服务器端的处理器过滤出合法的字符串数组合并进当前 state 并返回新状态agent_server.tsif (toolName set_notes) { const notes Array.isArray(toolInput.notes) ? (toolInput.notes as unknown[]).filter( (note): note is string typeof note string, ) : []; return { resultText: JSON.stringify({ status: ok, count: notes.length }), state: { ...state, notes }, }; }工具执行产生的新状态随后以AG-UISTATE_SNAPSHOT事件流式推回前端如 agent_server.ts 与 agent_server.ts 处所示触发前端useAgent({ updates: [OnStateChanged] })的重渲染——这正是Agent 写 → UI 实时读回路的底层机制。后端实现Python 版PreferencesInjectorMiddleware 与 Command 回写仓库中同一个 Demo 在 Python 侧有完整的对照实现shared_state_read_write.py使用 LangGraph LangChain 的create_agent组装。状态 SchemaAgentState继承 LangChain 的BaseAgentState声明了两个字段与前端RWAgentState一一对应shared_state_read_write.pyclass AgentState(BaseAgentState): preferences: Preferences # UI 写入agent.setState notes: list[str] # Agent 写入set_notes 工具set_notes 工具用 Command 更新状态Python 版的set_notes使用 LangGraph 的Command返回update{notes: notes}来写回共享状态同时附带一条ToolMessage告知模型操作成功shared_state_read_write.pytool def set_notes(notes: list[str], runtime: ToolRuntime) - Command: return Command( update{ notes: notes, messages: [ ToolMessage( contentNotes updated., nameset_notes, idstr(uuid.uuid4()), tool_call_idruntime.tool_call_id, ) ], } )PreferencesInjectorMiddleware每轮把偏好注入系统提示词PreferencesInjectorMiddleware继承AgentMiddleware通过wrap_model_call同步与awrap_model_call异步钩子在每次模型调用前读取request.state[preferences]把它渲染成一条SystemMessage并前置到消息列表最前面shared_state_read_write.pydef wrap_model_call(self, request, handler): prefs request.state.get(preferences) or {} prefs_message self._build_prefs_message(prefs) if prefs_message is None: return handler(request) return handler(request.override(messages[prefs_message, *request.messages]))中间件的构建逻辑与 TypeScript 版完全对齐只有存在至少一个偏好字段时才生成消息文案为 The user has shared these preferences with you: ... Tailor every response to these preferences. Address the user by name when appropriate.shared_state_read_write.py。图组装中间件顺序决定注入时机最终通过create_agent组装整张图注意两个中间件的注册顺序shared_state_read_write.pygraph create_agent( modelChatOpenAI(modelgpt-5.4), tools[set_notes], middleware[CopilotKitMiddleware(), PreferencesInjectorMiddleware()], state_schemaAgentState, system_prompt(...), )CopilotKitMiddleware负责 AG-UI 协议含STATE_SNAPSHOT事件发射PreferencesInjectorMiddleware负责偏好注入二者协作实现双向共享状态。这也印证了同一套前端与 AG-UI 协议可以对接多种后端框架——仓库中ag2、agno、crewai、langroid、pydantic-ai等多个集成目录都包含同名shared_state_read_write.py是理解UI 状态模型是后端无关的最佳旁证。完整调用链梳理综合前端、路由与后端实现一次编辑偏好 → 影响回复的完整链路如下用户在侧边栏修改偏好PreferencesCard触发onChange。父页面handlePreferencesChange调用agent.setState({ preferences, notes })。下一次发送消息时AG-UI 客户端把该state携带进RunAgentInput.state请求到达运行时路由/api/copilotkit-shared-state-read-write。路由把请求代理到 Agent 服务器的/shared-state-read-write端点TS或 LangGraph 图Python。后端每轮调用前读取state.preferences注入系统提示词TS 版拼进systemPromptPython 版由PreferencesInjectorMiddleware前置SystemMessage。模型回复期间若调用set_notes后端把完整notes列表合并进新状态TS 版返回state: { ...state, notes }Python 版返回Command(update{notes: notes})。新状态作为 AG-UISTATE_SNAPSHOT事件流式推送回前端。前端useAgent({ updates: [UseAgentUpdate.OnStateChanged] })触发重渲染NotesCard立即显示新笔记PreferencesCard底部同时用 JSON 实时展示共享的preferences快照preferences-card.tsx。反向的清空笔记同样走agent.setState统一出口验证了同一个 API 双向可用的设计。关键设计要点与最佳实践单向状态所有权意识preferences归 UI 所有UI 写、后端读notes归 Agent 所有Agent 写、UI 读。写入方要对字段负责读取方要容忍缺失——前端用??回退默认值后端用coercePreferences/_build_prefs_message对空值静默降级。避免覆盖对方写入UI 写preferences时必须带上latestNotesRef.current保留 Agent 的笔记这是多写者共享状态场景下的核心陷阱。全量替换而非增量 diffset_notes的工具描述与系统提示词都强制传完整列表避免并发写与部分更新导致状态不一致。输入净化即安全边界无论 TS 的coercePreferences还是 Python 的中间件都对来自前端的 state 做类型校验防止异常字段进入系统提示词shared-state-read-write-prompt.ts。协议层驱动重渲染前端不轮询、不手动刷新一切实时性都依赖 AG-UI 的STATE_SNAPSHOT事件 useAgent订阅这也是 CopilotKit 各示例通用的状态同步范式。延伸阅读对比阅读同目录下的单向读取示例shared-state-read README理解只读与读写两种状态模式的差异。流式状态写入示例shared-state-streaming README了解长文本状态如何边生成边同步。通用运行时路由copilotkit/route.ts对比多 Demo 共用路由与专用路由两种接线方式。Claude Agent SDK 适配层claude-agent-sdk-adapter.ts了解 HTTP 代理背后的 SDK 桥接实现。【免费下载链接】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),仅供参考
返回列表