From 64ca2f99b22041d29f5ef13df014860a2c556e17 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Wed, 22 Jul 2026 19:49:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=9A=20Agent=20=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E5=86=99=E5=85=A5=E5=AF=BC=E8=87=B4=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E4=B8=BB=E6=9E=84=E5=BB=BA=E4=B8=AD=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将执行前 project revision 漂移改为 blocked observation 并在同一 run 自动重新规划 为 file.write 和 file.patch 增加项目锁内 pending 与 revision 二次校验 补充 stale action 回归、真实外部模型与确定性可玩塔防验收记录 --- .../agent/runtime_actions/action_execution.rs | 27 ++- .../agent/runtime_actions/project_gates.rs | 43 +++- .../src/agent/runtime_driver/interaction.rs | 20 ++ .../src/agent/runtime_driver/main_loop.rs | 22 +-- .../agent/runtime_driver/pending_execution.rs | 19 +- .../src/agent/runtime_tools/file_ops.rs | 58 +++++- .../src-tauri/src/tests/project_tools.rs | 185 ++++++++++++++---- .../src/tests/runtime_actions/recovery.rs | 15 +- .../shared-memory/decision-log.md | 7 + docs/project-memory/shared-memory/pitfalls.md | 8 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 3 + 11 files changed, 317 insertions(+), 90 deletions(-) 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 59cce6ca4..bd46df656 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 @@ -250,8 +250,22 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ false, || observe_agent_runtime_file(root, &action.input), ), - "file.write" => observe_agent_runtime_file_write(root, agent_id, run_id, &action.input), - "file.patch" => observe_agent_runtime_file_patch(root, agent_id, run_id, &action.input), + "file.write" => observe_agent_runtime_file_write( + root, + agent_id, + run_id, + action, + &action_fingerprint, + pending_action, + ), + "file.patch" => observe_agent_runtime_file_patch( + root, + agent_id, + run_id, + action, + &action_fingerprint, + pending_action, + ), "file.delete" => { observe_agent_runtime_file_delete(root, agent_id, run_id, pending_action, &action.input) } @@ -564,6 +578,15 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc } } if validate_revision_gate { + match pending_project_revision_drift_observation(root, pending, true) { + Ok(Some(observation)) => return Err(observation), + Ok(None) => {} + Err(error) => { + return Err(agent_runtime_pending_reconciliation_observation( + tool, root, &error, + )); + } + } if let Err(error) = validate_agent_runtime_pending_verification_gate_before(root, pending) { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs index 027b17aed..915a95d14 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs @@ -347,11 +347,9 @@ pub(crate) fn agent_runtime_tool_requires_pending_revision_gate(tool: &str) -> b ) } -pub(crate) fn validate_agent_runtime_pending_verification_gate_before( - root: &Path, +fn agent_runtime_pending_expected_project_revision( pending: &AgentRuntimePendingToolAction, -) -> Result<(), String> { - validate_agent_runtime_project_revision(root, &pending.project_revision_before)?; +) -> Result { let prior_action_count = usize::try_from(pending.action_index).unwrap_or(usize::MAX); let prior_revision_advances = pending .observations @@ -360,11 +358,44 @@ pub(crate) fn validate_agent_runtime_pending_verification_gate_before( .take(prior_action_count) .filter(|observation| agent_runtime_observation_advances_project_revision(observation)) .count(); - let expected_revision = pending + pending .project_revision_before .revision .checked_add(u64::try_from(prior_revision_advances).unwrap_or(u64::MAX)) - .ok_or_else(|| "Agent Runtime 待执行动作的预期项目 revision 已达到上限".to_string())?; + .ok_or_else(|| "Agent Runtime 待执行动作的预期项目 revision 已达到上限".to_string()) +} + +pub(in crate::agent) fn pending_project_revision_drift_observation( + root: &Path, + pending: &AgentRuntimePendingToolAction, + validate_revision_gate: bool, +) -> Result, String> { + if !validate_revision_gate { + return Ok(None); + } + validate_agent_runtime_project_revision(root, &pending.project_revision_before)?; + let expected_revision = agent_runtime_pending_expected_project_revision(pending)?; + let current_revision = read_game_creator_agent_runtime_project_revision(root)?; + if current_revision.revision == expected_revision { + return Ok(None); + } + Ok(Some(AgentRuntimeToolObservation { + tool: pending.action.tool.clone(), + status: "blocked".to_string(), + summary: "并行项目变更使旧动作过期,旧动作未执行".to_string(), + detail: Some(format!( + "projectRevisionDrift=true · expectedRevision={expected_revision} · currentRevision={} · replanRequired=true", + current_revision.revision + )), + })) +} + +pub(crate) fn validate_agent_runtime_pending_verification_gate_before( + root: &Path, + pending: &AgentRuntimePendingToolAction, +) -> Result<(), String> { + validate_agent_runtime_project_revision(root, &pending.project_revision_before)?; + let expected_revision = agent_runtime_pending_expected_project_revision(pending)?; let current_revision = read_game_creator_agent_runtime_project_revision(root)?; if current_revision.revision != expected_revision { return Err(format!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs index 0b1386958..ad6fe1c7c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs @@ -167,6 +167,26 @@ pub(crate) fn pending_repository_context_drift_observation( })) } +pub(in crate::agent) fn pending_action_pre_execution_drift_observation( + root: &Path, + pending: &AgentRuntimePendingToolAction, +) -> Result, String> { + if let Some(observation) = pending_repository_context_drift_observation(root, pending)? { + return Ok(Some(observation)); + } + let validate_revision_gate = + agent_runtime_tool_requires_pending_revision_gate(&pending.action.tool); + if let Some(observation) = + pending_project_revision_drift_observation(root, pending, validate_revision_gate)? + { + return Ok(Some(observation)); + } + if validate_revision_gate { + validate_agent_runtime_pending_verification_gate_before(root, pending)?; + } + Ok(None) +} + pub(crate) fn reject_game_creator_agent_runtime_task_at( root: &Path, agent_id: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs index a94ae587d..3824beea1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs @@ -1837,8 +1837,8 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c ); return AgentBackgroundTaskOutcome::Finished; } - let repository_context_drift = - match pending_repository_context_drift_observation(&root, &pending_action) { + let pre_execution_drift = + match pending_action_pre_execution_drift_observation(&root, &pending_action) { Ok(observation) => observation, Err(error) => { let _ = mark_game_creator_agent_runtime_needs_reconciliation_at( @@ -1850,7 +1850,7 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c return AgentBackgroundTaskOutcome::NeedsReconciliation; } }; - if let Some(observation) = repository_context_drift { + if let Some(observation) = pre_execution_drift { pending_action.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string(); pending_action.observation = Some(observation.clone()); @@ -1876,22 +1876,6 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c durable_action = Some(pending_action); observation } else { - if agent_runtime_tool_requires_pending_revision_gate( - &pending_action.action.tool, - ) { - if let Err(error) = validate_agent_runtime_pending_verification_gate_before( - &root, - &pending_action, - ) { - let _ = mark_game_creator_agent_runtime_needs_reconciliation_at( - &root, - &mut runtime, - &pending_action, - &error, - ); - return AgentBackgroundTaskOutcome::NeedsReconciliation; - } - } let action_is_current = match mark_game_creator_agent_runtime_auto_action_executing_if_current( &root, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs index d503163c1..4e302d6c7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs @@ -136,8 +136,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( let auto_execution = pending.is_auto(); let observation = match pending.status.as_str() { AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED => { - let repository_context_drift = - match pending_repository_context_drift_observation(&root, &pending) { + let pre_execution_drift = + match pending_action_pre_execution_drift_observation(&root, &pending) { Ok(observation) => observation, Err(error) => { let _ = mark_game_creator_agent_runtime_needs_reconciliation_at( @@ -149,7 +149,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( return; } }; - if let Some(observation) = repository_context_drift { + if let Some(observation) = pre_execution_drift { pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string(); pending.observation = Some(observation.clone()); pending.updated_at = unix_timestamp(); @@ -173,19 +173,6 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( } observation } else { - if agent_runtime_tool_requires_pending_revision_gate(&pending.action.tool) { - if let Err(error) = - validate_agent_runtime_pending_verification_gate_before(&root, &pending) - { - let _ = mark_game_creator_agent_runtime_needs_reconciliation_at( - &root, - &mut runtime, - &pending, - &error, - ); - return; - } - } pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string(); pending.updated_at = unix_timestamp(); if let Err(error) = diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs index d4b86a4b5..1efa71332 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs @@ -100,8 +100,11 @@ pub(in crate::agent) fn observe_agent_runtime_file_write( root: &Path, agent_id: &str, run_id: &str, - input: &serde_json::Value, + action: &AgentRuntimeToolAction, + action_fingerprint: &str, + pending_action: Option<&AgentRuntimePendingToolAction>, ) -> AgentRuntimeToolObservation { + let input = &action.input; let path = input .get("path") .and_then(|value| value.as_str()) @@ -168,6 +171,30 @@ pub(in crate::agent) fn observe_agent_runtime_file_write( }; } }; + if let Some(pending) = pending_action { + if pending.agent_id != agent_id + || pending.run_id != run_id + || pending.action != *action + || pending.action_fingerprint != action_fingerprint + { + return agent_runtime_pending_reconciliation_observation( + "file.write", + root, + "等待项目锁后 file.write pending action 身份已变化", + ); + } + match pending_action_pre_execution_drift_observation(root, pending) { + Ok(Some(observation)) => return observation, + Ok(None) => {} + Err(error) => { + return agent_runtime_pending_reconciliation_observation( + "file.write", + root, + &error, + ); + } + } + } if let Err(error) = prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "file.write") { @@ -319,8 +346,11 @@ pub(in crate::agent) fn observe_agent_runtime_file_patch( root: &Path, agent_id: &str, run_id: &str, - input: &serde_json::Value, + action: &AgentRuntimeToolAction, + action_fingerprint: &str, + pending_action: Option<&AgentRuntimePendingToolAction>, ) -> AgentRuntimeToolObservation { + let input = &action.input; let path = agent_runtime_tool_input_text(input, &["path"]); if path.is_empty() { return AgentRuntimeToolObservation { @@ -404,6 +434,30 @@ pub(in crate::agent) fn observe_agent_runtime_file_patch( }; } }; + if let Some(pending) = pending_action { + if pending.agent_id != agent_id + || pending.run_id != run_id + || pending.action != *action + || pending.action_fingerprint != action_fingerprint + { + return agent_runtime_pending_reconciliation_observation( + "file.patch", + root, + "等待项目锁后 file.patch pending action 身份已变化", + ); + } + match pending_action_pre_execution_drift_observation(root, pending) { + Ok(Some(observation)) => return observation, + Ok(None) => {} + Err(error) => { + return agent_runtime_pending_reconciliation_observation( + "file.patch", + root, + &error, + ); + } + } + } if let Err(error) = prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "file.patch") { diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs index db7902436..6bce35bef 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs @@ -671,6 +671,122 @@ fn project_snapshot_rereads_durable_pending_after_project_lock_wait() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn agent_runtime_file_write_and_patch_replan_after_locked_revision_drift() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "并行写入 revision 围栏").expect("project init"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), + }, + ) + .expect("allow direct file mutations in test"); + fs::write(root.join("game/write-target.txt"), "write-original\n") + .expect("write file.write target"); + fs::write(root.join("game/patch-target.txt"), "patch-original\n") + .expect("write file.patch target"); + + let cases = [ + ( + "code-prototype", + "code-write-locked-drift-run", + AgentRuntimeToolAction { + tool: "file.write".to_string(), + reason: Some("写入完整游戏文件".to_string()), + input: serde_json::json!({ + "path": "game/write-target.txt", + "content": "stale-write\n" + }), + }, + "game/write-target.txt", + "write-original\n", + ), + ( + "design-director", + "design-patch-locked-drift-run", + AgentRuntimeToolAction { + tool: "file.patch".to_string(), + reason: Some("局部修改游戏文件".to_string()), + input: serde_json::json!({ + "path": "game/patch-target.txt", + "oldText": "patch-original", + "newText": "stale-patch", + "expectedReplacements": 1 + }), + }, + "game/patch-target.txt", + "patch-original\n", + ), + ]; + + for (index, (agent_id, run_id, action, target, expected_content)) in + cases.into_iter().enumerate() + { + let mut state = start_game_creator_agent_runtime_task_at( + &root, + agent_id, + "执行等待项目锁的并行文件修改", + run_id, + "agent-background-task", + "准备执行文件修改", + vec!["修改目标文件".to_string()], + ) + .expect("start runtime state"); + state.loop_iteration = 1; + let mut pending = pending_tool_action_for_test( + &root, + &state, + action.clone(), + AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING, + None, + ); + pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string(); + write_game_creator_agent_runtime_pending_tool_action(&root, &pending) + .expect("write pending file action"); + state.status = "running".to_string(); + state.phase = "action".to_string(); + state.pending_tool_action = Some(pending.summary()); + append_game_creator_agent_runtime_task(&root, &state).expect("append running task"); + write_game_creator_agent_runtime_state(&root, &state).expect("write running state"); + assert_eq!( + advance_project_revision_for_test( + &root, + "art-director", + &format!("art-file-drift-{index}"), + "file.write", + ), + u64::try_from(index + 1).expect("expected revision"), + ); + + let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action( + &root, + agent_id, + run_id, + &state.current_task, + &action, + Some(&pending.action_id), + Some(&pending), + ) + .await; + + assert_eq!(observation.status, "blocked", "{observation:?}"); + assert!(observation.summary.contains("旧动作未执行")); + assert!(observation + .detail + .as_deref() + .is_some_and(|detail| detail.contains("projectRevisionDrift=true"))); + assert_eq!( + fs::read_to_string(root.join(target)).expect("read preserved target"), + expected_content, + ); + } + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn agent_runtime_file_delete_revalidates_revision_after_acquiring_project_lock() { let root = unique_project_path(); @@ -791,7 +907,7 @@ async fn agent_runtime_file_delete_reports_reconciliation_when_audit_fails_after } #[tokio::test] -async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project_revision() { +async fn background_agent_runtime_file_delete_confirmation_replans_after_stale_project_revision() { let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); let target = root.join("game/stale-confirm-delete-target.txt"); @@ -813,8 +929,15 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project "response": "" }) .to_string(); + let final_response = "旧删除动作未执行,已按最新项目 revision 完成重新规划。"; let (sender, receiver) = mpsc::channel(); - let base_url = spawn_mock_llm_server_responses_with_capture(vec![delete_plan], Some(sender)); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec![ + delete_plan, + final_tool_plan_response(final_response.to_string()), + ], + Some(sender), + ); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -885,38 +1008,29 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project assert_eq!(approved.state.run_id, "design-stale-delete-confirm-run"); assert_eq!(approved.state.status, "running"); - let mut reconciled = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read stale delete runtime") - .state; - for _ in 0..250 { - if reconciled.phase == "needs-reconciliation" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - reconciled = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read stale delete reconciliation") - .state; - } - assert_eq!(reconciled.run_id, "design-stale-delete-confirm-run"); - assert_eq!(reconciled.status, "failed"); - assert_eq!(reconciled.phase, "needs-reconciliation"); - assert!(reconciled - .error - .as_deref() - .is_some_and(|error| error.contains("项目 revision 已变化"))); + let replanning_request = receiver + .recv_timeout(Duration::from_secs(10)) + .expect("same-run replanning request after stale delete approval"); + assert!(replanning_request.contains("design-stale-delete-confirm-run")); + assert!(replanning_request.contains("projectRevisionDrift=true")); + assert!(replanning_request.contains("旧动作未执行")); + + let completed = wait_for_agent_runtime_idle(&root, "design-director"); + assert_eq!(completed.run_id, "design-stale-delete-confirm-run"); + assert_eq!(completed.status, "idle"); + assert_eq!(completed.phase, "completed"); + assert_eq!(completed.last_response.as_deref(), Some(final_response)); + assert!(completed.error.is_none()); + assert!(completed.recent_tool_calls.iter().any(|call| { + call.tool == "file.delete" + && call.status == "blocked" + && call.summary.contains("旧动作未执行") + })); assert_eq!( fs::read_to_string(&target).expect("read target after stale approval"), "revision 漂移后必须保留\n" ); - assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let persisted: AgentRuntimePendingToolAction = serde_json::from_str( - &fs::read_to_string(&pending_path).expect("stale delete ledger must remain"), - ) - .expect("parse reconciled stale delete ledger"); - assert_eq!( - persisted.status, - AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED - ); + assert!(!pending_path.exists()); let records = read_agent_db_records_for_test(&root); assert!(records.iter().any(|record| { record["recordType"] == "agent.runtime.tool_confirmation.approved" @@ -930,13 +1044,10 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project record["recordType"] == "agent.runtime.file.delete" && record["path"] == "game/stale-confirm-delete-target.txt" })); - - cancel_game_creator_agent_runtime_task_at( - &root, - "design-director", - "design-stale-delete-confirm-run", - ) - .expect("cancel reconciled stale delete task after manual check"); + assert!(!records.iter().any(|record| { + record["recordType"] == "agent.runtime.tool_action.needs_reconciliation" + && record["actionId"] == pending_summary.action_id + })); fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/recovery.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/recovery.rs index 7f481d912..6ec5f1c24 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/recovery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/recovery.rs @@ -53,14 +53,13 @@ fn agent_runtime_run_status_rechecks_revision_inside_project_lock() { Some(&pending), ), ); - assert_eq!( - observation.status, - AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION - ); - assert!(observation - .detail - .as_deref() - .is_some_and(|detail| detail.contains("revision 已变化"))); + assert_eq!(observation.status, "blocked", "{observation:?}"); + assert!(observation.summary.contains("旧动作未执行")); + assert!(observation.detail.as_deref().is_some_and(|detail| detail + .contains("projectRevisionDrift=true") + && detail.contains("expectedRevision=0") + && detail.contains("currentRevision=1") + && detail.contains("replanRequired=true"))); fs::remove_dir_all(root).ok(); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 08699e8b0..0c4fb0276 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5261,3 +5261,10 @@ - 集成边界:集成修复只补齐 `tool_plan_handoff` 下沉测试不再继承父模块作用域后缺失的 `response_fingerprint`、`validate_ledger` 与 `AsRawFd` import,并对兼容重导出添加局部 `#[allow(unused_imports)]`;未删除兼容出口,编译警告总数仍为 `18`。 - 验收:客户端 crate 的 `cargo fmt --check`、`cargo check` 与 `cargo check --tests` 通过;`tool_plan_handoff` 为 `44/44`,`swarm_cli` 为 `35/35`,`browser` 为 `21 passed / 3 real Chrome ignored`。Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**,Provider lifecycle `17/17`,项目 revision `0 -> 2`,固定试玩 `37/37`,终局残留与泄漏均为 `0`。 - 残余验证缺口:Windows cross check 在进入项目代码前即因宿主缺少 `x86_64-w64-mingw32-gcc` 而停止;本轮不能据此宣称 Windows 交叉编译已通过,需在补齐宿主交叉链接器后复验。 + +## 2026-07-22 Agent Runtime 并行 revision 漂移自动重规划 + +- 背景:真实无人干预塔防 E2E 中,`quality-review` 已在其独立产物路径写入验证脚本并把项目 revision 从 `0` 推进到 `1`;并行的 `code-prototype` 随后准备写 `game/index.html`。该动作尚未执行,却因 planning 时保存的全局 revision 为 `0` 被标记为 `needs-reconciliation`,CLI 立即返回,项目没有生成。 +- 决策:pending action 在执行前发现 project revision 漂移时,必须持久化为 `blocked` observation,明确 `projectRevisionDrift=true / replanRequired=true / 旧动作未执行`,清理旧 pending,并让同一 Agent、同一 run 基于最新项目状态继续 planning。只有副作用可能已经发生、持久身份损坏或审计无法证明结果时才进入 `needs-reconciliation`。 +- 锁内边界:`file.write` 与 `file.patch` 在取得项目写锁后再次核对 pending 身份、仓库上下文、revision 和 verification gate,避免预检后与另一 Agent 的项目修改交错。revision 漂移只拒绝旧动作,不忽略并发变化,也不直接执行可能覆盖他人结果的旧写入。 +- 验收:三个定向回归、全部 `revision` 过滤测试 `32/32`、`cargo check --tests` 和 Linux 串行全量 `1147 passed / 5 ignored / 0 failed` 通过。确定性正式 E2E 继续以 `17/17` Provider lifecycle、revision `0 -> 2` 和 Chrome `37/37` 通过。新的独立真实 external-provider E2E 只写入一次塔防需求并立即 EOF,人工 approve / answer / steer 均为 `0`;父 turn `settled`,项目 revision `0 -> 8`,static smoke 和真实 Chrome `lane-defense-v1 37/37` 通过,唯一 Supervisor assistant 写入,pending、confirmation、user-input、reconciliation、sidecar、重复与敏感信息泄漏均为 `0`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 9d6c372fc..684fbb78a 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3535,3 +3535,11 @@ - 原因:Rust 子模块不会继承父模块的私有 `use` 作用域;兼容重导出的价值是维持旧调用面,不能用当前 facade 是否直接消费来判断;多个 Agent 即使写入范围互不重叠,全 crate 编译仍会读取全部模块,因而无法避开正在落盘的半成品。 - 处理:测试下沉时显式补齐自身依赖的 import,不把生产可见性为测试统一放宽。已确认属于旧调用面的重导出必须保留,只在精确重导出位置添加局部 `#[allow(unused_imports)]`,不得按 warning 机械删除或全局 suppress。并行阶段禁止启动全 crate 编译、全量测试和真实 E2E;各 Agent 只执行自己边界内的检查,待所有写入方完成后由主线程在稳定共享树统一验收。 - 验证:稳定树统一运行 `cargo fmt --check`、`cargo check`、`cargo check --tests`、三个定向测试组、Linux 串行全量和确定性真实 Runner + Chrome E2E;同时核对原测试名、`turn.report` 字段/顺序、raw JavaScript 哈希和兼容重导出。Windows cross check 若因宿主缺少交叉链接器而未进入项目代码,必须明确记录为残余验证缺口,不能写成项目代码已通过。 + +## 未执行的并行 stale 动作不能进入 needs-reconciliation + +- 现象:两个专业 Agent 在不同文件上并行工作,一个 Agent 先推进全局 project revision;另一个 Agent 的 pending 写动作尚未执行,却因 revision 与 planning 快照不同进入 `failed / needs-reconciliation`。Swarm CLI 随即提前结束,父 Supervisor 仍在等待回执,项目 revision 甚至可能尚未包含核心游戏文件。 +- 原因:旧实现把“执行前发现计划过期”和“执行后无法证明副作用结果”合并成同一种 reconciliation。全局 revision 会被任何合法项目修改推进,因此它能证明旧计划已过期,却不能证明尚未开始的动作产生了未知副作用。 +- 处理:在 pending 标记为 executing 之前检查 revision 漂移;漂移时写入可恢复的 blocked observation,明确旧动作未执行并要求同 run 重新规划。文件写入和 patch 在项目锁内再做一次同样检查,防止预检后的竞态。只有动作可能已落盘、账本身份冲突、审计失败或持久记录损坏时继续失败关闭到 reconciliation。 +- 验证:必须覆盖确认后 stale 动作和锁内 stale 动作两条路径,证明目标文件未改变、旧 pending 被收束、下一次 Provider planning 使用同一 run、最终状态可完成,并用单输入真实 external-provider E2E 验证并行专业 Agent 最终生成可试玩项目。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index b2cb2f693..f4cb2ae58 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -697,3 +697,6 @@ game-project/ - 同轮 `swarm_cli.rs` 从 `4420` 行降到 `68` 行并拆为 `9` 个子模块,最大生产模块 `observer.rs` 为 `843` 行、测试模块为 `1529` 行;原 `35` 个测试名与 `turn.report` 字段/顺序不变。`browser.rs` 从 `4036` 行降到 `24` 行并拆为 `11` 个子模块,最大生产模块 `capture.rs` 为 `733` 行、`playtest/mod.rs` 为 `612` 行,测试模块为 `1169` 行;内嵌 raw JavaScript 的搬迁前后哈希一致。 - 第三轮集成修复仅补 `tool_plan_handoff` 下沉测试缺失的 `response_fingerprint / validate_ledger / AsRawFd` import,并为兼容重导出添加局部 `#[allow(unused_imports)]`;兼容出口未删除,警告总数仍为 `18`。测试子模块不继承父模块 `use`,后续拆分必须显式补 import;并行写入期禁止全 crate 编译,统一门禁只能在四个 Agent 全部完成后的稳定共享树运行。 - 第三轮稳定树已通过 `cargo fmt --check`、`cargo check`、`cargo check --tests`,以及 `tool_plan_handoff 44/44`、`swarm_cli 35/35`、`browser 21 passed / 3 real Chrome ignored`;Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**,Provider lifecycle `17/17`、revision `0 -> 2`、固定试玩 `37/37`,残留与泄漏均为 `0`。Windows cross check 因宿主缺少 `x86_64-w64-mingw32-gcc`,在进入项目代码前停止,仍是明确的残余验证缺口。 +- 2026-07-22 的 V1.45 修复专业 Agent 并行修改导致的 stale pending 中断。执行前发现 project revision 漂移不再进入 `needs-reconciliation`,而是以 `blocked + projectRevisionDrift + replanRequired` observation 回到同一 run 继续 planning;`file.write / file.patch` 还会在项目写锁内复核 pending 身份、仓库上下文、revision 与 verification gate。该语义不放宽并发写安全:旧动作始终不执行,副作用未知、账本冲突或持久化损坏仍失败关闭。 +- V1.45 的独立真实 external-provider 验收只向 `--swarm-chat --init --autonomous-game-build` 写入一次“植物大战僵尸式塔防”需求后立即 EOF。最终 `status=PASS / turn.report=settled`,approve / answer / steer 为 `0`;两条初始 delivery 与一条 repair delivery 均被父 run 认领,项目 revision `0 -> 8`,`game/index.html` 为 `7814` bytes,static smoke、desktop / mobile 浏览器和 `lane-defense-v1 37/37` 全通过。唯一 Supervisor assistant 已提交,pending、confirmation、user-input、provider retry/handoff、tool-plan handoff、finalization、reconciliation、重复和全部隐私泄漏计数均为 `0`。 +- V1.45 稳定树同时通过三个新增/更新定向回归、`revision 32/32`、`cargo check --tests`、Linux 串行全量 `1147 passed / 5 ignored / 0 failed`、`cargo fmt --check`、encoding 与 `git diff --check`。确定性正式 E2E 继续为 **PASS**:Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`,终局残留与泄漏均为 `0`。