3237ad140a
为 Supervisor 与部门 Director 接入统一 Interaction Loop 和持久 Runtime 调度 在配置画布 API Key 时强制委派美术 Agent 并复用规范素材 收紧画布生成角色权限、图片内容校验和项目锁并发边界 完善专业 Agent revision 验证、任务恢复与 Provider 兼容处理 补充自主协作、真实画布生成、运行时收束测试及技术文档
1806 lines
71 KiB
Rust
1806 lines
71 KiB
Rust
use super::*;
|
|
use serde_json::Value;
|
|
use sha2::{Digest, Sha256};
|
|
|
|
const SUPERVISOR_COLLABORATION_POLICY_SCHEMA_VERSION: &str =
|
|
"game-creator-supervisor-collaboration-policy.v1";
|
|
const SUPERVISOR_COLLABORATION_CONTRACT_SCHEMA_VERSION: &str =
|
|
"game-creator-supervisor-collaboration-contract.v1";
|
|
const SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_SCHEMA_VERSION: &str =
|
|
"game-creator-supervisor-collaboration-policy-snapshot.v1";
|
|
const SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_SCHEMA_VERSION: &str =
|
|
"game-creator-supervisor-collaboration-policy-snapshot-binding.v1";
|
|
pub(crate) const SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH: &str =
|
|
".agent/collaboration-policy.json";
|
|
const SUPERVISOR_COLLABORATION_POLICY_MAX_BYTES: usize = 16 * 1024;
|
|
const SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_MAX_BYTES: usize = 24 * 1024;
|
|
const SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_MAX_BYTES: usize = 8 * 1024;
|
|
const SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH: &str = "initial-collaboration-batch";
|
|
const SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH: &str = "legacy-provider-batch-contract";
|
|
const SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT: &str =
|
|
"legacy-current-project-policy";
|
|
const SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES: usize = 3;
|
|
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN: usize = 3;
|
|
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM: usize = 16;
|
|
const AUTONOMOUS_GAME_BUILD_BASE_REQUIRED_STATIC_AGENT_IDS: [&str; 2] =
|
|
["code-prototype", "quality-review"];
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub(crate) enum SupervisorInitialCollaborationWave {
|
|
#[default]
|
|
Auto,
|
|
Static,
|
|
Isolated,
|
|
Mixed,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub(crate) struct SupervisorCollaborationPolicy {
|
|
pub(crate) schema_version: String,
|
|
#[serde(default)]
|
|
pub(crate) required_initial_wave: SupervisorInitialCollaborationWave,
|
|
#[serde(default)]
|
|
pub(crate) min_static_delegates: usize,
|
|
#[serde(default)]
|
|
pub(crate) required_static_agent_ids: Vec<String>,
|
|
#[serde(default)]
|
|
pub(crate) min_isolated_children: usize,
|
|
#[serde(default, skip_serializing_if = "is_zero")]
|
|
pub(crate) min_isolated_groups_before_claim: usize,
|
|
#[serde(default = "default_orchestrator_only_after_delegation")]
|
|
pub(crate) orchestrator_only_after_delegation: bool,
|
|
}
|
|
|
|
impl Default for SupervisorCollaborationPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
schema_version: SUPERVISOR_COLLABORATION_POLICY_SCHEMA_VERSION.to_string(),
|
|
required_initial_wave: SupervisorInitialCollaborationWave::Auto,
|
|
min_static_delegates: 0,
|
|
required_static_agent_ids: Vec::new(),
|
|
min_isolated_children: 0,
|
|
min_isolated_groups_before_claim: 0,
|
|
orchestrator_only_after_delegation: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn autonomous_game_build_has_canonical_art_asset(root: &Path) -> bool {
|
|
const ART_PATH: &str = "assets/art-spritesheet.png";
|
|
read_existing_manifest_for_project(root)
|
|
.ok()
|
|
.is_some_and(|manifest| {
|
|
manifest.assets.iter().any(|asset| {
|
|
asset.local_path == ART_PATH
|
|
&& asset.kind == "art-spritesheet"
|
|
&& asset.media_type.starts_with("image/")
|
|
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
|
|
&& resolve_local_project_path(root, ART_PATH)
|
|
.ok()
|
|
.and_then(|path| fs::read(path).ok())
|
|
.is_some_and(|bytes| {
|
|
bytes.len() > 8 && bytes.starts_with(b"\x89PNG\r\n\x1a\n")
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
fn autonomous_game_build_supervisor_collaboration_policy(
|
|
root: &Path,
|
|
) -> SupervisorCollaborationPolicy {
|
|
let mut required_static_agent_ids = AUTONOMOUS_GAME_BUILD_BASE_REQUIRED_STATIC_AGENT_IDS
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>();
|
|
if editor_api_key_is_configured() && !autonomous_game_build_has_canonical_art_asset(root) {
|
|
required_static_agent_ids.push("art-asset-plan".to_string());
|
|
}
|
|
SupervisorCollaborationPolicy {
|
|
required_initial_wave: SupervisorInitialCollaborationWave::Static,
|
|
min_static_delegates: required_static_agent_ids.len(),
|
|
required_static_agent_ids,
|
|
..SupervisorCollaborationPolicy::default()
|
|
}
|
|
}
|
|
|
|
fn apply_autonomous_game_build_required_static_agents(
|
|
root: &Path,
|
|
mut policy: SupervisorCollaborationPolicy,
|
|
) -> Result<SupervisorCollaborationPolicy, String> {
|
|
if editor_api_key_is_configured()
|
|
&& !autonomous_game_build_has_canonical_art_asset(root)
|
|
&& !policy
|
|
.required_static_agent_ids
|
|
.iter()
|
|
.any(|existing| existing == "art-asset-plan")
|
|
{
|
|
policy
|
|
.required_static_agent_ids
|
|
.push("art-asset-plan".to_string());
|
|
}
|
|
policy.min_static_delegates = policy
|
|
.min_static_delegates
|
|
.max(policy.required_static_agent_ids.len());
|
|
normalize_supervisor_collaboration_policy(policy)
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct SupervisorCollaborationUnboundPolicy {
|
|
policy: SupervisorCollaborationPolicy,
|
|
source: &'static str,
|
|
project_policy_status: &'static str,
|
|
project_policy_present: bool,
|
|
}
|
|
|
|
fn read_supervisor_collaboration_unbound_policy_for_run_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<SupervisorCollaborationUnboundPolicy, String> {
|
|
let (run_profile, _) =
|
|
agent_runtime_run_profile_identity_at(root, parent_agent_id, parent_run_id, None, None)?;
|
|
let autonomous_supervisor = parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD;
|
|
let policy_path = root.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH);
|
|
match fs::symlink_metadata(&policy_path) {
|
|
Ok(_) => {
|
|
let mut policy = read_supervisor_collaboration_policy_at(root)?;
|
|
if autonomous_supervisor {
|
|
policy = apply_autonomous_game_build_required_static_agents(root, policy)?;
|
|
}
|
|
return Ok(SupervisorCollaborationUnboundPolicy {
|
|
policy,
|
|
source: "project-policy-unbound",
|
|
project_policy_status: "current",
|
|
project_policy_present: true,
|
|
});
|
|
}
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
Err(error) => {
|
|
return Err(format!(
|
|
"读取 Project Supervisor 协作策略文件状态失败:{error}"
|
|
));
|
|
}
|
|
}
|
|
if autonomous_supervisor {
|
|
return Ok(SupervisorCollaborationUnboundPolicy {
|
|
policy: normalize_supervisor_collaboration_policy(
|
|
autonomous_game_build_supervisor_collaboration_policy(root),
|
|
)?,
|
|
source: "autonomous-run-default",
|
|
project_policy_status: "absent",
|
|
project_policy_present: false,
|
|
});
|
|
}
|
|
Ok(SupervisorCollaborationUnboundPolicy {
|
|
policy: SupervisorCollaborationPolicy::default(),
|
|
source: "project-policy-unbound",
|
|
project_policy_status: "current",
|
|
project_policy_present: false,
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub(crate) struct SupervisorCollaborationState {
|
|
pub(crate) initial_static_agent_ids: Vec<String>,
|
|
pub(crate) isolated_group_count: usize,
|
|
pub(crate) isolated_child_count: usize,
|
|
pub(crate) max_isolated_children_per_group: usize,
|
|
}
|
|
|
|
impl SupervisorCollaborationState {
|
|
pub(crate) fn has_collaboration(&self) -> bool {
|
|
!self.initial_static_agent_ids.is_empty() || self.isolated_group_count > 0
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub(crate) struct SupervisorCollaborationContract {
|
|
pub(crate) schema_version: String,
|
|
pub(crate) policy: SupervisorCollaborationPolicy,
|
|
pub(crate) policy_fingerprint: String,
|
|
pub(crate) initial_wave: bool,
|
|
pub(crate) initial_static_agent_ids: Vec<String>,
|
|
pub(crate) repair_delegate_count: usize,
|
|
pub(crate) isolated_spawn_count: usize,
|
|
pub(crate) isolated_child_count: usize,
|
|
pub(crate) contract_fingerprint: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub(crate) struct SupervisorCollaborationPolicySnapshot {
|
|
pub(crate) schema_version: String,
|
|
pub(crate) project_id: String,
|
|
pub(crate) parent_agent_id: String,
|
|
pub(crate) parent_run_id: String,
|
|
pub(crate) bound_from: String,
|
|
pub(crate) policy: SupervisorCollaborationPolicy,
|
|
pub(crate) policy_fingerprint: String,
|
|
pub(crate) snapshot_fingerprint: String,
|
|
pub(crate) bound_at: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub(crate) struct SupervisorCollaborationPolicySnapshotBinding {
|
|
pub(crate) schema_version: String,
|
|
pub(crate) project_id: String,
|
|
pub(crate) parent_agent_id: String,
|
|
pub(crate) parent_run_id: String,
|
|
pub(crate) bound_from: String,
|
|
pub(crate) policy_fingerprint: String,
|
|
pub(crate) snapshot_fingerprint: String,
|
|
pub(crate) bound_at: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub(crate) struct SupervisorCollaborationPolicyResolution {
|
|
pub(crate) policy: SupervisorCollaborationPolicy,
|
|
pub(crate) policy_fingerprint: String,
|
|
pub(crate) snapshot_fingerprint: Option<String>,
|
|
pub(crate) binding_source: Option<String>,
|
|
pub(crate) source: &'static str,
|
|
pub(crate) project_policy_status: &'static str,
|
|
pub(crate) current_project_policy_fingerprint: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub(crate) struct SupervisorCollaborationViolation {
|
|
pub(crate) summary: String,
|
|
pub(crate) detail: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub(crate) struct SupervisorCollaborationPreflight {
|
|
pub(crate) contract: Option<SupervisorCollaborationContract>,
|
|
pub(crate) force_durable_batch: bool,
|
|
pub(crate) violation: Option<SupervisorCollaborationViolation>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
struct SupervisorCollaborationActionSummary {
|
|
initial_static_agent_ids: Vec<String>,
|
|
repair_delegate_count: usize,
|
|
isolated_spawn_count: usize,
|
|
isolated_child_count: usize,
|
|
has_project_mutation: bool,
|
|
}
|
|
|
|
impl SupervisorCollaborationActionSummary {
|
|
fn has_collaboration_action(&self) -> bool {
|
|
!self.initial_static_agent_ids.is_empty()
|
|
|| self.repair_delegate_count > 0
|
|
|| self.isolated_spawn_count > 0
|
|
}
|
|
}
|
|
|
|
fn default_orchestrator_only_after_delegation() -> bool {
|
|
true
|
|
}
|
|
|
|
fn is_zero(value: &usize) -> bool {
|
|
*value == 0
|
|
}
|
|
|
|
pub(crate) fn read_supervisor_collaboration_policy_at(
|
|
root: &Path,
|
|
) -> Result<SupervisorCollaborationPolicy, String> {
|
|
let policy = read_agent_runtime_json_sidecar_with_max_bytes::<SupervisorCollaborationPolicy>(
|
|
root,
|
|
SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH,
|
|
"Project Supervisor 协作策略",
|
|
SUPERVISOR_COLLABORATION_POLICY_MAX_BYTES,
|
|
)?
|
|
.unwrap_or_default();
|
|
normalize_supervisor_collaboration_policy(policy)
|
|
}
|
|
|
|
pub(crate) fn write_supervisor_collaboration_policy_at(
|
|
root: &Path,
|
|
policy: SupervisorCollaborationPolicy,
|
|
) -> Result<SupervisorCollaborationPolicy, String> {
|
|
let policy = normalize_supervisor_collaboration_policy(policy)?;
|
|
write_agent_runtime_json_sidecar_with_max_bytes(
|
|
root,
|
|
SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH,
|
|
"Project Supervisor 协作策略",
|
|
&policy,
|
|
SUPERVISOR_COLLABORATION_POLICY_MAX_BYTES,
|
|
)?;
|
|
Ok(policy)
|
|
}
|
|
|
|
pub(crate) fn render_supervisor_collaboration_policy_for_prompt_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<String, String> {
|
|
let resolution =
|
|
resolve_supervisor_collaboration_policy_for_run_at(root, parent_agent_id, parent_run_id)?;
|
|
serde_json::to_string_pretty(&resolution.policy)
|
|
.map_err(|error| format!("序列化 Project Supervisor 协作策略失败:{error}"))
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_path_component(value: &str, fallback: &str) -> String {
|
|
let normalized = agent_runtime_confirmation_path_component(value, fallback);
|
|
if normalized == value {
|
|
return normalized;
|
|
}
|
|
let readable = normalized.chars().take(80).collect::<String>();
|
|
format!("{readable}--{:x}", Sha256::digest(value.as_bytes()))
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_relative_path(
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> String {
|
|
format!(
|
|
".agent/runtime/collaboration-policy-snapshots/{}/{}.json",
|
|
supervisor_collaboration_policy_snapshot_path_component(parent_agent_id, "agent"),
|
|
supervisor_collaboration_policy_snapshot_path_component(parent_run_id, "run")
|
|
)
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_binding_relative_path(
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> String {
|
|
format!(
|
|
".agent/runtime/collaboration-policy-snapshot-bindings/{}/{}.json",
|
|
supervisor_collaboration_policy_snapshot_path_component(parent_agent_id, "agent"),
|
|
supervisor_collaboration_policy_snapshot_path_component(parent_run_id, "run")
|
|
)
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_lock_id(
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> String {
|
|
format!(
|
|
"snapshot-{:x}",
|
|
Sha256::digest(format!("{parent_agent_id}\0{parent_run_id}").as_bytes())
|
|
)
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_policy_snapshot_path(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> PathBuf {
|
|
root.join(supervisor_collaboration_policy_snapshot_relative_path(
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
))
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_policy_snapshot_binding_path(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> PathBuf {
|
|
root.join(
|
|
supervisor_collaboration_policy_snapshot_binding_relative_path(
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
),
|
|
)
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_fingerprint(
|
|
snapshot: &SupervisorCollaborationPolicySnapshot,
|
|
) -> Result<String, String> {
|
|
let identity = serde_json::to_vec(&serde_json::json!({
|
|
"schemaVersion": snapshot.schema_version,
|
|
"projectId": snapshot.project_id,
|
|
"parentAgentId": snapshot.parent_agent_id,
|
|
"parentRunId": snapshot.parent_run_id,
|
|
"boundFrom": snapshot.bound_from,
|
|
"policy": snapshot.policy,
|
|
"policyFingerprint": snapshot.policy_fingerprint,
|
|
}))
|
|
.map_err(|error| format!("序列化 Project Supervisor 协作策略快照指纹失败:{error}"))?;
|
|
Ok(format!("{:x}", Sha256::digest(identity)))
|
|
}
|
|
|
|
fn validate_supervisor_collaboration_policy_snapshot(
|
|
root: &Path,
|
|
snapshot: &SupervisorCollaborationPolicySnapshot,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<(), String> {
|
|
if snapshot.schema_version != SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_SCHEMA_VERSION {
|
|
return Err(format!(
|
|
"不支持的 Project Supervisor 协作策略快照版本:{}",
|
|
snapshot.schema_version
|
|
));
|
|
}
|
|
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
|| snapshot.parent_agent_id != parent_agent_id
|
|
|| snapshot.parent_run_id != parent_run_id
|
|
|| parent_run_id.trim().is_empty()
|
|
{
|
|
return Err("Project Supervisor 协作策略快照父 run 身份不匹配".to_string());
|
|
}
|
|
if snapshot.project_id != game_creator_agent_runtime_context_project_id(root)? {
|
|
return Err("Project Supervisor 协作策略快照项目身份不匹配".to_string());
|
|
}
|
|
if !matches!(
|
|
snapshot.bound_from.as_str(),
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT
|
|
) {
|
|
return Err("Project Supervisor 协作策略快照绑定来源无效".to_string());
|
|
}
|
|
let normalized_policy = normalize_supervisor_collaboration_policy(snapshot.policy.clone())?;
|
|
if normalized_policy != snapshot.policy {
|
|
return Err("Project Supervisor 协作策略快照包含未规范化策略".to_string());
|
|
}
|
|
let policy_fingerprint = supervisor_collaboration_policy_fingerprint(&snapshot.policy)?;
|
|
if snapshot.policy_fingerprint != policy_fingerprint {
|
|
return Err("Project Supervisor 协作策略快照的策略指纹不匹配".to_string());
|
|
}
|
|
if snapshot.bound_at == 0
|
|
|| snapshot.snapshot_fingerprint
|
|
!= supervisor_collaboration_policy_snapshot_fingerprint(snapshot)?
|
|
{
|
|
return Err("Project Supervisor 协作策略快照身份指纹已变化".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_snapshot_binding(
|
|
snapshot: &SupervisorCollaborationPolicySnapshot,
|
|
) -> SupervisorCollaborationPolicySnapshotBinding {
|
|
SupervisorCollaborationPolicySnapshotBinding {
|
|
schema_version: SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_SCHEMA_VERSION.to_string(),
|
|
project_id: snapshot.project_id.clone(),
|
|
parent_agent_id: snapshot.parent_agent_id.clone(),
|
|
parent_run_id: snapshot.parent_run_id.clone(),
|
|
bound_from: snapshot.bound_from.clone(),
|
|
policy_fingerprint: snapshot.policy_fingerprint.clone(),
|
|
snapshot_fingerprint: snapshot.snapshot_fingerprint.clone(),
|
|
bound_at: snapshot.bound_at,
|
|
}
|
|
}
|
|
|
|
fn validate_supervisor_collaboration_policy_snapshot_binding(
|
|
root: &Path,
|
|
binding: &SupervisorCollaborationPolicySnapshotBinding,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<(), String> {
|
|
if binding.schema_version != SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_SCHEMA_VERSION {
|
|
return Err(format!(
|
|
"不支持的 Project Supervisor 协作策略快照绑定记录版本:{}",
|
|
binding.schema_version
|
|
));
|
|
}
|
|
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
|| binding.parent_agent_id != parent_agent_id
|
|
|| binding.parent_run_id != parent_run_id
|
|
|| parent_run_id.trim().is_empty()
|
|
{
|
|
return Err("Project Supervisor 协作策略快照绑定记录父 run 身份不匹配".to_string());
|
|
}
|
|
if binding.project_id != game_creator_agent_runtime_context_project_id(root)? {
|
|
return Err("Project Supervisor 协作策略快照绑定记录项目身份不匹配".to_string());
|
|
}
|
|
if !matches!(
|
|
binding.bound_from.as_str(),
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT
|
|
) || !matches!(binding.policy_fingerprint.len(), 64)
|
|
|| !binding
|
|
.policy_fingerprint
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_hexdigit())
|
|
|| !matches!(binding.snapshot_fingerprint.len(), 64)
|
|
|| !binding
|
|
.snapshot_fingerprint
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_hexdigit())
|
|
|| binding.bound_at == 0
|
|
{
|
|
return Err("Project Supervisor 协作策略快照绑定记录内容无效".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn read_supervisor_collaboration_policy_snapshot_binding_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<Option<SupervisorCollaborationPolicySnapshotBinding>, String> {
|
|
let relative_path = supervisor_collaboration_policy_snapshot_binding_relative_path(
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
);
|
|
let Some(binding) = read_agent_runtime_json_sidecar_with_max_bytes(
|
|
root,
|
|
&relative_path,
|
|
"Project Supervisor 协作策略快照绑定记录",
|
|
SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_MAX_BYTES,
|
|
)?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
validate_supervisor_collaboration_policy_snapshot_binding(
|
|
root,
|
|
&binding,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?;
|
|
Ok(Some(binding))
|
|
}
|
|
|
|
fn write_supervisor_collaboration_policy_snapshot_binding_at(
|
|
root: &Path,
|
|
snapshot: &SupervisorCollaborationPolicySnapshot,
|
|
) -> Result<SupervisorCollaborationPolicySnapshotBinding, String> {
|
|
let binding = supervisor_collaboration_policy_snapshot_binding(snapshot);
|
|
let relative_path = supervisor_collaboration_policy_snapshot_binding_relative_path(
|
|
&snapshot.parent_agent_id,
|
|
&snapshot.parent_run_id,
|
|
);
|
|
write_agent_runtime_json_sidecar_with_max_bytes(
|
|
root,
|
|
&relative_path,
|
|
"Project Supervisor 协作策略快照绑定记录",
|
|
&binding,
|
|
SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_BINDING_MAX_BYTES,
|
|
)?;
|
|
let persisted = read_supervisor_collaboration_policy_snapshot_binding_at(
|
|
root,
|
|
&snapshot.parent_agent_id,
|
|
&snapshot.parent_run_id,
|
|
)?
|
|
.ok_or_else(|| "Project Supervisor 协作策略快照绑定记录写入后不存在".to_string())?;
|
|
if persisted != binding {
|
|
return Err("Project Supervisor 协作策略快照绑定记录并发写入后内容冲突".to_string());
|
|
}
|
|
Ok(binding)
|
|
}
|
|
|
|
pub(crate) fn read_supervisor_collaboration_policy_snapshot_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<Option<SupervisorCollaborationPolicySnapshot>, String> {
|
|
let relative_path =
|
|
supervisor_collaboration_policy_snapshot_relative_path(parent_agent_id, parent_run_id);
|
|
let Some(snapshot) = read_agent_runtime_json_sidecar_with_max_bytes(
|
|
root,
|
|
&relative_path,
|
|
"Project Supervisor 协作策略快照",
|
|
SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_MAX_BYTES,
|
|
)?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
validate_supervisor_collaboration_policy_snapshot(
|
|
root,
|
|
&snapshot,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?;
|
|
Ok(Some(snapshot))
|
|
}
|
|
|
|
pub(crate) fn bind_supervisor_collaboration_policy_snapshot_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
policy: &SupervisorCollaborationPolicy,
|
|
bound_from: &str,
|
|
) -> Result<SupervisorCollaborationPolicySnapshot, String> {
|
|
let policy = normalize_supervisor_collaboration_policy(policy.clone())?;
|
|
if !matches!(
|
|
bound_from,
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT
|
|
) {
|
|
return Err("Project Supervisor 协作策略快照绑定来源无效".to_string());
|
|
}
|
|
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
|| parent_run_id.trim().is_empty()
|
|
{
|
|
return Err("只能为有效的 Project Supervisor 父 run 绑定协作策略快照".to_string());
|
|
}
|
|
let lock_id = supervisor_collaboration_policy_snapshot_lock_id(parent_agent_id, parent_run_id);
|
|
let _binding_lock = try_acquire_game_creator_agent_delegation_lock_with_wait(
|
|
root,
|
|
&lock_id,
|
|
"collaboration-policy-snapshot",
|
|
)?
|
|
.ok_or_else(|| "Project Supervisor 协作策略快照并发绑定冲突:正被其他进程绑定".to_string())?;
|
|
let existing_binding = read_supervisor_collaboration_policy_snapshot_binding_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?;
|
|
if let Some(existing) =
|
|
read_supervisor_collaboration_policy_snapshot_at(root, parent_agent_id, parent_run_id)?
|
|
{
|
|
if existing.policy != policy {
|
|
return Err("Project Supervisor 协作策略快照已绑定且与待恢复策略冲突".to_string());
|
|
}
|
|
let expected_binding = supervisor_collaboration_policy_snapshot_binding(&existing);
|
|
match existing_binding {
|
|
Some(binding) if binding != expected_binding => {
|
|
return Err("Project Supervisor 协作策略快照与绑定记录冲突".to_string());
|
|
}
|
|
Some(_) => {}
|
|
None => {
|
|
write_supervisor_collaboration_policy_snapshot_binding_at(root, &existing)?;
|
|
}
|
|
}
|
|
return Ok(existing);
|
|
}
|
|
let policy_fingerprint = supervisor_collaboration_policy_fingerprint(&policy)?;
|
|
let (effective_bound_from, bound_at) = if let Some(binding) = existing_binding.as_ref() {
|
|
if binding.policy_fingerprint != policy_fingerprint {
|
|
return Err("Project Supervisor 协作策略快照绑定记录与待恢复策略冲突".to_string());
|
|
}
|
|
let existing_batch_derived = matches!(
|
|
binding.bound_from.as_str(),
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH
|
|
);
|
|
let requested_batch_derived = matches!(
|
|
bound_from,
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH
|
|
| SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH
|
|
);
|
|
if binding.bound_from != bound_from && !(existing_batch_derived && requested_batch_derived)
|
|
{
|
|
return Err("Project Supervisor 协作策略快照绑定来源与恢复来源冲突".to_string());
|
|
}
|
|
(binding.bound_from.clone(), binding.bound_at)
|
|
} else {
|
|
(bound_from.to_string(), unix_timestamp())
|
|
};
|
|
let mut snapshot = SupervisorCollaborationPolicySnapshot {
|
|
schema_version: SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_SCHEMA_VERSION.to_string(),
|
|
project_id: game_creator_agent_runtime_context_project_id(root)?,
|
|
parent_agent_id: parent_agent_id.to_string(),
|
|
parent_run_id: parent_run_id.to_string(),
|
|
bound_from: effective_bound_from,
|
|
policy_fingerprint,
|
|
policy,
|
|
snapshot_fingerprint: String::new(),
|
|
bound_at,
|
|
};
|
|
snapshot.snapshot_fingerprint =
|
|
supervisor_collaboration_policy_snapshot_fingerprint(&snapshot)?;
|
|
if existing_binding.as_ref().is_some_and(|binding| {
|
|
binding != &supervisor_collaboration_policy_snapshot_binding(&snapshot)
|
|
}) {
|
|
return Err("Project Supervisor 协作策略快照无法按原绑定记录恢复".to_string());
|
|
}
|
|
let relative_path =
|
|
supervisor_collaboration_policy_snapshot_relative_path(parent_agent_id, parent_run_id);
|
|
write_agent_runtime_json_sidecar_with_max_bytes(
|
|
root,
|
|
&relative_path,
|
|
"Project Supervisor 协作策略快照",
|
|
&snapshot,
|
|
SUPERVISOR_COLLABORATION_POLICY_SNAPSHOT_MAX_BYTES,
|
|
)?;
|
|
let persisted =
|
|
read_supervisor_collaboration_policy_snapshot_at(root, parent_agent_id, parent_run_id)?
|
|
.ok_or_else(|| "Project Supervisor 协作策略快照写入后不存在".to_string())?;
|
|
if persisted != snapshot {
|
|
return Err("Project Supervisor 协作策略快照并发绑定后内容冲突".to_string());
|
|
}
|
|
if existing_binding.is_none() {
|
|
write_supervisor_collaboration_policy_snapshot_binding_at(root, &snapshot)?;
|
|
}
|
|
Ok(snapshot)
|
|
}
|
|
|
|
pub(crate) fn bind_supervisor_collaboration_policy_snapshot_for_new_batch_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
policy: &SupervisorCollaborationPolicy,
|
|
) -> Result<SupervisorCollaborationPolicySnapshot, String> {
|
|
let policy = normalize_supervisor_collaboration_policy(policy.clone())?;
|
|
bind_supervisor_collaboration_policy_snapshot_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
&policy,
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_INITIAL_BATCH,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn recover_supervisor_collaboration_policy_snapshot_from_batch_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
policy: &SupervisorCollaborationPolicy,
|
|
) -> Result<SupervisorCollaborationPolicySnapshot, String> {
|
|
bind_supervisor_collaboration_policy_snapshot_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
policy,
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_BATCH,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn resolve_supervisor_collaboration_policy_for_run_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<SupervisorCollaborationPolicyResolution, String> {
|
|
if let Some(snapshot) =
|
|
read_supervisor_collaboration_policy_snapshot_at(root, parent_agent_id, parent_run_id)?
|
|
{
|
|
let snapshot = bind_supervisor_collaboration_policy_snapshot_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
&snapshot.policy,
|
|
&snapshot.bound_from,
|
|
)?;
|
|
return supervisor_collaboration_policy_resolution_from_snapshot(root, snapshot);
|
|
}
|
|
let existing_binding = read_supervisor_collaboration_policy_snapshot_binding_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?;
|
|
if let Some(snapshot) =
|
|
recover_supervisor_collaboration_policy_snapshot_from_pending_batch_if_any_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?
|
|
{
|
|
return supervisor_collaboration_policy_resolution_from_snapshot(root, snapshot);
|
|
}
|
|
if existing_binding.is_some() {
|
|
return Err(
|
|
"Project Supervisor 协作策略快照绑定记录存在,但快照与可信 v2 batch 均不可恢复"
|
|
.to_string(),
|
|
);
|
|
}
|
|
let unbound_policy = read_supervisor_collaboration_unbound_policy_for_run_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)?;
|
|
let policy = unbound_policy.policy.clone();
|
|
let state = read_supervisor_collaboration_state_at(root, parent_agent_id, parent_run_id)?;
|
|
if state.has_collaboration() {
|
|
if !game_creator_agent_runtime_run_is_non_terminal_for_collaboration_migration_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
)? {
|
|
return Err(
|
|
"只有仍在运行且没有可信 v2 contract 的旧 Project Supervisor 父 run 才能迁移当前项目策略"
|
|
.to_string(),
|
|
);
|
|
}
|
|
let snapshot = bind_supervisor_collaboration_policy_snapshot_at(
|
|
root,
|
|
parent_agent_id,
|
|
parent_run_id,
|
|
&policy,
|
|
SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT,
|
|
)?;
|
|
return Ok(SupervisorCollaborationPolicyResolution {
|
|
policy: snapshot.policy,
|
|
policy_fingerprint: snapshot.policy_fingerprint,
|
|
snapshot_fingerprint: Some(snapshot.snapshot_fingerprint),
|
|
binding_source: Some(snapshot.bound_from),
|
|
source: "legacy-run-migration",
|
|
project_policy_status: "matched",
|
|
current_project_policy_fingerprint: Some(supervisor_collaboration_policy_fingerprint(
|
|
&policy,
|
|
)?),
|
|
});
|
|
}
|
|
Ok(SupervisorCollaborationPolicyResolution {
|
|
policy_fingerprint: supervisor_collaboration_policy_fingerprint(&policy)?,
|
|
policy,
|
|
snapshot_fingerprint: None,
|
|
binding_source: None,
|
|
source: unbound_policy.source,
|
|
project_policy_status: unbound_policy.project_policy_status,
|
|
current_project_policy_fingerprint: None,
|
|
})
|
|
}
|
|
|
|
fn supervisor_collaboration_policy_resolution_from_snapshot(
|
|
root: &Path,
|
|
snapshot: SupervisorCollaborationPolicySnapshot,
|
|
) -> Result<SupervisorCollaborationPolicyResolution, String> {
|
|
let current_policy = read_supervisor_collaboration_unbound_policy_for_run_at(
|
|
root,
|
|
&snapshot.parent_agent_id,
|
|
&snapshot.parent_run_id,
|
|
);
|
|
let (project_policy_status, current_project_policy_fingerprint) = match current_policy {
|
|
Ok(current) => {
|
|
let fingerprint = current
|
|
.project_policy_present
|
|
.then(|| supervisor_collaboration_policy_fingerprint(¤t.policy))
|
|
.transpose()?;
|
|
let status = if current.policy == snapshot.policy {
|
|
"matched"
|
|
} else {
|
|
"drifted"
|
|
};
|
|
(status, fingerprint)
|
|
}
|
|
Err(_) => ("unreadable", None),
|
|
};
|
|
Ok(SupervisorCollaborationPolicyResolution {
|
|
policy: snapshot.policy,
|
|
policy_fingerprint: snapshot.policy_fingerprint,
|
|
snapshot_fingerprint: Some(snapshot.snapshot_fingerprint),
|
|
binding_source: Some(snapshot.bound_from),
|
|
source: "run-snapshot",
|
|
project_policy_status,
|
|
current_project_policy_fingerprint,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_policy_status_for_run_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<String, String> {
|
|
let resolution =
|
|
resolve_supervisor_collaboration_policy_for_run_at(root, parent_agent_id, parent_run_id)?;
|
|
Ok(format!(
|
|
"source={} · bindingSource={} · policyFingerprint={} · snapshotFingerprint={} · projectPolicyStatus={} · currentProjectPolicyFingerprint={}",
|
|
resolution.source,
|
|
resolution.binding_source.as_deref().unwrap_or("none"),
|
|
resolution.policy_fingerprint,
|
|
resolution.snapshot_fingerprint.as_deref().unwrap_or("none"),
|
|
resolution.project_policy_status,
|
|
resolution
|
|
.current_project_policy_fingerprint
|
|
.as_deref()
|
|
.unwrap_or("unavailable"),
|
|
))
|
|
}
|
|
|
|
pub(crate) fn read_supervisor_collaboration_state_at(
|
|
root: &Path,
|
|
parent_agent_id: &str,
|
|
parent_run_id: &str,
|
|
) -> Result<SupervisorCollaborationState, String> {
|
|
let initial_static_agent_ids =
|
|
static_delegate_target_agent_ids_at(root, parent_agent_id, parent_run_id)?;
|
|
let isolated = isolated_agent_group_summary_at(root, parent_agent_id, parent_run_id)?;
|
|
Ok(SupervisorCollaborationState {
|
|
initial_static_agent_ids,
|
|
isolated_group_count: isolated.group_count,
|
|
isolated_child_count: isolated.child_count,
|
|
max_isolated_children_per_group: isolated.max_child_count,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_policy_fingerprint(
|
|
policy: &SupervisorCollaborationPolicy,
|
|
) -> Result<String, String> {
|
|
let bytes = serde_json::to_vec(policy)
|
|
.map_err(|error| format!("序列化 Project Supervisor 协作策略指纹失败:{error}"))?;
|
|
Ok(format!("{:x}", Sha256::digest(bytes)))
|
|
}
|
|
|
|
pub(crate) fn preflight_supervisor_collaboration_plan(
|
|
agent_id: &str,
|
|
actions: &[AgentRuntimeToolAction],
|
|
policy: &SupervisorCollaborationPolicy,
|
|
state: &SupervisorCollaborationState,
|
|
) -> Result<SupervisorCollaborationPreflight, String> {
|
|
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
|
return Ok(SupervisorCollaborationPreflight::default());
|
|
}
|
|
let policy = normalize_supervisor_collaboration_policy(policy.clone())?;
|
|
let initial_wave = !state.has_collaboration();
|
|
|
|
if actions.is_empty() {
|
|
if initial_wave && supervisor_collaboration_policy_has_initial_requirements(&policy) {
|
|
return Ok(SupervisorCollaborationPreflight {
|
|
violation: Some(SupervisorCollaborationViolation {
|
|
summary: "Project Supervisor 首批协作不能停留在计划更新".to_string(),
|
|
detail: "当前父 run 尚无协作事实,且项目 policy 明确要求首批专业协作;首批不能停留在计划更新,必须在同一 Provider 批次完整提交协作。".to_string(),
|
|
}),
|
|
..SupervisorCollaborationPreflight::default()
|
|
});
|
|
}
|
|
return Ok(SupervisorCollaborationPreflight::default());
|
|
}
|
|
|
|
let summary = summarize_supervisor_collaboration_actions(actions)?;
|
|
let has_collaboration_action = summary.has_collaboration_action();
|
|
|
|
if !initial_wave
|
|
&& has_collaboration_action
|
|
&& supervisor_collaboration_policy_has_initial_requirements(&policy)
|
|
{
|
|
if let Some(detail) = supervisor_collaboration_initial_wave_gap(&policy, state) {
|
|
return Ok(SupervisorCollaborationPreflight {
|
|
violation: Some(SupervisorCollaborationViolation {
|
|
summary: "Project Supervisor 首批协作不能跨 Provider 批次补齐".to_string(),
|
|
detail: format!(
|
|
"当前父 run 已有部分首批协作事实,但仍不满足项目合同:{detail}。只能恢复原 Provider action 批次或进入人工核对,不能另起批次补齐。"
|
|
),
|
|
}),
|
|
..SupervisorCollaborationPreflight::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
if policy.orchestrator_only_after_delegation
|
|
&& summary.has_project_mutation
|
|
&& (state.has_collaboration() || has_collaboration_action)
|
|
{
|
|
return Ok(SupervisorCollaborationPreflight {
|
|
violation: Some(SupervisorCollaborationViolation {
|
|
summary: "Project Supervisor 已进入协作编排,不能直接修改项目".to_string(),
|
|
detail: "当前父 run 已有协作事实,或本批次正在创建协作;请把项目修改交给专业 Agent,Supervisor 只继续委派、读取、认领回执和验证。".to_string(),
|
|
}),
|
|
..SupervisorCollaborationPreflight::default()
|
|
});
|
|
}
|
|
|
|
if initial_wave
|
|
&& supervisor_collaboration_policy_has_initial_requirements(&policy)
|
|
&& (has_collaboration_action || summary.has_project_mutation)
|
|
{
|
|
let candidate_state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: summary.initial_static_agent_ids.clone(),
|
|
isolated_group_count: summary.isolated_spawn_count,
|
|
isolated_child_count: summary.isolated_child_count,
|
|
max_isolated_children_per_group: summary.isolated_child_count,
|
|
};
|
|
if let Some(detail) = supervisor_collaboration_initial_wave_gap(&policy, &candidate_state) {
|
|
return Ok(SupervisorCollaborationPreflight {
|
|
violation: Some(SupervisorCollaborationViolation {
|
|
summary: "Project Supervisor 首批协作不满足项目合同".to_string(),
|
|
detail,
|
|
}),
|
|
..SupervisorCollaborationPreflight::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
if !has_collaboration_action {
|
|
return Ok(SupervisorCollaborationPreflight::default());
|
|
}
|
|
let contract = build_supervisor_collaboration_contract(&policy, initial_wave, &summary)?;
|
|
Ok(SupervisorCollaborationPreflight {
|
|
contract: Some(contract),
|
|
force_durable_batch: true,
|
|
violation: None,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn validate_supervisor_collaboration_contract(
|
|
actions: &[AgentRuntimeToolAction],
|
|
current_policy: &SupervisorCollaborationPolicy,
|
|
contract: &SupervisorCollaborationContract,
|
|
expected_initial_wave: Option<bool>,
|
|
) -> Result<(), String> {
|
|
if contract.schema_version != SUPERVISOR_COLLABORATION_CONTRACT_SCHEMA_VERSION {
|
|
return Err(format!(
|
|
"不支持的 Project Supervisor 协作合同版本:{}",
|
|
contract.schema_version
|
|
));
|
|
}
|
|
let policy = normalize_supervisor_collaboration_policy(current_policy.clone())?;
|
|
if contract.policy != policy {
|
|
return Err("Project Supervisor 协作策略在 Provider 批次执行前已变化".to_string());
|
|
}
|
|
let policy_fingerprint = supervisor_collaboration_policy_fingerprint(&policy)?;
|
|
if contract.policy_fingerprint != policy_fingerprint {
|
|
return Err("Project Supervisor 协作合同的策略指纹不匹配".to_string());
|
|
}
|
|
if expected_initial_wave.is_some_and(|expected| contract.initial_wave != expected) {
|
|
return Err("Project Supervisor 协作合同的 initialWave 与当前父 run 不一致".to_string());
|
|
}
|
|
let summary = summarize_supervisor_collaboration_actions(actions)?;
|
|
if contract.initial_static_agent_ids != summary.initial_static_agent_ids
|
|
|| contract.repair_delegate_count != summary.repair_delegate_count
|
|
|| contract.isolated_spawn_count != summary.isolated_spawn_count
|
|
|| contract.isolated_child_count != summary.isolated_child_count
|
|
{
|
|
return Err("Project Supervisor 协作合同与 Provider 动作组成不一致".to_string());
|
|
}
|
|
if policy.orchestrator_only_after_delegation
|
|
&& summary.has_project_mutation
|
|
&& summary.has_collaboration_action()
|
|
{
|
|
return Err("Project Supervisor 协作批次混入了项目 mutation".to_string());
|
|
}
|
|
if contract.initial_wave && supervisor_collaboration_policy_has_initial_requirements(&policy) {
|
|
let candidate_state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: summary.initial_static_agent_ids.clone(),
|
|
isolated_group_count: summary.isolated_spawn_count,
|
|
isolated_child_count: summary.isolated_child_count,
|
|
max_isolated_children_per_group: summary.isolated_child_count,
|
|
};
|
|
if let Some(detail) = supervisor_collaboration_initial_wave_gap(&policy, &candidate_state) {
|
|
return Err(format!("Project Supervisor 协作合同不完整:{detail}"));
|
|
}
|
|
}
|
|
let fingerprint = supervisor_collaboration_contract_fingerprint(contract)?;
|
|
if contract.contract_fingerprint != fingerprint {
|
|
return Err("Project Supervisor 协作合同指纹已变化".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_completion_gap(
|
|
policy: &SupervisorCollaborationPolicy,
|
|
state: &SupervisorCollaborationState,
|
|
) -> Option<String> {
|
|
if let Some(detail) = supervisor_collaboration_initial_wave_gap(policy, state) {
|
|
return Some(detail);
|
|
}
|
|
if state.isolated_group_count < policy.min_isolated_groups_before_claim {
|
|
return Some(format!(
|
|
"minIsolatedGroupsBeforeClaim={} · isolatedGroups={}/{}",
|
|
policy.min_isolated_groups_before_claim,
|
|
state.isolated_group_count,
|
|
policy.min_isolated_groups_before_claim,
|
|
));
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_initial_wave_gap(
|
|
policy: &SupervisorCollaborationPolicy,
|
|
state: &SupervisorCollaborationState,
|
|
) -> Option<String> {
|
|
let required_static_count = required_static_delegate_count(policy);
|
|
let required_isolated_count = required_isolated_child_count(policy);
|
|
let actual_static = state
|
|
.initial_static_agent_ids
|
|
.iter()
|
|
.cloned()
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
let missing_static_agents = policy
|
|
.required_static_agent_ids
|
|
.iter()
|
|
.filter(|agent_id| !actual_static.contains(*agent_id))
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
if actual_static.len() >= required_static_count
|
|
&& state.max_isolated_children_per_group >= required_isolated_count
|
|
&& missing_static_agents.is_empty()
|
|
{
|
|
return None;
|
|
}
|
|
Some(format!(
|
|
"requiredInitialWave={:?} · static={}/{} · isolatedChildrenPerGroup={}/{} · isolatedChildrenTotal={} · missingStaticAgents={}",
|
|
policy.required_initial_wave,
|
|
actual_static.len(),
|
|
required_static_count,
|
|
state.max_isolated_children_per_group,
|
|
required_isolated_count,
|
|
state.isolated_child_count,
|
|
if missing_static_agents.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
missing_static_agents.join(",")
|
|
}
|
|
))
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_policy_has_initial_requirements(
|
|
policy: &SupervisorCollaborationPolicy,
|
|
) -> bool {
|
|
required_static_delegate_count(policy) > 0 || required_isolated_child_count(policy) > 0
|
|
}
|
|
|
|
pub(crate) fn is_supervisor_orchestrator_project_mutation_tool(tool: &str) -> bool {
|
|
matches!(
|
|
tool.trim(),
|
|
"file.write"
|
|
| "file.patch"
|
|
| "file.delete"
|
|
| "project.patchset"
|
|
| "project.restore"
|
|
| "project.git_commit"
|
|
| "command.exec"
|
|
| "command.start"
|
|
| "command.stdin"
|
|
| "canvas.asset_generate"
|
|
)
|
|
}
|
|
|
|
fn normalize_supervisor_collaboration_policy(
|
|
mut policy: SupervisorCollaborationPolicy,
|
|
) -> Result<SupervisorCollaborationPolicy, String> {
|
|
if policy.schema_version != SUPERVISOR_COLLABORATION_POLICY_SCHEMA_VERSION {
|
|
return Err(format!(
|
|
"不支持的 Project Supervisor 协作策略版本:{}",
|
|
policy.schema_version
|
|
));
|
|
}
|
|
if policy.min_static_delegates > SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES {
|
|
return Err(format!(
|
|
"minStaticDelegates 不能超过 {SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES}"
|
|
));
|
|
}
|
|
if policy.min_isolated_children > SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN {
|
|
return Err(format!(
|
|
"minIsolatedChildren 不能超过 {SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN}"
|
|
));
|
|
}
|
|
if policy.min_isolated_groups_before_claim
|
|
> SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM
|
|
{
|
|
return Err(format!(
|
|
"minIsolatedGroupsBeforeClaim 不能超过 {SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM}"
|
|
));
|
|
}
|
|
let mut required_ids = std::collections::BTreeSet::new();
|
|
for agent_id in policy.required_static_agent_ids {
|
|
let agent_id = normalize_game_creator_runtime_agent_id(&agent_id)?;
|
|
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || agent_id.starts_with("child-") {
|
|
return Err(
|
|
"requiredStaticAgentIds 只能包含静态专业 Agent,不能包含 Supervisor 或动态 child"
|
|
.to_string(),
|
|
);
|
|
}
|
|
required_ids.insert(agent_id);
|
|
}
|
|
if required_ids.len() > SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES {
|
|
return Err(format!(
|
|
"requiredStaticAgentIds 不能超过 {SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES} 项"
|
|
));
|
|
}
|
|
policy.required_static_agent_ids = required_ids.into_iter().collect();
|
|
let required_action_slots = required_static_delegate_count(&policy)
|
|
.saturating_add(usize::from(required_isolated_child_count(&policy) > 0));
|
|
if required_action_slots > AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT {
|
|
return Err(format!(
|
|
"协作策略至少需要 {required_action_slots} 个 Provider action,超过单批次上限 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT}"
|
|
));
|
|
}
|
|
Ok(policy)
|
|
}
|
|
|
|
fn required_static_delegate_count(policy: &SupervisorCollaborationPolicy) -> usize {
|
|
let mode_minimum = usize::from(matches!(
|
|
policy.required_initial_wave,
|
|
SupervisorInitialCollaborationWave::Static | SupervisorInitialCollaborationWave::Mixed
|
|
));
|
|
mode_minimum
|
|
.max(policy.min_static_delegates)
|
|
.max(policy.required_static_agent_ids.len())
|
|
}
|
|
|
|
fn required_isolated_child_count(policy: &SupervisorCollaborationPolicy) -> usize {
|
|
let mode_minimum = usize::from(matches!(
|
|
policy.required_initial_wave,
|
|
SupervisorInitialCollaborationWave::Isolated | SupervisorInitialCollaborationWave::Mixed
|
|
));
|
|
mode_minimum.max(policy.min_isolated_children)
|
|
}
|
|
|
|
fn summarize_supervisor_collaboration_actions(
|
|
actions: &[AgentRuntimeToolAction],
|
|
) -> Result<SupervisorCollaborationActionSummary, String> {
|
|
let mut summary = SupervisorCollaborationActionSummary::default();
|
|
let mut static_agents = std::collections::BTreeSet::new();
|
|
for action in actions {
|
|
let tool = action.tool.trim();
|
|
summary.has_project_mutation |= is_supervisor_orchestrator_project_mutation_tool(tool);
|
|
match tool {
|
|
"agent.delegate" => {
|
|
let input = action.input.as_object().ok_or_else(|| {
|
|
"Project Supervisor agent.delegate input 必须是 object".to_string()
|
|
})?;
|
|
let agent_id = input
|
|
.get("agentId")
|
|
.or_else(|| input.get("agent_id"))
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| "Project Supervisor agent.delegate 缺少 agentId".to_string())?;
|
|
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
|
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
|| agent_id.starts_with("child-")
|
|
{
|
|
return Err(
|
|
"Project Supervisor agent.delegate 只能指向静态专业 Agent".to_string()
|
|
);
|
|
}
|
|
let repair = input
|
|
.get("repairOfDelegationId")
|
|
.or_else(|| input.get("repair_of_delegation_id"))
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|value| !value.trim().is_empty());
|
|
if repair {
|
|
summary.repair_delegate_count = summary.repair_delegate_count.saturating_add(1);
|
|
} else {
|
|
static_agents.insert(agent_id);
|
|
}
|
|
}
|
|
"agent.spawn_isolated" => {
|
|
let input = action.input.as_object().ok_or_else(|| {
|
|
"Project Supervisor agent.spawn_isolated input 必须是 object".to_string()
|
|
})?;
|
|
if input.get("joinMode").and_then(Value::as_str) != Some("all") {
|
|
return Err(
|
|
"Project Supervisor agent.spawn_isolated 只允许 joinMode=all".to_string(),
|
|
);
|
|
}
|
|
let children =
|
|
input
|
|
.get("children")
|
|
.and_then(Value::as_array)
|
|
.ok_or_else(|| {
|
|
"Project Supervisor agent.spawn_isolated 缺少 children".to_string()
|
|
})?;
|
|
if children.is_empty()
|
|
|| children.len() > SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN
|
|
{
|
|
return Err(format!(
|
|
"Project Supervisor agent.spawn_isolated children 必须在 1-{SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN} 之间"
|
|
));
|
|
}
|
|
summary.isolated_spawn_count = summary.isolated_spawn_count.saturating_add(1);
|
|
if summary.isolated_spawn_count > 1 {
|
|
return Err(
|
|
"Project Supervisor 单个 Provider 批次最多包含一个 agent.spawn_isolated"
|
|
.to_string(),
|
|
);
|
|
}
|
|
summary.isolated_child_count = children.len();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
summary.initial_static_agent_ids = static_agents.into_iter().collect();
|
|
Ok(summary)
|
|
}
|
|
|
|
pub(crate) fn supervisor_collaboration_actions_require_contract(
|
|
actions: &[AgentRuntimeToolAction],
|
|
) -> Result<bool, String> {
|
|
summarize_supervisor_collaboration_actions(actions)
|
|
.map(|summary| summary.has_collaboration_action())
|
|
}
|
|
|
|
fn build_supervisor_collaboration_contract(
|
|
policy: &SupervisorCollaborationPolicy,
|
|
initial_wave: bool,
|
|
summary: &SupervisorCollaborationActionSummary,
|
|
) -> Result<SupervisorCollaborationContract, String> {
|
|
let policy_fingerprint = supervisor_collaboration_policy_fingerprint(policy)?;
|
|
let mut contract = SupervisorCollaborationContract {
|
|
schema_version: SUPERVISOR_COLLABORATION_CONTRACT_SCHEMA_VERSION.to_string(),
|
|
policy: policy.clone(),
|
|
policy_fingerprint,
|
|
initial_wave,
|
|
initial_static_agent_ids: summary.initial_static_agent_ids.clone(),
|
|
repair_delegate_count: summary.repair_delegate_count,
|
|
isolated_spawn_count: summary.isolated_spawn_count,
|
|
isolated_child_count: summary.isolated_child_count,
|
|
contract_fingerprint: String::new(),
|
|
};
|
|
contract.contract_fingerprint = supervisor_collaboration_contract_fingerprint(&contract)?;
|
|
Ok(contract)
|
|
}
|
|
|
|
fn supervisor_collaboration_contract_fingerprint(
|
|
contract: &SupervisorCollaborationContract,
|
|
) -> Result<String, String> {
|
|
let identity = serde_json::to_vec(&serde_json::json!({
|
|
"schemaVersion": contract.schema_version,
|
|
"policy": contract.policy,
|
|
"policyFingerprint": contract.policy_fingerprint,
|
|
"initialWave": contract.initial_wave,
|
|
"initialStaticAgentIds": contract.initial_static_agent_ids,
|
|
"repairDelegateCount": contract.repair_delegate_count,
|
|
"isolatedSpawnCount": contract.isolated_spawn_count,
|
|
"isolatedChildCount": contract.isolated_child_count,
|
|
}))
|
|
.map_err(|error| format!("序列化 Project Supervisor 协作合同指纹失败:{error}"))?;
|
|
Ok(format!("{:x}", Sha256::digest(identity)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn delegate(agent_id: &str, repair: Option<&str>) -> AgentRuntimeToolAction {
|
|
AgentRuntimeToolAction {
|
|
tool: "agent.delegate".to_string(),
|
|
reason: Some("委派专业交付".to_string()),
|
|
input: serde_json::json!({
|
|
"agentId": agent_id,
|
|
"task": "完成专业交付",
|
|
"acceptanceCriteria": ["交付可验收"],
|
|
"expectedArtifacts": [],
|
|
"repairOfDelegationId": repair,
|
|
"runId": null,
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn spawn(children: usize) -> AgentRuntimeToolAction {
|
|
AgentRuntimeToolAction {
|
|
tool: "agent.spawn_isolated".to_string(),
|
|
reason: Some("并行隔离检查".to_string()),
|
|
input: serde_json::json!({
|
|
"children": (0..children).map(|index| serde_json::json!({
|
|
"templateAgentId": "code-prototype",
|
|
"task": format!("检查 {index}"),
|
|
"acceptanceCriteria": ["检查完成"],
|
|
"expectedArtifacts": [],
|
|
"writeScopes": [format!("game/check-{index}/**")],
|
|
})).collect::<Vec<_>>(),
|
|
"joinMode": "all",
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn verify() -> AgentRuntimeToolAction {
|
|
AgentRuntimeToolAction {
|
|
tool: "project.verify".to_string(),
|
|
reason: Some("验证项目".to_string()),
|
|
input: serde_json::json!({
|
|
"script": "test",
|
|
"expectedCommand": "cargo test",
|
|
"timeoutSeconds": 120,
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn mixed_policy() -> SupervisorCollaborationPolicy {
|
|
SupervisorCollaborationPolicy {
|
|
required_initial_wave: SupervisorInitialCollaborationWave::Mixed,
|
|
min_static_delegates: 2,
|
|
required_static_agent_ids: vec![
|
|
"art-director".to_string(),
|
|
"design-director".to_string(),
|
|
],
|
|
min_isolated_children: 2,
|
|
..SupervisorCollaborationPolicy::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_default_json_and_fingerprints_remain_v1_compatible() {
|
|
const OLD_DEFAULT_POLICY_JSON: &str = "{\"schemaVersion\":\"game-creator-supervisor-collaboration-policy.v1\",\"requiredInitialWave\":\"auto\",\"minStaticDelegates\":0,\"requiredStaticAgentIds\":[],\"minIsolatedChildren\":0,\"orchestratorOnlyAfterDelegation\":true}";
|
|
const OLD_DEFAULT_POLICY_FINGERPRINT: &str =
|
|
"9962617595c7d20ea24d7b18b4f77eac2160edaf0daaf83960d5c92014e8bd2b";
|
|
const OLD_DEFAULT_CONTRACT_FINGERPRINT: &str =
|
|
"90845e3e0f817ac99fb4f4574eae1662235739752268c4d9884f815518eab2bb";
|
|
|
|
let policy = SupervisorCollaborationPolicy::default();
|
|
assert_eq!(
|
|
serde_json::to_string(&policy).expect("serialize default policy"),
|
|
OLD_DEFAULT_POLICY_JSON
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<SupervisorCollaborationPolicy>(OLD_DEFAULT_POLICY_JSON)
|
|
.expect("deserialize old default policy"),
|
|
policy
|
|
);
|
|
assert_eq!(
|
|
supervisor_collaboration_policy_fingerprint(&policy)
|
|
.expect("fingerprint default policy"),
|
|
OLD_DEFAULT_POLICY_FINGERPRINT
|
|
);
|
|
|
|
let actions = vec![delegate("design-director", None)];
|
|
let contract = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&policy,
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight default policy")
|
|
.contract
|
|
.expect("default policy contract");
|
|
let contract_json = serde_json::to_value(&contract).expect("serialize default contract");
|
|
assert!(contract_json["policy"]
|
|
.get("minIsolatedGroupsBeforeClaim")
|
|
.is_none());
|
|
assert_eq!(contract.policy_fingerprint, OLD_DEFAULT_POLICY_FINGERPRINT);
|
|
assert_eq!(
|
|
contract.contract_fingerprint,
|
|
OLD_DEFAULT_CONTRACT_FINGERPRINT
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_initial_mixed_wave_allows_one_staged_group() {
|
|
let policy = SupervisorCollaborationPolicy {
|
|
min_isolated_groups_before_claim: 2,
|
|
..mixed_policy()
|
|
};
|
|
let actions = vec![
|
|
delegate("design-director", None),
|
|
delegate("art-director", None),
|
|
spawn(2),
|
|
];
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&policy,
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight staged mixed wave");
|
|
assert!(result.violation.is_none());
|
|
let contract = result.contract.expect("staged mixed contract");
|
|
validate_supervisor_collaboration_contract(&actions, &policy, &contract, Some(true))
|
|
.expect("validate staged mixed contract");
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_completion_requires_staged_isolated_groups() {
|
|
let policy = SupervisorCollaborationPolicy {
|
|
min_isolated_groups_before_claim: 2,
|
|
..mixed_policy()
|
|
};
|
|
let mut state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: vec![
|
|
"art-director".to_string(),
|
|
"design-director".to_string(),
|
|
],
|
|
isolated_group_count: 1,
|
|
isolated_child_count: 2,
|
|
max_isolated_children_per_group: 2,
|
|
};
|
|
let gap = supervisor_collaboration_completion_gap(&policy, &state)
|
|
.expect("one isolated group must leave a completion gap");
|
|
assert!(gap.contains("isolatedGroups=1/2"));
|
|
|
|
state.isolated_group_count = 2;
|
|
state.isolated_child_count = 4;
|
|
assert!(supervisor_collaboration_completion_gap(&policy, &state).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_nonzero_group_requirement_round_trips_stably() {
|
|
let policy = SupervisorCollaborationPolicy {
|
|
min_isolated_groups_before_claim: 2,
|
|
..SupervisorCollaborationPolicy::default()
|
|
};
|
|
let json = serde_json::to_string(&policy).expect("serialize nonzero group policy");
|
|
assert!(json.contains("\"minIsolatedGroupsBeforeClaim\":2"));
|
|
let decoded = serde_json::from_str::<SupervisorCollaborationPolicy>(&json)
|
|
.expect("deserialize nonzero group policy");
|
|
assert_eq!(decoded, policy);
|
|
assert_eq!(
|
|
supervisor_collaboration_policy_fingerprint(&policy)
|
|
.expect("fingerprint nonzero group policy"),
|
|
"7c2764dde3a5be4a62e11f0acdb7e1b2f6445377f1afdc548e1b7e137ce9fc84"
|
|
);
|
|
assert_eq!(
|
|
supervisor_collaboration_policy_fingerprint(&decoded)
|
|
.expect("fingerprint round-tripped group policy"),
|
|
"7c2764dde3a5be4a62e11f0acdb7e1b2f6445377f1afdc548e1b7e137ce9fc84"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_rejects_excessive_group_requirement() {
|
|
let policy = SupervisorCollaborationPolicy {
|
|
min_isolated_groups_before_claim:
|
|
SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM + 1,
|
|
..SupervisorCollaborationPolicy::default()
|
|
};
|
|
let error = normalize_supervisor_collaboration_policy(policy)
|
|
.expect_err("excessive group requirement must fail");
|
|
assert!(error.contains("minIsolatedGroupsBeforeClaim 不能超过 16"));
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_blocks_empty_required_initial_wave() {
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[],
|
|
&mixed_policy(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight empty required initial wave");
|
|
let violation = result
|
|
.violation
|
|
.expect("required initial wave must reject empty actions");
|
|
assert_eq!(
|
|
violation.summary,
|
|
"Project Supervisor 首批协作不能停留在计划更新"
|
|
);
|
|
assert!(violation.detail.contains("首批不能停留在计划更新"));
|
|
assert!(violation
|
|
.detail
|
|
.contains("必须在同一 Provider 批次完整提交协作"));
|
|
assert!(result.contract.is_none());
|
|
assert!(!result.force_durable_batch);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_allows_empty_actions_after_collaboration() {
|
|
let state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: vec!["design-director".to_string()],
|
|
..SupervisorCollaborationState::default()
|
|
};
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[],
|
|
&mixed_policy(),
|
|
&state,
|
|
)
|
|
.expect("preflight empty actions after collaboration");
|
|
assert_eq!(result, SupervisorCollaborationPreflight::default());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_allows_empty_actions_without_initial_requirements() {
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[],
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight empty actions without initial requirements");
|
|
assert_eq!(result, SupervisorCollaborationPreflight::default());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_blocks_partial_mixed_wave() {
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[delegate("design-director", None)],
|
|
&mixed_policy(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight partial mixed wave");
|
|
assert!(result.violation.is_some());
|
|
assert!(result.contract.is_none());
|
|
assert!(!result.force_durable_batch);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_accepts_complete_mixed_wave() {
|
|
let actions = vec![
|
|
delegate("design-director", None),
|
|
delegate("art-director", None),
|
|
spawn(2),
|
|
];
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&mixed_policy(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight complete mixed wave");
|
|
assert!(result.violation.is_none());
|
|
assert!(result.force_durable_batch);
|
|
let contract = result.contract.expect("durable collaboration contract");
|
|
validate_supervisor_collaboration_contract(
|
|
&actions,
|
|
&mixed_policy(),
|
|
&contract,
|
|
Some(true),
|
|
)
|
|
.expect("validate complete mixed contract");
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_allows_later_isolated_group_after_complete_wave() {
|
|
let state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: vec![
|
|
"art-director".to_string(),
|
|
"design-director".to_string(),
|
|
],
|
|
isolated_group_count: 1,
|
|
isolated_child_count: 2,
|
|
max_isolated_children_per_group: 2,
|
|
};
|
|
let actions = vec![spawn(1)];
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&mixed_policy(),
|
|
&state,
|
|
)
|
|
.expect("preflight later isolated group");
|
|
assert!(result.violation.is_none());
|
|
assert!(result.force_durable_batch);
|
|
let contract = result.contract.expect("later collaboration contract");
|
|
assert!(!contract.initial_wave);
|
|
assert_eq!(contract.isolated_spawn_count, 1);
|
|
assert_eq!(contract.isolated_child_count, 1);
|
|
validate_supervisor_collaboration_contract(
|
|
&actions,
|
|
&mixed_policy(),
|
|
&contract,
|
|
Some(false),
|
|
)
|
|
.expect("validate later isolated contract");
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_rejects_cross_batch_initial_wave_completion() {
|
|
let state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: vec!["design-director".to_string()],
|
|
..SupervisorCollaborationState::default()
|
|
};
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[delegate("art-director", None), spawn(2)],
|
|
&mixed_policy(),
|
|
&state,
|
|
)
|
|
.expect("preflight cross-batch initial wave completion");
|
|
let violation = result
|
|
.violation
|
|
.expect("partial initial wave must not be completed by a new batch");
|
|
assert!(violation.summary.contains("不能跨 Provider 批次补齐"));
|
|
assert!(result.contract.is_none());
|
|
assert!(!result.force_durable_batch);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_blocks_mutation_with_collaboration() {
|
|
let mut actions = vec![delegate("design-director", None)];
|
|
actions.push(AgentRuntimeToolAction {
|
|
tool: "file.write".to_string(),
|
|
reason: Some("总控直接写项目".to_string()),
|
|
input: serde_json::json!({"path":"game/out.txt","content":"blocked"}),
|
|
});
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight collaboration mutation");
|
|
assert!(result.violation.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_allows_repair_and_verify_after_delegation() {
|
|
let state = SupervisorCollaborationState {
|
|
initial_static_agent_ids: vec!["design-director".to_string()],
|
|
isolated_group_count: 0,
|
|
isolated_child_count: 0,
|
|
max_isolated_children_per_group: 0,
|
|
};
|
|
let actions = vec![delegate("design-director", Some("delivery-1")), verify()];
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&state,
|
|
)
|
|
.expect("preflight repair and verify");
|
|
assert!(result.violation.is_none());
|
|
assert!(result.force_durable_batch);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_rejects_contract_fingerprint_tampering() {
|
|
let actions = vec![delegate("design-director", None)];
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&actions,
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight contract");
|
|
let mut contract = result.contract.expect("contract");
|
|
contract.contract_fingerprint = "0".repeat(64);
|
|
assert!(validate_supervisor_collaboration_contract(
|
|
&actions,
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&contract,
|
|
Some(true),
|
|
)
|
|
.expect_err("tampered contract must fail")
|
|
.contains("合同指纹"));
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_default_has_no_completion_gap() {
|
|
assert!(supervisor_collaboration_completion_gap(
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_classifies_process_stdin_as_mutation() {
|
|
assert!(is_supervisor_orchestrator_project_mutation_tool(
|
|
"command.stdin"
|
|
));
|
|
assert!(!is_supervisor_orchestrator_project_mutation_tool(
|
|
"command.poll"
|
|
));
|
|
assert!(!is_supervisor_orchestrator_project_mutation_tool(
|
|
"command.terminate"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_requires_isolated_minimum_in_one_group() {
|
|
let policy = SupervisorCollaborationPolicy {
|
|
required_initial_wave: SupervisorInitialCollaborationWave::Isolated,
|
|
min_isolated_children: 3,
|
|
..SupervisorCollaborationPolicy::default()
|
|
};
|
|
let split_groups = SupervisorCollaborationState {
|
|
isolated_group_count: 3,
|
|
isolated_child_count: 3,
|
|
max_isolated_children_per_group: 1,
|
|
..SupervisorCollaborationState::default()
|
|
};
|
|
assert!(supervisor_collaboration_completion_gap(&policy, &split_groups).is_some());
|
|
let complete_group = SupervisorCollaborationState {
|
|
isolated_group_count: 1,
|
|
isolated_child_count: 3,
|
|
max_isolated_children_per_group: 3,
|
|
..SupervisorCollaborationState::default()
|
|
};
|
|
assert!(supervisor_collaboration_completion_gap(&policy, &complete_group).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_rejects_multiple_isolated_spawns_per_batch() {
|
|
let error = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[spawn(1), spawn(1)],
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect_err("multiple isolated groups must not share one provider batch");
|
|
assert!(error.contains("最多包含一个"));
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_classifies_snake_case_repair_as_repair() {
|
|
let mut action = delegate("design-director", None);
|
|
let input = action.input.as_object_mut().expect("delegate input");
|
|
input.remove("repairOfDelegationId");
|
|
input.insert(
|
|
"repair_of_delegation_id".to_string(),
|
|
Value::String("delivery-original".to_string()),
|
|
);
|
|
let result = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[action],
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect("preflight snake-case repair");
|
|
let contract = result.contract.expect("repair contract");
|
|
assert!(contract.initial_static_agent_ids.is_empty());
|
|
assert_eq!(contract.repair_delegate_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn supervisor_collaboration_policy_rejects_dynamic_child_as_static_delegate() {
|
|
let error = preflight_supervisor_collaboration_plan(
|
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
&[delegate("child-code-prototype-1", None)],
|
|
&SupervisorCollaborationPolicy::default(),
|
|
&SupervisorCollaborationState::default(),
|
|
)
|
|
.expect_err("dynamic child must not count as static delegate");
|
|
assert!(error.contains("静态专业 Agent"));
|
|
}
|
|
}
|