实现原理与完整验收指南)
CopilotKit 与 Agno 集成实战Shared State 双向读写UI ↔ Agent实现原理与完整验收指南【免费下载链接】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 的 Agno 集成中shared-state-read-write演示展示了 UI 与 Agent 之间同一份共享状态对象的双向读写前端通过agent.setState(...)把偏好写入 Agent 状态Agent 通过set_notes工具把笔记写回状态并由前端实时渲染。本文以 showcase/integrations/agno/qa/shared-state-read-write.md 这份 QA 测试计划为主体逐条拆解其前置条件、测试步骤与预期结果并结合 Agent 后端源码、演示页面源码 与 自定义 AGUI router 揭示底层实现原理。读完你将掌握该演示的功能验收路径、每个可定位元素testid的含义、set_notes的全量替换契约、以及 CopilotKit 为什么需要为 Agno 定制StateSnapshotEvent才能实现 Agent→UI 的状态回流。前置条件与运行环境该 QA 计划针对的是已部署在 dashboard 主机上的演示环境启动前需确认三个前提演示页面已部署并可访问/demos/shared-state-read-write路由需要能从 dashboard 主机正常加载。该页面由 showcase/integrations/agno/src/app/demos/shared-state-read-write/page.tsx 实现。Agent 后端健康/api/health返回正常OPENAI_API_KEY已设置。健康探针在 src/app/api/copilotkit/route.ts 的GET分支实现会返回agent_status与OPENAI_API_KEY是否设置的状态。Agno Agent 服务器暴露/shared-state-rw/agui端点这是关键端点——它不是 Agno 的通用/agui而是由 src/agent_server.py 挂载的状态感知state-awareAGUI 端点。之所以需要独立端点是因为 Agno 自带的 AGUI router 不向外发射StateSnapshotEvent而该演示依赖这个事件完成 Agent→UI 的状态回写详见源码级原理一节。前端运行时通过 src/app/api/copilotkit/route.ts 把shared-state-read-write这个 Agent 名称映射到HttpAgent({ url: \${AGENT_URL}/shared-state-rw/agui })即后端进程默认http://localhost:8000上挂载的状态感知路由。页面布局与可定位元素地图QA 计划要求页面在 3 秒内渲染完成左侧为两张卡片偏好卡片 笔记卡片右侧为CopilotSidebar聊天面板。各元素的data-testid与语义如下表testid所属组件语义preferences-cardpreferences-card.tsx偏好卡片标题 Your preferencespref-name偏好卡片姓名输入框placeholder 为 e.g. Ataipref-tone偏好卡片语气下拉formal / casual / playfulpref-language偏好卡片语言下拉English / Spanish / French / German / Japanesepref-state-json偏好卡片实时 JSON 预览同步展示preferences对象notes-cardnotes-card.tsx笔记卡片源码中标题为 Agent Scratch padnotes-empty笔记卡片笔记为空时的占位提示notes-list笔记卡片非空时的笔记列表容器note-item笔记卡片单条笔记条目notes-clear-button笔记卡片清空按钮仅在笔记非空时渲染兴趣选择采用 Badge 药丸按钮可选项定义在INTEREST_OPTIONSCooking、Travel、Tech、Music、Sports、Books、Movies选中态使用边框#BEC2FF、背景#BEC2FF1A。需要注意QA 文档中描述的笔记卡标题 Agent notes 与空态文案 No notes yet. Ask the agent to remember something. 与当前源码存在细微出入——源码实际使用标题 Agent Scratch pad空态文案为 the agent will make observations about you and note them here!。执行验收时建议以当前仓库实际渲染文案为准或同步更新 QA 文档。聊天输入框的 placeholder 由 demo-layout.tsx 中的CopilotSidebar的labels.chatInputPlaceholder配置为 Chat with the agent...。三个建议药丸suggestion pills定义在 suggestions.ts标题与消息原文如下Greet me→ Say hi and introduce yourself.Remember something→ Remember that I prefer morning meetings and that I dont eat dairy.Plan a weekend→ Suggest a weekend plan based on my interests.基础功能测试QA 计划的第一步验证页面骨架与最小交互闭环访问/demos/shared-state-read-write确认页面在 3 秒内渲染出左侧偏好 笔记卡片与右侧CopilotChat面板确认preferences-card可见且标题为 Your preferences确认notes-card可见空态元素notes-empty渲染确认聊天输入框 placeholder 为 Chat with the agent...确认三个建议药丸按原文标题可见发送 Hello确认 10 秒内返回 assistant 文本回复。页面结构上CopilotKit组件通过runtimeUrl/api/copilotkit与agentshared-state-read-write建立连接DemoContent通过useAgent订阅状态更新详见下文CopilotSidebar承载对话 UI。专项一UI 写入 → Agent 读取preferences 通过agent.setState这是读 写的第一条方向前端拥有preferences对象把它写进 Agent 状态Agent 每一轮都读取并据此调整回复。操作与断言在pref-name输入 Atai断言pref-state-json同步更新并包含name: Atai将pref-tone切换为formal断言 JSON 预览反映tone: formal将pref-language切换为Spanish断言 JSON 预览反映language: Spanish点击Cooking和Travel兴趣药丸断言两者呈现选中样式border#BEC2FF、bg#BEC2FF1A且 JSON 预览的interests数组同时包含两项发送 What do you know about me?断言 10 秒内回复引用姓名 Atai、formal 语气、西班牙语以及 Cooking/Travel 兴趣点击 Plan a weekend 药丸断言回复针对所选兴趣量身定制。实现原理前端每次编辑都会触发handlePreferencesChange调用agent.setState({ preferences: next, notes })见 page.tsx其中notes被透传以保留 Agent 已写入的内容。偏好卡片本身是纯受控表单完全不知道 Agent 的存在所有状态接线都在父级page.tsx一层完成——这是该演示刻意保持的解耦设计。后端一侧Agent 的定义见 src/agents/shared_state_read_write.py。关键点在于动态指令函数build_instructionsdef build_instructions(run_context: RunContext) - str: base dedent(...).strip() prefs_block _format_preferences(getattr(run_context, session_state, None) or {}) if prefs_block: return f{prefs_block}\n\n{base} return base该函数从run_context.session_state中读取preferences并通过_format_preferences生成形如[shared-state-read-write] preferences:开头、包含 Name / Preferred tone / Preferred language / Interests 的偏好块拼接到系统指令前。_format_preferences有一个防御性边界若 dict 为真但没有任何可识别键则返回空串而不是输出一个光秃秃的标题头与 google-adk 参考实现中的 guard 保持一致。让写入立即生效的关键是 Agent 构造参数agent Agent( modelOpenAIChat(idgpt-4o-mini, timeout120), tools[set_notes], cache_callablesFalse, # 每轮重新求值 instructions instructionsbuild_instructions, tool_call_limit5, )cache_callablesFalse使得build_instructions在每一轮都被重新求值而不是在 Agent 构造时缓存一次——这正是 UI 写入的偏好能在下一轮就生效的根本原因。专项二Agent 写入 → UI 读取notes 通过set_notes工具这是第二条方向Agent 通过set_notes工具写session_state[notes]前端订阅状态变化并实时重渲染笔记卡片。操作与断言点击 Remember something 药丸实际发送 Remember that I prefer morning meetings and that I dont eat dairy.15 秒内断言notes-list出现且包含至少 2 条note-item分别提及 morning meetings 与 dairy断言notes-empty不再渲染发送 Also remember I live in Berlin.15 秒内断言笔记列表增长旧笔记保留、新笔记追加。set_notes的全量替换契约QA 计划特别强调后续调用时旧笔记必须完整保留因为set_notes的契约是整体替换数组而非追加。Agent 的系统指令明确要求当用户要求记住某事时调用set_notes并传入完整的新列表已有笔记 新笔记每条笔记不超过 120 字符。源码实现 shared_state_read_write.py 还包含一个健壮性处理def set_notes(run_context: RunContext, notes: list[str]) - str: if run_context.session_state is None: run_context.session_state {} # 容忍模型把早期轮次的杂散 dict/None 传进来 # 统一强转为字符串避免流中途 AGUI 序列化失败崩溃。 cleaned [str(n) for n in (notes or []) if n is not None] run_context.session_state[notes] cleaned return fNotes updated. ({len(cleaned)} total)set_notes直接就地修改run_context.session_state[notes]为清洗后的列表并返回更新条数——所有条目被强转为纯字符串从实现上消除了序列化崩溃的隐患。前端如何感知 Agent 的写入前端通过useAgent订阅状态变更见 page.tsxconst { agent } useAgent({ agentId: shared-state-read-write, updates: [UseAgentUpdate.OnStateChanged], });useAgent({ updates: [OnStateChanged] })让组件对 Agent 的每次状态变更重渲染state.notes变化会传导到NotesCard重新渲染列表。专项三UI 写回 Agent 创作的状态切片清空笔记该演示还验证了同一字段上的反向写回UI 把 Agent 写出的笔记清空写回 Agent 状态。操作与断言在有笔记的前提下断言notes-clear-button可见点击 Clear 按钮断言笔记列表消失、notes-empty重新渲染询问 What do you remember about me?断言 Agent 不再引用已清空的笔记——证明状态确实通过agent.setState({ notes: [] })写回了 Agent。源码中handleClearNotes的实现在 page.tsxconst handleClearNotes () { agent.setState({ preferences, notes: [] } as RWAgentState); };注意preferences同样被透传保留与handlePreferencesChange中透传notes形成对称两侧写入都以读取当前另一切片并原样带回的方式避免互相覆盖。这是双向共享状态实现中最容易踩的坑——只写自己关心的切片会覆盖掉对方写的内容。专项四多轮状态持久化操作与断言将 tone 改为playful并添加Music兴趣发送 Write me a one-line haiku greeting.断言回复俏皮且引用音乐追加发送 Do it again in French.断言回复仍为俏皮语气、切换为法语、并继续承认音乐兴趣——证明偏好跨轮持久化刷新页面断言偏好重置为默认值tone: casual、language: English、空 interests、空 name笔记也重置为空。会话session语义偏好跨轮持久的机理在于后端在_run_agent_with_state_snapshot中调用agent.arun(..., session_idthread_id, session_statesession_state)见 agent_server.pyAgno 用同一个thread_id/session_id复用会话session_state在轮次间得以保留。而刷新页面后状态重置是因为状态本身是按会话per-session存在的初始值由页面加载时的useEffect一次性agent.setState({ preferences: INITIAL_PREFERENCES, notes: [] })播种见 page.tsx默认偏好为tone: casual、language: English、空 interests、空 name。刷新即重新播种因此回到默认值。错误处理与边界场景空消息发送空消息应为 no-op——不产生用户气泡也不产生 assistant 回复无偏好兜底取消全部兴趣并清空姓名后发送 Who am I?Agent 应正常回答而不崩溃——_format_preferences在没有任何可识别键时返回空串build_instructions因此跳过偏好块直接返回基础指令控制台洁净以上所有流程中 DevTools Console 不应出现未捕获错误。源码级原理Agno 缺少的StateSnapshotEvent补丁该演示能工作的关键是后端 agent_server.py 中_run_agent_with_state_snapshot这个自定义 AGUI handler。源码注释明确说明了动机Agno 自带的 AGUI routeragno.os.interfaces.agui不会向客户端发射StateSnapshotEvent。这意味着通过工具修改session_state的变更对订阅了useAgent({ updates: [OnStateChanged] })的 UI 是不可见的——往返链路是断的。该 handler 复刻了agno.os.interfaces.agui.router.run_agent的默认行为但做了三处关键调整压制内部流的RUN_STARTED/RUN_FINISHED自己先发射RunStartedEvent避免重复在RunFinishedEvent之前插入StateSnapshotEvent在 Agno 流结束后通过agent.aget_session_state(session_idthread_id)回退到同步get_session_state读回最终session_state再发射StateSnapshotEvent(typeEventType.STATE_SNAPSHOT, snapshotfinal_state)最后发射自己的RunFinishedEvent让快照落在 run 窗口内快照读取回退策略读状态时若有异常则回退到内存中的session_state避免单个失败导致整轮崩溃。正因为如此前端useAgent才能在每轮结束后拿到state.notes的最新值。这一补丁在 PARITY_NOTES.md 中有完整记录Agno 的 stock AGUI router 不发射状态事件没有这个 shim依赖set_notes/delegations的 Agent 侧状态写入对前端完全不可见。同款补丁还被subagents、gen-ui-agent等依赖 Agent 侧状态写入的演示复用见 route.ts。自动化测试佐证仓库内的 Playwright 测试 tests/e2e/shared-state-read-write.spec.ts 覆盖了本文的部分验收点并记录了真实的回归教训Greet me 药丸曾匹配到 feature-parity.json 中裸userMessage: hi的夹具回复了通用的 Hi there! Im your showcase assistant… 开场白而不是共享状态感知的问候修复方式是添加更长子串匹配的夹具使其优先命中同样Plan a weekend 曾匹配到裸plan夹具返回泛泛的 5 步内容营销计划修复后断言回复包含/interests panel/i。E2E 测试还通过正向 负向断言双重锁定行为例如断言助理消息包含/shared-state co-pilot/i且不包含通用开场白。这说明 QA 计划中的药丸测试并非只验证有回复而是验证回复来自正确的状态感知路径。预期结果与验收标准汇总QA 计划的最终验收标准页面 3 秒内加载assistant 文本回复 10 秒内返回偏好写入在变更时同步反映到pref-state-jsonAgent 创作的笔记在 remember 提示后 15 秒内出现在notes-card后续set_notes调用完整保留此前列表Clear 按钮完成 UI → Agent 状态的往返Agent 下一轮即失去对已清空笔记的访问无布局破坏、无未捕获的控制台错误。对照上述标准可以确认该演示完整覆盖了双向共享状态的四个方向——UI 写偏好Agent 读、Agent 写笔记UI 读、UI 清空笔记写回 Agent、以及跨轮持久化与刷新重置。无论你是要在自己的 CopilotKit 集成中实现类似的双向共享状态还是只想验证 Agno 后端是否实现了完整的状态契约都可以直接复用本文的测试路径与源码参考。【免费下载链接】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),仅供参考