支持调度Ready任务给Agent
新增 agent.schedule_ready 命令,将 manifest ready task 投递到对应 Agent 后台队列。 为调度入口接入默认确认权限、Tauri handler 和 native-only 门禁。 补充 Runtime 测试覆盖 ready 任务调度和默认策略拦截。 同步 Agent Runtime 技术方案和项目决策记录。
This commit is contained in:
@@ -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.update:ok · 任务 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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user