ARTICLE DETAIL

资讯详情

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

Cursor SDK 流式与 Run 生命周期:run.stream() 事件类型、wait() 语义与取消机制完整实践

Cursor SDK 流式与 Run 生命周期:run.stream() 事件类型、wait() 语义与取消机制完整实践 Cursor SDK 流式与 Run 生命周期run.stream() 事件类型、wait() 语义与取消机制完整实践【免费下载链接】pluginsCursor plugin specification and official plugins项目地址: https://gitcode.com/GitHub_Trending/plugins125/plugins本文基于 Cursor 官方插件仓库plugins125/plugins中cursor-sdk插件的参考文档 streaming.md 展开系统讲解cursor/sdk中run.stream()的异步事件流消费模型、9 种SDKMessage事件类型、run.wait()的终态语义、取消机制与背压处理。读完后你能独立编写一个跨 local/cloud 运行时通用的流式消费者正确区分启动失败与运行失败两类错误并避免漏排空流、误读FINISHED状态等常见坑。一、背景run.stream() 是什么在 Cursor TypeScript SDKcursor/sdk中一次agent.send(prompt)返回一个Run句柄Run暴露四个核心操作stream、wait、cancel、conversation。其中run.stream()是一个SDKMessage事件的异步生成器async generator——这是整个流式体系的基石。最关键的设计事实local 与 cloud 两种运行时的事件形状完全一致。写一个消费者就能在两种运行时上通用这正是 streaming.md 开篇强调的原则Same event shapes for local and cloud runtimes — write one consumer and it works everywhere.这个插件的结构也印证了这一点cursor-sdk插件由一份简短的入口 SKILL.md 和七份按需加载的参考文档组成runtime-choice、auth、error-handling、streaming、mcp、advanced、patterns其中 streaming 参考正是为消费流、选择事件类型、取消运行、stream 与 wait 二选一这类任务准备的见 SKILL.md 的路由表。二、何时用 stream何时只用 wait()原文档给出了一张决策表这是消费流之前必须先做的选择场景用 Stream?用wait()?向用户实时渲染输出CLI、聊天、Web UI是是只需要成功/失败的发射后不管脚本否是想在 CI 中记录工具调用以便调试的步骤是输出到 stderr是可观测性记录所有事件以便事后回放是持久化存储是轮询一个非本进程发起的 run用 Stream 也可以是表格里wait()一列全是是这不是巧合。原文档有一句值得直接记住的话You almost always wantwait(). You sometimes dont wantstream(). There is no stream without wait pattern thats correct — the stream tells you what happened,wait()tells you whether it succeeded.换言之stream 回答发生了什么wait()回答成功了吗两者职责正交不存在只要流、不要 wait的正确写法。SKILL.md 的 Top Five Traps 第 4 条 也把它列为新人必踩的坑run.stream()是观察手段run.wait()才是获取终态结果的手段跳过wait()你既无法判断 run 是完成、出错还是被取消还会泄漏 run 内部的 watchers。不想要实时输出的话单独调用wait()即可。三、标准消费者The Canonical Consumer以下是原文档给出的标准消费循环覆盖了全部 9 种事件类型可直接复制到项目中作为起点for await (const event of run.stream()) { switch (event.type) { case assistant: for (const block of event.message.content) { if (block.type text) process.stdout.write(block.text); // block.type tool_use means the assistant announced a tool call; // the actual execution will follow via tool_call events. } break; case thinking: // Reasoning content. Usually hidden from end users, kept for logs. process.stderr.write([thinking] ${event.text}\n); break; case tool_call: console.error([tool] ${event.name} ${event.status} (${event.call_id})); if (event.args ! undefined) console.error( args: ${JSON.stringify(event.args)}); if (event.result ! undefined) console.error( result: ${JSON.stringify(event.result)}); break; case status: console.error([status] ${event.status}); break; case task: if (event.text) console.error([task] ${event.text}); break; case user: // Echo of the prompt. Usually ignorable. break; case system: // Init metadata (model, tool list). Useful for logs. break; case request: // Request tracking. Log event.request_id for correlation. break; } } const result await run.wait();注意代码结构中的两个细节tool_use块与tool_call事件是两回事assistant消息里的tool_use块只是模型宣布要调工具真正的执行生命周期由后续tool_call事件承载。做 UI 时可以利用这个时间差——在 LLM 刚提出调用的瞬间就显示正在调用grep…。循环结束后才await run.wait()流自然结束时再取终态顺序不能颠倒。SKILL.md 的第三种调用模式示例 展示了这个循环在真实集成中的位置Agent.createagent.send 流式渲染 wait()finally中agent[Symbol.asyncDispose]()四者构成完整的生命周期闭环。四、事件参考Event Reference所有事件都携带agent_id和run_idtype字段判别其余内容。逐一说明4.1assistant—— 模型文本与工具调用声明模型输出的文本或工具调用声明{ type: assistant, message: { role: assistant, content: ArrayTextBlock | ToolUseBlock } }TextBlock{ type: text, text: string }—— 这就是要渲染给用户的正文。ToolUseBlock{ type: tool_use, id, name, input }—— assistant 在请求调用某个工具。你不需要对它做任何响应运行时会自动执行并通过tool_call事件回报结果。它的价值在于让 UI 能即时展示模型想干什么。4.2thinking—— 推理内容{ type: thinking, text: string, thinking_duration_ms?: number }处理原则一句话远离主 UI留在日志里。终端用户不需要看到推理过程但调试时它极其有价值invaluable when debugging。标准消费者里把它写到process.stderr正是这个取舍的体现。4.3tool_call—— 工具执行生命周期这是流里信息量最大的事件类型{ type: tool_call, call_id: string, name: string, status: running | completed | error, args?: unknown, result?: unknown, truncated?: { args?: boolean; result?: boolean } }生命周期语义先以status: running发射一次此时args可用result未定义再以status: completed或error发射第二次此时result可用。truncated标志表示该 payload 已被服务端裁剪——原文档明确提醒不要试图完整解析它。做结构化解析时务必先检查args ! undefined/result ! undefined见第七节反模式第 2 条。4.4status—— 运行生命周期迁移与SDKStatusMessage对齐取值与含义status值含义CREATINGCloud run 正在初始化clone、启动 VMRUNNING正在执行FINISHED成功完成ERROR运行中途失败CANCELLED运行被取消EXPIRED运行因超龄被回收其中CREATING阶段是 cloud 运行时特有的见 runtime-choice.mdCursor 会 provisioning 一台 VM、按startingRef克隆仓库、运行 agent、推送分支并按需开 PRlocal 运行则直接进入RUNNING。关键警告不要把FINISHED状态事件当作可以跳过wait()的信号。它只是提前通知heads-up不是终态结果。wait()返回的RunResult里包含 stream 拿不到的 usage、duration、git 信息。patterns.md 的 GitHub Action 示例 就是这一点的落地流里只打印status和tool_call摘要最终以result.status ! finished决定退出码0完成 /1启动失败 /2运行出错 /75可重试的瞬态失败。4.5task—— 高层任务进度如 Planning、Editing files 之类的摘要式进度消息。可选项适合做进度条式的简化 UI。4.6user—— 提示词回显run 开始时回显的用户 prompt消费者通常直接忽略标准消费者中就是空分支。4.7system—— 初始化元数据实际使用的模型、工具目录等。建议在流开始时记录一次日志即可。4.8request—— 请求追踪携带request_id用于与服务端可观测系统做关联。如果你有内部 tracing 体系日志里带上event.request_id。五、回调 vs 流onDelta / onStep除了run.stream()agent.send(...)本身也接受回调await agent.send(prompt, { onDelta: ({ update }) { /* raw executor delta */ }, onStep: ({ step }) { /* batched step after text/thinking/tool settle */ }, });两者的粒度与语义差异onDelta每个原始 executor delta 都会触发粒度比SDKMessage流细得多适合需要块内增量更新的本地 UI。集成场景里很少用到。onStep一个逻辑步骤完成时触发text thinking tools 打包在一起相当于把流里的assistanttool_call预组装好了再给你。一个容易被忽略的机制细节两个回调都会被 await之后才会投递下一个更新——即你可以通过返回 Promise 施加背压backpressure。原文档因此警告onDelta里不要放慢 I/O否则可能拖住整个 run。选择建议原文档原话的展开大多数消费者优先用run.stream()只有构建本地 UI、且确需更细粒度更新形状时才考虑onDelta/onStep。六、取消机制Cancellationif (run.supports(cancel)) { setTimeout(() run.cancel(), 30_000); }要点有三local 与 cloud 都支持run.cancel()。cloud 侧的实现在服务端POST 到服务端的 cancel 端点并用服务端的权威响应回调解本地状态。依然要用run.supports(cancel)守卫。通过Agent.getRun(...)拿到的 detached/replayed 句柄可能已经没有活跃的取消通道守卫是正确的防御姿态。这与 SKILL.md 陷阱 5 呼应Run的四个操作并非每个运行时都支持调用前一律先run.supports(...)不确定原因时可用run.unsupportedReason(op)查询。取消之后继续消费流直到结束你会收到一个终态status事件随后run.wait()以status: cancelledresolve。不要一取消就断开消费者——那正是第七节第一个反模式。七、免流观察Status Listener只关心状态迁移、不关心内容时有一个不需要stream()的轻量观察口const unsubscribe run.onDidChangeStatus(status { console.error([status-listener] ${status}); }); await run.wait(); unsubscribe();它不依赖流每次状态迁移都会触发适合只画进度指示、不渲染 agent 输出的界面如后台任务面板。用完记得调用返回的unsubscribe()。八、观察一个非本进程发起的 Run原文档给出了跨进程观察的完整范式const existing await Agent.getRun(runId, { runtime: cloud, agentId: bc-abc123, apiKey }); if (existing.supports(stream)) { for await (const event of existing.stream()) { // same loop } } const result await existing.wait();这里有两个来自 advanced.md 的配套事实必须一起理解回放流的保真度取决于原始 run 是否持久化了事件。Agent.getRun拿到的回放流是从持久化状态重建的原始 run 捕获了工具 payloadargs/result就有更早的 run 可能更稀疏。bc-前缀是 cloud agent ID不是 run ID。只拿到 run ID比如从日志或 webhook时把 runtime 提示传给Agent.getRun想从bc-agent 找它的 run ID用Agent.listRuns(bc-abc123, { runtime: cloud, apiKey })遍历。正因为句柄可能已脱离活跃事件存储run.supports(stream)守卫在跨进程场景下是必需而非装饰。另一个跨进程观察的补充手段是run.conversation()它返回累计的ConversationTurn[]适合渲染会话回放 UI本地活跃 run 会包含工具调用细节回放 run 则可能较稀疏cloud 侧conversation()是支持的它从流中尽力累计。九、背压与长流异步迭代器天然具备背压——运行时不会以快于你for await排空的速度产生事件。这带来一个反直觉的后果如果你的消费者在单事件里做重活写数据库、发网络请求你会以 run 的整个生命周期为尺度把它卡住。原文档的处方是当这件事重要时入队queue并带外out of band处理而不是在消费循环内同步执行重 I/O。这与第五节回调被 await的背压机制是同一个设计哲学的两面。十、常见错误Common Mistakes原文档列出的四个高频错误逐条展开没有排空流Not draining the stream——资源会保持打开。只要你开了run.stream()就必须消费完或者调用run.cancel()。这是长期运行服务中最隐蔽的泄漏来源之一。假设 tool 的args/result总是存在——它们是可选的解构前先判空标准消费者里的if (event.args ! undefined)就是防御姿势。不检查name就把tool_call.result解析成特定形状——每个工具都有自己的 result 形状。需要强类型时先按event.name分支再解析 payload。把status: FINISHED当作结束——它是终态状态但wait()仍必须 resolve 才能给你 usage/git/duration。永远同时await run.wait()。这四条加上 SKILL.md 陷阱 2 的两类失败模型error-handling 分类CursorAgentError抛出 run 根本没启动修环境后重试result.status error agent 干了活但活干砸了检查 transcript/git/工具输出构成了流消费 错误处理的完整防御清单。十一、小结一条可执行的心智模型把streaming.md与同 skill 其他参考串起来消费一次 Cursor SDK run 的正确姿势可以压缩成五条agent.send()之后先打印agent.agentId和run.idSKILL.md 生产实践第 3 条流卡住时这两个 ID 是排查的起点需要实时输出就for await (const event of run.stream())按type分支处理 9 种事件assistant渲染正文、tool_call记日志、status/task画进度user/system/request按需记录流结束或取消后必须await run.wait()用result.status区分 finished / error / cancelled并据此选择退出码对cancel、stream、conversation一律先run.supports(...)守卫尤其在Agent.getRun回放场景消费者内不做慢 I/O重活入队带外处理让背压自然生效。以上全部内容可在当前仓库中对照源码级文档验证入口路由见 SKILL.md运行时能力矩阵cancel 在 local/cloud 的语义差异见 runtime-choice.md跨进程检查与conversation()细节见 advanced.md五种真实集成模板CI 评审、定时任务、CLI、后端服务、扇出见 patterns.md插件总览见 cursor-sdk/README.md。【免费下载链接】pluginsCursor plugin specification and official plugins项目地址: https://gitcode.com/GitHub_Trending/plugins125/plugins创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表