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
Reviewed-on: #355 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
3654 lines
150 KiB
Rust
3654 lines
150 KiB
Rust
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(¤t, &expected_delivery)?;
|
||
if matches!(
|
||
current.status,
|
||
StaticDelegateDeliveryStatus::Dispatched | StaticDelegateDeliveryStatus::Ready
|
||
) {
|
||
suppress_static_delegate_delivery_at(root, ¤t)?;
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
}
|