支持调度Ready任务给Agent

新增 agent.schedule_ready 命令,将 manifest ready task 投递到对应 Agent 后台队列。

为调度入口接入默认确认权限、Tauri handler 和 native-only 门禁。

补充 Runtime 测试覆盖 ready 任务调度和默认策略拦截。

同步 Agent Runtime 技术方案和项目决策记录。
This commit is contained in:
AIGameCreator App
2026-07-10 06:58:46 +08:00
parent 1ccda076dc
commit 2c5635a35a
10 changed files with 258 additions and 7 deletions
@@ -55,6 +55,7 @@ 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',
@@ -388,6 +388,26 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
run_id: &str,
confirmed_command_id: Option<&str>,
confirmation_note: &str,
) -> Result<(AgentRuntimeResult, String), String> {
start_game_creator_agent_background_task_with_source_at(
root,
agent_id,
task,
run_id,
"agent-background-task",
confirmed_command_id,
confirmation_note,
)
}
fn start_game_creator_agent_background_task_with_source_at(
root: &Path,
agent_id: &str,
task: &str,
run_id: &str,
source: &str,
confirmed_command_id: Option<&str>,
confirmation_note: &str,
) -> Result<(AgentRuntimeResult, String), String> {
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
validate_project_root(root)?;
@@ -395,9 +415,14 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
if task.is_empty() {
return Err("Agent 后台任务不能为空".to_string());
}
let source = if source.trim().is_empty() {
"agent-background-task"
} else {
source.trim()
};
let run_id = unique_game_creator_agent_runtime_run_id(root, &agent_id, run_id)?;
let pending_task =
append_game_creator_agent_runtime_pending_task(root, &agent_id, task, &run_id)?;
append_game_creator_agent_runtime_pending_task(root, &agent_id, task, &run_id, source)?;
append_local_conversation_message_at(
root,
Some(&agent_id),
@@ -441,7 +466,7 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
&agent_id,
task,
&run_id,
"agent-background-task",
source,
"后台任务已投递",
game_creator_agent_background_task_default_plan(),
)?;
@@ -465,6 +490,108 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
Ok((result, run_id))
}
pub(crate) fn schedule_game_creator_agent_ready_tasks_at(
root: &Path,
limit: usize,
) -> Result<Vec<AgentRuntimeResult>, String> {
validate_project_root(root)?;
let limit = if limit == 0 { 16 } else { limit.min(16) };
let ready_tasks = {
let _lock = acquire_project_write_lock(root, "agent.schedule_ready")?;
let manifest = read_manifest_for_project(root)?;
let ready_task_ids = ready_task_ids_for_tasks(&manifest.tasks);
let mut ready_tasks = Vec::new();
for task_id in ready_task_ids.into_iter().take(limit) {
let Some(task) = manifest.tasks.iter().find(|task| task.id == task_id) else {
continue;
};
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Running)?;
ready_tasks.push(task.clone());
}
ready_tasks
};
let mut results = Vec::new();
for task in ready_tasks {
let run_id = format!("ready-{}-{}", task.id, unix_timestamp_nanos());
let task_text = render_manifest_ready_task_background_prompt(&task);
match start_game_creator_agent_background_task_with_source_at(
root,
&task.id,
&task_text,
&run_id,
"agent-ready-task-scheduler",
None,
"",
) {
Ok((result, actual_run_id)) => {
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.ready_task.scheduled",
"agentId": result.state.agent_id,
"taskId": task.id.clone(),
"runId": actual_run_id,
"source": result.state.source,
"title": task.title.clone(),
"group": agent_runtime_task_group_label(&task.group),
"role": task.role.clone(),
}),
)?;
results.push(result);
}
Err(error) => {
let _ = update_manifest_task_status_at(
root,
&task.id,
GameCreationAppTaskStatus::Failed,
);
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.ready_task.schedule_failed",
"agentId": task.id.clone(),
"taskId": task.id.clone(),
"source": "agent-ready-task-scheduler",
"title": task.title.clone(),
"error": sanitize_agent_runtime_text(&error, 240),
}),
);
return Err(error);
}
}
}
Ok(results)
}
fn render_manifest_ready_task_background_prompt(task: &GameCreationAppTaskState) -> String {
let dependencies = if task.dependencies.is_empty() {
"".to_string()
} else {
task.dependencies.join(", ")
};
let artifacts = if task.artifacts.is_empty() {
"未指定".to_string()
} else {
task.artifacts.join(", ")
};
let acceptance = if task.acceptance_criteria.is_empty() {
"未指定".to_string()
} else {
task.acceptance_criteria.join("")
};
format!(
"处理 manifest ready 任务:{}\n\n任务 ID{}\n专业组:{}\n角色:{}\n依赖:{}\n预期产物:{}\n验收标准:{}\n\n请按你的 Agent 职责自主规划、调用可用工具、记录观察,并在完成或阻塞时更新任务状态。",
task.title,
task.id,
agent_runtime_task_group_label(&task.group),
task.role,
dependencies,
artifacts,
acceptance
)
}
pub(crate) fn cancel_game_creator_agent_runtime_task_at(
root: &Path,
agent_id: &str,
@@ -4254,6 +4381,7 @@ fn append_game_creator_agent_runtime_pending_task(
agent_id: &str,
task: &str,
run_id: &str,
source: &str,
) -> Result<AgentRuntimeTaskRecord, String> {
let record = AgentRuntimeTaskRecord {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
@@ -4261,7 +4389,7 @@ fn append_game_creator_agent_runtime_pending_task(
task_id: agent_id.to_string(),
session_id: format!("agent-session-{agent_id}"),
run_id: run_id.to_string(),
source: "agent-background-task".to_string(),
source: source.trim().to_string(),
task: sanitize_agent_runtime_text(task, 180),
status: "pending".to_string(),
phase: "queued".to_string(),
@@ -479,6 +479,19 @@ pub(crate) fn resume_game_creator_agent_runtime_tasks(
resume_game_creator_agent_background_tasks_at(root)
}
#[tauri::command]
pub(crate) fn schedule_game_creator_agent_ready_tasks(
project_path: String,
limit: usize,
) -> 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.schedule_ready")?;
schedule_game_creator_agent_ready_tasks_at(root, limit)
}
#[tauri::command]
pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
check_game_creator_llm_config_from_config()
@@ -1206,6 +1206,7 @@ fn main() {
read_game_creator_agent_runtime,
read_game_creator_agent_runtimes,
resume_game_creator_agent_runtime_tasks,
schedule_game_creator_agent_ready_tasks,
check_game_creator_llm_config,
read_game_creator_app_config,
write_game_creator_app_config,
@@ -3918,6 +3918,107 @@ async fn background_agent_runtime_task_update_respects_project_policy() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_can_schedule_ready_manifest_tasks() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow ready task scheduler");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "开始处理 ready manifest 任务",
"plan": ["读取任务要求", "标记任务完成"],
"actions": [
{
"tool": "task.update",
"reason": "ready 任务已经完成拆解",
"input": { "taskId": "design-director", "status": "completed" }
}
],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![plan_json, "已完成 ready manifest 任务。".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"
}}
}}
}}"#
));
let scheduled =
schedule_game_creator_agent_ready_tasks_at(&root, 0).expect("schedule ready tasks");
assert_eq!(scheduled.len(), 1);
assert_eq!(scheduled[0].state.agent_id, "design-director");
assert_eq!(scheduled[0].state.source, "agent-ready-task-scheduler");
assert!(scheduled[0]
.state
.current_task
.contains("处理 manifest ready 任务:拆解创作方向"));
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("plan llm request");
assert!(plan_request.contains("处理 manifest ready 任务:拆解创作方向"));
assert!(plan_request.contains("task.update"));
let final_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("final reply llm request");
assert!(final_request.contains("任务 design-director 已更新为 completed"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.source, "agent-ready-task-scheduler");
assert!(runtime
.observations
.iter()
.any(|item| item.contains("task.updateok · 任务 design-director 已更新为 completed")));
let manifest_after: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
.expect("manifest json");
assert_task_status(&manifest_after, "design-director", "completed");
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"agent.runtime.ready_task.scheduled\""));
assert!(agent_db.contains("\"source\":\"agent-ready-task-scheduler\""));
fs::remove_dir_all(root).ok();
}
#[test]
fn schedule_ready_manifest_tasks_command_requires_auto_policy() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let error = schedule_game_creator_agent_ready_tasks(root.to_string_lossy().into_owned(), 0)
.expect_err("default policy should require confirmation");
assert!(error.contains("项目权限策略要求用户确认:agent.schedule_ready"));
let manifest: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
.expect("manifest json");
assert_task_status(&manifest, "design-director", "pending");
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(!agent_db.contains("\"recordType\":\"agent.runtime.ready_task.scheduled\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_can_run_limited_static_smoke() {
let root = unique_project_path();
+3 -2
View File
@@ -11086,6 +11086,7 @@ function isProjectPolicyConfirmableCommandId(value: string) {
'agent.retry',
'agent.resume',
'agent.delegate',
'agent.schedule_ready',
'agent.audit',
'agent.trace_read',
'preview.status',
@@ -15309,7 +15310,7 @@ export function App() {
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -15372,7 +15373,7 @@ export function App() {
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -4068,6 +4068,7 @@
- 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db``.agent/conversations/**/*.jsonl``.agent/runtime/events/*.jsonl``.agent/runtime/tasks/*.jsonl``.agent/activity.jsonl``.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。
- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations/<agentId>/<runId>/<commandId>.json` 票据并重新入队;工具权限 gate 在 deny 之后、confirm 阶段消费该票据,只放行对应 Agent/run/command 一次。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`
- 2026-07-10 调整:Agent Runtime 工具箱新增 `task.create`,用于让 Agent 把目标拆成新的 manifest 任务,而不只能更新 seed task。该工具默认 `confirm` 权限,写入前要求 taskId 唯一、依赖指向已有任务、列表长度受限,并写 `agent.runtime.task.create` 审计;策略要求确认或拒绝时不修改 `.agent/manifest.json`
- 2026-07-10 调整:Agent Runtime 新增 `agent.schedule_ready` 调度入口,默认 `confirm` 权限。命令会扫描 `.agent/manifest.json` 中依赖已完成且仍为 `pending` 的 ready task,先把任务标成 `running`,再用 taskId 作为 Agent id 投递到既有后台队列,source 记为 `agent-ready-task-scheduler`,并写 `agent.runtime.ready_task.scheduled` 审计;后续执行仍走原 per-agent 锁、任务 JSONL、LLM loop、工具策略和事件流,不新增独立 worker。默认确认策略下该命令不会静默调度。
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。
- 2026-07-10 调整:Agent Runtime state 新增 `currentGoal``waitingOn``currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。
- 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。
File diff suppressed because one or more lines are too long
@@ -33,6 +33,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'agent.retry', permission: 'confirm' },
{ id: 'agent.resume', permission: 'confirm' },
{ id: 'agent.delegate', permission: 'confirm' },
{ id: 'agent.schedule_ready', permission: 'confirm' },
{ id: 'agent.capabilities', permission: 'auto' },
{ id: 'agent.audit', permission: 'auto' },
{ id: 'llm.config_check', permission: 'auto' },
@@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor {
pub permission: GameCreationAppPermission,
}
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 47] = [
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 48] = [
command("help.show", GameCreationAppPermission::Auto),
command("project.create", GameCreationAppPermission::Confirm),
command("project.status", GameCreationAppPermission::Auto),
@@ -42,6 +42,7 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 47] = [
command("agent.retry", GameCreationAppPermission::Confirm),
command("agent.resume", GameCreationAppPermission::Confirm),
command("agent.delegate", GameCreationAppPermission::Confirm),
command("agent.schedule_ready", GameCreationAppPermission::Confirm),
command("agent.capabilities", GameCreationAppPermission::Auto),
command("agent.audit", GameCreationAppPermission::Auto),
command("llm.config_check", GameCreationAppPermission::Auto),
@@ -693,6 +694,7 @@ mod tests {
"agent.retry",
"agent.resume",
"agent.delegate",
"agent.schedule_ready",
] {
let lifecycle_command = GAME_CREATION_APP_COMMANDS
.iter()