From b27ffce9b4e585dad03f29e21f250674adb0c7e2 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 26 Aug 2026 08:42:42 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E5=AE=A1=E6=89=B9=E5=8D=A1=E7=9A=84?= =?UTF-8?q?=E4=BF=AE=E6=94=B9/=E9=80=80=E5=9B=9E=E6=84=8F=E8=A7=81?= =?UTF-8?q?=E5=8E=9F=E6=96=87=E9=80=81=E8=BF=9B=20Supervisor=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户在审批卡上写的意见此前只落到 `.agent/planning/approvals/v{n}.json` 就断了。 Supervisor 唯一的信息源是 `agent.run_status` 返回的 claimedDelegateContract, 那里面只有 contractStatus=user-revision-requested,没有承载原文的字段。于是它 按 playbook 第 6 条「把用户原话完整附在 task 里」写了句占位「请按审批卡上用户 提交的修改意见…」,子 Agent 收到的是空指令,只能自由发挥——实测用户写「把游戏 名称改成日本语」,产出的 v2 把标题从《裂隙脉冲》改成《裂潮航印》,仍是中文。 通道本来就有:决策卡的答案早就是这么送的——`append_user_input_answer_message` 把用户选择渲成一句 role=user 消息追加到 Supervisor 会话,下一轮 prompt 由 `prompt_history_sources` 现读现取。审批决定接上同一条通道即可。 因此不动 delivery schema,也不动 playbook: - schema 加字段要连带改 validator、run_status 投影,还得让 e43133875 的 `static_delegate_structured_result_follows_claim_snapshot` 跟着 rebase 新字段, 否则 claim 重放冲突原样复发——多一处必须手工同步的地方。 - playbook 第 6 条的规则本来就在,缺的是原文本身,不是规则。 落点用 Supervisor 的**当前活动会话**(sessionId 传 None),不用 delivery 上的 parentSessionId——那是委派发出时的快照,不保证仍是可写的活动会话。 messageId 用 responseId 派生:`project_receipt_locked` 会被 `reconcile_plan_gdd_approval_projections_locked` 在每次 hydrate 重跑,不幂等就 每刷新一次多一条。 守门一条,两个断言都验过非空转:去掉追加,第一条断言红;换成非幂等 append, 重放后消息数 2 vs 1,第二条断言红。 Co-Authored-By: Claude Opus 5 --- .../runtime_protocol/planning_approval.rs | 40 +++++++++++++++++ .../agent/runtime_protocol/planning_submit.rs | 45 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs index 27c300f3d..564f7a6c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs @@ -74,6 +74,42 @@ fn approval_observation(receipt: &PlanGddApprovalV1) -> AgentRuntimeToolObservat } } +/// 审批卡上的「修改/退回」是用户说的话,落点和决策卡的答案一样:Supervisor 自己 +/// 的会话文件。`append_user_input_answer_message` 已经为澄清答案建立了这条通道, +/// 审批决定沿用它。没有这一步 Supervisor 只能从 delivery 的 +/// `contractStatus=user-revision-requested` 知道「用户要改」,读不到要改什么—— +/// playbook 第 6 条的「把用户原话完整附在 task 里」就没有原话可附,返工委派只能 +/// 写一句占位,子 Agent 于是自由发挥。 +fn append_plan_gdd_revision_message( + root: &Path, + receipt: &PlanGddApprovalV1, +) -> Result<(), String> { + let label = if receipt.action == "reject" { + "退回" + } else { + "修改" + }; + append_local_conversation_message_for_session_idempotent_at( + root, + Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID), + // 落在 Supervisor 当前活动会话。delivery 上的 parentSessionId 是委派发出时 + // 的快照,不保证仍是可写的活动会话。 + None, + LocalConversationMessage { + role: "user".to_string(), + content: format!( + "我对 Fast GDD v{} 的审批:{}。意见原文:\n{}", + receipt.version, + label, + receipt.comment.as_deref().unwrap_or_default() + ), + agent_id: None, + }, + &format!("plan-gdd-decision-{}", receipt.response_id), + ) + .map(|_| ()) +} + fn receipt_decision_input( gdd: &PlanGddV1, input: &DecidePlanGddInputV1, @@ -1149,6 +1185,10 @@ fn project_receipt_locked( note_plan_gdd_projection_gap(root, receipt, "delivery-revision-mark", &error); recovery_pending = true; } + if let Err(error) = append_plan_gdd_revision_message(root, receipt) { + note_plan_gdd_projection_gap(root, receipt, "delivery-revision-message", &error); + recovery_pending = true; + } } // A replay may target an older receipt after a newer GDD has already been // submitted. The receipt still repairs its own audit/observation, but it diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs index b63d5953f..f5dee5b6d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs @@ -4192,6 +4192,51 @@ mod tests { cleanup_fixture(root); } + /// 审批卡上的「修改/退回」意见必须原文落进 Supervisor 会话——那是 playbook + /// 第 6 条「把用户原话完整附在 task 里」唯一的原话来源。delivery 的 + /// `contractStatus=user-revision-requested` 只说明「用户要改」,不带内容, + /// Supervisor 拿不到原文就只能在返工委派里写一句占位。 + /// + /// 追加必须幂等:`reconcile_plan_gdd_approval_projections_locked` 每次 hydrate + /// 都会为全部回执重跑 `project_receipt_locked`。 + #[test] + fn a_revision_comment_reaches_the_supervisor_conversation_once() { + let (root, gdd, _root_runtime) = acceptance_gate_fixture(true); + create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending"); + decide_plan_gdd_at( + &root, + &approval_input( + &gdd, + "revise", + "gdd-response-00000000-0000-4000-8000-000000000050", + Some("把游戏名称改成日本语".to_string()), + ), + ) + .expect("commit revise receipt"); + let supervisor_messages = || { + read_local_conversation_for_session_at( + &root, + Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID), + None, + ) + .expect("read supervisor conversation") + .messages + }; + let after_decision = supervisor_messages(); + assert!( + after_decision.iter().any(|message| message.role == "user" + && message.content.contains("把游戏名称改成日本语")), + "用户修改意见必须原文进入 Supervisor 会话" + ); + reconcile_plan_gdd_approval_projections_locked(&root).expect("replay receipt projections"); + assert_eq!( + supervisor_messages().len(), + after_decision.len(), + "投影重放不得重复追加同一条审批意见" + ); + cleanup_fixture(root); + } + /// reject 之后能不能在同一 lineage 重做,**不由提交门的 phase 判据决定**。 /// /// 提交门要求 session 的 activeRunId 等于当前策划子 run,而 schema 不变量禁止