Files
cursor-byok/cursor.md
T

469 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<request_id, CursorSessionHandle>`。
- 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<String>` 与 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<ActiveRun>,
request_bindings: RequestBindings,
}
struct ConversationIdentity {
conversation_id: ConversationId,
parent: Option<ParentConversationRef>,
}
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。
```