文档:DirectProject 命令接单化定稿
- 重写 ADR:逻辑回合归 Thread Manager、接单/拒单判据改成发生位置、拒单载荷复用 typed 错误、userItemId 由 clientTurnId 推导、提示与用户消息同级并删除详情、队列与埋点改挂回合完成、失败原因本轮不落历史 - CONTEXT.md 新增「逻辑回合」「接单」「拒单」「在途回合」四个术语 - 对话历史单一事实源 ADR 加注:两条已知边界已由新 ADR 重新决策 - Codex 原始历史技术方案加注:三句结论待实施时按新 ADR 修订 - docs/README.md 索引补上该 ADR
This commit is contained in:
+16
@@ -190,6 +190,22 @@ _Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
||||
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
|
||||
_Avoid_: 进度通知、快照轮询、第二套历史
|
||||
|
||||
**逻辑回合**:
|
||||
Thread Manager 拥有的一对回合边界(开始与结束),由接单动作开启、由这一轮的占用对象写出,不镜像 Codex 原生回合;界面忙碌态与回合结果只认它。
|
||||
_Avoid_: Codex 原生回合、原生日志、进程生命周期
|
||||
|
||||
**接单**:
|
||||
把一条用户消息交给宿主开始执行的动作,成立即表示这一轮已经存在;此后结果只由运行态事件回答。
|
||||
_Avoid_: 发送成功、命令调用、接口返回
|
||||
|
||||
**拒单**:
|
||||
接单成立之前拒绝这次请求(并发、权限、目录、参数、工程准备未就绪),只回一条可展示原因,不产生回合事件,也不写用户条目。
|
||||
_Avoid_: 回合失败、执行失败、失败事件
|
||||
|
||||
**在途回合**:
|
||||
界面本地已经把这条用户消息发出去、宿主还没有对应回合开始事件的那一小段状态。
|
||||
_Avoid_: 运行中回合、乐观锁、发送队列
|
||||
|
||||
**聊天投影**:
|
||||
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
|
||||
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
- [DirectProject 对话历史单一事实源](./adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md):AGC 项目开发对话只以项目对话历史与运行态事件为真相源,聊天投影不落盘。
|
||||
- [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。
|
||||
- [引用候选由宿主注入](./adr/【ADR】引用候选由宿主注入-2026-09-22.md):引用输入区只接受宿主注入的引用 provider,素材选择面板独立成组件,附件芯片成为本轮附件唯一事实源。
|
||||
- [DirectProject 命令接单化](./adr/【ADR】DirectProject命令接单化-2026-09-23.md):命令只负责接单、事件流回答整轮结果;尚为草案,未实施。
|
||||
- [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
|
||||
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
|
||||
- [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# 【ADR】DirectProject命令接单化
|
||||
|
||||
状态:提议(草案,设计已定稿、未实施)
|
||||
|
||||
> 本文与 `docs/README.md` 里对应的一行索引先随文档提交进仓库;状态保持「提议(草案,未实施)」,
|
||||
> 实施提交落地后改为「已接受」。
|
||||
|
||||
## 背景
|
||||
|
||||
`chat_with_game_creator_direct_codex` 现在从校验一路 await 到交付验证结束,一个命令调用覆盖整轮。
|
||||
于是命令边界承担了两件不属于它的事:
|
||||
|
||||
1. **回合失败的可见文案有两条来源。** 事件载荷 `turn.completed.failure.message` 是聊天里那条失败说明的
|
||||
来源,命令 Err 是横幅与 `详情:` 引用的来源。两者各有分工,但都由"这一轮结束"这个时刻触发,
|
||||
前端 `runTurn` 的 catch 因此同时兼职"接单被拒"与"回合失败"两种回执。
|
||||
2. **认证失败重试只能挂在这条 Err 上。** `withDirectCodexSessionRefresh` 在登录态失效后刷新会话并
|
||||
**重跑整个 operation**。重跑会再写一条用户消息:单飞锁随命令返回就已经释放,所以这条重跑路径今天
|
||||
会往历史里写第二条一样的用户消息。
|
||||
|
||||
还有一个先天的洞:回合边界今天**镜像 Codex 原生回合**——开始事件只在 `turn/start` 成功应答之后才进队列
|
||||
(`direct_runtime/user_input.rs:81` 之后要一路走到 app-server),于是"接单到 `turn/start` 之间"的失败
|
||||
(连不上 app-server、配置未就绪、历史注入失败、`turn/start` 被拒)没有任何事件可以解释,只能靠命令 Err。
|
||||
命令一旦不再 await,这些路径就会静默。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 命令 = 接单 / 拒单
|
||||
|
||||
命令只做:`clientTurnId` 校验 → 占用调用身份(并发拒单)→ 工作流恢复 → 用户条目校验 → 工程准备
|
||||
→ 接单成立 → 用户条目落盘 → 起 codex。成功后立刻返回,不在命令里等回合。
|
||||
|
||||
这里的"占用调用身份"只挡并发(早于工程准备,避免两个请求同时做准备),与 §2 的"登记逻辑回合占用"
|
||||
不是同一件事:后者拥有这一轮的终态出口。
|
||||
|
||||
**接单成立之前的任何失败都是拒单**:不产生回合事件、不写用户条目、不写失败诊断。
|
||||
|
||||
### 2. 逻辑回合由 Thread Manager 拥有
|
||||
|
||||
- 接单动作在 Thread Manager 内**原子地**完成"拒绝并发 / 登记占用 / 发出逻辑回合开始事件"。
|
||||
- 这条生命周期**不是** Codex 原生回合的镜像:发点在接单时,不在 `turn/start` 应答后;Codex 原生回合事件
|
||||
留在适配器内部,不再进事件队列。线上仍然只有**一对** `turn.started` / `turn.completed`。
|
||||
- 这一轮的**占用对象是唯一终态出口**,并且幂等:正常 / 失败 / 中断 / 取消 / 连接断开谁先到谁写;任务
|
||||
panic 或被取消时由它兜底补一条终态(保留 `host-dropped` 分类,只给"说不出原因"的这一种)。终态写出后
|
||||
占用才释放。
|
||||
- 因此"接单成功 ⇔ 事件流里有开始且有结束"是结构性成立,不依赖实现者记得给每条早退路径补事件。
|
||||
|
||||
### 3. 通道判据从"错误种类"改成"发生位置"
|
||||
|
||||
- **接单前发生的 = 拒单**:目录、权限、输入、并发、工程准备未就绪、宿主状态取不到。
|
||||
- **接单后发生的 = 回合失败**:连接、配置、历史注入、`turn/start` 被拒,以及回合过程中的一切。
|
||||
- `DirectTurnError::EnvironmentNotReady` 作为公共错误保留,接单前后都可能出现;它需要自己的失败分类
|
||||
(`environment-not-ready`),否则回合失败投影会把它写成 `model-failed`,界面语气就错了。
|
||||
|
||||
### 4. 拒单载荷 = 现有 typed 错误
|
||||
|
||||
命令返回类型改成结构化的 `DirectTurnError`(ts-rs 导出到 `chat/generated/`,与 `DirectThreadEvent` 同一套
|
||||
`cargo test export_bindings` 流程),并随载荷带一条由 `Display` 生成的用户文案(文案仍只在一处生成)。
|
||||
前端按变体分流:
|
||||
|
||||
- 认得的"前置条件不满足 / 用户参数无效"→ 与用户消息同级的提示,不上报;
|
||||
- 认不出的变体或非结构化错误 → 抛出,走既有捕获上报链路。
|
||||
|
||||
### 5. 回合身份由 `clientTurnId` 推导
|
||||
|
||||
`turn.started` / `turn.completed` 的 `userItemId` 由 `clientTurnId` 按现有规则算出
|
||||
(`direct-codex:{clientTurnId}:user`,与前端 `directCodexConversationMessageId` 同规则),**不读盘回填**:
|
||||
开始事件发生在用户条目落盘之前,落盘本身也可能失败。
|
||||
|
||||
### 6. 诊断留痕与错误上报都在宿主侧
|
||||
|
||||
`.agent/runtime/errors` + 应用日志 + 错误上报池由宿主投影写出;回合失败进池的责任从前端 catch 移到宿主。
|
||||
命令边界不再负责回合失败的文本。
|
||||
|
||||
### 7. 界面:同级提示,删除 `详情:`
|
||||
|
||||
- 失败说明与接单被拒提示都与用户消息**同级**,按事件顺序排在它后面,不嵌在这条用户消息里。
|
||||
- 删除 `详情:`:用户可见文案里不再出现该引用,前端删除解析与对应的第二次 IPC。
|
||||
- 顶部状态行只显示回合状态,不再承载错误文本。
|
||||
|
||||
### 8. 队列与埋点
|
||||
|
||||
- 前端发送队列的放行改为监听"回合完成"(收到终态事件,或接单被拒),不再由命令返回驱动。加 TODO:
|
||||
以后这条队列挪到 Rust 端,落点就是 Thread Manager 的接单动作。
|
||||
- 埋点结算挂在"回合完成";不能在接单返回时结算——成绩是回合末才入 `pending_runs` 的,提前结算会变成空操作。
|
||||
- 首页"运行中的项目"快照由 Thread Manager 的逻辑回合导出,任务侧不再单独维护一张表。
|
||||
|
||||
### 9. 认证失败不再重跑整轮
|
||||
|
||||
删掉 `withDirectCodexSessionRefresh` 的"刷新 + 重跑整轮";登录态失效按普通回合失败呈现。
|
||||
`cancel_direct_codex_turn` 用的是同一个包装,一并去掉。
|
||||
|
||||
### 10. 回合失败原因本轮不落历史
|
||||
|
||||
失败原因只走事件载荷与宿主诊断,不写进 `project.jsonl`——重进项目只会看到那条没有回复的用户消息。
|
||||
加 TODO:以后要做"进历史但不喂模型"的失败条目(暂定做法见「备选方案」第 3 条)。
|
||||
|
||||
## 影响与代价
|
||||
|
||||
- 命令返回后不再有 Err 兜底:回合一侧只剩事件流,宿主的占用对象必须真的兜住所有路径。
|
||||
- **落盘即接单**:接单成功但回合失败时,历史里会留下一条没有回复的用户消息,而且失败原因不在历史里
|
||||
(只在当轮界面与诊断文件里)。
|
||||
- 前端可以删掉的东西:`markTurnStopped()`(取消成功但事件未到时手动放掉忙碌态)、`turn.started` 的
|
||||
"重复开始保留第一次起点"分支、`详情:` 正则与 `read_agent_runtime_error_detail` 调用。
|
||||
- `kill -9` 的自愈变好:Thread Manager 随进程消失,新进程的订阅 bootstrap 不会出现"有开始没结束",
|
||||
界面不会卡在忙碌态。
|
||||
- 必须同步的注释:`chat/controller/useDirectProjectChatController.ts`(catch 的职责)、
|
||||
`chat/conversation/directTurnPresentation.ts`("`invoke` 直到整轮结束才返回"这句会变成错的)。
|
||||
- CLI 保持 await(它要那段回复文本),两个入口的分工在命令模块里写清楚。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **保留"刷新 + 重跑整轮"**:省掉用户重新登录,但重跑会重复落盘用户消息(现状即有),且重试语义与
|
||||
"命令在飞"绑死。已作废。
|
||||
2. **让 Codex 原生回合事件继续进队列**:等于线上有两对生命周期,接单后的前置失败仍然只能靠人工补事件。
|
||||
已作废。
|
||||
3. **"可见但不喂模型"的条目**:(a) 按条目 id 前缀在注入侧过滤;(b) 条目上挂显式标记(如 `agcLocal`);
|
||||
(c) 新增一种行结构。注意 `project.jsonl` 是项目主对话与 DirectProject **共用**的文件,信封类型两侧共用,
|
||||
改新行结构要连带改共享合同与读取侧(非 `response_item` 行现在是"失败关闭")。本轮不做,TODO 记 (b)
|
||||
为暂定做法。
|
||||
|
||||
## 明确不做
|
||||
|
||||
- 不给失败载荷加字段(不加 `detailRef`):横幅不再展开详情,诊断引用只留在宿主侧。
|
||||
- 不恢复 invoke 拒绝通道,也不为"接单后的前置失败"新增事件类型——它们走同一对逻辑回合事件。
|
||||
- 本轮不做"失败条目进历史但不喂模型"(TODO),不做 Rust 端发送队列(TODO)。
|
||||
|
||||
## 落地时要同步的文档与注释
|
||||
|
||||
- `docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`:事件带 `userItemId` 的事实、
|
||||
失败原因的通道、"`turn.started` 之前的早退不产生终态事件"(作废)、失败说明是否落历史。
|
||||
- `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md`:影响里的两条已知边界被本 ADR 取代。
|
||||
- `docs/project-memory/shared-memory/decision-log.md`:`host-dropped` 的两条口径。
|
||||
- `docs/README.md`:索引行去掉"未实施"。
|
||||
- 代码注释:`direct_runtime/user_input.rs` 的 `TODO(Direct 命令接单化)`、
|
||||
`chat/controller/useDirectProjectChatController.ts` 的 catch TODO、
|
||||
`direct_thread_wire.rs` 里 `userItemId`"由原生从已落盘条目上读取"的说明。
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
状态:已接受
|
||||
|
||||
> 注:本文件「影响」一节里的两条已知边界(① `kill -9` 后前端停在运行态;② `turn.started` 之前的早退
|
||||
> 不产生终态事件)已由 [`【ADR】DirectProject命令接单化-2026-09-23`](./【ADR】DirectProject命令接单化-2026-09-23.md)
|
||||
> 重新决策(草案,未实施);实施后这两条按新 ADR 收口,本文件不再作为它们的依据。
|
||||
|
||||
## 背景
|
||||
|
||||
AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(实时)、`turn-stream.jsonl`(文本段与工具交替顺序)、`tool-calls.jsonl`(已脱敏工具卡片),重进页面时还要额外接管活动回合快照。同一段文本和同一张工具卡片因此存在多个来源,实时与回读会互相覆盖,恢复路径也只能靠"哪个源先到"决定。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
更新时间:`2026-09-16`
|
||||
|
||||
> 注:本文件里"事件不带回合身份"、"`turn.started` 之前的早退不产生终态事件"、"失败说明不写进
|
||||
> `project.jsonl`"等结论,已由 [`【ADR】DirectProject命令接单化-2026-09-23`](../adr/【ADR】DirectProject命令接单化-2026-09-23.md)
|
||||
> (草案,未实施)重新决策;实施时需按该 ADR 的"落地时要同步的文档"一节逐句修订本文件。
|
||||
|
||||
## 目标
|
||||
|
||||
DirectProject 只使用 `.agent/conversations/project.jsonl` 作为对话历史。历史保存 Codex Responses API 的完整 item,使聊天展示与新线程恢复使用同一份事实来源;两者只是不同读取动作。
|
||||
|
||||
Reference in New Issue
Block a user