mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
981 lines
94 KiB
Markdown
981 lines
94 KiB
Markdown
# 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→READ`、`StrReplace→STR_REPLACE`、`AwaitShell→AWAIT`、`CallMcpTool→MCP`、`CreatePlan→CREATE_PLAN_V2` 和 `UpdateCurrentStep→COMMUNICATE_UPDATE`;该映射只属于 Cursor projector,Loop 继续只认识工具 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 都严格早于下一轮首个模型 interaction:frame `504 < 505`、`543 < 544`、`579 < 580`、`625 < 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 `493` 到 `1643` 的同一个当前 Turn,其 Step 数量为 `32 → 35 → 36 → 39 → 40 → 43 → 46 → 48 → 50`:UserMessage BlobID 始终不变,上一份 Step BlobID 序列始终是下一份的字节级前缀,但包住这些引用的当前 Turn 每次得到新的 BlobID。也就是说,未变化的 UserMessage/Step 节点必须复用,当前 Turn wrapper 随追加的 UI 状态更新;不能把“当前 Turn 更新”误写成“所有 Turn/Step 全量重建”。adapter 因此需要一份当前 Run 内、冻结值的 Cursor presentation snapshot,但它不是 canonical messages 的第二事实源。
|
||
- `ConversationStateStructure` 的其余字段不能笼统称为“全部冻结”。exchange `9005` 的 `read_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_result`,RunSSE 随后是 frame `1642` 的 `turn_ended` 和 `1643..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 复用同一 Turn,settled 只推进 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 同时处理 `data`、`blobId` 和 `blobIdWithData`,后两者需要时从 Blob store 取回。当前双图抓包分别包含 JPEG 与 PNG 的真实 MIME、BlobID 和 bytes。
|
||
- 服务端因此把文本与图片放在同一条有序 canonical user message 的 typed parts 中,图片只保存 MIME 与 bytes;UUID/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-Id` 与 `X-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. 总体目标
|
||
|
||
当前服务已经跑通:
|
||
|
||
```text
|
||
客户端请求
|
||
→ 构造 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,不能序列化内部 `CanonicalMessage`;`pending_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 恢复 calls,results 保持 completion_seq;ModelCycle 最多交付一次 Provider 明确报告的 usage 和一个已聚合 replay state,Length/Incomplete 失败。
|
||
- Cursor wire message id 已与内部 MessageId/ToolRoundId 分离;官方重复 `id="1"` 不再冲突或跨轮合并。opaque Cursor signature 原样 round-trip,自有 Provider replay 使用带版本的 envelope。
|
||
- Cursor presentation 以增量所有权交给 checkpoint worker;worker 独占 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 一次归一为 `ReasoningSpec`、`ModelLatency` 和 context-window 元数据;未知参数直接 Protocol failure。Provider 不再读取 Cursor 字段名。Responses 不再隐式补 `medium`,Anthropic 的必填 `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 state;ModelCycle 的合成事件测试只保留通用状态机职责。
|
||
- Blob hydration 对 pre-fetched 与 KV GET 数据使用同一 SHA-256 校验;KV GET 成功后写入本地 CAS,hash 不匹配走 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 framework;Tokio channel、oneshot、CancellationToken 和 RunActor 已足够。
|
||
11. 多客户端是当前明确需求,因此窄的 client port 是必要设计,不是 speculative abstraction;禁止提前设计不存在的客户端能力。
|
||
12. 遵循 TDD:先用失败测试固定不变量,再移动或修改实现。
|
||
|
||
## 5. 目标数据流与依赖
|
||
|
||
```text
|
||
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_id`;Cursor checkpoint 必须由 durable revision、ToolRound version、不可变基线与 typed completion/presentation 增量确定性构造。
|
||
|
||
## 6. 客户端通用边界
|
||
|
||
### 6.1 PreparedRun
|
||
|
||
客户端 adapter 把 wire 请求转换为 `PreparedRun`,至少包含:
|
||
|
||
- `RunId`、`ConversationId`;Cursor adapter 可由 `request_id` 确定性生成 RunId,但通用 store 只保存 `run_id`,不出现 Cursor `request_id` 字段。
|
||
- 主 Run 或子代理关系。
|
||
- 不丢失真实类型的 `SubagentKind`。
|
||
- 完整 `ModelSpec` 和子代理模型策略。
|
||
- 客户端已经编译完成的不可变 `PromptSpec`,只包含静态 instruction 和有序工具定义。
|
||
- `action`:`Start { initial_messages }` 或 `Resume`。只有真实新用户/Runtime 事件才追加;`resume_action` 不伪造新 user message。
|
||
- `base_revision`:adapter 从客户端传入的历史快照解析成的通用 revision。核心不读取 Cursor Blob。
|
||
|
||
Cursor request parser 必须同时产出:
|
||
|
||
```text
|
||
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 adapter;Loop 不需要知道 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 只在核心继续前必须完成客户端状态投射时存在。
|
||
- 唯一终态 `Completed`、`Cancelled` 或 `Failed { failure }`。
|
||
|
||
`revision_id` 标识有序 messages,`tool_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` |
|
||
|
||
|
||
运行期关系:
|
||
|
||
```text
|
||
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 才移除 Run;CursorSession 完成终局 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/baseline;ToolRound 的分组只使用 durable ToolRoundId,不使用 wire id 或一次 Provider 调用的 model_call_id。追加产生子 revision,回滚/恢复只选择旧 revision 作为新 Run 基点,不修改或删除旧消息,也不把回滚点之后的旧分支投给 LLM。Todo 和 Plan 从选中 revision 的 messages fold,不单独持久化可幂等推导的业务状态。
|
||
|
||
投射给 LLM 的请求必须满足:
|
||
|
||
```text
|
||
同一 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 由它创建内部 RunId;`AgentRunRequest.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 state;ModelCycle 不以“后一个覆盖前一个”的方式丢数据。
|
||
|
||
`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 测试。
|
||
|
||
单轮失败必须返回显式的诊断状态:
|
||
|
||
```text
|
||
ModelCycleFailure {
|
||
failure,
|
||
partial_text,
|
||
partial_reasoning,
|
||
usage,
|
||
}
|
||
```
|
||
|
||
`partial_text`、`partial_reasoning` 只用于诊断和关闭已显示的流式 UI,`usage` 只保留 Provider 已明确报告的值。未获得端点明确成功终态时,不把 partial assistant 追加到 canonical messages,也不创建伪完成 revision。任何未闭合或参数 JSON 不完整的 tool call 都不得进入 messages 或工具执行。
|
||
|
||
Reasoning 要求:
|
||
|
||
- Canonical assistant message 分开保存可展示 reasoning text 和 Provider 产生的不透明 replay state;replay state 有明确 provider kind,只由对应 adapter 解码。
|
||
- `reasoning_content` 不是所有端点的通用 wire 字段。OpenAI-compatible Chat 在具体模型要求时回传该字段;Responses 保留并回传 reasoning item/encrypted content;Anthropic 保留并回传 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_content`;canonical 的可展示 thinking 不能在跨端点或缺少 Chat replay capsule 时冒充该字段。这不是其他端点的通用字段。
|
||
- OpenAI Responses 请求显式包含 `reasoning.encrypted_content`,收集本轮全部 reasoning output item,并在下一轮完整放回 `input`;不能只保留最后一项或只保留展示 summary。
|
||
- Anthropic 收集完整 thinking blocks 及 signature 并原样回传。当前支持 adaptive thinking 的模型使用 `thinking.type=adaptive` 与 `output_config.effort`;旧模型所需的固定 `budget_tokens` 是另一种明确 route,不能收到 400 后自动 fallback。
|
||
|
||
Usage 只信任 Provider 每轮返回值,不自行估算。adapter 可以在端点内部接收多次累计 usage 更新,但向 ModelCycle 只能发出一次最终可信总量;重复 Usage 是 adapter 契约错误,不以后值覆盖。Run 对每轮只累加一次,不能把同一轮的累计快照逐条相加。Provider 没有报告时 usage 保持 `None`,Cursor `TurnEndedUpdate` 对应字段也保持缺省,不能上报一组伪造的零值。Turn 跨多次模型调用汇总时,只有每一轮都明确报告的字段才能求和;任一轮缺失就使 Turn 总量的该字段保持缺失。客户端不支持的明细不虚构。
|
||
|
||
端点上的具体汇总也必须明确:Chat 保存最后一个非空 usage snapshot;Responses 使用 terminal response usage;Anthropic 将 `message_start` 的 input/cache 与后续累计 output usage 合成为一个 terminal total。不得让 ModelCycle 猜测不同端点的增量语义。
|
||
|
||
### 9.1 LLM 调用边界
|
||
|
||
上游调用保持一条单向、最小知识的数据流:
|
||
|
||
```text
|
||
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 内不得硬编码 `32768`、`medium` 一类隐式策略。
|
||
- 映射按端点命名:Chat 为 `reasoning_effort`/`max_completion_tokens`,Responses 为 `reasoning.effort`/`max_output_tokens`,Anthropic 为 `thinking`/`output_config.effort`/`max_tokens`。Cursor `reasoning` 与 `effort` 归一为同一领域 effort;`context=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 的中性 history;Anthropic 和 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 batch,calls 保持 Provider 原始顺序,随后 result block 保持真实 completion_seq;不能用 call index 对 result 再排序。tool result 始终从 canonical String 投射。
|
||
- 每轮都显式发送完整 ModelRequest;`previous_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 组成 ModelRequest;Provider adapter 只把它转换为本端点请求并把原始流转换为 ModelEvent;ModelCycle 只验证通用事件状态机。`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 原始顺序投射;工具执行可以并行。结果按实际到达顺序处理,每个结果形成不可分割的消息对:
|
||
|
||
```text
|
||
assistant tool call X
|
||
tool result X
|
||
```
|
||
|
||
例如 start 顺序 A、B、C,结果到达顺序 B、A、C,messages 应为:
|
||
|
||
```text
|
||
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 时,先在同一事务中保存不可变 ToolRound(assistant 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”。
|
||
|
||
每个结果的核心顺序:
|
||
|
||
```text
|
||
收到 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 root,canonical text/runtime/user/assistant/tool result 按选中 revision 投射。内部的 `origin`、`runtime_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 完成的投射顺序:
|
||
|
||
```text
|
||
冻结本次 Cursor presentation delta,并在 frontier 上消费一次,得到新的 Step 前缀和 Turn ID
|
||
→ 从 parent stable revision + assistant/ToolRound + 当前 frontier 构造 staged checkpoint(stable 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_calls` 和 `ConversationStateStructure` 本身都是 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
|
||
|
||
```text
|
||
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 snapshot;settled 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 的共同过程:
|
||
|
||
```text
|
||
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 使用当前 conversation,root_parent_conversation_id 使用 conversation_group_id,根对话没有 group 时才等于当前 conversation;`accept_hook_additional_contexts = false`。
|
||
|
||
### 11.5 UpdateCurrentStep
|
||
|
||
- 只在子代理工具集合中。
|
||
- `suppress_subagent_progress_update_tool = true` 时从该子 Run 的 PromptSpec 中移除;这是显式能力控制,不是 fallback。
|
||
- LLM 名称为 `UpdateCurrentStep`,Cursor wire 为 `CommunicateUpdateToolCall`。
|
||
- 使用 `parent_tool_call_id` 更新父 Task 当前步骤并参与 checkpoint。
|
||
- `message_index` 是当前 Turn 中该 ConversationStep 的一基位置,不是本地调用次数。
|
||
- 仍走统一 tool start/arguments/end,Loop 不按名称判断。
|
||
|
||
## 12. Prompt 与工具资产
|
||
|
||
Cursor 资产目标:
|
||
|
||
```text
|
||
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。
|
||
|
||
主代理有序工具:
|
||
|
||
```text
|
||
Shell Grep Delete WebSearch WebFetch GenerateImage EditNotebook TodoWrite
|
||
StrReplace Write Read ReadLints Glob AskQuestion Task AwaitShell GetMcpTools
|
||
FetchMcpResource SwitchMode CallMcpTool
|
||
```
|
||
|
||
GeneralPurpose 子代理有序工具:
|
||
|
||
```text
|
||
Shell Grep Delete WebSearch WebFetch GenerateImage ReadLints EditNotebook
|
||
TodoWrite StrReplace Write Read Glob Task AwaitShell GetMcpTools
|
||
FetchMcpResource SwitchMode UpdateCurrentStep CallMcpTool
|
||
```
|
||
|
||
子代理没有 AskQuestion;子代理 Task schema 不含 `environment` 和 `cloud_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、等待时间、正则和输出状态。
|
||
- `ForceBackgroundShell`、`WriteShellStdin` 不作为当前模型工具;删除其独立 schema 和模型 dispatch,不把它们伪装成 Shell fallback。
|
||
- `PatchEdit` 模型名直接替换为 `StrReplace`,不保留 alias;Cursor 编辑 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_overrides:`Explicit(ModelSpec)`、`Inherit`、`Disabled`。
|
||
|
||
实际模型在子 Run 创建时解析一次后保持不变。真实 `SubagentKind` 不得压缩成 bool。Provider-neutral `ModelRequest` 放在 `model/inference.rs`;Cursor prompting 产出 PromptSpec,核心把 selected revision 投射为中性 typed history 后构造请求,Provider 只做端点 JSON 投射。`model/projection.rs` 是 Cursor/Provider 共同使用的 typed history 折叠,不属于任何具体 Provider。
|
||
|
||
## 14. 错误、取消和终态
|
||
|
||
核心使用:
|
||
|
||
```text
|
||
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_state`、`to_state`、`provider_call_index` 和 `revision_id`(存在时);工具事件再记录 call_id,Cursor 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:
|
||
|
||
```text
|
||
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,核心才发出 `Completed`;Cursor 随后严格发送 `turn_ended → staged pending=1 → settled pending=0 → 重发同一 settled checkpoint → EndStream`。`turn_ended` 只携带 Provider 实际报告的 Turn usage。纯文本 assistant 同样有 staged 阶段;它不是 ToolRound 特例。
|
||
- Provider 失败:`ModelCycleFailure` 中的 partial text/reasoning 只用于诊断/UI 收尾,不进入 canonical messages;Provider 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. 目标目录
|
||
|
||
目录是职责目标,不要求预建空文件;只有转发逻辑的文件应合并。
|
||
|
||
```text
|
||
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 roots,Prompt 改变时替换 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.rs`、`mcp.rs`、`task.rs`、`update.rs`。整理后只有确认其拥有独立状态机时才拆出。`tool_result.rs` 比旧 Loop 更长,也必须按语义缩小,不能只改路径。
|
||
|
||
## 16. 执行顺序
|
||
|
||
每阶段运行 `cargo fmt --check`、`cargo check`、`cargo 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/BidiAppend;`append_seqno` 排序去重在 Cursor adapter 完成。进入 RunActor 后不再读取 Cursor protobuf 或 append_seqno。
|
||
|
||
### 阶段 4:提取并严格化 ModelCycle
|
||
|
||
移动单轮 Provider 消费;发起每轮 HTTP 请求前先持久化零基 `provider_call_index`,并把它放在 ModelInvocation 而非 ModelRequest;定义诊断用 ModelCycleFailure,partial 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 snapshot,settled checkpoint 发布后释放大 payload;ToolRound 结束前保留完成墓碑,失败/取消 abort 未完成项,EndStream 后释放 session。补 unknown/empty/duplicate/wrong-run 和迟到消息测试,删除 `cursor/pending.rs`。
|
||
|
||
### 阶段 7:分离 Cursor 状态同步生命周期
|
||
|
||
CursorSession 消费 `StateCommitted`,checkpoint 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 barrier;final 在两份快照的新 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 已确认的 Prompt;Agent/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 不得完成 Turn;Chat reasoning_content、Responses 全部 reasoning items/encrypted content、Anthropic 全部 thinking blocks/signatures 均聚合后按自己端点回传;thinking 耗时正确;同轮多次累计 usage 只采用最终可信总量一次;失败 partial output 不进入 canonical messages,半截 tool call 不落盘也不执行。
|
||
3. `tool_order`:start A/B/C、result B/A/C 时,messages 为 B pair、A pair、C pair;每对相邻;assistant text/reasoning 仅出现在 B pair;object/array/scalar/null 结果确定性转成字符串;所有结果提交前不得下一轮。
|
||
4. `checkpoint_recovery`:pending 项是完整内联 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 不含本轮 assistant,settled 才折叠 assistant/results;同一 PromptSpec 内旧 roots 始终是字节级前缀且不重复 SET,跨模型/模式 Prompt 改变时只替换 system root;同一当前 Turn 的 UserMessage ID 不变、旧 Step IDs 是下一份的精确前缀、当前 Turn wrapper 随 frozen typed UI snapshot 更新;单个 ToolResult 不发布 checkpoint;checkpoint 实际引用的每个新 Blob 都先收到对应 SET ACK,而 `ConversationStateStructure` 自身绝不 SET;不存在 checkpoint ACK;恢复保留 `pendingToolCallStartedAtMs`,历史 Step 时间不重算;settled checkpoint 必须早于下一轮 ModelRequest;final state 构建失败不能落成 Completed;终局满足 `TurnEnded < staged pending=1 < settled pending=0 < 相同 settled checkpoint 重发 < EndStream`,重发不产生 revision。
|
||
5. `tool_runtime`:CursorSessionRegistry 正确关联 request_id;wire id 当前 Run 唯一并映射 call_id;unknown/wrong-run/empty/duplicate 失败;typed terminal result 保留到核心提交,完成墓碑保留到 ToolRound 结束;EndStream 后释放 session;通用 ToolRound 无 wire id 和 append_seqno。
|
||
6. `client_contract`:不需要 checkpoint 协议的 fake client 立即完成配对 state barrier 即可运行同一 Loop;barrier 不经过 ClientCommand,也没有 Cursor 类型。替换 client 不改变 engine、revision 和 Provider 请求;必要客户端状态失败必须阻止 Run 落成 Completed。
|
||
7. `mode_tools`:分别验证 Agent、Subagent、Ask、Plan、Debug、Multitask 的工具名、顺序和 schema;主代理 AskQuestion/无 Update,子代理相反,Task 无 Cloud 字段;CreatePlan 仅在 Plan mode;PatchEdit 不再作为模型工具名;suppress 字段可移除 UpdateCurrentStep。
|
||
8. `model_selection`:请求参数和顺序完整;Explicit/Inherit/Disabled;reasoning;SubagentKind;generalPurpose/typed/custom 的精确 wire 编码与原名保持;父/root conversation 关系;Provider route 不在 ModelSpec。
|
||
9. `error_lifecycle`:各 RunFailure、Cursor typed Error、无 assistant 错误文本;Provider 失败不创建 partial assistant revision,不发 TurnEnded;Completed/Cancelled/Failed 唯一终态;Cancelled 非 Provider Done。
|
||
10. `editing`:LF/CRLF/CR 输入归一化并写回 LF;匹配唯一性;三类编辑工具;增量跨 JSON token。
|
||
11. `interrupt`:新 Run 在同一事务选中传入 base revision、取得 active ownership 并取消旧 Run;Provider、工具等待停止;旧 Run 的迟到 Provider/tool 提交被拒绝且不能推进 conversation head;Run 终态后和旧 stream 关闭后分别释放两侧 registry/runtime。
|
||
12. `recovery`:provider_call_index 在 HTTP 前更新;Cursor eligible staged checkpoint 按原有 call 顺序、replay state 和 `pendingToolCallStartedAtMs` 恢复整个 pending ToolRound,不把旧进程中尚未进入客户端 checkpoint 的部分结果猜成已恢复;Cursor `resume_action` 不追加新 user message;回滚到旧 checkpoint 时选择旧 revision 并建立新分支,原分支不删除也不混入 ModelRequest。
|
||
13. `selected_images`:`data/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_wire`:wire/model call id 关联正确;前台和后台增量输出可见;后台终态可被 AwaitShell 消费;未知 id 走 typed Protocol error。
|
||
16. `subagent_protocol`:generalPurpose/typed/custom 编码、模型解析、父/root conversation、前后台 Task、UpdateCurrentStep 的一基 message_index 和父工具关系。
|
||
17. `shutdown`:收到 Ctrl-C 后立即停止接受新请求,取消所有 Run/工具执行并关闭 RunSSE;HTTP 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 --hard`、`git checkout --`、`git clean -fd`。
|
||
- 当前初始 schema 必须以通用 `RunId` 为键,不增加 Cursor `request_id` 外键或列;每个客户端 adapter 负责从自己的“具体执行尝试 ID”创建 RunId,Cursor 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 映射为该次执行的 RunId,wire `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 ACK;settled checkpoint 先于下一轮 ModelRequest;final 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 --check`、`cargo check`、`cargo clippy --all-targets -- -D warnings`、`cargo test` 和真实 Cursor 冒烟测试通过。
|
||
|
||
## 21. 最终交付说明
|
||
|
||
完成后必须报告:
|
||
|
||
1. 实际最终目录和文件增删移动清单。
|
||
2. 原 `loop_engine.rs`、`tool_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 --check`、`cargo check`、`cargo clippy --all-targets -- -D warnings`、`cargo test` 结果。
|
||
13. 尚无协议证据而未实现的部分。
|
||
|
||
不要只报告“重构完成”。必须说明新的状态所有权、核心不变量、删除的错误路径,以及非 Cursor fake client 如何证明核心没有被 Cursor 污染。
|