Files
cursor-byok/docs/一次性重构计划计划.md
T

94 KiB
Raw Blame History

cursor-server 一次性重构计划

重构目标:/Users/leokun/Documents/cursor-byok/cursor-server

本计划面向当前 Cursor 客户端,也明确支持后续其他客户端。多客户端不是预留空目录,而是本轮必须建立的真实边界:协议无关 Loop 不得拥有 Cursor protobuf、数字 wire_id、Blob、checkpoint 或 RunSSE 生命周期。

1. 依据和执行边界

开始前必须完整阅读:

  • /Users/leokun/Documents/cursor-byok/AGENTS.md
  • /Users/leokun/Documents/cursor-byok/Cursor上下文与状态同步抓包分析.md
  • cursor-server/src/run/engine.rs
  • cursor-server/src/cursor/
  • cursor-server/src/cursor/prompting/
  • cursor-server/src/provider/
  • cursor-server/src/model/
  • cursor-server/src/store/
  • prompt/

不要参考 main 分支中的旧实现设计协议行为。

所有 Cursor 协议结论只允许来自:

  1. 当前 protobuf。
  2. 当前抓包数据库。
  3. 《Cursor上下文与状态同步抓包分析.md》中已经确认的结论。
  4. 当前已通过真实 Cursor 客户端测试的代码。
  5. 本机运行的 Cursor.app 客户端代码交叉验证。

缺少 Cursor 协议证据时,不得通过 fallback、兼容分支或猜测补齐。未来客户端也不得继承未经证明的 Cursor 假设;它必须通过自己的 adapter 转换统一运行模型。

1.1 本轮 checkpoint 复核证据

本轮已交叉检查 protobuf、抓包数据库和本机 Cursor.app 实现,计划必须以下列事实为准:

  • 抓包来自 /Users/leokun/Library/Application Support/cursor-byok/cursor-proxy-debugger.db。旧版完整 RunSSE exchange 4521 用于复核 ToolRound;当前 Cursor 3.16.17 以 exchange 9005 为主,并用最近 50 条完整 Run 交叉验证终局时序。

  • 客户端恢复逻辑来自 /Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js 中实际运行的 checkpoint stream、retry/resume 和 composer checkpoint handler。

  • AgentServerMessage.conversation_checkpoint_update 只是 RunSSE 下行消息;protobuf 中没有 checkpoint 持久化 ACK。

  • 真实上行确认是 KvClientMessage.set_blob_result(id),它与 KvServerMessage.set_blob_args(id) 配对。checkpoint 不得引用尚未收到 SET ACK 的新 Blob。

  • ConversationStateStructure.pending_tool_calls 虽然字段名是 tool calls,实际元素不是 call_id 或 BlobID,而是内联的完整 assistant provider-message JSON。exchange 9005 的工具阶段中,一个元素同时包含 reasoning、3 个 tool call 和 Cursor execution contracts;终局纯文本阶段的一个元素则只包含 reasoning 与 text,没有 tool call。

  • pending contract 的 toolIdentifier 是 Cursor wire 枚举名,不是直接复制工具显示名。现有抓包明确覆盖 Read→READStrReplace→STR_REPLACEAwaitShell→AWAITCallMcpTool→MCPCreatePlan→CREATE_PLAN_V2UpdateCurrentStep→COMMUNICATE_UPDATE;该映射只属于 Cursor projectorLoop 继续只认识工具 name/call_id。

  • 因此 checkpoint 是两阶段投射。工具阶段先保持稳定历史不变并发布 pending=1 的完整 assistant 批次,结果齐全后把该 assistant 与 3 个 tool result 折叠进稳定历史并发布 pending=0。exchange 9005 对应的 root_prompt_messages_json 数量为 31 → 31 → 35,恰好新增一条 assistant 和三条 result。

  • exchange 9005 的完整 checkpoint 序列是 frame 467/493/504/526/543/565/579/613/625/1643/1644/1645,对应 (stable roots, pending)(31,0) → (31,1) → (35,0) → … → (48,0) → (48,1) → (49,0) → (49,0)。staged checkpoint 可能因 Blob ACK 与 UI 工具完成并行而较晚出现在流中;恢复语义取决于其冻结内容和 turn_ended eligibility,不以它和单个 ToolCallCompleted 的偶然先后推导额外屏障。

  • 四个工具轮的 settled checkpoint 都严格早于下一轮首个模型 interactionframe 504 < 505543 < 544579 < 580625 < 626。因此“所有 ToolResult 已落盘”还不是继续调用模型的全部条件;Cursor adapter 必须先完成 settled Blob ACK barrier 并发布 settled checkpoint。这里不存在 checkpoint wire ACK,核心等待的是与该次 StateCommitted 一一配对的进程内 client-state completion;两者不能混称。

  • 上述每个 checkpoint 的旧 root_prompt_messages_json 都逐项保持为下一份的字节级前缀;settled 只追加本轮 assistant/result roots,不重新编码历史 roots。这既是 Blob 图的不可变性,也是上游 LLM 前缀稳定的直接证据。

  • turns 不是 LLM stable roots 的别名。exchange 9005 从 frame 4931643 的同一个当前 Turn,其 Step 数量为 32 → 35 → 36 → 39 → 40 → 43 → 46 → 48 → 50UserMessage BlobID 始终不变,上一份 Step BlobID 序列始终是下一份的字节级前缀,但包住这些引用的当前 Turn 每次得到新的 BlobID。也就是说,未变化的 UserMessage/Step 节点必须复用,当前 Turn wrapper 随追加的 UI 状态更新;不能把“当前 Turn 更新”误写成“所有 Turn/Step 全量重建”。adapter 因此需要一份当前 Run 内、冻结值的 Cursor presentation snapshot,但它不是 canonical messages 的第二事实源。

  • ConversationStateStructure 的其余字段不能笼统称为“全部冻结”。exchange 9005read_paths 在 frame 579 → 625 从 11 项增到 12 项,新增值正是本轮成功 Read 的文件;因此成功的 typed Read completion 要确定性推进该集合。相反,token_details.used_tokens 是 Cursor 的上下文估算快照,不等于 Provider 返回的计费 usage;没有同等算法证据时保留客户端基线,不能拿 input/output usage 猜一个值。其他未由本服务明确拥有的 file/subagent/workspace 元数据保持客户端传入值。

  • 最终纯文本 assistant 也走同一两阶段投射:turn_ended → pending=1 → pending=0 → 相同 pending=0 重发 → EndStream。exchange 9005 对应稳定根数量为 48 → 48 → 49;最近正常完成的抓包稳定重复这一顺序。这里的 pending=1 是本轮最终 assistant 的暂存态,不是旧 ToolRound job。

  • exchange 9005 的最终 Blob SET 为 RunSSE frame 1638..1641,解码后分别是 thinking Step、assistant Step、更新后的 Turn 和 assistant root JSON;同一 request 的 Bidi id=122..125 均返回成功 set_blob_resultRunSSE 随后是 frame 1642turn_ended1643..1645 的两阶段 checkpoint。抓包未给两条 HTTP 流的单帧统一时间戳,只能证明四个 ACK 在 RunSSE 结束前到达;实现采用更强的确定性顺序,在全部 ACK 后才解除 final barrier。protobuf 直接内联发送 ConversationStateStructure,没有“checkpoint 自身 Blob”;服务端只能 SET 它实际引用的新节点。

  • frame 1643/1644/1645 的最后一个 Turn BlobID 完全相同;三帧分别是 (roots=48,pending=1)(roots=49,pending=0)、同一 settled 重发。终局 presentation 因此只冻结、消费一次:staged 与 settled 复用同一 Turnsettled 只推进 stable root。不能把同一个 presentation delta 分别应用给 staged 和 settled,否则会重复追加 thinking/text Step,并破坏 Step 前缀。

  • Cursor.app 将 turn_ended 之前的 checkpoint 标为 eligible,将其后的 checkpoint 标为 ineligible_terminal_turn。未见 turn_ended 就断流会按最新 eligible checkpoint 自动恢复;终局 checkpoint 后断流则不再自动恢复。

  • Cursor.app 的这个判定也暴露了一个很窄但真实的边界:若传输恰好断在 turn_ended 与第一份 ineligible_terminal_turn checkpoint 之间,客户端尚未见 terminal checkpoint,仍可以从上一份 eligible checkpoint 进入 retry/resume。这是官方已有时序的恢复语义,不能用服务端自造 checkpoint ACK 或改变终局帧顺序“修正”。恢复后是否重做末轮模型调用,只由客户端实际带回的 eligible state 决定。

  • Cursor.app 在本地把 checkpoint 写入 conversation/composer 状态,并在 stream 结束时等待本地写入完成;这是客户端内部持久化,不会回传给服务端。

  • 恢复时客户端使用最新 eligible checkpoint 作为新 conversation_state,并把 action 改为 resume_action。历史回滚也是选择旧 checkpoint,不是请求服务端删除 Blob 或 messages。

  • eligible checkpoint 若含 pending assistant,恢复动作不是再次请求 LLM。adapter 必须从这条完整 JSON 恢复 assistant、原始有序 calls 和 provider replay capsule,核心重建 durable ToolRound 并重新执行该批尚未进入 stable history 的工具;全部结果提交后才进入下一轮 LLM。纯文本 pending 只出现在 turn_ended 后的 terminal checkpoint,不属于自动恢复入口。

  • pendingToolCallStartedAtMs 是 pending assistant 内的冻结毫秒时间。工具阶段使用 durable ToolRound 的创建时间,并在 eligible checkpoint 恢复时一并读回;终局文本 staged 构造时只生成一次,settled 状态不再携带 pending assistant。任何 checkpoint 重建都不得重新生成历史 started/completed/thinking 时间,也不得写死为 0

  • Cursor AI-SDK message 的 id 不是 canonical 唯一键,也不是 ToolRound 身份。exchange 9005 中 frame 496/535/571/617 的四个不同 assistant 工具批次,其 id 都是字符串 "1",但 calls、内容和 BlobID 均不同;对应 tool result root 的 id 则等于 toolCallId。导入时必须由 root BlobID/位置确定性产生内部 MessageId,并为每个 assistant batch 恢复独立 ToolRoundId;生成后缀时 Cursor projector 明确产生这两类 wire id,不能把内部 MessageId 泄漏进 wire JSON,也不能用 wire id 分组 Provider messages。

  • reasoning signature 是 Cursor wire 上的不透明值。exchange 9005 的 pending reasoning 使用一个 707 字符的 URL-safe opaque signature,并不等同于本服务生成的 base64(JSON ProviderReplayState)。adapter 必须区分自有 envelope 与未知 Cursor signature:自有 envelope 可恢复给对应 Provider,未知值只能原样保留并在 Cursor checkpoint 中 round-trip,不能用 .ok() 静默丢弃,也不能交给无关 Provider 猜测。

  • SelectedImage 也遵循 Blob/CAS 语义。Cursor.app 的 _gatherImageSelections 读取并缩放图片、计算内容 hash、写本地 Blob store,并在首发 UserMessage 中使用 blobIdWithData { blob_id, data };恢复 helper 同时处理 datablobIdblobIdWithData,后两者需要时从 Blob store 取回。当前双图抓包分别包含 JPEG 与 PNG 的真实 MIME、BlobID 和 bytes。

  • 服务端因此把文本与图片放在同一条有序 canonical user message 的 typed parts 中,图片只保存 MIME 与 bytesUUID/path 不进入 ModelRequest。Chat、Responses、Anthropic 分别投射为自己端点的 image content。服务端自有 Cursor root 使用 {type:"image", data:<base64>, mimeType} 并有 round-trip 测试;现有抓包没有暴露官方旧 checkpoint 中 selected image root 的精确 JSON,因此不声称该字节格式已被官方 root 直接验证。

  • 子代理 Bidi 抓包同时携带 X-Parent-Request-IdX-Parent-Agent-Tool-Call-Id;前者与父 Run 的 run_id/request_id 相同,后者与父 Task call 对应。两者是一个原子 parent reference,缺任一字段都不能猜测。它们只在 Cursor request adapter 转为 RunKind::Subagent { parent_run_id, parent_tool_call_id, ... },不进入 Provider 请求。

2. 总体目标

当前服务已经跑通:

客户端请求
→ 构造 LLM 请求
→ 消费 Provider 流
→ 投射文本、thinking、tool call
→ 等待工具结果
→ 追加 messages
→ 下一轮 LLM
→ 客户端状态发布
→ 结束 Turn

本轮不是重新实现功能,而是整理职责、修复错误边界,使核心 Loop 短小、直接、可验证。

最终要求:

  • run/engine.rs 只表达协议无关 Loop。
  • client/ 是客户端与 Loop 之间唯一的通用命令和事件边界。
  • Cursor protobuf、Connect、RunSSE、BidiAppend、数字 wire_id、Blob 和 checkpoint 只存在于 cursor/
  • 未来客户端作为 cursor/ 的同级 adapter 接入,不修改核心 Loop。
  • Provider 端点适配只存在于 provider/
  • Prompt 和工具资产属于对应客户端 adapter。
  • 不可变 messages 与有序 conversation revisions 共同构成上下文事实源。
  • 每个运行期状态只有一个明确所有者。
  • 删除过时路径,不保留兼容层。
  • 不为了拆文件而拆文件。
  • 暂时保持单个 Rust crate;没有真实独立编译或复用需求时不拆 crate。

3. 当前主要问题

重构前的 run/loop_engine.rs 同时负责 Cursor 请求解析、模式和模型选择、MCP、runtime tag、Prompt、Provider 流、tool-call 聚合、Cursor interaction、工具等待、messages、Blob/checkpoint、usage、错误、取消和 Turn 生命周期;该路径已由 run/engine.rs + run/model_cycle.rs + cursor/session.rs 替代,收尾时只确认旧文件已删除,不保留 re-export。

Cursor 工具实现又分散在:

  • cursor/tools.rs
  • cursor/exec.rs
  • cursor/tool_result.rs
  • cursor/tool_stream.rs
  • cursor/edit.rs
  • cursor/pending.rs

这些文件已有可用实现,应按状态所有权和协议阶段整理,不得重写平行实现。

当前 prompting/ 只服务 Cursor Agent,应移动到 cursor/prompting/。未来客户端有自己的 Prompt 时,由自己的 adapter 编译;通用 Loop 只接收已确定的 Prompt 规格、工具定义和 messages。

Prompt 资产还存在重复、跨模式 schema 漂移,以及资产已删除但代码仍加载 Mode::Commit 的失配。不同模式工具集合不同,Agent 工具清单不能覆盖其他 mode;但抓包已确认主代理和子代理使用相同基础 system prompt,不能人为拆成两份静态资产。

本轮复核确认并已用测试固定的错误边界:

  • stable root Blob 必须是 Cursor 使用的 AI-SDK message JSON,不能序列化内部 CanonicalMessagepending_tool_calls 必须是完整 assistant JSON,不能写 call_id。checkpoint 只在 assistant staged/settled 边界发布,中间 ToolResult 不制造快照。
  • ModelRequest 不含 model_call_id;共享中性投影保持 typed role、assistant、call 和 result,不使用 OpenAI JSON 充当通用模型;ToolRound 只按 call index 恢复 callsresults 保持 completion_seqModelCycle 最多交付一次 Provider 明确报告的 usage 和一个已聚合 replay stateLength/Incomplete 失败。
  • Cursor wire message id 已与内部 MessageId/ToolRoundId 分离;官方重复 id="1" 不再冲突或跨轮合并。opaque Cursor signature 原样 round-trip,自有 Provider replay 使用带版本的 envelope。
  • Cursor presentation 以增量所有权交给 checkpoint workerworker 独占 root/Turn frontier,只追加新 root/Step、复用 UserMessage/旧 Step 引用并重建当前 Turn wrapper。每份 delta 在 frontier 上只能消费一次;staged/settled 需要相同展示值时复用消费后得到的 Turn ID,而不是再次应用 delta。thinking duration 和 typed ToolCall 时间/结果来自真实事件,不再从 canonical messages 重建。
  • Cursor 原始 effort/reasoning/thinking/fast/context 已在 request adapter 一次归一为 ReasoningSpecModelLatency 和 context-window 元数据;未知参数直接 Protocol failure。Provider 不再读取 Cursor 字段名。Responses 不再隐式补 mediumAnthropic 的必填 max_tokens 只允许来自 ModelSpec 或显式 CURSOR_PROVIDER_MAX_OUTPUT_TOKENS
  • ModelRequest 现在只有 RunEngine 一个构造位置,转发式 PromptCompiler::compile 已删除;全目标 cargo clippy --all-targets -- -D warnings 已通过,不以 allow 隐藏所有权问题。
  • Chat、Responses、Anthropic 均已有原始 HTTP request + SSE fixture,分别验证端点请求字段、终止事件、usage 和 replay stateModelCycle 的合成事件测试只保留通用状态机职责。
  • Blob hydration 对 pre-fetched 与 KV GET 数据使用同一 SHA-256 校验;KV GET 成功后写入本地 CAShash 不匹配走 typed Protocol error,取消/超时清理 pending GET。恢复路径不能仅因 protobuf 可解码就信任错误内容。

当前结构已经建立固定端点 Provider factory、ModelSpec/子代理模型解析、typed text/image history、Run span、revision/call/Blob ACK 结构化观测,以及 Exec/Interaction 共用的当前 Run 唯一 wire-id 空间。checkpoint 已按 stable roots、Turn/Step、derived state、recovery 和串行 worker 的真实状态职责落入 cursor/checkpoint/Cursor AI-SDK JSON 已按 encode/decode 落入 cursor/projection/interaction 已按模型流、query 和 typed ToolCall rendering 分开,tool codec 也只保留 request encoding 与 response decoding 两条 wire 方向。后续不能为了降行数继续机械拆文件。

4. Rust 实现原则

  1. 长代码通常意味着职责尚未识别。
  2. 目录即架构,目录边界必须与依赖方向和状态所有权一致。
  3. 只有拥有独立状态或独立不变量的职责才成为模块;纯转发文件应合并回所有者。
  4. 不保留 fallback、旧路径或兼容层。
  5. 不用工具名、客户端名或字符串状态驱动核心生命周期。
  6. 同一运行期状态只允许一个所有者修改。
  7. 使用 enum 和受约束类型表达状态,不使用互相矛盾的 bool 组合。
  8. 每次移动一个完整职责,每一步都保持可编译、可运行、测试通过。
  9. 优先使用现有依赖,不为重构随意增加 crate。
  10. 不引入新 actor frameworkTokio channel、oneshot、CancellationToken 和 RunActor 已足够。
  11. 多客户端是当前明确需求,因此窄的 client port 是必要设计,不是 speculative abstraction;禁止提前设计不存在的客户端能力。
  12. 遵循 TDD:先用失败测试固定不变量,再移动或修改实现。

5. 目标数据流与依赖

HTTP / Connect / 其他传输
            ↓
      Client adapter
  wire 解析、Prompt、工具映射
            ↓
        PreparedRun
            ↓
         RunActor
            ↓
        Run engine
    model cycle ↔ tool round
       ↓               ↑
 Provider adapter   ClientCommand
       ↓               ↑
 ModelEvent         ClientEvent
            ↓
 canonical history projection
            ↓
  revisioned messages store
            ↓
     StateCommitted(state_version)
            ↓
      Client adapter 投射;必要时完成配对 barrier
 Cursor: Blob SET → SET ACK → checkpoint

依赖规则:

  • model/ 不依赖 Cursor protobuf、Axum、SQLx 或具体 Provider JSON。
  • client/ 不依赖具体客户端。
  • run/ 不依赖 Cursor、Blob、checkpoint,也不根据工具名或客户端类型分支。
  • provider/ 不构造客户端事件,不执行工具,不写 messages。
  • store/ 不决定 Loop 或客户端生命周期。
  • cursor/ 可以依赖 client/model/run/store/ 的公开接口。
  • canonical history projection 属于 model/,只做 typed messages 的确定性折叠;Cursor 和各 Provider 分别读取它,二者不能相互依赖。ModelRequest 保存这份中性 history,而不是带 origin/runtime_event_id 的存储对象或任一端点 JSON。
  • Cursor checkpoint 是 durable revision、ToolRound stage、客户端基线元数据和 Cursor adapter 所拥有的确定性增量的协议投射,不是核心消息状态。
  • canonical conversation 保存 revision 父链、当前选中的 revision 和 active_run_id,不保存 Cursor head_blob_idCursor checkpoint 必须由 durable revision、ToolRound version、不可变基线与 typed completion/presentation 增量确定性构造。

6. 客户端通用边界

6.1 PreparedRun

客户端 adapter 把 wire 请求转换为 PreparedRun,至少包含:

  • RunIdConversationIdCursor adapter 可由 request_id 确定性生成 RunId,但通用 store 只保存 run_id,不出现 Cursor request_id 字段。
  • 主 Run 或子代理关系。
  • 不丢失真实类型的 SubagentKind
  • 完整 ModelSpec 和子代理模型策略。
  • 客户端已经编译完成的不可变 PromptSpec,只包含静态 instruction 和有序工具定义。
  • actionStart { initial_messages }Resume。只有真实新用户/Runtime 事件才追加;resume_action 不伪造新 user message。
  • base_revision:adapter 从客户端传入的历史快照解析成的通用 revision。核心不读取 Cursor Blob。

Cursor request parser 必须同时产出:

PreparedRun       # 交给通用 RunActor
CursorRunContext  # 留在 CursorSession

rules、commands、skills、MCP descriptors 和 request context 在 Cursor prompting 内分别编译为静态 PromptSpec 或 initial_messages。CursorRunContext 保存 mode、request_id、父工具关系及 Cursor 专属元数据。CheckpointBuilder 把客户端传入的 root/turn BlobID 当作不可变基线;未被本服务拥有的 file/subagent/workspace 元数据原样保留,成功 Read 产生的 read_paths、Todo/Plan 和 UpdateCurrentStep 等有明确来源的状态才由 typed completion 或选中 revision 确定性推进。后续 checkpoint 只追加新的 stable root 后缀,复用未变化的旧 Turn、UserMessage 和 Step 引用,并只为发生变化的当前 Turn 构造新的 wrapper;不得从 canonical messages 全量重新编码历史图。Cursor mode 和当前 Run 的 presentation snapshot 都只留在 Cursor adapterLoop 不需要知道 Agent、Ask、Plan、Debug、Multitask、Step Blob 或 typed tool result。

Cursor request preparation 必须在产生 PreparedRun 前完成 Blob 图 hydration:先使用 pre_fetched_blobs,缺失引用再通过 KV GET 取得;两条路径都验证 SHA-256(data) == BlobID 并写入同一 CAS。缺 Blob、hash 不匹配或类型无法解析都是明确 Protocol failure,不使用空历史 fallback。完整的 ConversationStateStructure 解析为 canonical base revision 后,RunActor 才可启动。

6.2 ClientCommand

客户端发给 RunActor

  • ToolResult { call_id, content }
  • RuntimeEvent { event_id, message }
  • ClientClosed { error }
  • Cancel

进入通用边界前,adapter 必须完成 wire id 到 call_id 的映射,并把工具结果规范化为字符串。核心不接收 Cursor 数字 id、protobuf oneof、Blob ACK 或 checkpoint 回执。

6.3 ClientEvent

RunActor 发给客户端 adapter

  • text/thinking start、delta、end。
  • tool call start、arguments delta、end。
  • 每轮可选的 Provider usage;端点未报告时不产生事件。
  • ExecuteToolRound { round_id, calls },只在完整 ModelCycle 成功且 ToolRound 已持久化后发出。
  • StateCommitted { revision_id, tool_round_version, cause, barrier },表示某个可投射的不可变状态已落盘;barrier 只在核心继续前必须完成客户端状态投射时存在。
  • 唯一终态 CompletedCancelledFailed { failure }

revision_id 标识有序 messagestool_round_version 标识同一 revision 上 pending/settled 工具状态的变化;两者不冒充对方。ToolRoundStarted 和中间 ToolResult 是顺序通知,不阻塞工具执行;initial state、settled ToolRound 和 final state 带与本次事件一一配对的内部 completion barrier。Cursor 在 initial/settled 时完成 Blob SET ACK 与 checkpoint 发布后解除 barrier,在 final 时完成 staged/settled Blob 构建和 ACK 后解除,随后核心才能开始首轮/下一轮模型或进入 Completed。无需快照的客户端立即完成该句柄即可。它不经过 ClientCommand 队列,不会与 ToolResult/RuntimeEvent 错位,也不是 protobuf 中不存在的 checkpoint ACK。

7. 状态所有权

状态 唯一所有者
不可变 messages store/messages.rs
revision 父链、当前 head 和 active Run store/conversations.rs
当前 Run 生命周期 run/actor.rs
单轮 Provider 流状态 run/model_cycle.rs
当前工具批次和 call_id run/tool_round.rs
Cursor wire_id 映射 cursor/tools/runtime.rs
Cursor request_id 会话路由 cursor/sessions.rs
canonical history 折叠 model/projection.rs
Cursor checkpoint wire 投射 cursor/projection/cursor/checkpoint/
Cursor Blob/checkpoint 发布 cursor/checkpoint/worker.rs
Cursor Prompt 和 mode cursor/prompting/
Provider 原始 SSE/JSON 对应 provider adapter
Provider 路由配置 provider/router.rs
Cursor 会话内 Blob SET 待确认 cursor/blob_sync.rs

运行期关系:

RunRegistry
└── ConversationId → ActiveRun
                      ├── RunId
                      └── CancellationToken

CursorSessionRegistry
└── Cursor request_id → CursorSession
                        └── CursorToolRuntime
                            ├── wire_id → call_id
                            └── call_id → pending Cursor execution

RunRegistry 只负责进程内的同对话中断:RunActor 在启动核心任务前同步登记 ConversationId → ActiveRun,替换时取消旧 token;旧 Run 的迟到 release 不能移除当前 Run。真正的 durable active ownership 仍由随后执行的 store 事务决定,RunRegistry 不能成为第二事实源,也不需要一张无人消费的 RunId → Actor 表。CursorSessionRegistry 负责让独立到达的 RunSSE/BidiAppend 找到同一 Cursor session,并在进入 ClientCommand 前完成 append_seqno、wire id 和 request id 校验。两边只通过 ClientEvent/ClientCommand 连接;通用 RunActor 不拥有 Cursor runtime。

两侧 registry 的释放点不同:RunActor 发出唯一终态后,RunRegistry 才移除 RunCursorSession 完成终局 checkpoint 投射并 EndStream,或传输关闭后,CursorSessionRegistry 才移除 session。这样终局 checkpoint 重发仍有归属,终态后的迟到 Bidi Blob ACK 也能被确定性拒绝,而不是误投给下一次 Run。

conversation 的 active ownership 不是仅存在于 RunRegistry 的内存索引。存储中由 conversations.active_run_id 表达唯一写入者。新 Run 先把客户端传入的历史解析为 base_revision_id,再在同一事务中设为 active owner、选中该 base revision 并把旧 Run 标记为 cancelled。新 Run 不接管旧 RunSSE 的未发消息,也不转移不存在的 checkpoint ACK 所有权。

任何新 revision 提交都同时校验 active_run_id 和 expected head revision。Run 结束时只允许 WHERE active_run_id = 当前 RunId 清空所有权;旧 Run 的迟到 Provider 事件、ToolResult 或终态都不能推进新 head。

8. 上下文不变量

8.1 Messages

Canonical message 是不可变对象;conversation revision 逻辑上是一个有序内部 MessageId 快照,存储上只记录唯一 parent revision 和本 revision 新追加的 MessageId,不复制整条历史。MessageId 由 adapter/核心确定性生成并在 conversation 内唯一,不能直接信任客户端 wire message id。客户端要求 round-trip 的 id 留在该 adapter 的 projection/baselineToolRound 的分组只使用 durable ToolRoundId,不使用 wire id 或一次 Provider 调用的 model_call_id。追加产生子 revision,回滚/恢复只选择旧 revision 作为新 Run 基点,不修改或删除旧消息,也不把回滚点之后的旧分支投给 LLM。Todo 和 Plan 从选中 revision 的 messages fold,不单独持久化可幂等推导的业务状态。

投射给 LLM 的请求必须满足:

同一 Run/revision 分支内,M(n) 是 M(n+1) 的不可变前缀

Cursor adapter 先将 conversation_state Blob 图解析为有序 canonical messages,以稳定 state digest 查找或导入 base revision。conversation_state 字段存在不等于已有历史:Cursor 新对话会发送 roots 为空的 state,此时必须建立空 base revision,由首份 checkpoint 写入 system root;只有非空恢复历史才校验恰好一个 system root。Cursor 的 RunSSE/Bidi request_id 标识一次具体执行尝试,adapter 由它创建内部 RunIdAgentRunRequest.run_id 在队列/子代理恢复时可能跨新 request 复用,只是 Cursor 逻辑元数据,不能作为 runs 主键。导入 root Blob 时,内部 MessageId 至少绑定 BlobID 和序位;同一 wire id 重复不构成冲突。RunActor 仅对 Start 动作的 initial_messages 以稳定内部 MessageId/runtime_event_id exactly-once 追加;Resume 直接从 base revision 继续。内部 ID 重复只接受相同内容,ID 相同但内容不同是 Store failure。

同一 Run 的 PromptSpec、ModelSpec 和 Provider route 固定,ModelRequest 始终由 PromptSpec + selected revision 的中性 typed history 确定性构造。时间戳、request id、model call id 等仅用于传输或观测的易变字段不得进入 LLM 输入。前缀稳定性在相同 PromptSpec、ModelSpec 和 Provider route 内验证;切换模型/端点可以产生新投射,不要求跨模型共享缓存。

ModelRequest 本身只保存可重放的模型输入,不保存 model_call_id。本轮调用 ID、零基 provider_call_index 和取消信号属于 ModelInvocation/RunActor 运行元数据;Provider 可以用调用 ID 产生 ModelEvent::Start,但不得把它序列化进上游请求。这样“请求对象可比较”和“调用实例可观测”不会混成同一个概念。

8.2 Runtime tag

Runtime tag 是系统生成的上下文事件,但以 user role 投射给 LLM;它不是用户输入。

  • 事件真实发生时实时追加。
  • 每个逻辑事件只追加一次。
  • 追加后不可修改、覆盖或回插。
  • 不在每次请求编译时重新生成。
  • 位于事件发生时上下文末尾。
  • 不为消除后来的矛盾修改旧 tag。

9. Provider 流、reasoning 与 usage

统一 ModelEvent 至少包括 Start、TextStart/Delta/End、ThinkingStart/Delta/End、ToolCallStart/ArgumentsDelta/End、ProviderReplayState、可选 Usage 和 Done。ProviderReplayState 由对应 Provider adapter 产生和消费,核心只随成功 assistant message 持久化。单轮可能包含多个 Responses reasoning item 或多个 Anthropic thinking block,因此 adapter 必须先聚合,再在 Done 前只发一个完整 replay stateModelCycle 不以“后一个覆盖前一个”的方式丢数据。

run/model_cycle.rs 严格验证:

  • 重复 start 失败。
  • 未 start 的 delta/end 失败。
  • 未知 tool index 或 call_id 的 delta 失败。
  • text、thinking 和 tool call 必须闭合。
  • Done 只能一次,Done 后不得再有事件。
  • EOF 前必须有端点明确证明的 Done。
  • tool index 和 call_id 在单轮内都必须唯一,ToolCallEnd 后的 arguments 必须是完整合法 JSON。
  • Provider adapter 必须先把端点自己的累计 usage 汇总成一个终值;公共 ModelCycle 收到第二个 Usage 即失败,不能覆盖前值或猜哪个才是最终总量。
  • 流不完整时不得执行工具。
  • Done(Stop) 只允许无工具的完整终局,Done(ToolUse) 只允许至少一个闭合工具调用;Length/Incomplete 是未完成轮次,不能被 Loop 当作正常 Turn 完成。Provider error 和 cancellation 分别走 stream error 与 Run cancellation,不伪装成 Done(Error/Aborted)

Provider adapter 也不能在裸 EOF 时伪造成功:

  • OpenAI Chat 依据明确 finish_reason 闭合对应 choice。
  • OpenAI Responses 依据明确 completed/done 事件。
  • Anthropic 依据 content block stop 和 message stop。
  • 端点没有独立 ToolCallEnd 时,可根据该端点真实终止信号合成 canonical end;不得根据 TCP/SSE EOF 或默认值补 end/Done。
  • cancellation 由 Run 生命周期处理,不伪装成 Provider Done。

每个 Provider 必须有原始 SSE fixture 测试。

单轮失败必须返回显式的诊断状态:

ModelCycleFailure {
    failure,
    partial_text,
    partial_reasoning,
    usage,
}

partial_textpartial_reasoning 只用于诊断和关闭已显示的流式 UIusage 只保留 Provider 已明确报告的值。未获得端点明确成功终态时,不把 partial assistant 追加到 canonical messages,也不创建伪完成 revision。任何未闭合或参数 JSON 不完整的 tool call 都不得进入 messages 或工具执行。

Reasoning 要求:

  • Canonical assistant message 分开保存可展示 reasoning text 和 Provider 产生的不透明 replay statereplay state 有明确 provider kind,只由对应 adapter 解码。
  • reasoning_content 不是所有端点的通用 wire 字段。OpenAI-compatible Chat 在具体模型要求时回传该字段;Responses 保留并回传 reasoning item/encrypted contentAnthropic 保留并回传 thinking block/signature。
  • Provider adapter 负责从原始流产生 replay state,并把它投射回自己的下一轮请求;Loop 不感知具体 JSON 字段。
  • 跨 Provider/模型时不伪造或解码其他 Provider 的 replay state;只投射目标端点明确接受的 canonical 内容。
  • thinking 耗时来自真实 ThinkingStart/ThinkingEnd。
  • Cursor stable/pending assistant 的 reasoning part 使用抓包已有的 signature 字段携带不透明 replay capsule;不能发明新的顶层 checkpoint 字段。服务端生成的 capsule 必须带明确 magic/version 后再编码,不能靠“尝试 base64+JSON”猜类型;未知 Cursor signature 原样 round-trip。Cursor adapter 只识别 envelope,不解释其中的 Provider 内容。
  • OpenAI Chat 只把本端点先前返回并保存的 replay state 回传为 reasoning_contentcanonical 的可展示 thinking 不能在跨端点或缺少 Chat replay capsule 时冒充该字段。这不是其他端点的通用字段。
  • OpenAI Responses 请求显式包含 reasoning.encrypted_content,收集本轮全部 reasoning output item,并在下一轮完整放回 input;不能只保留最后一项或只保留展示 summary。
  • Anthropic 收集完整 thinking blocks 及 signature 并原样回传。当前支持 adaptive thinking 的模型使用 thinking.type=adaptiveoutput_config.effort;旧模型所需的固定 budget_tokens 是另一种明确 route,不能收到 400 后自动 fallback。

Usage 只信任 Provider 每轮返回值,不自行估算。adapter 可以在端点内部接收多次累计 usage 更新,但向 ModelCycle 只能发出一次最终可信总量;重复 Usage 是 adapter 契约错误,不以后值覆盖。Run 对每轮只累加一次,不能把同一轮的累计快照逐条相加。Provider 没有报告时 usage 保持 NoneCursor TurnEndedUpdate 对应字段也保持缺省,不能上报一组伪造的零值。Turn 跨多次模型调用汇总时,只有每一轮都明确报告的字段才能求和;任一轮缺失就使 Turn 总量的该字段保持缺失。客户端不支持的明细不虚构。

端点上的具体汇总也必须明确:Chat 保存最后一个非空 usage snapshotResponses 使用 terminal response usageAnthropic 将 message_start 的 input/cache 与后续累计 output usage 合成为一个 terminal total。不得让 ModelCycle 猜测不同端点的增量语义。

9.1 LLM 调用边界

上游调用保持一条单向、最小知识的数据流:

PromptSpec + ModelSpec + selected revision
→ pure ModelRequest construction
→ Provider request projector
→ 原始 HTTP/SSE
→ Provider event decoder
→ ModelCycle 严格状态机
→ ModelCycleResult / ModelCycleFailure
  • ModelRequest 的纯构造只组装 PromptSpec、ModelSpec 和由选中 revision 确定性生成的中性 typed history,不携带存储用 origin/runtime_event_id,不产生 OpenAI/Anthropic JSON,也不需要单独的转发式 compiler 对象。
  • PromptSpec 在 Run 准备时编译一次并冻结:mode system prompt 与静态 tool schema 属于 PromptSpec;每次请求携带的 rules、commands、skills、workspace/MCP context 按抓包顺序成为 exactly-once initial user/runtime messages。每轮不得重新读取资产或重排动态上下文。
  • 每个 Provider adapter 自己处理 role/content block、tool schema、reasoning replay、usage 和终止信号;不写 store,不发 Cursor 事件。
  • 同一个 CancellationToken 必须同时覆盖等待 HTTP 响应头和读取 SSE 两段;不能只在流已经建立后监听取消,否则新 Run 无法及时中断仍卡在上游握手中的旧 Run。
  • ModelSpec 的 reasoning、max output 和领域参数必须由选中的 adapter 明确投射;端点不支持时直接返回 Provider failure,不能静默丢字段,也不能用另一个端点的默认值兜底。端点要求而 ModelSpec 未提供的必填值,只能来自启动时明确的 route 配置,否则配置失败;adapter 内不得硬编码 32768medium 一类隐式策略。
  • 映射按端点命名:Chat 为 reasoning_effort/max_completion_tokensResponses 为 reasoning.effort/max_output_tokensAnthropic 为 thinking/output_config.effort/max_tokens。Cursor reasoningeffort 归一为同一领域 effortcontext=272k/300k 解析为 ModelSpec 的 context-window 元数据;固定 BYOK 端点无法满足 fast=true 时直接失败,不能悄悄忽略或冒充 OpenAI service_tier
  • 不建立以 OpenAI role/content/tool_calls JSON 为形状的“通用 ProviderMessage”。Provider adapter 只读取仍保持 typed text/image/tool call/result 的中性 historyAnthropic 和 Responses 不得先生成 OpenAI JSON 再反向解析。禁止使用只有某一个 adapter 能理解的任意 Json message 作为跨端点捷径;Cursor 当前已经传递的 selected images 必须在 adapter 边界解析为带真实 MIME 和字节引用的 typed image part,再由每个端点明确投射或明确拒绝。
  • ModelCycle 只验证通用事件序列和产出结果,不根据端点名称分支。
  • model/projection.rs 可为 Cursor 和 Provider 共同需要的 typed history 结构做确定性分组,但不含任何端点 JSON。若端点要求把同一 ToolRound 重新组合为一个 assistant call batchcalls 保持 Provider 原始顺序,随后 result block 保持真实 completion_seq;不能用 call index 对 result 再排序。tool result 始终从 canonical String 投射。
  • 每轮都显式发送完整 ModelRequestprevious_response_id 等 Provider 服务端隐式状态不作为上下文事实源。需要续传的 reasoning item/signature 作为显式 replay state 进入完整请求。
  • 每个端点都用连续两轮 ModelRequest fixture 验证:在 PromptSpec、ModelSpec 和 route 不变时,第二轮只扩展第一轮的模型上下文前缀,不因 ID、时间或重新编译 Prompt 改写旧内容。

9.2 上游调用架构复核结论

正确且应保留的主干只有一条:RunEngine 从选中 revision 读取 canonical messages,经纯 model/projection 得到 typed history,和冻结的 PromptSpec/ModelSpec 组成 ModelRequestProvider adapter 只把它转换为本端点请求并把原始流转换为 ModelEventModelCycle 只验证通用事件状态机。model_call_id 与取消属于 ModelInvocation,不进入可重放请求;store、Cursor protobuf、checkpoint 和工具执行都没有进入 Provider。这条依赖方向自然、清晰,并满足最小知识。

当前实现已经按这条主干完成以下收敛,后续重构不得倒退为“通用 JSON”或 fallback

  • selected images 已在 Cursor request 边界解析为同一 user message 的 typed image parts;三种 Provider 都从同一中性 history 做端点投射,不由 Cursor adapter 私自拼上游 JSON。
  • Cursor 原始模型参数在 request adapter 归一化;Provider 只读取 ModelSpec 的领域语义。
  • 端点必填策略来自显式 ModelSpec/route 配置;Provider adapter 不发明默认值。
  • Provider replay 只回到产生它的端点;例如 Chat 只有在 provider_kind=openai_chat 且 capsule 含 reasoning_content 时才回传该字段,不能把另一端点的展示 thinking 冒充 Chat reasoning。
  • PromptSpec 只在一个 Run 内冻结。新 Run 切换模型/模式时正常编译新 PromptSpec 并替换 Cursor checkpoint 的 system root,历史 message roots 不重写;不能为了维持跨模型缓存而拒绝请求,也不能把旧模型的 system prompt 发给新模型。
  • 每个端点的请求 projector 和 SSE decoder 用原始 fixture 独立验证;ModelCycle 的合成事件测试不能替代端点协议测试。
  • 删除仅转发的 PromptCompiler::compile,保持 ModelRequest 只有一个构造位置。
  • 静态 mode tool 与动态 MCP tool 不得同名。同名时若静默覆盖 schema,一方面会改写已冻结的工具前缀,另一方面会让模型 schema 与 Cursor 执行 transport 的归属取决于隐式优先级。PromptCompiler 必须在 Run 准备时直接返回 Protocol failure;动态工具只能以确定顺序追加到静态工具后缀。

10. 工具批次、消息顺序与状态同步

10.1 正确顺序

ToolCallStart 按 Provider 原始顺序投射;工具执行可以并行。结果按实际到达顺序处理,每个结果形成不可分割的消息对:

assistant tool call X
tool result X

例如 start 顺序 A、B、C,结果到达顺序 B、A、C,messages 应为:

assistant tool call B
tool result B
assistant tool call A
tool result A
assistant tool call C
tool result C

完成顺序一旦落盘就是稳定历史。不得为了恢复 Provider 原始顺序阻塞已完成工具,也不得先追加一批悬空 calls。该轮 assistant text/reasoning 只附着在第一个落盘的 assistant tool-call message 上,其余 pair 不得复制;恢复执行时也必须保持这一事实。只有当该 ToolRound 的所有 call 都有已提交结果,而且目标 client adapter 已完成该 settled state 的必要投射时,才能发起下一轮 LLM。Cursor 的必要投射是新 Blob 获得 SET ACK 并发布 settled checkpoint;核心等待配对的内部 barrier,不等待协议中不存在的 checkpoint ACK。

10.2 ToolRound 持久化与 Cursor checkpoint

Provider 成功结束为 tools 时,先在同一事务中保存不可变 ToolRoundassistant text/reasoning/replay state、有序 calls 和当前 base revision),发出 StateCommitted(..., ToolRoundStarted),再以 ExecuteToolRound 通知客户端执行。流式 ToolCallStart/Delta/End 只用于 UI 投射,不触发提前执行。这份 active ToolRound 用来构造 checkpoint 中唯一一条、内容冻结的 pending assistant JSON,但不是投给下一轮 LLM 的悬空 assistant message。只要 ToolRound 尚未 settled,该 pending JSON 始终包含整批原始 calls,不按已完成结果过滤成“剩余 calls”。

每个结果的核心顺序:

收到 ToolResult
→ RunActor 分配单调 completion_seq
→ 同一事务中标记 call 完成、原子追加 assistant call + tool result、创建子 revision
→ StateCommitted(revision, tool_round_version, ToolResult(call_id))
→ 继续等待本 ToolRound 其余结果

message pair、completion_seq、ToolRound call 状态和新 revision 之间不能存在崩溃窗口。checkpoint 的 stable roots 只能从 durable parent/current revision 与 durable assistant/ToolRound 构造;Cursor Turn/Step 图则从同一 revision 加当前 Run 冻结的 presentation snapshot 构造,不能靠重新生成时间戳或从任意临时 JSON 猜测。checkpoint worker 独占一个可推进的 projection frontier:其中保存已经发布的 root IDs、旧 Turn IDs、当前 UserMessage ID 和当前 Step ID 前缀;一次 job 只能在该 frontier 后追加节点,成功发布后才推进它。所有 call 完成后,最后一次事务同时将 ToolRound 标记为 settled;下一轮 ModelRequest 只从此时的有序 revision 生成。Cursor checkpoint 的 stable history 必须把 canonical interleaved pairs 确定性折叠为抓包中的一条 assistant batch 加按 completion_seq 排列的 result messages;不得逐条序列化内部 CanonicalMessage 充当 Cursor Blob。

stable roots 是 Cursor/AI-SDK message JSON:静态 PromptSpec 作为 system rootcanonical text/runtime/user/assistant/tool result 按选中 revision 投射。内部的 originruntime_event_id 等字段不得泄漏进 wire JSON。恢复时逐个 BlobID 做 hash 校验并解码,内部 MessageId 绑定 root BlobID 与序位;旧 system root 只用于验证历史结构,验证后不导入 canonical messages。PromptSpec 在单个 Run 内冻结;新 Run 切换模型或模式而使 Prompt 改变时,checkpoint 以新内容寻址 Blob 替换 system root,其余历史 root 继续原样复用。跨 Prompt/模型不承诺共享前缀缓存,也不能因旧 system 文本不同拒绝整个对话。Prompt 未变时,服务端把客户端基线 root IDs/bytes 当作不可变 opaque 前缀原样复用,只对本服务生成的后缀做字节级 frontier 比较并追加;不要求用当前 JSON serializer 重新产生官方历史 bytes,也不提供“重编码整段历史”的 fallback。

CursorToolRuntime 收到 typed terminal result 后保留它,并以 call_id 向核心发送字符串 ToolResult;核心回送 StateCommitted(...ToolResult(call_id)) 后,CursorSession 发送 typed completed UI 事件,但必须把冻结的 typed ToolCall、started/completed 时间和 thinking duration 保留到本轮 settled checkpoint 的 Turn/Step Blob 已发布。随后释放大 payload,完成墓碑保留到 ToolRound settled。这个 presentation frontier 只负责 Cursor UI 序列化,不参与 LLM 请求或核心提交;进程或传输中断后,新 CursorSession 从客户端最后持久化的 ConversationStateStructure 恢复,不重放旧 session 内存结果。

CursorSession 对一次 assistant 完成的投射顺序:

冻结本次 Cursor presentation delta,并在 frontier 上消费一次,得到新的 Step 前缀和 Turn ID
→ 从 parent stable revision + assistant/ToolRound + 当前 frontier 构造 staged checkpointstable roots 不变、pending=1
→ 对 staged 实际新增引用的 Blob 执行 KV SET 并等待对应 Bidi set_blob_result
→ 从 committed revision + 同一 Turn ID 构造 settled checkpoint(只追加 stable root 后缀、pending=0
→ 对 settled 实际新增引用的 Blob 执行 KV SET 并等待对应 Bidi set_blob_result
→ 按生命周期发布 staged 与 settled checkpoint

pending_tool_callsConversationStateStructure 本身都是 RunSSE 内联值,不做 Blob SET。staged/settled 新引用的 root/step/turn/todo/plan 等 Blob 必须各自通过 ACK barrier;客户端传入且未改变的旧引用直接复用。staged 快照始终显式使用 parent stable revision,不能因为 canonical head 已提交就提前把本轮 assistant 放入 stable roots。

Blob SET 不做定时重发 fallback。每个新 Blob 只发送一个 set_blob_args(id) 并等待配对的 set_blob_result(id);拒绝、超时、会话取消或 checkpoint worker 失败都必须进入 Cursor typed Error/取消生命周期,不能只写日志后继续运行。

CursorSession 主循环不为每个 ToolResult 制造 checkpoint job。ToolRoundStarted 冻结 staged assistant;中间 ToolResult 只推进 durable ToolRound 和 typed UI;最后一个结果提交并将 round 标记为 settled 时,才生成 settled checkpoint。最终纯文本 assistant 直接生成一对 staged/settled 快照。blob_sync 通过 Bidi ACK 推进独立 worker,唯一 RunSSE writer 串行写出已就绪帧,不阻塞 ExecuteToolRound 或 typed interaction。这里不需要“先制造大量中间快照再合并”的队列策略。

ToolCallCompleted 是 typed 工具结果的 UI 事件,checkpoint 是可恢复快照;抓包中两者的相对顺序并不唯一,不建立伪全局屏障。硬约束是 checkpoint 不得引用未确认的新 Blob;整个 ToolRound 已提交后还必须让 settled checkpoint 出现在下一轮模型 interaction 之前。exchange 9005 的四轮顺序均满足这一点。

Cursor 抓包证明 checkpoint 可在“完整 assistant 暂存”和“该 assistant 已折叠进稳定历史”时发布;工具 assistant 只是其中一种。没有证据要求每个工具必须单独发布 checkpoint,因此本实现不为单个 ToolResult 发布 checkpoint。相同终局 settled checkpoint 重复发送是幂等的,不产生新 revision。

本轮实现复核没有发现 ACK、checkpoint 和下一轮模型之间的反向时序。回归测试会故意扣住第一份终局 Blob SET ACK;在 ACK 到达前不得出现 turn_ended 或 checkpoint,ACK 后才允许完整终局序列继续。测试同时断言同一 CursorSession 中已确认的 BlobID 不会再次 SET,恢复新 session 也不会重新 SET 客户端 checkpoint 已持有的 stable root。

10.3 Tool result

Canonical ToolResult.content 必须是 String。object、array、number、boolean 或 null 在 adapter 边界确定性序列化成 JSON 字符串,不能把任意 JSON value 原样放入 Provider tool message。

11. Cursor 工具协议

核心 Loop 只认识调用、参数增量、结果和 ToolRound 完整性。只有 Cursor codec 可以按 protobuf oneof 区分工具;只有抓包和 proto 明确证明存在独立多阶段 wire 行为时才建立工具专用模块。

11.1 Tool start

  • ToolCallStart 时立即创建 Cursor 占位卡片。
  • 未完成参数不能猜测。
  • 已可靠获得的 path、command 等字段可以按协议更新。
  • ToolCallEnd 后才把完整参数交给执行阶段。
  • 通用 JSON 增量解析不根据工具名分支。

11.2 CursorToolRuntime

CursorToolRuntime
├── wire_id → call_id
└── call_id → PendingCursorTool
  • wire_id 由 Cursor adapter 生成,只需当前 Run 唯一。
  • Bidi 数字 id 必须映射回非空 call_id。
  • unknown id、wrong Run、空 call_id 和重复终态产生 Protocol error 和结构化日志。
  • 不从 SQLite 查询活动工具。
  • 收到 Bidi/Exec 终态后保留 typed terminal result,先向核心发送字符串 ToolResult,不能在核心提交前删除 pending entry。
  • 收到该 call 的 StateCommitted 后把 typed result 移入 Cursor presentation snapshotsettled checkpoint 的 Turn/Step Blob 发布后才释放大 payload。wire_id/call_id 完成墓碑保留到 ToolRound 结束,以区分 duplicate 与 unknown;失败或取消时 abort/关闭尚未完成的 entry。
  • Cursor session 的 EndStream 或传输关闭后整体释放剩余 runtime。
  • 删除旧 cursor/pending.rs,不保留第二套 registry 或 wrapper。

11.3 编辑工具

Write、StrReplace、EditNotebook 的共同过程:

Read
→ CRLF/CR 规范化为 LF
→ 在 LF 文本上计算和校验编辑
→ 发布 Cursor 编辑阶段
→ 以 LF 写回
→ 返回结果

不在本轮恢复原文件 CRLF;那是独立行为变更。不得用模糊 fallback 隐藏 old_string 不匹配;目标不存在或不唯一时明确失败。Loop 不知道某工具是编辑工具。

11.4 Shell、MCP、Task

  • Shell runtime 保存协议要求的 call/wire/model-call id、command、cwd、输出流、exit code、后台状态和结果。

  • 后台输出继续通过 Cursor 事件返回;AwaitShell 等待已存在后台 Shell,不虚构额外 Loop 状态。Backgrounded 后进程归客户端后台 Shell 管理,成功 Run 结束不能 abort;失败/取消只 abort 尚未终态的 Exec。模型命令本身不得再用 nohup&disown 二次后台化,长驻命令保持前台形式并通过 block_until_ms=0 请求 Cursor 后台化;codec 不改写 shell 文本。

  • MCP 是动态 Tool 能力,不建立独立 Loop;保留 server、tool name、arguments、resource URI,结果字符串化,不实现假 fallback。

  • Task 创建新的子代理 Run,复用同一通用 engine;子 Run 有自己的 RunId,并从抓包明确的两个 Bidi headers 保留父 Run、parent tool call 和真实 subagent type。不能只读 tool-call header 后虚构 parent Run,也不能把 parent 丢成 None

  • 前台和后台 Task 的核心生命周期相同,差异只属于客户端展示及父 Run 是否等待。

  • Cursor Task codec 按抓包保留双层编码:generalPurpose 在 TaskArgs oneof 使用 unspecified,在 SubagentArgs 字符串中仍是 generalPurpose;有明确 oneof 的类型映射到对应 variant,自定义类型保留原始名称和大小写,不做 lowercase 往返。

  • SubagentArgs.parent_conversation_id 使用当前 conversationroot_parent_conversation_id 使用 conversation_group_id,根对话没有 group 时才等于当前 conversationaccept_hook_additional_contexts = false

11.5 UpdateCurrentStep

  • 只在子代理工具集合中。
  • suppress_subagent_progress_update_tool = true 时从该子 Run 的 PromptSpec 中移除;这是显式能力控制,不是 fallback。
  • LLM 名称为 UpdateCurrentStepCursor wire 为 CommunicateUpdateToolCall
  • 使用 parent_tool_call_id 更新父 Task 当前步骤并参与 checkpoint。
  • message_index 是当前 Turn 中该 ConversationStep 的一基位置,不是本地调用次数。
  • 仍走统一 tool start/arguments/endLoop 不按名称判断。

12. Prompt 与工具资产

Cursor 资产目标:

prompt/cursor/
├── tools.json
├── modes/
│   ├── agent.json
│   ├── subagent.json
│   ├── ask.json
│   ├── plan.json
│   ├── debug.json
│   └── multitask.json
├── agent/prompt.md
├── ask/prompt.md
├── plan/
│   ├── prompt.md
│   └── system_reminder.txt
├── debug/
│   ├── prompt.md
│   ├── system_reminder_initial.txt
│   └── system_reminder_continuing.txt
├── multitask/prompt.md
└── compaction/prompt.md

要求:

  • prompt/cursor/tools.json 收拢所有已确认的当前工具 schema;modes/agent.json 只决定 Agent profile,不能覆盖其他 mode 的工具集合。
  • 不使用 tools-full.json 名称。
  • tools.json 是唯一 schema catalog,可包含证据确认的 schema variant,如 Agent Task 与 Subagent Task。
  • modes/*.json 只保存有序工具名和必要 variant 名,不复制完整 schema。
  • catalog 选择预定义不可变 schema,不运行时修改共享 serde_json::Value
  • Agent 和 Subagent 确定性使用同一份 agent/prompt.md;抓包中两者基础 system prompt 的 Blob hash 相同。子代理身份、父任务和运行要求通过 Run 创建时 exactly-once 追加的 user/runtime messages 表达,不复制或派生第二份 system prompt。
  • UpdateCurrentStep 定义在 tools.json,不另建增量工具文件。
  • 无证据的 Ask/Plan/Debug/Multitask 差异不凭空设计,保留已验证行为并用 manifest 固定。
  • 删除重复 mode tools 文件和 Mode::Commit,不恢复假 Prompt。
  • Cursor request context 中的动态信息按固定顺序编译成 user role 的 canonical context messages,在 Run 创建时追加一次;不得在每轮模型请求中重新生成。
  • 动态 MCP tool 只按名字确定性追加在 mode tool 后缀;与任一 mode tool 同名时直接拒绝,不覆盖 schema,不切换为动态 MCP transport。

主代理有序工具:

Shell Grep Delete WebSearch WebFetch GenerateImage EditNotebook TodoWrite
StrReplace Write Read ReadLints Glob AskQuestion Task AwaitShell GetMcpTools
FetchMcpResource SwitchMode CallMcpTool

GeneralPurpose 子代理有序工具:

Shell Grep Delete WebSearch WebFetch GenerateImage ReadLints EditNotebook
TodoWrite StrReplace Write Read Glob Task AwaitShell GetMcpTools
FetchMcpResource SwitchMode UpdateCurrentStep CallMcpTool

子代理没有 AskQuestion;子代理 Task schema 不含 environmentcloud_base_branch

12.1 模型工具与 Cursor wire 分层

以下三层不能混为一谈:

  1. 模型侧工具 catalog:决定某个 mode 实际向 LLM 暴露哪些工具名和 schema。
  2. Cursor ToolCall oneof:决定 RunSSE 中如何展示和完成 typed ToolCall。
  3. Exec/Bidi transport:决定客户端执行阶段使用哪些 args、stream 和 result 消息。

模型侧工具名、Cursor ToolCall oneof 和 Exec/Bidi 阶段必须分别建模,不能通过同一个字符串路由表混合处理。

  • CreatePlan 保留在 Plan mode,使用对应 InteractionResponse 和 typed result 生命周期。
  • AwaitShell 保留为模型工具,Cursor codec 映射到 await_tool_call 的 task、等待时间、正则和输出状态。
  • ForceBackgroundShellWriteShellStdin 不作为当前模型工具;删除其独立 schema 和模型 dispatch,不把它们伪装成 Shell fallback。
  • PatchEdit 模型名直接替换为 StrReplace,不保留 aliasCursor 编辑 wire 生命周期继续由 edit_tool_call codec 表达。

13. 模型配置

建立纯领域 ModelSpec,至少保留:

  • model_id。
  • 可选 display_name。
  • reasoning/thinking 配置。
  • route-independent latency 要求(当前为 Standard/Fast)。
  • max output tokens 等限制。
  • 已确认的 context-window 元数据。
  • 客户端 adapter 已确认的其他模型语义必须先归一为明确领域字段;例如 Cursor fast 是 route/latency 要求,不是直接透传给 Chat/Responses/Anthropic 的任意参数。当前没有证据需要任意扩展参数,因此不保留 Vec<ModelParameter> 逃生口;未知客户端参数直接失败。ModelSpec 不保存 protobuf 对象,也不要求 Provider 认识 Cursor 字段名。

Provider URL、API key 和端点种类不属于 ModelSpec。provider/router.rs 在进程启动时由服务配置唯一构造一个 adapter;每次调用的 ModelSpec 只控制该端点的 model、reasoning 和明确支持的请求参数,不在运行时猜测或切换端点。路由失败直接返回 Provider failure,不尝试隐式 fallback。

CURSOR_MODEL 是可选的 Provider model-id override:设置后仅替换 Cursor 请求中的 model_id,仍保留该 Run 的 reasoning/context 参数;未设置时使用客户端明确选择的模型。不存在硬编码 gpt-5 fallback。这样 CURSOR_MODEL=deepseek-v4-flash 不会再把 Cursor 的 grok-4.6 误发给上游,同时未配置 override 时仍可保留主代理和子代理的真实模型选择。

子代理策略保存在 PreparedRun

  • requested_model:当前 Run 实际模型。
  • selected_subagent_models:候选/允许模型,不等于实际模型。
  • subagent_model_overridesExplicit(ModelSpec)InheritDisabled

实际模型在子 Run 创建时解析一次后保持不变。真实 SubagentKind 不得压缩成 bool。Provider-neutral ModelRequest 放在 model/inference.rsCursor prompting 产出 PromptSpec,核心把 selected revision 投射为中性 typed history 后构造请求,Provider 只做端点 JSON 投射。model/projection.rs 是 Cursor/Provider 共同使用的 typed history 折叠,不属于任何具体 Provider。

14. 错误、取消和终态

核心使用:

RunFailure = Protocol | Provider | Store | Client
RunOutcome = Completed | Cancelled | Failed(RunFailure)

Cancelled 是正常终止结果,不伪装成 Provider error 或 Protocol error。客户端 adapter 把 Failed 投射到自身错误协议;Cursor 必须返回 typed Error,不能写成 assistant 文本或正常 messages。

核心日志至少包含 RunId、conversation_id(可用时)、call_id(相关时)、error category 和完整错误链;Cursor adapter 额外记录 request_id 和 wire id。

每个 Run 使用一个 tracing span,状态推进统一记录 from_stateto_stateprovider_call_indexrevision_id(存在时);工具事件再记录 call_idCursor wire 层再记录 request_id、wire id、KV id 和 Blob ACK 耗时。runs 持久化最终 outcome、失败 category 和可诊断错误摘要,便于直接按 RunId 或 conversation_id 定位中途异常。不新建第二套日志数据库,也不默认记录 Prompt、工具结果正文或 API key。

新 run_request 打断旧 Run时:

  • 在同一存储事务中把新 Run 设为 conversation 的唯一 active owner,并把旧 Run 标为 cancelled;随后触发旧 Run 的 CancellationToken。
  • 停止 Provider 流和工具等待。
  • 每次创建子 revision 时,都以 RunId + expected head revision + active ownership 做条件提交;旧 Run 的迟到 Provider 事件和工具结果不能推进 conversation head。
  • 不再投射旧 Run 的新 checkpoint,终态后的迟到 Bidi Blob ACK 由旧 CursorSession 拒绝,不能路由给新 Run。
  • Cursor 停止 checkpoint 并正确关闭旧 RunSSE。
  • 旧 Run 终态发出后释放 ToolRoundState;旧 RunSSE EndStream/关闭后释放 CursorToolRuntime 和 CursorSession。
  • 保留已提交 messages/revisions 和 Blob;新 Run 以客户端传入的 checkpoint revision 作为基点,不默认继续服务端更晚但客户端未持有的分支。

通用 Run 生命周期使用受约束 enum:

Preparing
→ RunningModel
→ WaitingForTools
→ CommittingTool
→ WaitingForTools / RunningModel
→ Completing
→ Ended

Completed、Cancelled 和 Failed 都是唯一终态。

  • 正常完成:最终 assistant message 提交为 revision 后发出带 completion barrier 的 StateCommitted(FinalTurn)。CursorSession 从 parent stable revision 构造 pending=1 staged 快照,从最终 revision 构造 pending=0 settled 快照,并先使两份快照引用的新 Blob 获得 SET ACK;构造或 ACK 失败会使核心 Run 失败,不能先落成 Completed。状态准备成功后解除 barrier,核心才发出 CompletedCursor 随后严格发送 turn_ended → staged pending=1 → settled pending=0 → 重发同一 settled checkpoint → EndStreamturn_ended 只携带 Provider 实际报告的 Turn usage。纯文本 assistant 同样有 staged 阶段;它不是 ToolRound 特例。
  • Provider 失败:ModelCycleFailure 中的 partial text/reasoning 只用于诊断/UI 收尾,不进入 canonical messagesProvider usage 和失败元数据记入 Run 记录。核心发出 Failed(Provider)Cursor 发送 typed Error 并 Error EndStream,不发送 turn_ended,不伪造新 checkpoint。
  • Store 失败:无法提交的部分内容不得假装成功;直接 Failed(Store) 并由 Cursor 投射 typed Error。
  • Client 传输/Blob 同步失败:CursorSession 停止投射并取消 Run;若错误流仍可写则发送 typed Error,传输已断则只记录错误。新 Run 由客户端携带最后已持久化 checkpoint 恢复,服务端不重放旧 RunSSE 帧。
  • Cancelled:停止 Provider、abort 活动客户端执行并丢弃尚未提交的内存结果。旧 session 不再发布新 checkpoint,不发送 turn_ended,以 canceled EndStream 结束。

错误字符串本身永远不进入 assistant messages。通用 lifecycle 不直接发送 Cursor EndStream。

15. 目标目录

目录是职责目标,不要求预建空文件;只有转发逻辑的文件应合并。

cursor-server/                         # 单一 Rust 服务 crate
├── Cargo.toml                         # crate 元数据和现有依赖
├── build.rs                           # 从 cursor-proto 生成 prost 类型
├── README.md                          # 启动、架构、协议边界和核心不变量
├── migrations/                        # 当前数据库的初始 schema
│   └── 0001_initial.sql               # 不可变 messages、revision 父链、CAS、Run/ToolRound
├── src/
│   ├── main.rs                        # 进程入口,只初始化日志、读取配置并启动 App
│   ├── lib.rs                         # crate 对外模块出口
│   ├── app.rs                         # 依赖组装、HTTP 启动和优雅关闭
│   ├── config.rs                      # 监听地址、数据库和 Provider 配置
│   ├── error.rs                       # 服务级错误及向 RunFailure 的边界转换
│   │
│   ├── model/                         # 纯领域类型,不依赖客户端、Provider 或存储实现
│   │   ├── mod.rs                     # 领域类型出口
│   │   ├── message.rs                 # 内部 MessageId、CanonicalMessage、typed text/image part、Role、Origin、reasoning replay 和字符串 ToolResult
│   │   ├── conversation.rs            # ConversationId、revision 和 conversation head
│   │   ├── runtime_tag.rs             # RuntimeEvent 及 exactly-once 标识
│   │   ├── tool.rs                    # ToolDefinition、ToolCall 和 ToolResult
│   │   ├── usage.rs                   # 单轮 Provider usage 和 Turn 汇总
│   │   ├── model_spec.rs              # 完整模型参数及 reasoning 配置
│   │   ├── projection.rs              # canonical tool pairs → 不含存储元数据的 typed history
│   │   ├── inference.rs               # PromptSpec、typed-history ModelRequest 和运行期 ModelInvocation
│   │   └── run.rs                     # PreparedRun、Start/Resume、RunKind、SubagentKind 和父子关系
│   │
│   ├── client/                        # 所有客户端共同遵守的最小运行协议
│   │   ├── mod.rs                     # client port 出口
│   │   ├── command.rs                 # ToolResult、RuntimeEvent、ClientClosed 和 Cancel;不混入状态 barrier 回执
│   │   ├── event.rs                   # 模型流、StateCommitted、配对 completion barrier 和唯一终态
│   │   └── session.rs                 # RunActor 与客户端 adapter 之间的 channel 会话
│   │
│   ├── run/                           # 协议无关 Loop 和单个 Run 的状态机
│   │   ├── mod.rs                     # Run 核心出口
│   │   ├── registry.rs                # ConversationId → ActiveRun,同对话新 Run 中断旧 Run
│   │   ├── actor.rs                   # 启动前登记 active Run,运行并释放唯一状态推进者
│   │   ├── engine.rs                  # Model → Tools → Model 的薄编排流程
│   │   ├── model_cycle.rs             # 单轮 Provider 流验证、成功结果和失败诊断
│   │   ├── tool_round.rs              # call_id 配对、完成顺序、消息原子追加和全批次 barrier
│   │   └── lifecycle.rs               # 通用 RunOutcome 和唯一终止出口
│   │
│   ├── cursor/                        # Cursor 客户端 adapter,包含全部 Cursor wire 语义
│   │   ├── mod.rs                     # Cursor adapter 出口
│   │   ├── proto.rs                   # include prost 生成的 Cursor protobuf 类型
│   │   ├── connect.rs                 # Connect 5-byte envelope 编解码
│   │   ├── handlers.rs                # Axum/Connect 路由入口
│   │   ├── proxy.rs                   # 未匹配路由原样转发上游
│   │   ├── bidi_append.rs             # 上行解码、append_seqno 排序去重和 session 分发
│   │   ├── run_sse.rs                 # 下行 AgentServerMessage 编码和 SSE 关闭
│   │   ├── sessions.rs                # Cursor request_id → CursorSessionHandle
│   │   ├── command.rs                 # Cursor session mailbox 的 Append/Abort/Finished 命令
│   │   ├── inbox.rs                   # append_seqno 排序、去重和连续消息释放
│   │   ├── actor.rs                   # 组装一次 Cursor Run,并把 Bidi 消息分派到 KV/Exec/Interaction
│   │   ├── session.rs                 # ClientEvent/Command 桥接、CursorRunContext 和工具运行态所有权
│   │   ├── lifecycle.rs               # Cursor typed Error、成功/取消 EndStream 和输出关闭
│   │   ├── json_stream.rs             # 流式工具参数 JSON 的字符串字段解码
│   │   ├── projection/                # typed history ↔ Cursor AI-SDK message/pending JSON;不把 wire id 当内部身份
│   │   │   ├── mod.rs                 # Cursor JSON 投射出口和 replay envelope 版本
│   │   │   ├── encode.rs              # stable root 与 staged assistant 的确定性编码
│   │   │   ├── decode.rs              # root/pending JSON 到中性 typed history 的严格解码
│   │   │   └── tests.rs               # wire id、批次和 opaque replay round-trip
│   │   ├── presentation.rs            # 当前 Run 冻结的 thinking/tool UI 状态;只供 Turn/Step Blob
│   │   ├── interaction/               # 模型事件、query 与 typed ToolCall 的 Cursor UI 投射
│   │   │   ├── mod.rs                 # 通用模型流更新、thinking/usage 和 interaction envelope
│   │   │   ├── query.rs               # InteractionQuery 参数编码
│   │   │   └── render.rs              # typed ToolCall 占位、增量和完成态渲染
│   │   ├── blob_sync.rs               # Cursor KV GET/SET、ACK 和确认状态
│   │   ├── checkpoint/                # 两阶段 checkpoint 与不可变 Blob 图
│   │   │   ├── mod.rs                 # 独占 projection frontier,串行推进 staged/settled;不 SET state 本身
│   │   │   ├── worker.rs              # 串行消费 checkpoint job,构建、等待 Blob ACK 并发布
│   │   │   ├── roots.rs               # 复用基线 message rootsPrompt 改变时替换 system root,并追加 revision 后缀
│   │   │   ├── turns.rs               # frozen presentation → Step/Turn Blob,保留旧引用与时间
│   │   │   ├── derived.rs             # 从 canonical messages fold Todo/Plan wire Blob
│   │   │   └── recovery.rs            # Blob hydration、pending assistant 与冻结时间恢复
│   │   ├── request/                    # Cursor 请求到 PreparedRun 的一次性转换
│   │   │   ├── mod.rs                 # Cursor request parser 出口
│   │   │   ├── prepare.rs             # 一次性组装 PreparedRun 和 CursorRunContext
│   │   │   ├── context.rs             # request_context、rules、commands、skills、MCP
│   │   │   ├── images.rs              # SelectedImage Blob hydration → typed image parts
│   │   │   └── model.rs               # Cursor 模型和子代理模型策略解析
│   │   ├── prompting/                  # Cursor 专属 Prompt 选择和编译
│   │   │   ├── mod.rs                 # Cursor prompting 出口
│   │   │   ├── assets.rs              # 加载 prompt/cursor 的静态资产
│   │   │   ├── compiler.rs            # mode/assets → 不可变 PromptSpec,动态 context 另产 initial_messages
│   │   │   ├── catalog.rs             # tools.json + mode manifest 的确定性投射
│   │   │   └── derived_state.rs       # 只从 messages fold Todo/Plan 等派生状态
│   │   └── tools/                      # Cursor 工具 oneof、执行和多阶段 wire 协议
│   │       ├── mod.rs                  # Cursor tools 出口
│   │       ├── runtime.rs              # wire_id ↔ call_id 和 PendingCursorTool
│   │       ├── codec/                  # 工具生命周期消息与 protobuf oneof 的编解码
│   │       │   ├── mod.rs              # codec 最小出口
│   │       │   ├── request.rs          # ToolCall 参数 → ExecServerMessage
│   │       │   └── response.rs         # ExecClientMessage → typed completion/delta
│   │       ├── stream.rs               # 通用 arguments delta 和 Cursor 增量事件
│   │       ├── edit.rs                 # 编辑工具的 Read、LF 规范化和编辑 wire 阶段
│   │       ├── dispatch/               # 完整参数到各类 Cursor 执行协议
│   │       │   ├── mod.rs              # 按已证实协议类型分派
│   │       │   ├── exec.rs             # 单阶段 Exec 与动态 MCP
│   │       │   ├── edit.rs             # 编辑的隐藏 Read 阶段
│   │       │   ├── interaction.rs      # InteractionQuery 与批准后续阶段
│   │       │   ├── local.rs            # 无客户端执行的同步工具
│   │       │   └── await_shell.rs      # AwaitShell 文件/计时阶段
│   │       └── result/                 # terminal wire result → typed UI + String ToolResult
│   │           ├── mod.rs              # ToolCompletion 与结果通道
│   │           ├── await_shell.rs      # AwaitShell 终态
│   │           ├── interaction.rs      # InteractionResponse 终态
│   │           ├── local.rs            # 本地工具终态
│   │           ├── mcp_state.rs        # GetMcpTools 状态结果
│   │           └── exec/               # Exec terminal 结果
│   │               ├── mod.rs          # wire 结果类型分派
│   │               ├── output.rs       # canonical String 输出
│   │               └── render.rs       # Cursor typed ToolCall result
│   │
│   ├── provider/                       # LLM Provider 端点 adapter
│   │   ├── mod.rs                      # Provider trait 和 adapter 出口
│   │   ├── event.rs                    # 统一 ModelEvent 定义
│   │   ├── router.rs                   # 启动配置 → 唯一 Provider adapter
│   │   ├── openai_chat.rs              # Chat 请求、SSE、终态和按模型回传 reasoning_content
│   │   ├── openai_responses.rs         # Responses items、reasoning replay 和明确终态
│   │   └── anthropic.rs                # Anthropic blocks、thinking signature 和 message_stop
│   │
│   └── store/                          # SQLite 持久化,只保存 canonical 事实
│       ├── mod.rs                      # Store 接口和事务出口
│       ├── sqlite.rs                   # pool、PRAGMA、事务和 migration 启动
│       ├── messages.rs                 # 不可变 message 对象和内容一致的 exactly-once 写入
│       ├── revisions.rs                # revision 父链、本节点有序追加 message 和分支选择
│       ├── cas.rs                      # 客户端无关的内容寻址字节及引用边,不含 Cursor Blob 类型
│       ├── conversations.rs            # 当前 revision 和唯一 active_run_id,不含 Cursor head
│       ├── runs.rs                     # Run outcome、失败摘要、usage 和 provider_call_index
│       └── tool_rounds.rs              # durable active ToolRound、call 状态和 completion_seq
│
└── tests/                              # 跨模块行为和协议不变量测试
    ├── support/
    │   ├── fake_provider.rs            # 可精确控制 ModelEvent 的 Provider
    │   ├── fake_cursor.rs              # Connect frame/protobuf 测试解码
    │   └── fixtures.rs                 # 临时数据库、SSE 和 protobuf 测试数据
    ├── text_turn.rs                    # 无工具完整 Turn
    ├── tool_loop.rs                    # Model → Tools → Model 闭环
    ├── runtime_tag_once.rs             # runtime tag exactly-once
    ├── prefix_stability.rs             # ModelRequest 的不可变前缀
    ├── provider_stream.rs              # 三类 Provider 原始流、严格闭合和安全失败半成品
    ├── selected_images.rs              # SelectedImage Blob hydration、typed history、三端点与 Cursor root 投射
    ├── client_contract.rs              # 非 Cursor client 不修改核心即可运行
    ├── connect_wire.rs                 # Connect、RunSSE、BidiAppend 二进制兼容
    ├── checkpoint_recovery.rs          # Blob SET ACK、assistant 两阶段快照和终局恢复
    ├── revision_branch.rs              # 恢复/回滚选择旧 revision,不混入旧分支后缀
    ├── tool_order.rs                   # 结果到达顺序和相邻 call/result pair
    ├── subagent_protocol.rs             # Task 模型/父关系/前后台字段和 UpdateCurrentStep
    ├── error_lifecycle.rs              # typed error、取消和唯一终态
    └── interrupt.rs                    # 新 Run 原子接管、旧 Run 迟到写入拒绝和 registry shutdown

prompt/cursor/                          # Cursor adapter 的静态 Prompt 和工具资产
├── tools.json                          # 唯一工具 schema catalog
├── modes/                              # 各 Cursor mode 的有序工具/variant 清单
├── agent/                              # Agent 与 Subagent 共用的静态系统 Prompt
├── ask/                                # Ask mode Prompt
├── plan/                               # Plan Prompt 和 system reminder
├── debug/                              # Debug Prompt 和阶段 reminder
├── multitask/                          # Multitask Prompt
└── compaction/                         # 上下文压缩 Prompt

不预建 shell.rsmcp.rstask.rsupdate.rs。整理后只有确认其拥有独立状态机时才拆出。tool_result.rs 比旧 Loop 更长,也必须按语义缩小,不能只改路径。

16. 执行顺序

每阶段运行 cargo fmt --checkcargo checkcargo clippy --all-targets -- -D warnings 和相关测试。

阶段 0:保护工作区与基线

  • 记录 git status --short
  • 确认测试只使用临时数据库。
  • 删除资产已不存在且无协议依据的 Mode::Commit 加载路径。
  • 记录基线失败,区分原有失败与重构引入失败。

阶段 1:先补特征测试

固定当前 text/tool loop、tool start 顺序、结果到达顺序、assistant staged/settled checkpoint、Blob SET ACK、终局 turn_ended 时序、typed Error、各 mode 工具清单、模型选择和三类 Provider 原始终态。

阶段 2:建立纯模型与客户端边界

增加 ModelSpec、纯 ModelRequest、运行期 ModelInvocation、PreparedRun、RunAction(Start/Resume)、RunKind、ClientCommand、ClientEvent 和 session channels。将存储改为不可变 message + revision 父链,先用测试证明选择旧 revision 不会带入旧分支后缀。新 Run 从 adapter 解析的 base revision 取得 active ownership;所有新 revision 提交校验 RunId 和 expected head。必要的客户端状态完成用与 StateCommitted 配对的一次性 barrier 表达,不走 ClientCommand 队列;核心边界不包含 Cursor checkpoint ACK、Blob 或不透明发布 envelope。

阶段 3:提取 Cursor 请求准备

protobuf、mode、context、MCP、Prompt、工具集合和模型参数在 cursor/request/cursor/prompting 内完成转换。CursorSessionRegistry 关联 RunSSE/BidiAppendappend_seqno 排序去重在 Cursor adapter 完成。进入 RunActor 后不再读取 Cursor protobuf 或 append_seqno。

阶段 4:提取并严格化 ModelCycle

移动单轮 Provider 消费;发起每轮 HTTP 请求前先持久化零基 provider_call_index,并把它放在 ModelInvocation 而非 ModelRequest;定义诊断用 ModelCycleFailurepartial output 不进入 canonical messages;同时删除三个 adapter 的裸 EOF/default Done 和 Done(Error/Aborted)。分别实现 Chat reasoning_content、Responses reasoning items 和 Anthropic thinking blocks/signatures 的单次聚合 replay,使用原始 SSE/请求 fixtures 验证。

阶段 5:提取 ToolRound

只按 call_id 管理 durable ToolRound。工具可并行,结果按到达顺序处理;call 状态、completion_seq、相邻 message pair 和子 revision 在同一事务写入。assistant text/reasoning 只写入第一对;全部工具结果提交后触发 settled state barrier,完成目标客户端必要投射后才进入下一轮。Provider 投射若重组 ToolRound,只重组 calls,不得把 results 从 completion_seq 改回 call index 顺序。

阶段 6:收敛 CursorToolRuntime

把 pending、exec、typed terminal result 映射收敛到 cursor/tools/runtime.rs,由 CursorSession 唯一持有。BidiAppend 先通过 CursorSessionRegistry 定位 request_id,再把 wire id 转换成 call_id 后发送 ClientCommand。结果获得对应 StateCommitted 后移入 presentation snapshotsettled checkpoint 发布后释放大 payloadToolRound 结束前保留完成墓碑,失败/取消 abort 未完成项,EndStream 后释放 session。补 unknown/empty/duplicate/wrong-run 和迟到消息测试,删除 cursor/pending.rs

阶段 7:分离 Cursor 状态同步生命周期

CursorSession 消费 StateCommittedcheckpoint worker 独占并推进一个 projection frontier,从不可变的 base root/turn refs、parent/current revision、assistant/ToolRound 和冻结的 Cursor presentation snapshot 构建 staged/settled 快照。只 SET 快照实际新增引用的 Blob 并等待配对的 Bidi ACK;未变化的 UserMessage/Step 引用保持前缀,只重建变化的当前 Turn wrapper;不 SET ConversationStateStructure 本身,不对 checkpoint 等待回执,不定时重发 Blob SET,也不持久化跨 RunSSE 的帧 outbox。initial 与 ToolRound settled 在 checkpoint 发布后解除对应 state barrierfinal 在两份快照的新 Blob ACK 后解除 barrier,使构建失败能在核心进入 Completed 前传播。正常终局随后执行 TurnEnded → staged pending=1 → settled pending=0 → 重发相同 settled checkpoint → EndStream。Provider 错误执行 typed Error → Error EndStream,不发 TurnEnded。Cancelled abort 活动 Exec 且不创建新成功 checkpoint。新 Run 从传入 conversation_state 恢复;若 eligible checkpoint 含 pending assistant,则 adapter 恢复相应 ToolRound、原始 pendingToolCallStartedAtMs 和 staged 状态,不能把 pending JSON 当 call_id。typed Error、TurnEnded 和 EndStream 只留在 Cursor adapter。

阶段 8:迁移 Prompt/catalog

建立 prompt/cursor/tools.json 和 ordered mode manifests,迁移各 mode 已确认的 PromptAgent/Subagent 共用同一 Agent system prompt,子代理差异只从 initial_messages 注入。删除重复 schema、已由证据确认废弃的模型别名、Commit 和旧目录。对所有支持模式做工具名、顺序和 schema snapshot;不得用 Agent profile 覆盖 Ask/Plan/Debug/Multitask。

阶段 9:整理 Cursor 工具文件

移动现有实现,不重写平行实现。优先按 runtime、codec、result、stream、edit 五个真实职责整理;只有独立状态机才继续拆具体工具文件。

阶段 10:删除旧 Loop 并验证

run/engine.rs 完整工作后删除 run/loop_engine.rs 和兼容 re-export。运行完整测试,并用真实 Cursor 完成文本、工具、子代理、错误和中断冒烟测试。

17. 测试要求

必须覆盖:

  1. prefix_stability:同一 PromptSpec/ModelSpec/Provider route 和 revision 分支内 messages 前缀不变、runtime tag exactly once、Start/Resume 不重复追加;ModelRequest 不含 model_call_id/时间等调用元数据;三个 Provider 的连续两轮投射不改写旧上下文;跨模型/模式只替换 Cursor system root,不改写历史 message roots。
  2. provider_stream:完整流成功;缺 end/Done、duplicate start、unknown delta、Done 后事件失败;裸 EOF 不得补成功;Length/Incomplete 不得完成 TurnChat reasoning_content、Responses 全部 reasoning items/encrypted content、Anthropic 全部 thinking blocks/signatures 均聚合后按自己端点回传;thinking 耗时正确;同轮多次累计 usage 只采用最终可信总量一次;失败 partial output 不进入 canonical messages,半截 tool call 不落盘也不执行。
  3. tool_orderstart A/B/C、result B/A/C 时,messages 为 B pair、A pair、C pair;每对相邻;assistant text/reasoning 仅出现在 B pairobject/array/scalar/null 结果确定性转成字符串;所有结果提交前不得下一轮。
  4. checkpoint_recoverypending 项是完整内联 assistant JSON,而非 call_id/BlobID;工具 assistant 可含多个 call,纯文本 assistant 也有 pending stage;连续多个 wire id="1" 的 assistant batch 导入后仍有不同内部 MessageId/ToolRoundId;官方 opaque reasoning signature 无损 round-trip,自有 replay envelope 才解码;staged stable roots 不含本轮 assistantsettled 才折叠 assistant/results;同一 PromptSpec 内旧 roots 始终是字节级前缀且不重复 SET,跨模型/模式 Prompt 改变时只替换 system root;同一当前 Turn 的 UserMessage ID 不变、旧 Step IDs 是下一份的精确前缀、当前 Turn wrapper 随 frozen typed UI snapshot 更新;单个 ToolResult 不发布 checkpointcheckpoint 实际引用的每个新 Blob 都先收到对应 SET ACK,而 ConversationStateStructure 自身绝不 SET;不存在 checkpoint ACK;恢复保留 pendingToolCallStartedAtMs,历史 Step 时间不重算;settled checkpoint 必须早于下一轮 ModelRequestfinal state 构建失败不能落成 Completed;终局满足 TurnEnded < staged pending=1 < settled pending=0 < 相同 settled checkpoint 重发 < EndStream,重发不产生 revision。
  5. tool_runtimeCursorSessionRegistry 正确关联 request_idwire id 当前 Run 唯一并映射 call_idunknown/wrong-run/empty/duplicate 失败;typed terminal result 保留到核心提交,完成墓碑保留到 ToolRound 结束;EndStream 后释放 session;通用 ToolRound 无 wire id 和 append_seqno。
  6. client_contract:不需要 checkpoint 协议的 fake client 立即完成配对 state barrier 即可运行同一 Loopbarrier 不经过 ClientCommand,也没有 Cursor 类型。替换 client 不改变 engine、revision 和 Provider 请求;必要客户端状态失败必须阻止 Run 落成 Completed。
  7. mode_tools:分别验证 Agent、Subagent、Ask、Plan、Debug、Multitask 的工具名、顺序和 schema;主代理 AskQuestion/无 Update,子代理相反,Task 无 Cloud 字段;CreatePlan 仅在 Plan modePatchEdit 不再作为模型工具名;suppress 字段可移除 UpdateCurrentStep。
  8. model_selection:请求参数和顺序完整;Explicit/Inherit/DisabledreasoningSubagentKindgeneralPurpose/typed/custom 的精确 wire 编码与原名保持;父/root conversation 关系;Provider route 不在 ModelSpec。
  9. error_lifecycle:各 RunFailure、Cursor typed Error、无 assistant 错误文本;Provider 失败不创建 partial assistant revision,不发 TurnEndedCompleted/Cancelled/Failed 唯一终态;Cancelled 非 Provider Done。
  10. editingLF/CRLF/CR 输入归一化并写回 LF;匹配唯一性;三类编辑工具;增量跨 JSON token。
  11. interrupt:新 Run 在同一事务选中传入 base revision、取得 active ownership 并取消旧 RunProvider、工具等待停止;旧 Run 的迟到 Provider/tool 提交被拒绝且不能推进 conversation headRun 终态后和旧 stream 关闭后分别释放两侧 registry/runtime。
  12. recoveryprovider_call_index 在 HTTP 前更新;Cursor eligible staged checkpoint 按原有 call 顺序、replay state 和 pendingToolCallStartedAtMs 恢复整个 pending ToolRound,不把旧进程中尚未进入客户端 checkpoint 的部分结果猜成已恢复;Cursor resume_action 不追加新 user message;回滚到旧 checkpoint 时选择旧 revision 并建立新分支,原分支不删除也不混入 ModelRequest。
  13. selected_imagesdata/blobId/blobIdWithData 都经过 Blob hash 校验与 hydration;文本和多图保持同一 user message 内的顺序;Chat/Responses/Anthropic 生成各自明确的图片结构;UUID/path 不进入 ModelRequest;自有 Cursor root 可无损恢复 MIME 与 bytes。
  14. proxy:所有未匹配路由保留 method、path、query、必要 headers、body 和上游 status/headers/streaming body;上游连接失败返回正常 HTTP 错误,不伪造成 Cursor assistant 内容。
  15. shell_wirewire/model call id 关联正确;前台和后台增量输出可见;后台终态可被 AwaitShell 消费;未知 id 走 typed Protocol error。
  16. subagent_protocolgeneralPurpose/typed/custom 编码、模型解析、父/root conversation、前后台 Task、UpdateCurrentStep 的一基 message_index 和父工具关系。
  17. shutdown:收到 Ctrl-C 后立即停止接受新请求,取消所有 Run/工具执行并关闭 RunSSEHTTP graceful shutdown 最多等待 10 秒,随后强制释放 server/router,最后一个 Store 随之释放 SQLite pool,进程有界退出。

18. 必须保留且不得重新引入

必须保留:未匹配 HTTP 路由代理、Connect、RunSSE/BidiAppend、Cursor typed Error、unknown result 日志、tool result 字符串、各 Provider 自己的 reasoning replay、thinking 耗时、Shell 关联和后台输出、增量 JSON、工具占位、编辑流、LF 规范化、assistant staged/settled checkpoint、UpdateCurrentStep 父关系、子代理 Task 去 Cloud 字段、Provider usage、Ctrl-C 和新 Run 中断。

不得重新引入:AwaitShell 假状态、PatchEdit 模型 alias、ForceBackgroundShell/WriteShellStdin 独立模型工具、非字符串 tool content、空 call_id、未 Done 执行工具、assistant 错误文本、数据库查询活动工具、Cursor 细节进入通用 Loop。

必须保留:CreatePlan、AwaitShell 以及 Shell 的流式输出、后台状态和 typed 终态。

19. 数据库与工作区约束

  • 每阶段检查 git status --short,不覆盖用户未提交修改。
  • 不删除、重置、修改或提交工作区 DB、WAL/SHM 和抓包数据库。
  • 测试必须使用临时数据库,服务测试不得连接工作区 DB。
  • 不运行 git reset --hardgit checkout --git clean -fd
  • 当前初始 schema 必须以通用 RunId 为键,不增加 Cursor request_id 外键或列;每个客户端 adapter 负责从自己的“具体执行尝试 ID”创建 RunIdCursor adapter 的来源就是 RunSSE/Bidi request_id,而不是可复用的 AgentRunRequest.run_id。conversations 不保存 Cursor head_blob_id;直接表达不可变 messages、revision parent/membership、当前 revision、唯一 active Run、durable ToolRound 及其 completion_seq。Cursor Blob/CAS checkpoint 从 parent/current revision + durable assistant/ToolRound + CursorSession 版本化 projection 确定性构建;projection 不进入通用 schema。若现有 0001_initial.sql 尚未满足,直接修改当前初始 schema 和测试;不新增旧 schema 迁移或兼容路径。

20. 验收标准

  • run/engine.rs 可直接读出 Loop,无 Cursor 和具体工具名。
  • run/model/ 不导入 Cursor protobuf。
  • Cursor mode、wire id、Blob、checkpoint、ToolCallCompleted、RunSSE、EndStream 只在 cursor/
  • 通用 store 只使用 RunId,不认识 Cursor request_id 字段;Cursor adapter 将每次 RunSSE/Bidi request_id 映射为该次执行的 RunIdwire AgentRunRequest.run_id 不进入 Store 主键。
  • ToolRound 只按 call_id 工作,CursorToolRuntime 是数字 id 映射唯一所有者。
  • 工具按实际结果到达顺序形成相邻 pair;全部结果已提交且 settled client state 完成后才下一轮。
  • ToolRound call 状态、completion_seq、message pair 和子 revision 同事务;新 Run 从客户端传入状态解析 base revision,不接管旧 RunSSE 发布队列。
  • Blob SET ACK 先于引用它的 checkpoint;协议中不存在 checkpoint ACKsettled checkpoint 先于下一轮 ModelRequestfinal state 构建/ACK 失败不能落成 Completed;终局为 TurnEnded → staged pending=1 → settled pending=0 → 幂等重发 settled → EndStream
  • 官方终局顺序在 TurnEnded 与首个 terminal checkpoint 之间存在可观测的窄断流窗口;服务端不发明 checkpoint ACK,只接受客户端下次实际带回的 eligible checkpoint 作为恢复事实。
  • 静态 mode tool 前缀不被动态 MCP schema 覆盖;同名直接 Protocol failure。
  • Provider 不以裸 EOF/default 伪造 Done。
  • Prompt 全在 prompt/cursor/,只有一个 tools.json,不存在 tools-full.json 或重复完整 schema。
  • Agent/Subagent 共用抓包确认的 Agent system prompt,身份差异只追加一次;工具集合分别由 ordered manifest 决定。
  • 不存在 Commit、PatchEdit 模型 alias、ForceBackgroundShell 或 WriteShellStdin 独立模型工具;CreatePlan 和 AwaitShell 生命周期完整。
  • 错误不进入 assistant messages。
  • 非 Cursor fake client 能通过 client boundary 完成同一 Loop,且不修改 run/model/provider/store。
  • 无无意义转发模块、兼容层或 fallback。
  • cargo fmt --checkcargo checkcargo clippy --all-targets -- -D warningscargo test 和真实 Cursor 冒烟测试通过。

21. 最终交付说明

完成后必须报告:

  1. 实际最终目录和文件增删移动清单。
  2. loop_engine.rstool_result.rs 的职责去向。
  3. ClientCommand、ClientEvent 和 StateCommitted 的语义。
  4. Cursor 与未来客户端如何隔离。
  5. Prompt/schema 如何去重及工具集合测试。
  6. Provider 原始流严格闭合测试。
  7. ToolRound 与 CursorToolRuntime 的所有权和释放。
  8. tool pair 完成顺序及 ToolRound 完整性屏障。
  9. Blob SET/ACK、checkpoint、ToolCallCompleted 和终局重发的真实顺序。
  10. 模型和子代理策略保留方式。
  11. RunFailure、Completed、Cancelled、Failed、TurnEnded 和客户端 stream 生命周期。
  12. 所有测试和 cargo fmt --checkcargo checkcargo clippy --all-targets -- -D warningscargo test 结果。
  13. 尚无协议证据而未实现的部分。

不要只报告“重构完成”。必须说明新的状态所有权、核心不变量、删除的错误路径,以及非 Cursor fake client 如何证明核心没有被 Cursor 污染。