mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-03 18:23:51 +08:00
refactor: align project directory architecture
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
# 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。
|
||||
```
|
||||
Reference in New Issue
Block a user