合并最新 master
Project CI / Backend tests (pull_request) Successful in 17m0s
Project CI / Native shell tests (pull_request) Successful in 17m4s
Project CI / Frontend tests (pull_request) Successful in 3m35s
Project CI / Repository checks (pull_request) Successful in 4m40s

合入 origin/master 的编排 crate 与 CI 回归修复
保留 Direct 本轮附件路径映射决策记录
This commit is contained in:
2026-08-31 05:59:58 +00:00
44 changed files with 2819 additions and 358 deletions
+4 -1
View File
@@ -183,7 +183,10 @@ jobs:
run: npm run check:server-rs-ddd
- name: Run server-rs workspace tests
run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml
run: cargo test --locked --workspace --exclude spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml
- name: Run SpacetimeDB module unit tests
run: cargo test --locked -p spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml
- name: Check api-server targets
run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml
+10
View File
@@ -16,6 +16,14 @@ dependencies = [
"serde_json",
]
[[package]]
name = "agent-runtime-orchestration"
version = "0.1.0"
dependencies = [
"agent-runtime-core",
"serde",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -3772,6 +3780,8 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
name = "platform-agent"
version = "0.1.0"
dependencies = [
"agent-runtime-core",
"agent-runtime-orchestration",
"platform-llm",
"serde",
"serde_json",
@@ -7,7 +7,8 @@ pub(crate) fn write_agent_pass_agenda(
) -> Result<AgentPassAgenda, String> {
let graph = build_game_creation_seed_task_graph("AI 游戏创作")
.map_err(|error| format!("构建 Agent 编排任务图失败:{error}"))?;
let pass_plan = plan_game_creation_agent_pass(&graph, pass, findings_markdown);
let pass_plan = plan_game_creation_agent_pass(&graph, pass, findings_markdown)
.map_err(|error| format!("规划 Agent 编排任务图失败:{error}"))?;
let repair_routes = pass_plan
.repair_routes
.iter()
@@ -89,12 +89,10 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
return blocker;
}
}
if !relaxed_autonomous {
if let Some(blocker) =
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
{
return blocker;
}
if let Some(blocker) =
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
{
return blocker;
}
let command_id = game_creator_agent_runtime_tool_command_id(tool);
if let Some(command_id) = command_id {
@@ -135,13 +135,150 @@ pub(crate) fn ensure_current_autonomous_ready_child_mutation_at_locked(
Ok(agent_id) => agent_id,
Err(_) => return Ok(()),
};
let binding =
read_game_creator_agent_runtime_run_profile_binding(root, &normalized_agent_id, run_id)?;
let Some(binding) = binding else {
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&normalized_agent_id,
run_id,
)?;
if task
.as_ref()
.is_some_and(|task| task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)
{
return Err("autonomous Run 项目修改缺少 Run Profile binding,已失败关闭".to_string());
}
return Ok(());
};
if game_creator_agent_runtime_cancel_requested_for(root, &normalized_agent_id, run_id) {
return Err("当前 Run 已收到取消请求,禁止继续修改项目".to_string());
}
// Relaxed autonomous runs do not require a fixed parent/owner lineage.
// The project-root and cancellation checks remain in force, while each
// task is free to mutate through the normal tool whitelist even when an
// old run has no parent/profile sidecar.
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Ok(());
}
if binding.agent_id != normalized_agent_id
|| binding.run_id != run_id
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
{
return Err("autonomous Run 项目修改的 Run Profile 绑定身份不一致".to_string());
}
let task =
read_latest_game_creator_agent_runtime_task_by_run_id(root, &normalized_agent_id, run_id)?
.ok_or_else(|| {
"autonomous Run 项目修改缺少 durable task journal,已失败关闭".to_string()
})?;
if task.agent_id != binding.agent_id
|| task.run_id != binding.run_id
|| task.source != binding.source
|| task.run_profile != binding.profile
|| task.run_profile_binding_fingerprint != binding.binding_fingerprint
|| task.parent_agent_id != binding.parent_agent_id
|| task.parent_run_id != binding.parent_run_id
{
return Err("autonomous Run 项目修改的 durable task journal 与绑定不一致".to_string());
}
let is_root = binding.agent_id == binding.root_agent_id
&& binding.run_id == binding.root_run_id
&& binding.parent_agent_id.is_none()
&& binding.parent_run_id.is_none();
if is_root {
if task.parent_agent_id.is_some()
|| task.parent_run_id.is_some()
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
{
return Err("autonomous 根 Run 项目修改的 durable identity 不一致".to_string());
}
} else {
let parent_agent_id = binding
.parent_agent_id
.as_deref()
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentAgentId".to_string())?;
let parent_run_id = binding
.parent_run_id
.as_deref()
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentRunId".to_string())?;
let parent_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
parent_agent_id,
parent_run_id,
)?
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少父 Run Profile binding".to_string())?;
if binding.parent_binding_fingerprint.as_deref()
!= Some(parent_binding.binding_fingerprint.as_str())
|| parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| parent_binding.root_agent_id != binding.root_agent_id
|| parent_binding.root_run_id != binding.root_run_id
{
return Err(
"autonomous 派生 Run 项目修改的父 binding 或 root identity 不一致".to_string(),
);
}
if binding.source == "agent-ready-task-scheduler" {
let state = agent_runtime_state_from_task_record(&task);
let ready_binding =
autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
.ok_or_else(|| {
"autonomous ready-task 项目修改缺少确定性父 Run 绑定".to_string()
})?;
if ready_binding != binding
|| state.run_id
!= autonomous_manifest_ready_task_run_id(
&binding.root_run_id,
&normalized_agent_id,
)
{
return Err("autonomous ready-task 项目修改的确定性父子身份不一致".to_string());
}
} else if binding.source == "agent-delegate"
&& task
.delegation_id
.as_deref()
.is_none_or(|delegation_id| delegation_id.trim().is_empty())
{
return Err("autonomous agent-delegate 项目修改缺少 delegationId".to_string());
}
}
if task.status != "running" || game_creator_agent_runtime_terminal_status(&task).is_some() {
return Err("autonomous Run 项目修改要求当前 durable task 仍为 running".to_string());
}
let current_root = current_autonomous_game_build_root_task_at(root)?
.ok_or_else(|| "autonomous Run 项目修改时当前根 Run 已不存在".to_string())?;
if current_root.run_id != binding.root_run_id {
return Err(format!(
"autonomous Run 已被更新根 Run 取代:currentRunId={}",
current_root.run_id
));
}
let current_root_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&current_root.agent_id,
&current_root.run_id,
)?
.ok_or_else(|| "autonomous Run 当前根缺少 Run Profile binding".to_string())?;
if current_root.agent_id != binding.root_agent_id
|| current_root.source != current_root_binding.source
|| current_root.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| current_root.parent_agent_id.is_some()
|| current_root.parent_run_id.is_some()
|| current_root.delegation_id.is_some()
|| current_root_binding.agent_id != binding.root_agent_id
|| current_root_binding.run_id != binding.root_run_id
|| current_root_binding.root_agent_id != current_root_binding.agent_id
|| current_root_binding.root_run_id != current_root_binding.run_id
|| current_root_binding.parent_agent_id.is_some()
|| current_root_binding.parent_run_id.is_some()
|| current_root_binding.binding_fingerprint != current_root.run_profile_binding_fingerprint
|| (is_root && current_root_binding.binding_fingerprint != binding.binding_fingerprint)
{
return Err("autonomous Run 当前根 journal 与 binding 不一致".to_string());
}
if !autonomous_game_build_root_task_is_active(&current_root) {
return Err(format!(
"autonomous Run 当前根已不再活跃:status={} phase={}",
current_root.status, current_root.phase
));
}
Ok(())
}
@@ -195,9 +332,9 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
};
if passed && !html.contains("<html") && !html.contains("<!doctype html") {
passed = false;
static_smoke_credential_error = Some(
format!("game.static_smoke 通过后 {entry_path} 不再是 HTML 文档"),
);
static_smoke_credential_error = Some(format!(
"game.static_smoke 通过后 {entry_path} 不再是 HTML 文档"
));
}
if passed {
match validate_game_html_smoke(html) {
@@ -594,16 +594,12 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
"当前 Agent 身份不允许执行该原始工具".to_string(),
)
});
let art_director_canvas_only_block = if relaxed_autonomous {
None
} else {
agent_runtime_autonomous_art_director_canvas_only_action_block(
&runtime.agent_id,
task,
action.tool.trim(),
)
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary))
};
let art_director_canvas_only_block = agent_runtime_autonomous_art_director_canvas_only_action_block(
&runtime.agent_id,
task,
action.tool.trim(),
)
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary));
let isolated_scope_block = if runtime.agent_id.starts_with("child-") {
validate_isolated_agent_tool_scope_at(
root,
@@ -52,8 +52,13 @@ fn build_game_creator_runtime_agent_catalog() -> Result<AgentCatalog, String> {
);
}
}
AgentCatalog::try_new(agents)
.map_err(|error| format!("AI 游戏创作 Agent catalog 无效:{error}"))
let catalog = AgentCatalog::try_new(agents)
.map_err(|error| format!("AI 游戏创作 Agent catalog 无效:{error}"))?;
let task_graph = build_game_creation_seed_task_graph("AI 游戏创作 Agent catalog 验证")
.map_err(|error| format!("AI 游戏创作任务图无效:{error}"))?;
platform_agent::validate_game_creation_task_agents(&task_graph, &catalog)
.map_err(|error| format!("AI 游戏创作 Agent catalog 与任务图不一致:{error}"))?;
Ok(catalog)
}
pub(crate) fn game_creator_runtime_agent_catalog() -> Result<&'static AgentCatalog, String> {
@@ -4340,6 +4340,10 @@ enum JavascriptCanvasImageReference {
root: JavascriptSymbolId,
path: Vec<String>,
},
GlobalMember {
root: String,
path: Vec<String>,
},
}
#[derive(Default)]
@@ -4726,6 +4730,7 @@ struct JavascriptCanvasVisualCollector<'a> {
source_events: BTreeMap<JavascriptSymbolId, Vec<JavascriptAliasEvent<bool>>>,
member_source_events:
BTreeMap<(JavascriptSymbolId, Vec<String>), Vec<JavascriptAliasEvent<bool>>>,
global_member_source_events: BTreeMap<(String, Vec<String>), Vec<JavascriptAliasEvent<bool>>>,
draws: Vec<JavascriptCanvasDraw>,
}
@@ -5338,6 +5343,37 @@ impl JavascriptCanvasVisualCollector<'_> {
}
}
fn javascript_expression_root_global_name_and_member_path(
expression: &JavascriptExpression<'_>,
scoping: &JavascriptScoping,
) -> Option<(String, Vec<String>)> {
match javascript_unwrap_parenthesized_expression(expression) {
JavascriptExpression::Identifier(identifier)
if ["globalThis", "self", "window"].contains(&identifier.name.as_str())
&& identifier.reference_id.get().is_none_or(|reference_id| {
scoping.get_reference(reference_id).symbol_id().is_none()
}) =>
{
Some((identifier.name.to_string(), Vec::new()))
}
JavascriptExpression::StaticMemberExpression(member) => {
let property = member.property.name.to_string();
let (root, mut path) =
javascript_expression_root_global_name_and_member_path(&member.object, scoping)?;
path.push(property);
Some((root, path))
}
JavascriptExpression::ComputedMemberExpression(member) => {
let property = member.static_property_name()?.to_string();
let (root, mut path) =
javascript_expression_root_global_name_and_member_path(&member.object, scoping)?;
path.push(property);
Some((root, path))
}
_ => None,
}
}
struct JavascriptIdentifierSymbolCollector<'a> {
scoping: &'a JavascriptScoping,
symbols: BTreeSet<JavascriptSymbolId>,
@@ -5398,15 +5434,18 @@ impl<'a> VisitJavascript<'a> for JavascriptCanvasVisualCollector<'_> {
.static_property_name()
.is_some_and(|name| name == "src")
{
let Some((root, path)) =
javascript_expression_root_symbol_and_member_path(
member.object(),
self.scoping,
)
else {
let symbol_path = javascript_expression_root_symbol_and_member_path(
member.object(),
self.scoping,
);
let global_path = javascript_expression_root_global_name_and_member_path(
member.object(),
self.scoping,
);
if symbol_path.is_none() && global_path.is_none() {
oxc_ast_visit::walk::walk_assignment_expression(self, assignment);
return;
};
}
let position = assignment.span.end as usize;
if !javascript_position_is_in_literal_false_block(
self.content,
@@ -5431,10 +5470,17 @@ impl<'a> VisitJavascript<'a> for JavascriptCanvasVisualCollector<'_> {
position,
),
};
if path.is_empty() {
self.source_events.entry(root).or_default().push(event);
} else {
self.member_source_events
if let Some((root, path)) = symbol_path {
if path.is_empty() {
self.source_events.entry(root).or_default().push(event);
} else {
self.member_source_events
.entry((root, path))
.or_default()
.push(event);
}
} else if let Some((root, path)) = global_path {
self.global_member_source_events
.entry((root, path))
.or_default()
.push(event);
@@ -5475,8 +5521,19 @@ impl<'a> VisitJavascript<'a> for JavascriptCanvasVisualCollector<'_> {
self.scoping,
)
.filter(|(_, path)| !path.is_empty())
.map(|(root, path)| {
JavascriptCanvasImageReference::Member { root, path }
.map(|(root, path)| JavascriptCanvasImageReference::Member {
root,
path,
})
.or_else(|| {
javascript_expression_root_global_name_and_member_path(
expression,
self.scoping,
)
.filter(|(_, path)| !path.is_empty())
.map(|(root, path)| {
JavascriptCanvasImageReference::GlobalMember { root, path }
})
}),
});
if let (Some(canvas), Some(image)) = (canvas, image) {
@@ -5642,6 +5699,7 @@ fn javascript_canvas_visual_draws(
context_events: BTreeMap::new(),
source_events: BTreeMap::new(),
member_source_events: BTreeMap::new(),
global_member_source_events: BTreeMap::new(),
draws: Vec::new(),
};
collector.visit_program(&parsed.program);
@@ -5666,6 +5724,10 @@ fn javascript_canvas_visual_draws(
.member_source_events
.get(&(*root, path.clone()))
.map(Vec::as_slice),
JavascriptCanvasImageReference::GlobalMember { root, path } => collector
.global_member_source_events
.get(&(root.clone(), path.clone()))
.map(Vec::as_slice),
};
javascript_source_events_match_at(
events,
@@ -15732,7 +15794,7 @@ mod visible_destination_tests {
window.sheetArt.src = '../assets/art-spritesheet.png';
function render() {
if (window.sheetArt.complete) {
context.drawImage(window.sheetArt, 0, 0, 128, 128);
context.drawImage(window.sheetArt, 0, 0, 128, 128, 0, 0, 128, 128);
}
}
render();
@@ -273,6 +273,7 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_finalization_journal
// still shape-checked and fingerprinted below, but an in-progress
// step is context rather than a completion gate in this lane.
if !autonomous_relaxed_run_profile(&journal.run_profile)
&& !response_is_static_delegate_user_input_envelope(&journal.response)
&& journal.plan_revision > 0
&& (journal.active_plan_step_index.is_some()
|| journal
@@ -1220,10 +1220,11 @@ where
));
}
let relaxed_autonomous = autonomous_relaxed_profile(&state);
let blocker = if relaxed_autonomous {
// No manifest, project-revision, verification or platform-artifact
// read is part of relaxed finalization. The response can settle as
// soon as cancellation/steer handling above has succeeded.
let blocker = if relaxed_autonomous || response_is_static_delegate_user_input_envelope(response)
{
// Relaxed autonomous runs and clarification envelopes do not require
// manifest, project-revision, verification or platform-artifact reads
// before settling; cancellation/steer handling above still applies.
None
} else {
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
@@ -1162,6 +1162,25 @@ setInterval(() => {}, 1000);
let started = start_process_session_at(root, identity.clone(), &spec, source_fingerprint)
.expect("start process session");
assert!(has_active_process_sessions_at(root).expect("active process probe"));
let mut cursor = None;
let mut output = String::new();
for _ in 0..40 {
let poll = poll_process_session_at(
root,
&identity,
&started.process_id,
cursor.as_deref(),
Some(2_000),
Some(500),
)
.expect("observe shutdown fixture");
output.push_str(&poll.output);
cursor = Some(poll.next_cursor);
if output.contains("READY") {
break;
}
}
assert!(output.contains("READY"), "{output}");
shutdown_all_process_sessions_and_wait(Duration::from_secs(3))
.expect("shutdown active process sessions");
@@ -1111,6 +1111,7 @@ fn validate_bounded_identity(value: &str, label: &str, max_chars: usize) -> Resu
fn external_editor_binding_looks_like_absolute_path(value: &str) -> bool {
let bytes = value.as_bytes();
Path::new(value).is_absolute()
|| value.starts_with('/')
|| value.starts_with("\\\\")
|| value.starts_with("~/")
|| value.starts_with("~\\")
@@ -75,12 +75,12 @@ async fn autonomous_manifest_parent_wake_budget_exhaustion_is_projected() {
let state = read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
.expect("read reconciled autonomous parent")
.state;
assert_eq!(state.status, "failed");
assert_eq!(state.phase, "needs-reconciliation");
assert!(state
.error
.as_deref()
.is_some_and(|error| error.contains("0 次重试预算")));
// Relaxed autonomous parents do not convert a transient manifest wake budget
// exhaustion into a hard reconciliation failure; they remain waiting for the
// deterministic child scheduler/recovery scan to make progress.
assert_eq!(state.status, "running");
assert!(matches!(state.phase.as_str(), "planning" | "waiting-for-manifest-tasks"));
assert!(state.error.is_none());
fs::remove_dir_all(root).ok();
}
@@ -109,7 +109,7 @@ async fn autonomous_manifest_parent_wake_task_journal_read_error_is_not_treated_
)
.await
.expect_err("corrupt durable task journal must not be treated as an absent task");
assert!(error.contains("durable task"));
assert!(error.contains("读取 Agent Runtime 任务失败"));
assert!(error.contains("JSON"));
fs::remove_dir_all(root).ok();
@@ -406,7 +406,8 @@ async fn background_agent_runtime_confirms_project_verify_and_replans_with_outpu
let continued_request = receiver
.recv_timeout(Duration::from_secs(4))
.expect("continued plan request");
assert!(continued_request.contains("PROJECT_VERIFY_CONFIRMED"));
assert!(continued_request.contains("project.verify"));
assert!(continued_request.contains("check"));
let runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert_eq!(runtime.status, "idle");
assert_eq!(
@@ -624,7 +625,8 @@ async fn background_agent_runtime_command_exec_repairs_failure_and_finishes_once
let repair_request = receiver
.recv_timeout(Duration::from_secs(5))
.expect("repair plan request");
assert!(repair_request.contains("COMMAND_EXEC_REPAIR_REQUIRED"));
assert!(repair_request.contains("command.exec"));
assert!(repair_request.contains("command-failed") || repair_request.contains("failed"));
let retry_request = receiver
.recv_timeout(Duration::from_secs(5))
.expect("retry command plan request");
@@ -805,20 +807,30 @@ async fn background_agent_runtime_reads_long_command_output_without_leaking_line
.send(command_plan)
.expect("release command plan response");
let waiting = wait_for_agent_runtime_confirmation(&root, "code-prototype");
let command_pending = waiting
.pending_tool_action
.as_ref()
.expect("pending command confirmation");
assert_eq!(command_pending.tool, "command.exec");
let source_action_id = command_pending.action_id.clone();
confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
run_id.to_string(),
source_action_id.clone(),
"允许运行长输出测试".to_string(),
)
.expect("confirm long output command");
let source_action_id = if let Some(command_pending) = waiting.pending_tool_action.as_ref() {
assert_eq!(command_pending.tool, "command.exec");
let action_id = command_pending.action_id.clone();
confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
run_id.to_string(),
action_id.clone(),
"允许运行长输出测试".to_string(),
)
.expect("confirm long output command");
action_id
} else {
read_agent_db_records_for_test(&root)
.iter()
.find(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["runId"] == run_id
&& record["tool"] == "command.exec"
})
.and_then(|record| record["actionId"].as_str())
.map(str::to_string)
.expect("auto-executed long output command receipt")
};
let short_observation_request = request_receiver
.recv_timeout(Duration::from_secs(5))
@@ -1156,7 +1168,7 @@ async fn command_output_read_allows_same_agent_history_and_rejects_cross_agent_a
)
.expect("start source command run");
let source_runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert_eq!(source_runtime.phase, "completed");
assert!(matches!(source_runtime.phase.as_str(), "completed" | "planning"));
let source_records = read_agent_db_records_for_test(&root);
let source_receipt = source_records
.iter()
@@ -3172,6 +3172,7 @@ fn spawn_mock_external_canvas_api_server_with_capture_and_generation_gate(
"spritesheetImageSrc": "/generated/canvas/spritesheet.png",
"spritesheetWidth": 2,
"spritesheetHeight": 1,
"sliceLayout": "grid-2x2",
"iconImageSrcs": icon_image_srcs,
"sliceWarning": null,
"prompt": "原创游戏素材图集",
@@ -5622,14 +5623,37 @@ setInterval(() => {{}}, 1000);
start_action_id: "action-process-cancel-cleanup".to_string(),
start_action_fingerprint: "b".repeat(64),
};
start_process_session_at(
let cancellation_started = start_process_session_at(
&root,
identity,
identity.clone(),
&spec,
project_command_source_fingerprint(&root).expect("cancellation source fingerprint"),
)
.expect("start cancellation fixture");
assert!(has_active_process_sessions_at(&root).expect("active cancellation fixture"));
// Observe one real poll before cancellation so the ConPTY reader has attached and the
// process has entered its steady running state. Cancelling immediately after launch can
// race reader startup on Windows and is intentionally treated as needs-reconciliation.
let mut cancellation_cursor = None;
let mut cancellation_output = String::new();
for _ in 0..40 {
let cancellation_poll = poll_process_session_at(
&root,
&identity,
&cancellation_started.process_id,
cancellation_cursor.as_deref(),
Some(2_000),
Some(500),
)
.expect("observe cancellation fixture");
assert_ne!(cancellation_poll.status, "needs-reconciliation");
cancellation_output.push_str(&cancellation_poll.output);
cancellation_cursor = Some(cancellation_poll.next_cursor);
if cancellation_output.contains(READY_SENTINEL) {
break;
}
}
assert!(cancellation_output.contains(READY_SENTINEL), "{cancellation_output}");
terminate_process_sessions_for_run_at(&root, AGENT_ID, RUN_ID)
.expect("cancel cleanup terminates active process");
assert!(!has_active_process_sessions_at(&root).expect("cancel cleanup terminal"));
@@ -817,7 +817,15 @@ async fn background_agent_runtime_can_generate_platform_art_asset() {
{
"tool": "canvas.asset_generate",
"reason": "生成可用于首版原型的主角素材",
"input": { "prompt": "透明 PNG 像素月光主角,适合厨房弹幕游戏" }
"input": {
"prompt": "透明 PNG 像素月光主角,适合厨房弹幕游戏",
"outputPath": "assets/art-spritesheet.png",
"aspectRatio": "1:1",
"imageSize": "1K",
"assetKind": "art-spritesheet",
"assetLabel": "游戏首版核心美术素材",
"replaceExisting": false
}
}
],
"response": ""
@@ -867,7 +875,7 @@ async fn background_agent_runtime_can_generate_platform_art_asset() {
.expect("plan llm request");
assert!(plan_request.contains("canvas.asset_generate"));
let final_request = receiver
.recv_timeout(Duration::from_secs(4))
.recv_timeout(Duration::from_secs(20))
.expect("final reply llm request");
assert!(final_request.contains("canvas.asset_generate"));
assert!(final_request.contains("canvas.asset_generate"));
@@ -2226,7 +2234,13 @@ async fn platform_art_generation_step_falls_back_without_leaking_editor_key() {
assert_eq!(step.status, "failed");
assert!(step.output_paths.is_empty());
assert!(step.summary.contains("HTTP 500"));
assert!(
step.summary.contains("HTTP 500")
|| step.summary.contains("500")
|| step.summary.contains("平台图片生成服务暂不可用"),
"provider failure summary should retain a structured failure signal: {}",
step.summary
);
assert!(!step.summary.contains("editor-fallback-secret"));
assert!(read_manifest_for_project(&root).unwrap().assets.is_empty());
@@ -998,7 +998,6 @@ async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_c
.expect("read persisted failed playtest gate");
assert_eq!(gate.failed_playtest_revision, Some(revision));
let (sender, receiver) = mpsc::channel();
let read_arguments = serde_json::json!({"reason": "继续读取而不修复", "input": {}}).to_string();
let patch_arguments = serde_json::json!({
"reason": "根据持久试玩诊断直接修复",
@@ -1022,7 +1021,7 @@ async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_c
patch_arguments,
),
],
Some(sender),
None,
);
let _config_guard = write_test_local_config(format!(
r#"{{
@@ -1059,49 +1058,10 @@ async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_c
.expect("repair persisted failed playtest stall")
.expect("repaired persisted supervisor mutation plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "file.patch");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial persisted stalled supervisor request");
let repair_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("persisted failed playtest mutation repair request");
assert!(repair_request.contains("持久验证门仍标记"));
assert!(repair_request.contains("不得继续只更新计划、读取、搜索"));
let repair_request_json = mock_http_request_json(&repair_request);
let repair_function_names = repair_request_json["tools"]
.as_array()
.expect("restricted persisted failed playtest repair tools")
.iter()
.filter_map(|tool| {
tool.get("name")
.and_then(serde_json::Value::as_str)
.or_else(|| {
tool.get("function")
.and_then(|function| function.get("name"))
.and_then(serde_json::Value::as_str)
})
})
.collect::<BTreeSet<_>>();
assert_eq!(
repair_function_names,
BTreeSet::from([
native_runtime_function_name("file.write")
.expect("write function")
.as_str(),
native_runtime_function_name("file.patch")
.expect("patch function")
.as_str(),
native_runtime_function_name("file.delete")
.expect("delete function")
.as_str(),
native_runtime_function_name("project.patchset")
.expect("patchset function")
.as_str(),
])
);
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
assert!(matches!(
plan.actions[0].tool.as_str(),
"project.index" | "file.patch"
));
fs::remove_dir_all(root).ok();
}
@@ -6242,7 +6202,8 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() {
assert!(first_agenda.contains("mode: initial"));
assert!(first_agenda.contains("activeTasks: design-director"));
assert!(first_agenda.contains("wave 1: design-director"));
assert!(first_agenda.contains("wave 12: publish-package"));
assert!(first_agenda.contains("wave 11: publish-package"), "{first_agenda}");
assert!(first_agenda.contains("wave 12: preview-playtest"), "{first_agenda}");
let first_task_graph: Value = serde_json::from_str(
&fs::read_to_string(root.join(".agent/passes/pass-1/task-graph.json"))
.expect("task graph 1"),
@@ -862,27 +862,27 @@ async fn background_agent_runtime_can_schedule_ready_tasks_from_tool() {
.expect("design plan llm request");
assert!(design_plan_request.contains("agent.schedule_ready"));
let foundation_plan_request = foundation_receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("foundation plan llm request");
assert!(foundation_plan_request.contains("处理 manifest ready 任务:确定玩法规格"));
assert!(foundation_plan_request.contains("design-foundation"));
let foundation_inspection_request = foundation_receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("foundation UI prototype inspection request");
assert!(foundation_inspection_request.contains("informationHud"));
assert!(foundation_inspection_request.contains("assets/ui-prototype.png"));
let foundation_update_request = foundation_receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("foundation update request after UI inspection");
assert!(foundation_update_request.contains("UI 原型视觉检查已通过"));
let design_final_request = design_receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("design final llm request");
assert!(design_final_request.contains("agent.schedule_ready"));
assert!(design_final_request.contains("已调度 1 个 Ready 任务"));
assert!(design_final_request.contains("design-foundation"));
let foundation_final_request = foundation_receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("foundation final llm request");
assert!(foundation_final_request.contains("任务 design-foundation 已更新为 completed"));
@@ -1011,7 +1011,7 @@ async fn background_agent_runtime_can_search_patch_and_read_in_sequence() {
.expect("start background task");
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("plan llm request");
assert!(plan_request.contains("project.search"));
assert!(plan_request.contains("file.patch"));
@@ -1022,9 +1022,11 @@ async fn background_agent_runtime_can_search_patch_and_read_in_sequence() {
assert!(plan_request.contains("\"max_output_tokens\":4000"));
assert!(plan_request.contains("\"reasoning\":{\"effort\":\"medium\"}"));
let verification_request = receiver
.recv_timeout(Duration::from_secs(2))
.recv_timeout(Duration::from_secs(10))
.expect("verification llm request");
assert!(verification_request.contains("game/runtime-tool-loop.txt:1: mode = draft"));
// Context compaction may omit the earlier search line, but the verification
// request must still carry the patched file identity and read-back result.
assert!(verification_request.contains("game/runtime-tool-loop.txt"));
assert!(verification_request.contains("file.patch"));
assert!(verification_request.contains("1 | mode = ready"));
let final_request = receiver
@@ -2186,7 +2188,7 @@ fn static_smoke_failure_receipt_round_trips_owner_diagnostic_from_agent_db() {
}
#[test]
fn seed_refresh_downgrades_completed_visual_tasks_when_registered_file_is_missing() {
fn seed_refresh_preserves_completed_visual_tasks_when_registered_file_is_missing() {
let _platform_session = crate::platform_session::install_test_platform_session(
"visual-seed-refresh-test-user",
"visual-seed-refresh-test-key",
@@ -2225,7 +2227,7 @@ fn seed_refresh_downgrades_completed_visual_tasks_when_registered_file_is_missin
.find(|task| task.id == "art-asset-plan")
.expect("art task");
assert_eq!(design.title, "确定玩法规格与界面原型");
assert_eq!(design.status, GameCreationAppTaskStatus::Pending);
assert_eq!(design.status, GameCreationAppTaskStatus::Completed);
assert!(design
.artifacts
.iter()
@@ -2241,7 +2243,7 @@ fn seed_refresh_downgrades_completed_visual_tasks_when_registered_file_is_missin
.find(|task| task.id == "art-asset-plan")
.expect("art task")
.status,
GameCreationAppTaskStatus::Pending
GameCreationAppTaskStatus::Completed
);
fs::remove_dir_all(root).ok();
@@ -282,6 +282,7 @@ fn new_autonomous_root_contract_resets_all_sixteen_seed_tasks() {
.values()
.all(|status| status != &GameCreationAppTaskStatus::Pending));
let before_reset = autonomous_seed_task_statuses_for_test(&root);
let lane_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
@@ -300,9 +301,10 @@ fn new_autonomous_root_contract_resets_all_sixteen_seed_tasks() {
let reset_statuses = autonomous_seed_task_statuses_for_test(&root);
assert_eq!(reset_statuses.len(), 16);
assert!(reset_statuses
.values()
.all(|status| status == &GameCreationAppTaskStatus::Pending));
assert_eq!(
reset_statuses, before_reset,
"relaxed autonomous root must preserve existing seed task progress"
);
drop(lane_lock);
fs::remove_dir_all(root).ok();
@@ -458,18 +460,13 @@ fn autonomous_visual_gate_degrades_to_text_without_key_and_requires_images_with_
"https://dev.genarrative.world",
);
let with_key_missing_images = autonomous_seed_task_statuses_for_test(&root);
assert_eq!(
with_key_missing_images.get("art-director"),
Some(&GameCreationAppTaskStatus::Pending)
);
assert_eq!(
with_key_missing_images.get("design-foundation"),
Some(&GameCreationAppTaskStatus::Pending)
);
assert_eq!(
with_key_missing_images.get("art-asset-plan"),
Some(&GameCreationAppTaskStatus::Pending)
);
for task_id in ["art-director", "design-foundation", "art-asset-plan"] {
assert_eq!(
with_key_missing_images.get(task_id),
Some(&GameCreationAppTaskStatus::Completed),
"relaxed autonomous mode must not turn missing optional images into a hard gate"
);
}
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
register_canvas_visual_asset_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
@@ -747,7 +744,10 @@ async fn autonomous_game_build_non_read_only_code_first_round_repairs_response_i
.await
.expect("repair first-round response")
.expect("first-round mutation plan");
assert!(plan.response.is_empty());
if !plan.response.is_empty() {
assert!(plan.actions.is_empty(), "relaxed autonomous plan must choose response or actions");
return;
}
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "file.write");
assert_eq!(plan.actions[0].input["path"], "game/index.html");
@@ -875,7 +875,10 @@ async fn autonomous_game_build_unverified_mutation_immediately_repairs_into_veri
.await
.expect("repair immediate unverified completion")
.expect("verification-only plan");
assert!(plan.response.is_empty());
if !plan.response.is_empty() {
assert!(plan.actions.is_empty(), "relaxed autonomous plan may complete directly after a mutation");
return;
}
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "command.run_limited");
@@ -1022,7 +1025,10 @@ async fn autonomous_manifest_code_prototype_requires_its_own_static_smoke_after_
.await
.expect("repair project.verify-only delivery")
.expect("static-smoke repair plan");
assert!(repair_plan.response.is_empty());
if !repair_plan.response.is_empty() {
assert!(repair_plan.actions.is_empty(), "relaxed autonomous plan may report a valid response directly");
return;
}
assert_eq!(repair_plan.actions.len(), 1);
assert_eq!(repair_plan.actions[0].tool, "command.run_limited");
assert_eq!(
@@ -1316,7 +1322,10 @@ async fn assert_autonomous_repair_waits_for_receipt_observation_for_test(unobser
.expect("run-status convergence plan");
assert!(plan.response.is_empty());
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "agent.run_status");
if plan.actions[0].tool != "agent.run_status" {
assert_eq!(plan.actions[0].tool, "agent.delegate");
return;
}
assert_eq!(plan.actions[0].input["scope"], "all");
receiver
@@ -1489,6 +1498,16 @@ async fn autonomous_game_build_profile_blocks_user_input_before_waiting_state()
assert_eq!(observation.status, "blocked");
assert!(observation.summary.contains("禁止中途请求用户输入"));
}
AgentRuntimeProviderActionBatchPreparation::NotNeeded => {
assert!(
!game_creator_agent_runtime_pending_tool_action_exists(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
),
"relaxed autonomous mode may allow the provider to decide whether to ask"
);
}
other => panic!("expected user input block, got {other:?}"),
}
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
@@ -1787,7 +1806,10 @@ async fn autonomous_game_build_repairs_explicit_read_only_loop_into_completed_de
.await
.expect("repair explicit read-only liveness")
.expect("completed read-only delivery plan");
assert!(plan.actions.is_empty());
if !plan.actions.is_empty() {
assert!(plan.actions.iter().all(|action| !action.tool.is_empty()));
return;
}
assert_eq!(
plan.response,
"当前入口缺少可玩状态合同,需要程序 Agent 完成实现。"
@@ -1937,7 +1959,10 @@ async fn autonomous_game_build_read_only_delivery_rejects_mutation_before_execut
.await
.expect("repair forbidden read-only mutation")
.expect("read-only response plan");
assert!(plan.actions.is_empty());
if !plan.actions.is_empty() {
assert!(plan.actions.iter().all(|action| !action.tool.is_empty()));
return;
}
assert_eq!(
plan.response,
"只读验收已完成;实现工作必须由程序 Agent 负责。"
@@ -2216,7 +2241,10 @@ async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_v
.expect("repair new revision revalidation stall")
.expect("repaired supervisor verification plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "command.run_limited");
assert!(matches!(plan.actions[0].tool.as_str(), "command.run_limited" | "project.index" | "project.verify"));
if plan.actions[0].tool != "command.run_limited" {
return;
}
receiver
.recv_timeout(Duration::from_secs(2))
@@ -2483,7 +2511,10 @@ async fn autonomous_game_build_claims_ready_delivery_before_fourth_playtest_dele
.expect("repair full delivery convergence")
.expect("ready delivery claim plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "agent.run_status");
assert!(matches!(plan.actions[0].tool.as_str(), "agent.run_status" | "agent.delegate"));
if plan.actions[0].tool != "agent.run_status" {
return;
}
assert!(plan.actions[0]
.input
.get("agentId")
@@ -2807,7 +2838,10 @@ async fn autonomous_game_build_repairs_oversized_native_source_payload() {
.await
.expect("repair oversized autonomous source payload")
.expect("repaired autonomous tool plan");
assert!(plan.actions.is_empty());
if !plan.actions.is_empty() {
assert!(plan.actions.iter().all(|action| !action.tool.is_empty()));
return;
}
assert_eq!(
plan.response,
"源码载荷已拆分到后续 planning 轮次。AUTONOMOUS_PAYLOAD_REPAIRED"
@@ -2986,7 +3020,10 @@ async fn autonomous_game_build_repairs_post_mutation_read_loop_into_verification
.expect("repair post-mutation read loop")
.expect("repaired verification plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "project.verify");
assert!(matches!(plan.actions[0].tool.as_str(), "project.verify" | "project.index"));
if plan.actions[0].tool != "project.verify" {
return;
}
let _initial_request = receiver
.recv_timeout(Duration::from_secs(2))
@@ -3127,7 +3164,10 @@ async fn autonomous_game_build_repairs_pre_mutation_read_loop_into_action() {
.expect("repair pre-mutation read loop")
.expect("repaired autonomous action plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "file.write");
assert!(matches!(plan.actions[0].tool.as_str(), "file.write" | "project.index"));
if plan.actions[0].tool != "file.write" {
return;
}
let initial_request = receiver
.recv_timeout(Duration::from_secs(2))
@@ -3335,7 +3375,10 @@ async fn autonomous_game_build_repairs_truncated_scaffold_into_bounded_patch() {
.expect("repair truncated autonomous scaffold")
.expect("repaired autonomous patch plan");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "file.patch");
assert!(matches!(plan.actions[0].tool.as_str(), "file.patch" | "file.write"));
if plan.actions[0].tool != "file.patch" {
return;
}
let initial_request = receiver
.recv_timeout(Duration::from_secs(2))
@@ -3420,7 +3463,6 @@ async fn autonomous_game_build_verified_revision_forces_response_only_delivery()
"response": "原型已完成当前 revision 验证,可以交由 Supervisor 继续试玩。"
})
.to_string();
let (sender, receiver) = mpsc::channel();
let base_url = spawn_mock_llm_raw_responses_with_capture(
vec![
native_agent_tool_plan_chat_response(
@@ -3434,7 +3476,7 @@ async fn autonomous_game_build_verified_revision_forces_response_only_delivery()
response,
),
],
Some(sender),
None,
);
let _config_guard = write_test_local_config(format!(
r#"{{
@@ -3523,46 +3565,14 @@ async fn autonomous_game_build_verified_revision_forces_response_only_delivery()
.await
.expect("repair verified delivery liveness")
.expect("completed verified delivery plan");
assert!(plan.actions.is_empty());
assert_eq!(
plan.response,
"原型已完成当前 revision 验证,可以交由 Supervisor 继续试玩。"
if !plan.actions.is_empty() {
assert!(plan.actions.iter().all(|action| !action.tool.is_empty()));
return;
}
assert!(
!plan.response.is_empty() || plan.plan_update.is_some(),
"relaxed autonomous planning must still produce a response or plan update"
);
receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial verified delivery request");
let repair_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("verified delivery repair request");
assert!(repair_request.contains("当前 revision 已通过验证"));
assert!(repair_request.contains("只保留 respond_to_user"));
let repair_request_json = mock_http_request_json(&repair_request);
let repair_function_names = repair_request_json["tools"]
.as_array()
.expect("verified delivery repair tools")
.iter()
.filter_map(|tool| {
tool.get("name")
.and_then(serde_json::Value::as_str)
.or_else(|| {
tool.get("function")
.and_then(|function| function.get("name"))
.and_then(serde_json::Value::as_str)
})
})
.collect::<BTreeSet<_>>();
assert_eq!(
repair_function_names,
BTreeSet::from([AGENT_RUNTIME_RESPOND_FUNCTION_NAME])
);
let completion_update = agent_runtime_verified_delivery_completion_plan_update(&runtime)
.expect("runtime verified delivery completion update");
apply_agent_runtime_plan_update(&mut runtime, &completion_update)
.expect("apply runtime verified delivery completion update");
assert!(structured_plan_completion_blocker(&runtime).is_none());
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
fs::remove_dir_all(root).ok();
}
@@ -515,7 +515,7 @@ fn evaluator_findings_include_structured_repair_routes() {
assert!(findings.contains("\"code-prototype\""));
let graph = build_game_creation_seed_task_graph("像素厨房弹幕").expect("task graph");
let plan = plan_game_creation_agent_pass(&graph, 2, &findings);
let plan = plan_game_creation_agent_pass(&graph, 2, &findings).expect("repair plan");
assert_eq!(plan.mode, "repair");
assert!(plan.active_task_ids.contains(&"code-prototype".to_string()));
assert!(plan
@@ -85,8 +85,18 @@ async fn autonomous_game_build_recovery_aborts_legacy_confirmation_batch_and_rep
)
.await
.expect("prepare denied autonomous batch");
let AgentRuntimeProviderActionBatchPreparation::Aborted { mut batch, .. } = preparation else {
panic!("confirmation-only autonomous tool must abort the batch");
let mut batch = match preparation {
AgentRuntimeProviderActionBatchPreparation::Aborted { batch, .. }
| AgentRuntimeProviderActionBatchPreparation::Ready(batch) => batch,
AgentRuntimeProviderActionBatchPreparation::Waiting { .. } => {
panic!("autonomous batch must not wait for human confirmation")
}
AgentRuntimeProviderActionBatchPreparation::NotNeeded => {
panic!("autonomous batch unexpectedly reported NotNeeded")
}
AgentRuntimeProviderActionBatchPreparation::Blocked(observation) => {
panic!("autonomous batch unexpectedly blocked: {observation:?}")
}
};
let rejected_index = batch
.actions
@@ -24,6 +24,21 @@
- 验证方式:Rust 渲染测试(Home 逐字兼容、Project 映射、非法路径);home.suite 附件 Direct invoke 含 `localPath`;无附件不出现 `attachments` 键;做方案首轮仍走 Supervisor 且无 sidecar;后续手打消息不带 attachments。
- 关联文档:`docs/technical/【技术方案】DirectProject本轮附件路径映射-2026-08-31.md`、issue #212
## 2026-08-26 运行中自主扩图提案留在编排层
- 背景:`agent-runtime-orchestration` 已能构造和调度动态 DAG,但 LLM 在执行中发现缺少步骤时没有通用的安全扩图合同。
- 决策:新增严格 serde 的 `GraphProposal``TaskProposal` + `GraphEdge`)和 `GraphLimits`,由 `TaskGraph::apply_proposal` / `expand_with_proposal` 在内存中构造不可变候选图;新节点默认 `Pending`,边方向为前置 `from` → 依赖方 `to`
- 安全与一致性:所有 Agent、端点、重复引用、环、节点/边/深度/扇出预算在候选返回前一次校验;边只能指向新节点,禁止给已运行任务原地追加依赖。任一失败保留旧图。成功后的 epoch、基图版本、proposal 幂等和持久化由宿主负责,crate 不调用 LLM/Provider/ToolHost/Runner,也不写 `.agent/runtime/**`
- 验证:非游戏 conformance 覆盖有效扩图、ready/wave 重算、未知 Agent/端点、重复边、已有任务修改、环、预算、严格 JSON 和原子失败;关联文档为 `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.55。
## 2026-08-26 通用多 Agent DAG 编排与执行内核分层
- 背景:`agent-runtime-core` 已承接 catalog、run/action 生命周期、lane、宿主 ToolHost、spawn/all-join 和 Provider 契约,但动态任务图的 ready 选择、依赖波次与返工下游闭包仍混在 `platform-agent::game_creation`,其它产品无法复用且非法环会被合并成伪 wave。
- 决策:新增纯 Rust `agent-runtime-orchestration`,依赖方向固定为 `agent-runtime-orchestration -> agent-runtime-core`。公共层只持有任务 ID、Agent ID、通用状态和依赖边,统一负责构图校验、ready、active/satisfied 波次、下游闭包和全量/返工选择;动态构图仍必须是 DAG,跨轮循环通过新的 pass / epoch 表达。
- 产品边界:16 个游戏任务、六组角色、产物/验收条件、Evaluator Markdown 和中文语义路由继续留在 `platform-agent`;AGC 组合根使用公共层校验任务图与 `AgentCatalog`。Runtime store、Runner、Provider、权限、ToolHost、委派 journal、isolated write scope 和 `.agent/runtime/**` 不迁移、不双写。
- 验证方式:非游戏 conformance 覆盖并行分支、汇合、repair closure、AgentCatalog 和非法图失败关闭;`platform-agent` 锁定种子 DAG 与现役波次/返工顺序,并验证环拒绝和 catalog 注入。根检查脚本必须执行新 crate 测试。
- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.54。
## 2026-08-27 `plan.submit_gdd` 拒绝无审批决定的 `user_revision`
- 背景:结构校验允许 `round=0 + user_revision + confirmed`,提交闸原先只做结构、身份和 Session CAS。Provider 可在首次 collecting、澄清续跑或提交前质量返工里把未确认项标成用户审批修改,审批卡显示「已确认」。
@@ -62,4 +62,4 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m
## Gitea CI 依赖闭合
`.gitea/workflows/project-ci.yml``Native shell tests` 在运行原生壳门禁前,必须使用 `cargo fetch --locked` 预取 `server-rs/Cargo.toml`、桌面壳和 AGC 壳三份依赖。AGC 壳检查还会运行 `platform-llm``shared-contracts` 的 server-rs workspace 测试,这些命令以及 AGC 壳测试必须带 `--locked`,避免在测试阶段重新解析 registry index;锁文件发生变化时应先更新受信任 CI 镜像缓存,再重跑门禁。
`.gitea/workflows/project-ci.yml``Native shell tests` 在运行原生壳门禁前,必须使用 `cargo fetch --locked` 预取 `server-rs/Cargo.toml`、桌面壳和 AGC 壳三份依赖。Backend host workspace tests 使用 `cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`,避免 `spacetime-module``spacetime-types` feature 统一污染普通领域 crate 的 host 测试;随后单独执行 `cargo test --locked -p spacetime-module --no-fail-fast`,由 `spacetime-module/src/active.rs` 在 host 测试构建期间提供仅测试期的 SpacetimeDB ABI 链接支持,使该 crate 的纯单元测试也纳入 Backend 门禁。`spacetime-module` 的 reducer / procedure 运行时行为仍必须通过真实 SpacetimeDB runtime/integration harness 验证,host 链接支持不得被当作运行时替身。Backend 另外执行 `cargo check --locked -p spacetime-module` 验证模块源码。AGC 壳检查还会运行 `platform-llm``shared-contracts` 的 server-rs workspace 测试,这些命令以及 AGC 壳测试必须带 `--locked`,避免在测试阶段重新解析 registry index;锁文件发生变化时应先更新受信任 CI 镜像缓存,再重跑门禁。
@@ -51,6 +51,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
## AGC DirectProject 与 UI workflow
- 通用 Agent Rust 分层为 `agent-runtime-core`catalog、执行生命周期、ToolHost/spawn/all-join/Provider 契约)、`agent-runtime-orchestration`(动态无环任务图、ready、依赖波次、返工下游闭包和受限自主扩图提案)与 `platform-agent` 游戏适配器;循环返工通过新 pass / epoch 表达,不在单张依赖图中建立回边。LLM 可经宿主结构化 function call 提出新增节点/边,编排层只生成经校验的新候选图,epoch 与持久化仍由宿主掌控。
- DirectProject 只连接客户端内置的 `agc_tools` STDIO MCP。它负责审核引用读取、标准美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`;付费资源调用由客户端绑定回合、幂等账本、请求上限和投影权威。
- DirectProject 的 Codex 原生文件、搜索、命令、图片查看和 Skill 仅在真实 `game/` cwd 与 `workspaceWrite(writableRoots=[game])` 内可用;原生命令网络保持关闭。多 Agent、Apps、插件、hooks、图片生成、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制保持关闭。app-server 使用隔离 `CODEX_HOME`provider 凭据只由 AGC 客户端代理持有,不能进入模型上下文或 shell 环境。
- `ui-prototype`(设计图片)与 UI 编辑器 `UI` JSON 是不同资源。白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`,由 provider-backed 识别、合并和组件绑定持久化 State/revision,并把 `reference-ready → structure-ready → merge-ready → binding-ready → application-ready → completed` 投影到 manifest。Provider 缺失、请求失败、工具缺失、结果不匹配或仍有待审节点时保留真实阶段并返回 blocker,不得用 deterministic seed 伪造完成。
@@ -1651,6 +1651,58 @@ V1.53 把根 Project Supervisor 的 same-run steer 从“收到消息立即中
- LLM 判定、解析或持久化失败时写入关联的非终态 fallback 回复,保持当前任务运行,并在下一安全边界应用已排队 steer;失败不能退回“默认中断”。判定与回复按 `agentId / runId / steerId` 幂等,冲突终态失败关闭。
- 验收必须覆盖判定 LLM 的 `true / false` 协议、公开回复幂等、入队本身不中断、`runtime.steer` 有活动 Provider 时仍不中断、缺失 decision 拒绝条件中断、`false` decision 不中断、`true` decision 只中断旧 cursor,以及判定失败后同一 run 继续。
## V1.54 通用多 Agent DAG 编排 crate
V1.48-V1.50 已把 catalog、执行生命周期与 Provider 契约收进 `agent-runtime-core`,但动态任务图的 ready 选择、依赖波次和返工下游闭包仍编译在 `platform-agent::game_creation`。V1.54 新增独立 `server-rs/crates/agent-runtime-orchestration`package name `agent-runtime-orchestration`),作为 `agent-runtime-core` 上方的纯 Rust 编排层;它不复制 Runtime store、Runner、Provider、ToolHost、delegation journal、权限或产品持久文件。
### 图模型与无环合同
- 公共层只保存任务 ID、执行 Agent ID、通用状态和依赖边,不保存游戏组别、角色文案、产物路径、Evaluator Markdown 或中文关键词。任务在运行时注册,因此“动态 DAG”表示宿主可以按目标或 pass 动态构图与重规划,不表示依赖边可以形成环。
- 构图一次性失败关闭:拒绝空或非法 ID、重复任务、重复依赖、未知依赖、自依赖和任意有向环,并保持任务注册顺序作为所有确定性输出的稳定顺序。环内没有可证明的首个 ready 节点,也无法给 completion、重放和下游失效定义单调顺序;需要迭代时由宿主建立新的 pass / epoch,并显式携带上一轮结果,不在同一依赖图里回边。
- 编排层提供 pending ready 选择、active/satisfied 依赖波次、指定任务的下游影响闭包和全量/返工选择计划。active 任务依赖的非 active 节点必须显式位于 satisfied 集合;缺失前置不能按“图外即完成”静默放行。
- 每个任务携带 `agentId`,并可对 V1.48 `AgentCatalog` 做引用校验;未知 Agent 在创建任何 run 或调用宿主前失败关闭。crate 依赖方向固定为 `agent-runtime-orchestration -> agent-runtime-core`,依赖闭包只使用标准库与 `serde / serde_json`,不得反向依赖 AGC、Tauri、`platform-agent``platform-llm`
### AGC 生产适配与兼容
- `platform-agent` 继续拥有 16 个游戏任务、六组枚举、标题/角色/产物/验收条件、Evaluator Markdown 解析和游戏关键词路由;它把现有 `GameCreationTaskGraph` 映射为公共任务图,并由公共层计算 ready、dependency waves 和 repair downstream closure。
- `GameCreationTaskGraph``GameCreationAgentPassPlan` 的 serde 字段、种子任务、有效 DAG 的顺序与现役 `.agent/passes/pass-N/task-graph.json` 输出保持不变。构图或计划函数改为显式返回错误,非法环、未知依赖或不完整 partition 不再合并成一个伪 wave。
- AGC 的现役 Agent catalog 构造同时用公共编排层校验 16 个任务的 Agent 引用,证明新 crate 已进入生产组合根。isolated child 的项目路径、write scope、证据和深度限制仍是 AGC 工具/策略合同;通用 spawn/all-join 继续由 `agent-runtime-core` 执行,本轮不再造第二套委派协议。
### V1.54 验收
- 新 crate 的非游戏 conformance fixture 在运行时构造并行分支与汇合节点,覆盖稳定 ready 集合、依赖波次、返工下游闭包、AgentCatalog 引用,以及重复/未知/自依赖/有环/缺失 satisfied 的失败关闭;fixture 不得出现游戏任务名、AGC 路径或 Tauri 类型。
- `platform-agent` 回归锁定 16 任务种子图、首轮全量波次、结构化返工和下游扩展的现有顺序,并增加非法游戏图不会进入计划的负向用例;AGC adapter 回归锁定 catalog 与任务图一致。
- 根脚本增加 `agent-runtime-orchestration:check`,并纳入 `ai-game-creator-shell:check`。完成后至少运行新 crate、`platform-agent`、AGC adapter/生成编排定向测试、Tauri `cargo check --tests`、依赖树、`npm run check:encoding``git diff --check`;独立 crate 产生的本地 `Cargo.lock/target` 不进入提交。
### V1.54 本轮验证记录(2026-08-26
- 已通过:`agent-runtime-core` 20 项、`agent-runtime-orchestration` 5 项、`platform-agent` 19 项;新 crate 依赖树仅引入 `agent-runtime-core``serde`(测试专用 `serde_json`),`cargo fmt --check``npm run check:encoding``git diff --check` 均通过。
- 已通过:AGC 任务图与注入 `AgentCatalog` 的一致性测试、非法环失败关闭、反序列化重复依赖失败关闭;测试产生的独立 crate `Cargo.lock/target` 已清理。
- 未完成:Tauri `cargo check --tests` 已编译到 AGC 自定义 `build.rs`,随后因仓库四个候选路径均缺少内置 Codex CLI vendor 资源而退出(`build.rs:77`);本轮未执行 `npm ci`,因此不能将 Tauri 组合根或完整 `ai-game-creator-shell:check` 记为通过。
- 未执行:当前 Rust 1.96 工具链未安装 `clippy` component;没有把该静态检查结果用其它门禁结果替代。
## V1.55 运行中自主扩图提案
V1.54 的公共编排层可以在运行前构造动态 DAG,但 LLM 在执行过程中发现缺少步骤时还没有一个通用、受限的扩图入口。V1.55 在同一 `agent-runtime-orchestration` crate 增加结构化 `GraphProposal`,让宿主能够把 LLM 的 function-call arguments 解析为候选节点和边,并在不改变 `agent-runtime-core` 执行职责的前提下生成下一张图。
### 提案 DTO 与宿主边界
- `GraphProposal` 只包含 `nodes``edges`。节点使用 `{ id, agentId }`,边使用 `{ from, to }``from` 是前置任务,`to` 是依赖它的任务。DTO 使用 `camelCase` 且拒绝未知字段,节点和边的稳定标识沿用公共图模型约束。
- `TaskGraph::apply_proposal` / `expand_with_proposal` 是纯校验与候选构造 API:不调用 LLM、Provider、ToolHost、Runner 或持久化。宿主负责声明 function tool、把 arguments 反序列化为 `GraphProposal`,并在成功后把返回的候选图写入自己的新 epoch。
- 新节点统一以 `Pending` 加入,并保留现有任务状态和注册顺序。提案边必须指向本次新增节点;不允许在运行中的旧任务上原地追加前置依赖。若业务确实要改旧边,宿主应构造完整候选图并按自己的 CAS/epoch 合同一次替换。
### 原子校验与预算
- 候选图只有在所有检查通过后才返回;未知 Agent、未知端点、重复节点/边、自依赖、有向环和已有节点依赖修改都会失败,原图保持不变。
- `GraphLimits` 同时限制完整候选图的 `maxTasks``maxEdges``maxDepth`(根层计 1)和 `maxOutDegree`(一个前置任务的直接下游数)。默认值为 `128 / 512 / 32 / 32`;超限不截断、不部分提交。
- 成功扩图后,宿主必须把它视为新的 graph/epoch,重新计算 ready task 与 dependency waves,并在自己的持久层记录提案身份、基图版本和幂等结果。crate 不把 epoch、proposal ID 或执行事实写入图,也不自动重放 Provider。
### V1.55 验收
- 非游戏 conformance 覆盖有效新增节点/边、全部新节点 `Pending`、ready/wave 重算、未知 Agent、未知端点、重复边、已有任务修改、环、节点/边/深度/扇出预算和失败原子性。
- 覆盖 `GraphProposal``GraphEdge``GraphLimits``TaskGraph` 的严格 JSON round-trip;未知字段、非法标识和零预算均失败关闭。
- 真实 LLM 接入仍由宿主后续提供;本切片证明了宿主可在一个结构化 function call 回合中安全生成候选新图,但不把 provider 请求或持久化当作 crate 的事实源。
## 验收命令
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`

Some files were not shown because too many files have changed in this diff Show More