Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/delegation.rs
T
lhk229 70271b408e
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m39s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m13s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Backend tests (push) Successful in 9m16s
Project CI / Repository checks (push) Successful in 8m11s
Project CI / Native shell tests (push) Successful in 11m56s
Project CI / Frontend tests (push) Successful in 11m49s
Project CI / AI game creator shell web tests (push) Successful in 6m37s
删除策划agent v2,与退役功能解耦 (#355)
Reviewed-on: #355
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-20 14:49:57 +08:00

3654 lines
150 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::*;
use sha2::{Digest, Sha256};
const STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION: &str = "game-creator-static-delegate-delivery.v1";
const STATIC_DELEGATE_DELIVERY_DIR: &str = ".agent/runtime/delegation-deliveries";
const STATIC_DELEGATE_DELIVERY_MAX_BYTES: usize = 128 * 1024;
const STATIC_DELEGATE_CLAIM_SCHEMA_VERSION: &str = "game-creator-static-delegate-claim.v1";
const STATIC_DELEGATE_CLAIM_DIR: &str = ".agent/runtime/delegation-claims";
const STATIC_DELEGATE_CLAIM_MAX_BYTES: usize = 128 * 1024;
pub(crate) const STATIC_DELEGATE_READY_RECEIPTS_PAYLOAD_MAX_CHARS: usize = 6_000;
const STATIC_DELEGATE_MAX_ACCEPTANCE_CRITERIA: usize = 8;
const STATIC_DELEGATE_ACCEPTANCE_CRITERION_MAX_CHARS: usize = 240;
const STATIC_DELEGATE_MAX_EXPECTED_ARTIFACTS: usize = 16;
const STATIC_DELEGATE_EXPECTED_ARTIFACT_MAX_CHARS: usize = 240;
const STATIC_DELEGATE_MAX_EVIDENCE: usize = 16;
pub(crate) const STATIC_DELEGATE_USER_INPUT_PREFIX: &str = "AGC_NEEDS_USER_INPUT_V1\n";
/// 中转通道的上限必须由澄清问询 schema 推导。写死 500 时,一问三选的正常中文
/// 问询(501 字符)就会在父 run 认领回执时被拒收,整条委派链阻断;而 schema 本身
/// 允许的最大合法问询比 500 大一个数量级。
pub(crate) const STATIC_DELEGATE_USER_INPUT_MAX_RESPONSE_CHARS: usize =
STATIC_DELEGATE_USER_INPUT_PREFIX.len() + AGENT_RUNTIME_USER_INPUT_MAX_WIRE_CHARS;
/// 澄清信封是结构化协议载荷,只是恰好借用了「子 Agent 自由回复」这条文本通道。
/// 通道上每一处按普通自由文本盲切字符的定界都会把 JSON 拦腰砍断,父 run 认领回执
/// 时解析失败,整条委派链停在 needs-reconciliation。信封本身已由问询 schema 硬性
/// 定界(问题数、选项数、各字段字符上限逐项校验),不需要再叠一层盲切;凡是可能
/// 承载信封的定界点都从这里取上限,非信封文本仍走各自原有的上限。
pub(crate) fn static_delegate_result_detail_max_chars(
value: &str,
default_max_chars: usize,
) -> usize {
if response_is_static_delegate_user_input_envelope(value) {
STATIC_DELEGATE_USER_INPUT_MAX_RESPONSE_CHARS
} else {
default_max_chars
}
}
/// 这条回复是不是澄清信封,而不是一次交付收束。
///
/// 收束门禁按「任务是否做完」判据拦最终回复,而澄清信封恰恰相反:它是本 run
/// 就此挂起、把决定权交回用户,剩下的工作由用户答完之后的 continuation run 接着
/// 做。用完成度判据去拦它,对任何含「答完之后再做 X」步骤的计划都不可满足。
pub(crate) fn response_is_static_delegate_user_input_envelope(response: &str) -> bool {
response
.trim_start()
.starts_with(STATIC_DELEGATE_USER_INPUT_PREFIX)
}
/// 构造一份贴着问询 schema 上限的合法澄清信封,供跨模块的通道用例复用。
/// 通道必须容得下 schema 允许的最大合法问询,而不只是「碰巧短」的那一条。
#[cfg(test)]
pub(crate) fn schema_max_clarification_envelope() -> String {
let questions = (0..3)
.map(|index| {
serde_json::json!({
"id": format!("decision_{index}"),
"header": "关键决定",
"question": "当前要定的规则。".repeat(40),
"options": (0..3)
.map(|option| {
serde_json::json!({
"label": format!("{} · 平行方案", char::from(b'A' + option as u8)),
"description": "该方案的边界与代价。".repeat(20),
})
})
.collect::<Vec<_>>(),
})
})
.collect::<Vec<_>>();
format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({ "questions": questions })
)
}
const STATIC_DELEGATE_CLARIFICATION_ROUND_LIMIT_DEFAULT: u32 = 3;
// 链上重放的防环 / 防越界上限,远大于设计允许的最大 7 跳,纯粹是安全阀。
const STATIC_DELEGATE_LINEAGE_MAX_HOPS: usize = 32;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum StaticDelegateDeliveryStatus {
Dispatched,
Ready,
ClaimedByParent,
Suppressed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum StaticDelegateContractStatus {
EvidenceReady,
NeedsRepair,
NeedsUserInput,
UserRevisionRequested,
/// A structurally valid durable status introduced by a newer client.
///
/// The raw wire value is retained so an old client can safely read and
/// rewrite the record without silently changing the newer status.
Unknown(String),
}
impl StaticDelegateContractStatus {
pub(crate) fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown(_))
}
fn durable_value(&self) -> &str {
match self {
Self::EvidenceReady => "evidence-ready",
Self::NeedsRepair => "needs-repair",
Self::NeedsUserInput => "needs-user-input",
Self::UserRevisionRequested => "user-revision-requested",
Self::Unknown(value) => value,
}
}
}
impl serde::Serialize for StaticDelegateContractStatus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.durable_value())
}
}
impl<'de> serde::Deserialize<'de> for StaticDelegateContractStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Ok(match value.as_str() {
"evidence-ready" => Self::EvidenceReady,
"needs-repair" => Self::NeedsRepair,
"needs-user-input" => Self::NeedsUserInput,
"user-revision-requested" => Self::UserRevisionRequested,
_ => Self::Unknown(value),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StaticDelegateArtifact {
pub(crate) path: String,
pub(crate) sha256: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StaticDelegateEvidence {
pub(crate) kind: String,
pub(crate) summary: String,
#[serde(default)]
pub(crate) path: Option<String>,
#[serde(default)]
pub(crate) sha256: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StaticDelegateStructuredResult {
pub(crate) contract_status: StaticDelegateContractStatus,
#[serde(default)]
pub(crate) artifacts: Vec<StaticDelegateArtifact>,
#[serde(default)]
pub(crate) missing_expected_artifacts: Vec<String>,
#[serde(default)]
pub(crate) verification_required: bool,
#[serde(default)]
pub(crate) verified_revision: Option<u64>,
#[serde(default)]
pub(crate) evidence: Vec<StaticDelegateEvidence>,
#[serde(default)]
pub(crate) error: Option<String>,
#[serde(default)]
pub(crate) user_input_questions: Vec<AgentRuntimeUserInputQuestion>,
#[serde(default)]
pub(crate) user_input_questions_sha256: Option<String>,
}
impl Default for StaticDelegateStructuredResult {
fn default() -> Self {
Self {
contract_status: StaticDelegateContractStatus::NeedsRepair,
artifacts: Vec::new(),
missing_expected_artifacts: Vec::new(),
verification_required: false,
verified_revision: None,
evidence: Vec::new(),
error: None,
user_input_questions: Vec::new(),
user_input_questions_sha256: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StaticDelegateDeliveryRecord {
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_id: String,
pub(crate) target_agent_id: String,
pub(crate) target_session_id: String,
pub(crate) target_run_id: String,
#[serde(default)]
pub(crate) acceptance_criteria: Vec<String>,
#[serde(default)]
pub(crate) expected_artifacts: Vec<String>,
#[serde(default)]
pub(crate) repair_of_delegation_id: Option<String>,
#[serde(default)]
pub(crate) clarification_request_id: Option<String>,
#[serde(default)]
pub(crate) clarification_answers_sha256: Option<String>,
pub(crate) status: StaticDelegateDeliveryStatus,
pub(crate) terminal_status: Option<String>,
pub(crate) result_summary: Option<String>,
#[serde(default)]
pub(crate) structured_result: Option<StaticDelegateStructuredResult>,
pub(crate) claimed_by_action_id: Option<String>,
pub(crate) updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StaticDelegateReadyReceipt {
pub(crate) delegation_id: String,
pub(crate) target_agent_id: String,
pub(crate) status: String,
pub(crate) summary: String,
#[serde(default)]
pub(crate) acceptance_criteria: Vec<String>,
#[serde(default)]
pub(crate) expected_artifacts: Vec<String>,
#[serde(default)]
pub(crate) repair_of_delegation_id: Option<String>,
#[serde(default)]
pub(crate) structured_result: Option<StaticDelegateStructuredResult>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
enum StaticDelegateClaimStatus {
Prepared,
Committed,
Observed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct StaticDelegateClaimRecord {
schema_version: String,
parent_agent_id: String,
parent_run_id: String,
action_id: String,
status: StaticDelegateClaimStatus,
receipts: Vec<StaticDelegateReadyReceipt>,
updated_at: u64,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct StaticDelegateCompletionBarrier {
pub(crate) waiting_count: usize,
pub(crate) ready_unclaimed_count: usize,
pub(crate) unobserved_claim_count: usize,
pub(crate) repair_required_count: usize,
pub(crate) user_input_required_count: usize,
pub(crate) user_revision_pending_count: usize,
pub(crate) unknown_contract_status_count: usize,
}
impl StaticDelegateCompletionBarrier {
pub(crate) fn is_clear(self) -> bool {
self.waiting_count == 0
&& self.ready_unclaimed_count == 0
&& self.unobserved_claim_count == 0
&& self.repair_required_count == 0
&& self.user_input_required_count == 0
&& self.user_revision_pending_count == 0
&& self.unknown_contract_status_count == 0
}
/// 自动恢复/唤醒路径该不该收手。
///
/// `user_revision_pending_count` 计在这里是承重的:用户修订是一条显式的 Supervisor
/// 决策边界,在它派出续作之前父 run 绝不能被自动恢复(`runtime_tools/delivery.rs`
/// 的 `debug_assert` 把这份跨文件依赖钉在使用现场)。
pub(crate) fn has_waiting(self) -> bool {
self.waiting_count > 0
|| self.user_revision_pending_count > 0
|| self.unknown_contract_status_count > 0
}
/// 当前正在跑的这一轮,有没有**外部事件**值得 park 着等。
///
/// 和 `has_waiting()` 问的是相反的问题,所以刻意不计 `user_revision_pending_count`:
/// - `waitingDelegations > 0`:子 Agent 正在跑,park 等它 —— 会有回执到来。
/// - `unknownContractStatus > 0`:fail-closed,宁可停下也不按未知状态行动。
/// - `userRevisionPending > 0`:**没有任何东西在跑**。那条回执只能来自本 run 自己
/// 创建的修订委派,park 等它就是等自己,必然死锁。
///
/// 生产实测:用户点「修改」后 Supervisor park 在「等待专业 Agent 委派回执 / 回执全部
/// ready 后自动唤醒当前父 run」,8 分钟零事件——它在等一条只有它自己能造出来的回执。
/// main_loop 里本来就有一条专为 user_revision 写的分支(`phase=planning`、
/// `next_step=调用 agent.delegate…`),但被上游这道 park 门截胡了。
pub(crate) fn has_external_wait(self) -> bool {
self.waiting_count > 0 || self.unknown_contract_status_count > 0
}
pub(crate) fn detail(self) -> String {
format!(
"waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · userRevisionPending={} · unknownContractStatus={} · 必须认领专业 Agent 回执,处理 needs-user-input/needs-repair/user-revision-requested,或升级客户端后再继续",
self.waiting_count,
self.ready_unclaimed_count,
self.unobserved_claim_count,
self.repair_required_count,
self.user_input_required_count,
self.user_revision_pending_count,
self.unknown_contract_status_count
)
}
}
pub(crate) fn new_static_delegate_delivery(
parent_agent_id: &str,
parent_session_id: &str,
parent_run_id: &str,
parent_action_id: &str,
delegation_id: &str,
target_agent_id: &str,
target_session_id: &str,
target_run_id: &str,
) -> StaticDelegateDeliveryRecord {
new_static_delegate_delivery_with_contract(
parent_agent_id,
parent_session_id,
parent_run_id,
parent_action_id,
delegation_id,
target_agent_id,
target_session_id,
target_run_id,
&[],
&[],
None,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new_static_delegate_delivery_with_contract(
parent_agent_id: &str,
parent_session_id: &str,
parent_run_id: &str,
parent_action_id: &str,
delegation_id: &str,
target_agent_id: &str,
target_session_id: &str,
target_run_id: &str,
acceptance_criteria: &[String],
expected_artifacts: &[String],
repair_of_delegation_id: Option<&str>,
) -> StaticDelegateDeliveryRecord {
StaticDelegateDeliveryRecord {
schema_version: STATIC_DELEGATE_DELIVERY_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_id: delegation_id.to_string(),
target_agent_id: target_agent_id.to_string(),
target_session_id: target_session_id.to_string(),
target_run_id: target_run_id.to_string(),
acceptance_criteria: acceptance_criteria.to_vec(),
expected_artifacts: expected_artifacts.to_vec(),
repair_of_delegation_id: repair_of_delegation_id.map(str::to_string),
clarification_request_id: None,
clarification_answers_sha256: None,
status: StaticDelegateDeliveryStatus::Dispatched,
terminal_status: None,
result_summary: None,
structured_result: None,
claimed_by_action_id: None,
updated_at: unix_timestamp(),
}
}
pub(crate) fn create_or_read_static_delegate_delivery_at(
root: &Path,
expected: &StaticDelegateDeliveryRecord,
) -> Result<StaticDelegateDeliveryRecord, String> {
validate_static_delegate_delivery_record(expected)?;
if let Some(existing) = read_static_delegate_delivery_at(root, &expected.delegation_id)? {
validate_static_delegate_delivery_identity(&existing, expected)?;
return Ok(existing);
}
write_static_delegate_delivery_at(root, expected)?;
Ok(expected.clone())
}
pub(crate) fn reopen_suppressed_static_delegate_repair_at(
root: &Path,
expected: &StaticDelegateDeliveryRecord,
) -> Result<StaticDelegateDeliveryRecord, String> {
validate_static_delegate_delivery_record(expected)?;
if expected.repair_of_delegation_id.is_none() {
return Err("只有返工 delivery 可以从 suppressed 原地恢复".to_string());
}
let mut delivery = read_static_delegate_delivery_at(root, &expected.delegation_id)?
.ok_or_else(|| format!("静态委派 delivery 不存在:{}", expected.delegation_id))?;
validate_static_delegate_delivery_identity(&delivery, expected)?;
if delivery.status == StaticDelegateDeliveryStatus::Dispatched {
return Ok(delivery);
}
if delivery.status != StaticDelegateDeliveryStatus::Suppressed {
return Err("只有 suppressed 返工 delivery 可以原地恢复".to_string());
}
if delivery.terminal_status.is_some()
|| delivery.result_summary.is_some()
|| delivery.structured_result.is_some()
|| delivery.claimed_by_action_id.is_some()
{
return Err("已有终态结果的 suppressed 返工 delivery 不能重新启动".to_string());
}
delivery.status = StaticDelegateDeliveryStatus::Dispatched;
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
Ok(delivery)
}
pub(crate) fn mark_static_delegate_delivery_ready_at(
root: &Path,
child_agent_id: &str,
child_session_id: &str,
child_run_id: &str,
delegation_id: &str,
terminal_status: &str,
result_summary: &str,
) -> Result<StaticDelegateDeliveryRecord, String> {
let structured_result = StaticDelegateStructuredResult {
contract_status: if terminal_status == "completed" {
StaticDelegateContractStatus::EvidenceReady
} else {
StaticDelegateContractStatus::NeedsRepair
},
artifacts: Vec::new(),
missing_expected_artifacts: Vec::new(),
verification_required: false,
verified_revision: None,
evidence: Vec::new(),
error: (terminal_status != "completed").then(|| result_summary.to_string()),
user_input_questions: Vec::new(),
user_input_questions_sha256: None,
};
mark_static_delegate_delivery_ready_with_result_at(
root,
child_agent_id,
child_session_id,
child_run_id,
delegation_id,
terminal_status,
result_summary,
structured_result,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn mark_static_delegate_delivery_ready_with_result_at(
root: &Path,
child_agent_id: &str,
child_session_id: &str,
child_run_id: &str,
delegation_id: &str,
terminal_status: &str,
result_summary: &str,
structured_result: StaticDelegateStructuredResult,
) -> Result<StaticDelegateDeliveryRecord, String> {
let mut delivery = read_static_delegate_delivery_at(root, delegation_id)?
.ok_or_else(|| format!("静态委派 delivery 不存在:{delegation_id}"))?;
if delivery.target_agent_id != child_agent_id
|| delivery.target_session_id != child_session_id
|| delivery.target_run_id != child_run_id
{
return Err(format!(
"静态委派子任务身份与 delivery 不一致:{delegation_id}"
));
}
if delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent {
if delivery.terminal_status.as_deref() != Some(terminal_status)
|| delivery.result_summary.as_deref() != Some(result_summary)
|| delivery
.structured_result
.as_ref()
.is_some_and(|existing| existing != &structured_result)
{
return Err(format!("已认领静态委派收到冲突终态结果:{delegation_id}"));
}
return Ok(delivery);
}
if delivery.status == StaticDelegateDeliveryStatus::Suppressed {
return Ok(delivery);
}
if delivery.status == StaticDelegateDeliveryStatus::Ready {
if delivery.terminal_status.as_deref() != Some(terminal_status)
|| delivery.result_summary.as_deref() != Some(result_summary)
|| delivery
.structured_result
.as_ref()
.is_some_and(|existing| existing != &structured_result)
{
return Err(format!("静态委派终态结果已存在且内容冲突:{delegation_id}"));
}
return Ok(delivery);
}
delivery.status = StaticDelegateDeliveryStatus::Ready;
delivery.terminal_status = Some(terminal_status.to_string());
delivery.result_summary = Some(result_summary.to_string());
delivery.structured_result = Some(structured_result);
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
Ok(delivery)
}
/// claim 里的 `structuredResult` 是「父 Agent 在那个 action 上观察到了什么」的冻结
/// 快照;delivery 是当前真相。两者绝大多数时候必须逐字相等——不等就是漂移或篡改。
///
/// 唯一的例外是审批:用户在审批卡上点「修改 / 退回」后,
/// `mark_static_delegate_delivery_user_revision_requested_at` 会把 delivery 从
/// `EvidenceReady` 原地改写成 `UserRevisionRequested`,而 claim 快照仍停在
/// `EvidenceReady`。那不是漂移,是一次只由审批产生、且只能朝这个方向走的合法转移;
/// 快照记的那句「当时观察到 evidence-ready」现在依然为真,不该被改写。
///
/// 按全等判会把它当成冲突:`agent.run_status` 每次重放这条 claim 都 failed,
/// Supervisor 永远拿不到回执、也就永远建不出修订委派。生产实测卡死在第 43 轮空转,
/// 报「静态委派 claim 与 delivery 身份或结果冲突」。原型没有 claim 这层快照,单一
/// 真相就地改,结构上不存在这个冲突——这里翻译的是同一个语义:比较的是「delivery 是
/// 不是 receipt 的合法后继」,不是「两者永远全等」。
///
/// 放行面刻意压到最小:除 `contractStatus` 外每个字段都必须逐字不变,且方向唯一。
fn static_delegate_structured_result_follows_claim_snapshot(
snapshot: Option<&StaticDelegateStructuredResult>,
current: Option<&StaticDelegateStructuredResult>,
) -> bool {
if snapshot == current {
return true;
}
let (Some(snapshot), Some(current)) = (snapshot, current) else {
return false;
};
if snapshot.contract_status != StaticDelegateContractStatus::EvidenceReady
|| current.contract_status != StaticDelegateContractStatus::UserRevisionRequested
{
return false;
}
let mut rebased = current.clone();
rebased.contract_status = StaticDelegateContractStatus::EvidenceReady;
rebased == *snapshot
}
/// Mark an already claimed, evidence-ready planning delivery as waiting for a
/// user-requested revision. Approval is the only producer of this durable
/// status; keeping the transition here makes its evidence precondition and
/// idempotency explicit instead of allowing a generic delivery writer to
/// manufacture the state.
pub(crate) fn mark_static_delegate_delivery_user_revision_requested_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
delegation_id: &str,
) -> Result<StaticDelegateDeliveryRecord, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
validate_static_delegate_id(delegation_id, "delegationId", 160)?;
let mut delivery = read_static_delegate_delivery_at(root, delegation_id)?
.ok_or_else(|| format!("静态委派 delivery 不存在:{delegation_id}"))?;
if delivery.parent_agent_id != parent_agent_id || delivery.parent_run_id != parent_run_id {
return Err("用户修订只能改写同一 Supervisor 父 run 的 delivery".to_string());
}
if delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent {
return Err("用户修订只能改写已由 Supervisor 认领的 delivery".to_string());
}
let Some(result) = delivery.structured_result.as_mut() else {
return Err("用户修订的原 delivery 缺少 structuredResult".to_string());
};
match result.contract_status {
StaticDelegateContractStatus::UserRevisionRequested => return Ok(delivery),
StaticDelegateContractStatus::EvidenceReady => {}
_ => return Err("用户修订只能从 EvidenceReady delivery 派生".to_string()),
}
result.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
Ok(delivery)
}
pub(crate) fn suppress_static_delegate_delivery_at(
root: &Path,
expected: &StaticDelegateDeliveryRecord,
) -> Result<StaticDelegateDeliveryRecord, String> {
validate_static_delegate_delivery_record(expected)?;
let mut delivery = read_static_delegate_delivery_at(root, &expected.delegation_id)?
.ok_or_else(|| format!("静态委派 delivery 不存在:{}", expected.delegation_id))?;
if delivery != *expected {
return Err(format!(
"静态委派 delivery 在 suppression 前已变化:{}",
expected.delegation_id
));
}
if delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent {
return Ok(delivery);
}
delivery.status = StaticDelegateDeliveryStatus::Suppressed;
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
Ok(delivery)
}
pub(crate) fn static_delegate_completion_barrier_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<StaticDelegateCompletionBarrier, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
let claims = list_static_delegate_claims_at(root)?
.into_iter()
.filter(|claim| {
claim.parent_agent_id == parent_agent_id && claim.parent_run_id == parent_run_id
})
.collect::<Vec<_>>();
for claim in &claims {
commit_static_delegate_claim_at(root, claim.clone())?;
}
let claims = list_static_delegate_claims_at(root)?
.into_iter()
.filter(|claim| {
claim.parent_agent_id == parent_agent_id && claim.parent_run_id == parent_run_id
})
.collect::<Vec<_>>();
let mut barrier = StaticDelegateCompletionBarrier::default();
let deliveries = list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id && delivery.parent_run_id == parent_run_id
})
.collect::<Vec<_>>();
for delivery in &deliveries {
match delivery.status {
StaticDelegateDeliveryStatus::Dispatched => {
barrier.waiting_count = barrier.waiting_count.saturating_add(1);
}
StaticDelegateDeliveryStatus::Ready => {
barrier.ready_unclaimed_count = barrier.ready_unclaimed_count.saturating_add(1);
}
StaticDelegateDeliveryStatus::ClaimedByParent
| StaticDelegateDeliveryStatus::Suppressed => {}
}
}
barrier.unobserved_claim_count = claims
.iter()
.filter(|claim| claim.status != StaticDelegateClaimStatus::Observed)
.count();
barrier.repair_required_count = deliveries
.iter()
.filter(|delivery| {
let legacy_empty_contract = delivery.acceptance_criteria.is_empty()
&& delivery.expected_artifacts.is_empty()
&& delivery.repair_of_delegation_id.is_none();
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.repair_of_delegation_id.is_none()
&& !legacy_empty_contract
&& delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsRepair
|| (result.contract_status == StaticDelegateContractStatus::NeedsUserInput
&& delivery.clarification_answers_sha256.is_some())
})
&& !deliveries.iter().any(|candidate| {
candidate.repair_of_delegation_id.as_deref()
== Some(delivery.delegation_id.as_str())
&& matches!(
candidate.status,
StaticDelegateDeliveryStatus::Dispatched
| StaticDelegateDeliveryStatus::Ready
| StaticDelegateDeliveryStatus::ClaimedByParent
)
})
})
.count();
barrier.user_input_required_count = deliveries
.iter()
.filter(|delivery| {
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
})
&& delivery.clarification_answers_sha256.is_none()
&& !deliveries.iter().any(|candidate| {
candidate.repair_of_delegation_id.as_deref()
== Some(delivery.delegation_id.as_str())
&& matches!(
candidate.status,
StaticDelegateDeliveryStatus::Dispatched
| StaticDelegateDeliveryStatus::Ready
| StaticDelegateDeliveryStatus::ClaimedByParent
)
})
})
.count();
barrier.user_revision_pending_count = deliveries
.iter()
.filter(|delivery| {
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::UserRevisionRequested
})
&& !deliveries.iter().any(|candidate| {
candidate.repair_of_delegation_id.as_deref()
== Some(delivery.delegation_id.as_str())
&& matches!(
candidate.status,
StaticDelegateDeliveryStatus::Dispatched
| StaticDelegateDeliveryStatus::Ready
| StaticDelegateDeliveryStatus::ClaimedByParent
)
})
})
.count();
barrier.unknown_contract_status_count = deliveries
.iter()
.filter(|delivery| {
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery
.structured_result
.as_ref()
.is_some_and(|result| result.contract_status.is_unknown())
})
.count();
Ok(barrier)
}
pub(crate) fn static_delegate_run_status_may_include_receipts_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: Option<&str>,
) -> Result<bool, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
if let Some(action_id) = action_id {
validate_static_delegate_id(action_id, "actionId", 160)?;
}
Ok(list_static_delegate_deliveries_at(root)?
.into_iter()
.any(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& (delivery.status == StaticDelegateDeliveryStatus::Ready
|| (delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& action_id.is_some_and(|action_id| {
delivery.claimed_by_action_id.as_deref() == Some(action_id)
})))
}))
}
pub(crate) fn claim_ready_static_delegate_receipts_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
claim_ready_static_delegate_receipts_with_budget_at(
root,
parent_agent_id,
parent_run_id,
action_id,
STATIC_DELEGATE_READY_RECEIPTS_PAYLOAD_MAX_CHARS,
)
}
pub(crate) fn claim_ready_static_delegate_receipts_with_budget_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
max_payload_chars: usize,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
validate_static_delegate_id(action_id, "actionId", 160)?;
let claim_lock =
acquire_static_delegate_claim_lock_at(root, parent_agent_id, parent_run_id, action_id)?;
if let Some(claim) =
read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)?
{
return commit_static_delegate_claim_locked_with_budget_at(
root,
claim,
&claim_lock,
max_payload_chars,
);
}
let deliveries = list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& matches!(
delivery.status,
StaticDelegateDeliveryStatus::Ready
| StaticDelegateDeliveryStatus::ClaimedByParent
)
})
.collect::<Vec<_>>();
let mut required_delegation_ids = std::collections::BTreeSet::new();
let mut receipts = Vec::new();
for delivery in deliveries {
if delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent {
if delivery.claimed_by_action_id.as_deref() != Some(action_id) {
continue;
}
required_delegation_ids.insert(delivery.delegation_id.clone());
}
receipts.push(static_delegate_ready_receipt_from_delivery(delivery));
}
receipts.sort_by(|left, right| left.delegation_id.cmp(&right.delegation_id));
if receipts.is_empty() {
return Ok(receipts);
}
receipts = select_static_delegate_receipt_batch(
receipts,
&required_delegation_ids,
max_payload_chars,
)?;
if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& receipts
.iter()
.any(|receipt| !required_delegation_ids.contains(&receipt.delegation_id))
{
resolve_supervisor_collaboration_policy_for_run_at(root, parent_agent_id, parent_run_id)?;
}
let delivery_locks = acquire_static_delegate_delivery_locks_at(
root,
receipts
.iter()
.map(|receipt| receipt.delegation_id.clone())
.collect(),
)?;
for expected in &receipts {
let delivery = read_static_delegate_delivery_at(root, &expected.delegation_id)?
.ok_or_else(|| format!("静态委派 delivery 在认领前消失:{}", expected.delegation_id))?;
if delivery.parent_agent_id != parent_agent_id
|| delivery.parent_run_id != parent_run_id
|| !matches!(
delivery.status,
StaticDelegateDeliveryStatus::Ready | StaticDelegateDeliveryStatus::ClaimedByParent
)
|| (delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.claimed_by_action_id.as_deref() != Some(action_id))
|| static_delegate_ready_receipt_from_delivery(delivery) != *expected
{
return Err(format!(
"静态委派 delivery 在预算选择后发生变化:{}",
expected.delegation_id
));
}
}
let claim = StaticDelegateClaimRecord {
schema_version: STATIC_DELEGATE_CLAIM_SCHEMA_VERSION.to_string(),
parent_agent_id: parent_agent_id.to_string(),
parent_run_id: parent_run_id.to_string(),
action_id: action_id.to_string(),
status: StaticDelegateClaimStatus::Prepared,
receipts,
updated_at: unix_timestamp(),
};
write_static_delegate_claim_at(root, &claim)?;
commit_static_delegate_claim_with_locks_with_budget_at(
root,
claim,
&claim_lock,
delivery_locks,
max_payload_chars,
)
}
fn serialize_static_delegate_ready_receipts_payload(
receipts: &[StaticDelegateReadyReceipt],
) -> Result<String, String> {
serde_json::to_string(&serde_json::json!({
"ready": true,
"receipts": receipts,
}))
.map_err(|error| format!("序列化专业 Agent ready receipts 失败:{error}"))
}
fn static_delegate_ready_receipt_from_delivery(
delivery: StaticDelegateDeliveryRecord,
) -> StaticDelegateReadyReceipt {
StaticDelegateReadyReceipt {
delegation_id: delivery.delegation_id,
target_agent_id: delivery.target_agent_id,
status: delivery
.terminal_status
.unwrap_or_else(|| "unknown".to_string()),
summary: delivery.result_summary.unwrap_or_default(),
acceptance_criteria: delivery.acceptance_criteria,
expected_artifacts: delivery.expected_artifacts,
repair_of_delegation_id: delivery.repair_of_delegation_id,
structured_result: delivery.structured_result,
}
}
fn select_static_delegate_receipt_batch(
receipts: Vec<StaticDelegateReadyReceipt>,
required_delegation_ids: &std::collections::BTreeSet<String>,
max_payload_chars: usize,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
let mut selected = receipts
.iter()
.filter(|receipt| required_delegation_ids.contains(&receipt.delegation_id))
.cloned()
.collect::<Vec<_>>();
if !selected.is_empty() {
let required_payload_chars = serialize_static_delegate_ready_receipts_payload(&selected)?
.chars()
.count();
if required_payload_chars > max_payload_chars {
return Err(format!(
"专业 Agent 已认领 ready receipts 超过单次完整观察上限:{} > {}",
required_payload_chars, max_payload_chars
));
}
// 缺失 claim journal 时只恢复已经归属当前 action 的 receipt,下一轮再认领新 Ready。
return Ok(selected);
}
for receipt in receipts
.iter()
.filter(|receipt| !required_delegation_ids.contains(&receipt.delegation_id))
.cloned()
{
let mut next = selected.clone();
next.push(receipt);
next.sort_by(|left, right| left.delegation_id.cmp(&right.delegation_id));
let fits = serialize_static_delegate_ready_receipts_payload(&next)?
.chars()
.count()
<= max_payload_chars;
if fits {
selected = next;
continue;
}
break;
}
if selected.is_empty() {
return Err("专业 Agent ready receipts 无法形成完整观察批次".to_string());
}
Ok(selected)
}
pub(crate) fn mark_static_delegate_claim_observed_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<bool, String> {
let claim_lock =
acquire_static_delegate_claim_lock_at(root, parent_agent_id, parent_run_id, action_id)?;
let Some(mut claim) =
read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)?
else {
return Ok(false);
};
if claim.status == StaticDelegateClaimStatus::Prepared {
let delivery_locks = acquire_static_delegate_delivery_locks_at(
root,
claim
.receipts
.iter()
.map(|receipt| receipt.delegation_id.clone())
.collect(),
)?;
commit_static_delegate_claim_with_locks_at(root, claim, &claim_lock, delivery_locks)?;
claim = read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)?
.ok_or_else(|| "静态委派 claim 在标记 observation 前消失".to_string())?;
}
if claim.status != StaticDelegateClaimStatus::Observed {
if claim.status != StaticDelegateClaimStatus::Committed {
return Err("静态委派 claim 尚未完成,不能标记 observation".to_string());
}
claim.status = StaticDelegateClaimStatus::Observed;
claim.updated_at = unix_timestamp();
write_static_delegate_claim_at(root, &claim)?;
}
Ok(true)
}
pub(crate) fn mark_static_delegate_claim_observed_for_receipts_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
observed_delegation_ids: &std::collections::BTreeSet<String>,
) -> Result<bool, String> {
let claim_lock =
acquire_static_delegate_claim_lock_at(root, parent_agent_id, parent_run_id, action_id)?;
let Some(mut claim) =
read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)?
else {
return Ok(false);
};
let claim_delegation_ids = claim
.receipts
.iter()
.map(|receipt| receipt.delegation_id.clone())
.collect::<std::collections::BTreeSet<_>>();
if &claim_delegation_ids != observed_delegation_ids {
return Err(format!(
"静态委派 observation 未完整包含 claim receipts:expected={} observed={}",
claim_delegation_ids.len(),
observed_delegation_ids.len()
));
}
if claim.status == StaticDelegateClaimStatus::Prepared {
let delivery_locks = acquire_static_delegate_delivery_locks_at(
root,
claim
.receipts
.iter()
.map(|receipt| receipt.delegation_id.clone())
.collect(),
)?;
commit_static_delegate_claim_with_locks_at(root, claim, &claim_lock, delivery_locks)?;
claim = read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)?
.ok_or_else(|| "静态委派 claim 在标记 observation 前消失".to_string())?;
}
if claim.status != StaticDelegateClaimStatus::Observed {
if claim.status != StaticDelegateClaimStatus::Committed {
return Err("静态委派 claim 尚未完成,不能标记 observation".to_string());
}
claim.status = StaticDelegateClaimStatus::Observed;
claim.updated_at = unix_timestamp();
write_static_delegate_claim_at(root, &claim)?;
}
Ok(true)
}
fn commit_static_delegate_claim_at(
root: &Path,
claim: StaticDelegateClaimRecord,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
validate_static_delegate_claim_record(&claim)?;
let claim_lock = acquire_static_delegate_claim_lock_at(
root,
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
)?;
commit_static_delegate_claim_locked_at(root, claim, &claim_lock)
}
fn commit_static_delegate_claim_locked_at(
root: &Path,
claim: StaticDelegateClaimRecord,
claim_lock: &AgentRuntimeTaskLock,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
commit_static_delegate_claim_locked_with_budget_at(
root,
claim,
claim_lock,
STATIC_DELEGATE_READY_RECEIPTS_PAYLOAD_MAX_CHARS,
)
}
fn commit_static_delegate_claim_locked_with_budget_at(
root: &Path,
claim: StaticDelegateClaimRecord,
claim_lock: &AgentRuntimeTaskLock,
max_payload_chars: usize,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
validate_static_delegate_claim_record(&claim)?;
let latest = read_static_delegate_claim_at(
root,
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
)?
.ok_or_else(|| "静态委派 claim 在提交前消失".to_string())?;
validate_static_delegate_claim_identity_and_receipts(&latest, &claim)?;
let delivery_locks = acquire_static_delegate_delivery_locks_at(
root,
latest
.receipts
.iter()
.map(|receipt| receipt.delegation_id.clone())
.collect(),
)?;
commit_static_delegate_claim_with_locks_with_budget_at(
root,
latest,
claim_lock,
delivery_locks,
max_payload_chars,
)
}
fn commit_static_delegate_claim_with_locks_at(
root: &Path,
claim: StaticDelegateClaimRecord,
_claim_lock: &AgentRuntimeTaskLock,
_delivery_locks: Vec<AgentRuntimeTaskLock>,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
commit_static_delegate_claim_with_locks_with_budget_at(
root,
claim,
_claim_lock,
_delivery_locks,
STATIC_DELEGATE_READY_RECEIPTS_PAYLOAD_MAX_CHARS,
)
}
fn commit_static_delegate_claim_with_locks_with_budget_at(
root: &Path,
claim: StaticDelegateClaimRecord,
_claim_lock: &AgentRuntimeTaskLock,
_delivery_locks: Vec<AgentRuntimeTaskLock>,
max_payload_chars: usize,
) -> Result<Vec<StaticDelegateReadyReceipt>, String> {
validate_static_delegate_claim_record(&claim)?;
let mut claim = read_static_delegate_claim_at(
root,
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
)?
.ok_or_else(|| "静态委派 claim 在提交期间消失".to_string())?;
let payload = serialize_static_delegate_ready_receipts_payload(&claim.receipts)?;
if payload.chars().count() > max_payload_chars {
return Err(format!(
"专业 Agent ready receipts 超过单次完整观察上限:{} > {}",
payload.chars().count(),
max_payload_chars
));
}
for receipt in &claim.receipts {
let mut delivery = read_static_delegate_delivery_at(root, &receipt.delegation_id)?
.ok_or_else(|| {
format!(
"静态委派 claim 对应 delivery 不存在:{}",
receipt.delegation_id
)
})?;
if delivery.parent_agent_id != claim.parent_agent_id
|| delivery.parent_run_id != claim.parent_run_id
|| delivery.target_agent_id != receipt.target_agent_id
|| delivery.terminal_status.as_deref() != Some(receipt.status.as_str())
|| delivery.result_summary.as_deref() != Some(receipt.summary.as_str())
|| delivery.acceptance_criteria != receipt.acceptance_criteria
|| delivery.expected_artifacts != receipt.expected_artifacts
|| delivery.repair_of_delegation_id != receipt.repair_of_delegation_id
|| !static_delegate_structured_result_follows_claim_snapshot(
receipt.structured_result.as_ref(),
delivery.structured_result.as_ref(),
)
{
return Err(format!(
"静态委派 claim 与 delivery 身份或结果冲突:{}",
receipt.delegation_id
));
}
match delivery.status {
StaticDelegateDeliveryStatus::Ready => {
delivery.status = StaticDelegateDeliveryStatus::ClaimedByParent;
delivery.claimed_by_action_id = Some(claim.action_id.clone());
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
}
StaticDelegateDeliveryStatus::ClaimedByParent
if delivery.claimed_by_action_id.as_deref() == Some(claim.action_id.as_str()) => {}
_ => {
return Err(format!(
"静态委派 claim 对应 delivery 状态冲突:{}",
receipt.delegation_id
));
}
}
}
if claim.status == StaticDelegateClaimStatus::Prepared {
claim.status = StaticDelegateClaimStatus::Committed;
claim.updated_at = unix_timestamp();
write_static_delegate_claim_at(root, &claim)?;
}
Ok(claim.receipts)
}
fn acquire_static_delegate_claim_lock_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<AgentRuntimeTaskLock, String> {
let lock_id = static_delegate_claim_stem(parent_agent_id, parent_run_id, action_id);
try_acquire_game_creator_agent_delegation_lock_with_wait(root, &lock_id, "static-claim")?
.ok_or_else(|| format!("静态委派 claim 正在更新,请重试:{action_id}"))
}
fn acquire_static_delegate_delivery_locks_at(
root: &Path,
mut delivery_ids: Vec<String>,
) -> Result<Vec<AgentRuntimeTaskLock>, String> {
delivery_ids.sort();
delivery_ids.dedup();
let mut delivery_locks = Vec::with_capacity(delivery_ids.len());
for delegation_id in delivery_ids {
let delivery_lock = try_acquire_game_creator_agent_delegation_lock_with_wait(
root,
&delegation_id,
"static-delivery",
)?
.ok_or_else(|| format!("静态委派 delivery 正在更新,请重试认领:{delegation_id}"))?;
delivery_locks.push(delivery_lock);
}
Ok(delivery_locks)
}
pub(crate) fn active_static_delegate_delivery_count_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<usize, String> {
Ok(list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& matches!(
delivery.status,
StaticDelegateDeliveryStatus::Dispatched | StaticDelegateDeliveryStatus::Ready
)
})
.count())
}
pub(crate) fn static_delegate_target_agent_ids_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<Vec<String>, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
let mut target_agent_ids = list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& delivery.repair_of_delegation_id.is_none()
&& delivery.status != StaticDelegateDeliveryStatus::Suppressed
})
.map(|delivery| delivery.target_agent_id)
.collect::<Vec<_>>();
target_agent_ids.sort();
target_agent_ids.dedup();
Ok(target_agent_ids)
}
/// 唯一权威判据:某条 delivery 是否处于「等待用户澄清」状态——也就是说,从它出发的
/// 下一跳(若存在)应当被归类为澄清 continuation,而不是质量返工。
/// `validate_static_delegate_repair_request_at` 与
/// `validate_static_delegate_clarification_continuation_at` 共用这一个判据源,
/// 避免两处口径漂移;用户修订状态另由下方判据单独识别。
fn static_delegate_original_is_awaiting_clarification(
delivery: &StaticDelegateDeliveryRecord,
) -> bool {
delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
})
}
/// 唯一权威判据:某条 delivery 是否由用户审批的「修改」动作标记为待修订。
///
/// 该状态只由后续审批工作包写入;本包只让 lineage 重放认识它,不能自行生成或
/// 把其它状态静默映射成它。
/// 该原 delivery 是否正等着用户提出的修订(而不是质量返工)。
///
/// 用户修订和质量返工都带 `repairOfDelegationId`,但额度完全不同:`repair_depth`
/// 防的是 runaway agent,而用户修订每一轮都由人触发,人本身就是循环边界。委派 task
/// 末尾那句「你在这条链路上的位置」必须按这个判据分开渲染,否则用户第一次点修改就会
/// 被告知「这是唯一返工轮」。
pub(crate) fn static_delegate_original_awaits_user_revision_at(
root: &Path,
delegation_id: &str,
) -> Result<bool, String> {
Ok(read_static_delegate_delivery_at(root, delegation_id)?
.as_ref()
.is_some_and(static_delegate_original_is_user_revision_requested))
}
fn static_delegate_original_is_user_revision_requested(
delivery: &StaticDelegateDeliveryRecord,
) -> bool {
delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::UserRevisionRequested
})
}
/// A newer durable contract status is intentionally not a repairable contract.
/// The old client may preserve and report it, but must not manufacture a
/// mutation under semantics it does not understand.
fn static_delegate_original_has_unknown_contract_status(
delivery: &StaticDelegateDeliveryRecord,
) -> bool {
delivery
.structured_result
.as_ref()
.is_some_and(|result| result.contract_status.is_unknown())
}
/// 沿 repair_of_delegation_id 反向重放整条链,现算目标 delivery 的
/// (repair_depth, clarification_round)。两个维度都是运行时派生值,故意不落盘:
/// - 链根(repair_of_delegation_id 为 None):depth = 0,round = 0。
/// - 澄清跳(父节点处于 awaiting-clarification):round = parent.round + 1,
/// depth 原样继承(不重置),否则可以插一次澄清洗掉返工深度,变成无限返工。
/// - 用户修订跳(父节点为 UserRevisionRequested):depth/round 都原样继承,
/// 因为该跳由用户显式触发,不属于 runaway-agent 质量返工。
/// - 质量返工跳(父节点不处于上述两种状态):depth = parent.depth + 1,
/// round 重置为 0,因为返工后策划节点重新开工,不能因为返工吃掉预设的澄清轮次预算。
///
/// 防环 / 防越界:反向重放阶段一旦遇到重复 id、缺失的上游节点,或跳数超出
/// STATIC_DELEGATE_LINEAGE_MAX_HOPS,立即 fail closed,返回 (u32::MAX, u32::MAX),
/// 使调用方的深度门 / 轮次门必然拒绝,而不是静默放行。
pub(crate) fn static_delegate_lineage_counters(
deliveries: &[StaticDelegateDeliveryRecord],
delegation_id: &str,
) -> (u32, u32) {
let Some(mut chain) = static_delegate_lineage_nodes(deliveries, delegation_id) else {
return (u32::MAX, u32::MAX);
};
// chain 目前是 [目标 .. 根],反转成 [根 .. 目标] 便于按 R1/R2/R3 正向传播。
chain.reverse();
let mut depth = 0u32;
let mut round = 0u32;
for parent in &chain[..chain.len().saturating_sub(1)] {
if static_delegate_original_is_awaiting_clarification(*parent) {
round += 1;
} else if static_delegate_original_is_user_revision_requested(*parent) {
// 用户明确触发的修订不是 runaway-agent 返工;保留两个运行时派生计数。
} else {
depth += 1;
round = 0;
}
}
(depth, round)
}
/// Return whether the target's durable lineage contains a status introduced by
/// a newer client. A malformed lineage is an error rather than a negative
/// answer: callers that use this as a mutation/Provider gate must fail closed.
pub(crate) fn static_delegate_lineage_contains_unknown_contract_status(
deliveries: &[StaticDelegateDeliveryRecord],
delegation_id: &str,
) -> Result<bool, String> {
let chain = static_delegate_lineage_nodes(deliveries, delegation_id)
.ok_or_else(|| "静态委派谱系无效,无法检查未知 contractStatus".to_string())?;
Ok(chain.iter().any(|delivery| {
delivery
.structured_result
.as_ref()
.is_some_and(|result| result.contract_status.is_unknown())
}))
}
fn static_delegate_lineage_nodes<'a>(
deliveries: &'a [StaticDelegateDeliveryRecord],
delegation_id: &str,
) -> Option<Vec<&'a StaticDelegateDeliveryRecord>> {
let mut chain: Vec<&StaticDelegateDeliveryRecord> = Vec::new();
let mut seen_ids: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut current_id = delegation_id;
loop {
if !seen_ids.insert(current_id) || chain.len() >= STATIC_DELEGATE_LINEAGE_MAX_HOPS {
return None;
}
let Some(node) = deliveries
.iter()
.find(|delivery| delivery.delegation_id == current_id)
else {
return None;
};
chain.push(node);
match node.repair_of_delegation_id.as_deref() {
Some(parent_id) => current_id = parent_id,
None => break,
}
}
Some(chain)
}
pub(crate) fn static_delegate_clarification_round_limit_at(
_root: &Path,
_parent_agent_id: &str,
_parent_run_id: &str,
) -> Result<u32, String> {
Ok(STATIC_DELEGATE_CLARIFICATION_ROUND_LIMIT_DEFAULT)
}
pub(crate) fn validate_static_delegate_repair_request_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
candidate_delegation_id: &str,
target_agent_id: &str,
acceptance_criteria: &[String],
expected_artifacts: &[String],
repair_of_delegation_id: Option<&str>,
) -> Result<(), String> {
let Some(repair_of_delegation_id) = repair_of_delegation_id
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(());
};
validate_static_delegate_id(repair_of_delegation_id, "repairOfDelegationId", 160)?;
if repair_of_delegation_id == candidate_delegation_id {
return Err("静态委派 repairOfDelegationId 不能指向自己".to_string());
}
let deliveries = list_static_delegate_deliveries_at(root)?;
let original = deliveries
.iter()
.find(|delivery| delivery.delegation_id == repair_of_delegation_id)
.ok_or_else(|| {
format!("静态委派返工引用的原 delivery 不存在:{repair_of_delegation_id}")
})?;
if original.parent_agent_id != parent_agent_id || original.parent_run_id != parent_run_id {
return Err("静态委派返工必须留在同一 Supervisor 父 run".to_string());
}
if original.status != StaticDelegateDeliveryStatus::ClaimedByParent {
return Err("静态委派返工只能引用已由父 Agent 认领的原回执".to_string());
}
if original.target_agent_id != target_agent_id {
return Err("静态委派返工必须交回原专业 Agent".to_string());
}
if static_delegate_original_has_unknown_contract_status(original) {
return Err(
"静态委派原 delivery 的 contractStatus 由更新版本写入,当前版本拒绝返工".to_string(),
);
}
let (original_repair_depth, original_clarification_round) =
static_delegate_lineage_counters(&deliveries, &original.delegation_id);
if static_delegate_original_is_awaiting_clarification(original) {
let clarification_round_limit =
static_delegate_clarification_round_limit_at(root, parent_agent_id, parent_run_id)?;
if original_clarification_round >= clarification_round_limit {
return Err("静态委派澄清轮次已达上限".to_string());
}
} else if static_delegate_original_is_user_revision_requested(original) {
// 用户修订不消耗 repair_depth/clarification_round,但链上重放的 fail-closed
// 哨兵仍不能被绕过;否则损坏或成环的 durable lineage 可能被误放行。
if original_repair_depth == u32::MAX || original_clarification_round == u32::MAX {
return Err("静态委派谱系计数无效,拒绝继续".to_string());
}
} else if original_repair_depth >= 1 {
return Err("静态委派返工深度最多为 1".to_string());
}
if static_delegate_lineage_contains_unknown_contract_status(
&deliveries,
&original.delegation_id,
)
.map_err(|_| "静态委派谱系计数无效,拒绝继续".to_string())?
{
return Err(
"静态委派原 delivery 所在谱系含更新版本 contractStatus,当前版本拒绝返工".to_string(),
);
}
if original.acceptance_criteria != acceptance_criteria
|| original.expected_artifacts != expected_artifacts
{
return Err(
"静态委派返工必须完整继承原 acceptanceCriteria 和 expectedArtifacts".to_string(),
);
}
if deliveries.iter().any(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& delivery.repair_of_delegation_id.as_deref() == Some(repair_of_delegation_id)
&& delivery.delegation_id != candidate_delegation_id
&& delivery.status != StaticDelegateDeliveryStatus::Suppressed
}) {
return Err("同一静态委派最多允许一轮返工".to_string());
}
Ok(())
}
pub(crate) fn validate_static_delegate_clarification_continuation_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
input: &serde_json::Value,
repair_of_delegation_id: Option<&str>,
) -> Result<Option<String>, String> {
let input_text = |keys: &[&str]| -> String {
keys.iter()
.find_map(|key| input.get(*key).and_then(serde_json::Value::as_str))
.unwrap_or_default()
.trim()
.to_string()
};
let Some(repair_of_delegation_id) = repair_of_delegation_id
.map(str::trim)
.filter(|value| !value.is_empty())
else {
if !input_text(&[
"continuationOfDelegationId",
"continuation_of_delegation_id",
])
.is_empty()
|| !input_text(&["questionsSha256", "questions_sha256"]).is_empty()
|| !input_text(&["answersSha256", "answers_sha256"]).is_empty()
{
return Err(
"澄清 continuation 必须同时提交 repairOfDelegationId 并指向原 delivery".to_string(),
);
}
return Ok(None);
};
let continuation_of = input_text(&[
"continuationOfDelegationId",
"continuation_of_delegation_id",
]);
let input_questions_sha = input_text(&["questionsSha256", "questions_sha256"]);
let input_answers_sha = input_text(&["answersSha256", "answers_sha256"]);
let original =
read_static_delegate_delivery_at(root, repair_of_delegation_id)?.ok_or_else(|| {
format!("澄清 continuation 引用的原 delivery 不存在:{repair_of_delegation_id}")
})?;
let needs_user_input = static_delegate_original_is_awaiting_clarification(&original);
if !needs_user_input {
if !continuation_of.is_empty()
|| !input_questions_sha.is_empty()
|| !input_answers_sha.is_empty()
{
return Err("普通返工不能携带澄清 continuation 绑定".to_string());
}
return Ok(None);
}
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| original.parent_agent_id != parent_agent_id
|| original.parent_run_id != parent_run_id
|| original.status != StaticDelegateDeliveryStatus::ClaimedByParent
{
return Err(
"澄清 continuation 必须由认领原回执的 Project Supervisor 在同一父 run 创建".to_string(),
);
}
let questions_sha256 = original
.structured_result
.as_ref()
.and_then(|result| result.user_input_questions_sha256.as_deref())
.filter(|value| valid_static_delegate_sha256(value))
.ok_or_else(|| "澄清 continuation 的原 delivery 缺少问题指纹".to_string())?;
let answers_sha256 = original
.clarification_answers_sha256
.as_deref()
.filter(|value| valid_static_delegate_sha256(value))
.ok_or_else(|| "澄清 continuation 尚未取得该原 delivery 对应的用户回答".to_string())?;
let continuation_of = input_text(&[
"continuationOfDelegationId",
"continuation_of_delegation_id",
]);
if continuation_of != repair_of_delegation_id {
return Err("澄清 continuation 必须绑定原 delegationId".to_string());
}
// 两个指纹是可选的:原 delivery 已经唯一确定了它们,下面的 continuation identity
// 也只用这份权威值算,输入侧填了只是重复一遍。要求 Supervisor 手抄 128 个十六进制
// 字符没有任何信息增益,抄错却会一路打到硬失败,所以缺省时由 Runtime 自己补齐。
// 填了就仍然逐字校验——它能证明本轮续跑对应的确实是这次已回答的请求。
if (!input_questions_sha.is_empty() && input_questions_sha != questions_sha256)
|| (!input_answers_sha.is_empty() && input_answers_sha != answers_sha256)
{
return Err("澄清 continuation 的问题或答案指纹与已回答请求不一致".to_string());
}
let continuation_identity = format!(
"clarification-continuation-{:x}",
Sha256::digest(format!(
"{parent_run_id}\n{repair_of_delegation_id}\n{questions_sha256}\n{answers_sha256}"
))
);
Ok(Some(continuation_identity))
}
pub(crate) fn bind_static_delegate_clarification_answer_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
delegation_id: &str,
request_id: &str,
questions_sha256: &str,
answers_sha256: &str,
) -> Result<(), String> {
validate_static_delegate_id(delegation_id, "delegationId", 160)?;
validate_static_delegate_id(request_id, "requestId", 160)?;
if !valid_static_delegate_sha256(questions_sha256)
|| !valid_static_delegate_sha256(answers_sha256)
{
return Err("子 Agent 澄清回答缺少有效 SHA-256 绑定".to_string());
}
let _lock = try_acquire_game_creator_agent_delegation_lock_with_wait(
root,
delegation_id,
"static-clarification-answer",
)?
.ok_or_else(|| format!("静态委派澄清绑定正在更新:{delegation_id}"))?;
let mut delivery = read_static_delegate_delivery_at(root, delegation_id)?
.ok_or_else(|| format!("静态委派澄清原 delivery 不存在:{delegation_id}"))?;
let expected_questions_sha = delivery
.structured_result
.as_ref()
.filter(|result| result.contract_status == StaticDelegateContractStatus::NeedsUserInput)
.and_then(|result| result.user_input_questions_sha256.as_deref())
.ok_or_else(|| "静态委派澄清原 delivery 不是 needs-user-input".to_string())?;
if delivery.parent_agent_id != parent_agent_id
|| delivery.parent_run_id != parent_run_id
|| delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent
|| expected_questions_sha != questions_sha256
{
return Err("静态委派澄清回答与原 delivery 身份或问题指纹冲突".to_string());
}
match (
delivery.clarification_request_id.as_deref(),
delivery.clarification_answers_sha256.as_deref(),
) {
(None, None) => {
delivery.clarification_request_id = Some(request_id.to_string());
delivery.clarification_answers_sha256 = Some(answers_sha256.to_string());
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
Ok(())
}
(Some(existing_request), Some(existing_answers))
if existing_request == request_id && existing_answers == answers_sha256 =>
{
Ok(())
}
_ => Err("静态委派澄清回答已绑定到不同请求或答案".to_string()),
}
}
pub(crate) fn static_delegate_clarification_pending_matches_delivery_at(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<bool, String> {
if pending.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| pending.action.tool != GAME_CREATOR_USER_INPUT_REQUEST_TOOL
{
return Ok(false);
}
let Some(delegation_id) = agent_runtime_delegate_clarification_delegation_id(&pending.task)
else {
return Ok(false);
};
let Some(delivery) = read_static_delegate_delivery_at(root, delegation_id)? else {
return Ok(false);
};
let Some(result) = delivery.structured_result.as_ref().filter(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
&& result.user_input_questions_sha256.is_some()
}) else {
return Ok(false);
};
let questions = parse_game_creator_agent_user_input_questions(&pending.action.input)?;
let questions_sha256 = format!(
"{:x}",
Sha256::digest(
serde_json::to_vec(&questions)
.map_err(|error| format!("序列化 Supervisor 澄清问题失败:{error}"))?
)
);
Ok(delivery.parent_agent_id == pending.agent_id
&& delivery.parent_session_id == pending.session_id
&& delivery.parent_run_id == pending.run_id
&& delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.clarification_answers_sha256.is_none()
&& result.user_input_questions == questions
&& result.user_input_questions_sha256.as_deref() == Some(questions_sha256.as_str())
&& pending
.task
.contains(&format!("questionsSha256={questions_sha256}")))
}
fn valid_static_delegate_sha256(value: &str) -> bool {
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_static_delegate_structured_result_at(
root: &Path,
terminal_status: &str,
expected_artifacts: &[String],
verification_required: bool,
verification_status: Option<&str>,
verification_tool: Option<&str>,
verified_revision: Option<u64>,
error: Option<&str>,
) -> Result<StaticDelegateStructuredResult, String> {
let mut artifacts = Vec::new();
let mut missing_expected_artifacts = Vec::new();
for expected in expected_artifacts {
let normalized = normalize_static_delegate_expected_artifact(expected)?;
let path = resolve_local_project_path(root, &normalized)?;
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => metadata,
Ok(_) => {
missing_expected_artifacts.push(normalized);
continue;
}
Err(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => {
missing_expected_artifacts.push(normalized);
continue;
}
Err(io_error) => {
return Err(format!(
"读取静态委派 expected artifact 失败:{}: {io_error}",
path.display()
));
}
};
if metadata.len() > 512 * 1024 * 1024 {
return Err(format!("静态委派 expected artifact 过大:{normalized}"));
}
let mut file = File::open(&path)
.map_err(|io_error| format!("打开静态委派 artifact 失败:{normalized}: {io_error}"))?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).map_err(|io_error| {
format!("读取静态委派 artifact 失败:{normalized}: {io_error}")
})?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
artifacts.push(StaticDelegateArtifact {
path: normalized,
sha256: format!("{:x}", hasher.finalize()),
});
}
let verification_passed = !verification_required
|| (verification_status == Some("passed") && verified_revision.is_some());
let completed = terminal_status == "completed";
// 终态信封在这里解析,而这里是**投递回执时**——子 run 此刻已经终止,没有任何
// 一轮可以把解析错误回灌给它。早期实现在这一步直接 `?`,于是一次格式手滑
// (实测:option 对象里多写了一个 `id` 字段)就把整条委派判成投递失败,父
// Supervisor 直接进 needs-reconciliation 停下等人。
//
// 信封格式属于「本次 Provider 输出写错」,不是「durable 权威损坏」。
// 降级成 needs-repair 并把解析错误当返工理由
// 带上:Supervisor 用既有的一次返工额度就能让子 Agent 重写,不需要人工介入。
let (user_input_questions, user_input_questions_sha256, user_input_parse_error) =
match parse_static_delegate_user_input_request(error) {
Ok((questions, sha256)) => (questions, sha256, None),
Err(parse_error) => (None, None, Some(parse_error)),
};
let needs_user_input = completed && user_input_questions.is_some();
let contract_status = if needs_user_input {
StaticDelegateContractStatus::NeedsUserInput
} else if user_input_parse_error.is_some() {
StaticDelegateContractStatus::NeedsRepair
} else if completed && missing_expected_artifacts.is_empty() && verification_passed {
StaticDelegateContractStatus::EvidenceReady
} else {
StaticDelegateContractStatus::NeedsRepair
};
let mut evidence = Vec::new();
if verification_status == Some("passed") {
let kind = verification_tool.unwrap_or("project.verify").to_string();
evidence.push(StaticDelegateEvidence {
summary: format!(
"{kind} 已通过 revision {}",
verified_revision.unwrap_or_default()
),
kind,
path: None,
sha256: None,
});
}
let derived_error = if contract_status == StaticDelegateContractStatus::NeedsRepair {
// 坏信封的返工理由必须是**解析失败在哪**,而不是把那段无法解析的原文照抄
// 回去——后者对子 Agent 没有任何可操作信息。
user_input_parse_error
.map(|parse_error| {
redact_agent_runtime_error(
root,
&format!(
"AGC_NEEDS_USER_INPUT_V1 信封无法解析:{parse_error}。请严格按信封契约重写问题(questions 恰好一题,元素只含 id/header/question/options,option 只含 label/description),或者直接完成交付收束。"
),
500,
)
})
.or_else(|| error.map(|value| redact_agent_runtime_error(root, value, 500)))
.filter(|value| !value.trim().is_empty())
.or_else(|| {
if !completed {
Some(format!("专业 Agent 终态为 {terminal_status}"))
} else if !missing_expected_artifacts.is_empty() {
Some(format!(
"缺少 {} 个预期产物",
missing_expected_artifacts.len()
))
} else if !verification_passed {
Some("专业 Agent 修改尚无通过的 verification gate".to_string())
} else {
None
}
})
} else {
None
};
Ok(StaticDelegateStructuredResult {
contract_status,
artifacts,
missing_expected_artifacts,
verification_required,
verified_revision: verification_passed.then_some(verified_revision).flatten(),
evidence,
error: derived_error,
user_input_questions: user_input_questions.unwrap_or_default(),
user_input_questions_sha256,
})
}
/// 从信封载荷里切出第一个括号配平的 JSON 对象。
///
/// serde 的 from_str 要求整段输入就是一个值,尾部多一个字节就整包拒收。而模型在长
/// 嵌套 JSON 字符串的尾部会退化:27 个历史 run 的 68 条真实澄清信封里,有 2 条把信封
/// 写完整之后继续吐垃圾(「 马会」「સwerhu рҭ. 北京赛车? тру. [ ]」),信封本身
/// 一个字节都没坏,却和真正写坏的信封一样被判死,整条委派链停在 needs-repair。
///
/// 信封是终态协议载荷,配平的那个对象之后不存在任何协议内容,按配平定界即可。
/// 反过来,少写闭合符的那一类(同一批里 5 条,结尾是 `}]}` 而非 `}]}]}`)在这里
/// 仍然失败:那是模型真的没把结构写完,补括号只是替它猜一个它没表达的形状。
fn static_delegate_user_input_balanced_object(payload: &str) -> Result<&str, String> {
if !payload.starts_with('{') {
return Err("信封标记后不是 JSON 对象".to_string());
}
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
for (index, character) in payload.char_indices() {
if in_string {
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
in_string = false;
}
continue;
}
match character {
'"' => in_string = true,
'{' | '[' => depth += 1,
'}' | ']' => {
depth = depth
.checked_sub(1)
.ok_or_else(|| "信封 JSON 括号不闭合".to_string())?;
if depth == 0 {
return Ok(&payload[..index + character.len_utf8()]);
}
}
_ => {}
}
}
Err("信封 JSON 括号不闭合".to_string())
}
pub(crate) fn parse_static_delegate_user_input_request(
response: Option<&str>,
) -> Result<(Option<Vec<AgentRuntimeUserInputQuestion>>, Option<String>), String> {
let Some(response) = response.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok((None, None));
};
if !response.starts_with(STATIC_DELEGATE_USER_INPUT_PREFIX) {
return Ok((None, None));
}
let payload = response[STATIC_DELEGATE_USER_INPUT_PREFIX.len()..].trim();
// 长度上限约束的是协议载荷本身,所以按配平后的切片算:退化尾巴既然不进解析器,
// 也就不该替一条合法信封把通道撑爆。
let payload = static_delegate_user_input_balanced_object(payload)
.map_err(|reason| format!("子 Agent 用户澄清请求 JSON 无效:{reason}"))?;
if STATIC_DELEGATE_USER_INPUT_PREFIX.chars().count() + payload.chars().count()
> STATIC_DELEGATE_USER_INPUT_MAX_RESPONSE_CHARS
{
return Err("子 Agent 用户澄清请求超过回执长度上限".to_string());
}
let value = serde_json::from_str::<serde_json::Value>(payload)
.map_err(|error| format!("子 Agent 用户澄清请求 JSON 无效:{error}"))?;
let questions = parse_game_creator_agent_user_input_questions(&value)?;
let serialized = serde_json::to_vec(&questions)
.map_err(|error| format!("序列化子 Agent 用户澄清问题失败:{error}"))?;
let sha256 = format!("{:x}", Sha256::digest(serialized));
Ok((Some(questions), Some(sha256)))
}
pub(crate) fn suppress_static_delegate_deliveries_for_parent_terminal_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<(), String> {
let expected = list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id
&& delivery.parent_run_id == parent_run_id
&& matches!(
delivery.status,
StaticDelegateDeliveryStatus::Dispatched | StaticDelegateDeliveryStatus::Ready
)
})
.collect::<Vec<_>>();
let _delivery_locks = acquire_static_delegate_delivery_locks_at(
root,
expected
.iter()
.map(|delivery| delivery.delegation_id.clone())
.collect(),
)?;
for expected_delivery in expected {
let current = read_static_delegate_delivery_at(root, &expected_delivery.delegation_id)?
.ok_or_else(|| {
format!(
"父 run 终态清理时静态委派 delivery 丢失:{}",
expected_delivery.delegation_id
)
})?;
validate_static_delegate_delivery_identity(&current, &expected_delivery)?;
if matches!(
current.status,
StaticDelegateDeliveryStatus::Dispatched | StaticDelegateDeliveryStatus::Ready
) {
suppress_static_delegate_delivery_at(root, &current)?;
}
}
Ok(())
}
pub(crate) fn static_delegate_deliveries_for_parent_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<Vec<StaticDelegateDeliveryRecord>, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
let mut deliveries = list_static_delegate_deliveries_at(root)?
.into_iter()
.filter(|delivery| {
delivery.parent_agent_id == parent_agent_id && delivery.parent_run_id == parent_run_id
})
.collect::<Vec<_>>();
deliveries.sort_by(|left, right| left.delegation_id.cmp(&right.delegation_id));
Ok(deliveries)
}
pub(crate) fn claimed_static_delegate_deliveries_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<Vec<StaticDelegateDeliveryRecord>, String> {
Ok(
static_delegate_deliveries_for_parent_at(root, parent_agent_id, parent_run_id)?
.into_iter()
.filter(|delivery| delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent)
.collect(),
)
}
pub(crate) fn static_delegate_claim_exists_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<bool, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
validate_static_delegate_id(action_id, "actionId", 160)?;
read_static_delegate_claim_at(root, parent_agent_id, parent_run_id, action_id)
.map(|claim| claim.is_some())
}
pub(crate) fn read_static_delegate_delivery_at(
root: &Path,
delegation_id: &str,
) -> Result<Option<StaticDelegateDeliveryRecord>, String> {
validate_static_delegate_id(delegation_id, "delegationId", 160)?;
let relative_path = static_delegate_delivery_relative_path(delegation_id);
let Some(record) =
read_agent_runtime_json_sidecar_with_max_bytes::<StaticDelegateDeliveryRecord>(
root,
&relative_path,
"静态委派 delivery",
STATIC_DELEGATE_DELIVERY_MAX_BYTES,
)?
else {
return Ok(None);
};
validate_static_delegate_delivery_record(&record)?;
if record.delegation_id != delegation_id {
return Err("静态委派 delivery 文件名与记录身份不一致".to_string());
}
Ok(Some(record))
}
pub(crate) fn list_static_delegate_deliveries_at(
root: &Path,
) -> Result<Vec<StaticDelegateDeliveryRecord>, String> {
let dir = resolve_local_project_path(root, STATIC_DELEGATE_DELIVERY_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!("读取静态委派 delivery 目录失败:{error}")),
};
let mut ids = std::collections::BTreeSet::new();
for entry in entries {
let entry = entry.map_err(|error| format!("读取静态委派 delivery 条目失败:{error}"))?;
let path = entry.path();
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| "静态委派 delivery 文件名不是 UTF-8".to_string())?;
let id = if let Some(id) = file_name.strip_suffix(".json") {
Some(id)
} else {
file_name
.strip_prefix('.')
.and_then(|value| value.strip_suffix(".json.previous"))
};
let Some(id) = id else {
continue;
};
validate_static_delegate_id(id, "delegationId", 160)?;
ids.insert(id.to_string());
}
let mut records = Vec::with_capacity(ids.len());
for id in ids {
records.push(
read_static_delegate_delivery_at(root, &id)?
.ok_or_else(|| format!("静态委派 delivery 在枚举后消失:{id}"))?,
);
}
Ok(records)
}
fn list_static_delegate_claims_at(root: &Path) -> Result<Vec<StaticDelegateClaimRecord>, String> {
let dir = resolve_local_project_path(root, STATIC_DELEGATE_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!("读取静态委派 claim 目录失败:{error}")),
};
let mut file_stems = std::collections::BTreeSet::new();
for entry in entries {
let entry = entry.map_err(|error| format!("读取静态委派 claim 条目失败:{error}"))?;
let file_name = entry
.file_name()
.into_string()
.map_err(|_| "静态委派 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()..]
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err("静态委派 claim 文件名无效".to_string());
}
file_stems.insert(stem.to_string());
}
let mut claims = Vec::with_capacity(file_stems.len());
for stem in file_stems {
let relative_path = format!("{STATIC_DELEGATE_CLAIM_DIR}/{stem}.json");
let claim = read_agent_runtime_json_sidecar_with_max_bytes::<StaticDelegateClaimRecord>(
root,
&relative_path,
"静态委派 claim",
STATIC_DELEGATE_CLAIM_MAX_BYTES,
)?
.ok_or_else(|| format!("静态委派 claim 在枚举后消失:{stem}"))?;
validate_static_delegate_claim_record(&claim)?;
if static_delegate_claim_relative_path(
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
) != relative_path
{
return Err("静态委派 claim 文件名与记录身份不一致".to_string());
}
claims.push(claim);
}
Ok(claims)
}
pub(crate) fn write_static_delegate_delivery_at(
root: &Path,
record: &StaticDelegateDeliveryRecord,
) -> Result<(), String> {
validate_static_delegate_delivery_record(record)?;
write_agent_runtime_json_sidecar_with_max_bytes(
root,
&static_delegate_delivery_relative_path(&record.delegation_id),
"静态委派 delivery",
record,
STATIC_DELEGATE_DELIVERY_MAX_BYTES,
)
}
fn read_static_delegate_claim_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> Result<Option<StaticDelegateClaimRecord>, String> {
let relative_path =
static_delegate_claim_relative_path(parent_agent_id, parent_run_id, action_id);
let claim = read_agent_runtime_json_sidecar_with_max_bytes::<StaticDelegateClaimRecord>(
root,
&relative_path,
"静态委派 claim",
STATIC_DELEGATE_CLAIM_MAX_BYTES,
)?;
if let Some(claim) = &claim {
validate_static_delegate_claim_record(claim)?;
if claim.parent_agent_id != parent_agent_id
|| claim.parent_run_id != parent_run_id
|| claim.action_id != action_id
{
return Err("静态委派 claim 文件与请求身份不一致".to_string());
}
}
Ok(claim)
}
fn write_static_delegate_claim_at(
root: &Path,
claim: &StaticDelegateClaimRecord,
) -> Result<(), String> {
validate_static_delegate_claim_record(claim)?;
write_agent_runtime_json_sidecar_with_max_bytes(
root,
&static_delegate_claim_relative_path(
&claim.parent_agent_id,
&claim.parent_run_id,
&claim.action_id,
),
"静态委派 claim",
claim,
STATIC_DELEGATE_CLAIM_MAX_BYTES,
)
}
fn static_delegate_claim_relative_path(
parent_agent_id: &str,
parent_run_id: &str,
action_id: &str,
) -> String {
format!(
"{STATIC_DELEGATE_CLAIM_DIR}/{}.json",
static_delegate_claim_stem(parent_agent_id, parent_run_id, action_id)
)
}
fn static_delegate_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 static_delegate_delivery_relative_path(delegation_id: &str) -> String {
format!("{STATIC_DELEGATE_DELIVERY_DIR}/{delegation_id}.json")
}
fn validate_static_delegate_delivery_record(
record: &StaticDelegateDeliveryRecord,
) -> Result<(), String> {
if record.schema_version != STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION {
return Err("静态委派 delivery schemaVersion 不受支持".to_string());
}
validate_static_delegate_id(&record.parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(&record.parent_session_id, "parentSessionId", 160)?;
validate_static_delegate_id(&record.parent_run_id, "parentRunId", 160)?;
validate_static_delegate_id(&record.parent_action_id, "parentActionId", 160)?;
validate_static_delegate_id(&record.delegation_id, "delegationId", 160)?;
validate_static_delegate_id(&record.target_agent_id, "targetAgentId", 96)?;
if record.target_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| record.target_agent_id.starts_with("child-")
{
return Err(
"静态委派 targetAgentId 只能是静态专业 Agent,不能是 Supervisor 或动态 child"
.to_string(),
);
}
validate_static_delegate_id(&record.target_session_id, "targetSessionId", 160)?;
validate_static_delegate_id(&record.target_run_id, "targetRunId", 160)?;
validate_static_delegate_text_list(
&record.acceptance_criteria,
"acceptanceCriteria",
STATIC_DELEGATE_MAX_ACCEPTANCE_CRITERIA,
STATIC_DELEGATE_ACCEPTANCE_CRITERION_MAX_CHARS,
)?;
validate_static_delegate_expected_artifacts(&record.expected_artifacts)?;
if let Some(repair_of_delegation_id) = record.repair_of_delegation_id.as_deref() {
validate_static_delegate_id(repair_of_delegation_id, "repairOfDelegationId", 160)?;
if repair_of_delegation_id == record.delegation_id {
return Err("静态委派 repairOfDelegationId 不能指向自己".to_string());
}
}
if record.clarification_request_id.is_some() != record.clarification_answers_sha256.is_some()
|| record
.clarification_answers_sha256
.as_deref()
.is_some_and(|value| !valid_static_delegate_sha256(value))
{
return Err("静态委派 delivery 澄清回答绑定无效".to_string());
}
if let Some(request_id) = record.clarification_request_id.as_deref() {
validate_static_delegate_id(request_id, "clarificationRequestId", 160)?;
if record.status != StaticDelegateDeliveryStatus::ClaimedByParent
|| record.structured_result.as_ref().is_none_or(|result| {
result.contract_status != StaticDelegateContractStatus::NeedsUserInput
})
{
return Err("只有已认领 needs-user-input delivery 可以绑定澄清回答".to_string());
}
}
if record.status == StaticDelegateDeliveryStatus::Dispatched
&& (record.terminal_status.is_some()
|| record.result_summary.is_some()
|| record.structured_result.is_some()
|| record.claimed_by_action_id.is_some())
{
return Err("dispatched 静态委派 delivery 含非法终态字段".to_string());
}
if matches!(
record.status,
StaticDelegateDeliveryStatus::Ready | StaticDelegateDeliveryStatus::ClaimedByParent
) && (record.terminal_status.as_deref().is_none_or(str::is_empty)
|| record.result_summary.is_none())
{
return Err("ready 静态委派 delivery 缺少终态结果".to_string());
}
if matches!(
record.status,
StaticDelegateDeliveryStatus::Ready | StaticDelegateDeliveryStatus::ClaimedByParent
) && (!record.acceptance_criteria.is_empty()
|| !record.expected_artifacts.is_empty()
|| record.repair_of_delegation_id.is_some())
&& record.structured_result.is_none()
{
return Err("带合同的静态委派 delivery 缺少 structuredResult".to_string());
}
if let Some(result) = record.structured_result.as_ref() {
validate_static_delegate_structured_result(
result,
record.terminal_status.as_deref().unwrap_or_default(),
&record.expected_artifacts,
)?;
}
if record.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& record
.claimed_by_action_id
.as_deref()
.is_none_or(str::is_empty)
{
return Err("claimed 静态委派 delivery 缺少 actionId".to_string());
}
if record.status != StaticDelegateDeliveryStatus::ClaimedByParent
&& record.claimed_by_action_id.is_some()
{
return Err("未认领静态委派 delivery 不能保存 claimedByActionId".to_string());
}
Ok(())
}
fn validate_static_delegate_delivery_identity(
existing: &StaticDelegateDeliveryRecord,
expected: &StaticDelegateDeliveryRecord,
) -> Result<(), String> {
if existing.parent_agent_id != expected.parent_agent_id
|| existing.parent_session_id != expected.parent_session_id
|| existing.parent_run_id != expected.parent_run_id
|| existing.parent_action_id != expected.parent_action_id
|| existing.delegation_id != expected.delegation_id
|| existing.target_agent_id != expected.target_agent_id
|| existing.target_session_id != expected.target_session_id
|| existing.target_run_id != expected.target_run_id
|| existing.acceptance_criteria != expected.acceptance_criteria
|| existing.expected_artifacts != expected.expected_artifacts
|| existing.repair_of_delegation_id != expected.repair_of_delegation_id
{
return Err(format!(
"静态委派 delivery 身份冲突:{}",
expected.delegation_id
));
}
Ok(())
}
fn validate_static_delegate_claim_record(claim: &StaticDelegateClaimRecord) -> Result<(), String> {
if claim.schema_version != STATIC_DELEGATE_CLAIM_SCHEMA_VERSION {
return Err("静态委派 claim schemaVersion 不受支持".to_string());
}
validate_static_delegate_id(&claim.parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(&claim.parent_run_id, "parentRunId", 160)?;
validate_static_delegate_id(&claim.action_id, "actionId", 160)?;
if claim.receipts.is_empty() || claim.receipts.len() > 16 {
return Err("静态委派 claim 回执数量无效".to_string());
}
let mut delegation_ids = std::collections::BTreeSet::new();
for receipt in &claim.receipts {
validate_static_delegate_id(&receipt.delegation_id, "delegationId", 160)?;
validate_static_delegate_id(&receipt.target_agent_id, "targetAgentId", 96)?;
validate_static_delegate_text_list(
&receipt.acceptance_criteria,
"receipt.acceptanceCriteria",
STATIC_DELEGATE_MAX_ACCEPTANCE_CRITERIA,
STATIC_DELEGATE_ACCEPTANCE_CRITERION_MAX_CHARS,
)?;
validate_static_delegate_expected_artifacts(&receipt.expected_artifacts)?;
if let Some(repair_of_delegation_id) = receipt.repair_of_delegation_id.as_deref() {
validate_static_delegate_id(
repair_of_delegation_id,
"receipt.repairOfDelegationId",
160,
)?;
}
if let Some(result) = receipt.structured_result.as_ref() {
validate_static_delegate_structured_result(
result,
&receipt.status,
&receipt.expected_artifacts,
)?;
} else if !receipt.acceptance_criteria.is_empty()
|| !receipt.expected_artifacts.is_empty()
|| receipt.repair_of_delegation_id.is_some()
{
return Err("带合同的静态委派 claim 回执缺少 structuredResult".to_string());
}
if receipt.status.trim().is_empty()
|| receipt.status.chars().count() > 64
|| receipt.summary.chars().count() > 240
|| !delegation_ids.insert(receipt.delegation_id.as_str())
{
return Err("静态委派 claim 回执内容无效".to_string());
}
}
Ok(())
}
fn validate_static_delegate_claim_identity_and_receipts(
latest: &StaticDelegateClaimRecord,
expected: &StaticDelegateClaimRecord,
) -> Result<(), String> {
if latest.schema_version != expected.schema_version
|| latest.parent_agent_id != expected.parent_agent_id
|| latest.parent_run_id != expected.parent_run_id
|| latest.action_id != expected.action_id
|| latest.receipts != expected.receipts
{
return Err("静态委派 claim 身份或回执内容冲突".to_string());
}
Ok(())
}
fn validate_static_delegate_id(value: &str, label: &str, max_chars: usize) -> Result<(), String> {
if value.is_empty()
|| value.chars().count() > max_chars
|| value.contains("..")
|| value
.chars()
.any(|character| !(character.is_ascii_alphanumeric() || "-_.".contains(character)))
{
return Err(format!("{label} 不是安全标识"));
}
Ok(())
}
fn validate_static_delegate_text_list(
values: &[String],
label: &str,
max_items: usize,
max_chars: usize,
) -> Result<(), String> {
if values.len() > max_items {
return Err(format!("{label} 最多支持 {max_items} 项"));
}
let mut unique = std::collections::BTreeSet::new();
for value in values {
if value.trim() != value
|| value.is_empty()
|| value.chars().count() > max_chars
|| !unique.insert(value.as_str())
{
return Err(format!("{label} 含空白、重复或超限内容"));
}
}
Ok(())
}
fn normalize_static_delegate_expected_artifact(value: &str) -> Result<String, String> {
if value.contains('*') || value.contains('?') || value.contains('[') || value.contains(']') {
return Err("静态委派 expectedArtifacts 只接受精确文件路径,不接受 glob".to_string());
}
let normalized = normalize_relative_path(value)?;
if normalized != value.trim() {
return Err("静态委派 expectedArtifacts 必须是规范相对路径".to_string());
}
if normalized
.split('/')
.next()
.is_some_and(|part| part.eq_ignore_ascii_case(".agent"))
{
return Err("静态委派 expectedArtifacts 不能指向 .agent 控制面".to_string());
}
reject_sensitive_project_file_read(&normalized)?;
Ok(normalized)
}
fn validate_static_delegate_expected_artifacts(values: &[String]) -> Result<(), String> {
validate_static_delegate_text_list(
values,
"expectedArtifacts",
STATIC_DELEGATE_MAX_EXPECTED_ARTIFACTS,
STATIC_DELEGATE_EXPECTED_ARTIFACT_MAX_CHARS,
)?;
for value in values {
normalize_static_delegate_expected_artifact(value)?;
}
Ok(())
}
fn validate_static_delegate_structured_result(
result: &StaticDelegateStructuredResult,
terminal_status: &str,
expected_artifacts: &[String],
) -> Result<(), String> {
if result.artifacts.len() > STATIC_DELEGATE_MAX_EXPECTED_ARTIFACTS
|| result.missing_expected_artifacts.len() > STATIC_DELEGATE_MAX_EXPECTED_ARTIFACTS
|| result.evidence.len() > STATIC_DELEGATE_MAX_EVIDENCE
{
return Err("静态委派 structuredResult 数量超限".to_string());
}
let expected = expected_artifacts
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
let mut covered = std::collections::BTreeSet::new();
for artifact in &result.artifacts {
let path = normalize_static_delegate_expected_artifact(&artifact.path)?;
if !expected.contains(&path)
|| !covered.insert(path)
|| artifact.sha256.len() != 64
|| !artifact
.sha256
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err("静态委派 structuredResult artifact 无效".to_string());
}
}
for path in &result.missing_expected_artifacts {
let path = normalize_static_delegate_expected_artifact(path)?;
if !expected.contains(&path) || !covered.insert(path) {
return Err("静态委派 structuredResult missing artifact 无效".to_string());
}
}
if covered != expected {
return Err("静态委派 structuredResult 未覆盖全部 expectedArtifacts".to_string());
}
if result.verified_revision == Some(0) {
return Err("静态委派 verifiedRevision 必须大于 0".to_string());
}
match &result.contract_status {
StaticDelegateContractStatus::EvidenceReady
| StaticDelegateContractStatus::UserRevisionRequested => {
if terminal_status != "completed"
|| !result.missing_expected_artifacts.is_empty()
|| (result.verification_required && result.verified_revision.is_none())
{
return Err(
"静态委派 evidence-ready/user-revision-requested 与客观证据冲突".to_string(),
);
}
}
StaticDelegateContractStatus::NeedsUserInput => {
if terminal_status != "completed"
|| result.user_input_questions.is_empty()
|| result.user_input_questions.len() > 3
{
return Err("静态委派 needs-user-input 与终态或问题数量冲突".to_string());
}
let expected_sha = serde_json::to_vec(&result.user_input_questions)
.map(|bytes| format!("{:x}", Sha256::digest(bytes)))
.map_err(|error| format!("序列化静态委派用户问题失败:{error}"))?;
if result.user_input_questions_sha256.as_deref() != Some(expected_sha.as_str()) {
return Err("静态委派 needs-user-input 问题指纹无效".to_string());
}
}
StaticDelegateContractStatus::NeedsRepair | StaticDelegateContractStatus::Unknown(_) => {}
}
if !matches!(
result.contract_status,
StaticDelegateContractStatus::NeedsUserInput
) && (!result.user_input_questions.is_empty()
|| result.user_input_questions_sha256.is_some())
{
return Err("非 needs-user-input 静态委派不能携带用户问题".to_string());
}
if result
.error
.as_ref()
.is_some_and(|error| error.trim().is_empty() || error.chars().count() > 500)
{
return Err("静态委派 structuredResult error 无效".to_string());
}
for item in &result.evidence {
if item.kind.trim().is_empty()
|| item.kind.chars().count() > 96
|| item.summary.trim().is_empty()
|| item.summary.chars().count() > 500
{
return Err("静态委派 structuredResult evidence 无效".to_string());
}
if let Some(path) = item.path.as_deref() {
normalize_relative_path(path)?;
}
if item.sha256.as_ref().is_some_and(|sha256| {
sha256.len() != 64
|| !sha256
.chars()
.all(|character| character.is_ascii_hexdigit())
}) {
return Err("静态委派 structuredResult evidence sha256 无效".to_string());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// 中转通道和澄清问询 schema 之间隔着一个字符数上限,两边没有任何东西相连。
/// 通道窄于 schema 的代价不是「问询被截断」:子 Agent 提了一个完全合法的问题,
/// Runtime 会在父 run 认领回执时整包拒收,委派链就此阻断——现场实测一问三选的
/// 正常中文问询是 501 字符,而当时的上限恰好是 500。
///
/// 所以这里锁的是**包含关系**:schema 允许的最大合法问询,必须能过通道。
#[test]
fn user_input_relay_channel_admits_the_largest_schema_legal_request() {
let long_text = |count: usize| "问".repeat(count);
let questions = (0..3)
.map(|index| {
serde_json::json!({
"id": format!("q{index}{}", "a".repeat(60)),
"header": long_text(12),
"question": long_text(400),
"options": (0..3)
.map(|option| serde_json::json!({
"label": format!("{option}{}", long_text(59)),
"description": long_text(240),
}))
.collect::<Vec<_>>(),
})
})
.collect::<Vec<_>>();
let response = format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({ "questions": questions })
);
assert!(
response.chars().count() <= STATIC_DELEGATE_USER_INPUT_MAX_RESPONSE_CHARS,
"schema 允许的最大问询 {} 字符超过了中转通道上限 {}",
response.chars().count(),
STATIC_DELEGATE_USER_INPUT_MAX_RESPONSE_CHARS
);
let (parsed, sha256) = parse_static_delegate_user_input_request(Some(&response))
.expect("largest schema-legal clarification must pass the relay channel");
assert_eq!(parsed.expect("questions").len(), 3);
assert!(sha256.is_some_and(|value| value.len() == 64));
}
/// 澄清信封走的是 error 文本通道,会被按普通错误消息截断。现场子 Agent 的真实
/// 输出 521 字符,截到 500 再补一个省略号正好 501——JSON 拦腰断在末尾,父 run
/// 解析失败停在 needs-reconciliation。放宽拒收上限治不了这个:载荷在到达解析器
/// 之前就已经被切了。
#[test]
fn truncating_a_clarification_envelope_makes_it_unparseable() {
let response = format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({
"questions": [{
"id": "core_loop",
"header": "第1轮·核心",
"question": "影子能力在首个可玩闭环里承担什么作用?这决定关卡布局与原型优先级,也决定第一批谜题按什么规则组合。",
"options": [
{ "label": "A · 暗影分身", "description": "影子沿地面或墙面独立移动,可压机关、挡感应光、穿窄缝;规则直观,代价是要处理可达范围与回收。" },
{ "label": "B · 暗影桥梁", "description": "调整光源与站位让影子延展成短暂平台或连接导电点;偏空间构图,代价是碰撞与落脚可读性更严格。" },
],
}]
})
);
// 完整信封能过通道。
parse_static_delegate_user_input_request(Some(&response)).expect("intact envelope parses");
// 同一条信封被截断后必然解析失败——这正是现场那条 needs-reconciliation。
let truncated: String = response
.chars()
.take(response.chars().count() - 20)
.collect();
let error = parse_static_delegate_user_input_request(Some(&truncated))
.expect_err("a truncated envelope must not parse");
assert!(error.contains("JSON 无效"), "unexpected error: {error}");
}
#[test]
fn an_envelope_missing_its_closing_brackets_still_fails() {
// verify-farm-2 现场原文,结尾是 `}]}` 而非 `}]}]}`。
let response = concat!(
"AGC_NEEDS_USER_INPUT_V1\n",
r#"{"questions":[{"id":"core_loop_goal","header":"第1轮·当前要决定:一局里玩家靠什么目标获得满足","question":"现在先定核心闭环,才能控制 MVP范围。","options":[{"label":"A · 推荐:短周期订单经营","description":"围绕播种、收获、加工并完成限时订单推进;目标清晰、反馈快,代价是自由建造与长期规划较少。"},{"label":"B · 自主农场成长","description":"围绕规划田地、逐步扩建并达成阶段里程碑;沉浸和成长感更强,代价是前期目标反馈较慢、系统边界更难控。"},{"label":"需要原型验证","description":"制作 30~90 分钟微型原型,包含种植、收获和一种目标;让 2~3 名目标玩家试玩,观察是否理解目标、是否愿意继续一轮;通过标准是多数玩家无需讲解即可完成闭环并主动开始第二轮。"}]}"#,
);
let error = parse_static_delegate_user_input_request(Some(response))
.expect_err("an envelope that stops short of closing must not parse");
assert!(error.contains("括号不闭合"), "unexpected error: {error}");
}
/// 定界只认字符串外的括号。信封正文里出现的括号字符必须被跳过,否则一条完全
/// 合法的信封会因为问题文案里写了 `}` 而被提前切断。
#[test]
fn balanced_object_scanning_ignores_brackets_inside_strings() {
let response = format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({
"questions": [{
"id": "brace_heavy",
"header": "括号",
"question": "存档格式写成 {\"slot\": [1]} 还是二进制?",
"options": [
{"label": "A · JSON", "description": "形如 {\"slot\": [1]} 的文本存档,可读但体积大。"},
{"label": "B · 二进制", "description": "紧凑但要自己写工具才能看,形如 ]}]} 的字节序列。"}
]
}]
})
);
let (questions, _) = parse_static_delegate_user_input_request(Some(&response))
.expect("brackets inside strings must not terminate the scan");
assert_eq!(questions.expect("questions present").len(), 1);
}
#[test]
fn static_delegate_target_agent_ids_include_claimed_and_exclude_suppressed_or_repair() {
let root = std::env::temp_dir().join(format!(
"genarrative-static-targets-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "project-1", "静态委派目标汇总测试")
.expect("project init");
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
let parent_run_id = "target-summary-parent-run";
bind_supervisor_collaboration_policy_snapshot_at(
&root,
parent_agent_id,
parent_run_id,
&SupervisorCollaborationPolicy::default(),
"legacy-current-project-policy",
)
.expect("bind collaboration snapshot");
let claimed = new_static_delegate_delivery(
parent_agent_id,
"target-summary-parent-session",
parent_run_id,
"target-summary-claimed-action",
"target-summary-claimed-delivery",
"design-director",
"target-summary-claimed-session",
"target-summary-claimed-run",
);
create_or_read_static_delegate_delivery_at(&root, &claimed).expect("create claimed");
mark_static_delegate_delivery_ready_at(
&root,
&claimed.target_agent_id,
&claimed.target_session_id,
&claimed.target_run_id,
&claimed.delegation_id,
"completed",
"设计交付完成",
)
.expect("mark claimed ready");
claim_ready_static_delegate_receipts_at(
&root,
parent_agent_id,
parent_run_id,
"target-summary-claim-action",
)
.expect("claim delivery");
for delivery in [
new_static_delegate_delivery(
parent_agent_id,
"target-summary-parent-session",
parent_run_id,
"target-summary-art-action",
"target-summary-art-delivery",
"art-director",
"target-summary-art-session",
"target-summary-art-run",
),
new_static_delegate_delivery(
parent_agent_id,
"target-summary-parent-session",
parent_run_id,
"target-summary-duplicate-action",
"target-summary-duplicate-delivery",
"design-director",
"target-summary-duplicate-session",
"target-summary-duplicate-run",
),
] {
create_or_read_static_delegate_delivery_at(&root, &delivery)
.expect("create included delivery");
}
let suppressed = new_static_delegate_delivery(
parent_agent_id,
"target-summary-parent-session",
parent_run_id,
"target-summary-suppressed-action",
"target-summary-suppressed-delivery",
"code-prototype",
"target-summary-suppressed-session",
"target-summary-suppressed-run",
);
let suppressed = create_or_read_static_delegate_delivery_at(&root, &suppressed)
.expect("create suppressed");
suppress_static_delegate_delivery_at(&root, &suppressed).expect("suppress delivery");
let repair = new_static_delegate_delivery_with_contract(
parent_agent_id,
"target-summary-parent-session",
parent_run_id,
"target-summary-repair-action",
"target-summary-repair-delivery",
"repair-only-agent",
"target-summary-repair-session",
"target-summary-repair-run",
&[],
&[],
Some(&claimed.delegation_id),
);
create_or_read_static_delegate_delivery_at(&root, &repair).expect("create repair");
assert_eq!(
static_delegate_target_agent_ids_at(&root, parent_agent_id, parent_run_id)
.expect("read target agent ids"),
vec!["art-director".to_string(), "design-director".to_string()]
);
fs::remove_dir_all(root).ok();
}
#[test]
fn static_delegate_user_input_envelope_is_structured_and_fingerprinted() {
let response = format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({
"questions": [{
"id": "target_platform",
"header": "平台",
"question": "主要运行在哪里?",
"options": [
{"label": "Web", "description": "浏览器运行"},
{"label": "移动端", "description": "手机或平板运行"}
]
}]
})
);
let (questions, sha256) = parse_static_delegate_user_input_request(Some(&response))
.expect("valid child clarification envelope");
let questions = questions.expect("questions present");
assert_eq!(questions.len(), 1);
assert_eq!(sha256.as_deref().map(str::len), Some(64));
let result = build_static_delegate_structured_result_at(
&std::env::temp_dir(),
"completed",
&[],
false,
None,
None,
None,
Some(&response),
)
.expect("build needs-user-input result");
assert_eq!(
result.contract_status,
StaticDelegateContractStatus::NeedsUserInput
);
validate_static_delegate_structured_result(&result, "completed", &[])
.expect("needs-user-input result validates");
}
#[test]
fn static_delegate_user_input_envelope_fails_closed_when_malformed() {
let error = parse_static_delegate_user_input_request(Some(
"AGC_NEEDS_USER_INPUT_V1\n{\"questions\":[]}",
))
.expect_err("empty question envelope must fail");
assert!(error.contains("user.input_request"));
}
/// 坏信封是 Provider 输出质量问题,不是 durable 权威损坏。它必须变成一次可返工
/// 的 needs-repair,而不是把整条委派判成投递失败、把父 Supervisor 推进
/// needs-reconciliation——解析发生在子 run 终止之后,那条路上没有任何一轮能自愈。
#[test]
fn a_malformed_user_input_envelope_degrades_to_needs_repair_with_the_parse_reason() {
// 实测形态:option 对象里多写了一个 `id` 字段。
let response = concat!(
"AGC_NEEDS_USER_INPUT_V1\n",
"{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·当前要决定:核心闭环形状\",",
"\"question\":\"它决定首个可玩闭环长什么样。\",\"options\":[",
"{\"id\":\"a\",\"label\":\"A · 甲方案\",\"description\":\"甲方案的后果\"},",
"{\"id\":\"b\",\"label\":\"B · 乙方案\",\"description\":\"乙方案的后果\"},",
"{\"id\":\"c\",\"label\":\"需要原型验证\",\"description\":\"做个微型原型看看\"}]}]}"
);
parse_static_delegate_user_input_request(Some(response))
.expect_err("an option carrying an extra field is still a malformed envelope");
let result = build_static_delegate_structured_result_at(
&std::env::temp_dir(),
"completed",
&[],
false,
None,
None,
None,
Some(response),
)
.expect("a malformed envelope must not fail the whole delivery");
assert_eq!(
result.contract_status,
StaticDelegateContractStatus::NeedsRepair
);
assert!(result.user_input_questions.is_empty());
assert!(result.user_input_questions_sha256.is_none());
let reason = result.error.as_deref().expect("a repair reason is derived");
// 返工理由要说清错在哪,而不是把那段无法解析的原文照抄回去。
assert!(reason.contains("AGC_NEEDS_USER_INPUT_V1 信封无法解析"));
assert!(reason.contains("label"));
validate_static_delegate_structured_result(&result, "completed", &[])
.expect("the degraded result still validates");
}
#[test]
fn stale_prepared_claim_snapshot_cannot_downgrade_observed_claim() {
let root = std::env::temp_dir().join(format!(
"genarrative-static-claim-monotonic-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "project-1", "静态委派 claim 单调状态测试")
.expect("project init");
let parent_run_id = "claim-monotonic-parent-run";
let claim_action_id = "claim-monotonic-action";
bind_supervisor_collaboration_policy_snapshot_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
&SupervisorCollaborationPolicy::default(),
"legacy-current-project-policy",
)
.expect("bind collaboration snapshot");
let delivery = new_static_delegate_delivery(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"claim-monotonic-parent-session",
parent_run_id,
"claim-monotonic-delegate-action",
"claim-monotonic-delivery",
"design-director",
"claim-monotonic-child-session",
"claim-monotonic-child-run",
);
create_or_read_static_delegate_delivery_at(&root, &delivery).expect("create delivery");
mark_static_delegate_delivery_ready_at(
&root,
&delivery.target_agent_id,
&delivery.target_session_id,
&delivery.target_run_id,
&delivery.delegation_id,
"completed",
"设计结论已完成",
)
.expect("mark delivery ready");
claim_ready_static_delegate_receipts_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
claim_action_id,
)
.expect("claim receipt");
let mut stale = read_static_delegate_claim_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
claim_action_id,
)
.expect("read committed claim")
.expect("committed claim exists");
stale.status = StaticDelegateClaimStatus::Prepared;
mark_static_delegate_claim_observed_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
claim_action_id,
)
.expect("mark claim observed");
commit_static_delegate_claim_at(&root, stale).expect("replay stale prepared claim");
let latest = read_static_delegate_claim_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
claim_action_id,
)
.expect("read latest claim")
.expect("latest claim exists");
assert_eq!(latest.status, StaticDelegateClaimStatus::Observed);
assert!(static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
)
.expect("read final barrier")
.is_clear());
fs::remove_dir_all(root).ok();
}
#[test]
fn missing_claim_journal_recovers_required_receipt_before_new_ready_prefix() {
let root = std::env::temp_dir().join(format!(
"genarrative-static-required-recovery-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "project-1", "静态委派必选回执恢复测试")
.expect("project init");
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
let parent_run_id = "required-recovery-parent-run";
let action_id = "required-recovery-claim-action";
bind_supervisor_collaboration_policy_snapshot_at(
&root,
parent_agent_id,
parent_run_id,
&SupervisorCollaborationPolicy::default(),
"legacy-current-project-policy",
)
.expect("bind collaboration snapshot");
let required = new_static_delegate_delivery(
parent_agent_id,
"required-recovery-parent-session-with-long-identity",
parent_run_id,
"required-recovery-delegate-action-with-long-identity",
"zz-required-recovery-delivery-with-long-identity",
"design-director",
"required-recovery-child-session-with-long-identity",
"required-recovery-child-run-with-long-identity",
);
create_or_read_static_delegate_delivery_at(&root, &required)
.expect("create required delivery");
mark_static_delegate_delivery_ready_at(
&root,
&required.target_agent_id,
&required.target_session_id,
&required.target_run_id,
&required.delegation_id,
"completed",
"必须优先恢复的已认领回执",
)
.expect("mark required delivery ready");
let required_receipts = claim_ready_static_delegate_receipts_at(
&root,
parent_agent_id,
parent_run_id,
action_id,
)
.expect("create original required claim");
assert_eq!(required_receipts.len(), 1);
fs::remove_file(root.join(static_delegate_claim_relative_path(
parent_agent_id,
parent_run_id,
action_id,
)))
.expect("remove claim journal to simulate torn legacy state");
let optional = new_static_delegate_delivery(
parent_agent_id,
"p",
parent_run_id,
"a",
"aa-new-ready",
"art-director",
"s",
"r",
);
create_or_read_static_delegate_delivery_at(&root, &optional)
.expect("create optional ready delivery");
mark_static_delegate_delivery_ready_at(
&root,
&optional.target_agent_id,
&optional.target_session_id,
&optional.target_run_id,
&optional.delegation_id,
"completed",
"新回执",
)
.expect("mark optional delivery ready");
let required_budget = serialize_static_delegate_ready_receipts_payload(&required_receipts)
.expect("serialize required receipt")
.chars()
.count();
let optional_lock = try_acquire_game_creator_agent_delegation_lock_with_wait(
&root,
&optional.delegation_id,
"static-delivery",
)
.expect("acquire deferred optional delivery lock")
.expect("deferred optional delivery lock available");
let recovered = claim_ready_static_delegate_receipts_with_budget_at(
&root,
parent_agent_id,
parent_run_id,
action_id,
required_budget,
)
.expect("recover required receipt without consuming optional prefix");
drop(optional_lock);
assert_eq!(recovered, required_receipts);
let deferred = read_static_delegate_delivery_at(&root, &optional.delegation_id)
.expect("read deferred optional delivery")
.expect("deferred optional delivery exists");
assert_eq!(deferred.status, StaticDelegateDeliveryStatus::Ready);
assert!(deferred.claimed_by_action_id.is_none());
fs::remove_dir_all(root).ok();
}
fn claimed_static_delegate_for_lineage_test(
parent_run_id: &str,
delegation_id: &str,
repair_of_delegation_id: Option<&str>,
contract_status: StaticDelegateContractStatus,
) -> StaticDelegateDeliveryRecord {
let mut delivery = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-lineage-parent-session",
parent_run_id,
&format!("{delegation_id}-action"),
delegation_id,
"design-director",
&format!("{delegation_id}-session"),
&format!("{delegation_id}-run"),
&[],
&[],
repair_of_delegation_id,
);
let mut result = StaticDelegateStructuredResult::default();
result.contract_status = contract_status;
delivery.status = StaticDelegateDeliveryStatus::ClaimedByParent;
delivery.terminal_status = Some("completed".to_string());
delivery.result_summary = Some("M1C-0 lineage test".to_string());
delivery.structured_result = Some(result);
delivery.claimed_by_action_id = Some(format!("{delegation_id}-claim-action"));
delivery
}
#[test]
fn static_delegate_user_revision_preserves_counters_and_bypasses_depth_gate() {
let root = std::env::temp_dir().join(format!(
"genarrative-static-user-revision-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "m1c0-user-revision", "M1C-0 用户修订分类测试")
.expect("project init");
let parent_run_id = "m1c0-user-revision-parent-run";
let root_delivery = claimed_static_delegate_for_lineage_test(
parent_run_id,
"m1c0-user-revision-d0",
None,
StaticDelegateContractStatus::NeedsRepair,
);
write_static_delegate_delivery_at(&root, &root_delivery).expect("write root delivery");
// 先证明做游戏链路的普通返工仍然受 depth=1 门限制。
let mut first_revision = claimed_static_delegate_for_lineage_test(
parent_run_id,
"m1c0-user-revision-d1",
Some(&root_delivery.delegation_id),
StaticDelegateContractStatus::NeedsRepair,
);
write_static_delegate_delivery_at(&root, &first_revision)
.expect("write first repair delivery");
let ordinary_repair_error = validate_static_delegate_repair_request_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
"m1c0-user-revision-d2-ordinary",
"design-director",
&[],
&[],
Some(&first_revision.delegation_id),
)
.expect_err("ordinary repair at depth=1 must remain blocked");
assert!(ordinary_repair_error.contains("深度最多为 1"));
// 同一节点被用户审批标记为修订后,第一次用户修订不应再被当作质量返工。
first_revision
.structured_result
.as_mut()
.expect("first revision structured result")
.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
write_static_delegate_delivery_at(&root, &first_revision)
.expect("rewrite first delivery as user revision");
let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries");
assert_eq!(
static_delegate_lineage_counters(&deliveries, &first_revision.delegation_id),
(1, 0),
"用户修订跳的父节点已有一次普通返工,两个计数仍应保持 (1,0)"
);
validate_static_delegate_repair_request_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
"m1c0-user-revision-d2",
"design-director",
&[],
&[],
Some(&first_revision.delegation_id),
)
.expect("user revision must bypass the depth=1 quality-repair gate");
// 连续第二次用户修订仍挂在同一 lineage 上,不能把任一计数重新解释成返工。
let second_revision = claimed_static_delegate_for_lineage_test(
parent_run_id,
"m1c0-user-revision-d2",
Some(&first_revision.delegation_id),
StaticDelegateContractStatus::UserRevisionRequested,
);
write_static_delegate_delivery_at(&root, &second_revision)
.expect("write second user revision delivery");
let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries");
assert_eq!(
static_delegate_lineage_counters(&deliveries, &second_revision.delegation_id),
(1, 0),
"连续用户修订不能增加 repair_depth,也不能重置 clarification_round"
);
validate_static_delegate_repair_request_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
"m1c0-user-revision-d3",
"design-director",
&[],
&[],
Some(&second_revision.delegation_id),
)
.expect("a second consecutive user revision must remain admissible");
fs::remove_dir_all(root).ok();
}
#[test]
fn static_delegate_user_revision_parent_hop_preserves_depth_and_clarification_round() {
let mut quality_root = new_static_delegate_delivery(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-counter-session",
"m1c0-counter-run",
"m1c0-counter-d0-action",
"m1c0-counter-d0",
"design-director",
"m1c0-counter-d0-session",
"m1c0-counter-d0-run",
);
let mut quality_result = StaticDelegateStructuredResult::default();
quality_result.contract_status = StaticDelegateContractStatus::NeedsRepair;
quality_root.structured_result = Some(quality_result);
let mut clarification = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-counter-session",
"m1c0-counter-run",
"m1c0-counter-d1-action",
"m1c0-counter-d1",
"design-director",
"m1c0-counter-d1-session",
"m1c0-counter-d1-run",
&[],
&[],
Some("m1c0-counter-d0"),
);
let mut clarification_result = StaticDelegateStructuredResult::default();
clarification_result.contract_status = StaticDelegateContractStatus::NeedsUserInput;
clarification.structured_result = Some(clarification_result);
let mut user_revision = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-counter-session",
"m1c0-counter-run",
"m1c0-counter-d2-action",
"m1c0-counter-d2",
"design-director",
"m1c0-counter-d2-session",
"m1c0-counter-d2-run",
&[],
&[],
Some("m1c0-counter-d1"),
);
let mut user_revision_result = StaticDelegateStructuredResult::default();
user_revision_result.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
user_revision.structured_result = Some(user_revision_result);
let mut deliveries = vec![quality_root, clarification, user_revision];
// 对照组,不是主张:计数循环只遍历 chain[..len-1](父节点集合),目标节点自身的
// status 从不参与判定。所以「目标是 UserRevisionRequested」这条断言与新分支无关,
// 单靠它证明不了用户修订分类。
assert_eq!(
static_delegate_lineage_counters(&deliveries, "m1c0-counter-d2"),
(1, 1),
"目标自身是用户修订时,两个计数只由其父链决定"
);
// 真正打到新分支的是「父节点为 UserRevisionRequested」的下一跳。
let continuation = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-counter-session",
"m1c0-counter-run",
"m1c0-counter-d3-action",
"m1c0-counter-d3",
"design-director",
"m1c0-counter-d3-session",
"m1c0-counter-d3-run",
&[],
&[],
Some("m1c0-counter-d2"),
);
deliveries.push(continuation);
assert_eq!(
static_delegate_lineage_counters(&deliveries, "m1c0-counter-d3"),
(1, 1),
"父节点是用户修订时,depth 与 clarification_round 都必须原样继承"
);
// 反证:同一条链只把父节点改回普通质量返工,上一条必须变成 (2, 0)。缺了这条,
// 上一条断言在新分支被删掉后依然成立(走 else 支得到 (2, 0) 才会失败),
// 但没有对照就看不出它究竟钉住了什么。
deliveries[2]
.structured_result
.as_mut()
.expect("d2 structured result")
.contract_status = StaticDelegateContractStatus::NeedsRepair;
assert_eq!(
static_delegate_lineage_counters(&deliveries, "m1c0-counter-d3"),
(2, 0),
"父节点不是用户修订时必须回到质量返工分类"
);
}
#[test]
fn static_delegate_user_revision_barrier_blocks_every_revision_until_continuation() {
let root = std::env::temp_dir().join(format!(
"genarrative-static-user-revision-barrier-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(
&root,
"m1c1-user-revision-barrier",
"M1C-1 用户修订 barrier 测试",
)
.expect("project init");
let parent_run_id = "m1c1-user-revision-barrier-parent-run";
let first = claimed_static_delegate_for_lineage_test(
parent_run_id,
"m1c1-user-revision-barrier-first",
None,
StaticDelegateContractStatus::EvidenceReady,
);
write_static_delegate_delivery_at(&root, &first).expect("write first delivery");
mark_static_delegate_delivery_user_revision_requested_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
&first.delegation_id,
)
.expect("mark first delivery for user revision");
let first_barrier = static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
)
.expect("read first revision barrier");
assert_eq!(first_barrier.user_revision_pending_count, 1);
assert!(!first_barrier.is_clear());
assert!(first_barrier.has_waiting());
assert!(first_barrier.detail().contains("userRevisionPending=1"));
let mut continuation = claimed_static_delegate_for_lineage_test(
parent_run_id,
"m1c1-user-revision-barrier-continuation",
Some(&first.delegation_id),
StaticDelegateContractStatus::EvidenceReady,
);
continuation.status = StaticDelegateDeliveryStatus::Dispatched;
continuation.terminal_status = None;
continuation.result_summary = None;
continuation.structured_result = None;
continuation.claimed_by_action_id = None;
write_static_delegate_delivery_at(&root, &continuation).expect("write continuation");
let active_barrier = static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
)
.expect("read active continuation barrier");
assert_eq!(active_barrier.user_revision_pending_count, 0);
assert_eq!(active_barrier.waiting_count, 1);
continuation.status = StaticDelegateDeliveryStatus::ClaimedByParent;
continuation.terminal_status = Some("completed".to_string());
continuation.result_summary = Some("second revision candidate".to_string());
continuation.structured_result = Some(StaticDelegateStructuredResult {
contract_status: StaticDelegateContractStatus::EvidenceReady,
..StaticDelegateStructuredResult::default()
});
continuation.claimed_by_action_id =
Some("m1c1-user-revision-barrier-continuation-claim".to_string());
write_static_delegate_delivery_at(&root, &continuation)
.expect("complete continuation delivery");
assert!(
static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
)
.expect("read completed continuation barrier")
.is_clear(),
"the previous revision is satisfied once its continuation is claimed"
);
mark_static_delegate_delivery_user_revision_requested_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
&continuation.delegation_id,
)
.expect("mark a repair-node delivery for the second revision");
let second_barrier = static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
)
.expect("read second revision barrier");
assert_eq!(
second_barrier.user_revision_pending_count, 1,
"a revision whose parent delivery is itself a repair node must still block"
);
assert!(!second_barrier.is_clear());
fs::remove_dir_all(root).ok();
}
#[test]
fn static_delegate_user_revision_reuses_evidence_ready_objective_constraints() {
let base = StaticDelegateStructuredResult {
contract_status: StaticDelegateContractStatus::UserRevisionRequested,
..StaticDelegateStructuredResult::default()
};
validate_static_delegate_structured_result(&base, "completed", &[])
.expect("completed user revision with complete evidence is valid");
let failed = validate_static_delegate_structured_result(&base, "failed", &[])
.expect_err("failed terminal status must reject a user revision result");
assert!(failed.contains("客观证据冲突"));
let mut missing = base.clone();
missing.missing_expected_artifacts = vec!["design.md".to_string()];
let missing_error = validate_static_delegate_structured_result(
&missing,
"completed",
&["design.md".to_string()],
)
.expect_err("missing expected artifact must reject a user revision result");
assert!(missing_error.contains("客观证据冲突"));
let mut unverified = base;
unverified.verification_required = true;
let verification_error =
validate_static_delegate_structured_result(&unverified, "completed", &[])
.expect_err("required verification without a revision must be rejected");
assert!(verification_error.contains("客观证据冲突"));
}
#[test]
fn static_delegate_lineage_boundary_and_status_deserialization_fail_closed() {
let mut deliveries = Vec::new();
let mut ids = Vec::new();
for index in 0..33 {
ids.push(format!("m1c0-lineage-boundary-{index}"));
}
for index in 0..33 {
let parent = (index > 0).then(|| ids[index - 1].as_str());
let mut delivery = new_static_delegate_delivery(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-boundary-session",
"m1c0-boundary-run",
&format!("{}-action", ids[index]),
&ids[index],
"design-director",
&format!("{}-session", ids[index]),
&format!("{}-run", ids[index]),
);
delivery.repair_of_delegation_id = parent.map(str::to_string);
let mut result = StaticDelegateStructuredResult::default();
result.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
delivery.structured_result = Some(result);
deliveries.push(delivery);
}
assert_eq!(
static_delegate_lineage_counters(&deliveries[..32], &ids[31]),
(0, 0),
"32 条 delivery(31 跳)仍应在安全阀内"
);
assert_eq!(
static_delegate_lineage_counters(&deliveries, &ids[32]),
(u32::MAX, u32::MAX),
"超过 32-hop 安全阀必须 fail closed"
);
for (status, wire) in [
(
StaticDelegateContractStatus::EvidenceReady,
"evidence-ready",
),
(StaticDelegateContractStatus::NeedsRepair, "needs-repair"),
(
StaticDelegateContractStatus::NeedsUserInput,
"needs-user-input",
),
(
StaticDelegateContractStatus::UserRevisionRequested,
"user-revision-requested",
),
] {
let encoded = serde_json::to_value(&status).expect("serialize known status");
assert_eq!(encoded, serde_json::json!(wire));
assert_eq!(
serde_json::from_value::<StaticDelegateContractStatus>(encoded)
.expect("round-trip known status"),
status
);
}
let unknown_wire = "future-contract-status";
let unknown =
serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(unknown_wire))
.expect("unknown durable contract status is preserved explicitly");
assert_eq!(
unknown,
StaticDelegateContractStatus::Unknown(unknown_wire.to_string())
);
assert_eq!(
serde_json::to_value(&unknown).expect("serialize unknown status"),
serde_json::json!(unknown_wire)
);
assert!(
serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(42)).is_err(),
"non-string durable contract status must still fail closed"
);
// Durable sidecar 的未知变体可读,但必须进入 barrier、拒绝返工,并在读-改-写后
// 保留原始 wire 字符串;不能吞掉或降级成 Default(NeedsRepair)。
let root = std::env::temp_dir().join(format!(
"genarrative-static-unknown-status-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "m1c0-unknown-status", "未知静态委派状态解析测试")
.expect("project init");
let acceptance_criteria = vec!["保留未知合同状态".to_string()];
let mut record = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-unknown-status-session",
"m1c0-unknown-status-run",
"m1c0-unknown-status-action",
"m1c0-unknown-status-delivery",
"design-director",
"m1c0-unknown-status-target-session",
"m1c0-unknown-status-target-run",
&acceptance_criteria,
&[],
None,
);
record.status = StaticDelegateDeliveryStatus::ClaimedByParent;
record.terminal_status = Some("completed".to_string());
record.result_summary = Some("unknown status fixture".to_string());
record.claimed_by_action_id = Some("m1c0-unknown-status-claim-action".to_string());
let mut result = StaticDelegateStructuredResult::default();
result.contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string());
record.structured_result = Some(result);
write_static_delegate_delivery_at(&root, &record).expect("write unknown status fixture");
let loaded = read_static_delegate_delivery_at(&root, &record.delegation_id)
.expect("read unknown status fixture")
.expect("unknown status delivery exists");
assert_eq!(loaded, record);
let barrier = static_delegate_completion_barrier_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-unknown-status-run",
)
.expect("read unknown status completion barrier");
assert_eq!(barrier.unknown_contract_status_count, 1);
assert!(!barrier.is_clear());
assert!(barrier.has_waiting());
assert!(barrier.detail().contains("unknownContractStatus=1"));
let repair_error = validate_static_delegate_repair_request_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-unknown-status-run",
"m1c0-unknown-status-repair-candidate",
"design-director",
&acceptance_criteria,
&[],
Some(&record.delegation_id),
)
.expect_err("unknown root status must reject the first repair unconditionally");
assert!(repair_error.contains("更新版本") && repair_error.contains("拒绝返工"));
// 读-改-写只改变外层字段时,未知 status 的 raw wire 值必须原样保留。
let mut rewritten = loaded;
rewritten.result_summary = Some("unknown status rewritten summary".to_string());
rewritten.updated_at = unix_timestamp();
write_static_delegate_delivery_at(&root, &rewritten).expect("rewrite unknown status");
let delivery_path = root
.join(STATIC_DELEGATE_DELIVERY_DIR)
.join(format!("{}.json", record.delegation_id));
let rewritten_raw: serde_json::Value = serde_json::from_slice(
&fs::read(&delivery_path).expect("read rewritten unknown status sidecar"),
)
.expect("parse rewritten unknown status sidecar");
assert_eq!(
rewritten_raw["structuredResult"]["contractStatus"],
serde_json::json!(unknown_wire)
);
// lineage 中的 Unknown 按“其它”分支计数,不能被误识别成用户修订或澄清。
let mut unknown_lineage = deliveries[..2].to_vec();
unknown_lineage[0]
.structured_result
.as_mut()
.expect("lineage root structured result")
.contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string());
assert_eq!(
static_delegate_lineage_counters(&unknown_lineage, &ids[1]),
(1, 0)
);
assert!(static_delegate_lineage_contains_unknown_contract_status(
&unknown_lineage,
&ids[1]
)
.expect("inspect unknown lineage"));
// 损坏仍按原有整体 fail-closed 语义处理;即使目录里另有合法 delivery,也不能
// 跳过坏记录后继续计算 barrier/lineage。
let unrelated = new_static_delegate_delivery(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"m1c0-unrelated-session",
"m1c0-unrelated-run",
"m1c0-unrelated-action",
"m1c0-unrelated-delivery",
"art-director",
"m1c0-unrelated-target-session",
"m1c0-unrelated-target-run",
);
write_static_delegate_delivery_at(&root, &unrelated)
.expect("write unrelated valid delivery");
for (label, bytes) in [
("截断 JSON", b"{not-json".to_vec()),
("非 UTF-8", vec![b'{', 0xff, b'}']),
(
"超限 sidecar",
vec![b' '; STATIC_DELEGATE_DELIVERY_MAX_BYTES + 1],
),
] {
fs::write(&delivery_path, &bytes).expect("write damaged delivery sidecar");
let error = list_static_delegate_deliveries_at(&root)
.expect_err("damaged sidecar must lock the whole delivery directory");
assert!(
error.contains("静态委派 delivery"),
"{label} returned an unrelated error: {error}"
);
write_static_delegate_delivery_at(&root, &rewritten)
.expect("restore valid unknown status sidecar");
}
fs::remove_dir_all(root).ok();
}
}