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 9bda4866b..529d7c2bc 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` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `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 末尾都带着 `sourceAgentId` / `sourceRunId` / `sourceActionId` 三个字段,把它们原样抄成 evidence 的 `{agentId, runId, actionId}`,用一次 `agent.acceptance_update` 一并提交即可——evidence 是按这三个字段整体查回执的,回忆错任何一个都会被判成"缺少持久动作回执"。不要为了取这些字段再去查动作历史。取证完成前审批卡不会出现。 6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。 【转达的规则】 diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs index c85d9c1df..0c5d4467b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs @@ -294,7 +294,12 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ false, || observe_agent_runtime_file(root, &action.input), ); - append_agent_runtime_file_read_source_action_id(&mut observation, action_id); + append_agent_runtime_file_read_evidence_ref( + &mut observation, + agent_id, + run_id, + action_id, + ); observation } "file.write" => observe_agent_runtime_file_write( @@ -534,8 +539,10 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ /// 追加在 detail 首行末尾是安全的:`agent_runtime_action_safe_detail_value` 解析 /// `file.read` 时只取前三个 `·` 字段(path / sha256 / lines),多出来的字段不参与, /// durable receipt 与既有的取证解析都不受影响。 -fn append_agent_runtime_file_read_source_action_id( +fn append_agent_runtime_file_read_evidence_ref( observation: &mut AgentRuntimeToolObservation, + agent_id: &str, + run_id: &str, action_id: Option<&str>, ) { if observation.status != "ok" { @@ -547,13 +554,26 @@ fn append_agent_runtime_file_read_source_action_id( if observation.summary.contains("sourceActionId=") { return; } + let agent_id = agent_id.trim(); + let run_id = run_id.trim(); + if agent_id.is_empty() || run_id.is_empty() { + return; + } // 写进 **summary** 而不是 detail:`file.read` 的 detail 在投影给模型和事件流之前 // 会被换成 durable receipt 的 safe_detail(只有 path / contentSha256 / lines 三个 // 字段),追加在 detail 上的字段到不了模型手里。实测一次 run 里模型照着新指令 // 满世界找 sourceActionId、一次也没看到,只能反复重读全文再猜 actionId,28 次 // file.read、5 次 acceptance_update 才收敛。summary 是原样保留的,也没有任何 // 解析方依赖它的形状。 - observation.summary = format!("{} · sourceActionId={action_id}", observation.summary); + // 给的是**整个三元组**,不是一个碎片。`agent.acceptance_update` 的 evidence 引用 + // 要求 {agentId, runId, actionId} 三个字段,回执查找也按三元组整体做 key。早期 + // 只给 actionId,另外两个靠模型回忆——实测它第一次就把其中一个记错,白吃一次 + // 拒绝。反正 `validate_fast_gdd_evidence_identity` 只接受当前根 run 的回执, + // 合法取值唯一,本来就不该让它猜。 + observation.summary = format!( + "{} · sourceAgentId={agent_id} · sourceRunId={run_id} · sourceActionId={action_id}", + observation.summary + ); } pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock( @@ -929,15 +949,17 @@ mod file_read_source_action_id_tests { #[test] fn appending_the_source_action_id_keeps_the_receipt_safe_detail_parseable() { let mut observation = file_read_observation(); - append_agent_runtime_file_read_source_action_id( + append_agent_runtime_file_read_evidence_ref( &mut observation, + "project-supervisor", + "run-1", Some("action-0123456789abcdef01234567"), ); assert!( - observation - .summary - .ends_with("· sourceActionId=action-0123456789abcdef01234567"), - "actionId 必须写在 summary 上——detail 会被换成 safe_detail,到不了模型手里" + observation.summary.ends_with( + "· sourceAgentId=project-supervisor · sourceRunId=run-1 · sourceActionId=action-0123456789abcdef01234567" + ), + "evidence 三元组必须整份写在 summary 上——detail 会被换成 safe_detail,到不了模型手里;只给 actionId 则另外两个字段要靠模型回忆,实测会记错", ); let detail = observation.detail.clone().expect("detail"); assert!(!detail.contains("sourceActionId="), "detail 保持原样"); @@ -959,18 +981,38 @@ mod file_read_source_action_id_tests { let mut failed = file_read_observation(); failed.status = "failed".to_string(); let before = failed.summary.clone(); - append_agent_runtime_file_read_source_action_id(&mut failed, Some("action-1")); + append_agent_runtime_file_read_evidence_ref( + &mut failed, + "project-supervisor", + "run-1", + Some("action-1"), + ); assert_eq!(failed.summary, before); let mut missing = file_read_observation(); let before = missing.summary.clone(); - append_agent_runtime_file_read_source_action_id(&mut missing, None); + append_agent_runtime_file_read_evidence_ref( + &mut missing, + "project-supervisor", + "run-1", + None, + ); assert_eq!(missing.summary, before); let mut twice = file_read_observation(); - append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-1")); + append_agent_runtime_file_read_evidence_ref( + &mut twice, + "project-supervisor", + "run-1", + Some("action-1"), + ); let once = twice.summary.clone(); - append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-2")); + append_agent_runtime_file_read_evidence_ref( + &mut twice, + "project-supervisor", + "run-1", + Some("action-2"), + ); assert_eq!(twice.summary, once, "已经带了 id 就不再追加第二个"); } } 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 2444cdbe2..7fdf1a632 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,每次都用 maxLines=240(上限)以尽量一页读完、少分页,各页必须来自同一内容 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 的 {{agentId, runId, actionId}}——每次 file.read 的 observation 末尾都带着自己的 sourceAgentId / sourceRunId / sourceActionId,三个字段原样照抄,不要自己回忆 agentId 或 runId,也不要为取得它们额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。" ) } else if root_control_authority { format!( @@ -1689,7 +1689,9 @@ mod tests { assert!(later_prompt.contains("Goal Contract 已冻结且不可重写")); assert!(later_prompt.contains("从 startLine=1 开始分页")); assert!(later_prompt.contains("直到 EOF")); - assert!(later_prompt.contains("全部分页 file.read 的 actionId")); + assert!(later_prompt.contains("全部分页 file.read 的 {agentId, runId, actionId}")); + // evidence 是三元组,不是一个 actionId;指令必须点名另外两个字段从哪来。 + assert!(later_prompt.contains("sourceAgentId / sourceRunId / sourceActionId")); assert!(!later_prompt.contains("Goal Contract 首轮协议")); } 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 1e8f47450..62866db3f 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 @@ -198,11 +198,20 @@ fn acceptance_evidence_tools_at<'a>( for identity in identities { let record = receipts .get(&identity) - .ok_or_else(|| format!("Acceptance Graph evidence 缺少持久动作回执:{}", identity.2))?; + // 查找按 (agentId, runId, actionId) 整体做 key,三者任一对不上都会落空。 + // 早期这句只打印 actionId,于是一次「runId 写错」被报成「这个 actionId 没有 + // 回执」——实测把模型带进了「我的读取没落盘」的错误结论,重读全文再猜; + // 排查的人也会先去查时序和落盘顺序。报全三元组才指得对方向。 + .ok_or_else(|| { + format!( + "Acceptance Graph evidence 缺少持久动作回执:agentId={} · runId={} · actionId={}", + identity.0, identity.1, identity.2 + ) + })?; if agent_db_record_text(record, "status") != Some("ok") { return Err(format!( - "Acceptance Graph evidence 动作未成功:{}", - identity.2 + "Acceptance Graph evidence 动作未成功:agentId={} · runId={} · actionId={}", + identity.0, identity.1, identity.2 )); } let tool = agent_db_record_text(record, "tool")