From ceb484ce393c24d30715743e72fd2fb3d4d4b235 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 16 Sep 2026 09:24:05 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E5=88=92Agent?= =?UTF-8?q?=E4=B8=AD=E6=96=AD=E6=89=B9=E6=AC=A1=E8=87=AA=E5=8A=A8=E8=87=AA?= =?UTF-8?q?=E6=84=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单次策划工具执行不再竞争项目级写锁,仅保留策划命令锁与原子写入边界 项目重开时自动接续执行标记未落盘的策划工具批次,补齐不确定结果并交回Provider自愈 新增中断批次恢复定向测试,不重放文件副作用且不要求用户手动重试 同步策划Agent恢复合同与共享决策记录 --- .../src-tauri/src/agent/design_runtime.rs | 135 +++++++++++++++++- .../shared-memory/decision-log.md | 7 + ...】策划Agent生产迁移与工作区浏览-2026-09-10.md | 2 + 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index ebe6152b8..1a5a6e486 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -527,10 +527,6 @@ fn process_design_batch( let result = if uncertain { Err("进程在工具执行期间中断,执行结果未保存。未重复执行;请读取实际工作区确认结果后再决定下一步。".to_string()) } else { - let _write = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "design.tool", - )?; execute_design_tool(root, resources, session, &call) }; let error = result @@ -1026,6 +1022,15 @@ pub(crate) async fn continue_design_agent_at( finish_design_command(root, resources, session, active, run, emit).await } +async fn recover_uncertain_design_batch( + root: &Path, + resources: &DesignResources, + session: DesignSession, + active: File, +) -> Result { + finish_design_command(root, resources, session, active, true, |_| {}).await +} + pub(crate) async fn decide_design_phase_at( root: &Path, resources: &DesignResources, @@ -1058,7 +1063,8 @@ fn ensure_design_runtime_active(root: &Path) -> Result<(), String> { } #[tauri::command] -pub(crate) fn hydrate_design_agent_session( +pub(crate) async fn hydrate_design_agent_session( + app: tauri::AppHandle, project_path: String, ) -> Result, String> { let root = Path::new(project_path.trim()); @@ -1084,8 +1090,33 @@ pub(crate) fn hydrate_design_agent_session( if session.project_id != project_id { return Err("策划会话与当前项目不匹配".into()); } - let active = try_open_game_creator_agent_runtime_task_lock_file(root, DESIGN_ACTIVE_LOCK)?; - Ok(Some(design_view(&session, active.is_none()))) + let Some(active) = + try_open_game_creator_agent_runtime_task_lock_file(root, DESIGN_ACTIVE_LOCK)? + else { + return Ok(Some(design_view(&session, true))); + }; + if design_session_has_uncertain_batch(&session) { + let resources = DesignResources::new(resolve_design_resources_root(&app)?)?; + let view = recover_uncertain_design_batch(root, &resources, session, active).await?; + return Ok(Some(view)); + } + drop(active); + Ok(Some(design_view(&session, false))) +} + +fn design_session_has_uncertain_batch(session: &DesignSession) -> bool { + let Some(batch) = session.pending_batch.as_ref() else { + return false; + }; + if !batch.executing || batch.cursor >= batch.calls.len() { + return false; + } + let call_id = batch.calls[batch.cursor].id.as_str(); + session.turn.as_ref().is_some_and(|turn| turn.pending) + && !session.history.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call_output") + && item.get("call_id").and_then(Value::as_str) == Some(call_id) + }) } fn design_session_error_is_recoverable(error: &str) -> bool { @@ -1958,4 +1989,94 @@ mod tests { .any(|message| message.text.contains("重试后继续"))); assert!(next.session.last_error.is_none()); } + + #[tokio::test(flavor = "current_thread")] + async fn uncertain_batch_hydrate_continues_the_original_turn_without_replaying_file_tools() { + let (_temp, root, resources) = init_design_project(); + execute_design_file_tool( + &root, + "write_file", + &json!({"path":"project/00_concept/design.md","content":"概念"}), + ) + .expect("write concept"); + let mut session = new_design_session("design-fake", "quality"); + let call = platform_llm::LlmToolCall { + id: "interrupted-call".into(), + name: "patch_file".into(), + arguments: json!({ + "path":"project/00_concept/design.md", + "old_text":"概念", + "new_text":"概念设计" + }) + .to_string(), + }; + session.history.push(json!({ + "type":"function_call", + "call_id":call.id, + "name":call.name, + "arguments":call.arguments, + })); + session.messages = vec![DesignMessage { + id: "turn:user".into(), + role: "user".into(), + text: "继续".into(), + }]; + session.turn = Some(DesignTurn { + id: "turn-recovery".into(), + pending: true, + request_index: 0, + attempt: 0, + }); + session.pending_batch = Some(DesignToolBatch { + calls: vec![call], + cursor: 0, + executing: true, + }); + assert!(design_session_has_uncertain_batch(&session)); + write_design_session(&root, &session).expect("write interrupted session"); + + let _fake = fake_provider::install( + vec![Ok(fake_response( + "recovered-after-uncertain-tool", + "已读取文件并确认。", + Vec::new(), + ))], + 0, + ); + let view = recover_uncertain_design_batch(&root, &resources, session, { + try_open_game_creator_agent_runtime_task_lock_file( + &root, + ".agent/design-agent/active.lock", + ) + .expect("open active lock") + .expect("active lock is free") + }) + .await + .expect("recover uncertain batch"); + + assert!(!view.running); + assert!(view.session.last_error.is_none()); + let restored = read_design_session(&root) + .expect("read restored") + .expect("session"); + assert!(restored.pending_batch.is_none()); + assert!(!restored.turn.expect("turn").pending); + assert!(restored.history.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call_output") + && item.get("call_id").and_then(Value::as_str) == Some("interrupted-call") + && item + .get("output") + .and_then(Value::as_str) + .is_some_and(|output| output.contains("执行结果未保存")) + })); + assert!(restored.history.iter().any(|item| { + item.get("role").and_then(Value::as_str) == Some("assistant") + && item.get("content").is_some() + })); + assert!( + fs::read_to_string(root.join("design_artifacts/project/00_concept/design.md")) + .expect("read target") + == "概念" + ); + } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d778bcade..804e272ea 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2,6 +2,13 @@ > 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。 > 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。 +## 2026-09-16 策划 Agent 工具执行退出项目级写锁并自动接续中断批次 + +- 背景:策划 Agent 每个 `read_file` / `write_file` / `patch_file` 工具都在执行前竞争全局项目写锁,但同一会话已由 `.agent/design-agent/active.lock` 串行化,工具目标又限定在 `design_artifacts`;项目锁既不覆盖「工具 + 会话 checkpoint」事务,还把进程中断时的 `executing=true` 不确定窗口扩大到等锁与工具执行全程。真机项目出现 `pendingBatch.executing=true`、`function_call` 无配对 output、UI 只显示工作中且无错误的状态。 +- 决策:单次策划工具不再竞争项目级写锁,只保留策划命令锁与既有原子写入;GameAgent / DirectProject 的公共项目锁实现与调用不变。重开项目 hydrate 时,若命令锁可获取且当前批次处于 `executing=true`、当前 call 无 output,则自动续跑原回合:为该 call 补写「执行结果未保存」的工具错误、跳过剩余调用并交回 Provider 自愈;不得重放文件副作用,也不要求用户手动重试。 +- 验证:新增定向用例证明中断批次自动补齐工具 output、收到后续 assistant 回复、清空 pendingBatch 并结束原 turn,同时目标文件保持未修改(未重放 `patch_file`);策划 Runtime 定向 14 条、策划工具 3 条通过,`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 +- 关联文档:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 + ## 2026-09-16 图标图集自动拆图上限提高到 256 - 背景:AGC 图标图集自动连通域识别在一次生成中识别出 86 个区域,原有 64 片上限在后处理阶段阻断了请求;该上限同时影响 api-server 自动 / 手动切片、SpacetimeDB 批量落库和统一生成结果 item 数量。 diff --git a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md index ce891e8fc..45701f921 100644 --- a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md +++ b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md @@ -132,6 +132,8 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 一轮有多个工具调用时沿用正常工具执行循环。澄清或审批进入等待后,不继续请求 Provider,也不执行同批剩余文件操作;未执行调用明确记录为因等待用户而未执行,不伪造成功结果。恢复历史必须保持工具调用与结果配对,避免出现缺少 tool output 的协议错误。这属于协议与暂停处理,不引入同轮调用次数门禁。 +单次策划工具不再竞争项目级写锁;策划命令锁与会话原子写入已保证同一会话内工具按批次顺序执行。若进程在工具执行标记与结果落盘之间中断,重开项目时的只读 hydrate 必须在拿到策划命令锁后自动续跑原回合,为不确定调用补写“执行结果未保存”的工具错误、跳过剩余调用,并把错误交回 Provider 自愈;不得重放文件副作用,也不要求用户手动恢复。 + 迁移工具集合: ```text