补齐Ready任务调度入口

开发模式 Agent 状态栏新增调度 Ready 控制

调度前复用项目权限确认并合并返回 Runtime 状态

补充前端测试覆盖开发入口和普通用户隐藏

更新 Runtime V1 文档并移除已调用命令白名单例外
This commit is contained in:
AIGameCreator App
2026-07-10 07:06:58 +08:00
parent 2c5635a35a
commit 436d67ae24
5 changed files with 272 additions and 1 deletions
@@ -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',
+66
View File
@@ -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<ProjectPermissionPolicyView>(
'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<AgentRuntimeResult[]>(
'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
</button>
{devMode ? (
<button
type="button"
disabled={!localProject}
onClick={() => void scheduleReadyAgentTasks()}
>
调度 Ready
</button>
) : null}
</div>
</header>
<div className="agent-status-list" aria-label="Agent 状态列表">
@@ -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<string, unknown>) => {
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<string, unknown>) => {
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',
@@ -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`。
@@ -50,6 +50,7 @@ Agent Runtime 负责:
- 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/<agentId>.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/<agentId>.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。