恢复Agent后台任务队列

- 新增 Agent Runtime 恢复命令,按 agent.resume 自动策略重接任务

- 恢复时优先处理遗留 running,再串行 drain pending

- 补充前端 runtime refresh 和 Rust/React 回归测试

- 更新 AI 游戏创作 App 实施计划与决策记录
This commit is contained in:
AIGameCreator App
2026-07-10 04:28:18 +08:00
parent b98f34a727
commit 0f9cfb0451
9 changed files with 511 additions and 5 deletions
@@ -233,6 +233,83 @@ pub(crate) fn read_game_creator_agent_runtimes_at(
root: &Path,
) -> Result<Vec<AgentRuntimeResult>, String> {
validate_project_root(root)?;
let agent_ids = collect_game_creator_agent_runtime_agent_ids(root)?;
agent_ids
.into_iter()
.map(|agent_id| read_game_creator_agent_runtime_at(root, &agent_id))
.collect()
}
pub(crate) fn resume_game_creator_agent_background_tasks_at(
root: &Path,
) -> Result<Vec<AgentRuntimeResult>, String> {
validate_project_root(root)?;
let mut resumed = Vec::new();
for agent_id in collect_game_creator_agent_runtime_agent_ids(root)? {
let Some(task) = read_recoverable_game_creator_agent_runtime_task(root, &agent_id)? else {
continue;
};
let Some(runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)?
else {
continue;
};
let source = if task.source.trim().is_empty() {
"agent-background-task"
} else {
task.source.as_str()
};
let state = match start_game_creator_agent_runtime_task_at(
root,
&agent_id,
&task.task,
&task.run_id,
source,
"后台任务从上次运行状态恢复",
game_creator_agent_background_task_default_plan(),
) {
Ok(state) => state,
Err(error) => {
let fallback = default_game_creator_agent_runtime_state(&agent_id, &task.run_id);
let _ = fail_game_creator_agent_runtime_turn_at(root, fallback, &error);
continue;
}
};
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.recovered",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"recoveredFromStatus": task.status,
"task": state.current_task,
}),
);
let _ = append_game_creator_agent_background_task_started_record(root, &state);
let result = read_game_creator_agent_runtime_at(root, &agent_id)?;
let root = root.to_path_buf();
let background_agent_id = agent_id.clone();
let background_task = task.task.clone();
tauri::async_runtime::spawn(async move {
let _runtime_lock = runtime_lock;
drain_game_creator_agent_background_tasks(
root,
background_agent_id,
background_task,
state,
)
.await;
});
resumed.push(result);
}
Ok(resumed)
}
fn collect_game_creator_agent_runtime_agent_ids(
root: &Path,
) -> Result<std::collections::BTreeSet<String>, String> {
let manifest = read_manifest_for_project(root)?;
let mut agent_ids = std::collections::BTreeSet::new();
for task in manifest.tasks {
@@ -250,10 +327,7 @@ pub(crate) fn read_game_creator_agent_runtimes_at(
agent_ids.insert(role.task_id.to_string());
}
}
agent_ids
.into_iter()
.map(|agent_id| read_game_creator_agent_runtime_at(root, &agent_id))
.collect()
Ok(agent_ids)
}
pub(crate) fn start_game_creator_agent_background_task_at(
@@ -3359,6 +3433,25 @@ fn read_next_pending_game_creator_agent_runtime_task(
.find(|record| record.status == "pending"))
}
fn read_recoverable_game_creator_agent_runtime_task(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
if let Some(task) = records
.iter()
.find(|record| record.status == "running")
.cloned()
{
return Ok(Some(task));
}
Ok(records
.into_iter()
.find(|record| record.status == "pending"))
}
fn read_all_game_creator_agent_runtime_tasks(
path: &Path,
) -> Result<Vec<AgentRuntimeTaskRecord>, String> {
@@ -412,6 +412,18 @@ pub(crate) fn read_game_creator_agent_runtimes(
read_game_creator_agent_runtimes_at(root)
}
#[tauri::command]
pub(crate) fn resume_game_creator_agent_runtime_tasks(
project_path: String,
) -> Result<Vec<AgentRuntimeResult>, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
enforce_project_permission_policy(root, "conversation.write")?;
enforce_project_permission_policy(root, "agent.run_status")?;
enforce_project_auto_permission_policy(root, "agent.resume")?;
resume_game_creator_agent_background_tasks_at(root)
}
#[tauri::command]
pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
check_game_creator_llm_config_from_config()
@@ -1173,6 +1173,7 @@ fn main() {
start_game_creator_agent_runtime_task,
read_game_creator_agent_runtime,
read_game_creator_agent_runtimes,
resume_game_creator_agent_runtime_tasks,
check_game_creator_llm_config,
read_game_creator_app_config,
write_game_creator_app_config,
@@ -585,6 +585,30 @@ pub(crate) fn enforce_project_permission_policy(
Ok(())
}
pub(crate) fn enforce_project_auto_permission_policy(
root: &Path,
command_id: &str,
) -> Result<(), String> {
let view = read_project_permission_policy_at(root)?;
if view
.policy
.denied_commands
.iter()
.any(|command| command == command_id)
{
return Err(format!("项目权限策略拒绝执行:{command_id}"));
}
if view
.policy
.confirm_commands
.iter()
.any(|command| command == command_id)
{
return Err(format!("项目权限策略要求用户确认:{command_id}"));
}
Ok(())
}
pub(crate) fn list_local_project_files_at(
root: &Path,
) -> Result<ListLocalProjectFilesResult, String> {
@@ -66,6 +66,20 @@ fn wait_for_agent_runtime_idle(root: &Path, agent_id: &str) -> AgentRuntimeState
runtime
}
fn write_agent_runtime_task_record_for_test(root: &Path, record: &AgentRuntimeTaskRecord) {
let path = root
.join(".agent/runtime/tasks")
.join(format!("{}.jsonl", record.agent_id));
fs::create_dir_all(path.parent().expect("task parent")).expect("runtime task dir");
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.expect("open runtime task file");
serde_json::to_writer(&mut file, record).expect("task record json");
file.write_all(b"\n").expect("write runtime task record");
}
fn test_local_config_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
@@ -1947,6 +1961,307 @@ async fn background_agent_runtime_can_replan_after_observation() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_recovers_stale_running_task() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "恢复上一进程遗留任务后直接回复",
"plan": ["确认恢复任务", "回复开发者"],
"actions": [],
"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"
}}
}}
}}"#
));
let task_path = root.join(".agent/runtime/tasks/design-director.jsonl");
fs::create_dir_all(task_path.parent().expect("task parent")).expect("runtime task dir");
let task_record = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: "design-director".to_string(),
session_id: "agent-session-design-director".to_string(),
run_id: "design-recover-run".to_string(),
source: "agent-background-task".to_string(),
task: "恢复上一进程遗留任务".to_string(),
status: "running".to_string(),
phase: "planning".to_string(),
current_action: "上一进程正在规划".to_string(),
error: None,
updated_at: unix_timestamp().saturating_sub(600),
};
fs::write(
&task_path,
format!(
"{}\n",
serde_json::to_string(&task_record).expect("task record json")
),
)
.expect("write stale running task");
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("runtime lock dir");
fs::write(
&lock_path,
serde_json::json!({
"agentId": "design-director",
"pid": 0,
"createdAt": 1,
})
.to_string(),
)
.expect("write stale lock");
let resumed = resume_game_creator_agent_background_tasks_at(&root).expect("resume stale task");
assert_eq!(resumed.len(), 1);
assert_eq!(resumed[0].state.run_id, "design-recover-run");
assert_eq!(resumed[0].state.status, "running");
assert_eq!(
resumed[0].state.current_action,
"后台任务从上次运行状态恢复"
);
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("recovered plan request");
assert!(plan_request.contains("恢复上一进程遗留任务"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(
runtime.last_response.as_deref(),
Some("已恢复并完成上一进程遗留的后台任务。")
);
let runtime_result =
read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result");
assert!(runtime_result
.recent_tasks
.iter()
.any(|task| task.run_id == "design-recover-run"
&& task.status == "completed"
&& task.phase == "completed"));
assert!(resume_game_creator_agent_background_tasks_at(&root)
.expect("resume after completed")
.is_empty());
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"agent.runtime.background_task.recovered\""));
assert!(agent_db.contains("\"recoveredFromStatus\":\"running\""));
fs::remove_dir_all(root).ok();
}
#[test]
fn background_agent_runtime_resume_command_requires_auto_resume_policy() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let task_record = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: "design-director".to_string(),
session_id: "agent-session-design-director".to_string(),
run_id: "design-confirm-run".to_string(),
source: "agent-background-task".to_string(),
task: "默认确认策略下不能静默恢复".to_string(),
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待恢复".to_string(),
error: None,
updated_at: unix_timestamp(),
};
write_agent_runtime_task_record_for_test(&root, &task_record);
let error = resume_game_creator_agent_runtime_tasks(root.to_string_lossy().into_owned())
.expect_err("agent.resume confirm should block automatic recovery");
assert!(error.contains("项目权限策略要求用户确认:agent.resume"));
let task_snapshot = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read runtime after blocked resume");
assert!(task_snapshot
.recent_tasks
.iter()
.any(|task| task.run_id == "design-confirm-run" && task.status == "pending"));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_recovers_pending_task() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "恢复排队任务后直接回复",
"plan": ["确认排队任务", "回复开发者"],
"actions": [],
"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"
}}
}}
}}"#
));
let task_record = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: "design-director".to_string(),
session_id: "agent-session-design-director".to_string(),
run_id: "design-pending-recover-run".to_string(),
source: "agent-background-task".to_string(),
task: "恢复排队后台任务".to_string(),
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待后台执行".to_string(),
error: None,
updated_at: unix_timestamp().saturating_sub(60),
};
write_agent_runtime_task_record_for_test(&root, &task_record);
let resumed =
resume_game_creator_agent_background_tasks_at(&root).expect("resume pending task");
assert_eq!(resumed.len(), 1);
assert_eq!(resumed[0].state.run_id, "design-pending-recover-run");
assert_eq!(resumed[0].state.status, "running");
assert_eq!(
resumed[0].state.current_action,
"后台任务从上次运行状态恢复"
);
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("pending task plan request");
assert!(plan_request.contains("恢复排队后台任务"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert_eq!(
runtime.last_response.as_deref(),
Some("已恢复并完成排队后台任务。")
);
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recoveredFromStatus\":\"pending\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_recovers_stale_running_before_pending_task() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let (sender, receiver) = mpsc::channel();
let running_plan_json = serde_json::json!({
"thinkingSummary": "先恢复遗留运行任务",
"plan": ["完成遗留 running"],
"actions": [],
"response": "遗留运行任务先完成。"
})
.to_string();
let pending_plan_json = serde_json::json!({
"thinkingSummary": "再继续排队任务",
"plan": ["完成 pending"],
"actions": [],
"response": "后续排队任务已完成。"
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![running_plan_json, pending_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"
}}
}}
}}"#
));
let running_task = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: "design-director".to_string(),
session_id: "agent-session-design-director".to_string(),
run_id: "design-stale-running-run".to_string(),
source: "agent-background-task".to_string(),
task: "先处理遗留运行任务".to_string(),
status: "running".to_string(),
phase: "planning".to_string(),
current_action: "上一进程正在规划".to_string(),
error: None,
updated_at: unix_timestamp().saturating_sub(600),
};
let pending_task = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: "design-director".to_string(),
session_id: "agent-session-design-director".to_string(),
run_id: "design-pending-after-stale-run".to_string(),
source: "agent-background-task".to_string(),
task: "后续排队任务".to_string(),
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待当前 Agent 空闲".to_string(),
error: None,
updated_at: unix_timestamp().saturating_sub(300),
};
write_agent_runtime_task_record_for_test(&root, &running_task);
write_agent_runtime_task_record_for_test(&root, &pending_task);
let resumed =
resume_game_creator_agent_background_tasks_at(&root).expect("resume ordered tasks");
assert_eq!(resumed.len(), 1);
assert_eq!(resumed[0].state.run_id, "design-stale-running-run");
let first_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("first recovered plan request");
assert!(first_request.contains("先处理遗留运行任务"));
let second_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("pending plan request after recovered running");
assert!(second_request.contains("后续排队任务"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.run_id, "design-pending-after-stale-run");
assert_eq!(runtime.status, "idle");
assert_eq!(
runtime.last_response.as_deref(),
Some("后续排队任务已完成。")
);
let runtime_result =
read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result");
assert!(runtime_result
.recent_tasks
.iter()
.any(|task| task.run_id == "design-stale-running-run" && task.status == "completed"));
assert!(runtime_result
.recent_tasks
.iter()
.any(|task| task.run_id == "design-pending-after-stale-run" && task.status == "completed"));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_plan_request_includes_same_agent_continuity_context() {
let root = unique_project_path();
+16
View File
@@ -11913,6 +11913,7 @@ export function App() {
const agentConversationSavingRef = useRef(false);
const agentConversationBackgroundBusyRef = useRef(false);
const agentConversationLoadVersionRef = useRef(0);
const agentRuntimeResumeProjectPathRef = useRef<string | null>(null);
const agentRunHistoryLoadingMoreRef = useRef(false);
const initialProjectOpenedRef = useRef(false);
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
@@ -19520,9 +19521,24 @@ export function App() {
const invoke = resolveTauriInvoke();
if (!invoke || !nextProjectPath) {
setAgentRuntimeById({});
agentRuntimeResumeProjectPathRef.current = null;
return;
}
try {
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
try {
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
'resume_game_creator_agent_runtime_tasks',
{ projectPath: nextProjectPath },
);
for (const runtimeResult of resumedRuntimes) {
rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult));
}
} catch {
// Runtime recovery is best-effort; reading current status below remains authoritative.
}
}
const runtimes = await invoke<AgentRuntimeResult[]>(
'read_game_creator_agent_runtimes',
{ projectPath: nextProjectPath },
@@ -13310,6 +13310,12 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
@@ -13445,6 +13451,9 @@ describe('AI 游戏创作 App 界面边界', () => {
manifest,
};
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [
{
@@ -13499,6 +13508,13 @@ describe('AI 游戏创作 App 界面边界', () => {
'最近任务:pending / queued · 排队补齐世界观拆解',
);
});
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'resume_game_creator_agent_runtime_tasks',
),
).toHaveLength(1);
});
});
it('ignores stale agent conversation reads after switching agents', async () => {
@@ -13523,6 +13539,12 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
@@ -13639,6 +13661,12 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
@@ -13724,6 +13752,9 @@ describe('AI 游戏创作 App 界面边界', () => {
fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
await screen.findByText('正在保存用户消息');
await waitFor(() => {
expect(releaseOldSave).not.toBeNull();
});
fireEvent.click(
within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }),
);
@@ -13760,6 +13791,12 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
@@ -13933,6 +13970,12 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_project_file') {
runReadCount += 1;
return {
@@ -4069,6 +4069,7 @@
- 2026-07-10 调整:Agent Runtime V1 后台工具箱新增受策略保护的 `task.update`。Agent 只能把 `.agent/manifest.json` 中已有 seed task 的状态更新为 `pending``running``waiting-for-confirmation``completed``failed`Runtime 必须复用项目写锁、`task.update` 权限策略和 `.agent/agent.db` 审计记录;策略要求确认或拒绝时不得修改 manifest,不得创建新任务。
- 2026-07-10 调整:Agent Runtime V1 后台工具箱新增只读 `file.list`。Agent 可自行列出项目文件摘要或相对路径范围内的条目,先观察项目结构再决定是否读取具体文件;该工具必须受 `file.list` 项目权限策略保护,observation 只返回项目相对路径、类型和大小,不读取文件内容、不返回本机绝对路径。
- 2026-07-10 调整:Agent Runtime V1 后台工具箱新增受策略保护的 `agent.delegate`。Agent 可把任务投递给另一个 Agent 的独立后台队列,复用目标 Agent 既有锁和 pending drain 语义;策略要求确认或拒绝时不得写目标 Agent 对话、不得启动目标任务,也不得写 `agent.runtime.agent.delegate` 审计记录。
- 2026-07-10 调整:Agent Runtime V1 新增 `resume_game_creator_agent_runtime_tasks` 恢复入口。客户端读取项目 Runtime 时对每个项目路径最多自动尝试一次恢复;恢复命令必须通过 `agent.resume` 自动权限,默认需要确认或被拒绝时不会静默启动。恢复扫描 `.agent/runtime/tasks/<agentId>.jsonl` 里的上一进程遗留 `running``pending` 任务,同一 Agent 同时存在二者时先重接遗留 `running`,再由既有 drain 串行继续 `pending`,并写 `agent.runtime.background_task.recovered` 审计记录。该能力只恢复本地 JSONL 队列到当前 App 进程,不是跨重启常驻 worker,也不承诺恢复已发出的上游 LLM 请求。
- 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。
- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。
- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。
@@ -49,6 +49,7 @@ Agent Runtime 负责:
- 2026-07-10 补充:后台任务工具箱已加入 `project.diff`。Agent 可在 loop 中基于已存在 checkpoint 查看当前项目新增、修改和删除摘要;Runtime 复用 `project.diff` 项目权限策略,策略要求确认或拒绝时不执行 diff,observation 只包含 checkpoint id、三类计数和项目相对路径,不返回本机绝对路径或文件正文。
- 2026-07-10 补充:后台任务工具箱已加入 `agent.run_status`。Agent 可在 loop 中读取自己、目标 Agent 或一组 Agent 的 Runtime 状态摘要,判断同伴是否正在运行、最近任务和最近工具动作;Runtime 复用 `agent.run_status` 项目权限策略,策略要求确认或拒绝时不读取状态,observation 不返回 `.agent/runtime/*` 文件绝对路径。
- 2026-07-10 补充:后台任务工具箱已加入 `agent.delegate`。Agent 可在 loop 中把明确任务投递到另一个 Agent 的独立后台队列,复用目标 Agent 原有锁和 pending drain 语义;同一目标 Agent 串行,不同目标 Agent 可并行。该工具受 `agent.delegate` 策略保护,策略要求确认或拒绝时不会写目标对话、不会启动目标后台任务,也不会写 `agent.runtime.agent.delegate` 审计记录。
- 2026-07-10 补充:Runtime 增加 `resume_game_creator_agent_runtime_tasks` 恢复入口。客户端读取项目 Runtime 时会对每个项目路径最多自动尝试一次恢复;恢复命令必须通过 `agent.resume` 自动权限,默认需要确认或被拒绝时不会静默启动。恢复扫描 `.agent/runtime/tasks/<agentId>.jsonl` 中上一进程遗留的 `running` 或仍为 `pending` 的任务,同一 Agent 同时存在二者时先重接遗留 `running`,再由既有 drain 串行继续 `pending`;恢复动作写 `agent.runtime.background_task.recovered` 审计记录。该能力只是把本地 JSONL 队列重接到当前 App 进程,不是独立常驻 worker,也不承诺恢复已经发出的上游 LLM 请求。
- 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。
- 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents/<group>/<role>.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。
- 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;普通聊天写 `.agent/conversations/project.jsonl`,进入单个 agent 后只写对应 `.agent/conversations/agents/<agentId>.jsonl`,不把原始对话混进项目黑板或角色私有记忆。
@@ -270,7 +271,7 @@ game-project/
- Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。
- 本地项目初始化会创建 `game/``assets/``memory/``memory/agents/``exports/``.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`
- v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion``role``content``agentId``updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/<agentId>.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/<agentId>.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。后台任务的核心 loop 最多 3 轮:每轮把已有 observation 带回 LLM 让 Agent 重新规划,只有 actions 为空且 response 非空时提前收束,否则继续执行白名单工具,跑满后再进入最终回复整理。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read``conversation.read``asset.list``project.index``project.diff``file.list``file.read``agent.run_status`,以及受策略保护的写/运行工具 `memory.write``file.write``command.run_limited``blackboard.write``agent.message``agent.delegate`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作,供状态面板展示最近动作;`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/<agentId>.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/<agentId>.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running``pending` 时,先重接 `running`,再由 drain 继续 `pending`后台任务的核心 loop 最多 3 轮:每轮把已有 observation 带回 LLM 让 Agent 重新规划,只有 actions 为空且 response 非空时提前收束,否则继续执行白名单工具,跑满后再进入最终回复整理。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.background_task.recovered` / `agent.runtime.tool_observation` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read``conversation.read``asset.list``project.index``project.diff``file.list``file.read``agent.run_status`,以及受策略保护的写/运行工具 `memory.write``file.write``command.run_limited``blackboard.write``agent.message``agent.delegate`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作,供状态面板展示最近动作;`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
- 2026-07-10 补充:当前工具箱还开放 `preview.start`,审计记录类型为 `agent.runtime.preview.start`;该工具不会打开任意 URL,只启动当前授权项目的 `127.0.0.1` 本地预览,并和 Tauri 用户命令共用同一个 `PreviewRegistry`
- 2026-07-10 补充:当前工具箱还开放 `canvas.asset_generate`,审计记录类型为 `agent.runtime.canvas.asset_generate`;该工具只通过配置好的 External Editor API 生成并回流素材,不暴露任意上传 / 任意网络请求能力。
- 2026-07-10 补充:当前工具箱还开放 `task.list`;该工具只读取 manifest 任务图、状态、依赖、产物和 `readyTaskIds`,并受 `task.list` 项目权限策略保护。