d2254d1e8c
## 概要 - 抽取通用 `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
2136 lines
74 KiB
Rust
2136 lines
74 KiB
Rust
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 = §ion[..section_end];
|
||
if let Some(fence_start) = section.find("```json") {
|
||
let payload_start = fence_start + "```json".len();
|
||
let after_start = §ion[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("依赖环"));
|
||
}
|
||
}
|