Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/isolated_agent.rs
T
kdletters 5822b64d7c 合并 Godot 编辑器插件与常用操作指导到主分支
接入 Godot 原生桥、受控执行、Runner 回执与编辑器操作指南
保留主分支 Cocos 和 Unity 跨工程能力及外置提示词结构
解决插件生命周期、工具目录、前端启动和文档合并冲突
2026-09-20 17:38:48 +08:00

2666 lines
98 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::agent::{
read_agent_runtime_json_sidecar_with_max_bytes, sanitize_prompt_context,
write_agent_runtime_json_sidecar_with_max_bytes, AGENT_RUNTIME_TASK_MAX_CHARS,
};
use super::project::{
normalize_relative_path, resolve_local_project_path, unix_timestamp, validate_project_root,
};
use platform_agent::game_creation::{
derive_game_creation_isolated_agent_group_at_depth,
derive_game_creation_isolated_agent_identity, join_game_creation_isolated_agent_results,
validate_game_creation_isolated_agent_child_result,
validate_game_creation_isolated_agent_spawn_request_at_depth,
GameCreationIsolatedAgentArtifact, GameCreationIsolatedAgentChildResult,
GameCreationIsolatedAgentChildSpec, GameCreationIsolatedAgentEvidence,
GameCreationIsolatedAgentJoinMode, GameCreationIsolatedAgentResultStatus,
GameCreationIsolatedAgentSpawnRequest,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
pub(crate) const ISOLATED_AGENT_INSTANCE_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-instance.v1";
pub(crate) const ISOLATED_AGENT_GROUP_SCHEMA_VERSION: &str = "game-creator-isolated-agent-group.v1";
pub(crate) const ISOLATED_AGENT_RESULT_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-result.v1";
pub(crate) const ISOLATED_AGENT_JOIN_DELIVERY_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-join-delivery.v1";
pub(crate) const ISOLATED_AGENT_JOIN_CLAIM_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-join-claim.v1";
pub(crate) const ISOLATED_AGENT_JOIN_PROMPT_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-join-prompt.v1";
pub(crate) const ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION: &str =
"game-creator-isolated-agent-private-memory.v1";
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
"project.verify",
"project.bootstrap",
"project.git_commit",
"command.exec",
"command.start",
"command.stdin",
"cocos.editor.execute",
"unity.editor.execute",
"godot.editor.execute",
"preview.start",
"agent.delegate",
"agent.spawn_isolated",
"agent.goal_contract",
"agent.acceptance_update",
"project.restore",
"agent.schedule_ready",
"canvas.asset_generate",
"canvas.asset_import",
"task.create",
"task.update",
];
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[
"project.verify",
"project.bootstrap",
"project.git_commit",
"command.exec",
"command.start",
"command.stdin",
"cocos.editor.execute",
"unity.editor.execute",
"godot.editor.execute",
"preview.start",
"agent.delegate",
"agent.spawn_isolated",
"agent.goal_contract",
"agent.acceptance_update",
"project.restore",
"agent.schedule_ready",
"canvas.asset_generate",
"canvas.asset_import",
"task.create",
"task.update",
"blackboard.write",
];
const ISOLATED_AGENT_INSTANCE_DIR: &str = ".agent/runtime/isolated-agents/instances";
const ISOLATED_AGENT_GROUP_DIR: &str = ".agent/runtime/isolated-agents/groups";
const ISOLATED_AGENT_RESULT_DIR: &str = ".agent/runtime/isolated-agents/results";
const ISOLATED_AGENT_JOIN_DELIVERY_DIR: &str = ".agent/runtime/isolated-agents/join-deliveries";
const ISOLATED_AGENT_JOIN_CLAIM_DIR: &str = ".agent/runtime/isolated-agents/join-claims";
const ISOLATED_AGENT_PRIVATE_MEMORY_DIR: &str = ".agent/runtime/isolated-agents/memory";
const ISOLATED_AGENT_RECORD_MAX_BYTES: usize = 512 * 1024;
const ISOLATED_AGENT_PRIVATE_MEMORY_MAX_BYTES: usize = 64 * 1024;
const ISOLATED_AGENT_MAX_SCANNED_ARTIFACT_ENTRIES: usize = 10_000;
const ISOLATED_AGENT_MAX_RESULT_ARTIFACTS: usize = 32;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct IsolatedAgentInstanceRecord {
pub(crate) schema_version: String,
pub(crate) parent_agent_id: String,
pub(crate) parent_session_id: String,
pub(crate) parent_run_id: String,
pub(crate) parent_action_id: String,
pub(crate) delegation_group_id: String,
pub(crate) delegation_id: String,
pub(crate) child_index: usize,
pub(crate) instance_id: String,
pub(crate) template_agent_id: String,
pub(crate) session_id: String,
pub(crate) run_id: String,
pub(crate) depth: u8,
pub(crate) task: String,
pub(crate) acceptance_criteria: Vec<String>,
pub(crate) expected_artifacts: Vec<String>,
pub(crate) write_scopes: Vec<String>,
pub(crate) created_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct IsolatedAgentGroupRecord {
pub(crate) schema_version: String,
pub(crate) parent_agent_id: String,
pub(crate) parent_session_id: String,
pub(crate) parent_run_id: String,
pub(crate) parent_action_id: String,
pub(crate) delegation_group_id: String,
pub(crate) join_run_id: String,
pub(crate) depth: u8,
pub(crate) join_mode: GameCreationIsolatedAgentJoinMode,
pub(crate) request: GameCreationIsolatedAgentSpawnRequest,
pub(crate) instance_ids: Vec<String>,
pub(crate) created_at: u64,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct IsolatedAgentGroupSummary {
pub(crate) group_count: usize,
pub(crate) child_count: usize,
pub(crate) max_child_count: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct IsolatedAgentResultRecord {
pub(crate) schema_version: String,
pub(crate) delegation_group_id: String,
pub(crate) child_index: usize,
pub(crate) result: GameCreationIsolatedAgentChildResult,
pub(crate) recorded_at: u64,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum IsolatedAgentJoinDeliveryStatus {
Dispatched,
ClaimedByParent,
Suppressed,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum IsolatedAgentJoinDeliveryTarget {
#[default]
Continuation,
ParentWake,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct IsolatedAgentJoinDeliveryRecord {
pub(crate) schema_version: String,
pub(crate) parent_agent_id: String,
pub(crate) parent_run_id: String,
pub(crate) delegation_group_id: String,
pub(crate) join_run_id: String,
pub(crate) status: IsolatedAgentJoinDeliveryStatus,
#[serde(default)]
pub(crate) delivery_target: IsolatedAgentJoinDeliveryTarget,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) queued_run_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) claimed_by_action_id: Option<String>,
pub(crate) updated_at: u64,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum IsolatedAgentJoinClaimStatus {
Prepared,
Committed,
Observed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct IsolatedAgentTerminalTask {
pub(crate) agent_id: String,
pub(crate) session_id: String,
pub(crate) run_id: String,
pub(crate) delegation_id: String,
pub(crate) status: String,
pub(crate) phase: String,
pub(crate) terminal_detail: Option<String>,
pub(crate) error: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct IsolatedAgentVerificationGateSnapshot {
pub(crate) agent_id: String,
pub(crate) run_id: String,
pub(crate) requires_verification: bool,
pub(crate) mutation_revision: Option<u64>,
pub(crate) verified_revision: Option<u64>,
pub(crate) last_verification_tool: Option<String>,
pub(crate) last_verification_status: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct JoinDispatch {
pub(crate) parent_agent_id: String,
pub(crate) parent_session_id: String,
pub(crate) parent_run_id: String,
pub(crate) parent_action_id: String,
pub(crate) delegation_group_id: String,
pub(crate) join_run_id: String,
pub(crate) source: String,
pub(crate) prompt: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct IsolatedAgentJoinClaimRecord {
pub(crate) schema_version: String,
pub(crate) parent_agent_id: String,
pub(crate) parent_run_id: String,
pub(crate) action_id: String,
pub(crate) status: IsolatedAgentJoinClaimStatus,
pub(crate) joins: Vec<JoinDispatch>,
pub(crate) updated_at: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct IsolatedAgentBuildResult {
pub(crate) result: GameCreationIsolatedAgentChildResult,
pub(crate) join_dispatch: Option<JoinDispatch>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct IsolatedAgentCancelTarget {
pub(crate) delegation_group_id: String,
pub(crate) delegation_id: String,
pub(crate) instance_id: String,
pub(crate) session_id: String,
pub(crate) run_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct IsolatedAgentPrivateMemoryRecord {
schema_version: String,
instance_id: String,
content: String,
updated_at: u64,
}
pub(crate) fn create_or_read_isolated_group_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
parent_session_id: &str,
parent_action_id: &str,
request: &GameCreationIsolatedAgentSpawnRequest,
) -> Result<IsolatedAgentGroupRecord, String> {
validate_project_root(root)?;
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(parent_session_id, "parentSessionId", 160)?;
validate_safe_id(parent_run_id, "parentRunId", 160)?;
validate_safe_id(parent_action_id, "parentActionId", 256)?;
let parent_depth = if parent_agent_id.starts_with("child-") {
let parent = resolve_isolated_agent_instance_at(root, parent_agent_id)?;
if parent.session_id != parent_session_id || parent.run_id != parent_run_id {
return Err("动态隔离父 Agent 的 session/run 身份不一致".to_string());
}
parent.depth
} else {
0
};
let request = sanitize_spawn_request(root, request)?;
validate_game_creation_isolated_agent_spawn_request_at_depth(&request, parent_depth)
.map_err(|error| error.to_string())?;
let duplicate = list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?
.into_iter()
.find(|group| {
group.parent_agent_id == parent_agent_id
&& group.parent_session_id == parent_session_id
&& group.parent_run_id == parent_run_id
&& group.parent_action_id != parent_action_id
&& group.depth == parent_depth + 1
&& group.request == request
});
if let Some(duplicate) = duplicate {
return Err(format!(
"相同动态隔离 Agent 请求已由 action {} 创建,不得在同一父 run 重复 spawn",
duplicate.parent_action_id
));
}
let derived = derive_game_creation_isolated_agent_group_at_depth(
parent_action_id,
&request,
parent_depth,
)
.map_err(|error| error.to_string())?;
let group_path = isolated_group_relative_path(&derived.delegation_group_id);
let existing =
read_json_record::<IsolatedAgentGroupRecord>(root, &group_path, "动态隔离 Agent group")?;
let created_at = existing
.as_ref()
.map(|record| record.created_at)
.unwrap_or_else(unix_timestamp);
let group = IsolatedAgentGroupRecord {
schema_version: ISOLATED_AGENT_GROUP_SCHEMA_VERSION.to_string(),
parent_agent_id: parent_agent_id.to_string(),
parent_session_id: parent_session_id.to_string(),
parent_run_id: parent_run_id.to_string(),
parent_action_id: parent_action_id.to_string(),
delegation_group_id: derived.delegation_group_id.clone(),
join_run_id: derived.join_run_id.clone(),
depth: derived.depth,
join_mode: derived.join_mode,
request: request.clone(),
instance_ids: derived
.children
.iter()
.map(|child| child.instance_id.clone())
.collect(),
created_at,
};
validate_isolated_group_record(root, &group)?;
if existing.as_ref().is_some_and(|record| record != &group) {
return Err(format!(
"parentActionId 已绑定不同的动态隔离 group:{parent_action_id}"
));
}
for (child, spec) in derived.children.iter().zip(request.children.iter()) {
let relative_path = isolated_instance_relative_path(&child.instance_id);
let existing = read_json_record::<IsolatedAgentInstanceRecord>(
root,
&relative_path,
"动态隔离 Agent instance",
)?;
let instance = IsolatedAgentInstanceRecord {
schema_version: ISOLATED_AGENT_INSTANCE_SCHEMA_VERSION.to_string(),
parent_agent_id: parent_agent_id.to_string(),
parent_session_id: parent_session_id.to_string(),
parent_run_id: parent_run_id.to_string(),
parent_action_id: parent_action_id.to_string(),
delegation_group_id: child.delegation_group_id.clone(),
delegation_id: child.delegation_id.clone(),
child_index: child.child_index,
instance_id: child.instance_id.clone(),
template_agent_id: child.template_agent_id.clone(),
session_id: isolated_child_session_id(&child.instance_id),
run_id: isolated_child_run_id(&child.delegation_id),
depth: derived.depth,
task: spec.task.clone(),
acceptance_criteria: spec.acceptance_criteria.clone(),
expected_artifacts: spec.expected_artifacts.clone(),
write_scopes: spec.write_scopes.clone(),
created_at: existing
.as_ref()
.map(|record| record.created_at)
.unwrap_or(created_at),
};
validate_isolated_instance_record(root, &instance)?;
match existing {
Some(record) if record != instance => {
return Err(format!(
"动态隔离 Agent instance 身份冲突:{}",
child.instance_id
));
}
Some(_) => {}
None => write_json_record(root, &relative_path, "动态隔离 Agent instance", &instance)?,
}
}
if existing.is_none() {
write_json_record(root, &group_path, "动态隔离 Agent group", &group)?;
}
Ok(group)
}
pub(crate) fn isolated_agent_group_summary_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<IsolatedAgentGroupSummary, String> {
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(parent_run_id, "parentRunId", 160)?;
let mut summary = IsolatedAgentGroupSummary::default();
for group in list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?
.into_iter()
.filter(|group| {
group.parent_agent_id == parent_agent_id && group.parent_run_id == parent_run_id
}) {
summary.group_count = summary.group_count.saturating_add(1);
summary.child_count = summary
.child_count
.saturating_add(group.request.children.len());
summary.max_child_count = summary.max_child_count.max(group.request.children.len());
}
Ok(summary)
}
pub(crate) fn isolated_agent_spawn_has_durable_side_effect_at(
root: &Path,
parent_agent_id: &str,
parent_session_id: &str,
parent_run_id: &str,
parent_action_id: &str,
) -> Result<bool, String> {
validate_project_root(root)?;
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(parent_session_id, "parentSessionId", 160)?;
validate_safe_id(parent_run_id, "parentRunId", 160)?;
validate_safe_id(parent_action_id, "parentActionId", 256)?;
let mut found = false;
for group in list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?
.into_iter()
.filter(|group| group.parent_action_id == parent_action_id)
{
if group.parent_agent_id != parent_agent_id
|| group.parent_session_id != parent_session_id
|| group.parent_run_id != parent_run_id
{
return Err("恢复 isolated spawn 时 durable group 父身份冲突".to_string());
}
found = true;
}
for instance in list_json_records(
root,
ISOLATED_AGENT_INSTANCE_DIR,
"动态隔离 Agent instance",
|record| validate_isolated_instance_record(root, record),
)?
.into_iter()
.filter(|instance| instance.parent_action_id == parent_action_id)
{
if instance.parent_agent_id != parent_agent_id
|| instance.parent_session_id != parent_session_id
|| instance.parent_run_id != parent_run_id
{
return Err("恢复 isolated spawn 时 durable instance 父身份冲突".to_string());
}
found = true;
}
Ok(found)
}
pub(crate) fn list_isolated_agent_instances_at(
root: &Path,
) -> Result<Vec<IsolatedAgentInstanceRecord>, String> {
list_json_records(
root,
ISOLATED_AGENT_INSTANCE_DIR,
"动态隔离 Agent instance",
|record| validate_isolated_instance_record(root, record),
)
}
pub(crate) fn resolve_isolated_agent_instance_at(
root: &Path,
instance_id: &str,
) -> Result<IsolatedAgentInstanceRecord, String> {
validate_safe_id(instance_id, "instanceId", 96)?;
let record = read_json_record::<IsolatedAgentInstanceRecord>(
root,
&isolated_instance_relative_path(instance_id),
"动态隔离 Agent instance",
)?
.ok_or_else(|| format!("未找到动态隔离 Agent instance:{instance_id}"))?;
validate_isolated_instance_record(root, &record)?;
if record.instance_id != instance_id {
return Err("动态隔离 Agent instance 文件名与记录身份不一致".to_string());
}
Ok(record)
}
pub(crate) fn render_isolated_agent_task_contract(
instance: &IsolatedAgentInstanceRecord,
) -> Result<String, String> {
let contract = serde_json::to_string_pretty(&serde_json::json!({
"task": instance.task,
"acceptanceCriteria": instance.acceptance_criteria,
"expectedArtifacts": instance.expected_artifacts,
"writeScopes": instance.write_scopes,
}))
.map_err(|error| format!("序列化动态隔离子任务合同失败:{error}"))?;
let rendered = format!(
prompt_text!("goalContext.isolated_task_contract"),
contract = contract,
);
if rendered.chars().count() > AGENT_RUNTIME_TASK_MAX_CHARS {
return Err(format!(
"动态隔离子任务与持久化合同合计超过 {} 字符",
AGENT_RUNTIME_TASK_MAX_CHARS
));
}
Ok(rendered)
}
pub(crate) fn validate_isolated_agent_tool_scope_at(
root: &Path,
instance_id: &str,
tool: &str,
input: &Value,
) -> Result<(), String> {
let instance = resolve_isolated_agent_instance_at(root, instance_id)?;
let tool = tool.trim();
if tool == "project.git_commit" {
return Err(
"动态隔离子 Agent 作用域拒绝 project.git_commit:最终提交由父 Agent 统一负责"
.to_string(),
);
}
if tool == "memory.write" {
let scope = input
.get("scope")
.and_then(Value::as_str)
.unwrap_or("agent")
.trim();
if scope != "agent" {
return Err(format!(
"动态隔离子 Agent 只能写入自己的 instance 私有记忆,拒绝 scope={scope}"
));
}
let target_agent_id = ["agentId", "agent_id", "targetAgentId", "target_agent_id"]
.into_iter()
.filter_map(|key| input.get(key).and_then(Value::as_str))
.map(str::trim)
.find(|value| !value.is_empty());
if target_agent_id.is_some_and(|target| target != instance.instance_id) {
return Err("动态隔离子 Agent 只能写入自己的 instance 私有记忆".to_string());
}
}
if ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) {
return Err(format!(
"动态隔离子 Agent 默认拒绝无 writeScope 落点的工具:{tool}"
));
}
if tool == "project.patchset" {
let changes = input
.get("changes")
.and_then(Value::as_array)
.filter(|changes| !changes.is_empty())
.ok_or_else(|| "project.patchset 缺少非空 changes".to_string())?;
for change in changes {
let path = change
.get("path")
.and_then(Value::as_str)
.ok_or_else(|| "project.patchset change 缺少字符串 path".to_string())?;
let path = normalize_relative_path(path)?;
resolve_local_project_path(root, &path)?;
if !instance
.write_scopes
.iter()
.any(|scope| path_matches_write_scope(&path, scope))
{
return Err(format!(
"动态隔离子 Agent patchset 路径超出 writeScopes:{path}"
));
}
}
return Ok(());
}
if !matches!(tool, "file.write" | "file.patch" | "file.delete") {
return Ok(());
}
let path = input
.get("path")
.and_then(Value::as_str)
.ok_or_else(|| format!("{tool} 缺少字符串 path"))?;
let path = normalize_relative_path(path)?;
resolve_local_project_path(root, &path)?;
if instance
.write_scopes
.iter()
.any(|scope| path_matches_write_scope(&path, scope))
{
Ok(())
} else {
Err(format!("动态隔离子 Agent 写入路径超出 writeScopes:{path}"))
}
}
pub(crate) fn read_isolated_agent_private_memory_at(
root: &Path,
instance_id: &str,
) -> Result<String, String> {
let instance = resolve_isolated_agent_instance_at(root, instance_id)?;
let record = read_json_record::<IsolatedAgentPrivateMemoryRecord>(
root,
&isolated_private_memory_relative_path(&instance.instance_id),
"动态隔离 Agent 私有临时记忆",
)?;
let Some(record) = record else {
return Ok(String::new());
};
validate_isolated_private_memory_record(&record, &instance)?;
Ok(record.content)
}
pub(crate) fn write_isolated_agent_private_memory_at(
root: &Path,
instance_id: &str,
content: &str,
) -> Result<String, String> {
let instance = resolve_isolated_agent_instance_at(root, instance_id)?;
if content.as_bytes().len() > ISOLATED_AGENT_PRIVATE_MEMORY_MAX_BYTES {
return Err(format!(
"动态隔离 Agent 私有临时记忆超过 {} 字节上限",
ISOLATED_AGENT_PRIVATE_MEMORY_MAX_BYTES
));
}
let relative_path = isolated_private_memory_relative_path(&instance.instance_id);
let record = IsolatedAgentPrivateMemoryRecord {
schema_version: ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION.to_string(),
instance_id: instance.instance_id.clone(),
content: content.to_string(),
updated_at: unix_timestamp(),
};
validate_isolated_private_memory_record(&record, &instance)?;
write_json_record(root, &relative_path, "动态隔离 Agent 私有临时记忆", &record)?;
Ok(relative_path)
}
pub(crate) fn build_isolated_child_result_at(
root: &Path,
instance_id: &str,
task: &IsolatedAgentTerminalTask,
expected_artifacts: &[String],
verification_gate: &IsolatedAgentVerificationGateSnapshot,
evidence: &[GameCreationIsolatedAgentEvidence],
) -> Result<IsolatedAgentBuildResult, String> {
let instance = resolve_isolated_agent_instance_at(root, instance_id)?;
validate_terminal_task_identity(&instance, task)?;
if expected_artifacts != instance.expected_artifacts.as_slice() {
return Err("动态隔离子 Agent expectedArtifacts 与实例契约不一致".to_string());
}
validate_verification_gate(&instance, verification_gate)?;
let status = terminal_result_status(task)?;
let completed = status == GameCreationIsolatedAgentResultStatus::Completed;
let artifacts = collect_expected_artifacts(root, expected_artifacts, completed)?;
let evidence = sanitize_evidence(root, evidence)?;
if completed && verification_gate.requires_verification {
let expected_kind = verification_gate
.last_verification_tool
.as_deref()
.unwrap_or_default();
if !evidence.iter().any(|item| item.kind == expected_kind) {
return Err(format!(
"动态隔离子 Agent 缺少通过验证的安全 evidence:{expected_kind}"
));
}
}
let summary_source = task
.terminal_detail
.as_deref()
.or(task.error.as_deref())
.unwrap_or(match status {
GameCreationIsolatedAgentResultStatus::Completed => "动态隔离子任务已完成",
GameCreationIsolatedAgentResultStatus::Failed => "动态隔离子任务失败",
GameCreationIsolatedAgentResultStatus::Cancelled => "动态隔离子任务已取消",
GameCreationIsolatedAgentResultStatus::BudgetExhausted => "动态隔离子任务预算已耗尽",
});
let summary = sanitize_persisted_text(root, summary_source, 2_000);
let error = matches!(
status,
GameCreationIsolatedAgentResultStatus::Failed
| GameCreationIsolatedAgentResultStatus::BudgetExhausted
)
.then(|| {
sanitize_persisted_text(
root,
task.error
.as_deref()
.or(task.terminal_detail.as_deref())
.unwrap_or("动态隔离子任务未提供错误详情"),
2_000,
)
});
let verified_revision = (verification_gate.last_verification_status.as_deref()
== Some("passed"))
.then_some(verification_gate.verified_revision)
.flatten();
let result = GameCreationIsolatedAgentChildResult {
delegation_id: instance.delegation_id,
instance_id: instance.instance_id,
template_agent_id: instance.template_agent_id,
run_id: instance.run_id,
status,
summary,
artifacts,
evidence,
verified_revision,
error,
};
validate_game_creation_isolated_agent_child_result(&result)
.map_err(|error| error.to_string())?;
let join_dispatch = record_isolated_child_result_at(root, &result)?;
Ok(IsolatedAgentBuildResult {
result,
join_dispatch,
})
}
pub(crate) fn build_isolated_child_result_with_failure_fallback_at(
root: &Path,
instance_id: &str,
task: &IsolatedAgentTerminalTask,
expected_artifacts: &[String],
verification_gate: &IsolatedAgentVerificationGateSnapshot,
evidence: &[GameCreationIsolatedAgentEvidence],
) -> Result<IsolatedAgentBuildResult, String> {
match build_isolated_child_result_at(
root,
instance_id,
task,
expected_artifacts,
verification_gate,
evidence,
) {
Ok(result) => Ok(result),
Err(error)
if terminal_result_status(task)?
== GameCreationIsolatedAgentResultStatus::Completed =>
{
let failure = format!("动态隔离子 Agent 结果发布失败:{error}");
let failed_task = IsolatedAgentTerminalTask {
agent_id: task.agent_id.clone(),
session_id: task.session_id.clone(),
run_id: task.run_id.clone(),
delegation_id: task.delegation_id.clone(),
status: "failed".to_string(),
phase: "failed".to_string(),
terminal_detail: Some(failure.clone()),
error: Some(failure),
};
build_isolated_child_result_at(
root,
instance_id,
&failed_task,
expected_artifacts,
verification_gate,
evidence,
)
}
Err(error) => Err(error),
}
}
pub(crate) fn record_isolated_child_result_at(
root: &Path,
result: &GameCreationIsolatedAgentChildResult,
) -> Result<Option<JoinDispatch>, String> {
validate_game_creation_isolated_agent_child_result(result)
.map_err(|error| error.to_string())?;
let instance = resolve_isolated_agent_instance_at(root, &result.instance_id)?;
if result.delegation_id != instance.delegation_id
|| result.template_agent_id != instance.template_agent_id
|| result.run_id != instance.run_id
{
return Err("动态隔离子 Agent result 身份与实例不一致".to_string());
}
let path = isolated_result_relative_path(&instance.instance_id);
let existing =
read_json_record::<IsolatedAgentResultRecord>(root, &path, "动态隔离 Agent result")?;
let record = IsolatedAgentResultRecord {
schema_version: ISOLATED_AGENT_RESULT_SCHEMA_VERSION.to_string(),
delegation_group_id: instance.delegation_group_id.clone(),
child_index: instance.child_index,
result: result.clone(),
recorded_at: existing
.as_ref()
.map(|record| record.recorded_at)
.unwrap_or_else(unix_timestamp),
};
validate_isolated_result_record(root, &record)?;
match existing {
Some(existing) if existing != record => {
return Err(format!(
"动态隔离 Agent terminal result 已存在且内容冲突:{}",
result.instance_id
));
}
Some(_) => {}
None => write_json_record(root, &path, "动态隔离 Agent result", &record)?,
}
build_join_dispatch_if_ready_at(root, &instance.delegation_group_id)
}
pub(crate) fn reconcile_all_isolated_groups_at(root: &Path) -> Result<Vec<JoinDispatch>, String> {
let groups = list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?;
let mut dispatches = Vec::new();
for group in groups {
if let Some(dispatch) = build_join_dispatch_if_ready_at(root, &group.delegation_group_id)? {
dispatches.push(dispatch);
}
}
Ok(dispatches)
}
pub(crate) fn isolated_join_completion_barrier_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<Option<String>, String> {
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(parent_run_id, "parentRunId", 160)?;
let groups = list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?;
let claims = list_isolated_join_claims_at(root)?;
let journaled_claimed_groups = claims
.iter()
.filter(|claim| {
claim.parent_agent_id == parent_agent_id && claim.parent_run_id == parent_run_id
})
.flat_map(|claim| {
claim
.joins
.iter()
.map(|join| (claim.action_id.clone(), join.delegation_group_id.clone()))
})
.collect::<BTreeSet<_>>();
let mut waiting_groups = 0usize;
let mut ready_unclaimed_groups = 0usize;
let mut unjournaled_claimed_groups = 0usize;
for group in groups.into_iter().filter(|group| {
group.parent_agent_id == parent_agent_id && group.parent_run_id == parent_run_id
}) {
let Some(join) = build_join_dispatch_if_ready_at(root, &group.delegation_group_id)? else {
waiting_groups = waiting_groups.saturating_add(1);
continue;
};
match read_isolated_join_delivery_at(root, &join)? {
Some(delivery)
if delivery.status == IsolatedAgentJoinDeliveryStatus::ClaimedByParent =>
{
let claimed_by_action_id = delivery
.claimed_by_action_id
.as_deref()
.ok_or_else(|| "动态隔离 Agent 已认领 delivery 缺少 actionId".to_string())?;
if !journaled_claimed_groups.contains(&(
claimed_by_action_id.to_string(),
join.delegation_group_id.clone(),
)) {
unjournaled_claimed_groups = unjournaled_claimed_groups.saturating_add(1);
}
}
_ => {
ready_unclaimed_groups = ready_unclaimed_groups.saturating_add(1);
}
}
}
let unobserved_claims = claims
.iter()
.filter(|claim| {
claim.parent_agent_id == parent_agent_id
&& claim.parent_run_id == parent_run_id
&& claim.status != IsolatedAgentJoinClaimStatus::Observed
})
.count();
if waiting_groups == 0
&& ready_unclaimed_groups == 0
&& unjournaled_claimed_groups == 0
&& unobserved_claims == 0
{
return Ok(None);
}
Ok(Some(format!(
"waitingGroups={waiting_groups} · readyUnclaimedGroups={ready_unclaimed_groups} · unjournaledClaimedGroups={unjournaled_claimed_groups} · unobservedJoinClaims={unobserved_claims} · 必须调用 agent.run_status 取得并持久观察 all-join 后再继续"
)))
}
pub(crate) fn read_isolated_join_claim_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<Option<IsolatedAgentJoinClaimRecord>, String> {
let relative_path =
isolated_join_claim_relative_path(parent_agent_id, parent_run_id, action_id);
let claim = read_agent_runtime_json_sidecar_with_max_bytes::<IsolatedAgentJoinClaimRecord>(
root,
&relative_path,
"动态隔离 Agent join claim",
ISOLATED_AGENT_RECORD_MAX_BYTES,
)?;
if let Some(claim) = &claim {
validate_isolated_join_claim_record(root, claim)?;
if claim.parent_agent_id != parent_agent_id
|| claim.parent_run_id != parent_run_id
|| claim.action_id != action_id
{
return Err("动态隔离 Agent join claim 文件与请求身份不一致".to_string());
}
}
Ok(claim)
}
pub(crate) fn write_isolated_join_claim_at(
root: &Path,
claim: &IsolatedAgentJoinClaimRecord,
) -> Result<(), String> {
validate_isolated_join_claim_record(root, claim)?;
write_agent_runtime_json_sidecar_with_max_bytes(
root,
&isolated_join_claim_relative_path(
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
),
"动态隔离 Agent join claim",
claim,
ISOLATED_AGENT_RECORD_MAX_BYTES,
)
}
pub(crate) fn list_isolated_join_claims_at(
root: &Path,
) -> Result<Vec<IsolatedAgentJoinClaimRecord>, String> {
let dir = resolve_local_project_path(root, ISOLATED_AGENT_JOIN_CLAIM_DIR)?;
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(format!(
"读取动态隔离 Agent join claim 目录失败:{}: {error}",
dir.display()
))
}
};
let mut stems = BTreeSet::new();
for entry in entries {
let entry =
entry.map_err(|error| format!("读取动态隔离 Agent join claim 条目失败:{error}"))?;
let file_name = entry
.file_name()
.into_string()
.map_err(|_| "动态隔离 Agent join claim 文件名不是 UTF-8".to_string())?;
let stem = if let Some(stem) = file_name.strip_suffix(".json") {
Some(stem)
} else {
file_name
.strip_prefix('.')
.and_then(|value| value.strip_suffix(".json.previous"))
};
let Some(stem) = stem.filter(|value| value.starts_with("claim-")) else {
continue;
};
if stem.len() != "claim-".len() + 64
|| !stem["claim-".len()..]
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
{
return Err("动态隔离 Agent join claim 文件名无效".to_string());
}
stems.insert(stem.to_string());
}
let mut claims = Vec::with_capacity(stems.len());
for stem in stems {
let relative_path = format!("{ISOLATED_AGENT_JOIN_CLAIM_DIR}/{stem}.json");
let claim = read_agent_runtime_json_sidecar_with_max_bytes::<IsolatedAgentJoinClaimRecord>(
root,
&relative_path,
"动态隔离 Agent join claim",
ISOLATED_AGENT_RECORD_MAX_BYTES,
)?
.ok_or_else(|| format!("动态隔离 Agent join claim 在枚举后消失:{stem}"))?;
validate_isolated_join_claim_record(root, &claim)?;
if isolated_join_claim_relative_path(
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
) != relative_path
{
return Err("动态隔离 Agent join claim 文件名与记录身份不一致".to_string());
}
claims.push(claim);
}
let mut owner_by_group = BTreeMap::<String, (String, String, String)>::new();
for claim in &claims {
for join in &claim.joins {
let owner = (
claim.parent_agent_id.clone(),
claim.parent_run_id.clone(),
claim.action_id.clone(),
);
if let Some(existing) = owner_by_group.insert(join.delegation_group_id.clone(), owner) {
return Err(format!(
"动态隔离 Agent join group 同时归属多个 claim journal:{} / {}:{}:{} / {}:{}:{}",
join.delegation_group_id,
existing.0,
existing.1,
existing.2,
claim.parent_agent_id,
claim.parent_run_id,
claim.action_id
));
}
}
}
Ok(claims)
}
pub(crate) fn isolated_join_claim_lock_id(
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> String {
isolated_join_claim_stem(parent_agent_id, parent_run_id, action_id)
}
pub(crate) fn read_isolated_join_delivery_at(
root: &Path,
join: &JoinDispatch,
) -> Result<Option<IsolatedAgentJoinDeliveryRecord>, String> {
let record = read_json_record::<IsolatedAgentJoinDeliveryRecord>(
root,
&isolated_join_delivery_relative_path(&join.delegation_group_id),
"动态隔离 Agent join delivery",
)?;
if let Some(record) = &record {
validate_isolated_join_delivery_record(root, record, join)?;
}
Ok(record)
}
pub(crate) fn write_isolated_join_delivery_at(
root: &Path,
join: &JoinDispatch,
status: IsolatedAgentJoinDeliveryStatus,
queued_run_id: Option<&str>,
claimed_by_action_id: Option<&str>,
) -> Result<IsolatedAgentJoinDeliveryRecord, String> {
write_isolated_join_delivery_with_target_at(
root,
join,
status,
None,
queued_run_id,
claimed_by_action_id,
)
}
pub(crate) fn write_isolated_parent_wake_join_delivery_at(
root: &Path,
join: &JoinDispatch,
) -> Result<IsolatedAgentJoinDeliveryRecord, String> {
write_isolated_join_delivery_with_target_at(
root,
join,
IsolatedAgentJoinDeliveryStatus::Dispatched,
Some(IsolatedAgentJoinDeliveryTarget::ParentWake),
None,
None,
)
}
fn write_isolated_join_delivery_with_target_at(
root: &Path,
join: &JoinDispatch,
status: IsolatedAgentJoinDeliveryStatus,
requested_target: Option<IsolatedAgentJoinDeliveryTarget>,
queued_run_id: Option<&str>,
claimed_by_action_id: Option<&str>,
) -> Result<IsolatedAgentJoinDeliveryRecord, String> {
let existing = read_isolated_join_delivery_at(root, join)?;
if let Some(existing) = &existing {
let transition_allowed = match (existing.status, status) {
(current, next) if current == next => true,
(
IsolatedAgentJoinDeliveryStatus::Dispatched,
IsolatedAgentJoinDeliveryStatus::ClaimedByParent
| IsolatedAgentJoinDeliveryStatus::Suppressed,
) => true,
_ => false,
};
if !transition_allowed {
return Err(format!(
"动态隔离 Agent join delivery 状态不可逆:{:?} -> {:?}",
existing.status, status
));
}
if requested_target.is_some_and(|target| target != existing.delivery_target) {
return Err("动态隔离 Agent join delivery 不能更改投递目标".to_string());
}
}
let delivery_target = existing
.as_ref()
.map(|record| record.delivery_target)
.or(requested_target)
.unwrap_or_default();
let queued_run_id = queued_run_id
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| {
existing
.as_ref()
.and_then(|record| record.queued_run_id.clone())
});
if let Some(run_id) = &queued_run_id {
validate_safe_id(run_id, "queuedRunId", 160)?;
if run_id != &join.join_run_id {
return Err(format!(
"动态隔离 Agent join 必须使用稳定 runId:expected={}, actual={run_id}",
join.join_run_id
));
}
}
let claimed_by_action_id = claimed_by_action_id
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| {
existing
.as_ref()
.and_then(|record| record.claimed_by_action_id.clone())
});
if let (Some(existing_action_id), Some(next_action_id)) = (
existing
.as_ref()
.and_then(|record| record.claimed_by_action_id.as_deref()),
claimed_by_action_id.as_deref(),
) {
if existing_action_id != next_action_id {
return Err("动态隔离 Agent join 已被其他 actionId 认领".to_string());
}
}
if status == IsolatedAgentJoinDeliveryStatus::ClaimedByParent {
let action_id = claimed_by_action_id
.as_deref()
.ok_or_else(|| "动态隔离 Agent join 认领缺少 actionId".to_string())?;
validate_safe_id(action_id, "claimedByActionId", 256)?;
} else if claimed_by_action_id.is_some() {
return Err("未认领的动态隔离 Agent join 不能保存 claimedByActionId".to_string());
}
if let Some(existing) = &existing {
if existing.status == status
&& existing.delivery_target == delivery_target
&& existing.queued_run_id == queued_run_id
&& existing.claimed_by_action_id == claimed_by_action_id
{
return Ok(existing.clone());
}
}
let record = IsolatedAgentJoinDeliveryRecord {
schema_version: ISOLATED_AGENT_JOIN_DELIVERY_SCHEMA_VERSION.to_string(),
parent_agent_id: join.parent_agent_id.clone(),
parent_run_id: join.parent_run_id.clone(),
delegation_group_id: join.delegation_group_id.clone(),
join_run_id: join.join_run_id.clone(),
status,
delivery_target,
queued_run_id,
claimed_by_action_id,
updated_at: unix_timestamp(),
};
validate_isolated_join_delivery_record(root, &record, join)?;
write_json_record(
root,
&isolated_join_delivery_relative_path(&join.delegation_group_id),
"动态隔离 Agent join delivery",
&record,
)?;
Ok(record)
}
pub(crate) fn list_non_terminal_isolated_children_for_parent_cancel_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<Vec<IsolatedAgentCancelTarget>, String> {
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(parent_run_id, "parentRunId", 160)?;
let groups = list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?;
let mut targets = Vec::new();
for group in groups.into_iter().filter(|group| {
group.parent_agent_id == parent_agent_id && group.parent_run_id == parent_run_id
}) {
for instance_id in group.instance_ids {
let instance = resolve_isolated_agent_instance_at(root, &instance_id)?;
if read_isolated_result_at(root, &instance)?.is_none() {
targets.push(IsolatedAgentCancelTarget {
delegation_group_id: instance.delegation_group_id,
delegation_id: instance.delegation_id,
instance_id: instance.instance_id,
session_id: instance.session_id,
run_id: instance.run_id,
});
}
}
}
targets.sort_by(|left, right| left.instance_id.cmp(&right.instance_id));
Ok(targets)
}
fn build_join_dispatch_if_ready_at(
root: &Path,
delegation_group_id: &str,
) -> Result<Option<JoinDispatch>, String> {
let group = read_json_record::<IsolatedAgentGroupRecord>(
root,
&isolated_group_relative_path(delegation_group_id),
"动态隔离 Agent group",
)?
.ok_or_else(|| format!("未找到动态隔离 Agent group:{delegation_group_id}"))?;
validate_isolated_group_record(root, &group)?;
let mut results = Vec::with_capacity(group.instance_ids.len());
for instance_id in &group.instance_ids {
let instance = resolve_isolated_agent_instance_at(root, instance_id)?;
let Some(record) = read_isolated_result_at(root, &instance)? else {
return Ok(None);
};
results.push(record.result);
}
let derived = derive_game_creation_isolated_agent_group_at_depth(
&group.parent_action_id,
&group.request,
group.depth.saturating_sub(1),
)
.map_err(|error| error.to_string())?;
let joined = join_game_creation_isolated_agent_results(&derived, results)
.map_err(|error| error.to_string())?;
let mut prompt = serde_json::json!({
"schemaVersion": ISOLATED_AGENT_JOIN_PROMPT_SCHEMA_VERSION,
"kind": "agent-isolated-join",
"delegationGroupId": joined.delegation_group_id,
"joinMode": joined.join_mode,
"results": joined.results,
});
sanitize_json_value(root, &mut prompt);
let prompt = serde_json::to_string(&prompt)
.map_err(|error| format!("序列化动态隔离 Agent join prompt 失败:{error}"))?;
serde_json::from_str::<Value>(&prompt)
.map_err(|error| format!("动态隔离 Agent join prompt 清洗后不是 JSON:{error}"))?;
Ok(Some(JoinDispatch {
parent_agent_id: group.parent_agent_id,
parent_session_id: group.parent_session_id,
parent_run_id: group.parent_run_id,
parent_action_id: group.parent_action_id,
delegation_group_id: group.delegation_group_id,
join_run_id: group.join_run_id,
source: "agent-isolated-join".to_string(),
prompt,
}))
}
fn validate_isolated_group_record(
root: &Path,
record: &IsolatedAgentGroupRecord,
) -> Result<(), String> {
if record.schema_version != ISOLATED_AGENT_GROUP_SCHEMA_VERSION {
return Err(format!(
"不支持的动态隔离 Agent group schema:{}",
record.schema_version
));
}
validate_safe_id(&record.parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(&record.parent_session_id, "parentSessionId", 160)?;
validate_safe_id(&record.parent_run_id, "parentRunId", 160)?;
validate_safe_id(&record.parent_action_id, "parentActionId", 256)?;
let parent_depth = record
.depth
.checked_sub(1)
.ok_or_else(|| "动态隔离 Agent group.depth 不能为 0".to_string())?;
let sanitized = sanitize_spawn_request(root, &record.request)?;
if sanitized != record.request {
return Err("动态隔离 Agent group 含未清洗文本".to_string());
}
let derived = derive_game_creation_isolated_agent_group_at_depth(
&record.parent_action_id,
&record.request,
parent_depth,
)
.map_err(|error| error.to_string())?;
let instance_ids = derived
.children
.iter()
.map(|child| child.instance_id.clone())
.collect::<Vec<_>>();
if record.delegation_group_id != derived.delegation_group_id
|| record.join_run_id != derived.join_run_id
|| record.depth != derived.depth
|| record.join_mode != derived.join_mode
|| record.instance_ids != instance_ids
{
return Err("动态隔离 Agent group 派生身份不一致".to_string());
}
Ok(())
}
fn validate_isolated_join_delivery_record(
root: &Path,
record: &IsolatedAgentJoinDeliveryRecord,
join: &JoinDispatch,
) -> Result<(), String> {
if record.schema_version != ISOLATED_AGENT_JOIN_DELIVERY_SCHEMA_VERSION {
return Err(format!(
"不支持的动态隔离 Agent join delivery schema:{}",
record.schema_version
));
}
validate_safe_id(&record.parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(&record.parent_run_id, "parentRunId", 160)?;
validate_safe_id(&record.delegation_group_id, "delegationGroupId", 160)?;
validate_safe_id(&record.join_run_id, "joinRunId", 160)?;
let group = read_json_record::<IsolatedAgentGroupRecord>(
root,
&isolated_group_relative_path(&record.delegation_group_id),
"动态隔离 Agent group",
)?
.ok_or_else(|| "动态隔离 Agent join delivery 找不到 group".to_string())?;
validate_isolated_group_record(root, &group)?;
if record.parent_agent_id != group.parent_agent_id
|| record.parent_run_id != group.parent_run_id
|| record.delegation_group_id != group.delegation_group_id
|| record.join_run_id != group.join_run_id
|| record.parent_agent_id != join.parent_agent_id
|| record.parent_run_id != join.parent_run_id
|| record.delegation_group_id != join.delegation_group_id
|| record.join_run_id != join.join_run_id
{
return Err("动态隔离 Agent join delivery 身份不一致".to_string());
}
match (record.delivery_target, record.queued_run_id.as_deref()) {
(IsolatedAgentJoinDeliveryTarget::ParentWake, Some(_)) => {
return Err("动态隔离 Agent parent-wake delivery 不能含 queuedRunId".to_string());
}
(IsolatedAgentJoinDeliveryTarget::Continuation, None)
if record.status == IsolatedAgentJoinDeliveryStatus::Dispatched =>
{
return Err("动态隔离 Agent continuation delivery 缺少 queuedRunId".to_string());
}
(_, Some(queued_run_id)) => {
validate_safe_id(queued_run_id, "queuedRunId", 160)?;
if queued_run_id != record.join_run_id {
return Err("动态隔离 Agent join delivery 使用了非稳定 queuedRunId".to_string());
}
}
(_, None) => {}
}
match (record.status, record.claimed_by_action_id.as_deref()) {
(IsolatedAgentJoinDeliveryStatus::ClaimedByParent, Some(action_id)) => {
validate_safe_id(action_id, "claimedByActionId", 256)?;
}
(IsolatedAgentJoinDeliveryStatus::ClaimedByParent, None) => {
return Err("动态隔离 Agent join delivery 缺少 claimedByActionId".to_string());
}
(_, Some(_)) => {
return Err("未认领的动态隔离 Agent join delivery 含 claimedByActionId".to_string());
}
(_, None) => {}
}
Ok(())
}
fn validate_isolated_instance_record(
root: &Path,
record: &IsolatedAgentInstanceRecord,
) -> Result<(), String> {
if record.schema_version != ISOLATED_AGENT_INSTANCE_SCHEMA_VERSION {
return Err(format!(
"不支持的动态隔离 Agent instance schema:{}",
record.schema_version
));
}
validate_safe_id(&record.parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(&record.parent_session_id, "parentSessionId", 160)?;
validate_safe_id(&record.parent_run_id, "parentRunId", 160)?;
validate_safe_id(&record.parent_action_id, "parentActionId", 256)?;
validate_safe_id(&record.instance_id, "instanceId", 96)?;
validate_safe_id(&record.session_id, "sessionId", 160)?;
validate_safe_id(&record.run_id, "runId", 160)?;
let identity = derive_game_creation_isolated_agent_identity(
&record.parent_action_id,
record.child_index,
&record.template_agent_id,
)
.map_err(|error| error.to_string())?;
if record.delegation_group_id != identity.delegation_group_id
|| record.delegation_id != identity.delegation_id
|| record.instance_id != identity.instance_id
|| record.session_id != isolated_child_session_id(&identity.instance_id)
|| record.run_id != isolated_child_run_id(&identity.delegation_id)
|| record.depth == 0
{
return Err("动态隔离 Agent instance 派生身份不一致".to_string());
}
let child = GameCreationIsolatedAgentChildSpec {
template_agent_id: record.template_agent_id.clone(),
task: record.task.clone(),
acceptance_criteria: record.acceptance_criteria.clone(),
expected_artifacts: record.expected_artifacts.clone(),
write_scopes: record.write_scopes.clone(),
};
let request = GameCreationIsolatedAgentSpawnRequest {
children: vec![child],
join_mode: GameCreationIsolatedAgentJoinMode::All,
};
validate_game_creation_isolated_agent_spawn_request_at_depth(
&request,
record.depth.saturating_sub(1),
)
.map_err(|error| error.to_string())?;
if sanitize_spawn_request(root, &request)? != request {
return Err("动态隔离 Agent instance 含未清洗文本".to_string());
}
Ok(())
}
fn validate_isolated_result_record(
root: &Path,
record: &IsolatedAgentResultRecord,
) -> Result<(), String> {
if record.schema_version != ISOLATED_AGENT_RESULT_SCHEMA_VERSION {
return Err(format!(
"不支持的动态隔离 Agent result schema:{}",
record.schema_version
));
}
validate_game_creation_isolated_agent_child_result(&record.result)
.map_err(|error| error.to_string())?;
let instance = resolve_isolated_agent_instance_at(root, &record.result.instance_id)?;
if record.delegation_group_id != instance.delegation_group_id
|| record.child_index != instance.child_index
|| record.result.delegation_id != instance.delegation_id
|| record.result.template_agent_id != instance.template_agent_id
|| record.result.run_id != instance.run_id
{
return Err("动态隔离 Agent result 记录身份不一致".to_string());
}
Ok(())
}
fn validate_isolated_private_memory_record(
record: &IsolatedAgentPrivateMemoryRecord,
instance: &IsolatedAgentInstanceRecord,
) -> Result<(), String> {
if record.schema_version != ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION {
return Err(format!(
"不支持的动态隔离 Agent 私有临时记忆 schema:{}",
record.schema_version
));
}
if record.instance_id != instance.instance_id {
return Err("动态隔离 Agent 私有临时记忆身份与 instance 不一致".to_string());
}
if record.content.as_bytes().len() > ISOLATED_AGENT_PRIVATE_MEMORY_MAX_BYTES {
return Err(format!(
"动态隔离 Agent 私有临时记忆超过 {} 字节上限",
ISOLATED_AGENT_PRIVATE_MEMORY_MAX_BYTES
));
}
Ok(())
}
fn validate_terminal_task_identity(
instance: &IsolatedAgentInstanceRecord,
task: &IsolatedAgentTerminalTask,
) -> Result<(), String> {
if task.agent_id != instance.instance_id
|| task.session_id != instance.session_id
|| task.run_id != instance.run_id
|| task.delegation_id != instance.delegation_id
{
return Err("动态隔离子 Agent 终态 task 身份不一致".to_string());
}
terminal_result_status(task).map(|_| ())
}
fn terminal_result_status(
task: &IsolatedAgentTerminalTask,
) -> Result<GameCreationIsolatedAgentResultStatus, String> {
if task.phase == "budget-exhausted" {
return Ok(GameCreationIsolatedAgentResultStatus::BudgetExhausted);
}
match task.status.as_str() {
"completed" => Ok(GameCreationIsolatedAgentResultStatus::Completed),
"failed" => Ok(GameCreationIsolatedAgentResultStatus::Failed),
"cancelled" => Ok(GameCreationIsolatedAgentResultStatus::Cancelled),
_ => Err(format!(
"动态隔离子 Agent task 尚未终态:{} / {}",
task.status, task.phase
)),
}
}
fn validate_verification_gate(
instance: &IsolatedAgentInstanceRecord,
gate: &IsolatedAgentVerificationGateSnapshot,
) -> Result<(), String> {
if gate.agent_id != instance.instance_id || gate.run_id != instance.run_id {
return Err("动态隔离子 Agent verification gate 身份不一致".to_string());
}
if gate.requires_verification {
let mutation = gate
.mutation_revision
.filter(|revision| *revision > 0)
.ok_or_else(|| {
"动态隔离子 Agent verification gate 缺少 mutationRevision".to_string()
})?;
let verified = gate
.verified_revision
.filter(|revision| *revision >= mutation)
.ok_or_else(|| "动态隔离子 Agent 尚未通过当前 revision 验证".to_string())?;
if verified == 0 || gate.last_verification_status.as_deref() != Some("passed") {
return Err("动态隔离子 Agent verification gate 未通过".to_string());
}
if !matches!(
gate.last_verification_tool.as_deref(),
Some("project.verify" | "game.static_smoke")
) {
return Err("动态隔离子 Agent verification tool 无效".to_string());
}
}
Ok(())
}
fn sanitize_spawn_request(
root: &Path,
request: &GameCreationIsolatedAgentSpawnRequest,
) -> Result<GameCreationIsolatedAgentSpawnRequest, String> {
let mut request = request.clone();
for child in &mut request.children {
child.task = sanitize_persisted_text(root, &child.task, 4_000);
for criterion in &mut child.acceptance_criteria {
*criterion = sanitize_persisted_text(root, criterion, 500);
}
for path in child
.expected_artifacts
.iter()
.chain(child.write_scopes.iter())
{
if is_private_or_sensitive_path(path.trim_end_matches("/**")) {
return Err(format!("动态隔离 Agent 不允许私有或敏感路径:{path}"));
}
}
}
Ok(request)
}
fn sanitize_evidence(
root: &Path,
evidence: &[GameCreationIsolatedAgentEvidence],
) -> Result<Vec<GameCreationIsolatedAgentEvidence>, String> {
let mut sanitized = Vec::with_capacity(evidence.len());
for item in evidence {
let mut item = item.clone();
item.summary = sanitize_persisted_text(root, &item.summary, 1_000);
if let Some(path) = &item.path {
item.path = Some(normalize_relative_path(path)?);
}
sanitized.push(item);
}
Ok(sanitized)
}
fn collect_expected_artifacts(
root: &Path,
patterns: &[String],
require_each: bool,
) -> Result<Vec<GameCreationIsolatedAgentArtifact>, String> {
let mut artifacts = BTreeMap::<String, String>::new();
for pattern in patterns {
let matches = collect_artifact_matches(root, pattern)?;
if require_each && matches.is_empty() {
return Err(format!(
"动态隔离子 Agent 缺少 expected artifact:{pattern}"
));
}
for path in matches {
let absolute = resolve_local_project_path(root, &path)?;
artifacts.insert(path, sha256_file(&absolute)?);
if artifacts.len() > ISOLATED_AGENT_MAX_RESULT_ARTIFACTS {
return Err(format!(
"动态隔离子 Agent artifacts 超过 {ISOLATED_AGENT_MAX_RESULT_ARTIFACTS} 项"
));
}
}
}
Ok(artifacts
.into_iter()
.map(|(path, sha256)| GameCreationIsolatedAgentArtifact { path, sha256 })
.collect())
}
fn collect_artifact_matches(root: &Path, pattern: &str) -> Result<Vec<String>, String> {
let prefix = pattern
.split('/')
.take_while(|part| !part.contains('*'))
.collect::<Vec<_>>()
.join("/");
let start = if prefix.is_empty() {
root.to_path_buf()
} else {
resolve_local_project_path(root, &prefix)?
};
if !start.exists() {
return Ok(Vec::new());
}
let mut candidates = Vec::new();
let mut stack = vec![start];
let mut scanned = 0usize;
while let Some(path) = stack.pop() {
let metadata = fs::symlink_metadata(&path)
.map_err(|error| format!("读取 expected artifact 失败:{}: {error}", path.display()))?;
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_file() {
let relative = path
.strip_prefix(root)
.map_err(|_| "expected artifact 不在项目目录内".to_string())?
.components()
.map(|part| part.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
if !is_private_or_sensitive_path(&relative) && glob_matches_path(pattern, &relative) {
candidates.push(relative);
}
continue;
}
if !metadata.is_dir() {
continue;
}
for entry in fs::read_dir(&path).map_err(|error| {
format!(
"读取 expected artifact 目录失败:{}: {error}",
path.display()
)
})? {
let entry = entry.map_err(|error| {
format!(
"读取 expected artifact 条目失败:{}: {error}",
path.display()
)
})?;
scanned += 1;
if scanned > ISOLATED_AGENT_MAX_SCANNED_ARTIFACT_ENTRIES {
return Err("动态隔离子 Agent expected artifact 扫描超过安全上限".to_string());
}
let candidate = entry.path();
let relative = candidate
.strip_prefix(root)
.unwrap_or(&candidate)
.to_string_lossy()
.replace('\\', "/");
if !is_private_or_sensitive_path(&relative) {
stack.push(candidate);
}
}
}
candidates.sort();
candidates.dedup();
Ok(candidates)
}
fn glob_matches_path(pattern: &str, path: &str) -> bool {
fn segment_matches(pattern: &str, value: &str) -> bool {
let pattern = pattern.as_bytes();
let value = value.as_bytes();
let (mut pi, mut vi, mut star, mut matched) = (0, 0, None, 0);
while vi < value.len() {
if pi < pattern.len() && pattern[pi] == value[vi] {
pi += 1;
vi += 1;
} else if pi < pattern.len() && pattern[pi] == b'*' {
star = Some(pi);
matched = vi;
pi += 1;
} else if let Some(star_index) = star {
matched += 1;
vi = matched;
pi = star_index + 1;
} else {
return false;
}
}
while pi < pattern.len() && pattern[pi] == b'*' {
pi += 1;
}
pi == pattern.len()
}
fn recurse(pattern: &[&str], path: &[&str]) -> bool {
match pattern.split_first() {
None => path.is_empty(),
Some((head, rest)) if *head == "**" => {
recurse(rest, path) || (!path.is_empty() && recurse(pattern, &path[1..]))
}
Some((head, rest)) => {
!path.is_empty() && segment_matches(head, path[0]) && recurse(rest, &path[1..])
}
}
}
recurse(
&pattern.split('/').collect::<Vec<_>>(),
&path.split('/').collect::<Vec<_>>(),
)
}
fn read_isolated_result_at(
root: &Path,
instance: &IsolatedAgentInstanceRecord,
) -> Result<Option<IsolatedAgentResultRecord>, String> {
let record = read_json_record::<IsolatedAgentResultRecord>(
root,
&isolated_result_relative_path(&instance.instance_id),
"动态隔离 Agent result",
)?;
if let Some(record) = &record {
validate_isolated_result_record(root, record)?;
}
Ok(record)
}
fn path_matches_write_scope(path: &str, scope: &str) -> bool {
let prefix = scope.strip_suffix("/**").unwrap_or(scope);
path == prefix || path.starts_with(&format!("{prefix}/"))
}
fn isolated_child_session_id(instance_id: &str) -> String {
format!("isolated-session-{instance_id}")
}
fn isolated_child_run_id(delegation_id: &str) -> String {
format!(
"agent-isolated-{}",
delegation_id.chars().take(24).collect::<String>()
)
}
fn isolated_instance_relative_path(instance_id: &str) -> String {
format!("{ISOLATED_AGENT_INSTANCE_DIR}/{instance_id}.json")
}
fn isolated_group_relative_path(group_id: &str) -> String {
format!("{ISOLATED_AGENT_GROUP_DIR}/{group_id}.json")
}
fn isolated_result_relative_path(instance_id: &str) -> String {
format!("{ISOLATED_AGENT_RESULT_DIR}/{instance_id}.json")
}
fn isolated_join_delivery_relative_path(group_id: &str) -> String {
format!("{ISOLATED_AGENT_JOIN_DELIVERY_DIR}/{group_id}.json")
}
fn isolated_join_claim_relative_path(
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> String {
format!(
"{ISOLATED_AGENT_JOIN_CLAIM_DIR}/{}.json",
isolated_join_claim_stem(parent_agent_id, parent_run_id, action_id)
)
}
fn isolated_join_claim_stem(parent_agent_id: &str, parent_run_id: &str, action_id: &str) -> String {
let identity = format!("{parent_agent_id}\n{parent_run_id}\n{action_id}");
let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes()));
format!("claim-{fingerprint}")
}
fn isolated_private_memory_relative_path(instance_id: &str) -> String {
format!("{ISOLATED_AGENT_PRIVATE_MEMORY_DIR}/{instance_id}.json")
}
fn validate_safe_id(value: &str, label: &str, max_chars: usize) -> Result<(), String> {
if value.is_empty() || value.trim() != value || value.chars().count() > max_chars {
return Err(format!("动态隔离 Agent {label} 非法"));
}
if value.chars().any(|character| {
!(character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.'))
}) {
return Err(format!(
"动态隔离 Agent {label} 只能包含 ASCII 字母、数字、点、短横线和下划线"
));
}
Ok(())
}
fn validate_isolated_join_claim_record(
root: &Path,
claim: &IsolatedAgentJoinClaimRecord,
) -> Result<(), String> {
if claim.schema_version != ISOLATED_AGENT_JOIN_CLAIM_SCHEMA_VERSION {
return Err("动态隔离 Agent join claim schemaVersion 不受支持".to_string());
}
validate_safe_id(&claim.parent_agent_id, "parentAgentId", 96)?;
validate_safe_id(&claim.parent_run_id, "parentRunId", 160)?;
validate_safe_id(&claim.action_id, "actionId", 256)?;
if claim.joins.is_empty() || claim.joins.len() > 16 {
return Err("动态隔离 Agent join claim 数量无效".to_string());
}
let mut previous_group_id: Option<&str> = None;
for join in &claim.joins {
if join.parent_agent_id != claim.parent_agent_id
|| join.parent_run_id != claim.parent_run_id
|| join.source != "agent-isolated-join"
{
return Err("动态隔离 Agent join claim 与父 run 身份不一致".to_string());
}
validate_safe_id(&join.parent_agent_id, "join.parentAgentId", 96)?;
validate_safe_id(&join.parent_session_id, "join.parentSessionId", 160)?;
validate_safe_id(&join.parent_run_id, "join.parentRunId", 160)?;
validate_safe_id(&join.parent_action_id, "join.parentActionId", 256)?;
validate_safe_id(&join.delegation_group_id, "join.delegationGroupId", 160)?;
validate_safe_id(&join.join_run_id, "join.joinRunId", 160)?;
if previous_group_id.is_some_and(|previous| previous >= join.delegation_group_id.as_str()) {
return Err(
"动态隔离 Agent join claim 必须按 delegationGroupId 严格排序且不能重复".to_string(),
);
}
let current = build_join_dispatch_if_ready_at(root, &join.delegation_group_id)?
.ok_or_else(|| "动态隔离 Agent join claim 对应 group 尚未 ready".to_string())?;
if current != *join {
return Err("动态隔离 Agent join claim 与当前 durable join 结果冲突".to_string());
}
previous_group_id = Some(&join.delegation_group_id);
}
Ok(())
}
fn is_private_or_sensitive_path(path: &str) -> bool {
let path = path.trim_start_matches("./").to_ascii_lowercase();
path == ".agent"
|| path.starts_with(".agent/")
|| path == ".git"
|| path.starts_with(".git/")
|| path == "node_modules"
|| path.starts_with("node_modules/")
|| path == ".env"
|| path.starts_with(".env.")
|| path.ends_with(".pem")
|| path.ends_with(".key")
|| path.contains("credentials")
|| path.contains("game-creator.config")
}
fn sanitize_persisted_text(root: &Path, value: &str, max_chars: usize) -> String {
let mut value = sanitize_prompt_context(value);
let root_text = root.to_string_lossy();
if !root_text.is_empty() {
value = value.replace(root_text.as_ref(), "$PROJECT_ROOT");
}
if let Ok(canonical) = root.canonicalize() {
let canonical = canonical.to_string_lossy();
if !canonical.is_empty() {
value = value.replace(canonical.as_ref(), "$PROJECT_ROOT");
}
}
for token in value
.split_whitespace()
.map(|token| {
token.trim_matches(|character: char| {
matches!(
character,
'"' | '\'' | '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';'
)
})
})
.filter(|token| {
Path::new(token).is_absolute()
|| (token.len() > 2
&& token.as_bytes()[1] == b':'
&& token.as_bytes()[0].is_ascii_alphabetic())
})
.map(str::to_string)
.collect::<BTreeSet<_>>()
{
value = value.replace(&token, "[redacted-absolute-path]");
}
let trimmed = value.trim();
let mut output = trimmed.chars().take(max_chars).collect::<String>();
if output.is_empty() {
output = "[redacted sensitive context]".to_string();
}
output
}
fn sanitize_json_value(root: &Path, value: &mut Value) {
match value {
Value::String(text) => *text = sanitize_persisted_text(root, text, 4_000),
Value::Array(items) => {
for item in items {
sanitize_json_value(root, item);
}
}
Value::Object(map) => {
for (key, value) in map.iter_mut() {
let key = key.to_ascii_lowercase();
if key.contains("token")
|| key.contains("secret")
|| key.contains("password")
|| key.contains("cookie")
|| key.contains("authorization")
|| key.contains("apikey")
|| key.contains("api_key")
{
*value = Value::String("[redacted sensitive context]".to_string());
} else {
sanitize_json_value(root, value);
}
}
}
_ => {}
}
}
fn sha256_file(path: &Path) -> Result<String, String> {
let mut file = File::open(path)
.map_err(|error| format!("读取 artifact 失败:{}: {error}", path.display()))?;
let mut digest = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let count = file
.read(&mut buffer)
.map_err(|error| format!("读取 artifact 失败:{}: {error}", path.display()))?;
if count == 0 {
break;
}
digest.update(&buffer[..count]);
}
Ok(format!("{:x}", digest.finalize()))
}
fn write_json_record<T: Serialize>(
root: &Path,
relative_path: &str,
label: &str,
value: &T,
) -> Result<(), String> {
write_agent_runtime_json_sidecar_with_max_bytes(
root,
relative_path,
label,
value,
ISOLATED_AGENT_RECORD_MAX_BYTES,
)
}
fn read_json_record<T: serde::de::DeserializeOwned>(
root: &Path,
relative_path: &str,
label: &str,
) -> Result<Option<T>, String> {
let path = resolve_local_project_path(root, relative_path)?;
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(format!(
"读取 {label} 元数据失败:{}: {error}",
path.display()
))
}
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(format!("{label} 必须是普通文件"));
}
if metadata.len() > ISOLATED_AGENT_RECORD_MAX_BYTES as u64 {
return Err(format!("{label} 超过安全大小上限"));
}
let mut file = File::open(&path)
.map_err(|error| format!("读取 {label} 失败:{}: {error}", path.display()))?;
let mut bytes = Vec::with_capacity(metadata.len() as usize);
file.by_ref()
.take((ISOLATED_AGENT_RECORD_MAX_BYTES + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| format!("读取 {label} 失败:{}: {error}", path.display()))?;
if bytes.len() > ISOLATED_AGENT_RECORD_MAX_BYTES {
return Err(format!("{label} 超过安全大小上限"));
}
serde_json::from_slice(&bytes)
.map(Some)
.map_err(|error| format!("解析 {label} 失败:{}: {error}", path.display()))
}
fn list_json_records<T, F>(
root: &Path,
relative_dir: &str,
label: &str,
validate: F,
) -> Result<Vec<T>, String>
where
T: serde::de::DeserializeOwned,
F: Fn(&T) -> Result<(), String>,
{
let dir = resolve_local_project_path(root, relative_dir)?;
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(format!("读取 {label} 目录失败:{}: {error}", dir.display())),
};
let mut paths = Vec::<PathBuf>::new();
for entry in entries {
let entry = entry.map_err(|error| format!("读取 {label} 条目失败:{error}"))?;
let path = entry.path();
if path.extension().and_then(|value| value.to_str()) == Some("json") {
paths.push(path);
}
}
paths.sort();
let mut records = Vec::with_capacity(paths.len());
for path in paths {
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("{label} 文件名不是 UTF-8"))?;
let relative_path = format!("{relative_dir}/{file_name}");
let record = read_json_record::<T>(root, &relative_path, label)?
.ok_or_else(|| format!("{label} 在枚举后消失:{relative_path}"))?;
validate(&record)?;
records.push(record);
}
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
fn request(children: Vec<(&str, &str)>) -> GameCreationIsolatedAgentSpawnRequest {
GameCreationIsolatedAgentSpawnRequest {
children: children
.into_iter()
.map(|(template, scope)| GameCreationIsolatedAgentChildSpec {
template_agent_id: template.to_string(),
task: format!("完成 {scope} 子任务"),
acceptance_criteria: vec!["产物可验证".to_string()],
expected_artifacts: vec![scope.to_string()],
write_scopes: vec![scope.to_string()],
})
.collect(),
join_mode: GameCreationIsolatedAgentJoinMode::All,
}
}
fn create_group(
root: &Path,
action: &str,
request: &GameCreationIsolatedAgentSpawnRequest,
) -> IsolatedAgentGroupRecord {
create_or_read_isolated_group_at(
root,
"code-prototype",
"parent-run",
"parent-session",
action,
request,
)
.unwrap()
}
fn completed_result(
instance: &IsolatedAgentInstanceRecord,
) -> GameCreationIsolatedAgentChildResult {
GameCreationIsolatedAgentChildResult {
delegation_id: instance.delegation_id.clone(),
instance_id: instance.instance_id.clone(),
template_agent_id: instance.template_agent_id.clone(),
run_id: instance.run_id.clone(),
status: GameCreationIsolatedAgentResultStatus::Completed,
summary: "已完成".to_string(),
artifacts: vec![GameCreationIsolatedAgentArtifact {
path: format!("game/{}/main.js", instance.child_index),
sha256: "a".repeat(64),
}],
evidence: vec![GameCreationIsolatedAgentEvidence {
kind: "project.verify".to_string(),
summary: "验证通过".to_string(),
path: None,
sha256: None,
}],
verified_revision: Some(1),
error: None,
}
}
#[test]
fn create_group_is_idempotent() {
let temp = tempdir().unwrap();
let request = request(vec![("code-prototype", "game/a/**")]);
let first = create_group(temp.path(), "action-idempotent", &request);
let second = create_group(temp.path(), "action-idempotent", &request);
assert_eq!(first, second);
assert_eq!(
list_isolated_agent_instances_at(temp.path()).unwrap().len(),
1
);
let duplicate = create_or_read_isolated_group_at(
temp.path(),
"code-prototype",
"parent-run",
"parent-session",
"action-duplicate-request",
&request,
)
.unwrap_err();
assert!(duplicate.contains("不得在同一父 run 重复 spawn"));
assert_eq!(
list_isolated_agent_instances_at(temp.path()).unwrap().len(),
1
);
}
#[test]
fn isolated_agent_group_summary_counts_idempotent_group_once() {
let temp = tempdir().unwrap();
let request = request(vec![("code-a", "game/a/**"), ("code-b", "game/b/**")]);
let first = create_group(temp.path(), "action-summary", &request);
let second = create_group(temp.path(), "action-summary", &request);
assert_eq!(first, second);
assert_eq!(
isolated_agent_group_summary_at(temp.path(), "code-prototype", "parent-run")
.expect("summarize groups"),
IsolatedAgentGroupSummary {
group_count: 1,
child_count: 2,
max_child_count: 2,
}
);
}
#[test]
fn completed_child_with_missing_expected_artifact_dispatches_failed_join_result() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-missing-artifact",
&request(vec![("code-prototype", "game/missing/**")]),
);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let task = IsolatedAgentTerminalTask {
agent_id: instance.instance_id.clone(),
session_id: instance.session_id.clone(),
run_id: instance.run_id.clone(),
delegation_id: instance.delegation_id.clone(),
status: "completed".to_string(),
phase: "completed".to_string(),
terminal_detail: Some("只读检查完成".to_string()),
error: None,
};
let gate = IsolatedAgentVerificationGateSnapshot {
agent_id: instance.instance_id.clone(),
run_id: instance.run_id.clone(),
requires_verification: false,
mutation_revision: None,
verified_revision: None,
last_verification_tool: None,
last_verification_status: None,
};
let built = build_isolated_child_result_with_failure_fallback_at(
temp.path(),
&instance.instance_id,
&task,
&instance.expected_artifacts,
&gate,
&[],
)
.unwrap();
assert_eq!(
built.result.status,
GameCreationIsolatedAgentResultStatus::Failed
);
assert!(built.result.artifacts.is_empty());
assert!(built
.result
.error
.as_deref()
.is_some_and(|error| error.contains("缺少 expected artifact:game/missing/**")));
assert!(built.join_dispatch.is_some());
}
#[test]
fn write_tools_stay_inside_instance_scopes_and_dangerous_tools_are_denied() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-scope",
&request(vec![("code-prototype", "game/a/**")]),
);
let instance_id = &group.instance_ids[0];
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"file.write",
&serde_json::json!({"path": "game/a/main.js"}),
)
.is_ok());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"file.delete",
&serde_json::json!({"path": "game/b/main.js"}),
)
.is_err());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"project.patchset",
&serde_json::json!({
"changes": [
{"operation": "create", "path": "game/a/main.js", "content": "ok"},
{"operation": "delete", "path": "game/a/old.js", "expectedSha256": "a".repeat(64)}
]
}),
)
.is_ok());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"project.patchset",
&serde_json::json!({
"changes": [
{"operation": "create", "path": "game/a/main.js", "content": "ok"},
{"operation": "create", "path": "game/b/main.js", "content": "blocked"}
]
}),
)
.is_err());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"agent.spawn_isolated",
&serde_json::json!({}),
)
.is_err());
for tool in ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS {
let error = validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
tool,
&serde_json::json!({}),
)
.expect_err("unscoped isolated child tool must be denied");
assert!(
error.contains("动态隔离子 Agent"),
"unexpected {tool} error: {error}"
);
}
for tool in [
"command.output_read",
"command.poll",
"command.terminate",
"command.run_limited",
"preview.validate",
"project.checkpoint",
] {
assert!(
validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
tool,
&serde_json::json!({}),
)
.is_ok(),
"{tool} should remain available to isolated children"
);
}
}
#[test]
fn rendered_child_task_contains_persisted_contract_without_expanding_write_scope() {
let temp = tempdir().unwrap();
let request = GameCreationIsolatedAgentSpawnRequest {
children: vec![GameCreationIsolatedAgentChildSpec {
template_agent_id: "code-prototype".to_string(),
task: "实现严格局部子任务".to_string(),
acceptance_criteria: vec!["验收标记 acceptance-contract-unique".to_string()],
expected_artifacts: vec!["game/contract/output.unique".to_string()],
write_scopes: vec!["game/contract/**".to_string()],
}],
join_mode: GameCreationIsolatedAgentJoinMode::All,
};
let group = create_group(temp.path(), "action-render-contract", &request);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let rendered = render_isolated_agent_task_contract(&instance).unwrap();
for expected in [
"实现严格局部子任务",
"acceptanceCriteria",
"验收标记 acceptance-contract-unique",
"expectedArtifacts",
"game/contract/output.unique",
"writeScopes",
"game/contract/**",
"继承上下文只用于理解这项局部任务",
"写入范围限于 writeScopes",
"仅向父 Agent 报告这项局部任务的实际结果与证据",
] {
assert!(
rendered.contains(expected),
"missing {expected}: {rendered}"
);
}
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
&instance.instance_id,
"file.write",
&serde_json::json!({"path": "game/contract/output.unique"}),
)
.is_ok());
let error = validate_isolated_agent_tool_scope_at(
temp.path(),
&instance.instance_id,
"file.write",
&serde_json::json!({"path": "game/outside/output.unique"}),
)
.unwrap_err();
assert!(
error.contains("超出 writeScopes"),
"unexpected error: {error}"
);
}
#[test]
fn child_agent_cannot_git_commit_even_when_paths_are_inside_write_scopes() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-git-commit-scope",
&request(vec![("code-prototype", "game/a/**")]),
);
let error = validate_isolated_agent_tool_scope_at(
temp.path(),
&group.instance_ids[0],
"project.git_commit",
&serde_json::json!({
"message": "提交子任务产物",
"paths": ["game/a/main.js", "game/a/assets/player.png"]
}),
)
.unwrap_err();
assert_eq!(
error,
"动态隔离子 Agent 作用域拒绝 project.git_commit:最终提交由父 Agent 统一负责"
);
}
#[test]
fn same_template_produces_distinct_instances_sessions_and_runs() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-same-template",
&request(vec![
("code-prototype", "game/a/**"),
("code-prototype", "game/b/**"),
]),
);
let first =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let second =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[1]).unwrap();
assert_ne!(first.instance_id, second.instance_id);
assert_ne!(first.session_id, second.session_id);
assert_ne!(first.run_id, second.run_id);
assert_eq!(first.template_agent_id, second.template_agent_id);
}
#[test]
fn all_join_waits_for_every_terminal_result_and_reconciles_stably() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-all-join",
&request(vec![("code-a", "game/a/**"), ("code-b", "game/b/**")]),
);
let first =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let second =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[1]).unwrap();
assert!(
isolated_join_completion_barrier_at(temp.path(), "code-prototype", "parent-run")
.unwrap()
.is_some_and(|detail| detail.contains("waitingGroups=1"))
);
assert!(
record_isolated_child_result_at(temp.path(), &completed_result(&first))
.unwrap()
.is_none()
);
let dispatch = record_isolated_child_result_at(temp.path(), &completed_result(&second))
.unwrap()
.unwrap();
assert_eq!(dispatch.join_run_id, group.join_run_id);
assert_eq!(dispatch.parent_session_id, "parent-session");
assert!(serde_json::from_str::<Value>(&dispatch.prompt).is_ok());
assert!(
isolated_join_completion_barrier_at(temp.path(), "code-prototype", "parent-run")
.unwrap()
.is_some_and(|detail| detail.contains("readyUnclaimedGroups=1"))
);
let reconciled = reconcile_all_isolated_groups_at(temp.path()).unwrap();
assert_eq!(reconciled, vec![dispatch]);
}
#[test]
fn join_delivery_is_persistent_and_cannot_reopen_after_parent_claims_it() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-join-delivery",
&request(vec![("code-prototype", "game/a/**")]),
);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let dispatch = record_isolated_child_result_at(temp.path(), &completed_result(&instance))
.unwrap()
.unwrap();
let dispatched = write_isolated_join_delivery_at(
temp.path(),
&dispatch,
IsolatedAgentJoinDeliveryStatus::Dispatched,
Some(&dispatch.join_run_id),
None,
)
.unwrap();
assert_eq!(
dispatched.status,
IsolatedAgentJoinDeliveryStatus::Dispatched
);
assert_eq!(
dispatched.delivery_target,
IsolatedAgentJoinDeliveryTarget::Continuation
);
let claimed = write_isolated_join_delivery_at(
temp.path(),
&dispatch,
IsolatedAgentJoinDeliveryStatus::ClaimedByParent,
None,
Some("run-status-action-1"),
)
.unwrap();
assert_eq!(
claimed.status,
IsolatedAgentJoinDeliveryStatus::ClaimedByParent
);
assert_eq!(
claimed.claimed_by_action_id.as_deref(),
Some("run-status-action-1")
);
assert_eq!(
claimed.queued_run_id.as_deref(),
Some(&*dispatch.join_run_id)
);
let unjournaled =
isolated_join_completion_barrier_at(temp.path(), "code-prototype", "parent-run")
.unwrap()
.expect("claimed delivery without journal must block completion");
assert!(
unjournaled.contains("unjournaledClaimedGroups=1"),
"{unjournaled}"
);
write_isolated_join_claim_at(
temp.path(),
&IsolatedAgentJoinClaimRecord {
schema_version: ISOLATED_AGENT_JOIN_CLAIM_SCHEMA_VERSION.to_string(),
parent_agent_id: dispatch.parent_agent_id.clone(),
parent_run_id: dispatch.parent_run_id.clone(),
action_id: "run-status-action-1".to_string(),
status: IsolatedAgentJoinClaimStatus::Observed,
joins: vec![dispatch.clone()],
updated_at: unix_timestamp(),
},
)
.unwrap();
assert_eq!(
isolated_join_completion_barrier_at(temp.path(), "code-prototype", "parent-run")
.unwrap(),
None
);
assert!(write_isolated_join_delivery_at(
temp.path(),
&dispatch,
IsolatedAgentJoinDeliveryStatus::Dispatched,
Some(&dispatch.join_run_id),
None,
)
.is_err());
assert_eq!(
read_isolated_join_delivery_at(temp.path(), &dispatch)
.unwrap()
.unwrap(),
claimed
);
}
#[test]
fn parent_wake_delivery_is_persistent_idempotent_and_claim_inherits_target() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-parent-wake",
&request(vec![("code-prototype", "game/a/**")]),
);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let dispatch = record_isolated_child_result_at(temp.path(), &completed_result(&instance))
.unwrap()
.unwrap();
let dispatched =
write_isolated_parent_wake_join_delivery_at(temp.path(), &dispatch).unwrap();
assert_eq!(
dispatched.delivery_target,
IsolatedAgentJoinDeliveryTarget::ParentWake
);
assert_eq!(
dispatched.status,
IsolatedAgentJoinDeliveryStatus::Dispatched
);
assert!(dispatched.queued_run_id.is_none());
assert_eq!(
write_isolated_parent_wake_join_delivery_at(temp.path(), &dispatch).unwrap(),
dispatched
);
assert_eq!(
read_isolated_join_delivery_at(temp.path(), &dispatch)
.unwrap()
.unwrap(),
dispatched
);
let claimed = write_isolated_join_delivery_at(
temp.path(),
&dispatch,
IsolatedAgentJoinDeliveryStatus::ClaimedByParent,
None,
Some("parent-wake-claim"),
)
.unwrap();
assert_eq!(
claimed.delivery_target,
IsolatedAgentJoinDeliveryTarget::ParentWake
);
assert!(claimed.queued_run_id.is_none());
assert_eq!(
claimed.claimed_by_action_id.as_deref(),
Some("parent-wake-claim")
);
}
#[test]
fn join_delivery_infers_legacy_continuation_and_rejects_invalid_target_combinations() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-delivery-target-validation",
&request(vec![("code-prototype", "game/a/**")]),
);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let dispatch = record_isolated_child_result_at(temp.path(), &completed_result(&instance))
.unwrap()
.unwrap();
let path = isolated_join_delivery_relative_path(&dispatch.delegation_group_id);
let mut legacy = serde_json::json!({
"schemaVersion": ISOLATED_AGENT_JOIN_DELIVERY_SCHEMA_VERSION,
"parentAgentId": dispatch.parent_agent_id,
"parentRunId": dispatch.parent_run_id,
"delegationGroupId": dispatch.delegation_group_id,
"joinRunId": dispatch.join_run_id,
"status": "dispatched",
"queuedRunId": dispatch.join_run_id,
"updatedAt": unix_timestamp(),
});
write_json_record(temp.path(), &path, "动态隔离 Agent join delivery", &legacy).unwrap();
let inferred = read_isolated_join_delivery_at(temp.path(), &dispatch)
.unwrap()
.unwrap();
assert_eq!(
inferred.delivery_target,
IsolatedAgentJoinDeliveryTarget::Continuation
);
assert!(write_isolated_parent_wake_join_delivery_at(temp.path(), &dispatch).is_err());
legacy["deliveryTarget"] = Value::String("parent-wake".to_string());
write_json_record(temp.path(), &path, "动态隔离 Agent join delivery", &legacy).unwrap();
assert!(read_isolated_join_delivery_at(temp.path(), &dispatch).is_err());
legacy["deliveryTarget"] = Value::String("continuation".to_string());
legacy.as_object_mut().unwrap().remove("queuedRunId");
write_json_record(temp.path(), &path, "动态隔离 Agent join delivery", &legacy).unwrap();
assert!(read_isolated_join_delivery_at(temp.path(), &dispatch).is_err());
}
#[test]
fn corrupt_record_fails_closed() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-corrupt",
&request(vec![("code-prototype", "game/a/**")]),
);
let path = resolve_local_project_path(
temp.path(),
&isolated_instance_relative_path(&group.instance_ids[0]),
)
.unwrap();
let mut file = File::create(path).unwrap();
file.write_all(b"{broken-json").unwrap();
assert!(resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).is_err());
assert!(list_isolated_agent_instances_at(temp.path()).is_err());
}
}