CopilotKit × Mastra:基于 useInterrupt 的聊天内 HITL 中断实战(含完整 QA 验证清单) 📅 发布时间:2026/9/14 12:52:01 👁 浏览次数: CopilotKit × Mastra基于 useInterrupt 的聊天内 HITL 中断实战含完整 QA 验证清单【免费下载链接】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 仓库中 Mastra 集成示例的gen-ui-interruptdemo 展开讲解如何在聊天转写区inline内实现「人在环」Human-in-the-Loop交互后端 Mastra 工具通过suspend()挂起 agentic 循环ag-ui/mastra桥接层将其映射为 AG-UI interrupt 事件前端用useInterrupt低层原语渲染时间选择卡片用户选择后resolve(...)恢复 Mastra run 并携带resumeData重新执行工具。读完本文你将掌握这条原生中断链路的后端/前端完整实现细节、关键实现陷阱以及一套可直接执行的端到端 QA 验证清单。1. 前置条件与链路总览1.1 前置条件摘自 QA 基线Demo 已部署并可访问位于 dashboard host 的/demos/gen-ui-interrupt路径Agent 后端健康检查通过/api/healthOPENAI_API_KEY已在 Railway 环境变量中设置关键机制说明QA 文档原注时间选择卡片通过useInterrupt({ renderInChat: true })内联渲染在聊天转写区内。与 LangGraph 的interrupt()API 不同Mastra 路径是一个原生 suspend 工具后端schedule_meeting工具src/mastra/tools/interrupt.ts调用suspend({ topic, attendee, slots })ag-ui/mastra桥接层把该 suspend 映射为 AG-UI interruptlegacyon_interruptCUSTOM 事件 标准的RUN_FINISHEDinterrupt-outcome随后useInterrupt渲染TimePickerCard用户点选某个 slot 后resolve(...)会恢复 Mastra run工具的execute以resumeData被重新调用。1.2 中断生命周期整条链路可以概括为六个阶段用户发送 Book a call with sales建议项或等价措辞模型依据 agent 指令调用schedule_meeting工具参数topic、可选attendee工具首次执行时返回suspend({ topic, attendee, slots })agentic 循环在此暂停ag-ui/mastra桥接层把 suspend 暴露为 AG-UI interruptmastra_suspend包装 on_interruptCUSTOM 事件run 以 interrupt-outcome 结束RUN_FINISHED前端useInterrupt的render回调解析 payload 并在聊天流内渲染TimePickerCard用户选择 slot 或取消 →resolve(...)→ Mastra run 恢复 →execute第二次进入executionContext.agent.resumeData携带用户选择 → 工具返回确认文案 → agent 生成最终回复。这条链路与旧版「Strategy-B」workaround前端自造schedule_meeting配合useHumanInTheLoop不同是真实的后端挂起见 src/mastra/tools/interrupt.ts 顶部注释。2. 后端实现原生 suspend 工具schedule_meeting后端工具定义在 showcase/integrations/mastra/src/mastra/tools/interrupt.ts核心结构如下export const scheduleMeetingInterruptTool createTool({ id: schedule_meeting, description: Ask the user to pick a meeting time. Surfaces an interactive time-picker to the user and returns their selection. Call this whenever the user asks to book or schedule a meeting., inputSchema: z.object({ topic: z.string().describe(What the meeting is about (e.g. Intro with sales).), attendee: z.string().optional().describe(Who the meeting is with (e.g. Alice), if known.), }), suspendSchema: z.object({ topic: z.string(), attendee: z.string().optional(), slots: z.array(z.object({ label: z.string(), iso: z.string() })), }), resumeSchema: z.object({ chosen_time: z.string().optional(), chosen_label: z.string().optional(), cancelled: z.boolean().optional(), }), execute: async (inputData, executionContext) { const { suspend, resumeData } executionContext?.agent ?? {}; // 第二次进入用户已 resolve带选择恢复 if (resumeData) { if (resumeData.cancelled) { return The user cancelled — no meeting was scheduled.; } const when resumeData.chosen_label ?? resumeData.chosen_time ?? the chosen time; return Scheduled ${inputData.topic} for ${when}.; } // 第一次进入挂起并携带 picker payload return suspend?.({ topic: inputData.topic, attendee: inputData.attendee, slots: generateCandidateSlots(), }); }, });几个源码级要点都是该文件注释明确标注的「load-bearing」细节必须return suspend(...)直接返回不能写成await suspend(); return x。后者会让工具「完成」在 fast streaming 下 agentic 循环会越过暂停点继续执行。suspend/resumeData位于executionContext.agent子对象下AgentToolExecutionContext不在executionContext顶层。直接从顶层解构会得到undefined导致suspend is not a function工具错误模型反复重调直至撞上 step 上限且永远没有tool-call-suspendedchunk。候选 slot 由后端生成generateCandidateSlots()基于当前时间生成固定四个相对标签 ——Tomorrow 10:00 AM、Tomorrow 2:00 PM、Monday 9:00 AM、Monday 3:30 PM并附带 ISO 时间戳。「下周一」的计算保证至少距今 2 天以上避免周日/周一时 Monday 与 Tomorrow 语义冲突。前端只在这些字段缺失时才回退到自己的生成器。2.1 Agent 与运行时装配承载该工具的 agent 定义在 showcase/integrations/mastra/src/mastra/agents/index.tsid: interrupt-agent模型gpt-4o-mini仅注册一个工具schedule_meeting系统指令要求只要用户要求预约/安排会议就必须调用schedule_meeting并传入简短topic已知时传attendee工具挂起后由 picker 处理决策agent 不得自行征求批准工具返回后简短确认是否已排期及时间或说明用户已取消。记忆使用LibSQLStoreworking memory 启用schema 为共享的AgentState。resume 能力依赖实例级 storageMastra 实例在 showcase/integrations/mastra/src/mastra/index.ts 中配置了storage: new LibSQLStore({ id: mastra-storage, url: :memory: })挂起的 agentic-loop 快照才能被重新加载完成恢复。前端路由侧则在 showcase/integrations/mastra/src/app/api/copilotkit/route.ts 中将别名gen-ui-interrupt映射到interruptAgent并校验其存在缺失时报 interruptAgent missing from Mastra config。3. 前端实现useInterrupt 低层原语demo 页面在 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/page.tsx完整继承 QA 文档所验证的交互契约CopilotKit runtimeUrl/api/copilotkit agentgen-ui-interrupt div classNameflex justify-center items-center h-screen w-full div classNameh-full w-full max-w-4xl {/* QA: max-w-4xl、full-height 容器 */} Chat / /div /div /CopilotKitChat组件内的关键 hookuseInterrupt({ agentId: gen-ui-interrupt, renderInChat: true, // 内联渲染到转写区非 portal 到 body render: ({ event, resolve }) { // Mastra 将 suspend 值包装为 { type: mastra_suspend, toolName, suspendPayload, ... } // 且 AG-UI adapter 会将其 JSON 字符串化 —— 先 parse再读 suspendPayload const raw event.value ?? {}; const parsed (typeof raw string ? JSON.parse(raw) : raw) as { suspendPayload?: SuspendPayload } SuspendPayload; const payload: SuspendPayload parsed.suspendPayload ?? parsed; const slots payload.slots payload.slots.length 0 ? payload.slots : generateFallbackSlots(); return ( TimePickerCard topic{payload.topic ?? a call} attendee{payload.attendee} slots{slots} onSubmit{(result) { // 延迟 resolve等 React 先提交 picked/cancelled 徽标 // 再让 useInterrupt 清除 interrupt 元素单个 rAF 不可靠 setTimeout(() resolve(result), 500); }} / ); }, });源码注释揭示了三个前端陷阱payload 是包装对象event.value里的原始值不是业务数据而是mastra_suspend包装业务字段topic/attendee/slots在suspendPayload内且整体可能被 JSON 字符串化需要「字符串则 parse、再取suspendPayload取不到就整体当 payload」的容错解包。resolve 必须延迟先让本地 state 把time-picker-picked/time-picker-cancelled徽标渲染出来并 commit再调用resolve(...)否则useInterrupt会立刻清除 interrupt 元素用户看不到「Booked / Cancelled」的只读结果。单个requestAnimationFrame不够可靠demo 使用 500mssetTimeout。slot 兜底若 suspend payload 未携带 slots回退到 showcase/integrations/mastra/src/app/demos/_shared/interrupt-fallback-slots.ts 的generateFallbackSlots()与后端生成逻辑镜像同样相对Date.now()计算避免硬编码日期一周内过期。3.1 TimePickerCard 三态状态机卡片组件 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/_components/time-picker-card.tsx 由本地 statepicked/cancelled驱动三种互斥形态每种形态对应 QA 文档断言的data-testid形态data-testid内容可交互初始time-picker-card头部Book a calloutline 徽标 若有With {attendee}行 主题标题 Pick a time that works for you. 描述2x2 grid 恰好 4 个 slot 按钮time-picker-slot底部 ghost 按钮 None of these worktime-picker-cancel已选择time-picker-picked绿色Bookedsuccess 徽标 所选 label 加粗显示交互卡片整体卸载已取消time-picker-cancelled红色Cancelled徽标 No time picked.防重复提交由disabled picked ! null || cancelled保证首次点击后立即禁用所有按钮因此快速双击只会提交一次选择。提交回调的 payload 严格对齐后端resumeSchema选择 slot 时发送{ chosen_time: s.iso, chosen_label: s.label }点击取消时发送{ cancelled: true }。3.2 建议项Suggestion Pills建议项在 showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/suggestions.ts 中通过useConfigureSuggestions注册available: always两条 verbatim 标题Book a call with sales消息Book an intro call with the sales team to discuss pricing.Schedule a 1:1 with Alice消息Schedule a 1:1 with Alice next week to review Q2 goals.QA 文档要求校验的是 pill 标题的逐字内容而非消息体。4. QA 验证流程完整检查清单以下清单完整继承自 showcase/integrations/mastra/qa/gen-ui-interrupt.md可直接作为回归验收脚本执行。4.1 基础功能Basic Functionality访问/demos/gen-ui-interrupt页面在 3 秒内渲染完成CopilotChat位于max-w-4xl、全高、rounded-2xl容器中首次加载时输入框 placeholder 可见、转写区为空且页面上不存在data-testidtime-picker-card发送 Helloagent 仅以纯文本回复不出现 picker —— 只有明确要求预约/排期时 agent 才会调用schedule_meeting。4.2 功能专项检查Feature-Specific Checks建议项Suggestions两个建议 pill 均可见标题逐字为 Book a call with sales 与 Schedule a 1:1 with Alice。中断触发 内联渲染useInterrupt 低层原语点击 Book a call with sales或手动输入 Use schedule_meeting to book an intro call with the sales team about pricing.60 秒内 agent 调用schedule_meeting后端工具suspend()且 picker内联出现data-testidtime-picker-card确认该卡片是聊天转写区的后代节点未 portal 到body区别于hitl-in-app的模态形态——断言不存在body [data-testidtime-picker-card]卡片头部显示 Book a call eyebrow 徽标、主题标题topic以及 Pick a time that works for you. 描述2x2 网格中恰好 4 个 slot 按钮data-testidtime-picker-slot标签为后端生成的相对时间Tomorrow 10:00 AM、Tomorrow 2:00 PM、Monday 9:00 AM、Monday 3:30 PM网格下方存在 None of these work ghost 按钮data-testidtime-picker-cancel。选择 slot 的恢复路径Pick-a-Slot Resume Path点击四个 slot 之一例如 Monday 9:00 AM卡片切换为data-testidtime-picker-picked—— Booked 成功徽标 所选 label 加粗 —— 且交互卡片卸载不再有time-picker-cardagent 恢复运行并产出确认消息后端返回Scheduled {topic} for {chosen_label}.agent 据此回复。取消路径Cancel Path发送 Use schedule_meeting to book a 1:1 with Alice next week to review Q2 goals.新的 picker 内联渲染data-testidtime-picker-card当 agent 提供了 attendee 时eyebrow 旁出现 With Alice 行点击 None of these work卡片切换为data-testidtime-picker-cancelled—— Cancelled 徽标 No time picked.agent 恢复运行并回复会议未被排期。多轮Multi-Turn在一次选择或取消之后再发送一条预约指令确认新的独立 picker 渲染旧卡片保持已解决态interrupt 生命周期干净地重放第二次 resume 端到端可用。契约检查interrupt 是低层原语Contract Check仅工具触发路径会渲染 picker普通对话消息如 Whats the weather?不应出现 picker不出现审批对话框式模态本 demo 是内联而非 modal注意picker 展示的topic来自模型的工具调用参数非确定性—— QA 不要对 topic 文案做断言。4.3 错误处理Error Handling发送空消息应为 no-op快速双击 slot 按钮仅提交一次选择首次 pick/cancel 后按钮即禁用贯穿 pick / cancel / 多轮全过程无未捕获的 console 错误。4.4 预期结果Expected Results聊天页 3 秒内加载完成纯文本响应 10 秒内返回收到预约类提示后 60 秒内内联渲染 pickerpicker 通过 slot 按钮{chosen_time, chosen_label}或 None of these work{cancelled: true}解决解决后卡片只读agent resume 产生的确认消息引用了所选 slot 或取消事实无布局破坏、无未捕获 console 错误、单次 interrupt 不出现重复 picker。5. 关键要点与陷阱小结Mastra 的中断是工具级原生 suspend而非 LangGraphinterrupt()那样的图节点机制桥接层负责把 suspend chunk 翻译为 AG-UI interrupt 事件族on_interruptCUSTOM RUN_FINISHEDinterrupt-outcome。return suspend(...)与解构位置是后端两个最易踩的坑前者写错会让循环越过暂停点后者写错会让suspend为undefined并引发工具调用死循环见 interrupt.ts 中对应注释。resume 需要实例 storagenew Mastra({ storage })缺省时挂起的循环快照无法重载恢复链路断裂。前端解包 延迟 resolvemastra_suspend包装、JSON 字符串化、以及 500ms 延迟 resolve 三者共同保证「先展示结果徽标、再清除 interrupt 元素」的视觉正确性。确定性边界slot 标签由后端按当前时间生成四个固定相对标签但topic由模型产出、不可断言QA 断言应锚定data-testid、按钮数量、徽标文案等结构化特征。内联 vs 模态renderInChat: true使卡片成为转写区后代节点如需应用表面模态approval-dialog 风格参照同仓库的hitl-in-appdemo二者是同一 interrupt 机制在不同渲染面上的用法。【免费下载链接】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),仅供参考