Files
Genarrative/server-rs/crates/platform-agent/src/game_creation.rs
T
kdletters d2254d1e8c
Project CI / Repository checks (push) Successful in 2m16s
Project CI / Frontend tests (push) Successful in 6m37s
Project CI / Backend tests (push) Failing after 7m45s
Project CI / Native shell tests (push) Failing after 7m6s
抽取多 Agent 编排 crate 并支持运行中自主扩图 (#207)
## 概要

- 抽取通用 `agent-runtime-orchestration` crate,承接多 Agent DAG 的构图校验、ready/wave、下游闭包和全量/返工选择。
- 保留 `platform-agent` 的游戏领域任务与语义路由,避免把 Runtime、Provider、ToolHost 和持久化职责下沉到公共编排层。
- 增加 `GraphProposal` / `TaskProposal` / `GraphEdge` / `GraphLimits`,允许宿主在执行中安全应用 LLM 提出的新增节点和边。
- 扩图采用候选图原子校验:未知 Agent/端点、重复边、自依赖、环及节点/边/深度/扇出预算都会拒绝,失败时原图保持不变;新增节点默认为 `Pending`。

## 验证

- `npm run agent-runtime-orchestration:check`(15 项通过)
- `cargo test --manifest-path server-rs/crates/platform-agent/Cargo.toml`(19 项通过)
- `npm run agc:skill-pack:check`
- `npm run check:encoding`
- `git diff --check`

前端 typecheck 本轮未执行:当前工作树未安装 `node_modules/tsc`,命令会报 `tsc is not recognized`。

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/207
2026-08-31 10:51:11 +08:00

2136 lines
74 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use agent_runtime_core::AgentCatalog;
use agent_runtime_orchestration::{
OrchestrationError, PlanSelection, TaskGraph, TaskNode, TaskStatus,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::error::PlatformAgentError;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GameCreationAgentGroup {
Design,
Art,
Code,
Balance,
Audio,
Publishing,
}
pub const GAME_CREATION_AGENT_GROUPS: [GameCreationAgentGroup; 6] = [
GameCreationAgentGroup::Design,
GameCreationAgentGroup::Art,
GameCreationAgentGroup::Code,
GameCreationAgentGroup::Balance,
GameCreationAgentGroup::Audio,
GameCreationAgentGroup::Publishing,
];
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GameCreationTaskStatus {
Pending,
Running,
WaitingForConfirmation,
Completed,
Failed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationTask {
pub id: String,
pub title: String,
pub group: GameCreationAgentGroup,
pub role: String,
pub status: GameCreationTaskStatus,
pub dependencies: Vec<String>,
pub artifacts: Vec<String>,
pub acceptance_criteria: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationTaskGraph {
pub goal: String,
pub tasks: Vec<GameCreationTask>,
}
pub const GAME_CREATION_ISOLATED_AGENT_MIN_CHILDREN: usize = 1;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN: usize = 3;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_DEPTH: u8 = 1;
pub const GAME_CREATION_ISOLATED_AGENT_TEMPLATE_ID_MAX_CHARS: usize = 96;
pub const GAME_CREATION_ISOLATED_AGENT_TASK_MAX_CHARS: usize = 4_000;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_ACCEPTANCE_CRITERIA: usize = 8;
pub const GAME_CREATION_ISOLATED_AGENT_ACCEPTANCE_CRITERION_MAX_CHARS: usize = 500;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_EXPECTED_ARTIFACTS: usize = 8;
pub const GAME_CREATION_ISOLATED_AGENT_EXPECTED_ARTIFACT_MAX_CHARS: usize = 240;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_WRITE_SCOPES: usize = 8;
pub const GAME_CREATION_ISOLATED_AGENT_WRITE_SCOPE_MAX_CHARS: usize = 240;
pub const GAME_CREATION_ISOLATED_AGENT_INSTANCE_HASH_CHARS: usize = 24;
pub const GAME_CREATION_ISOLATED_AGENT_RESULT_SUMMARY_MAX_CHARS: usize = 2_000;
pub const GAME_CREATION_ISOLATED_AGENT_RESULT_ERROR_MAX_CHARS: usize = 2_000;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_ARTIFACTS: usize = 32;
pub const GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_EVIDENCE: usize = 32;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GameCreationIsolatedAgentJoinMode {
All,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentChildSpec {
pub template_agent_id: String,
pub task: String,
pub acceptance_criteria: Vec<String>,
pub expected_artifacts: Vec<String>,
pub write_scopes: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentSpawnRequest {
pub children: Vec<GameCreationIsolatedAgentChildSpec>,
pub join_mode: GameCreationIsolatedAgentJoinMode,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentDerivedIdentity {
pub child_index: usize,
pub delegation_group_id: String,
pub delegation_id: String,
pub instance_id: String,
pub template_agent_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentDelegationGroup {
pub parent_action_id: String,
pub delegation_group_id: String,
pub join_run_id: String,
pub depth: u8,
pub join_mode: GameCreationIsolatedAgentJoinMode,
pub children: Vec<GameCreationIsolatedAgentDerivedIdentity>,
}
pub type GameCreationIsolatedAgentDerivedGroup = GameCreationIsolatedAgentDelegationGroup;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GameCreationIsolatedAgentResultStatus {
Completed,
Failed,
Cancelled,
BudgetExhausted,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentArtifact {
pub path: String,
pub sha256: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentEvidence {
pub kind: String,
pub summary: String,
pub path: Option<String>,
pub sha256: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentChildResult {
pub delegation_id: String,
pub instance_id: String,
pub template_agent_id: String,
pub run_id: String,
pub status: GameCreationIsolatedAgentResultStatus,
pub summary: String,
pub artifacts: Vec<GameCreationIsolatedAgentArtifact>,
pub evidence: Vec<GameCreationIsolatedAgentEvidence>,
pub verified_revision: Option<u64>,
pub error: Option<String>,
}
pub type GameCreationIsolatedAgentResult = GameCreationIsolatedAgentChildResult;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationIsolatedAgentJoinResult {
pub delegation_group_id: String,
pub run_id: String,
pub join_mode: GameCreationIsolatedAgentJoinMode,
pub results: Vec<GameCreationIsolatedAgentChildResult>,
}
pub fn validate_game_creation_isolated_agent_spawn_request(
request: &GameCreationIsolatedAgentSpawnRequest,
) -> Result<(), PlatformAgentError> {
validate_game_creation_isolated_agent_spawn_request_at_depth(request, 0)
}
pub fn validate_game_creation_isolated_agent_spawn_request_at_depth(
request: &GameCreationIsolatedAgentSpawnRequest,
parent_depth: u8,
) -> Result<(), PlatformAgentError> {
if parent_depth >= GAME_CREATION_ISOLATED_AGENT_MAX_DEPTH {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent 最大深度为 {GAME_CREATION_ISOLATED_AGENT_MAX_DEPTH}"
)));
}
if !(GAME_CREATION_ISOLATED_AGENT_MIN_CHILDREN..=GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN)
.contains(&request.children.len())
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent children 数量必须为 {GAME_CREATION_ISOLATED_AGENT_MIN_CHILDREN}..={GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN}"
)));
}
let mut write_scope_prefixes = Vec::<(usize, &str, &str)>::new();
for (child_index, child) in request.children.iter().enumerate() {
validate_game_creation_isolated_agent_child_spec(child, child_index)?;
for scope in &child.write_scopes {
let prefix = scope
.strip_suffix("/**")
.expect("validated write scope must end with /**");
for (sibling_index, sibling_scope, sibling_prefix) in &write_scope_prefixes {
if *sibling_index != child_index
&& project_path_prefixes_overlap(prefix, sibling_prefix)
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent sibling writeScopes 不得重叠:{sibling_scope} 与 {scope}"
)));
}
}
write_scope_prefixes.push((child_index, scope, prefix));
}
}
Ok(())
}
pub fn derive_game_creation_isolated_agent_identity(
parent_action_id: impl AsRef<str>,
child_index: usize,
template_agent_id: impl AsRef<str>,
) -> Result<GameCreationIsolatedAgentDerivedIdentity, PlatformAgentError> {
let parent_action_id = parent_action_id.as_ref();
let template_agent_id = template_agent_id.as_ref();
validate_isolated_agent_parent_action_id(parent_action_id)?;
if child_index >= GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent childIndex 必须小于 {GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN}"
)));
}
validate_isolated_agent_safe_id(
template_agent_id,
"templateAgentId",
GAME_CREATION_ISOLATED_AGENT_TEMPLATE_ID_MAX_CHARS,
)?;
let delegation_group_id = isolated_agent_sha256_hex(parent_action_id.as_bytes());
let delegation_id =
isolated_agent_sha256_hex(format!("{delegation_group_id}{child_index}").as_bytes());
let instance_id = format!(
"child-{}",
delegation_id
.chars()
.take(GAME_CREATION_ISOLATED_AGENT_INSTANCE_HASH_CHARS)
.collect::<String>()
);
Ok(GameCreationIsolatedAgentDerivedIdentity {
child_index,
delegation_group_id,
delegation_id,
instance_id,
template_agent_id: template_agent_id.to_string(),
})
}
pub fn derive_game_creation_isolated_agent_group(
parent_action_id: impl AsRef<str>,
request: &GameCreationIsolatedAgentSpawnRequest,
) -> Result<GameCreationIsolatedAgentDelegationGroup, PlatformAgentError> {
derive_game_creation_isolated_agent_group_at_depth(parent_action_id, request, 0)
}
pub fn derive_game_creation_isolated_agent_group_at_depth(
parent_action_id: impl AsRef<str>,
request: &GameCreationIsolatedAgentSpawnRequest,
parent_depth: u8,
) -> Result<GameCreationIsolatedAgentDelegationGroup, PlatformAgentError> {
let parent_action_id = parent_action_id.as_ref();
validate_game_creation_isolated_agent_spawn_request_at_depth(request, parent_depth)?;
validate_isolated_agent_parent_action_id(parent_action_id)?;
let children = request
.children
.iter()
.enumerate()
.map(|(child_index, child)| {
derive_game_creation_isolated_agent_identity(
parent_action_id,
child_index,
&child.template_agent_id,
)
})
.collect::<Result<Vec<_>, _>>()?;
let delegation_group_id = isolated_agent_sha256_hex(parent_action_id.as_bytes());
let join_run_id = format!(
"agent-isolated-join-{}",
delegation_group_id
.chars()
.take(GAME_CREATION_ISOLATED_AGENT_INSTANCE_HASH_CHARS)
.collect::<String>()
);
Ok(GameCreationIsolatedAgentDelegationGroup {
parent_action_id: parent_action_id.to_string(),
delegation_group_id,
join_run_id,
depth: parent_depth + 1,
join_mode: request.join_mode,
children,
})
}
pub fn validate_game_creation_isolated_agent_child_result(
result: &GameCreationIsolatedAgentChildResult,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_sha256(&result.delegation_id, "delegationId")?;
validate_isolated_agent_safe_id(&result.instance_id, "instanceId", 96)?;
if !result.instance_id.starts_with("child-") {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent instanceId 必须以 child- 开头",
));
}
validate_isolated_agent_safe_id(
&result.template_agent_id,
"templateAgentId",
GAME_CREATION_ISOLATED_AGENT_TEMPLATE_ID_MAX_CHARS,
)?;
validate_isolated_agent_text(&result.run_id, "runId", 160, false)?;
validate_isolated_agent_text(
&result.summary,
"result.summary",
GAME_CREATION_ISOLATED_AGENT_RESULT_SUMMARY_MAX_CHARS,
true,
)?;
if result.artifacts.len() > GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_ARTIFACTS {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent result.artifacts 最多支持 {GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_ARTIFACTS} 项"
)));
}
if result.evidence.len() > GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_EVIDENCE {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent result.evidence 最多支持 {GAME_CREATION_ISOLATED_AGENT_MAX_RESULT_EVIDENCE} 项"
)));
}
let mut artifact_paths = HashSet::new();
for artifact in &result.artifacts {
validate_isolated_agent_project_relative_path(
&artifact.path,
"result.artifacts.path",
GAME_CREATION_ISOLATED_AGENT_EXPECTED_ARTIFACT_MAX_CHARS,
false,
)?;
validate_isolated_agent_sha256(&artifact.sha256, "result.artifacts.sha256")?;
if !artifact_paths.insert(artifact.path.as_str()) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent result.artifacts path 不能重复:{}",
artifact.path
)));
}
}
for evidence in &result.evidence {
validate_isolated_agent_evidence(evidence)?;
}
if result.verified_revision == Some(0) {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent verifiedRevision 必须大于 0",
));
}
if let Some(error) = &result.error {
validate_isolated_agent_text(
error,
"result.error",
GAME_CREATION_ISOLATED_AGENT_RESULT_ERROR_MAX_CHARS,
true,
)?;
}
match result.status {
GameCreationIsolatedAgentResultStatus::Completed if result.error.is_some() => {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent completed 结果不能携带 error",
));
}
GameCreationIsolatedAgentResultStatus::Failed
| GameCreationIsolatedAgentResultStatus::BudgetExhausted
if result.error.is_none() =>
{
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent failed 或 budget-exhausted 结果必须携带 error",
));
}
_ => {}
}
Ok(())
}
pub fn join_game_creation_isolated_agent_results(
group: &GameCreationIsolatedAgentDelegationGroup,
results: impl AsRef<[GameCreationIsolatedAgentChildResult]>,
) -> Result<GameCreationIsolatedAgentJoinResult, PlatformAgentError> {
validate_game_creation_isolated_agent_delegation_group(group)?;
let results = results.as_ref();
if results.len() != group.children.len() {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent joinMode=all 需要 {} 个终态结果,实际收到 {} 个",
group.children.len(),
results.len()
)));
}
let mut joined = Vec::with_capacity(results.len());
let mut seen_delegation_ids = HashSet::new();
for result in results {
validate_game_creation_isolated_agent_child_result(result)?;
if !seen_delegation_ids.insert(result.delegation_id.as_str()) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent join 结果 delegationId 重复:{}",
result.delegation_id
)));
}
}
for expected in &group.children {
let Some(result) = results
.iter()
.find(|result| result.delegation_id == expected.delegation_id)
else {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent join 缺少 childIndex={} 的结果",
expected.child_index
)));
};
if result.instance_id != expected.instance_id
|| result.template_agent_id != expected.template_agent_id
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent join 身份不一致:childIndex={},delegationId={}",
expected.child_index, expected.delegation_id
)));
}
joined.push(result.clone());
}
Ok(GameCreationIsolatedAgentJoinResult {
delegation_group_id: group.delegation_group_id.clone(),
run_id: group.join_run_id.clone(),
join_mode: group.join_mode,
results: joined,
})
}
fn validate_game_creation_isolated_agent_child_spec(
child: &GameCreationIsolatedAgentChildSpec,
child_index: usize,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_safe_id(
&child.template_agent_id,
&format!("children[{child_index}].templateAgentId"),
GAME_CREATION_ISOLATED_AGENT_TEMPLATE_ID_MAX_CHARS,
)?;
validate_isolated_agent_text(
&child.task,
&format!("children[{child_index}].task"),
GAME_CREATION_ISOLATED_AGENT_TASK_MAX_CHARS,
true,
)?;
validate_isolated_agent_text_list(
&child.acceptance_criteria,
&format!("children[{child_index}].acceptanceCriteria"),
1,
GAME_CREATION_ISOLATED_AGENT_MAX_ACCEPTANCE_CRITERIA,
GAME_CREATION_ISOLATED_AGENT_ACCEPTANCE_CRITERION_MAX_CHARS,
)?;
validate_isolated_agent_text_list(
&child.expected_artifacts,
&format!("children[{child_index}].expectedArtifacts"),
1,
GAME_CREATION_ISOLATED_AGENT_MAX_EXPECTED_ARTIFACTS,
GAME_CREATION_ISOLATED_AGENT_EXPECTED_ARTIFACT_MAX_CHARS,
)?;
validate_isolated_agent_text_list(
&child.write_scopes,
&format!("children[{child_index}].writeScopes"),
1,
GAME_CREATION_ISOLATED_AGENT_MAX_WRITE_SCOPES,
GAME_CREATION_ISOLATED_AGENT_WRITE_SCOPE_MAX_CHARS,
)?;
for artifact in &child.expected_artifacts {
validate_isolated_agent_project_relative_path(
artifact,
&format!("children[{child_index}].expectedArtifacts"),
GAME_CREATION_ISOLATED_AGENT_EXPECTED_ARTIFACT_MAX_CHARS,
true,
)?;
}
for scope in &child.write_scopes {
validate_isolated_agent_write_scope(scope, child_index)?;
}
Ok(())
}
fn validate_game_creation_isolated_agent_delegation_group(
group: &GameCreationIsolatedAgentDelegationGroup,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_parent_action_id(&group.parent_action_id)?;
if group.depth == 0 || group.depth > GAME_CREATION_ISOLATED_AGENT_MAX_DEPTH {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent group.depth 必须为 1..={GAME_CREATION_ISOLATED_AGENT_MAX_DEPTH}"
)));
}
if !(GAME_CREATION_ISOLATED_AGENT_MIN_CHILDREN..=GAME_CREATION_ISOLATED_AGENT_MAX_CHILDREN)
.contains(&group.children.len())
{
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent group.children 数量非法",
));
}
let expected_group_id = isolated_agent_sha256_hex(group.parent_action_id.as_bytes());
if group.delegation_group_id != expected_group_id {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent delegationGroupId 与 parentActionId 不匹配",
));
}
let expected_join_run_id = format!(
"agent-isolated-join-{}",
expected_group_id
.chars()
.take(GAME_CREATION_ISOLATED_AGENT_INSTANCE_HASH_CHARS)
.collect::<String>()
);
if group.join_run_id != expected_join_run_id {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent joinRunId 与 delegationGroupId 不匹配",
));
}
for (child_index, child) in group.children.iter().enumerate() {
let expected = derive_game_creation_isolated_agent_identity(
&group.parent_action_id,
child_index,
&child.template_agent_id,
)?;
if child != &expected {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent 派生身份不一致:childIndex={child_index}"
)));
}
}
Ok(())
}
fn validate_isolated_agent_parent_action_id(
parent_action_id: &str,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_text(parent_action_id, "parentActionId", 256, false)
}
fn validate_isolated_agent_safe_id(
value: &str,
label: &str,
max_chars: usize,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_text(value, label, max_chars, false)?;
if value.chars().any(|character| {
!(character.is_ascii_alphanumeric() || character == '-' || character == '_')
}) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 只能包含 ASCII 字母、数字、短横线和下划线"
)));
}
Ok(())
}
fn validate_isolated_agent_text_list(
values: &[String],
label: &str,
min_items: usize,
max_items: usize,
max_chars: usize,
) -> Result<(), PlatformAgentError> {
if !(min_items..=max_items).contains(&values.len()) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 数量必须为 {min_items}..={max_items}"
)));
}
let mut seen = HashSet::new();
for value in values {
validate_isolated_agent_text(value, label, max_chars, false)?;
if !seen.insert(value.as_str()) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 不能包含重复项:{value}"
)));
}
}
Ok(())
}
fn validate_isolated_agent_text(
value: &str,
label: &str,
max_chars: usize,
allow_multiline: bool,
) -> Result<(), PlatformAgentError> {
if value.trim().is_empty() {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 不能为空"
)));
}
if value.trim() != value {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 首尾不能包含空白字符"
)));
}
let char_count = value.chars().count();
if char_count > max_chars {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 不能超过 {max_chars} 个字符"
)));
}
if value.chars().any(|character| {
character.is_control() && !(allow_multiline && matches!(character, '\n' | '\t'))
}) {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 包含非法控制字符"
)));
}
Ok(())
}
fn validate_isolated_agent_write_scope(
scope: &str,
child_index: usize,
) -> Result<(), PlatformAgentError> {
let Some(prefix) = scope.strip_suffix("/**") else {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent children[{child_index}].writeScopes 必须是以 /** 结尾的项目相对 glob 前缀:{scope}"
)));
};
validate_isolated_agent_project_relative_path(
prefix,
&format!("children[{child_index}].writeScopes"),
GAME_CREATION_ISOLATED_AGENT_WRITE_SCOPE_MAX_CHARS - 3,
false,
)
}
fn validate_isolated_agent_project_relative_path(
value: &str,
label: &str,
max_chars: usize,
allow_glob: bool,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_text(value, label, max_chars, false)?;
if value.starts_with('/')
|| value.starts_with('~')
|| value.contains('\\')
|| value.contains(':')
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 必须是项目相对路径:{value}"
)));
}
for part in value.split('/') {
if part.is_empty() || part == "." || part == ".." {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 包含非法路径段:{value}"
)));
}
if allow_glob {
if part
.chars()
.any(|character| matches!(character, '?' | '[' | ']' | '{' | '}' | '!'))
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 只支持 * glob:{value}"
)));
}
} else if part.contains('*') {
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 不能包含 glob:{value}"
)));
}
}
Ok(())
}
fn project_path_prefixes_overlap(left: &str, right: &str) -> bool {
let left = left.split('/').collect::<Vec<_>>();
let right = right.split('/').collect::<Vec<_>>();
left.iter()
.zip(right.iter())
.take(left.len().min(right.len()))
.all(|(left, right)| left == right)
}
fn validate_isolated_agent_evidence(
evidence: &GameCreationIsolatedAgentEvidence,
) -> Result<(), PlatformAgentError> {
validate_isolated_agent_text(&evidence.kind, "result.evidence.kind", 64, false)?;
if evidence.kind.chars().any(|character| {
!(character.is_ascii_alphanumeric()
|| character == '-'
|| character == '_'
|| character == '.')
}) {
return Err(invalid_isolated_agent_input(
"动态隔离子 Agent result.evidence.kind 只能包含 ASCII 字母、数字、短横线、下划线和点",
));
}
validate_isolated_agent_text(&evidence.summary, "result.evidence.summary", 1_000, true)?;
if let Some(path) = &evidence.path {
validate_isolated_agent_project_relative_path(path, "result.evidence.path", 240, false)?;
}
if let Some(sha256) = &evidence.sha256 {
validate_isolated_agent_sha256(sha256, "result.evidence.sha256")?;
}
Ok(())
}
fn validate_isolated_agent_sha256(value: &str, label: &str) -> Result<(), PlatformAgentError> {
if value.len() != 64
|| value
.bytes()
.any(|byte| !(byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)))
{
return Err(invalid_isolated_agent_input(format!(
"动态隔离子 Agent {label} 必须是 64 位小写十六进制 SHA-256"
)));
}
Ok(())
}
fn invalid_isolated_agent_input(message: impl Into<String>) -> PlatformAgentError {
PlatformAgentError::InvalidInput(message.into())
}
// platform-agent deliberately keeps this domain contract dependency-free.
fn isolated_agent_sha256_hex(input: &[u8]) -> String {
const INITIAL: [u32; 8] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
];
const ROUND: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
let bit_len = (input.len() as u64) * 8;
let mut padded = Vec::with_capacity(input.len() + 72);
padded.extend_from_slice(input);
padded.push(0x80);
while padded.len() % 64 != 56 {
padded.push(0);
}
padded.extend_from_slice(&bit_len.to_be_bytes());
let mut state = INITIAL;
for chunk in padded.chunks_exact(64) {
let mut words = [0_u32; 64];
for (index, bytes) in chunk.chunks_exact(4).enumerate() {
words[index] = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
}
for index in 16..64 {
let sigma0 = words[index - 15].rotate_right(7)
^ words[index - 15].rotate_right(18)
^ (words[index - 15] >> 3);
let sigma1 = words[index - 2].rotate_right(17)
^ words[index - 2].rotate_right(19)
^ (words[index - 2] >> 10);
words[index] = words[index - 16]
.wrapping_add(sigma0)
.wrapping_add(words[index - 7])
.wrapping_add(sigma1);
}
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
for index in 0..64 {
let sum1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let choose = (e & f) ^ ((!e) & g);
let temp1 = h
.wrapping_add(sum1)
.wrapping_add(choose)
.wrapping_add(ROUND[index])
.wrapping_add(words[index]);
let sum0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let majority = (a & b) ^ (a & c) ^ (b & c);
let temp2 = sum0.wrapping_add(majority);
h = g;
g = f;
f = e;
e = d.wrapping_add(temp1);
d = c;
c = b;
b = a;
a = temp1.wrapping_add(temp2);
}
state[0] = state[0].wrapping_add(a);
state[1] = state[1].wrapping_add(b);
state[2] = state[2].wrapping_add(c);
state[3] = state[3].wrapping_add(d);
state[4] = state[4].wrapping_add(e);
state[5] = state[5].wrapping_add(f);
state[6] = state[6].wrapping_add(g);
state[7] = state[7].wrapping_add(h);
}
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(64);
for byte in state.into_iter().flat_map(u32::to_be_bytes) {
output.push(HEX[(byte >> 4) as usize] as char);
output.push(HEX[(byte & 0x0f) as usize] as char);
}
output
}
pub fn build_game_creation_seed_task_graph(
goal: impl AsRef<str>,
) -> Result<GameCreationTaskGraph, PlatformAgentError> {
let goal = goal.as_ref().trim();
if goal.is_empty() {
return Err(PlatformAgentError::InvalidInput(
"游戏创作目标不能为空".to_string(),
));
}
let graph = GameCreationTaskGraph {
goal: goal.to_string(),
tasks: vec![
task(
"design-director",
"拆解创作方向",
GameCreationAgentGroup::Design,
"Director",
[],
[".agent/spec.md"],
["创作目标、范围和专业组分工明确"],
),
task(
"design-foundation",
"确定玩法规格与界面原型",
GameCreationAgentGroup::Design,
"Gameplay",
["design-director"],
[
"memory/project.md",
"game/game_design.md",
"assets/ui-prototype.png",
],
["核心循环、胜负条件和第一版关卡目标明确,且已生成可读的 16:9 横屏界面原型图"],
),
task(
"balance-director",
"确定数值口径",
GameCreationAgentGroup::Balance,
"Director",
["design-foundation"],
[".agent/passes/pass-*/groups/balance/director.md"],
["难度、节奏和得分口径可指导数值表"],
),
task(
"balance-seed",
"生成初版数值",
GameCreationAgentGroup::Balance,
"Difficulty",
["balance-director"],
["game/balance.json"],
["速度、生命、得分和难度参数可被程序组读取"],
),
task(
"art-director",
"确定视觉方向",
GameCreationAgentGroup::Art,
"Director",
["design-foundation"],
[".agent/passes/pass-*/groups/art/director.md"],
["角色、场景和 UI 的统一视觉方向明确"],
),
task(
"art-asset-plan",
"生成首版美术素材",
GameCreationAgentGroup::Art,
"Asset",
["art-director"],
["assets/manifest.art.json", "assets/art-spritesheet.png"],
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记",
],
),
task(
"art-polish",
"检查美术可用性",
GameCreationAgentGroup::Art,
"Polish",
["art-asset-plan"],
[".agent/passes/pass-*/groups/art/polish.md"],
["首版资产不阻塞可玩原型和后续画板精修"],
),
task(
"audio-director",
"确定声音方向",
GameCreationAgentGroup::Audio,
"Director",
["design-foundation"],
[".agent/passes/pass-*/groups/audio/director.md"],
["BGM 氛围和交互音效边界明确"],
),
task(
"audio-asset-plan",
"规划音乐音效",
GameCreationAgentGroup::Audio,
"SFX",
["audio-director"],
["assets/manifest.audio.json"],
["BGM 和核心交互音效需求已明确"],
),
task(
"code-director",
"拆解程序实现",
GameCreationAgentGroup::Code,
"Director",
[
"design-foundation",
"balance-seed",
"art-polish",
"audio-asset-plan",
],
[".agent/passes/pass-*/groups/code/director.md"],
["渲染、输入、状态和数据读取边界明确"],
),
task(
"code-prototype",
"生成可运行原型",
GameCreationAgentGroup::Code,
"Code",
["code-director"],
["game/"],
["本地 Web 游戏项目可以通过 HTTP server 打开"],
),
task(
"quality-review",
"执行质量评审",
GameCreationAgentGroup::Code,
"Review",
["code-prototype"],
[".agent/findings.md", ".agent/run.latest.json"],
["玩法、资产、数值、程序和发布包装通过跨专业组质量评审"],
),
task(
"preview-readiness",
"执行静态自检",
GameCreationAgentGroup::Code,
"Preview",
["quality-review"],
[".agent/logs/command.log", ".agent/run.latest.json"],
["HTML 自包含且通过本地静态 smoke"],
),
task(
"preview-playtest",
"预览并试玩验收",
GameCreationAgentGroup::Code,
"Playtest",
["publish-package"],
[".agent/logs/preview.log"],
["预览不是空白页,主循环和基础输入可用"],
),
task(
"publish-strategy",
"整理运营定位",
GameCreationAgentGroup::Publishing,
"Director",
["preview-readiness"],
[".agent/passes/pass-*/groups/publishing/director.md"],
["标题、卖点、标签和封面方向明确"],
),
task(
"publish-package",
"整理发布包装",
GameCreationAgentGroup::Publishing,
"Publish",
["publish-strategy"],
["exports/README.md"],
["标题、简介、标签、封面需求和导出检查已完成"],
),
],
};
compile_game_creation_task_graph(&graph)?;
Ok(graph)
}
fn compile_game_creation_task_graph(
graph: &GameCreationTaskGraph,
) -> Result<TaskGraph, PlatformAgentError> {
let tasks = graph
.tasks
.iter()
.map(|task| {
TaskNode::try_new(
&task.id,
&task.id,
match task.status {
GameCreationTaskStatus::Pending => TaskStatus::Pending,
GameCreationTaskStatus::Running => TaskStatus::Running,
GameCreationTaskStatus::WaitingForConfirmation => TaskStatus::Waiting,
GameCreationTaskStatus::Completed => TaskStatus::Completed,
GameCreationTaskStatus::Failed => TaskStatus::Failed,
},
task.dependencies.iter().cloned(),
)
})
.collect::<Result<Vec<_>, _>>()
.map_err(invalid_orchestration)?;
TaskGraph::try_new(&graph.goal, tasks).map_err(invalid_orchestration)
}
pub fn validate_game_creation_task_agents(
graph: &GameCreationTaskGraph,
catalog: &AgentCatalog,
) -> Result<(), PlatformAgentError> {
compile_game_creation_task_graph(graph)?
.validate_agents(catalog)
.map_err(invalid_orchestration)
}
pub fn select_ready_game_creation_tasks(
graph: &GameCreationTaskGraph,
) -> Result<Vec<GameCreationTask>, PlatformAgentError> {
let orchestration_graph = compile_game_creation_task_graph(graph)?;
Ok(orchestration_graph
.ready_task_ids()
.into_iter()
.filter_map(|task_id| graph.tasks.iter().find(|task| task.id == task_id))
.cloned()
.collect())
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationAgentRepairRoute {
pub issue: String,
pub task_ids: Vec<String>,
pub reason: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationAgentPassPlan {
pub pass: u8,
pub mode: String,
pub active_task_ids: Vec<String>,
pub carried_task_ids: Vec<String>,
pub dependency_waves: Vec<Vec<String>>,
pub repair_focus: Vec<String>,
pub repair_routes: Vec<GameCreationAgentRepairRoute>,
pub summary: String,
}
pub fn plan_game_creation_agent_pass(
graph: &GameCreationTaskGraph,
pass: u8,
findings_markdown: &str,
) -> Result<GameCreationAgentPassPlan, PlatformAgentError> {
let orchestration_graph = compile_game_creation_task_graph(graph)?;
let structured_repair_routes =
extract_game_creation_evaluator_repair_routes(graph, findings_markdown);
let mut repair_focus = extract_game_creation_evaluator_issues(findings_markdown);
if repair_focus.is_empty() && !structured_repair_routes.is_empty() {
repair_focus = structured_repair_routes
.iter()
.map(|route| route.issue.clone())
.collect();
}
let repair_routes = if pass <= 1 || repair_focus.is_empty() {
Vec::new()
} else if !structured_repair_routes.is_empty() {
structured_repair_routes
} else {
route_game_creation_repair_issues(graph, &repair_focus)
};
let repair_routes =
expand_game_creation_repair_route_impacts(&orchestration_graph, repair_routes)?;
let mut selected_task_ids = Vec::new();
for route in &repair_routes {
for task_id in &route.task_ids {
push_unique(&mut selected_task_ids, task_id);
}
}
let selection = if pass <= 1 || repair_focus.is_empty() || selected_task_ids.is_empty() {
PlanSelection::All
} else {
PlanSelection::Repair {
task_ids: selected_task_ids,
}
};
let orchestration_plan = orchestration_graph
.plan(selection)
.map_err(invalid_orchestration)?;
let active_task_ids = orchestration_plan.active_task_ids().to_vec();
let carried_task_ids = orchestration_plan.carried_task_ids().to_vec();
let dependency_waves = orchestration_plan.dependency_waves().to_vec();
let mode = if pass <= 1 || repair_focus.is_empty() {
"initial"
} else {
"repair"
};
let summary = if mode == "initial" {
format!(
"第 {pass} 轮全量调度 {} 个组内角色任务",
active_task_ids.len()
)
} else if carried_task_ids.is_empty() {
format!(
"第 {pass} 轮按 Evaluator 反馈全量返工 {} 个任务",
active_task_ids.len()
)
} else {
format!(
"第 {pass} 轮按 Evaluator 反馈重跑 {} 个任务,carry-over {} 个任务",
active_task_ids.len(),
carried_task_ids.len()
)
};
Ok(GameCreationAgentPassPlan {
pass,
mode: mode.to_string(),
active_task_ids,
carried_task_ids,
dependency_waves,
repair_focus,
repair_routes,
summary,
})
}
pub fn extract_game_creation_evaluator_issues(findings_markdown: &str) -> Vec<String> {
let explicit_issues = extract_markdown_list_section(findings_markdown, "## 问题");
if !explicit_issues.is_empty() {
return explicit_issues;
}
findings_markdown
.lines()
.map(str::trim)
.scan(false, |in_repair_routes, line| {
if line.starts_with("## Repair Routes") {
*in_repair_routes = true;
return Some(None);
}
if *in_repair_routes && line.starts_with("## ") {
*in_repair_routes = false;
}
Some((!*in_repair_routes).then_some(line))
})
.flatten()
.filter_map(|line| line.strip_prefix("- "))
.filter(|line| !line.starts_with("pass:") && !line.starts_with("status:"))
.filter(|line| {
!line.starts_with("issue:")
&& !line.starts_with("taskIds:")
&& !line.starts_with("reason:")
&& *line != "none"
})
.map(str::trim)
.filter(|line| !line.is_empty() && !line.contains("暂无上一轮问题"))
.map(str::to_string)
.collect()
}
pub fn extract_game_creation_evaluator_repair_routes(
graph: &GameCreationTaskGraph,
findings_markdown: &str,
) -> Vec<GameCreationAgentRepairRoute> {
let Some(payload) = extract_repair_routes_json_payload(findings_markdown) else {
return Vec::new();
};
let Ok(routes) = serde_json::from_str::<Vec<GameCreationAgentRepairRoute>>(payload) else {
return Vec::new();
};
sanitize_repair_routes(graph, routes)
}
pub fn route_game_creation_repair_issues(
graph: &GameCreationTaskGraph,
issues: &[String],
) -> Vec<GameCreationAgentRepairRoute> {
issues
.iter()
.map(|issue| route_game_creation_repair_issue(graph, issue))
.collect()
}
fn extract_markdown_list_section(markdown: &str, heading: &str) -> Vec<String> {
let mut in_section = false;
let mut values = Vec::new();
for line in markdown.lines().map(str::trim) {
if line == heading {
in_section = true;
continue;
}
if in_section && line.starts_with("## ") {
break;
}
if !in_section {
continue;
}
if let Some(value) = line.strip_prefix("- ") {
let value = value.trim();
if !value.is_empty() && !value.contains("暂无上一轮问题") {
values.push(value.to_string());
}
}
}
values
}
fn extract_repair_routes_json_payload(markdown: &str) -> Option<&str> {
let section_start = markdown.find("## Repair Routes")?;
let section = &markdown[section_start..];
let section_end = section
.find("\n## ")
.filter(|end| *end > 0)
.unwrap_or(section.len());
let section = &section[..section_end];
if let Some(fence_start) = section.find("```json") {
let payload_start = fence_start + "```json".len();
let after_start = &section[payload_start..];
let payload_end = after_start.find("```")?;
return Some(after_start[..payload_end].trim());
}
let payload_start = section.find('[')?;
let payload_end = section.rfind(']')?;
if payload_start > payload_end {
return None;
}
Some(section[payload_start..=payload_end].trim())
}
fn sanitize_repair_routes(
graph: &GameCreationTaskGraph,
routes: Vec<GameCreationAgentRepairRoute>,
) -> Vec<GameCreationAgentRepairRoute> {
let known_task_ids = graph
.tasks
.iter()
.map(|task| task.id.as_str())
.collect::<HashSet<_>>();
routes
.into_iter()
.filter_map(|route| {
let issue = route.issue.trim();
if issue.is_empty() {
return None;
}
let mut task_ids = Vec::new();
for task_id in route.task_ids {
let task_id = task_id.trim();
if known_task_ids.contains(task_id) {
push_unique(&mut task_ids, task_id);
}
}
if task_ids.is_empty() {
return None;
}
let reason = route.reason.trim();
Some(GameCreationAgentRepairRoute {
issue: issue.to_string(),
task_ids,
reason: if reason.is_empty() {
"structured".to_string()
} else {
reason.to_string()
},
})
})
.collect()
}
fn expand_game_creation_repair_route_impacts(
graph: &TaskGraph,
routes: Vec<GameCreationAgentRepairRoute>,
) -> Result<Vec<GameCreationAgentRepairRoute>, PlatformAgentError> {
routes
.into_iter()
.map(|route| {
let expanded_task_ids = graph
.expand_downstream(&route.task_ids)
.map_err(invalid_orchestration)?;
let reason = if expanded_task_ids.len() > route.task_ids.len()
&& !route.reason.contains("dependency-impact")
{
format!("{}+dependency-impact", route.reason)
} else {
route.reason
};
Ok(GameCreationAgentRepairRoute {
issue: route.issue,
task_ids: expanded_task_ids,
reason,
})
})
.collect()
}
fn route_game_creation_repair_issue(
graph: &GameCreationTaskGraph,
issue: &str,
) -> GameCreationAgentRepairRoute {
let mut task_ids = Vec::new();
let lower = issue.to_ascii_lowercase();
let mut reasons = Vec::new();
if contains_any(&lower, &["handoff", "handoffs", "交接", "专业组", "六组"]) {
task_ids = graph.tasks.iter().map(|task| task.id.clone()).collect();
reasons.push("cross-group-handoff");
} else {
if contains_any(
&lower,
&[
"title",
"design",
"核心循环",
"玩法",
"胜负",
"目标",
"任务",
],
) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Design);
reasons.push("design-spec");
}
if contains_any(&lower, &["balance", "数值", "难度", "得分", "速度", "生命"]) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Balance);
reasons.push("balance");
}
if contains_any(
&lower,
&[
"art",
"美术",
"视觉",
"资产",
"manifest.art",
"角色",
"场景",
],
) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Art);
reasons.push("art-assets");
}
if contains_any(&lower, &["audio", "音乐", "音效", "bgm", "manifest.audio"]) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Audio);
reasons.push("audio-assets");
}
if contains_any(
&lower,
&[
"gamehtml",
"html",
"canvas",
"requestanimationframe",
"输入",
"键盘",
"鼠标",
"触摸",
"click",
"keydown",
"keyup",
"失败",
"胜利",
"重开",
"eval",
"fetch",
"websocket",
"serviceworker",
"remote",
"远程",
"自包含",
],
) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Code);
reasons.push("code-runtime");
}
if contains_any(
&lower,
&[
"publish",
"publishing",
"运营",
"发布",
"readme",
"标签",
"简介",
],
) {
push_group_task_ids(graph, &mut task_ids, GameCreationAgentGroup::Publishing);
reasons.push("publishing");
}
}
if task_ids.is_empty() {
task_ids = graph.tasks.iter().map(|task| task.id.clone()).collect();
reasons.push("unclassified-full-pass");
}
GameCreationAgentRepairRoute {
issue: issue.to_string(),
task_ids,
reason: reasons.join("+"),
}
}
fn push_group_task_ids(
graph: &GameCreationTaskGraph,
task_ids: &mut Vec<String>,
group: GameCreationAgentGroup,
) {
for task in graph.tasks.iter().filter(|task| task.group == group) {
push_unique(task_ids, &task.id);
}
}
fn push_unique(values: &mut Vec<String>, value: &str) {
if !values.iter().any(|item| item == value) {
values.push(value.to_string());
}
}
fn contains_any(value: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| value.contains(needle))
}
fn task<const D: usize, const A: usize, const C: usize>(
id: &str,
title: &str,
group: GameCreationAgentGroup,
role: &str,
dependencies: [&str; D],
artifacts: [&str; A],
acceptance_criteria: [&str; C],
) -> GameCreationTask {
GameCreationTask {
id: id.to_string(),
title: title.to_string(),
group,
role: role.to_string(),
status: GameCreationTaskStatus::Pending,
dependencies: dependencies.map(str::to_string).to_vec(),
artifacts: artifacts.map(str::to_string).to_vec(),
acceptance_criteria: acceptance_criteria.map(str::to_string).to_vec(),
}
}
fn invalid_orchestration(error: OrchestrationError) -> PlatformAgentError {
PlatformAgentError::InvalidInput(format!("多 Agent 编排任务图无效:{error}"))
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
fn isolated_child(
template_agent_id: &str,
task: &str,
write_scope: &str,
) -> GameCreationIsolatedAgentChildSpec {
GameCreationIsolatedAgentChildSpec {
template_agent_id: template_agent_id.to_string(),
task: task.to_string(),
acceptance_criteria: vec!["定向测试通过".to_string()],
expected_artifacts: vec![write_scope.to_string()],
write_scopes: vec![write_scope.to_string()],
}
}
fn isolated_request(
children: Vec<GameCreationIsolatedAgentChildSpec>,
) -> GameCreationIsolatedAgentSpawnRequest {
GameCreationIsolatedAgentSpawnRequest {
children,
join_mode: GameCreationIsolatedAgentJoinMode::All,
}
}
fn completed_isolated_result(
identity: &GameCreationIsolatedAgentDerivedIdentity,
artifact_path: &str,
) -> GameCreationIsolatedAgentChildResult {
GameCreationIsolatedAgentChildResult {
delegation_id: identity.delegation_id.clone(),
instance_id: identity.instance_id.clone(),
template_agent_id: identity.template_agent_id.clone(),
run_id: format!("isolated-run-{}", identity.child_index),
status: GameCreationIsolatedAgentResultStatus::Completed,
summary: "子任务已完成并通过验证".to_string(),
artifacts: vec![GameCreationIsolatedAgentArtifact {
path: artifact_path.to_string(),
sha256: "a".repeat(64),
}],
evidence: vec![GameCreationIsolatedAgentEvidence {
kind: "project.verify".to_string(),
summary: "定向验证通过".to_string(),
path: None,
sha256: None,
}],
verified_revision: Some(7),
error: None,
}
}
#[test]
fn isolated_spawn_request_serde_uses_tool_contract_shape_and_only_all_join() {
let request = isolated_request(vec![isolated_child(
"code-prototype",
"实现边界清晰的功能",
"game/feature-a/**",
)]);
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["joinMode"], "all");
assert_eq!(value["children"][0]["templateAgentId"], "code-prototype");
assert_eq!(
value["children"][0]["acceptanceCriteria"][0],
"定向测试通过"
);
assert_eq!(
value["children"][0]["expectedArtifacts"][0],
"game/feature-a/**"
);
assert_eq!(
serde_json::from_value::<GameCreationIsolatedAgentSpawnRequest>(value).unwrap(),
request
);
assert!(
serde_json::from_value::<GameCreationIsolatedAgentSpawnRequest>(serde_json::json!({
"children": [],
"joinMode": "any"
}))
.is_err()
);
}
#[test]
fn isolated_spawn_validation_enforces_children_depth_and_field_budgets() {
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![])).is_err()
);
let child = isolated_child("code-prototype", "实现边界清晰的功能", "game/feature-a/**");
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![
child.clone(),
isolated_child("code-review", "审查功能", "review/feature-a/**"),
isolated_child("code-test", "验证功能", "tests/feature-a/**"),
isolated_child("code-doc", "整理结果", "docs/feature-a/**"),
]))
.is_err()
);
assert!(
validate_game_creation_isolated_agent_spawn_request_at_depth(
&isolated_request(vec![child.clone()]),
1,
)
.is_err()
);
let mut unsafe_id = child.clone();
unsafe_id.template_agent_id = "../code-prototype".to_string();
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![unsafe_id]))
.is_err()
);
let mut oversized_task = child.clone();
oversized_task.task = "x".repeat(GAME_CREATION_ISOLATED_AGENT_TASK_MAX_CHARS + 1);
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![
oversized_task
]))
.is_err()
);
let mut empty_criteria = child.clone();
empty_criteria.acceptance_criteria.clear();
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![
empty_criteria
]))
.is_err()
);
let mut too_many_artifacts = child;
too_many_artifacts.expected_artifacts = (0
..=GAME_CREATION_ISOLATED_AGENT_MAX_EXPECTED_ARTIFACTS)
.map(|index| format!("game/artifact-{index}.txt"))
.collect();
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![
too_many_artifacts
]))
.is_err()
);
}
#[test]
fn isolated_spawn_validation_requires_disjoint_relative_glob_prefixes() {
let disjoint = isolated_request(vec![
isolated_child("code-a", "实现 A", "game/a/**"),
isolated_child("code-ab", "实现 AB", "game/ab/**"),
]);
assert!(validate_game_creation_isolated_agent_spawn_request(&disjoint).is_ok());
let mut same_child_overlap = isolated_child("code-a", "实现 A", "game/a/**");
same_child_overlap
.write_scopes
.push("game/a/generated/**".to_string());
assert!(
validate_game_creation_isolated_agent_spawn_request(&isolated_request(vec![
same_child_overlap,
]))
.is_ok()
);
let overlapping = isolated_request(vec![
isolated_child("code-a", "实现 A", "game/feature/**"),
isolated_child("code-b", "实现 B", "game/feature/ui/**"),
]);
assert!(validate_game_creation_isolated_agent_spawn_request(&overlapping).is_err());
for scope in [
"/game/feature/**",
"../game/feature/**",
"game/*/feature/**",
"game/feature",
"game\\feature/**",
] {
let request =
isolated_request(vec![isolated_child("code-prototype", "实现功能", scope)]);
assert!(
validate_game_creation_isolated_agent_spawn_request(&request).is_err(),
"scope should be rejected: {scope}"
);
}
}
#[test]
fn isolated_agent_sha256_matches_standard_vectors() {
assert_eq!(
isolated_agent_sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
isolated_agent_sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn isolated_agent_identity_derivation_is_stable_and_index_specific() {
let first =
derive_game_creation_isolated_agent_identity("parent-action-42", 0, "code-prototype")
.unwrap();
let repeated =
derive_game_creation_isolated_agent_identity("parent-action-42", 0, "code-prototype")
.unwrap();
let second =
derive_game_creation_isolated_agent_identity("parent-action-42", 1, "code-prototype")
.unwrap();
assert_eq!(first, repeated);
assert_eq!(
first.delegation_group_id,
"1a1d77b83f9249d522831a8fc0f4255d3ed9e756b14ed581421142ac5ef87852"
);
assert_eq!(
first.delegation_id,
"999a7f4fa2fd0011835df832baae7bb677842e01e216a71232696454db9020e1"
);
assert_eq!(first.instance_id, "child-999a7f4fa2fd0011835df832");
assert_eq!(
second.delegation_id,
"402be063af793d6a1869fb052b285d8c1400e7c36465ae9b8ea665ea7b40b573"
);
assert_ne!(first.instance_id, second.instance_id);
}
#[test]
fn isolated_child_result_requires_structured_terminal_evidence() {
let identity =
derive_game_creation_isolated_agent_identity("parent-action-42", 0, "code-prototype")
.unwrap();
let completed = completed_isolated_result(&identity, "game/feature-a/main.js");
assert!(validate_game_creation_isolated_agent_child_result(&completed).is_ok());
let value = serde_json::to_value(&completed).unwrap();
assert_eq!(value["templateAgentId"], "code-prototype");
assert_eq!(value["verifiedRevision"], 7);
assert_eq!(value["artifacts"][0]["sha256"], "a".repeat(64));
assert_eq!(value["evidence"][0]["kind"], "project.verify");
let mut invalid_sha = completed.clone();
invalid_sha.artifacts[0].sha256 = "ABC".to_string();
assert!(validate_game_creation_isolated_agent_child_result(&invalid_sha).is_err());
let mut completed_with_error = completed.clone();
completed_with_error.error = Some("不应存在".to_string());
assert!(validate_game_creation_isolated_agent_child_result(&completed_with_error).is_err());
let mut failed_without_error = completed.clone();
failed_without_error.status = GameCreationIsolatedAgentResultStatus::Failed;
assert!(validate_game_creation_isolated_agent_child_result(&failed_without_error).is_err());
let mut invalid_revision = completed;
invalid_revision.verified_revision = Some(0);
assert!(validate_game_creation_isolated_agent_child_result(&invalid_revision).is_err());
}
#[test]
fn isolated_join_waits_for_all_and_canonicalizes_child_order() {
let request = isolated_request(vec![
isolated_child("code-a", "实现 A", "game/a/**"),
isolated_child("code-b", "实现 B", "game/b/**"),
]);
let group =
derive_game_creation_isolated_agent_group("parent-action-42", &request).unwrap();
let first = completed_isolated_result(&group.children[0], "game/a/main.js");
let second = completed_isolated_result(&group.children[1], "game/b/main.js");
assert!(join_game_creation_isolated_agent_results(&group, vec![first.clone()]).is_err());
let joined =
join_game_creation_isolated_agent_results(&group, vec![second.clone(), first.clone()])
.unwrap();
assert_eq!(joined.delegation_group_id, group.delegation_group_id);
assert_eq!(joined.run_id, group.join_run_id);
assert_eq!(joined.join_mode, GameCreationIsolatedAgentJoinMode::All);
assert_eq!(joined.results, vec![first.clone(), second]);
let mut wrong_instance = first;
wrong_instance.instance_id = "child-wrong".to_string();
assert!(
join_game_creation_isolated_agent_results(
&group,
vec![
wrong_instance,
completed_isolated_result(&group.children[1], "game/b/main.js"),
],
)
.is_err()
);
}
#[test]
fn seed_task_graph_requires_goal() {
assert!(build_game_creation_seed_task_graph(" ").is_err());
}
#[test]
fn seed_task_graph_covers_all_professional_groups() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let groups = graph
.tasks
.iter()
.map(|task| task.group)
.collect::<HashSet<_>>();
for group in GAME_CREATION_AGENT_GROUPS {
assert!(groups.contains(&group), "missing group: {group:?}");
}
let design = graph
.tasks
.iter()
.find(|task| task.id == "design-foundation")
.expect("design foundation task");
assert_eq!(design.title, "确定玩法规格与界面原型");
assert_eq!(design.group, GameCreationAgentGroup::Design);
assert_eq!(design.role, "Gameplay");
assert_eq!(design.status, GameCreationTaskStatus::Pending);
assert_eq!(design.dependencies, ["design-director"]);
assert_eq!(
design.artifacts,
[
"memory/project.md",
"game/game_design.md",
"assets/ui-prototype.png"
]
);
assert_eq!(
design.acceptance_criteria,
["核心循环、胜负条件和第一版关卡目标明确,且已生成可读的 16:9 横屏界面原型图"]
);
let art = graph
.tasks
.iter()
.find(|task| task.id == "art-asset-plan")
.expect("art asset task");
assert_eq!(art.title, "生成首版美术素材");
assert_eq!(art.group, GameCreationAgentGroup::Art);
assert_eq!(art.role, "Asset");
assert_eq!(art.status, GameCreationTaskStatus::Pending);
assert_eq!(art.dependencies, ["art-director"]);
assert_eq!(
art.artifacts,
["assets/manifest.art.json", "assets/art-spritesheet.png"]
);
assert_eq!(
art.acceptance_criteria,
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"
]
);
}
#[test]
fn seed_task_graph_validates_against_an_injected_agent_catalog() {
use agent_runtime_core::AgentDescriptor;
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let catalog = AgentCatalog::try_new(graph.tasks.iter().map(|task| {
AgentDescriptor::try_new(&task.id, &task.role, std::iter::empty::<&str>())
.expect("agent descriptor")
}))
.expect("agent catalog");
validate_game_creation_task_agents(&graph, &catalog).expect("known task agents");
let incomplete = AgentCatalog::try_new(graph.tasks.iter().skip(1).map(|task| {
AgentDescriptor::try_new(&task.id, &task.role, std::iter::empty::<&str>())
.expect("agent descriptor")
}))
.expect("incomplete catalog");
let error = validate_game_creation_task_agents(&graph, &incomplete)
.expect_err("missing task agent must fail closed");
assert!(error.to_string().contains("未注册 Agent"));
}
#[test]
fn code_director_waits_for_design_assets_audio_and_balance() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let code_director = graph
.tasks
.iter()
.find(|task| task.id == "code-director")
.unwrap();
assert_eq!(
code_director.dependencies,
vec![
"design-foundation",
"balance-seed",
"art-polish",
"audio-asset-plan"
]
);
}
#[test]
fn ready_tasks_follow_completed_dependencies() {
let mut graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
assert_eq!(
select_ready_game_creation_tasks(&graph)
.expect("ready tasks")
.iter()
.map(|task| task.id.as_str())
.collect::<Vec<_>>(),
vec!["design-director"]
);
graph
.tasks
.iter_mut()
.find(|task| task.id == "design-director")
.unwrap()
.status = GameCreationTaskStatus::Completed;
assert_eq!(
select_ready_game_creation_tasks(&graph)
.expect("ready tasks")
.iter()
.map(|task| task.id.as_str())
.collect::<Vec<_>>(),
vec!["design-foundation"]
);
graph
.tasks
.iter_mut()
.find(|task| task.id == "design-foundation")
.unwrap()
.status = GameCreationTaskStatus::Completed;
assert_eq!(
select_ready_game_creation_tasks(&graph)
.expect("ready tasks")
.iter()
.map(|task| task.id.as_str())
.collect::<Vec<_>>(),
vec!["balance-director", "art-director", "audio-director"]
);
}
#[test]
fn publish_package_waits_for_preview() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let publish = graph
.tasks
.iter()
.find(|task| task.id == "publish-package")
.unwrap();
assert_eq!(publish.dependencies, vec!["publish-strategy"]);
}
#[test]
fn pass_plan_dispatches_all_role_tasks_on_first_pass() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let plan = plan_game_creation_agent_pass(
&graph,
1,
"# Evaluator Findings\n\n- pass: 0\n- status: needs-revision\n\n- 暂无上一轮问题,Generator 可开始首轮实现。\n",
)
.expect("initial pass plan");
assert_eq!(plan.mode, "initial");
assert_eq!(plan.active_task_ids.len(), 16);
assert!(plan.carried_task_ids.is_empty());
assert_eq!(plan.repair_routes, Vec::new());
assert_eq!(plan.dependency_waves[0], vec!["design-director"]);
}
#[test]
fn pass_plan_routes_canvas_and_input_issues_to_code_group() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let plan = plan_game_creation_agent_pass(
&graph,
2,
"# Evaluator Findings\n\n- pass: 1\n- status: needs-revision\n\n- gameHtml 缺少 canvas、requestAnimationFrame 和输入监听。\n",
)
.expect("repair pass plan");
assert_eq!(plan.mode, "repair");
assert_eq!(
plan.active_task_ids,
vec![
"code-director",
"code-prototype",
"quality-review",
"preview-readiness",
"preview-playtest",
"publish-strategy",
"publish-package"
]
);
assert!(
plan.carried_task_ids
.contains(&"design-director".to_string())
);
assert_eq!(
plan.repair_routes[0].reason,
"code-runtime+dependency-impact"
);
assert_eq!(plan.dependency_waves[0], vec!["code-director"]);
}
#[test]
fn pass_plan_prefers_structured_repair_routes_from_findings() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let plan = plan_game_creation_agent_pass(
&graph,
2,
r#"# Evaluator Findings
- pass: 1
- status: needs-revision
## Repair Routes
```json
[
{
"issue": "gameHtml 缺少输入监听,但 Evaluator 已定位只需要重做 code-prototype。",
"taskIds": ["code-prototype", "unknown-task", "code-prototype"],
"reason": "structured-code-prototype"
}
]
```
"#,
)
.expect("structured repair pass plan");
assert_eq!(plan.mode, "repair");
assert_eq!(
plan.active_task_ids,
vec![
"code-prototype",
"quality-review",
"preview-readiness",
"preview-playtest",
"publish-strategy",
"publish-package"
]
);
assert_eq!(
plan.repair_routes[0].task_ids,
vec![
"code-prototype",
"quality-review",
"preview-readiness",
"preview-playtest",
"publish-strategy",
"publish-package"
]
);
assert_eq!(
plan.repair_routes[0].reason,
"structured-code-prototype+dependency-impact"
);
assert!(plan.carried_task_ids.contains(&"code-director".to_string()));
}
#[test]
fn pass_plan_reruns_downstream_groups_when_asset_plan_changes() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let plan = plan_game_creation_agent_pass(
&graph,
2,
r#"# Evaluator Findings
- pass: 1
- status: needs-revision
## Repair Routes
```json
[
{
"issue": "美术资产清单缺少主角动作,需要重做 art-asset-plan。",
"taskIds": ["art-asset-plan"],
"reason": "structured-art-asset"
}
]
```
"#,
)
.expect("asset repair pass plan");
assert_eq!(
plan.active_task_ids,
vec![
"art-asset-plan",
"art-polish",
"code-director",
"code-prototype",
"quality-review",
"preview-readiness",
"preview-playtest",
"publish-strategy",
"publish-package",
]
);
assert!(plan.carried_task_ids.contains(&"art-director".to_string()));
assert!(
plan.dependency_waves
.iter()
.any(|wave| wave == &vec!["art-asset-plan".to_string()])
);
assert!(
plan.dependency_waves
.iter()
.any(|wave| wave == &vec!["publish-package".to_string()])
);
assert_eq!(
plan.repair_routes[0].reason,
"structured-art-asset+dependency-impact"
);
}
#[test]
fn pass_plan_routes_cross_group_handoff_issue_to_full_repair() {
let graph = build_game_creation_seed_task_graph("做一个像素风横版动作原型").unwrap();
let plan = plan_game_creation_agent_pass(
&graph,
2,
"# Evaluator Findings\n\n- pass: 1\n- status: needs-revision\n\n- handoffs 缺少 publishing 专业组交接。\n",
)
.expect("cross-group pass plan");
assert_eq!(plan.mode, "repair");
assert_eq!(plan.active_task_ids.len(), 16);
assert!(plan.carried_task_ids.is_empty());
assert_eq!(plan.repair_routes[0].reason, "cross-group-handoff");
}
#[test]
fn pass_plan_rejects_a_cyclic_game_task_graph() {
let graph = GameCreationTaskGraph {
goal: "验证非法环".to_string(),
tasks: vec![
task(
"left",
"左节点",
GameCreationAgentGroup::Design,
"Left",
["right"],
[],
["左节点完成"],
),
task(
"right",
"右节点",
GameCreationAgentGroup::Code,
"Right",
["left"],
[],
["右节点完成"],
),
],
};
let error = plan_game_creation_agent_pass(&graph, 1, "")
.expect_err("cyclic graph must fail closed");
assert!(error.to_string().contains("依赖环"));
}
}