深入 OpenMuse 与 CopilotKit AG-UI 集成:Rich Threads 持久化与流式事件渲染内幕
【免费下载链接】openmuseA personal agent with a browser, terminal, files, and work that keeps going built with CopilotKit and AG-UI.项目地址: https://gitcode.com/gh_mirrors/op/openmuse
OpenMuse 是一个自带浏览器、终端和文件的个人 Agent,任务关闭 App 后也能继续推进。它基于 CopilotKit 构建,核心体验由 AG-UI 流式事件与 Rich Threads 持久化两套机制支撑:回复实时流式渲染为工具卡片,整段对话随时可重放、重命名、归档与恢复。本文带你走进源码,讲清这套机制是如何实现的。
3 件事让你用上 Rich Threads 持久化
OpenMuse 的客户端是共享 iOS、Android 与 Web 的 React Native 应用(源码位于 apps/mobile/),聊天、工具卡片与文档全部通过 CopilotKit 的 headless 钩子渲染。服务端则是一个 Hono API,内嵌 CopilotRuntime 与任务引擎(apps/server/src/agent.ts)。
启用 Rich Threads 只需三步:
- 克隆仓库并安装依赖:
git clone https://gitcode.com/gh_mirrors/op/openmuse - 运行
npx copilotkit@latest login与npx copilotkit@latest project select,生成项目密钥 - 把密钥作为
CPK_INTELLIGENCE_API_KEY放入 API 服务器环境变量后重启
密钥必须只留在服务端——缺失时 API 会直接启动失败,避免"静默丢对话"。
主对话为何刷新后依然是同一条
这是新手最容易困惑的地方:为什么重新打开应用,主对话不会变成一条新对话?
服务端先"占座"。客户端启动时请求 /api/main-thread:服务端用insertIfAbsent为每个用户固化一个主对话 threadId,再通过intelligence.getOrCreateThread在 CopilotKit Intelligence 中预置它。这样即便首条消息还没发出就刷新,主对话也保持不变。
客户端做重放。选择一条已保存对话时,chat.tsx 会挂载一个私有 Agent 实例(agentId形如openmuse-<threadId>),然后调用copilotkit.connectAgent让 Intelligence 重放整条线程消息。切换过的对话会保持挂载,草稿与消息队列不会因页面导航而丢失。
线程菜单全靠useThreads。threads.tsx 中一个useThreads({ agentId: "default", includeArchived: true, limit: 20 })钩子就覆盖了列出、重命名、归档、恢复与分页加载更多;侧边聊天则由客户端生成全新 threadId,在首次运行时落盘。注意自动命名被关闭(agent.ts 中generateThreadNames: false),命名完全交给用户。
流式事件渲染内幕:一条 AG-UI 事件之旅
流式体验的关键在 engine/conversation.ts:ConversationAgent继承 AG-UI 的AbstractAgent,其run()返回一个Observable<BaseEvent>事件流。一次回复的骨架是:
RUN_STARTED→TEXT_MESSAGE_START/TEXT_MESSAGE_CONTENT(增量文本)/TEXT_MESSAGE_END→ 穿插TOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END/TOOL_CALL_RESULT→RUN_FINISHED(失败则RUN_ERROR)
这些事件经 app.ts 中挂载在/api/copilotkit/*的 runtime 以 SSE 推给客户端,代码里甚至专门把字符串 chunk 转成字节流,保证浏览器正确接收。
工具卡片是"钩子"而非硬编码。客户端用useRenderTool为每个工具注册渲染器(如browse_web→ 带实时截图的浏览器卡片、search_mail→ 邮件卡片),再由useRenderToolCall在消息流中按toolCallId配对渲染,详见 chat.tsx。这就是"生成式 UI":Agent 调一次工具,界面上就多一张可交互卡片。
富工具消息:任务卡片为何永远是"最新"的
重放一条老对话时,消息里存的是任务 ID 而不是结果快照。渲染器(thread-artifacts.tsx 中的TaskThreadCard)拿到 ID 后,实时请求/api/agent/tasks/<id>拉取当前任务状态、浏览器预览与 PDF 链接。
还有一个安全细节:文件和预览的短时效签名 URL 每次由服务端现场生成,绝不存入线程消息。线程只保留稳定 ID,既避免链接过期,也让历史对话可无限期回放。
跟随队列与停止:让对话"有手感"的细节
- 回复期间输入框保持可编辑:后续消息进入可见、可删除的队列,当前回复及其持久化完成后按序发送(conversation-queue.ts)。
- 停止即暂停队列:点停止按钮调用
copilotkit.stopAgent,草稿完整保留,队列挂起而非清空;SDK 层的错误也会通过订阅onError统一收敛(conversation-run.ts)。 - 队列只活在打开的 App 里:它不是服务器收件箱,委派给服务端的长任务则是独立的持久化工作,两者互不混淆。
下一步:从哪里继续读
- 官方文档入口:docs/README.md
- Rich Threads 配置与验证边界:docs/RICH-THREADS.md
- 交互设计与对话行为:docs/EXPERIENCE.md
- 服务端 Agent 与工具定义:apps/server/src/engine/conversation.ts
- 客户端线程与聊天实现:apps/mobile/src/
- 相关测试(真实 runtime + 模拟 Intelligence 边界):tests/rich-threads.test.ts
一句话总结:Rich Threads 负责"对话永远在",AG-UI 流式事件负责"过程看得见"。两者在 OpenMuse 中分工清晰——持久化交给 CopilotKit Intelligence 与稳定 ID,渲染交给事件驱动的 headless 钩子——这正是它既像聊天应用、又像工作台的原因。
【免费下载链接】openmuseA personal agent with a browser, terminal, files, and work that keeps going built with CopilotKit and AG-UI.项目地址: https://gitcode.com/gh_mirrors/op/openmuse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考