补齐Ready任务调度入口
开发模式 Agent 状态栏新增调度 Ready 控制 调度前复用项目权限确认并合并返回 Runtime 状态 补充前端测试覆盖开发入口和普通用户隐藏 更新 Runtime V1 文档并移除已调用命令白名单例外
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user