允许Agent恢复项目快照

后台 Runtime 工具箱新增 project.restore

Agent 可按权限策略请求恢复到指定 checkpoint

补充恢复成功和确认阻断测试

同步 Runtime V1 文档和项目记忆
This commit is contained in:
AIGameCreator App
2026-07-10 08:33:05 +08:00
parent bdf3ae87da
commit d150f77301
4 changed files with 239 additions and 4 deletions
File diff suppressed because one or more lines are too long
@@ -5543,6 +5543,190 @@ async fn background_agent_runtime_project_diff_respects_project_policy() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_can_restore_checkpoint() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
write_local_project_file_at(&root, "game/notes.txt", "v1").expect("write notes");
let checkpoint = create_local_project_checkpoint_at(&root).expect("create checkpoint");
write_local_project_file_at(&root, "game/notes.txt", "v2").expect("change notes");
write_local_project_file_at(&root, "game/temp.txt", "temp").expect("add temp file");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow project restore");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "本轮修改需要回滚到 checkpoint",
"plan": ["恢复 checkpoint", "确认恢复结果"],
"actions": [
{
"tool": "project.restore",
"reason": "撤销本轮偏离目标的文件修改",
"input": { "checkpointId": checkpoint.checkpoint_id }
}
],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![plan_json, "checkpoint 已恢复。".to_string()],
Some(sender),
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"design-director": {{
"apiKey": "design-key",
"baseUrl": {base_url:?},
"model": "design-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"design-director",
"后台恢复到安全 checkpoint",
"design-project-restore-run",
)
.expect("start background task");
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("plan llm request");
assert!(plan_request.contains("project.restore"));
assert!(plan_request.contains("checkpointId"));
let final_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("final reply llm request");
assert!(final_request.contains("project.restore"));
assert!(final_request.contains("已恢复 checkpoint"));
assert!(final_request.contains("restoredCount="));
assert!(final_request.contains("deletedCount=1"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert!(runtime
.tool_policy
.auto_tools
.contains(&"project.restore".to_string()));
assert!(runtime
.observations
.iter()
.any(|item| item.contains("project.restoreok · 已恢复 checkpoint")));
assert!(runtime.recent_tool_calls.iter().any(|call| {
call.tool == "project.restore"
&& call.status == "ok"
&& call
.detail
.as_deref()
.is_some_and(|detail| detail.contains("deletedCount=1"))
}));
assert_eq!(
fs::read_to_string(root.join("game/notes.txt")).expect("restored notes"),
"v1"
);
assert!(!root.join("game/temp.txt").exists());
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"project.restore\""));
assert!(agent_db.contains("\"tool\":\"project.restore\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_project_restore_respects_project_policy() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
write_local_project_file_at(&root, "game/blocked-notes.txt", "v1").expect("write notes");
let checkpoint = create_local_project_checkpoint_at(&root).expect("create checkpoint");
write_local_project_file_at(&root, "game/blocked-notes.txt", "v2").expect("change notes");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: vec!["project.restore".to_string()],
agent_policies: BTreeMap::new(),
},
)
.expect("write policy");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "尝试恢复 checkpoint",
"plan": ["恢复 checkpoint", "等待权限结果"],
"actions": [
{
"tool": "project.restore",
"reason": "恢复项目到安全状态",
"input": { "checkpointId": checkpoint.checkpoint_id }
}
],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender));
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"design-director": {{
"apiKey": "design-key",
"baseUrl": {base_url:?},
"model": "design-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"design-director",
"后台尝试恢复 checkpoint",
"design-project-restore-policy-run",
)
.expect("start background task");
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("plan llm request");
assert!(plan_request.contains("confirmTools"));
assert!(plan_request.contains("project.restore"));
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
let runtime = wait_for_agent_runtime_confirmation(&root, "design-director");
assert_eq!(runtime.status, "waiting-for-confirmation");
assert_eq!(runtime.task_queue.waiting_for_confirmation, 1);
assert!(runtime
.tool_policy
.confirm_tools
.contains(&"project.restore".to_string()));
assert!(runtime.observations.iter().any(|item| item.contains(
"project.restorewaiting-for-confirmation · 项目权限策略要求用户确认:project.restore"
)));
assert!(!runtime
.observations
.iter()
.any(|item| item.contains("project.restoreok")));
assert_eq!(
fs::read_to_string(root.join("game/blocked-notes.txt")).expect("blocked notes"),
"v2"
);
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(!agent_db.contains("\"recordType\":\"project.restore\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_can_read_other_agent_status() {
let root = unique_project_path();
@@ -32,6 +32,7 @@
- 2026-07-10 补充:主窗口 Agent 状态栏在开发模式新增“调度 Ready”控制,用来显式调用 `schedule_game_creator_agent_ready_tasks`。该控制先读取项目策略,命中 `agent.schedule_ready` confirm 时走现有确认弹窗,确认后才把 manifest ready task 投递到对应 Agent Runtime;普通用户窗口不展示这个开发控制。
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.schedule_ready`,让 Agent 在完成或更新 manifest task 后可按项目权限策略自行调度新 ready task。该工具复用同一个 scheduler:扫描依赖已完成且仍为 pending 的 manifest task,先标为 running,再按 taskId 投递到对应 Agent 的既有后台队列;策略要求确认时当前 Agent 停在 `waiting-for-confirmation`,不会静默启动下游 Agent。
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `project.checkpoint`,让 Agent 在 `file.write``task.update` 或批量修改前自行创建本地 checkpoint。该工具复用 `project.checkpoint` 策略和项目写锁,observation 只返回 checkpoint id、文件数和总字节数,不返回本机绝对路径;策略要求确认时不创建 checkpoint。
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `project.restore`,让 Agent 在 diff 或自检发现走偏后可请求恢复到指定 checkpoint。该工具复用 `project.restore` 确认策略和项目写锁,observation 只返回 checkpoint id、恢复文件数和删除文件数;默认确认策略下不会静默回滚用户项目。
- 影响范围:`apps/ai-game-creator-shell` 的 Tauri command、Agent Runtime state/event、开发窗口单 Agent 聊天、项目内 Agent 对话弹窗、`appSurface.test.ts` 和 AI 游戏创作 App 实施计划。
- 验证方式:运行 Tauri Rust 后台 Agent 并行测试、壳前端 appSurface 测试、壳 typecheck、编码检查和 `git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
File diff suppressed because one or more lines are too long