From 95d606886069854c981c58b6185613624e8583ff Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 21 Aug 2026 09:10:16 +0000 Subject: [PATCH] =?UTF-8?q?=E5=8F=96=E8=AF=81=E8=A6=86=E7=9B=96=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=85=88=E6=8A=98=E5=8F=A0=E9=87=8D=E5=A4=8D=E5=88=86?= =?UTF-8?q?=E9=A1=B5=EF=BC=8C=E5=B9=B6=E8=AE=A9=20Fast=20GDD=20=E5=B0=BD?= =?UTF-8?q?=E9=87=8F=E4=B8=80=E9=A1=B5=E8=AF=BB=E5=AE=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run 21 的 GDD 是 133 行,而 file.read 的默认 maxLines 是 120,于是被迫分成 1-120 / 121-133 两页。模型把第二页读了两遍,把三个 actionId 全提交上去, validate_fast_gdd_file_read_coverage 排序后严格走 `start_line == next_line`: 1-120 → next_line = 121 ✓ 121-133 → next_line = 134 ✓ 121-133 → 121 != 134 ✗「必须从第 1 行无缺口、无重叠地覆盖到文件末尾」 连吃三次 agent.acceptance_update 拒绝,第四次才猜对该交哪两个 id。 同一页读两遍不削弱证据,不该判成重叠。覆盖检查前先按完全相同的 (startLine, endLine, contentSha256) 折叠——内容 SHA 在上一步已经要求全体一致, 折叠掉的确实是同一页的重复回执。真缺口与部分重叠照旧拒绝,有用例钉住。 顺带把取证指令和 playbook 第 5 步改成「每次都传 maxLines=240(上限),尽量一页 读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段」,让典型 GDD 根本 不进分页逻辑。 另记一笔本次排查暴露的可观测性缺口(未修):agent.acceptance_update 的拒绝理由只 回灌给模型,durable receipt 是 detailUnavailable=true / safeDetail=null,事后在 日志和 agent.db 里都查不到,这类拒绝无法复盘。 Co-Authored-By: Claude Opus 5 --- .../runtime/plan/supervisor-playbook.md | 2 +- .../provider_request_builders.rs | 2 +- .../runtime_protocol/acceptance_graph.rs | 57 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md index d0fae2074..9bda4866b 100644 --- a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md +++ b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md @@ -4,7 +4,7 @@ 2. 冻结后立即用一次 `agent.delegate` 把任务委派给 `project-planning`,`expectedArtifacts` 写 `game/fast_gdd.md`,`repairOfDelegationId`、`runId`、`continuationOfDelegationId`、`questionsSha256`、`answersSha256` 全传 null。已有委派尚未收束时不要重复委派。 3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。 4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery,并提交 observation 给出的 `questionsSha256`、`answersSha256`。 -5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行分页读到 `game/fast_gdd.md` 末尾取证。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。 +5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。 6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。 【转达的规则】 diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs index 3e832ddf8..2444cdbe2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs @@ -392,7 +392,7 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( ) } else if plan_root { format!( - "{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。" + "{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,每次都用 maxLines=240(上限)以尽量一页读完、少分页,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。" ) } else if root_control_authority { format!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs index a0f68a94c..1e8f47450 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs @@ -349,6 +349,17 @@ fn validate_fast_gdd_file_read_coverage( return Err("Fast GDD file.read evidence 不属于同一 Markdown 内容".to_string()); } coverage.sort_by_key(|item| (item.start_line, item.end_line)); + // 同一页被读了两遍不削弱证据,却会让下面那趟严格的 `start_line == next_line` + // walk 判成重叠:实测一次 133 行的 GDD 分成 1-120 / 121-133 两页,模型把第二页 + // 读了两次、把三个 actionId 全提交上来,于是连吃三次「必须从第 1 行无缺口、无 + // 重叠地覆盖到文件末尾」,试到第四次才猜对该交哪两个。这里先按完全相同的 + // (startLine, endLine, contentSha256) 折叠——内容 SHA 在上面已经要求全体一致, + // 所以折叠掉的确实是同一页的重复回执,不是两段不同内容。 + coverage.dedup_by(|left, right| { + left.start_line == right.start_line + && left.end_line == right.end_line + && left.content_sha256 == right.content_sha256 + }); if total_lines == 0 { if coverage.len() != 1 || coverage[0].start_line != 0 || coverage[0].end_line != 0 { return Err("空 Fast GDD 的 file.read evidence 覆盖不唯一".to_string()); @@ -1218,6 +1229,52 @@ pub(crate) fn render_game_creator_agent_runtime_acceptance_graph_for_prompt_at( mod tests { use super::*; + fn page(start: usize, end: usize, total: usize) -> AcceptanceEvidenceReceipt { + AcceptanceEvidenceReceipt { + tool: "file.read".to_string(), + project_revision_before: 1, + project_revision_after: 1, + input_summary: Some(format!( + "path=game/fast_gdd.md · startLine={start} · maxLines=240" + )), + safe_detail: Some( + serde_json::json!({ + "path": "game/fast_gdd.md", + "contentSha256": "b".repeat(64), + "lines": format!("{start}-{end} of {total}"), + }) + .to_string(), + ), + } + } + + /// 同一页被读了两遍不削弱证据。实测 133 行的 GDD 被默认 maxLines=120 逼成两页, + /// 模型把第二页读了两次并提交了三个 actionId,于是连吃三次「无缺口、无重叠」。 + #[test] + fn repeating_an_identical_page_is_not_an_overlap() { + let first = page(1, 120, 133); + let second = page(121, 133, 133); + let duplicate = page(121, 133, 133); + let receipts = vec![&first, &second, &duplicate]; + let (sha, total) = + validate_fast_gdd_file_read_coverage(&receipts).expect("duplicate page is tolerated"); + assert_eq!(sha, "b".repeat(64)); + assert_eq!(total, 133); + } + + /// 真的缺口与真的重叠照旧拒绝——去重只折叠完全相同的分页。 + #[test] + fn real_gaps_and_partial_overlaps_are_still_rejected() { + let first = page(1, 120, 133); + let gapped = page(122, 133, 133); + let receipts = vec![&first, &gapped]; + validate_fast_gdd_file_read_coverage(&receipts).expect_err("a real gap must fail"); + + let overlapping = page(100, 133, 133); + let receipts = vec![&first, &overlapping]; + validate_fast_gdd_file_read_coverage(&receipts).expect_err("a partial overlap must fail"); + } + fn root_fixture() -> (tempfile::TempDir, PathBuf, AgentRuntimeRunProfileBinding) { let temporary = crate::tests::canonical_test_tempdir("acceptance-graph-"); let root = temporary.path().join("project");