diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 311c68009..d42276459 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -55,7 +55,6 @@ const rustSharedContractSource = fs.readFileSync( const allowedUncalledTauriCommands = [ 'open_game_creator_launcher_window', 'open_game_creator_workspace_window', - 'schedule_game_creator_agent_ready_tasks', ]; const sourceExtensions = new Set([ '.json', diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index eb95e2596..593122c0e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -20326,6 +20326,63 @@ export function App() { } } + async function scheduleReadyAgentTasks(skipPolicyConfirm = false) { + const invoke = resolveTauriInvoke(); + if (!invoke) { + setAgentRunStatus('需要在 Tauri App 内运行'); + return; + } + const nextProjectPath = resolveChatProjectPath(localProject); + if (!nextProjectPath) { + setAgentRunStatus('请先初始化本地项目'); + return; + } + try { + if (!skipPolicyConfirm) { + const policyView = await invoke( + 'read_project_permission_policy', + { projectPath: nextProjectPath }, + ); + if (policyView.policy.confirmCommands.includes('agent.schedule_ready')) { + requestProjectPolicyConfirmation( + 'agent.schedule_ready', + nextProjectPath, + `调度 ${nextProjectPath} 中已 Ready 的 Agent 任务`, + () => void scheduleReadyAgentTasks(true), + ); + setAgentRunStatus('等待确认调度 Ready 任务'); + return; + } + } + setAgentRunStatus('正在调度 Ready 任务'); + const scheduledRuntimes = await invoke( + 'schedule_game_creator_agent_ready_tasks', + { projectPath: nextProjectPath, limit: 16 }, + ); + for (const runtimeResult of scheduledRuntimes) { + rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult)); + } + await refreshManifest(nextProjectPath); + await refreshAgentRuntimes(nextProjectPath); + const message = + scheduledRuntimes.length > 0 + ? `已调度 ${scheduledRuntimes.length} 个 Ready 任务。` + : '没有可调度的 Ready 任务。'; + setAgentRunStatus(message); + setMessages((current) => [ + ...current, + { role: 'assistant', text: message }, + ]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setAgentRunStatus(message); + setMessages((current) => [ + ...current, + { role: 'assistant', text: message }, + ]); + } + } + const agentStatusCards = deriveAgentStatusCards( manifest, agentRunTrace ?? agentRunHistory[0]?.trace ?? null, @@ -21189,6 +21246,15 @@ export function App() { > 刷新 Agent + {devMode ? ( + + ) : null}
diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 6d91fd5cd..434123083 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -13713,6 +13713,210 @@ describe('AI 游戏创作 App 界面边界', () => { }); }); + it('lets the developer schedule ready manifest tasks into agent runtimes', async () => { + const manifest = createGameCreationAppManifest( + 'local-project-draft', + '未命名游戏原型', + ); + const runtimeTask = { + schemaVersion: 'game-creator-agent-runtime-task.v1', + agentId: 'design-director', + taskId: 'design-director', + sessionId: 'agent-session-design-director', + runId: 'ready-design-director-1', + source: 'agent-ready-task-scheduler', + task: '执行 Ready 任务:拆解创作方向', + status: 'running', + phase: 'planning', + currentAction: '准备读取项目上下文', + error: null, + updatedAt: 20, + }; + const runtimeState = { + schemaVersion: 'game-creator-agent-runtime.v1', + agentId: 'design-director', + taskId: 'design-director', + sessionId: 'agent-session-design-director', + runId: 'ready-design-director-1', + source: 'agent-ready-task-scheduler', + status: 'running', + phase: 'planning', + currentTask: '执行 Ready 任务:拆解创作方向', + currentGoal: '把 ready 任务投递给对应 Agent', + currentAction: '准备读取项目上下文', + waitingOn: 'Agent 输出计划或回复', + loopIteration: 1, + maxLoopIterations: 3, + toolActionBudget: 3, + plan: ['读取项目上下文'], + planSteps: [], + activePlanStepIndex: null, + observations: [], + taskQueue: { + total: 1, + pending: 0, + running: 1, + completed: 0, + failed: 0, + latestRunId: 'ready-design-director-1', + updatedAt: 20, + }, + allowedTools: ['conversation.read'], + lastResponse: null, + error: null, + updatedAt: 20, + }; + const runtimeResult = { + state: runtimeState, + sessionPath: + '/tmp/authorized-game/.agent/runtime/agents/design-director.json', + eventPath: + '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', + taskPath: + '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', + taskQueue: runtimeState.taskQueue, + recentEvents: [], + recentTasks: [runtimeTask], + }; + let scheduled = false; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'append_local_permission_log') { + return {}; + } + if (command === 'init_local_game_project') { + const projectPath = String(args?.projectPath ?? ''); + return { + projectPath, + manifestPath: `${projectPath}/.agent/manifest.json`, + manifest, + }; + } + 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 scheduled ? [runtimeResult] : []; + } + if (command === 'schedule_game_creator_agent_ready_tasks') { + scheduled = true; + return [runtimeResult]; + } + if (command === 'append_local_conversation_message') { + return { + path: '/tmp/authorized-game/.agent/conversations/project.jsonl', + agentId: null, + messages: [], + }; + } + if (command === 'get_local_game_manifest') { + return manifest; + } + if (command === 'read_local_project_file') { + throw new Error( + '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', + ); + } + if (command === 'list_local_project_files') { + return { projectPath: String(args?.projectPath ?? ''), files: [] }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); + + await screen.findByText('想做什么游戏?'); + const scheduleButton = await screen.findByRole('button', { + name: '调度 Ready', + }); + await waitFor(() => { + expect((scheduleButton as HTMLButtonElement).disabled).toBe(false); + }); + + fireEvent.click(scheduleButton); + + await waitFor(() => { + expect( + invoke.mock.calls.some( + ([command, args]) => + command === 'schedule_game_creator_agent_ready_tasks' && + args?.projectPath === '/tmp/authorized-game' && + args?.limit === 16, + ), + ).toBe(true); + }); + expect(await screen.findAllByText('已调度 1 个 Ready 任务。')).toHaveLength( + 2, + ); + const agentStatusList = screen.getByLabelText('Agent 状态列表'); + await waitFor(() => { + const designCard = within(agentStatusList).getByRole('button', { + name: /拆解创作方向/, + }); + expect(designCard.textContent).toContain( + 'Runtime:running / planning', + ); + expect(designCard.textContent).toContain( + '当前目标:把 ready 任务投递给对应 Agent', + ); + expect(designCard.textContent).toContain( + '最近任务:running / planning · 执行 Ready 任务:拆解创作方向', + ); + }); + }); + + it('keeps ready task scheduling out of the normal user window', async () => { + const manifest = createGameCreationAppManifest( + 'local-project-draft', + '未命名游戏原型', + ); + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'append_local_permission_log') { + return {}; + } + if (command === 'init_local_game_project') { + const projectPath = String(args?.projectPath ?? ''); + return { + projectPath, + manifestPath: `${projectPath}/.agent/manifest.json`, + manifest, + }; + } + if (command === 'resume_game_creator_agent_runtime_tasks') { + return []; + } + if (command === 'read_game_creator_agent_runtimes') { + return []; + } + if (command === 'read_local_project_file') { + throw new Error( + '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', + ); + } + if (command === 'list_local_project_files') { + return { projectPath: String(args?.projectPath ?? ''), files: [] }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); + + await screen.findByText('想做什么游戏?'); + + expect(screen.queryByRole('button', { name: '调度 Ready' })).toBeNull(); + expect( + invoke.mock.calls.some( + ([command]) => command === 'schedule_game_creator_agent_ready_tasks', + ), + ).toBe(false); + }); + it('ignores stale agent conversation reads after switching agents', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index dd6c83026..7450719fb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -29,6 +29,7 @@ - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `project.diff`,让 Agent 可基于已存在 checkpoint 观察本地项目新增、修改和删除摘要。该工具复用 `project.diff` 项目权限策略,策略要求确认或拒绝时不会执行 diff;observation 只包含 checkpoint id、三类计数和项目相对路径,不返回本机绝对路径或文件正文。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.run_status`,让 Agent 可在 loop 中读取自己、目标 Agent 或一组 Agent 的 Runtime 状态摘要,用于判断同伴是否正在运行、最近任务和最近工具动作。该工具复用 `agent.run_status` 项目权限策略,策略要求确认或拒绝时不会读取状态;observation 只包含 agentId、status、phase、runId、当前任务、当前动作、下一步、计划摘要、最近任务、最近工具和错误摘要,不返回 `.agent/runtime/*` 文件绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.delegate`,让 Agent 可把明确子任务投递到另一个 Agent 的独立后台队列。该工具复用目标 Agent 既有锁和 pending drain 语义,不创建平行 runtime;同一目标 Agent 串行,不同 Agent 可并行。该工具使用独立 `agent.delegate` 权限策略,策略要求确认或拒绝时不会写目标对话、不会启动目标后台任务,也不会写 `agent.runtime.agent.delegate` 审计记录。 +- 2026-07-10 补充:主窗口 Agent 状态栏在开发模式新增“调度 Ready”控制,用来显式调用 `schedule_game_creator_agent_ready_tasks`。该控制先读取项目策略,命中 `agent.schedule_ready` confirm 时走现有确认弹窗,确认后才把 manifest ready task 投递到对应 Agent Runtime;普通用户窗口不展示这个开发控制。 - 影响范围:`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`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 235bfd0dc..dea9163f0 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -50,6 +50,7 @@ Agent Runtime 负责: - 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`,开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态卡用同一套前端归一化逻辑合并状态;该事件只做实时 UI 通知,`.agent/runtime/agents`、`events` 和 `tasks` 仍是重开项目后的事实源。 - 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。 - 2026-07-10 补充:Runtime 新增 `agent.schedule_ready` 调度入口。开发构建可在权限确认后扫描 manifest ready task,把依赖已完成且仍为 `pending` 的任务标成 `running`,并按 taskId 投递到对应 Agent 的既有后台队列;source 固定为 `agent-ready-task-scheduler`,审计记录写 `agent.runtime.ready_task.scheduled`。该入口只把 manifest ready task 接入现有 per-agent 队列、锁、JSONL、LLM loop、工具策略和事件流,不新增独立 worker,也不会在默认确认策略下静默启动。 +- 2026-07-10 补充:主窗口 Agent 状态栏的“调度 Ready”只在开发模式显示。点击后复用项目策略确认弹窗,确认通过才调用 `schedule_game_creator_agent_ready_tasks`,并把返回的 Runtime 合并回 Agent 状态卡;普通用户窗口继续只展示状态和单 Agent 对话入口,不直接暴露 ready-task 调度按钮。 - 2026-07-10 补充:单 Agent 聊天和后台 planning prompt 会读取同一个 Agent 的 Runtime 连续上下文,把本 Agent 最近 status / phase / runId / 当前任务 / 下一步、最近回复、计划、观察、最近 3 条工具动作、最近事件、最近 3 条任务记录和工具策略摘要带入下一轮推理;上下文按规范 taskId 隔离,不读取其他 Agent 的 runtime 文件,并在进入 prompt 前过滤密钥和本机绝对路径。新后台 run 启动时会继承本 Agent 上次 `recentToolCalls` 和 `lastResponse`,让多轮任务不丢失结构化行动证据。 - 2026-07-10 补充:后台任务工具箱已加入 `preview.start`。Agent 可在 loop 中自行请求启动当前项目的本地 HTTP 预览;Runtime 会复用 `preview.start` 策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑,并把 `agent.runtime.preview.start` 写入 `.agent/agent.db`。该 observation 只向 LLM 返回 localhost URL 与端口,不返回用户项目绝对路径。 - 2026-07-10 补充:后台任务工具箱已加入 `canvas.asset_generate`。Agent 可在 loop 中自行给出素材 prompt,通过 AppData / Tauri 配置里的 `editorApi` 调用 External Editor API 生成首版美术素材、下载到 `assets/canvas-generated/` 并登记 manifest;Runtime 复用 `canvas.asset_generate` 策略和项目写锁,并写入 `agent.runtime.canvas.asset_generate` 审计记录。API Key 不进入 observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。