# Cursor BYOK 架构重构交接 ## 使用方式 这是新对话的完整交接上下文。开始工作前: 1. 阅读根目录 `AGENTS.md`。 2. 阅读 `.agents/skills/cursor-prefix-stability/SKILL.md`。 3. 查看 `git status` 和当前 feature 分支最新提交。 4. 不要直接开始大规模移动 `server/src/cursor`;先完成本文“第一阶段测试护栏”。 ## 本轮目标 本次不是功能缩减,而是代码模块消除和状态机收敛: - 保留当前 Cursor Proto、工具、Checkpoint、恢复、打断、后台完成、子 Task、本地 BYOK 和官方上游转发能力。 - 以 `conversation_id` 为 Cursor 协议的核心状态身份。 - `request_id` 只作为可能重复的传输关联 ID,不能作为 Conversation、Run、Session 或 Trace 的唯一身份。 - 将 Cursor 生命周期收敛为一个状态写入者。 - 目录即架构,减少顶层模块数量,明确核心和扩展边界。 - 面向多人协作,禁止跨目录访问内部实现。 ## 当前阶段已经完成的代码 本 feature 分支当前提交的是第一轮目录与模块整理,不包含后续 Conversation 状态机重写: - Cursor Proto 唯一来源收敛到 `protocols/cursor/`。 - 根目录开发辅助工具统一移动到 `support/`: - `support/cursor-protocol-extractor/` - `support/cursor-capture/` - `support/benchmarks/` - Desktop 前端按架构整理为: - `apps/desktop/src/features/` - `apps/desktop/src/shell/` - `apps/desktop/src/shared/` - Server 的通用客户端端口并入 `server/src/run/`。 - `server/src/web/` 重命名为 `server/src/search/`。 - 模型观测数据结构聚合为 `server/src/model/observability.rs`。 - Release 辅助脚本移动到 `.github/scripts/`。 - 老配置导入能力保留,不应在后续重构中误删。 已完成过的验证: - Workspace `cargo check` 通过。 - Desktop build 通过。 - `support/benchmarks/semble` crate check 通过。 - Cursor 生命周期相关测试曾通过:interrupt 12、error lifecycle 4、background completion 5、connect wire 6。 重新工作时仍需基于当前提交再次运行针对性验证,不能只依赖上述历史结果。 ## 需要保留但未提交的独立工作区修改 `apps/docs/scripts/build-product-demo.mjs` 有一个独立的 Windows `spawnSync(..., shell: true)` 修正。它不属于本次架构提交,已从提交中排除;不要覆盖或删除。 ## 当前实现规模 `server/src/cursor` 当前约 64 个 Rust 文件、约 1.9 万行。 主要分布: | 模块 | 文件数 | 行数约 | |---|---:|---:| | `tools/` | 26 | 6382 | | `request/` | 7 | 2785 | | `checkpoint/` | 7 | 1159 | | `interaction/` | 3 | 1105 | | `session.rs` | 1 | 868 | | `projection/` | 4 | 796 | 问题不是协议服务太复杂,而是同一条生命周期被多个模块分段持有。 ## 当前协议链路 ```text Cursor BidiAppend → handlers.rs 选择 local/upstream → bidi_append.rs 解码 AgentClientMessage → CursorSessionRegistry.get_or_create(request_id) → CursorActor:append 排序、协议消息分发、Run 启动 → CursorSession:Run/Tool/Checkpoint/Interrupt 协调 → RunActor / RunEngine:通用模型循环和历史提交 → OutputHub → RunSSE ``` 主要客户端消息类型: - `RunRequest` - `ExecClientMessage` - `ExecClientControlMessage` - `KvClientMessage` - `ConversationAction` - `InteractionResponse` - `ClientHeartbeat` 运行时 Action: - `UserMessageAction`:软打断并继续当前 Conversation。 - `InjectContextAction`:软打断、追加运行时事件并继续。 - `CancelAction`:硬取消活动 Run。 - `CancelSubagentAction`:终止对应的普通子 Task 工具调用,不改变父 Conversation 生命周期。 主要服务端消息类型: - `InteractionUpdate` - `InteractionQuery` - `ExecServerMessage` - `ExecServerControlMessage` - `ConversationCheckpointUpdate` - Connect terminal frame ## 已确认的身份语义 | 身份 | 语义与约束 | |---|---| | `conversation_id` | 持久化 Conversation 的唯一核心身份 | | `ParentConversationRef` | 子 Conversation 的不可变创建来源 | | internal `run_id` | 服务端一次循环引擎执行,全局唯一 | | Cursor `run_id` | `AgentRunRequest.run_id`,用于 `expected_run_id` 等 wire 关联 | | `agent_session_id` | Cursor Agent 会话实例 | | `request_id` | BidiAppend 与 RunSSE 的可重复传输关联 ID | | `append_seqno` | 一个传输代次内的顺序,不是身份 | | `message_id` / `injection_id` | 事件幂等身份 | | `trace_id` | 一次链路观测的服务端唯一身份 | 身份约束: - `request_id → conversation_id` 必须强关联。 - 同一个 `request_id` 可以在同一个 Conversation 内因重试或队列再次出现。 - 同一个 `request_id` 如果绑定到不同 Conversation,必须报协议冲突。 - 并发重复且 wire 无法区分时应拒绝冲突,不能猜测路由。 - 缺少 `conversation_id` 时,只能使用已经存在的 request binding;禁止回退到 `request_id`。 - `InjectContextAction.expected_run_id` 应比较 Cursor wire `run_id`,不能比较 `request_id`。 ## ParentConversationRef 决策 任何根或子 Task 都是独立 Conversation。Conversation 不维护 children,不订阅子状态,不管理子生命周期。 父 Conversation 只看到一次普通 ToolCall。子 Conversation 只保存不可变的创建溯源: ```rust struct ParentConversationRef { parent_conversation_id: ConversationId, parent_run_id: RunId, parent_tool_call_id: ToolCallId, } ``` 三个字段都必须保留: - `parent_conversation_id`:属于哪条 Conversation 链。 - `parent_run_id`:父 Conversation 的哪次服务端 Run 创建。 - `parent_tool_call_id`:哪次普通 ToolCall 创建。 它不表示父子状态管理,也不应形成内存 children 树。 ## 当前高风险点 ### P0:request_id 被错误提升为核心身份 - `CursorSessionRegistry.runs` 使用 `HashMap`。 - RunSSE 使用 request ID 查找和等待路由。 - Trace 表使用 `request_id TEXT PRIMARY KEY`。 - 父 Run 通过 `active_run_for_cursor_request(request_id)` 查找最近活动 Run。 - `request::prepare` 在缺失 Conversation ID 时回退到 request ID。 ### P0:Wire Run 身份没有进入生命周期 - `AgentRunRequest.run_id`、`agent_session_id` 基本未使用。 - 内部 RunId 当前由 `request_id + UUID` 生成。 - `InjectContextAction.expected_run_id` 当前错误地与 `context.request_id` 比较。 ### P0:生命周期有多个写入者 以下模块都能直接 cancel token、关闭 output 或发送 terminal: - `bidi_append.rs` - `actor.rs` - `session.rs` - `run_sse.rs` - `sessions.rs` - `lifecycle.rs` - 通用 `RunEngine` 还会独立产生 `RunOutcome` 可能导致终态竞争、terminal 重复、错误原因不稳定。 ### P0:Actor 与 Session 是重叠事件循环 - Actor 管 wire 输入、排序、工具回传和 runtime action。 - Session 管 Run 事件、工具状态、Checkpoint、interrupt 和 terminal。 - RunEngine 内还有第三层模型循环。 ### P1:Transport 与 Run 生命周期绑定 - 当前 RunSSE Drop 会直接取消共享 Run token。 - 正确语义应为 `SSE disconnect = DetachTransport`,而不是必然 Cancel Run。 ### P1:旁路状态 - `cancelled_conversations: HashSet` 与 Store、RunOutcome、CancellationToken 平行存在。 - OutputHub 可以重放,但 Handle 关闭后 Registry 很快删除;晚到 RunSSE 可能永久等待。 - `CancelAction` 在 BidiAppend 和 Actor 两处处理。 - `CursorCommand::Abort` 没有生产发送者,主要只在测试中使用。 - `ClientCommand::Cancel` 没有 Cursor 生产发送者。 ## 受保护的历史不变量 后续移动 `request / projection / checkpoint / run` 时,必须保持: - 未发生 compaction 时,第 N 轮 provider history 是第 N+1 轮的严格结构前缀。 - 旧消息不能编辑、合并、重排或重新生成。 - request context 是 append-only 事件;内容相同不重复追加,变化时在本轮 runtime message 前追加。 - `A → B → A` 必须保留三个不同事件;同一事件重试必须幂等。 - Checkpoint encode/decode 必须保留 request-context wire identity。 - 自动 compaction 是显式前缀重置,只保留最新 request context。 - 后台完成和注入不能制造重复 request context。 ## 目标状态模型 Conversation 本身没有 Completed/Cancelled 终态,结束的是 Run: ```text Idle → Preparing → Modeling ↔ WaitingTools → Interrupting → Modeling → Checkpointing → Finalizing → Idle ``` 唯一生命周期写入位置: ```text server/src/cursor/conversation/runtime.rs ``` 建议模型: ```rust struct ConversationRuntime { identity: ConversationIdentity, active_run: Option, request_bindings: RequestBindings, } struct ConversationIdentity { conversation_id: ConversationId, parent: Option, } enum ConversationState { Idle, Active(ActiveRunState), } enum ActiveRunPhase { Preparing, Modeling, WaitingTools, Interrupting, Checkpointing, Finalizing, } ``` 所有其他模块只能向它发送命令或事件,不能直接关闭输出或修改生命周期。 ## 目标目录 ```text protocols/ └── cursor/ # Proto 唯一来源 server/src/cursor/ ├── mod.rs # 只组合模块 ├── protocol/ # wire 类型与编解码 │ ├── mod.rs │ ├── proto.rs │ ├── wire.rs │ └── identity.rs ├── gateway/ # HTTP/Bidi/SSE 入口 │ ├── mod.rs │ ├── bidi.rs │ ├── stream.rs │ └── routing.rs ├── conversation/ # 唯一核心状态域 │ ├── mod.rs │ ├── identity.rs │ ├── registry.rs │ ├── state.rs │ ├── runtime.rs # 唯一生命周期写入者 │ ├── output.rs │ ├── sync.rs │ └── history/ │ ├── request/ │ ├── projection/ │ └── checkpoint/ ├── tools/ # 可扩展普通工具能力 │ ├── mod.rs # ToolPort │ ├── codec.rs │ ├── runtime.rs │ ├── dispatch.rs │ ├── completion.rs │ ├── presentation.rs │ ├── edit.rs │ └── subtask.rs ├── prompting/ # 无状态确定性编译 │ ├── mod.rs │ ├── compiler.rs │ └── assets.rs ├── apis/ # 可替换产品能力 │ ├── mod.rs │ ├── upstream.rs │ ├── account.rs │ ├── models.rs │ ├── analytics.rs │ └── tab.rs └── observability.rs # 只观察,不写生命周期 ``` 顶层架构概念最终收敛为: ```text protocol gateway conversation tools prompting apis observability ``` ## 核心与扩展边界 核心: - Conversation identity 与 ParentConversationRef。 - RequestBinding 一致性。 - 单一生命周期状态机。 - canonical history、projection、checkpoint 与恢复。 - RunEngine 端口。 - terminal exactly-once。 允许扩展: - 具体 Tool 实现,包括子 Task Tool。 - Cursor 官方上游代理。 - Account、Models、Analytics、Tab 等产品 API。 - Provider。 - Presentation 和 Observability sink。 Conversation 核心只认识 `ToolCall / ToolResult / ToolInterrupted`,不能认识或保存子 Conversation 状态。 ## 模块消除映射 | 当前 | 目标 | |---|---| | `actor.rs + session.rs + command.rs + lifecycle.rs + inbox.rs` | `conversation/runtime.rs + state.rs` | | `sessions.rs` | `conversation/registry.rs + output.rs` | | `handlers.rs + bidi_append.rs + run_sse.rs` | `gateway/` | | `connect.rs + proto.rs` | `protocol/` | | `blob_sync.rs + context_sync.rs` | `conversation/sync.rs` | | `request + projection + checkpoint` | `conversation/history/`,保留语义 | | `interaction + json_stream + presentation + tools/stream` | `tools/presentation.rs + codec.rs` | | `tools/dispatch/* + tools/result/*` | `dispatch.rs + completion.rs` | | `proxy/account/model_catalog/analytics/tab` | `apis/` | 最终应删除: - `cancelled_conversations` 旁路状态。 - `CursorCommand::Finished`。 - 无生产用途的 `CursorCommand::Abort`。 - 多处 `lifecycle::finish/cancel/fail`。 - 旧路径和兼容 re-export。 ## 安全实施顺序 ### 第一阶段:测试护栏,不移动目录 先增加行为测试: - 同 request、同 Conversation、同事件:幂等。 - 同 request、同 Conversation、新 Cursor run:正确建立新代次。 - 同 request、不同 Conversation:协议冲突。 - `expected_run_id` 使用 wire run ID。 - ParentConversationRef 三字段准确并可持久恢复。 - RunSSE 先到、重连、晚到。 - cancel 发生在 prepare/model/tools/checkpoint 各阶段。 - soft interrupt 继续同一 Conversation。 - late ToolResult 不污染新一轮。 - final checkpoint 必须先于 terminal。 - terminal frame 恰好一次。 - SSE detach 与 CancelAction 独立。 ### 第二阶段:身份收敛 - 引入强类型 ID。 - 消费 `AgentRunRequest.run_id` 和 `agent_session_id`。 - 建立 RequestBinding,不再假设 request ID 唯一。 - 删除 Conversation ID fallback。 - ParentConversationRef 改为三元组并持久化。 - Trace 使用独立 `trace_id`,不再以 request ID 为主键。 - 不修改工具、Checkpoint 或输出 wire 行为。 ### 第三阶段:ConversationRegistry - Registry 以 Conversation ID 为主键。 - request ID 仅作为反向路由索引。 - RunSSE 根据 binding 定位 Conversation。 - 不再通过 request ID 查询“最近父 Run”。 ### 第四阶段:生命周期单写入者 - 所有 cancel 转为 ConversationCommand。 - SSE Drop 转为 DetachTransport。 - terminal 只能从 Conversation runtime 发出。 - 删除 `cancelled_conversations` 和重复 lifecycle 调用。 ### 第五阶段:合并 Actor 与 Session - 使用一个 `tokio::select!` 处理 wire、Run、Tool、Checkpoint。 - prepare 作为异步任务返回事件。 - 删除 `run_resources.take()` 和 `CursorCommand::Finished`。 - Run 结束后 Conversation 回到 Idle,可继续下一 Run。 ### 第六阶段:目录和 Tools 收敛 - 生命周期稳定后再机械移动目录。 - 同一提交删除旧路径,不保留兼容层。 - 对 Tools 先锁定 wire 行为,再合并 codec、dispatch、completion、presentation。 ## 多人协作规则 - `conversation/runtime.rs` 同一时间只由一个负责人修改。 - `mod.rs` 只组合和导出,不放业务。 - 跨目录只能使用公开 façade,禁止引用内部子模块。 - `tools` 不引用 `ConversationRuntime`。 - `apis` 不访问 Conversation 内部状态。 - `observability` 只能订阅事件。 - Provider 不认识 Cursor Proto。 - 一个 PR 不同时重写 identity、lifecycle 和 tool wire。 - 目录迁移设置短合并窗口,避免多人同时修改路径。 - 每阶段必须保持可编译、可测试、可回滚。 最终依赖方向: ```text Gateway ↓ Conversation Runtime ├── History ├── ToolPort └── Generic Run ↓ Provider APIs → Gateway 公开接口 Observability ← 领域事件 ``` ## 新对话建议的第一条任务 ```text 阅读 cursor.md、AGENTS.md 和 cursor-prefix-stability skill。不要立即移动目录。 先为 Cursor 身份建立 characterization tests,覆盖 request_id 重复、conversation_id 冲突、wire run_id 的 expected_run_id 校验,以及 ParentConversationRef 三元组。测试完成后给出最小身份收敛改动,不要同时重写生命周期和 Tools。 ```