Merge remote-tracking branch 'web/master' into feat/pixel_art2
This commit is contained in:
@@ -1609,6 +1609,24 @@ mod tests {
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "manifest-dag-policy", "测试正式任务图等待")
|
||||
.expect("init manifest DAG policy project");
|
||||
let session_id = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve manifest DAG Supervisor session");
|
||||
append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&session_id,
|
||||
"测试正式任务图等待",
|
||||
"manifest-dag-policy-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue trusted autonomous root task");
|
||||
assert!(!autonomous_manifest_dag_in_progress_at(&root).expect("read pending DAG"));
|
||||
|
||||
update_manifest_task_status_at(
|
||||
|
||||
@@ -937,14 +937,16 @@ async fn missing_completed_visual_asset_fails_same_child_without_retry() {
|
||||
"missing visual output must fail the current logical task"
|
||||
);
|
||||
|
||||
assert!(schedule_autonomous_game_build_ready_tasks_at(
|
||||
let scheduled_after_failure = schedule_autonomous_game_build_ready_tasks_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
PARENT_RUN_ID,
|
||||
1,
|
||||
)
|
||||
.expect("repeat scheduling after visual failure")
|
||||
.is_empty());
|
||||
.expect("schedule other ready work after visual failure");
|
||||
assert!(scheduled_after_failure
|
||||
.iter()
|
||||
.all(|scheduled| scheduled.state.agent_id != CHILD_ID));
|
||||
let records = read_all_game_creator_agent_runtime_tasks(&game_creator_agent_runtime_task_path(
|
||||
&root, CHILD_ID,
|
||||
))
|
||||
|
||||
+94
-10
@@ -1,6 +1,74 @@
|
||||
use super::*;
|
||||
|
||||
async fn run_after_pending_stack_boundary<T>(
|
||||
future: std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'static>>,
|
||||
) -> T
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
// Debug builds give pending execution, the background main loop, and queue draining large
|
||||
// poll frames. The boxed future keeps that large frame out of its caller before a joined child
|
||||
// task gives it an independent poll boundary. JoinSet still aborts the child if its parent
|
||||
// continuation is dropped.
|
||||
let mut tasks = tokio::task::JoinSet::new();
|
||||
tasks.spawn(future);
|
||||
match tasks
|
||||
.join_next()
|
||||
.await
|
||||
.expect("pending continuation task must exist")
|
||||
{
|
||||
Ok(output) => output,
|
||||
Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
|
||||
Err(error) => panic!("pending continuation task was cancelled: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_game_creator_agent_background_task_after_pending_stack_boundary(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
task: String,
|
||||
runtime: AgentRuntimeState,
|
||||
continuation: AgentRuntimeContinuationContext,
|
||||
) -> AgentBackgroundTaskOutcome {
|
||||
run_after_pending_stack_boundary(Box::pin(async move {
|
||||
run_game_creator_agent_background_task_with_context(
|
||||
root,
|
||||
agent_id,
|
||||
task,
|
||||
runtime,
|
||||
continuation,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
) {
|
||||
run_after_pending_stack_boundary(Box::pin(async move {
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
pending: AgentRuntimePendingToolAction,
|
||||
runtime: AgentRuntimeState,
|
||||
) {
|
||||
run_after_pending_stack_boundary(Box::pin(async move {
|
||||
continue_game_creator_agent_pending_tool_action_within_stack_boundary(
|
||||
root, agent_id, pending, runtime,
|
||||
)
|
||||
.await;
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
mut pending: AgentRuntimePendingToolAction,
|
||||
@@ -61,7 +129,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
usize::try_from(batch.loop_iteration.saturating_sub(1)).unwrap_or(usize::MAX);
|
||||
continuation.context_stalled = false;
|
||||
continuation.applied_steer_cursor = batch.planned_steer_cursor;
|
||||
let outcome = run_game_creator_agent_background_task_with_context(
|
||||
let outcome = run_game_creator_agent_background_task_after_pending_stack_boundary(
|
||||
root.clone(),
|
||||
agent_id.clone(),
|
||||
pending.task.clone(),
|
||||
@@ -70,7 +138,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
)
|
||||
.await;
|
||||
if matches!(outcome, AgentBackgroundTaskOutcome::Finished) {
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(
|
||||
root, agent_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -79,7 +150,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
if !has_persisted_terminal_observation
|
||||
&& stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime)
|
||||
{
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if let Err(error) = validate_agent_runtime_pending_context(&root, &runtime, &pending) {
|
||||
@@ -341,14 +413,18 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
&pending,
|
||||
&observation,
|
||||
) {
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(
|
||||
root, agent_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if observation.is_waiting_for_confirmation()
|
||||
&& stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime)
|
||||
{
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if observation.is_waiting_for_confirmation() {
|
||||
@@ -525,7 +601,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
}),
|
||||
);
|
||||
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if !auto_execution {
|
||||
@@ -702,7 +779,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
runtime,
|
||||
&format!("恢复 Agent Runtime context bundle 失败:{error}"),
|
||||
);
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(
|
||||
root, agent_id,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -753,7 +833,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
runtime,
|
||||
&format!("持久化 Agent Runtime context bundle 失败:{error}"),
|
||||
);
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(
|
||||
root, agent_id,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -787,7 +870,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
return;
|
||||
}
|
||||
}
|
||||
let outcome = run_game_creator_agent_background_task_with_context(
|
||||
let outcome = run_game_creator_agent_background_task_after_pending_stack_boundary(
|
||||
root.clone(),
|
||||
agent_id.clone(),
|
||||
pending.task.clone(),
|
||||
@@ -796,7 +879,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
)
|
||||
.await;
|
||||
if matches!(outcome, AgentBackgroundTaskOutcome::Finished) {
|
||||
drain_next_game_creator_agent_background_tasks(root, agent_id).await;
|
||||
drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+66
-136
@@ -300,55 +300,36 @@ async fn supervisor_autonomous_durable_batch_rejects_invalid_responsibilities_wi
|
||||
let AgentRuntimeProviderActionBatchPreparation::Ready(valid_batch) = preparation else {
|
||||
panic!("valid autonomous responsibilities must form a ready durable batch");
|
||||
};
|
||||
assert_eq!(valid_batch.actions.len(), 2);
|
||||
assert_eq!(valid_batch.actions.len(), 3);
|
||||
assert!(valid_batch.collaboration_contract.is_some());
|
||||
let valid_batch_id = valid_batch.batch_id.clone();
|
||||
let mut quality_not_read_only = autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"直接修改 game/index.html,修复试玩阻塞并重新验证。",
|
||||
&[],
|
||||
);
|
||||
quality_not_read_only[1].input["acceptanceCriteria"] = serde_json::json!([
|
||||
"直接写入 game/index.html 修复试玩问题",
|
||||
"修改后执行静态验证并交付新 revision"
|
||||
]);
|
||||
let mut design_not_read_only = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
design_not_read_only[0].input["task"] = serde_json::json!("直接修改项目并完成首轮策划实现。");
|
||||
design_not_read_only[0].input["acceptanceCriteria"] = serde_json::json!(["直接修改项目文件"]);
|
||||
let mut design_with_artifacts = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
design_with_artifacts[0].input["expectedArtifacts"] =
|
||||
serde_json::json!(["game/game_design.md"]);
|
||||
let mut art_missing_spec = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
art_missing_spec[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]);
|
||||
let mut code_with_artifacts = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
code_with_artifacts[2].input["expectedArtifacts"] = serde_json::json!(["game/index.html"]);
|
||||
|
||||
let invalid_cases = vec![
|
||||
(
|
||||
"code-missing-game-index",
|
||||
"game/index.html",
|
||||
autonomous_initial_responsibility_actions_for_test(
|
||||
"创建可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/main.js"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&[],
|
||||
),
|
||||
"design-not-read-only",
|
||||
"design-director",
|
||||
design_not_read_only,
|
||||
),
|
||||
(
|
||||
"code-read-only",
|
||||
"code-prototype",
|
||||
autonomous_initial_responsibility_actions_for_test(
|
||||
"只读检查 game/index.html,不要修改任何项目文件。",
|
||||
&["game/index.html"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&[],
|
||||
),
|
||||
),
|
||||
(
|
||||
"quality-not-read-only",
|
||||
"quality-review",
|
||||
quality_not_read_only,
|
||||
),
|
||||
(
|
||||
"quality-with-artifacts",
|
||||
"design-with-artifacts",
|
||||
"expectedArtifacts",
|
||||
autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&["game/index.html"],
|
||||
),
|
||||
design_with_artifacts,
|
||||
),
|
||||
("art-missing-spec", "assets/art-spec.png", art_missing_spec),
|
||||
(
|
||||
"code-with-artifacts",
|
||||
"expectedArtifacts",
|
||||
code_with_artifacts,
|
||||
),
|
||||
];
|
||||
for (case_name, expected_error, actions) in invalid_cases {
|
||||
@@ -389,12 +370,7 @@ async fn supervisor_autonomous_durable_batch_rejects_invalid_responsibilities_wi
|
||||
rewrite_autonomous_responsibility_batch_actions_for_test(
|
||||
&root,
|
||||
&mut legacy_v2_batch,
|
||||
autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"直接修改 game/index.html,修复试玩阻塞并重新验证。",
|
||||
&[],
|
||||
),
|
||||
valid_autonomous_initial_responsibility_actions_for_test(),
|
||||
);
|
||||
let legacy_v2_schema = "game-creator-provider-action-batch.v2";
|
||||
legacy_v2_batch.schema_version = legacy_v2_schema.to_string();
|
||||
@@ -600,68 +576,37 @@ async fn autonomous_game_build_repairs_supervisor_failed_playtest_stall_into_mut
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
fn autonomous_initial_responsibility_actions_for_test(
|
||||
code_task: &str,
|
||||
code_artifacts: &[&str],
|
||||
quality_task: &str,
|
||||
quality_artifacts: &[&str],
|
||||
) -> Vec<AgentRuntimeToolAction> {
|
||||
fn valid_autonomous_initial_responsibility_actions_for_test() -> Vec<AgentRuntimeToolAction> {
|
||||
vec![
|
||||
AgentRuntimeToolAction {
|
||||
tool: "agent.delegate".to_string(),
|
||||
reason: Some("委派程序 Agent 形成可玩入口".to_string()),
|
||||
input: serde_json::json!({
|
||||
"agentId": "code-prototype",
|
||||
"task": code_task,
|
||||
"acceptanceCriteria": [
|
||||
"game/index.html 必须形成可直接试玩的完整入口",
|
||||
"程序交付必须完成当前 revision 的静态验证"
|
||||
],
|
||||
"expectedArtifacts": code_artifacts,
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
},
|
||||
AgentRuntimeToolAction {
|
||||
tool: "agent.delegate".to_string(),
|
||||
reason: Some("委派质量 Agent 独立只读验收".to_string()),
|
||||
input: serde_json::json!({
|
||||
"agentId": "quality-review",
|
||||
"task": quality_task,
|
||||
"acceptanceCriteria": [
|
||||
"只读核对可玩性、交互闭环和阻塞问题",
|
||||
"返回可追溯的验收结论,不修改项目文件"
|
||||
],
|
||||
"expectedArtifacts": quality_artifacts,
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
},
|
||||
autonomous_initial_leader_responsibility_action_for_test("design-director", &[]),
|
||||
autonomous_initial_leader_responsibility_action_for_test(
|
||||
"art-director",
|
||||
&["assets/art-spec.png"],
|
||||
),
|
||||
autonomous_initial_leader_responsibility_action_for_test("code-director", &[]),
|
||||
]
|
||||
}
|
||||
|
||||
fn valid_autonomous_initial_responsibility_actions_for_test() -> Vec<AgentRuntimeToolAction> {
|
||||
autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"只读验收 game/index.html 的可玩性与交互闭环;不要修改任何项目文件。",
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn autonomous_art_director_responsibility_action_for_test(
|
||||
fn autonomous_initial_leader_responsibility_action_for_test(
|
||||
agent_id: &str,
|
||||
expected_artifacts: &[&str],
|
||||
) -> AgentRuntimeToolAction {
|
||||
let read_only = matches!(agent_id, "design-director" | "code-director");
|
||||
AgentRuntimeToolAction {
|
||||
tool: "agent.delegate".to_string(),
|
||||
reason: Some("委派美术总监生成统一视觉规范图".to_string()),
|
||||
reason: Some("建立首批 Leader 专业规划".to_string()),
|
||||
input: serde_json::json!({
|
||||
"agentId": "art-director",
|
||||
"task": "生成项目统一视觉规范图并写入项目资产目录。",
|
||||
"acceptanceCriteria": [
|
||||
"使用画布生成接口产出后续 UI 与图集共用的规范图",
|
||||
"生成结果必须登记为项目本地 icon-spec 资产"
|
||||
],
|
||||
"agentId": agent_id,
|
||||
"task": if read_only {
|
||||
format!("由 {agent_id} 只读完成首轮专业规划,不得修改项目")
|
||||
} else {
|
||||
"生成首轮统一视觉规范图供后续专业 Agent 使用".to_string()
|
||||
},
|
||||
"acceptanceCriteria": if read_only {
|
||||
serde_json::json!(["只读输出专业规划,不得修改项目文件"])
|
||||
} else {
|
||||
serde_json::json!(["生成并登记统一视觉规范图"])
|
||||
},
|
||||
"expectedArtifacts": expected_artifacts,
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
@@ -713,9 +658,7 @@ async fn supervisor_autonomous_initial_art_director_requires_canonical_art_spec_
|
||||
)
|
||||
.expect("start autonomous Supervisor runtime");
|
||||
let mut actions = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
actions.push(autonomous_art_director_responsibility_action_for_test(&[
|
||||
"assets/art-preview.png",
|
||||
]));
|
||||
actions[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]);
|
||||
let plan = supervisor_collaboration_plan_for_test(actions);
|
||||
let revision = read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read art contract project revision");
|
||||
@@ -834,40 +777,21 @@ async fn supervisor_autonomous_initial_responsibilities_reject_invalid_plans_bef
|
||||
)
|
||||
.expect("start autonomous Supervisor runtime");
|
||||
|
||||
let code_missing_game_index = autonomous_initial_responsibility_actions_for_test(
|
||||
"创建可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/main.js"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&[],
|
||||
);
|
||||
let code_read_only = autonomous_initial_responsibility_actions_for_test(
|
||||
"只读检查 game/index.html,不要修改任何项目文件。",
|
||||
&["game/index.html"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&[],
|
||||
);
|
||||
let mut quality_not_read_only = autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"直接修改 game/index.html,修复试玩阻塞并重新验证。",
|
||||
&[],
|
||||
);
|
||||
quality_not_read_only[1].input["acceptanceCriteria"] = serde_json::json!([
|
||||
"直接写入 game/index.html 修复试玩问题",
|
||||
"修改后执行静态验证并交付新 revision"
|
||||
]);
|
||||
let quality_with_artifacts = autonomous_initial_responsibility_actions_for_test(
|
||||
"创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。",
|
||||
&["game/index.html"],
|
||||
"只读验收 game/index.html 的可玩性;不要修改任何项目文件。",
|
||||
&["game/index.html"],
|
||||
);
|
||||
let valid = valid_autonomous_initial_responsibility_actions_for_test();
|
||||
let mut missing_design = valid.clone();
|
||||
missing_design.remove(0);
|
||||
let mut design_not_read_only = valid.clone();
|
||||
design_not_read_only[0].input["task"] = serde_json::json!("直接修改项目完成策划实现");
|
||||
design_not_read_only[0].input["acceptanceCriteria"] = serde_json::json!(["直接修改项目文件"]);
|
||||
let mut art_missing_spec = valid.clone();
|
||||
art_missing_spec[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]);
|
||||
let mut code_with_artifacts = valid.clone();
|
||||
code_with_artifacts[2].input["expectedArtifacts"] = serde_json::json!(["game/index.html"]);
|
||||
let responses = [
|
||||
("code-missing-game-index", &code_missing_game_index),
|
||||
("code-read-only", &code_read_only),
|
||||
("quality-not-read-only", &quality_not_read_only),
|
||||
("quality-with-artifacts", &quality_with_artifacts),
|
||||
("missing-design", &missing_design),
|
||||
("design-not-read-only", &design_not_read_only),
|
||||
("art-missing-spec", &art_missing_spec),
|
||||
("code-with-artifacts", &code_with_artifacts),
|
||||
("valid-responsibilities", &valid),
|
||||
]
|
||||
.into_iter()
|
||||
@@ -904,7 +828,13 @@ async fn supervisor_autonomous_initial_responsibilities_reject_invalid_plans_bef
|
||||
.await
|
||||
.expect("repair invalid initial responsibilities")
|
||||
.expect("valid initial responsibility plan");
|
||||
assert_eq!(plan.actions, valid);
|
||||
let mut expected_valid = valid.clone();
|
||||
expected_valid.sort_by(|left, right| {
|
||||
left.input["agentId"]
|
||||
.as_str()
|
||||
.cmp(&right.input["agentId"].as_str())
|
||||
});
|
||||
assert_eq!(plan.actions, expected_valid);
|
||||
assert!(plan.response.is_empty());
|
||||
|
||||
let requests = (0..5)
|
||||
|
||||
@@ -5036,24 +5036,36 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b
|
||||
.expect("project init");
|
||||
let delegate_function =
|
||||
native_runtime_function_name("agent.delegate").expect("delegate function");
|
||||
let code_arguments = serde_json::json!({
|
||||
"reason": "委派可玩原型实现",
|
||||
let design_arguments = serde_json::json!({
|
||||
"reason": "委派策划 Leader",
|
||||
"input": {
|
||||
"agentId": "code-prototype",
|
||||
"task": "实现可直接试玩的游戏原型",
|
||||
"acceptanceCriteria": ["项目能够启动并完成最小玩法闭环"],
|
||||
"expectedArtifacts": ["game/index.html"],
|
||||
"agentId": "design-director",
|
||||
"task": "只读拆解首轮玩法目标和专业分工,不得修改项目",
|
||||
"acceptanceCriteria": ["只读输出策划规划,不得修改项目文件"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let quality_arguments = serde_json::json!({
|
||||
"reason": "委派独立质量评审",
|
||||
let art_arguments = serde_json::json!({
|
||||
"reason": "委派美术 Leader",
|
||||
"input": {
|
||||
"agentId": "quality-review",
|
||||
"task": "只读评审可玩性与闯关闭环,不要修改任何项目文件",
|
||||
"acceptanceCriteria": ["只读给出阻塞试玩的问题和验收结论"],
|
||||
"agentId": "art-director",
|
||||
"task": "生成首轮统一视觉规范图供后续专业 Agent 使用",
|
||||
"acceptanceCriteria": ["生成并登记统一视觉规范图"],
|
||||
"expectedArtifacts": ["assets/art-spec.png"],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let code_arguments = serde_json::json!({
|
||||
"reason": "委派程序 Leader",
|
||||
"input": {
|
||||
"agentId": "code-director",
|
||||
"task": "只读拆解首轮程序实现边界,不得修改项目",
|
||||
"acceptanceCriteria": ["只读输出程序规划,不得修改项目文件"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
@@ -5061,16 +5073,21 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b
|
||||
})
|
||||
.to_string();
|
||||
let recovered_response = native_agent_tool_plan_chat_response_with_calls(vec![
|
||||
(
|
||||
"call-autonomous-upstream-400-design",
|
||||
delegate_function.as_str(),
|
||||
design_arguments,
|
||||
),
|
||||
(
|
||||
"call-autonomous-upstream-400-art",
|
||||
delegate_function.as_str(),
|
||||
art_arguments,
|
||||
),
|
||||
(
|
||||
"call-autonomous-upstream-400-code",
|
||||
delegate_function.as_str(),
|
||||
code_arguments,
|
||||
),
|
||||
(
|
||||
"call-autonomous-upstream-400-quality",
|
||||
delegate_function.as_str(),
|
||||
quality_arguments,
|
||||
),
|
||||
]);
|
||||
let (request_notice_sender, request_notice_receiver) = mpsc::channel();
|
||||
let base_url = spawn_mock_llm_upstream_400_then_raw_response(
|
||||
@@ -5156,7 +5173,7 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b
|
||||
assert!(request_notice_receiver
|
||||
.recv_timeout(Duration::from_millis(100))
|
||||
.is_err());
|
||||
assert_eq!(plan.actions.len(), 2);
|
||||
assert_eq!(plan.actions.len(), 3);
|
||||
assert!(plan
|
||||
.actions
|
||||
.iter()
|
||||
|
||||
@@ -707,7 +707,7 @@ async fn background_agent_runtime_can_schedule_ready_manifest_tasks() {
|
||||
));
|
||||
|
||||
let scheduled =
|
||||
schedule_game_creator_agent_ready_tasks_at(&root, 0).expect("schedule ready tasks");
|
||||
schedule_game_creator_agent_ready_tasks_at(&root, 1).expect("schedule one ready task");
|
||||
assert_eq!(scheduled.len(), 1);
|
||||
assert_eq!(scheduled[0].state.agent_id, "design-director");
|
||||
assert_eq!(scheduled[0].state.source, "agent-ready-task-scheduler");
|
||||
|
||||
@@ -3986,6 +3986,14 @@
|
||||
- 处理:先以 CAS 单独 commit `queued -> executing`,成功后才调 ToolHost;调用返回后再 commit observation。恢复见到 executing 或 ToolHost 返回 Unknown 时只能进入 reconciliation,不得自动重执行。重复 resume 不得继续增 revision 或重复 event。
|
||||
- 验证:在“ToolHost 已调用、observation commit 失败”处注入故障,序列化快照并用新 engine 重载;断言重复 resume 后 ToolHost 计数仍为 1,且只有显式 reconcile observation 才恢复 running。
|
||||
|
||||
## Runtime pending 恢复不能让大型 async frame 共用默认 worker 栈(2026-08-03)
|
||||
|
||||
- 现象:Supervisor collaboration durable isolated spawn 恢复测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。
|
||||
- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queue 和 Agent 主循环各自形成大型 async poll frame;恢复路径在同一次 poll 调用链直接进入下一层状态机,累计超过 worker 默认栈。
|
||||
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。边界必须保留结构化取消语义;当前使用 boxed future 与 `JoinSet`,父 continuation 被丢弃时同步 abort 子任务。不得只增大 CI 的 `RUST_MIN_STACK`,否则生产默认栈仍可能崩溃。
|
||||
- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖 policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`。
|
||||
|
||||
## Provider 可扩展不能用一个全局 protocol 枚举代替实例隔离
|
||||
|
||||
- 现象:把 `openai_chat / openai_responses / anthropic` 直接当 Provider 身份,注册第二个同协议 endpoint 时发生 ID 冲突;或为方便调用把 API Key、base URL、raw-log 目录放进全局状态,并行请求后日志串目录。
|
||||
|
||||
@@ -746,6 +746,7 @@ game-project/
|
||||
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
|
||||
- 2026-07-17 起,同一 Runtime 文档的“V1.32 Runtime 强制 Supervisor 协作合同”作为 mixed swarm 可靠性事实源。项目可用 `.agent/collaboration-policy.json` 约束首波 static/isolated 模式、数量和 required static Agent;Runtime 在任何 child 副作用前整批校验并把合同指纹固化进 Provider batch v2。当前父 run 一旦形成 delivery/group,正式 `project-supervisor` 默认只负责编排、状态认领和验证,不再直接执行项目 mutation;专业 Agent/isolated child 权限与唯一 Supervisor 最终回复边界保持不变。
|
||||
- 2026-07-17 V1.32 最终代码已完成独立真实 Provider PASS:首批 mixed batch、三 isolated child、Runner 强杀恢复、专业返工、宿主验证、唯一最终回复与零重复/残留/泄漏同时成立。真实报告计数、隔离重试配置和仍待收敛的 tool-plan repair 成本统一以 Runtime 文档 V1.32 章节与共享决策记录为准。
|
||||
- 2026-08-03 恢复执行补充约束:整个 pending action continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,不能让 pending executor、task queue 与 Agent 主循环的大型 async poll frame 在同一 worker 调用栈连续嵌套。边界输入必须先装箱,避免泛型 helper 在真正 spawn 前仍把大型 future 保留在调用方 async frame;边界同时必须随父 continuation 取消子任务并保持 durable action、batch、run/session 身份及恢复防重语义,当前使用 boxed future 与 `JoinSet` 承担该约束。CI 和生产均使用默认 worker 栈验证,不以提高 `RUST_MIN_STACK` 代替代码边界。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.34 动态隔离子 Agent writeScopes 命令绕过封堵”作为 isolated child 的现行能力事实源。在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP;原生工具策略统一显示 `denied`,模板、项目 policy 与用户确认均不能放宽。保留固定只读 `command.run_limited`、同身份 `command.output_read / command.poll / command.terminate`、既有预览的 `preview.validate`,以及严格位于 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。
|
||||
- V1.34 的新单动作在 confirmation 和 OS launcher 前拒绝;新多 action 原生 batch 只要含一个 denied member 就在独立 pending-action sidecar、confirmation、OS spawn、revision 和任何成员项目副作用前整批 abort,只保留 `aborted / nextActionIndex=0` batch 事实。旧 pending / approval / batch 真正进入执行器时仍重新应用当前 child 边界,旧 executing 未知结果继续进入既有 reconciliation。该安全收紧由恶意 sibling 写入、策略快照、batch、旧 pending 执行器重验和 isolated/mixed/collaboration/provider-batch 回归证明;不因本切片重跑已通过且 isolated mutation 为 0 的 V1.31/V1.32 外部 Provider suite。通用命令只有在后续 scope-aware OS sandbox 对所有后代强制同一 `writeScopes` 并通过独立决策与测试后才可重新评估开放。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.35 多 ready isolated all-join 原子认领与恢复”作为 `agent.run_status` 同父 run 多 group 认领的现行事实源。Runtime 按 `delegationGroupId` 排序并一次性预取全部 join 锁;任一后续锁忙时保持零 delivery mutation、零 claim sidecar。全锁就绪后,同一 action 的 durable claim journal 按 `prepared -> committed -> observed` 推进;部分 commit 或 Runner 恢复只能复用该 journal 幂等补齐。只认领可完整放入优先 `readyIsolatedJoins` 观察预算的有序前缀,未观察旧 claim 可由后续 action 完整重放,但不创建第二份 isolated claim。每个 claimed delivery 必须由匹配原 action/group 的 journal 覆盖;无 journal 的旧 delivery 每轮只迁移一个原 action,已有 journal 不得扩写或状态倒退,跨 action group 归属冲突失败关闭。成功 observation 写入 pending sidecar 后只能把本轮完整输出的 claim 标记 `observed`,任一未观察或无 journal claim 继续阻断 finalization;每个 group 审计按 `actionId + delegationGroupId` 唯一,并在 Agent DB 锁内修复 torn tail、全量核对后幂等追加。
|
||||
|
||||
@@ -60,7 +60,7 @@ Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模
|
||||
|
||||
AI 游戏创作客户端使用 `npm run agc`,开发态 game-chat 使用 `npm run agc:game-chat`。两个入口都先由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 在 Tauri CLI 启动前检查固定地址 `http://127.0.0.1:3080/`:只有端口空闲时才继续启动。现有 marker 只包含 API target,不能证明监听器属于当前 worktree;即使页面和 target 看似匹配,也不得复用已经存在的 3080。旧 worktree Vite、无响应监听器或非 AGC 服务一律在创建原生窗口前失败关闭,并提示先停止旧服务;启动器不擅自终止无法证明归属的进程。
|
||||
|
||||
Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:旧 3080 已就绪时,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID <pid> /T /F`。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对 3080 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。
|
||||
Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:旧 3080 已就绪时,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID <pid> /T /F`。Linux 容器中的孤儿后代退出后可能暂时保留为 zombie,`kill(-PGID, 0)` 仍会返回成功;启动器必须结合 `/proc/<pid>/stat` 判断同组是否还存在非 zombie 成员,不能把等待 PID 1 回收误报为清理失败。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对 3080 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。
|
||||
|
||||
Windows 本地 `npm run dev` / `npm run dev:api-server` / `npm run dev:bgfilter-worker` 会用空的 `RUSTC_WRAPPER` / `CARGO_BUILD_RUSTC_WRAPPER` 覆盖 `server-rs/.cargo/config.toml` 里的 `sccache`,从而直连真实 `rustc`。完整栈和 `dev:api-server` 把 API 与 BgFilter worker 作为一个 Rust 重启单元:源码变化时先停两个进程,再先启动并验活 worker、最后启动并验活 API,避免两个 `cargo run` 并发链接同一个 Windows 可执行文件。不要把 wrapper 绕过值写成 `rustc`;Cargo 会按 wrapper 协议调用 `rustc <真实rustc路径> - ...`,最终报 `multiple input filenames provided` 并导致 api-server 无法启动。排查本地启动失败时,先看 dev 日志是否出现该错误,再确认脚本注入的 wrapper 为空。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user