diff --git a/docs/README.md b/docs/README.md index 11600184b..731b66111 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,11 @@ - [浏览器内 AI Web 工程沙箱预览](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md) - [AI Web 工程 Runner 安全模型](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) +### AI 游戏创作 Runtime + +- [AI 游戏创作 Agent Runtime 交互边界重构实施计划](./technical/【技术方案】AI游戏创作Agent%20Runtime交互边界重构实施计划-2026-08-12.md) +- [AI 游戏创作 Agent Runtime V1.1](./technical/【技术方案】AI游戏创作Agent%20Runtime%20V1.1-2026-07-12.md) + ### 后端与公开数据 - [外部生成 Worker 化方案](./technical/【后端架构】外部生成Worker化方案-2026-06-03.md) diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime交互边界重构实施计划-2026-08-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime交互边界重构实施计划-2026-08-12.md index d3dfe8d0c..a3628b63c 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime交互边界重构实施计划-2026-08-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime交互边界重构实施计划-2026-08-12.md @@ -1,13 +1,13 @@ # AI 游戏创作 Agent Runtime 交互边界重构实施计划 -更新时间:`2026-08-12` -状态:评审中 +更新时间:`2026-08-13` +状态:评审中(协议冻结前禁止进入工程编码) ## 0. 目标与范围 统一 Consumer(GUI / CLI / 自动化测试)与 Runtime 之间的公开交互边界,达到: -- **Consumer 只做两件事**:`render(state)` 与 `dispatch(intent)`,中间不保留决策。 +- **Consumer 对 Runtime 生命周期只做两件事**:`render(snapshot)` 与 `dispatch(command)`,不保留跨轮业务真相或生命周期决策;conversation/response stream 仍是独立展示通道,但不得反向推导 Runtime 状态。 - **交互 Loop 收归后端 Supervisor Shell**:Consumer 不再根据 Runtime 状态自行选择 start / steer / confirm / retry / resume。 - **GUI、CLI、测试夹具是同一套协议的平等 Consumer**,GUI 没有任何特权通道。 - **Runner 自驱**:工作发现、恢复、继续执行不依赖 Consumer 在线或主动触发。 @@ -24,102 +24,395 @@ ## 1. 交互 Loop 协议(Interaction Contract) -这是 Consumer 与 Supervisor Shell 之间唯一的公开协议。协议不绑定 transport(Tauri 命令 / Runner 协议 / 进程内函数均可用同一套 DTO)。 +这是 Consumer 与 Supervisor Shell 之间唯一的公开 Runtime 控制协议。协议不绑定 transport(Tauri 命令、Runner 协议和进程内测试均复用同一语义),但 transport 必须把调用来源传给 Shell,不能信任 Consumer 自报权限。 -### 1.1 出向事件(Shell → Consumer) +权威执行拓扑固定为:Tauri/CLI 只是 transport adapter;启用 External Runner 时,五个写命令和 Public projector 的权威 Shell handler 必须在已经取得 project execution owner 的 Runner 内执行并写项目 ledger,GUI 不得先行写一份平行 ledger。Public read/订阅通过 Runner 返回已修复投影;Runner 不可达时 transport 返回 `TRANSIENT_UNAVAILABLE`,但不能把可能陈旧的 Snapshot 伪装成成功响应。非 owner 进程不得修复 dirty journal。未启用 Runner 的进程内模式和测试使用同一 handler,并先取得等价 project owner。Developer read 在 Tauri/受信任开发 CLI 内只读内部状态并经过宿主来源 capability,不承担 Public 投影修复。 -| 事件 | 含义 | 现状落点 | 是否新增 | -|---|---|---|---| -| `progress` | 运行推进 | `status/phase` + `recentEvents` + `waitingOn/nextStep`,经 `read_game_creator_agent_runtimes` 与 `game-creator-agent-progress` 事件 | 收敛 | -| `needs_input` | 等待澄清回答 | `userInputRequest` / phase `waiting-for-user-input`(`AgentRuntimeUserInputRequest`) | 收敛 | -| `approval_required` | 等待开发者批准工具动作 | `pendingToolAction` / phase `waiting-for-confirmation`(`AgentRuntimePendingToolActionSummary`) | 收敛 | -| `tool_request` | 工具已请求/执行中 | 现散在 `recentToolCalls` + phase `action` | **新增派生** | -| `artifact` | 产物 / manifest 变化 | `game-creator-manifest-invalidated` + finalization journal | 收敛 | -| `done` | 本轮终态 | `completed` + `AgentRuntimeFinalizationJournal` + responseStream `committed` | 收敛 | -| `error` | 失败 / 需人工核对 | `failed` / `needs-reconciliation` + `error` | 收敛 | +V1 初始 `schemaVersion`(Rust 字段 `schema_version`)固定为 `game-creator-agent-interaction.v1`。Snapshot、事件 envelope、命令和公开错误必须携带该值,或由同一 transport 在调用前明确协商到该值;不支持的 major 返回 `PROTOCOL_VERSION_UNSUPPORTED`,写命令失败关闭。Rust 字段按 camelCase 序列化;公开枚举使用本文冻结的 lowerCamelCase wire value;所有公开 ID 均为不透明字符串,Consumer 不得从 ID 推导路径、run 或时序。V1 写命令使用严格字段合同:缺少必填字段、未知字段、重复字段、错误类型或超出长度/数量上限均返回 `INVALID_REQUEST`,不执行任何副作用;不通过“忽略未知字段”实现协议兼容,后续字段只能通过新 schemaVersion 引入。 -协议 DTO(camelCase 序列化,与现有一致): +### 1.1 项目身份、事实源与双 Snapshot + +- 项目 manifest 的稳定 `projectId` 是公开协议身份;`projectPath` 只作为本地 transport locator。Shell 每次调用都先 canonicalize locator、验证项目已在现有本地项目授权/目录簿中、取得并重读 manifest,再验证 `projectId` 一致。仅持有任意路径字符串不构成授权;符号链接、替换目录和 TOCTOU 按现有安全 path resolver/目录句柄约束处理。路径不进入公开 DTO、错误、事件、指纹或报告。 +- Runtime durable state 及其同事务/同锁持久投影是业务事实;`SupervisorPublicSnapshot` 是 Consumer 唯一可见的完整状态事实。事件、命令 ack、错误和 response stream 都不能被合并成另一份 Runtime 状态。 +- V1 每个项目只有一个 Public Snapshot、一个 `snapshotRevision` 和一个项目级事件流;Snapshot 只包含当前 Project Supervisor。专业 Agent/动态 child 只折叠为协作数量,不公开身份或列表。 ```rust -enum AgentRuntimeOutboundEvent { - Progress(AgentRuntimeProgress), - NeedsInput { request: AgentRuntimeUserInputRequestView }, - ApprovalRequired { action: AgentRuntimePendingToolActionSummary }, - ToolRequest(AgentRuntimeToolRequest), // 新增:Shell 从 recentToolCalls+phase 投影 - Artifact(AgentRuntimeArtifactEvent), // 收敛 manifest-invalidated + finalization - Done(AgentRuntimeDoneEvent), - Error(AgentRuntimePublicError), -} - -struct AgentRuntimeProgress { - project_path: String, agent_id: String, run_id: String, - status: String, phase: String, - current_task: String, current_action: String, - plan_steps: Vec, active_plan_step_index: Option, - waiting_on: String, next_step: String, // Shell 负责填充,Consumer 不再映射 phase→文案 +struct SupervisorPublicSnapshot { + schema_version: String, + snapshot_revision: u64, + event_cursor: String, + project_id: String, + supervisor: Option, + interactions: Vec, updated_at: u64, } -``` -> 与现状的关键差异:`progress` 里的 `waiting_on`/`next_step` 由 **Shell 填充**。当前是前端在 `model.ts:612/644` 硬编码 phase→中文文案,收归 Shell 后前端删除该映射。 +struct SupervisorRuntimeSummary { + agent_id: String, + session_id: String, + run_id: String, + status: AgentRuntimePublicStatus, + stage: AgentRuntimePublicStage, + completed_step_count: u32, + total_step_count: u32, + current_step_summary: Option, + waiting_on: Option, + next_step: Option, + collaborator_count: u32, + outcome: Option, + error: Option, + updated_at: u64, +} -### 1.2 入向命令(Consumer → Shell) - -| 命令 | 吸收的旧命令 | 说明 | -|---|---|---| -| `submit_intent` | `start_*` / `steer_*` | 一条消息可能 steer 进现有 run,也可能 start 新 run,由 Shell 判定 | -| `answer` | `answer_game_creator_agent_runtime_user_input` | 回答澄清 | -| `approve` | `confirm_*` / `reject_*` | 批准或拒绝,含 policy 确认卡 | -| `cancel` | `cancel_game_creator_agent_runtime_task` | 取消 | -| `resume` | `resume_*` / `confirm_resume_*` / `retry_*` / `confirm_retry_*` / `schedule_game_creator_agent_ready_tasks` | 恢复/重试/调度统一入口 | - -命令签名: - -```rust -#[tauri::command] -async fn submit_game_creator_agent_intent( - project_path: String, session_id: String, intent: String, - run_profile: Option, source: Option, -) -> Result; - -#[tauri::command] -async fn answer_game_creator_agent_interaction( - project_path: String, run_id: String, action_id: String, - request_id: String, response_id: String, answers: BTreeMap, -) -> Result; - -#[tauri::command] -async fn approve_game_creator_agent_interaction( - project_path: String, run_id: String, action_id: String, - approved: bool, note: String, -) -> Result; - -#[tauri::command] -async fn cancel_game_creator_agent_run( - project_path: String, agent_id: String, run_id: String, -) -> Result; - -#[tauri::command] -async fn resume_game_creator_agent_project( +struct DeveloperRuntimeSnapshot { + // 独立开发 DTO,不嵌入 Public 类型或 event cursor: + schema_version: String, + project_id: String, + source_snapshot_revision: u64, project_path: String, -) -> Result, String>; -``` - -协议外 API(不进交互 Loop,作为管理面保留在 Shell 上):goal CRUD、`compact`、会话管理、配置读写。 - -### 1.3 统一 InteractionRequired 语义 - -Shell 向 Consumer 暴露"必须等待外部回答"的单一概念,替代现在分散的 `pendingToolAction` / `userInputRequest` / policy 确认卡: - -```rust -enum AgentRuntimeInteractionRequired { - UserInput { request: AgentRuntimeUserInputRequestView }, - Approval { action: AgentRuntimePendingToolActionSummary }, - PolicyApproval { policy: String }, // 新增:吸收 confirm_resume/confirm_retry 的 agent.resume 确认卡 + selected_agent_id: String, + selected_session_id: String, + selected_run_id: String, + current_task: String, + current_action: String, + plan_revision: u64, + plan_steps: Vec, + active_plan_step_index: Option, + recent_tool_calls: Vec, + interaction_records: Vec, } ``` -> 现状的 `confirm_resume`(`commands.rs:1148`)与 `confirm_retry`(`commands.rs:1020`)是"要求用户确认策略"的产物,由 GUI 弹确认卡实现。收归 Shell 后,Shell 评估 `enforce_project_auto_permission_policy`(`verification.rs:813`),需要确认时返回 `InteractionRequired::PolicyApproval`,用户确认后经统一的 `approve` 命令继续。 +Public Snapshot 白名单固定为:稳定项目与当前 Project Supervisor 身份、紧凑阶段、完成数/总数、当前步骤摘要、等待对象、下一步、协作数量、正式用户可回答的最小交互、终态摘要和稳定公开错误。它不得包含项目绝对路径、完整任务/action/plan、动态 child 身份、原始 observation、工具名称/参数/计划、Provider 原文、`recentToolCalls` 或内部 interaction fingerprint。`tool_request` 不进入正式公开 Snapshot 或事件。 + +Developer Snapshot 使用独立命令和 Rust DTO,不嵌入 Public Snapshot,避免开发调用方误订阅正式事件后把两种投影合并。仅前端 `devMode`、query/hash 或调用者提供的布尔值不构成授权;Tauri 端只允许 debug 构建中受信任的 `developer` 窗口标签,受信任开发 CLI 使用显式本地 capability,进程内测试使用 test capability;release/client/supervisor-chat 和 Runner 普通 Consumer 一律返回 `PERMISSION_DENIED`。后续若开放其它开发调用方,必须新增等价的服务端 capability,不得复用 Public read 权限。 + +`snapshotRevision` 仅在 Public 白名单字段的规范化值真实变化并成功持久化时递增;Developer-only 变化不推进。数组按稳定 identity 排序、枚举和缺省值统一规范化后再比较,不能因文件遍历顺序产生新 revision。`updatedAt` 是该 Public 投影最后真实变化的持久时间,不直接复制底层 Runtime 每次内部写入的时间,也不参与顺序判断。投影修复若重建出同一规范化 Public Snapshot,不递增 revision、不更新时间,也不创建新逻辑事件。 + + +Projection-dirty 的提交顺序冻结为以下四步,所有会改变 Public 白名单的 Runtime 写入口必须复用,不得各自发明顺序: + +1. 在 project lock 内写入并同步 dirty journal,记录 `projectId`、operation identity、变更前 durable digest、预期写入口和 `journalVersion`。 +2. 调用现有 Runtime durable writer 原子提交业务事实;业务写入失败则将 journal 标记为可关闭的 no-op,不产生 Public revision。 +3. 从已提交的 durable state 生成规范化 Public Snapshot;若 hash 变化,按事件规则一次性持久化新 Snapshot、revision、event record 和 cursor;若 hash 未变化则只关闭 dirty journal。 +4. 同步 projection ledger 后关闭 journal,再进行 best-effort event delivery。任何中间崩溃都由 owner 恢复或 Public read 按 operation identity 幂等重跑第 3/4 步,不重复第 2 步业务副作用。 + +因此,“Runtime durable state 已提交但 projection 未刷新”是可自动补投影状态;“Runtime durable state 是否提交无法证明”不是可补投影状态,必须进入 `needs-reconciliation`。 + +Public status/stage 是稳定枚举,不直接透传内部 phase。映射必须穷尽已知内部状态:排队为 `Queued`;planning/LLM 为 `Running/Planning`;action/observation/协作为 `Running/Executing|Coordinating`;user input、developer approval、确定性 retry/lane/timer、paused 分别为 `Waiting` 下的明确 stage;completed、failed/budget-exhausted、cancelled 和 needs-reconciliation 分别映射稳定终态/核对态。遇到未知或互相矛盾的内部 status/phase 时不得猜成 Running,而要投影 `NeedsReconciliation` 和脱敏 `PUBLIC_STATE_INVALID`。具体映射表与 DTO 同模块维护并做穷尽契约测试。 + +进度只从当前 Supervisor 的可信结构化计划计算:`totalStepCount=planSteps.len()`,`completedStepCount` 只计 completed,当前摘要只取唯一 active step 的脱敏标题;没有结构化计划时为 `0/0`,不得按 tool/action/loop 数猜进度。`collaboratorCount` 只计当前 Supervisor run 的 durable、尚未终结专业协作单元并去重,不包含历史 child。`waitingOn/nextStep/outcome/error` 是有界、脱敏、仅展示的 Shell 文本,Consumer 不得解析它们路由命令;可执行能力只由 interaction view 和写命令结果决定。 + +所有可能改变 Public 白名单的 Runtime 写入都必须经过统一 projection-dirty 协议:先在同一 project lock 下写 durable dirty journal,再提交原 Runtime 变更,随后重建 Public Snapshot/事件并关闭 journal。变更前崩溃可重建为无变化,变更后崩溃可由 Public read、订阅启动、Runner 启动或项目 wake 幂等补投影。P2 必须枚举并接入现有 state、task、interaction、终态和恢复写入口;不允许依赖 Consumer 轮询偶然发现漏掉的内部变更。 + + +### 1.1.1 身份来源与生命周期 + +公开协议中的三类身份不是同一个概念,来源和生命周期固定如下: + +| 身份 | 权威来源 | 生命周期与约束 | +|---|---|---| +| `projectId` | 项目 manifest 的持久字段 | 创建项目时生成一次;迁移时只允许从已验证的旧 manifest 显式导入;写入后不可变。缺失、重复或 manifest 校验失败时项目进入 `needs-reconciliation`,不得按路径或名称猜测身份。 | +| `sessionId` | 现有项目会话管理记录 | 由会话管理面创建并持久化,带项目归属、角色和 `sessionRevision`;Project Supervisor 只能绑定一个当前有效 session。结束或切换 session 后旧 session 不可作为新命令 target。Runtime 命令不隐式创建或切换 session。 | +| `runId` | Runtime durable run 记录 | Shell 在产生 Runtime 副作用前预分配并持久化;一个 runId 只对应一次 run,终态后不可复用。`acceptedRunId` 只是该同一 runId 的公开回显,不是第二套身份。 | + +`actionId`、child instanceId、executor generation 和下游 provider request identity 只属于内部 durable 记录;它们可以参与内部恢复和幂等,但不进入正式 Public Snapshot、公开事件、公开错误或 Consumer 路由。`projectPath` 只在 transport 到 Shell 的第一步作为 locator 使用,完成 canonicalize、目录簿授权、目录句柄绑定和 manifest 复核后丢弃;后续日志和协议只使用 `projectId`。 + +所有身份校验都在取得 project execution owner 后、写入 request ledger 前完成。locator canonicalize 与 manifest 复核必须针对同一已打开目录句柄完成;复核失败返回结构化错误并不产生 ledger 记录。会话或 run 的 revision 只由其权威持久化记录递增,不能使用 Consumer 看到的时间戳或事件 sequence 代替。 + +### 1.1.2 Public 摘要枚举与投影提交合同 + +`status`、`stage`、`waitingOn`、`nextStep` 和 `outcome` 是公开稳定枚举,不向 Consumer 透传内部 phase,也不要求 Consumer 解析自然语言。V1 至少冻结以下值: + +| 字段 | 稳定值 | +|---|---| +| `status` | `idle`、`queued`、`running`、`waiting`、`completed`、`failed`、`cancelled`、`needsReconciliation` | +| `stage` | `idle`、`planning`、`executing`、`coordinating`、`waitingForUserInput`、`waitingForPolicyApproval`、`waitingForDeveloperApproval`、`waitingForTimer`、`reconciling`、`completed`、`failed`、`cancelled` | +| `waitingOn` | `none`、`userInput`、`policyApproval`、`developerApproval`、`timer`、`runner`、`reconciliation` | +| `nextStep` | `none`、`submitIntent`、`answerInteraction`、`approveInteraction`、`cancelRun`、`resumeProject`、`waitForRunner`、`reconcile` | +| `outcome` | `none`、`success`、`failure`、`cancelled`、`unknown` | + +`waitingOn` 和 `nextStep` 只用于展示提示,任何可执行按钮必须来自当前 Public `interactions` 或明确的五命令能力;Consumer 不得根据这两个字段自行拼装命令。文案由 Shell 根据稳定值本地化并做长度、路径、Provider 原文和敏感信息过滤;文案变化不改变协议状态,不单独推进 `snapshotRevision`。 + +Public projector 的一次提交以 `projectId` 为边界,在 `project execution owner → supervisor project lock → projection journal` 的锁序内完成: + +1. 读取并规范化 durable Runtime state、当前 Supervisor session/run 和 User audience interaction;校验身份、枚举和白名单。 +2. 对规范化 Public DTO 计算 `publicSnapshotHash`。与已持久化 hash 相同则关闭 dirty journal,不增加 revision、sequence、cursor 或 `updatedAt`。 +3. 有真实 Public 变化时分配 `snapshotRevision = previous + 1`、`sequence = previousSequence + 1`,并在同一 journal 中预分配 `eventId` 与新 opaque cursor。`eventId = sha256(RFC 8785 canonical JSON([projectId, snapshotRevision, publicSnapshotHash]))` 的编码结果只作为不透明 ID 返回;Consumer 不得解析其构成。 +4. 持久化 Snapshot、事件记录和 journal 状态。底层存储不能提供单文件事务时,使用 journal 恢复保证“旧 Snapshot/无事件”或“新 Snapshot/有唯一事件记录”两种可重建结果,不允许出现新 Snapshot 配旧 cursor 或同 revision 多事件。 +5. 只有 Snapshot 与事件记录均可读后才允许投递;投递失败不回滚事实,后续订阅或 wake 按同一 eventId 补投。 + +`event_cursor` 的流起点为项目专属的不透明 `origin` 游标;每个事件的 cursor 由 Shell 生成并持久化,禁止按时间、路径或可猜测的数字直接编码。读 Snapshot 与建立订阅必须共享一次 project projection lock 的线性化边界:Consumer 先取得 Snapshot 返回的 cursor,再以该 cursor 作为 `afterCursor` 建立订阅;订阅端先补发 cursor 之后已持久化的记录,再接收新投递。这样读取与订阅之间发生的事件不会丢失。 + + +### 1.1.3 Project owner、GUI-owner 与并发栅栏 + +项目执行所有权、Runner 存活所有权和 GUI 存活所有权是三层独立门禁,不得用一个布尔值互相替代: + +| 所有权 | 持有者 | 持久/运行时记录 | 失效行为 | +|---|---|---|---| +| project execution owner | 当前实际执行 Runtime 写入和调度的 Runner/进程 | 项目私有 owner record:`ownerInstanceId`、单调 `ownerGeneration`、lease 到期点 | 取得前不扫描、不写入、不修复;lease 失效后旧 owner 被 fencing,不能继续提交。 | +| Runner owner | 当前 Runner 进程 | Runner boot identity、drain 状态和 heartbeat | Runner drain 或进程失活时停止新调度;恢复只能由新 boot 在重新取得 project owner 后执行。 | +| GUI-owner | 当前授权 Runner 存活的 GUI 会话 | 现有 GUI-owner lease/heartbeat 与 release 协议 | heartbeat 到期或收到 release 后停止新调度;不撤销已持久 Runtime 事实,不把 GUI 断线当作任务取消。 | + +owner record 的取得、续租和释放使用同一项目锁内的 CAS;generation 每次成功换主递增,旧 generation 的写入返回 `OWNER_FENCED`,不得覆盖新 owner 的 ledger、Runtime state 或 projection。lease 到期不能只凭本地时钟判定可接管:新进程必须先取得 owner,再在锁内复核 Runner drain、GUI-owner 和 manifest projectId。时钟只用于 lease 超时提示,CAS/generation 才是权威。 + +调度 worker 的顺序固定为:取得/续租 project owner → 检查 Runner drain 与 GUI-owner → 从 durable wake/runnable 索引取一项 → 在 dequeue 前再次校验 owner generation 和项目状态 → 以同一 operation identity 调用 Runtime。任何检查失败都不得先 dequeue 后补救;旧 worker 在失去 generation 后的结果一律按 fencing 处理并进入既有 reconciliation 路径。 + +### 1.2 出向事件、有序性与重连 + +```rust +struct AgentRuntimeOutboundEnvelope { + schema_version: String, + event_id: String, + sequence: u64, + cursor: String, + snapshot_revision: u64, + project_id: String, + // 项目级事件可为空;有值时只作为当前 Supervisor 的刷新定位提示。 + agent_id: Option, + session_id: Option, + run_id: Option, + event: AgentRuntimeOutboundEvent, +} + +enum AgentRuntimeOutboundEvent { + SnapshotChanged, +} +``` + +V1 将公开事件刻意收窄为项目级失效通知。`agentId/sessionId/runId` 只在事件生成点存在当前 Project Supervisor 且能从同一投影线性化点确定时填充;项目级恢复、空 Supervisor 或多 Run 影响事件保持为空。它们不是状态载荷,也不能替代 Snapshot 中的当前身份。事件不携带 interaction、artifact、terminal 或 error 状态载荷;这些内容全部从完整 Public Snapshot 读取,避免事件类型演变成第二个 read model。规则如下: + +1. `eventId` 标识一次已提交的 Public Snapshot revision;同一项目同一 revision 的重建/重投沿用同一确定性 ID。投递至少一次,Consumer 按 ID 去重。 +2. `sequence` 在 `projectId` 事件流内从 1 严格递增,且一次有效 Public revision 最多对应一个 sequence。初始空投影的 revision 为 0、cursor 为流起点且不产生事件;第一次真实 Public 变化提交 revision/sequence 1。`cursor` 是绑定同一项目和 sequence 的不透明日志位置,不能跨项目使用或解析。 +3. Projection journal 的业务事实与投影提交顺序按 1.1.2 冻结;事件日志追加只允许在 Snapshot 可完整读取后进行。若崩溃在两步之间,恢复只补同一 revision 的唯一事件;不生成第二 revision 或第二 eventId。 +4. 一个 envelope 引用的 revision 发布时必须已经可读。Consumer 收到后读取完整 Snapshot,只接受 `snapshotRevision >= envelope.snapshotRevision`;本地已有更高 revision 时忽略提示。短暂读不到目标 revision 时有界重读,仍失败则显示结构化暂态错误,绝不合并事件载荷。 +5. 首次读取 Snapshot 得到与该读取线性化点一致的 `eventCursor`,随后从 `afterCursor` 订阅;读取与订阅间发生的更新会出现在补读结果中。断线后沿用最后确认处理的 cursor 补读。sequence 回退或缺口只触发补读与全量刷新,不猜测状态。 +6. 每个项目至少保留最近 256 条 envelope。cursor 未知、属于其它项目或已过窗口时返回 `CURSOR_INVALID` / `CURSOR_EXPIRED`;Consumer 重新读取完整 Snapshot,并从新 `eventCursor` 继续订阅。 +7. Snapshot 才是状态事实;事件日志只承担通知、缺口检测和审计定位,不能独立还原 Runtime 状态。 + +### 1.3 五个写命令、结果合同与幂等状态机 + +| 命令 | 吸收的旧命令 | V1 业务输入 | +|---|---|---| +| `submit_intent` | CLI `Reply/Execute/Resume` 与 `start_*` / `steer_*` | 用户消息、目标 Project Supervisor sessionId 与公开 runProfile;Shell 判定 direct reply/start/steer/resume,source 由 transport 派生 | +| `answer` | `answer_*_user_input` | `interactionId + responseId + answers` | +| `approve` | `confirm_*` / `reject_*` | `interactionId + responseId + decision(approve/reject)` | +| `cancel` | `cancel_*` | 当前公开 Project Supervisor `runId` | +| `resume` | `resume_*` / `retry_*` / `schedule_*` | 项目级恢复意图;若需确认则只创建/返回 InteractionRequired | + +所有写命令都包含: + +```rust +struct AgentRuntimeCommandMeta { + schema_version: String, + project_id: String, + request_id: String, +} + +struct InteractionResponseMeta { + interaction_id: String, + response_id: String, + expected_interaction_revision: u64, +} + +struct AgentRuntimeCommandResponse { + request_id: String, + request_fingerprint: String, + replayed: bool, + observed_snapshot_revision: u64, + result: T, +} + +enum AgentRuntimeCommandAck { + IntentAccepted { + disposition: IntentDisposition, // Reply | Start | Steer | Resume + accepted_run_id: Option, + response_message_id: Option, + }, + InteractionAccepted { interaction_id: String, decision: Option }, + CancelAccepted { run_id: String }, + ResumeAccepted { affected_run_count: u32 }, + InteractionRequired { interaction_id: String, interaction_revision: u64 }, +} + +struct AgentRuntimePublicError { + schema_version: String, + code: String, + kind: AgentRuntimePublicErrorKind, + retryable: bool, + message: String, + request_id: Option, + request_fingerprint: Option, + replayed: bool, + observed_snapshot_revision: Option, + interaction_required: bool, +} +``` + +`submit_intent` 要求 Consumer 先经现有会话管理面取得明确的 Project Supervisor `sessionId`;Shell 锁内验证该 session 属于本项目/当前 Supervisor,active session 已变化则返回 `TARGET_STALE`,本轮不把会话 CRUD 隐式塞入 Runtime 命令。`source` 由受信任 transport 固定映射,Consumer 不得自报任意 source;`runProfile` 使用公开白名单枚举并在锁内校验当前项目支持。Shell 复用现有 interaction kernel 决定 direct reply/execute/resume,再对 execute 决定 start/steer;direct reply 仍只写 conversation/response stream,不伪造 Runtime Snapshot 变化,其稳定 responseMessageId 预先写入 request ledger。显式 `resume` 命令服务按钮/自动化的结构化恢复意图,自然语言“继续”也可由 `submit_intent` 路由到同一内部实现。 + +`submit_intent/cancel/resume` 不使用项目级 `snapshotRevision` 作为业务 CAS:无关的进度刷新不能让用户命令无效。它们在锁内以 payload 中的精确目标身份和当前 durable state 校验可执行性;`cancel` 的 run 已切换时返回 `TARGET_STALE`。`answer/approve` 使用 interaction 自身的 `expectedInteractionRevision`,而不是全项目 Snapshot revision;Public Snapshot 中的 interaction view 同时投影该值。命令返回值只是最小 ack 和操作完成时观察到的 revision,不携带 Runtime state;Consumer 成功或 `interactionRequired=true` 后都重读完整 Snapshot。 command ack 的 `responseMessageId` 只用于在既有 conversation/response-stream 管道定位 direct reply;对话正文仍通过原有 durable conversation read/stream 获取,不进入 Snapshot、事件或 command response。`observedSnapshotRevision` 是操作完成时已闭合的 Public revision,不表示命令结果本身是一份状态。 + +公开错误使用稳定 `code/kind/retryable/message/interactionRequired`,命中 ledger 的错误还返回原 `requestFingerprint` 和 `replayed`。Consumer 不解析中文 message。最小错误矩阵如下;未知错误码按不可重试失败关闭: + +| code | 语义 | retryable / Consumer 动作 | +|---|---|---| +| `PROTOCOL_VERSION_UNSUPPORTED` / `INVALID_REQUEST` / `PERMISSION_DENIED` | ledger 前的版本、格式或权限拒绝 | false;修正客户端/权限,不能原请求盲重试 | +| `TARGET_STALE` / `INTERACTION_STALE` / `INTERACTION_ALREADY_RESOLVED` | 精确目标或 interaction 已变化 | false;重读 Snapshot,若仍需操作则新 requestId | +| `IDEMPOTENCY_KEY_REUSED` | 同 requestId/responseId 被不同内容复用 | false;视为调用方错误 | +| `COMMAND_IN_PROGRESS` | 同请求已有 live executor 或同 response 正在 Resolving | true;同 payload/requestId 读回或重试,不启动第二 executor | +| `COMMAND_RESULT_UNKNOWN` / `NEEDS_RECONCILIATION` | 已受理操作的外部结果无法证明 | false;重读并进入人工核对,禁止换 ID 自动重放 | +| `OWNER_UNAVAILABLE` / `OWNER_FENCED` / `TRANSIENT_UNAVAILABLE` | 尚未受理,或当前执行者已失去 owner generation | true;完全相同 requestId 可重试;旧 owner 不得继续写入 | +| `REQUEST_NOT_FOUND` | 当前 project ledger 没有该 requestId 的受理记录 | false;不泄漏项目存在性;调用方根据原 transport 结果决定是否用原 requestId 重试 | +| `CURSOR_INVALID` / `CURSOR_EXPIRED` | 事件补读起点非法或过期 | 不适用于写重试;全量读取 Snapshot 后换新 cursor | +| `INTERNAL` | 已脱敏的未分类内部失败 | false,除非未来细分为明确暂态 code | + +`retryable=true` 仅表示可用同一 requestId 重试同一请求;需要基于新 Snapshot 改变 payload 时必须使用新 requestId。 + + +### 1.3.1 请求结果读回与崩溃语义 + +`read_game_creator_agent_command_result(projectId, requestId)` 是只读恢复接口,不是第六个 Runtime 写命令。它返回项目内 request ledger 的以下稳定投影: + +```rust +struct AgentRuntimeCommandResultView { + schema_version: String, + project_id: String, + request_id: String, + request_fingerprint: String, + status: CommandLedgerStatus, + replayable: bool, + result: Option, + error: Option, + observed_snapshot_revision: Option, +} +``` + +读回先做 locator/projectId/调用来源复核,再在 project command lock 内查询;不能跨项目按 requestId 搜索。`prepared`/`executing` 返回 `COMMAND_IN_PROGRESS` 或等价的 `status`,Consumer 继续用同一 requestId 读回;`succeeded`/`rejected` 永久返回已保存结果;`outcome-unknown` 返回 `COMMAND_RESULT_UNKNOWN` 并标记 `needs-reconciliation`。如果 requestId 从未被受理,返回不泄漏项目存在性的 `REQUEST_NOT_FOUND`;该错误只表示“本次调用没有留下受理记录”,调用方仍需根据原 transport 响应决定是否使用同一 requestId 重试,不能据此生成新 requestId 重放未知副作用。已进入 ledger 的确定性业务拒绝必须通过 `rejected` 结果读回,而不是依赖错误文字重新判断。 + +request ledger 的状态转移冻结为: + +```text +prepared -> executing -> succeeded | rejected | outcome-unknown +prepared -> rejected (可证明尚未产生副作用的业务拒绝) +executing -> succeeded | rejected (有权威 durable 证据) +outcome-unknown -> needs-reconciliation(终态,不自动回退) +``` + +每条记录保存 `ledgerVersion`、创建/更新时间、请求指纹、操作身份、执行者 generation、状态和完整结果引用;写入使用临时文件/同步/原子替换,恢复时按版本校验,损坏记录保留原始证据并阻止同 requestId 再执行。`replayed=true` 仅表示返回已持久化的同一结果,不代表再次执行。任何“Shell 已写入 prepared 但 transport 未收到响应”的情况都必须先读回;不能以 unknown-command fallback 或新 requestId 规避 ledger。 + +request ledger 是项目级私有 durable 记录,状态为 `prepared / executing / succeeded / rejected / outcome-unknown`,并保存规范请求指纹、预分配的内部 operation identity、executor boot/generation 及完整权威成功或错误结果。恢复所需的用户消息/answers 只保存有界私有 payload 或指向既有 durable conversation/interaction record 的稳定引用,沿用现有内容安全、权限和脱敏规则;绝不复制到 Public Snapshot、事件或错误。requestId 提供幂等受理与结果读回边界,不承诺无法判定的外部副作用 exactly-once;这种窗口必须显式 outcome-unknown。处理顺序固定: + +1. transport 先 canonicalize root、验证本地项目授权、manifest `projectId` 与调用权限;版本/身份/权限失败发生在 ledger 之前,保证攻击者不能向任意项目写记录。 +2. Shell 用 RFC 8785 canonical JSON 规范化“schema version + command kind + projectId + 完整业务 payload(含 interaction/response identity、decision、answers 和任何精确 target)”,计算小写 SHA-256。`requestId`、路径、时间戳及 transport 字段不进指纹。 +3. 在 project command lock 内先按 `requestId` 查 ledger,再做任何当前状态校验。相同 ID/相同指纹的 `succeeded` 或 `rejected` 直接返回原结果;不同指纹返回 `IDEMPOTENCY_KEY_REUSED`;`prepared/executing` 返回 `COMMAND_IN_PROGRESS`(可同 ID重试/读回);`outcome-unknown` 返回原 `COMMAND_RESULT_UNKNOWN`,不再执行。 +4. 只有 ledger 未命中时才验证 target/interaction 当前状态,并在副作用前原子写 `prepared`;`submit_intent` 的 acceptedRunId/steerId 以及下游 Runner requestId 必须在 prepared 中预分配并在恢复时复用,不能在重试中生成第二身份。业务拒绝也原子落为 `rejected`,使同一请求重放得到相同结果。进入内部执行前转为 `executing` 并绑定 executor;完成内部状态写后必须先闭合对应 Public projection,再写入并回读 `succeeded/rejected` 权威结果和 `observedSnapshotRevision`。 +5. 崩溃恢复只能依据 ledger、executor 生命状态、Runtime journal 和既有 durable identity 前向闭合;仍有 live executor 时不得由第二执行者接管。能证明未产生副作用可用同一 operation identity 继续;能证明结果则幂等补投影/结果;外部结果不明则原子转为 `outcome-unknown` 并使项目进入 `needs-reconciliation`,禁止自动换 requestId 或重复入队。 +6. Consumer 对 transport 超时、`COMMAND_IN_PROGRESS` 或明确暂态错误只可重发完全相同 payload 和同一 requestId,或调用 `read_game_creator_agent_command_result(projectId, requestId)`;读回接口同样先完成 locator/project identity/权限复核,禁止跨项目扫描。 +7. V1 ledger 跟随项目 Runtime durable archive 生命周期保存,不按时间或条数隐式淘汰。若将来压缩,必须先设计持久 tombstone,使已淘汰 requestId 仍能失败关闭。 + +现有 Runner 内存 request cache、`acceptedRunId` 和 Goal CAS 只作为内部附加护栏,不替代公开 ledger。goal CRUD、`compact`、会话管理和配置读写属于管理面,不进入这五个 Runtime Loop 命令;它们若是公开写操作,继续遵守各自现行 CAS/权限合同,不能借本次重构降级。 + + +### 1.3.2 五命令请求体冻结 + +五个公开写命令使用严格 tagged union;除 `schemaVersion/projectId/requestId` 外,业务字段如下。未列出的字段不属于 V1,transport 不得透传额外字段参与执行;未知字段、重复字段和错误字段类型统一返回 `INVALID_REQUEST`,不得通过忽略未知字段实现兼容。后续新增字段必须提升 schemaVersion 或经过显式兼容协议协商,并同步更新请求指纹规则。 + +```rust +struct SubmitIntentCommand { + meta: AgentRuntimeCommandMeta, + session_id: String, + message: String, + run_profile: AgentRuntimeRunProfile, +} + +struct AnswerCommand { + meta: AgentRuntimeCommandMeta, + interaction: InteractionResponseMeta, + answers: Vec, +} + +struct ApproveCommand { + meta: AgentRuntimeCommandMeta, + interaction: InteractionResponseMeta, + decision: ApprovalDecision, // Approve | Reject,必填 +} + +struct CancelCommand { + meta: AgentRuntimeCommandMeta, + session_id: String, + run_id: String, +} + +struct ResumeCommand { + meta: AgentRuntimeCommandMeta, + resume_scope: AgentRuntimeResumeScope, // Project + reason: ResumeReason, +} +``` + +`submit_intent.message` 不能为空,长度上限沿用现有 Runtime 输入限制;`sessionId` 必须是当前 Project Supervisor 的 session。`cancel` 必须同时提交当前 `sessionId + runId`,防止旧 runId 被新会话误取消;run 已终结、session 已切换或绑定关系不一致均为 `TARGET_STALE`。V1 `resumeScope` 只允许 `project`,`reason` 使用稳定枚举 `userRequested | ownerRecovered | timerElapsed | interactionResolved`;`ownerRecovered/timerElapsed/interactionResolved` 只能由受信任 Shell/Runner 生成,Consumer 只能提交 `userRequested`。`runProfile` 只允许已注册的公开 profile 名称和版本,不能携带 Provider、模型、工具、提示词或路径配置。 + +`AnswerCommand` 的答案结构只允许 Public Snapshot 当前 interaction view 中声明的 question/option/自由回答约束;缺失、重复、越界或不符合约束返回 `INVALID_REQUEST`,不产生部分写入。`ApproveCommand` 的 decision 不允许由 UI button、命令名或缺省值推断。`requestFingerprint` 覆盖上述规范化业务请求体以及 `schemaVersion/commandKind/projectId`,不覆盖 transport source、locator、时间戳、重试次数或响应展示文案。通过身份、权限和版本校验的请求,即使因 target stale、interaction stale、当前状态不允许或策略拒绝而没有 Runtime 副作用,也必须在 ledger 中以 `rejected` 持久化;只有格式、版本、身份或权限失败且请求尚未进入项目 ledger 的情况才返回 `REQUEST_NOT_FOUND`。 + +### 1.4 InteractionRequired 身份、版本和解决状态机 + +```rust +// Shell 内部 durable record;不直接作为正式公开 DTO。 +struct AgentRuntimeInteractionRecord { + interaction_id: String, + interaction_revision: u64, + kind: AgentRuntimeInteractionKind, + scope: AgentRuntimeInteractionScope, + audience: AgentRuntimeInteractionAudience, + project_id: String, + agent_id: Option, + session_id: Option, + run_id: Option, + action_id: Option, + request_fingerprint: String, + bound_state_fingerprint: String, + status: AgentRuntimeInteractionStatus, + resolution: Option, + private_presentation: AgentRuntimeInteractionPrivatePresentation, +} + +struct AgentRuntimeInteractionView { + interaction_id: String, + interaction_revision: u64, + kind: AgentRuntimeInteractionKind, + scope: AgentRuntimeInteractionScope, + status: AgentRuntimeInteractionPublicStatus, + presentation: AgentRuntimePublicInteractionPresentation, +} + +enum AgentRuntimePublicInteractionPresentation { + UserInput { + questions: Vec, + allow_freeform: bool, + }, + PolicyApproval { + title: String, + summary: String, + allowed_decisions: Vec, + }, +} + +enum AgentRuntimeInteractionKind { UserInput, ToolApproval, PolicyApproval } +enum AgentRuntimeInteractionScope { Project, Run, Action } +enum AgentRuntimeInteractionAudience { User, Developer } +enum AgentRuntimeInteractionStatus { Open, Resolving, Resolved, Superseded } +enum AgentRuntimeInteractionPublicStatus { Open, Resolving } +``` + +`interactionId` 在项目内稳定唯一,`interactionRevision` 从 1 开始并只在该 interaction 的可回答内容、约束或状态变化时递增;`responseId` 在单个 interaction 内唯一。`boundStateFingerprint` 绑定创建交互的 durable 对象与策略前提,不能用全项目 revision 替代。项目级 resume/retry 的 PolicyApproval 使用 `scope=Project`,可在锁内覆盖重新枚举出的多个 run,因此 agent/session/run/action 均可为空;ToolApproval 使用 Action scope 并绑定精确 action;UserInput 按真实落点使用 Run 或 Action scope。 + +Public Snapshot 只投影 `audience=User` 且状态为 Open/Resolving 的最小 view;Open 可回答,Resolving 只显示处理中并禁用再次提交,Resolved/Superseded 从公开列表移除。正式写命令仍在执行时重新校验当前调用来源与项目策略,不公开 audience、request fingerprint、bound fingerprint、内部 actionId、策略字符串、工具名称/参数或动态 child 身份。`audience=Developer`(包括现行 ToolApproval)只进入 Developer Snapshot 的脱敏 debug interaction view;正式 Public 仅通过 Supervisor summary 的 `waitingOn=developer-approval` 表示暂停,不创建可点击 interaction。内部 private presentation 与 Public presentation 使用不同 DTO;Public presentation 采用严格 tagged union 和长度上限:UserInput 从本地私有 user-input sidecar 经过敏感信息/路径过滤后,复用现行最多 3 题、每题 2–3 选项与自由回答约束;若问题或选项不能安全公开则转为 `audience=Developer`/needs-reconciliation,不把原文带入 Public。用户 PolicyApproval 只含有界、脱敏的行为影响摘要和允许 decision。Public view 只允许 UserInput/UserInput 和 PolicyApproval/PolicyApproval 两种 kind/variant 组合,未知或不匹配的 variant 按不支持协议失败关闭;presentation 不是可执行 payload。Developer ToolApproval 使用独立 debug DTO。 + +`answer` 仅接受 UserInput,`approve` 仅接受 ToolApproval/PolicyApproval;命令与 kind 不匹配返回 `INVALID_REQUEST` 且零副作用。`answer/approve` 的处理顺序为:先走 request ledger 重放检查,再取得 project execution owner 与 interaction lock,重读 record,校验 `interactionId + expectedInteractionRevision`、状态为 Open、bound fingerprint 仍与 durable 对象一致,然后重新执行当前权限和策略检查。通过后以 responseId 和 response fingerprint 原子转为 Resolving,调用既有内部 answer/confirm/reject/resume 实现,最后写 Resolved 和权威 command result;response fingerprint 覆盖 interactionId、interactionRevision、responseId、decision/answers,不能只散列自由文本。`approve` 必须显式携带 `decision=approve|reject`;禁止从按钮、命令名或缺省值猜测。 + +即使 Consumer 更换 command requestId,同一 `responseId`、相同 response fingerprint 也只创建新 command ledger 的幂等成功结果并返回原 interaction resolution,不重复消费;同一 responseId 不同内容失败为 `IDEMPOTENCY_KEY_REUSED`。已由其它 response 解决返回 `INTERACTION_ALREADY_RESOLVED` 并携带 `interactionRequired=false`;interaction revision、绑定对象或策略前提漂移返回 `INTERACTION_STALE`,零副作用。项目其它无关 Runtime/Snapshot 更新不使 interaction stale。禁止只凭持久化的 `"agent.resume"` 字符串或旧 policy 文案直接恢复。 --- @@ -133,7 +426,7 @@ enum AgentRuntimeInteractionRequired { | Runner 自驱续跑定时器 | `runtime_driver/provider_recovery.rs` 的 `schedule_waiting_provider_retry_wake_after_lane_release` 等 | 已存在,P4 直接复用 | | 确定性 e2e | `scripts/agent-runtime-deterministic-playable-e2e.mjs` + `deterministic-lane-defense-provider.mjs`(`expectedProviderStats`/`expectedChildReport` 断言) | P0 基线扩展(协议级 trace) | | 版本协商 fallback | 前端 `model.ts:1186-1226` `isMissing*CommandError` | 迁移期新旧并存的标准模式 | -| 逐命令幂等 | `accepted_run_id`(`runtime_state.rs:1548`)、runner requestId 缓存(`runner/protocol.rs:353`)、goal CAS | P1 设计直接沿用 | +| 内部幂等护栏 | `accepted_run_id`(`runtime_state.rs:1548`)、runner requestId 缓存(`runner/protocol.rs:353`)、goal CAS | 继续复用;P1 建 request ledger 底座,P3 接入五个公开写命令 | | 进程内集成测试 | `src-tauri/tests/`(`command_runtime.rs`、`runtime_actions/`、`collaboration/`、`goal.rs`) | P1-P6 每步回归的护栏 | ### 2.2 需要收敛/改造的点 @@ -151,169 +444,201 @@ enum AgentRuntimeInteractionRequired { ## 3. 分阶段实施计划 -> 主线:**先建安全网 → 建统一入口 → 统一状态读取 → 收回决策 → Runner 自驱 → 迁移全部 Consumer → 删旧面**。 +> 依赖顺序:**先建安全网 → 建持久协议底座 → 建唯一读模型 → 一次性开放完整写协议并收回决策 → Runner 自驱 → 迁移 Consumer → 删除旧公开面**。后续阶段不得反向依赖尚未落地的公开 DTO。 -### P0 行为基线(安全网) +### P0 行为基线与协议验证框架 -**目标**:在改动前建立可判断"公开行为是否变化"的验证能力。 +**目标**:在改动前建立可判断迁移语义和新协议安全性的验证入口,不冻结旧 DTO。 -- 复用 `deterministic-lane-defense-provider.mjs`,在现有 `agent-runtime-deterministic-playable-e2e.mjs` 基础上**增加协议级事件 trace**: - - 录制一条确定性完整会话的**出向事件序列**(progress/needs_input/approval_required/done/error 的顺序与关键载荷),归一化 timestamp、`run_id`/`steer_id`/`request_id` 随机 ID、`accepted_run_id` 对账值。 - - 断言当前 master 的 trace 与预期一致(快照 diff)。 -- 建立统一的回归命令,P1-P6 每阶段结束必跑: - - 确定性 e2e(功能正确性) - - `src-tauri/tests/` 进程内集成测试(单元级护栏) - - 协议级 trace(迁移等价性) -- **不动 Runtime 代码**,只建安全网。 +- 复用确定性 Provider 与现有进程内 Runtime 测试,记录当前 master 的用户可见语义:提交模式、目标 run、等待/终态、批准/拒绝、取消、恢复及重复副作用计数;统一归一化随机 ID 和时间戳。 +- 为第 1 节建立尚未启用的契约 fixture:项目身份、Public 白名单、Developer capability、revision/cursor、request ledger 状态机、Interaction 状态机、结构化错误和崩溃点。 +- 基线比较只要求业务语义和副作用一致,不要求旧 DTO、旧事件名字或旧命令调用序列与新协议相同。 +- P0 不修改 Runtime 生产行为,也不以空 handler、ignored 断言或永真 stub 让新契约提前通过。 -**验收**:上述三条命令在当前 master 全绿,trace 基线文件入库。 +**完成门禁**:当前 master 基线可重复通过;每个协议不变量都有明确测试入口和预期失败原因,能区分“尚未实现”与“错误通过”。 -### P1+P2 统一入口 + 状态读取(adapter + Snapshot 投影) +### P1 持久协议底座 -**目标**:Consumer 改走新协议调用旧实现;状态读取收敛为稳定的公开 Snapshot。**旧接口全保留**为迁移期兼容路径。 +**目标**:先落不依赖公开命令和 Snapshot 的共享基础设施,避免 P1 命令反向依赖 P2/P3。P1 不注册五个新公开命令。 -**P1 适配器(`submit_intent` / `approve` / `answer` / `cancel` / `resume` 新命令)**: +- 新增 `agent/supervisor_shell/` 内部模块,集中处理 canonical root、manifest project identity、调用来源 capability、结构化公开错误映射和 project lock 顺序;`schemaVersion`、五命令、错误码及 Snapshot/Interaction DTO 放入同一共享契约模块,由 Rust 与 TypeScript 绑定共同生成/校验,避免两端手抄漂移。 +- 实现带 schema 的 command ledger、interaction ledger、projection journal 基础读写:私有目录、原子写/回读、损坏隔离、锁、状态转移校验、同 ID 指纹冲突和 archive 生命周期。 +- 将 RFC 8785 + SHA-256 规范化、request/response fingerprint、公开文本脱敏和稳定 ID 生成收敛为单一实现;禁止各命令自行拼接字符串做指纹。 +- 明确锁顺序为 `project execution owner → supervisor project lock → command/interaction/projection 子记录`;不得持有文件锁等待 Consumer,也不得绕过现有 Runtime 的 run/action 锁顺序。长时间内部执行使用 ledger ownership 标记而非长期占用 transport 线程锁。 +- 现有公开命令和 Runner 内存 request cache 行为不变;P1 只通过存储/状态机单测和 crash-point 测试验证底座。 -> **P1 与 P3 的边界**:P1 只做"命令可用 + `submit_intent` 内部判定 steer/start"。`answer`/`approve`/`resume` 在 P1 **只是入口封装**(内部调旧函数),"收到交互该调哪个命令、能否重试"的判定**仍在前端**;到 P3 才把判定收走,前端只剩 `submit`/`respond` 两种动作。 +**完成门禁**:ledger 状态转移、相同/不同指纹、torn write、损坏记录、权限拒绝和锁竞争均失败关闭;尚无新公开调用面,旧行为基线不变。 -- 新增 `src-tauri/src/agent/supervisor_shell/`,内含: - - `intent.rs`:`submit_intent` 内部路由——读当前 runtime → 判定 steer/start(逻辑取自 `swarm_cli/turn_dispatch.rs` 的 steer 判定与前端 `matchingAgentRuntimeForSteer`)→ 调现有 `steer_game_creator_agent_runtime_task_for_profile_at` 或 `start_game_creator_supervisor_background_task_for_session_at` → 返回 `{ mode, accepted_run_id, runtime }`。 - - `interaction.rs`:`approve/answer/cancel` 薄封装现有 `confirm/reject/answer/cancel` 内部函数(P1 只封装,判定仍在前端)。 - - `resume.rs`:`resume_game_creator_agent_project` 吸收 `resume/confirm_resume/retry/confirm_retry/schedule_ready`——Shell 判定是否需 policy 确认、是否需先 cancel 再重试(`needs-reconciliation` 分支,逻辑取自 `App.tsx:6205` `handleProjectSupervisorRetry`)。 -- 新命令与旧命令**同时注册**(`main.rs` invoke_handler)。 -- 前端新增"调新命令 → 后端报 unknown command → 回退旧命令"的版本协商(复用 `isMissing*CommandError` 模式),保证打包版本不一致时旧链路可用。 -- **fallback 边界**:仅"命令不存在(版本不兼容)"确定性回退;写操作的其他错误原样呈现,不做盲目重试(防重复入队)。 +### P2 Public/Developer Snapshot 与项目级事件流 -**P2 Snapshot 投影(状态读取收敛)**: +**目标**:先建立稳定、完整、可重连的唯一公开读模型,继续保留旧 read 接口供迁移。 -> **投影 = 读模型**:把同一份 Runtime durable state(唯一事实来源)按一个稳定、精简、面向消费的 schema 重新导出,作为 Consumer 的权威视图。它**不是新的事实来源**,只是同一份事实的另一种呈现;Consumer 依赖投影,业务真相仍在 durable state。 +- 从现有 Runtime state、task/event、response stream 元数据和 pending sidecar 生成第 1.1 节双投影;Public 只包含当前 Project Supervisor,Developer 通过独立受信任 capability 读取选定内部 run。 +- 在 shadow/read-only 语义下把既有 user-input、pending tool confirmation 和可识别的 policy confirm 物化为稳定 Interaction record/view:首次投影在 owner/lock 内持久化 identity,后续按 bound fingerprint 复用;旧 sidecar 缺少可信绑定时投影 needs-reconciliation,不能每次读取生成新 interactionId。P2 只建立/刷新 record,不改变旧命令的交互行为。 +- project projection ledger 原子维护当前规范化 Public Snapshot、revision、event cursor、最近 256 条 envelope 及恢复 journal;每次 Public read 先在 projection lock 内修复未闭合 journal,再返回同一线性化点的 Snapshot/cursor。 +- 提供 `read_game_creator_agent_runtime_snapshot`、Public 事件订阅和 `afterCursor` 有界补读;Developer read 使用独立命令/DTO,不与 Public 返回 union。 +- Public revision 只由白名单变化推进;事件只发布 `SnapshotChanged`,同 revision 的恢复沿用同 eventId。测试不得依赖 Tauri best-effort event 自身保存补读历史。 +- P2 不删除 Consumer normalize/merge,也不注册五个写命令;只允许测试或 shadow observer 对照旧 read 与新 Snapshot。 -- 新增 `supervisor_shell/snapshot.rs`:`AgentRuntimeSnapshot` 投影。 - - 输入:现有 `AgentRuntimeResult.state` + `recent_events`/`recent_tasks`/`response_stream`/`user_input_request`。 - - 输出:稳定的公开视图(agent/session/run 身份、status/phase、`InteractionRequired`、progress、终态、公开错误)。 - - **内部字段(recentToolCalls、observations、allowedTools、contextUsage 等)不进 Snapshot**。 -- 关键:**后端先补"稳定读取"**——现状前端 `normalizeAgentRuntimeState` 做跨轮 carry-forward,是因为后端 read 在恢复/竞态时字段不稳定。P2 后端投影保证同一 run 身份下字段自洽,前端才能删掉自己的修补。 -- 新增 `read_game_creator_agent_runtime_snapshot(s)` 命令(或改造现有 read 返回 Snapshot),旧 `read_game_creator_agent_runtime(s)` 保留。 -- 前端 `normalizeAgentRuntimeState` / `mergeAgentRuntimeStateIntoMap` / phase→文案映射**依赖 P2 稳定后删除**(本轮先做投影,下一阶段删前端逻辑)。 +**完成门禁**:重复、乱序、缺口、读订阅竞态、cursor 非法/过期、投影崩溃窗口和本地高 revision 均通过完整 Snapshot 收敛;正式 Public 零路径、完整任务/action/plan、动态 child、原始工具计划和 Provider 正文;Developer capability 服务端拒绝未授权来源。 -**验收**: -- 新命令与旧命令对同一场景返回的终态一致(用 P0 trace + 确定性 e2e 断言)。 -- 前端在"走新 Snapshot"下渲染与旧路径一致(组件回归)。 -- 现有 `command_runtime.rs` / `collaboration/` 测试全绿(旧逻辑未动)。 +### P3 完整写协议与 Interaction Loop 收归 -### P3 Loop 收归 Supervisor Shell +**目标**:在 P1 底座和 P2 唯一读模型都可用后,一次性注册真实可用的五命令;不发布“DTO 已存在但仍要求 Consumer 选择旧分支”的半成品协议。 -**目标**:Consumer 只 dispatch 意图,不再持有生命周期判断。 +- `submit_intent` 将 CLI 的 Reply/Execute/Resume interaction kernel 和 GUI 的 start/steer 判定上提到 Shell:先决定 direct reply/execute/resume,execute 再读取当前 Project Supervisor 与 run profile 决定 start/steer;source 由 transport 派生,最终调用现有内部实现。 +- 接管 P2 已物化的 user-input/tool-confirm Interaction record,并为项目 resume/retry policy confirm 创建稳定 record;`answer/approve` 只按 interaction response meta 路由,approve 显式处理 approve/reject。 +- `cancel` 锁内校验精确当前 Supervisor run;`resume` 统一处理 pending、确定性 retry/timer、ready task 和 reconciliation policy,遇到需人工确认时创建项目级 PolicyApproval 而不是执行。 +- 五命令全部先走 request ledger,再进行 target/interaction 校验和内部调用;成功、业务拒绝、并发 in-progress、崩溃可证明结果及 outcome unknown 都按第 1.3 节闭合。 +- Shell 负责生成 Public `stage/waitingOn/nextStep` 和安全 Interaction presentation;CLI 的 Reply/Execute/Resume、Consumer 的 start/steer/confirm/retry/resume 判断在此阶段只作为旧公开路径的兼容实现存在,不作为新命令输入。 +- 新旧公开命令并存,但新命令从注册之日起即具备完整生产语义。所有旧写 wrapper 同时改为经过同一 Shell project lock 和 projection-dirty/Interaction 同步 adapter:旧接口可以没有新 requestId 保证,但不能绕过 Interaction 状态、Public 投影或与新命令并发写出矛盾事实。P3 通过进程内调用和专用协议 harness 验证,不提前迁移正式 CLI/GUI 调用点。 -- 完成 `submit_intent` / `approve` / `answer` / `cancel` / `resume` 对全部旧分叉的吸收(P1 已建,本轮做全): - - `approve` 吸收 confirm/reject,并按 interaction kind 分派(`UserInput`/`Approval`/`PolicyApproval`)。 - - `resume` 吸收 resume/confirm_resume/retry/confirm_retry/schedule_ready。 -- 建立 `AgentRuntimeInteractionRequired`(见 1.3),Shell 统一暴露"需等待外部回答"。 -- Shell 负责填充 `waiting_on`/`next_step`(从 phase 映射,逻辑上提自 `model.ts:612/644`)。 -- 新增派生事件 `tool_request`(Shell 从 `recentToolCalls` + phase 投影)。 -- **前端删除**: - - `submitProjectSupervisorRuntimeTask` 的 steer/start 决策(`model.ts:849`)。 - - `agentRuntimeCanCancel/CanRetry/CanConfirm` 门禁(`model.ts:1131-1154`)。 - - `App.tsx:5981/6127/6205` 的 confirm/retry/repair 路由逻辑。 -- CLI(`swarm_cli`)改为调用同一 Shell:决策逻辑上提后,CLI 只保留 stdin/stdout 终端交互与 observer 渲染。 +**完成门禁**:五命令的同 requestId 重放、同键异内容、并发重复、业务拒绝重放、Runner 强杀读回和 unknown outcome 全部闭合;无关 Snapshot 更新不使 interaction 失效,interaction/策略漂移失败关闭;项目级 PolicyApproval 可安全覆盖锁内重新枚举的多个 run;新旧路径用户可见终态与副作用计数等价。 -**验收**: -- CLI 走新协议跑通完整 supervisor+子 Agent 流程(`agent-swarm-test-chat.mjs` / 确定性 e2e)。 -- GUI 提交、确认、重试、恢复均通过统一 `submit_intent/approve/resume`,无 `steer_*`/`confirm_*`/`retry_*` 直接调用。 -- P0 协议级 trace 在"新旧实现各放一遍"下事件序列一致。 +### P4 Runner 自驱与安全恢复 -### P4 Runner 自驱 +**目标**:Runner 合法存活期间,工作发现、确定性唤醒和安全恢复不依赖 Consumer 调用,同时保持 owner、drain 和未知外部结果边界。 -**目标**:工作发现、恢复、继续执行不依赖 Consumer 触发。**这是重构主线的一部分,不是独立项目。** +**项目目录簿**: -- 项目目录簿:`runner/state.rs` 的 `known_roots`(当前进程内)→ 持久化到 AppData(复用 runner 的 `--config-dir`),Runner 重启后仍知道持有过哪些项目。 -- 启动自恢复:`runner/server.rs` 启动完成后,对 known roots 调 `has_recoverable_game_creator_agent_background_tasks_at`(`recovery_scan.rs:425`),有可恢复工作则自动 `resume_game_creator_agent_background_tasks_at`。 -- 空闲自扫描:主 accept loop(`server.rs:275`,已有 25ms `EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL`)内增加"是否有 pending 任务需 wake"的轻量检查,替代 Consumer 调 `schedule_game_creator_agent_ready_tasks` / `wake_pending`。 -- 吸收 `schedule_game_creator_agent_ready_tasks`:manifest ready 任务由 Runner 扫描发现并调度,删除前端 devMode 按钮(`App.tsx:10550`)。 -- **边界(不在本轮)**:Runner 仍由 GUI 启动(`main.rs:2124`),保留 GUI-owner watchdog(`server.rs:169`)与 `game_chat_release` 退出协议(`main.rs:2311`)。"自驱 = 存活期间自调度 + 启动自恢复",**不含**开机自启/无 GUI 常驻。 -- 对应删除前端恢复触发职责:`App.tsx:2799/10341`、`useDeveloperAgentPanel.ts:684` 的启动时 resume、resume 确认卡回调。 +- 将进程内 `known_roots` 扩展为 AppData 私有 `game-creator-known-roots.v1`。记录稳定 project identity、canonical root、首次/末次登记时间和有效状态,不保存用户输入、Provider 内容或凭据。 +- Unix 父目录/文件权限分别为 `0700/0600`;Windows 使用仅当前用户可访问的等价 ACL。写入使用同目录临时文件、文件同步、原子替换及目录同步(平台支持时);读取校验 schema、普通文件/非链接、owner/ACL、重复 identity 和重复 canonical path。 +- root 每次使用前重新 canonicalize 并重读 manifest identity。目录消失只标记失效;搬迁仅在新的、已授权 locator 注册并能唯一证明同一 projectId 时更新,不主动遍历磁盘寻找项目。identity/path 冲突或目录簿损坏保留证据并进入 reconciliation。 -**验收**: -- Runner 进程内:确认一个 pending 任务后无需任何 Consumer 调用即自动执行;恢复 pending 动作后自动续跑。 -- 确定性 e2e 增加"Runner 独立进程跑完整流程"用例(复用 `agent-runtime-real-e2e/harness/process.mjs` 的二进制编译能力)。 -- `confirm_resume` 恢复确认卡流程改为 `InteractionRequired::PolicyApproval` → `approve`。 +**wake、扫描与 owner**: -### P5 迁移 GUI / CLI / Tests +- Shell 成功提交工作、解决 interaction、写入确定性 timer、lane 释放、manifest ready 或 owner 状态变化后发送按 projectId 去重的有界进程内 wake;队列已满时只合并同 project wake,不阻塞持久提交。durable runnable state 本身是丢 wake 后的恢复依据,wake 不是事实源。 +- Runner 启动时及兜底轮询前,逐 root 完成 canonicalize/identity 校验并取得既有 project execution owner;未取得 owner 时不得扫描该项目 durable task、修改 Runtime 或标记活跃。每个 wake/扫描批次都有时间和工作量预算,同一热项目完成一批后重新排队,不能饿死其它 root。 +- 现有 25ms socket accept loop 继续只处理连接与 heartbeat,不在该线程中扫描目录或执行 Runtime 工作。另建阻塞式调度 worker(或等价专用 Runtime task)消费进程内 wake/队列信号;兜底扫描每个 Runner 最快 30 秒一轮,带抖动、轮转且每轮最多 8 个 root;启动恢复也使用同一有界批次并持续轮转,不能启动瞬间全量扫盘。 +- draining、GUI-owner 丢失或 project owner 释放开始后立即停止新 Runtime 调度;不得把已经受理的 cancel、读回、状态查询和 reconciliation 操作误判为普通新调度。除取消/收束类命令外,尚未写入 prepared 的会触发 Runtime 执行的新命令返回 `OWNER_UNAVAILABLE`;已开始的内部工作按现行 interruption/handoff/reconciliation 合同收束。 -**目标**:三类 Consumer 全部迁移到统一 Intent、Snapshot、Runtime Output。 +**恢复矩阵**: -- GUI: - - 状态渲染改读 `AgentRuntimeSnapshot`;删除 `normalizeAgentRuntimeState` / `mergeAgentRuntimeStateIntoMap` / phase 文案映射。 - - 交互全走 `submit_intent/approve/answer/cancel/resume`;删除 steer/confirm/retry/resume 直接调用与门禁。 - - 保留只读消费形态:response stream 展示、对话合并、画布资产编排(这些不进 Loop 协议)。 -- CLI:`cli.rs` 与 `swarm_cli` 改用 Shell 命令;删除各自状态机(决策已上提)。 -- Tests:`src-tauri/tests/` 迁移到新命令;`agent-runtime-real-e2e` 与确定性 e2e 走同一协议。 -- 迁移顺序:先 CLI(最薄)→ 再 Tests → 最后 GUI(唯一消费 response stream / conversation 合并 / goal CAS / 委派修复路由,工作量最大)。 +| durable 状态 / 条件 | 自动动作 | 禁止动作 | +|---|---|---| +| command `prepared` 且可证明未产生副作用 | 取得 owner 后继续同一 request | 不创建替代 requestId | +| command `executing` 且结果有可信 durable 证据 | 幂等补 command result / Public 投影 | 不重复内部或外部副作用 | +| command `executing` 且外部结果未知 | `outcome-unknown` + `needs-reconciliation` | 不自动重放 | +| Runtime `pending` 且身份可信、owner 已取得 | 可调度一次 | 不跨 owner 重复调度 | +| 等待确定性 timer / lane release | 到期或 wake 后继续 | 未到期不轮询重放 | +| Open user input / tool approval / policy approval | 保持 InteractionRequired,只刷新投影 | 不自动批准或把权限拒绝当恢复失败 | +| interaction `resolving` 且结果可证明 | 幂等补 Resolved 和 command result | 不重新消费 response | +| interaction `resolving` 且外部结果未知 | supersede/reconciliation,保留 response 证据 | 不以第二 response 自动解决 | +| Runtime `executing` 且 Provider/工具/进程结果未知 | `needs-reconciliation` | 不自动重放 request/action slot | +| journal、ledger、interaction 或 run 身份损坏/冲突 | `needs-reconciliation` | 不猜测、不覆盖证据 | +| 业务已完成、revision 尚未分配 | 由 dirty journal 生成唯一下一 revision/eventId | 不回滚业务事实、不跳号 | +| revision 已持久化但事件未闭合 | 幂等补同 revision/eventId | 不生成第二事件或第二终态 | +| project owner 未取得 | 不扫描 durable task、不执行 | 不越权读取后调度 | +| Runner draining / GUI-owner 丢失 | 停止新调度并收束进行中工作;允许只读、结果读回、取消和 reconciliation | 不接受新的自动恢复或新的 Runtime dispatch | +| root 不存在、搬迁或 identity 冲突 | 可证明失效则标记;其余 reconciliation | 不按旧路径执行 | -**验收**:GUI、CLI、Tests 调用面收敛到同一组命令,代码差异只剩输入输出形式。 +**边界**:Runner 仍由 GUI 启动,保留 GUI-owner watchdog 与 `game_chat_release` 退出协议;本轮不实现开机自启或无 GUI 常驻。 -### P6 删除旧公开面 -**目标**:Interaction Contract 成为唯一稳定公开边界。 +自动调度只允许处理以下状态:身份可信、已取得 project execution owner、项目不处于 draining、任务为 `pending` 且其依赖已满足,或确定性 timer/lane 到期且可证明尚未执行;投影补偿只允许重复生成已确定的 Snapshot/event 结果。`waiting for user input`、`waiting for policy approval`、`waiting for developer approval` 永不自动推进;`executing`、Provider/工具/进程结果未知、状态身份冲突和 `needs-reconciliation` 永不自动重放。Runner 的调度 worker 在 owner lease、GUI-owner lease 或 drain 状态任一失效时先停止 dequeue,再决定进行中工作如何收束;不得先取出任务后再补做 owner 检查。 -- 删除旧 Tauri 命令:`start_game_creator_agent_runtime_task` / `start_game_creator_supervisor_runtime_task` / `steer_*` / `confirm_*` / `reject_*` / `retry_*` / `confirm_retry_*` / `resume_game_creator_agent_runtime_tasks` / `confirm_resume_*` / `schedule_game_creator_agent_ready_tasks` / `read_game_creator_agent_runtime(s)`(`main.rs:2188-2208`)。 -- 删除对应后端 wrapper 与前端 `app/types.ts` 旧 DTO、旧事件协议、迁移期兼容逻辑。 -- 现有 start / steer / resume / recovery 能力作为 Shell 内部实现保留(改名/内联)。 +所有自动动作都必须记录可恢复的 wake reason 和 operation identity。wake 只负责唤醒,不能证明任务仍可执行;worker 每次 dequeue 前重新读取 durable state、owner generation 和 project drain 状态,校验通过后才创建或继续同一 operation。兜底扫描发现不满足上述条件的 root 时只记录跳过原因,不改变 Runtime 状态;扫描过程不得为了“发现新项目”而遍历未登记目录。 -**验收**:`grep` 无旧命令名残留;全量回归(P0 基线 + 单元 + e2e)全绿。 +**完成门禁**:目录簿安全合同和恢复矩阵逐行验证;25ms socket loop 零持久目录扫描/Runtime 执行;丢 wake 可由有界兜底恢复;未知结果、owner 冲突、drain 和 GUI-owner 丢失均零自动重放/新调度。 + +### P5 迁移 CLI、Tests、GUI + +**目标**:只迁移 Consumer,不在本阶段新增协议语义。顺序固定为 CLI → 面向公开协议的 Tests → GUI。 + +- 面向用户/自动化的 Supervisor CLI 改为 Public Snapshot + 五命令 + event cursor;现有 `--swarm-chat` 若继续展示完整专业 Agent 状态,必须明确归类为受信任开发 CLI 并走 Developer capability,不能一边读取内部字段一边宣称是正式 Public Consumer。 +- 公开 e2e/fixture 迁移到同一协议;直接调用内部 start/steer/resume 的单元和恢复回归继续保留,不把内部能力误算为 Consumer。 +- GUI 正式 Supervisor 只使用 Public Snapshot;删除 normalize/merge、phase 文案、steer/start、confirm/retry/resume 和启动 schedule-ready 决策。开发窗口显式走 Developer read capability;开发者对当前 Project Supervisor 的写操作仍走同一五命令,专业/child Agent 的直接调试控制属于既有受信任管理面,不伪装成正式 Supervisor 协议。 +- Consumer 只按结构化 code/kind/retryable/interactionRequired 分流;事件去重、缺口和 cursor 过期都只触发完整 Snapshot 读取。 +- 迁移期 fallback 仅处理 transport 明确的 unknown command:该错误证明新 handler 未执行,才可调用旧命令。任何已到达新 handler 的结构化错误或超时都不得 fallback;超时只可同 requestId 重试/读回。 + +**完成门禁**:三类正式 Consumer 的 Runtime 调用面一致,差异只剩输入输出形态;Public/Developer 类型无交叉;迁移前后用户语义和副作用计数等价。 + +### P6 删除旧公开面并最终收口 + +**目标**:Interaction Contract 成为唯一稳定公开 Runtime 控制边界。 + +- 从 Tauri invoke handler 和其它正式 transport 删除旧 start/steer/confirm/reject/answer/cancel/retry/resume/schedule/read 注册、旧公开 DTO、旧事件和 migration fallback。 +- 删除 Consumer 旧调用点与生命周期分支;管理面 goal/compact/session/config 按第 1.3 节边界保留。 +- Shell 内部 start/steer/resume/recovery 函数、Runner 内部方法及验证这些能力的回归测试允许保留或重命名,不设置全仓旧名称为零的伪门禁。 +- 同步 Runtime V1.1、智能体 App 实施计划、文档索引和长期架构记忆,确保本计划不成为与权威 Runtime 并行的冲突事实源。 + +**完成门禁**:定向静态检查证明 invoke handler、正式 transport、Consumer 调用点和公开 DTO 不再引用旧协议;协议契约、进程内、确定性、独立进程恢复、真实 Runner 和前端验证覆盖最终调用面。 --- -## 4. 迁移策略(新旧并存 + 等价性) +## 4. 迁移与兼容原则 -1. **接口即实现,不留空窗**:新命令从注册第一天起就是真实可用——内部套用现有旧函数(adapter 套旧实现是**常态、透明**,前端不知道也不关心)。不存在"接口先立、实现待填"的中间态;分阶段的不是"接口 vs 实现",而是"谁先切到新接口"。 -2. **新旧并存**:P1 起新命令与旧命令同时注册,Consumer 逐个切换,旧路径逐条下线(strangler fig)。 -3. **版本协商 fallback(仅用于迭代空窗期)**:前端调新命令,**仅当**后端报 unknown command(前端版本 ≠ 后端版本,打包错位)时回退旧命令;其他运行错误**原样呈现,不盲目重试**(防重复入队)。后端新版随应用覆盖到位后,fallback 即死代码,P6 删除。 -4. **等价性保障**: - - 确定性 provider + 协议级 trace(P0)作为新旧实现的对照基线。 - - 进程内集成测试每阶段全跑。 - - 关键迁移点(steer/start 判定、resume 路由、needs-reconciliation 重试)用"同一输入 → 新旧实现输出一致"的单测锁定。 +1. **依赖先行,不发布半协议**:P1 只建内部底座,P2 先稳定 read,P3 才同时注册完整五命令;公开 handler 出现时必须可真实执行、读回和恢复。 +2. **新旧并存只发生在 P3–P5**:旧公开命令服务尚未迁移的 Consumer,新协议服务已迁移 Consumer;两者调用同一内部 Runtime 能力并共享 Shell project lock、Interaction 和 projection 同步,只有新协议承诺 request ledger 的安全重试/读回合同。 +3. **fallback 不处理不确定结果**:只有 transport 的 unknown command 可走旧命令;超时、崩溃、结构化错误和结果未知必须沿新 request ledger 收敛。 +4. **等价比较看语义,不冻结旧结构**:比较用户可见状态、目标 run、interaction 结果、终态和副作用唯一性;不要求命令名、事件名、DTO 或中间调用序列一致。 +5. **旧接口删除前先完成调用图证明**:区分正式 Consumer、内部实现、恢复工具和回归测试,P6 只删除公开注册及调用,不误伤 Runtime 内部能力。 --- ## 5. 里程碑与验收门禁 -| 里程碑 | 交付 | 门禁 | +| 里程碑 | 交付 | 必须证明 | |---|---|---| -| M0 | P0 基线 + trace 入库 | 回归三件套全绿 | -| M1 | P1 新命令 + adapter(新旧并存) | 新/旧命令终态一致;现有测试全绿 | -| M2 | P2 Snapshot 投影 + 前端读 Snapshot | 前端删 normalize 后渲染回归一致 | -| M3 | P3 Loop 收归(Intent + InteractionRequired) | CLI 走新协议跑通完整流程;前端无 steer/confirm/retry 直调 | -| M4 | P4 Runner 自驱 | Runner 独立进程自动跑完;resume 确认走统一 approve | -| M5 | P5 全部 Consumer 迁移 | GUI/CLI/Tests 调用面收敛到同一协议 | -| M6 | P6 删旧面 | 无旧命令残留;全量回归绿 | +| M0 | P0 行为基线与协议 fixture | 旧语义可复验;每个新不变量有非伪造测试入口 | +| M1 | P1 持久协议底座 | ledger/锁/权限/崩溃状态机闭合,尚无半成品公开命令 | +| M2 | P2 双 Snapshot 与事件流 | Public 唯一事实、Developer 服务端授权、revision/cursor 可恢复 | +| M3 | P3 五命令与 Interaction Loop | 幂等/冲突/读回/Interaction/策略复核闭合,新旧语义等价 | +| M4 | P4 Runner 自驱 | 目录簿与恢复矩阵闭合,owner/drain/未知结果失败关闭 | +| M5 | P5 Consumer 迁移 | CLI/Tests/GUI 只经统一协议,正式/开发读模型隔离 | +| M6 | P6 旧公开面删除 | 正式注册/调用/DTO 无旧协议,内部能力与回归测试保留 | + +任一阶段只能依赖已完成的前序里程碑;不能以“后续阶段会补”为理由放行当前公开合同缺口。 --- -## 6. 风险与未决问题 +## 6. 关键风险与强制约束 -| 风险/问题 | 影响 | 缓解 | -|---|---|---| -| P2 依赖"后端 read 先稳定",否则前端不敢删 normalize | 阶段顺序敏感 | P2 后端投影先行,前端删逻辑放同一阶段尾 | -| P4 自驱与 GUI-owner 安全模型冲突 | 若误解为"无 GUI 常驻"会引安全评审 | 计划内明确边界,实现不越界 | -| steer/start 判定含 UX 语义(mode/source/runProfile、steerId 生成、acceptedRunId 对账) | 收归 Shell 后前端展示可能退化 | Shell 返回 `{ mode, accepted_run_id, steer_decision }` 补足展示信息 | -| `tool_request` 无现成单一落点 | 需 Shell 派生投影 | 提前排进 P2/P3 投影工作量 | -| 协议级 trace 的随机 ID 归一化 | 基线易碎 | 复用确定性 provider,归一化规则集中一处 | +| 风险 | 强制约束 | +|---|---| +| Snapshot 与 durable state 在文件崩溃窗口不同步 | projection journal + 单一锁序;Public read 先修复,事件永远只作提示 | +| 全项目 revision 导致无关更新误杀交互 | 回答 CAS 使用 interactionRevision;精确 target 命令锁内重读 durable identity | +| 重试先做当前状态校验而失去首次结果 | 固定先查 request ledger,再做状态/策略校验;成功和业务拒绝都持久化 | +| responseId 换 requestId 造成二次消费 | interaction resolution 同时保存 responseId/fingerprint,独立于 command requestId 去重 | +| command/interaction executing 的外部结果未知 | outcome-unknown / reconciliation;禁止自动重放或换 ID | +| Tauri 前端 devMode 被当作权限 | Developer read 只认服务端构建、窗口标签或显式受信任 capability | +| Public interaction 正文可能泄漏私有问题、工具计划或路径 | interaction audience + kind 白名单 + 公共内容安全过滤;不安全内容降为 Developer/reconciliation,原始信息仅私有 sidecar/开发 capability | +| 目录簿泄漏本地路径或扫描失控 | 私有权限、仅 AppData 持久化、公开零路径、wake 优先和有界低频轮转 | +| P3/P5 重复迁移导致阶段不可独立审查 | P3 只完成后端协议与 harness;P5 只切换 Consumer 和删 Consumer 决策 | +| 旧名称静态检查误删内部能力 | P6 只检查正式 transport、Consumer 和公开 DTO | + +--- ## 7. 建议实施顺序(一句话) -P0 建安全网 → P1+P2(adapter + Snapshot,纯增量、旧接口全留、fallback 兜底)→ P3 收 Loop → P5 迁移(先 CLI 后 GUI)→ P6 删旧面;P4(Runner 自驱)作为主线中心件贯穿 M4,不单独立项,但边界(存活期间自调度,不含无 GUI 常驻)在计划内写死。 +P0 建语义基线与契约 fixture → P1 建身份/权限/ledger/锁底座 → P2 建 Public/Developer Snapshot 与项目事件流 → P3 一次性开放完整五命令并收归 Interaction Loop → P4 按恢复矩阵实现 Runner 自驱 → P5 按 CLI、Tests、GUI 迁移 → P6 定向删除旧公开面。 --- -## 8. 相对原方案的调整点 +## 8. 协议冻结输出 -本计划在原方案基础上做了以下调整,均基于 master 现状与既有安全模型: +本方案提交后,以下内容视为 V1 编码前的冻结合同,不在实现 PR 中由 Consumer 或 transport 自行解释: -1. **P0 基线**:原方案的 Golden Replay 改为"确定性 e2e + 协议级事件 trace"——在既有确定性 provider e2e(`deterministic-lane-defense-provider.mjs`)上扩展,录制协议级事件序列并归一化 timestamp 与随机 ID,不重建基线体系。 +- 身份:manifest `projectId` 不可变;`sessionId` 来自会话管理面;`runId` 由 Shell 在 prepared 阶段预分配;所有 locator、owner generation 和内部 operation identity 不进入 Public 协议。 +- 所有权:project execution owner、Runner owner、GUI-owner 分层;owner generation 是 fencing 权威;未取得 owner、draining 或 GUI-owner 失效时不扫描、不执行、不接受自动恢复。 +- 投影:先 durable 业务事实,再按 dirty journal 补 Public projection;已提交事实但投影未刷新可幂等补偿,事实提交结果未知必须 reconciliation。 +- 事件:项目级 at-least-once `SnapshotChanged`;`snapshotRevision`、`sequence`、`eventId`、opaque `cursor` 的持久关系不可改变;事件只通知,Consumer 只能重读完整 Snapshot。 +- 写入:五命令统一 request ledger;相同 requestId/指纹回放原结果,异指纹冲突;结果未知不换 ID 重放;结果读回只按 `(projectId, requestId)` 查询。 +- 交互:`interactionId + interactionRevision + responseId` 独立于全项目 revision;Shell 在锁内重读交互和策略;项目级 PolicyApproval 不要求绑定单一 run。 +- 公开面:Public 字段白名单和稳定枚举是唯一正式 Supervisor 展示合同;Developer Snapshot 必须经过服务端 capability;`tool_request` 不进入 Public。 -2. **P4 定位与边界**:原方案将"Supervisor Lifecycle Coordinator"列为独立阶段;现作为主线组成部分(里程碑 M4,不单独立项)。自驱限于"Runner 存活期间自调度 + 启动自恢复",排除"开机自启 / 无 GUI 常驻",与既有 GUI-owner 安全模型一致。 +任何实现若无法满足上述合同,必须先修改本技术方案并重新评审,不得通过新增 Consumer fallback、缓存或隐式状态字段绕过。 -3. **术语收敛**:原方案"Interaction Contract / Intent / Snapshot"统一为本计划"交互 Loop 协议"(5 入向命令 + 7 出向事件)与"投影"(读模型),含义不变。 +--- -4. **迁移原则显式化**:接口即实现(adapter 套旧逻辑为常态);fallback 仅在版本空窗期、只认 unknown command。原方案未明确此点。 +## 9. 本轮设计修订结论 + +1. Snapshot 是唯一公开 Runtime 状态事实;事件收窄为项目级 `SnapshotChanged`,不再公开可被误合并的 run/interaction/tool 状态载荷。 +2. 项目 manifest `projectId` 是协议身份,`projectPath` 只是每次都要 canonicalize 和复核的私有 locator。 +3. Public Snapshot 只含当前 Project Supervisor 紧凑摘要与协作数量;Developer Snapshot 使用独立 DTO 和服务端 capability,前端 devMode 不算授权。 +4. 五命令不再统一滥用全项目 Snapshot revision:interaction 回答使用独立 interactionRevision,cancel 使用精确 run target,其余命令锁内校验 durable state。 +5. `approve` 显式携带 approve/reject decision;requestId 负责命令幂等受理/结果读回,responseId 负责 interaction response 去重,两层身份不可互相替代,未知外部结果不虚假承诺 exactly-once。 +6. request ledger 明确“身份/权限 → 指纹 → 先查 ledger → 再校验状态 → 写 prepared → 执行 → 权威结果”的顺序,并持久化成功和业务拒绝;外部结果未知统一 reconciliation。 +7. P1 改为内部持久协议底座,P2 先提供唯一 read model,P3 才注册完整可用的五命令;P3 不迁移 Consumer,P5 不再重复设计后端 Loop。 +8. Runner 自驱补齐跨平台目录权限、丢 wake 恢复、启动有界轮转、interaction resolving 和 command executing 恢复矩阵。 +9. P6 只删除正式 transport、Consumer 和公开 DTO 的旧协议引用,内部 start/steer/resume/recovery 能力和回归测试保留。