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 e1e59edbf..d0fae2074 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` 末尾取证,用 `agent.action_history` 取回全部分页 actionId,再用一次 `agent.acceptance_update` 把它们完整写进 evidence。取证完成前审批卡不会出现。 +5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行分页读到 `game/fast_gdd.md` 末尾取证。每次 `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/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs index dfbd62a90..5eead20f3 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 @@ -283,16 +283,20 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ false, || observe_agent_runtime_file_list(root, &action.input), ), - "file.read" => observe_agent_runtime_project_snapshot_with_lock( - root, - agent_id, - run_id, - action, - &action_fingerprint, - pending_action, - false, - || observe_agent_runtime_file(root, &action.input), - ), + "file.read" => { + let mut observation = observe_agent_runtime_project_snapshot_with_lock( + root, + agent_id, + run_id, + action, + &action_fingerprint, + pending_action, + false, + || observe_agent_runtime_file(root, &action.input), + ); + append_agent_runtime_file_read_source_action_id(&mut observation, action_id); + observation + } "file.write" => observe_agent_runtime_file_write( root, agent_id, @@ -513,6 +517,47 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ } } +/// 让 `file.read` 的 observation 自带 durable actionId。 +/// +/// §13.0 的审批前置取证要求把每一页 `file.read` 的 actionId 放进 +/// `agent.acceptance_update.evidence`,而 observation 结构里只有 +/// `{tool,status,summary,detail}`,模型没有第二条途径拿到它——只能回头查 +/// `agent.action_history`。实测它会为了一个 actionId 连查四次:拿到 1 条怀疑漏了 +/// 分页,拿到全量又怀疑混进了别的工具,而这些结果自始至终都在它上下文里 +/// (`agent.action_history` 的 observation detail 有 8000 字符的专属额度,不会被裁)。 +/// 加 prompt 约束对这种"不敢信"没用,把取 id 这件事从工具变成事实才有用。 +/// +/// `command.exec` 早就是这么做的:durable observation 直接返回可复用的 +/// `sourceActionId`,合同里明写「不要先猜 actionId 或为取得它额外查询动作历史」。 +/// 这里把同一条路铺给 `file.read`。 +/// +/// 追加在 detail 首行末尾是安全的:`agent_runtime_action_safe_detail_value` 解析 +/// `file.read` 时只取前三个 `·` 字段(path / sha256 / lines),多出来的字段不参与, +/// durable receipt 与既有的取证解析都不受影响。 +fn append_agent_runtime_file_read_source_action_id( + observation: &mut AgentRuntimeToolObservation, + action_id: Option<&str>, +) { + if observation.status != "ok" { + return; + } + let Some(action_id) = action_id.map(str::trim).filter(|value| !value.is_empty()) else { + return; + }; + let Some(detail) = observation.detail.as_deref() else { + return; + }; + let Some(first) = detail.lines().next() else { + return; + }; + if first.contains("sourceActionId=") { + return; + } + let rest = detail[first.len()..].to_string(); + let first = first.to_string(); + observation.detail = Some(format!("{first} · sourceActionId={action_id}{rest}")); +} + pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock( root: &Path, agent_id: &str, @@ -863,3 +908,69 @@ mod canvas_only_execution_tests { ); } } + +#[cfg(test)] +mod file_read_source_action_id_tests { + use super::*; + + fn file_read_observation() -> AgentRuntimeToolObservation { + AgentRuntimeToolObservation { + tool: "file.read".to_string(), + status: "ok".to_string(), + summary: "已读取 game/fast_gdd.md 第 1-134 行(共 134 行)".to_string(), + detail: Some(format!( + "game/fast_gdd.md · sha256={} · lines 1-134 of 134 +第一行内容", + "a".repeat(64) + )), + } + } + + /// 追加的 sourceActionId 必须能被模型看见,且不能破坏 durable receipt 的 + /// safe_detail 解析——取证门就是从那里读 path / contentSha256 / lines 的。 + #[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( + &mut observation, + Some("action-0123456789abcdef01234567"), + ); + let detail = observation.detail.clone().expect("detail"); + assert!(detail + .lines() + .next() + .expect("first line") + .ends_with("· sourceActionId=action-0123456789abcdef01234567")); + assert!(detail.contains("第一行内容"), "首行之后的内容必须保留"); + + let root = std::env::temp_dir(); + let safe_detail = crate::agent::runtime_actions::action_audit:: + agent_runtime_action_receipt_public_safe_detail_for_test(&root, &observation) + .expect("safe detail still parses"); + let value = serde_json::from_str::(&safe_detail).expect("json"); + assert_eq!(value["path"], "game/fast_gdd.md"); + assert_eq!(value["lines"], "1-134 of 134"); + assert_eq!(value["contentSha256"], "a".repeat(64)); + } + + /// 失败的读取、缺 actionId、以及重复调用都不得改写 detail。 + #[test] + fn appending_is_a_no_op_without_a_successful_read_or_an_action_id() { + let mut failed = file_read_observation(); + failed.status = "failed".to_string(); + let before = failed.detail.clone(); + append_agent_runtime_file_read_source_action_id(&mut failed, Some("action-1")); + assert_eq!(failed.detail, before); + + let mut missing = file_read_observation(); + let before = missing.detail.clone(); + append_agent_runtime_file_read_source_action_id(&mut missing, None); + assert_eq!(missing.detail, before); + + let mut twice = file_read_observation(); + append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-1")); + let once = twice.detail.clone(); + append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-2")); + assert_eq!(twice.detail, 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 612e62f04..3e832ddf8 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。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。" + "{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 返工。" ) } else if root_control_authority { format!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs index fab7a573c..51f7b29fd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs @@ -128,15 +128,16 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> { /// needs-user-input observation,也就没有调用它的时机;广告出去只会让它在别的时点 /// 调一次,撞上 Runtime 已经装好的那份 pending 而硬失败。 /// -/// `file.read`、`agent.acceptance_update`、`agent.action_history` 只为 §13.0 的审批 -/// 前置取证门存在(分页读 `game/fast_gdd.md` 并列出全部分页 actionId)。 +/// `file.read` 与 `agent.acceptance_update` 只为 §13.0 的审批前置取证门存在(分页读 +/// `game/fast_gdd.md` 并列出全部分页 actionId)。**`agent.action_history` 不在其中**: +/// 每次 `file.read` 的 observation 已经自带 `sourceActionId`,取证不需要回头查历史; +/// 留着它只会让模型为同一个 id 反复确认(实测连查四次,答案一直在上下文里)。 pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static str] { &[ "file.read", "agent.delegate", "agent.goal_contract", "agent.acceptance_update", - "agent.action_history", "agent.run_status", ] } @@ -172,7 +173,6 @@ pub(crate) fn agent_runtime_plan_root_supervisor_tools_for_stage( "file.read", "agent.delegate", "agent.acceptance_update", - "agent.action_history", "agent.run_status", ], }