diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs index f8aec8f1f..4d18851aa 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs @@ -214,19 +214,12 @@ pub(crate) fn observe_agent_runtime_agent_message( /// 平坦的 depth <= 1 门,那时「唯一返工轮」对澄清跳也成立;本仓库改成按谱系分类后 /// 把预算抬到 3,这句话就变成了假天花板——生产实测 4 次澄清续跑全部命中它,命中后 /// 全部直接出稿,没有任何一个 run 走到第 2 轮。 -/// - `EnvelopeRetry`:上一跳的 AGC_NEEDS_USER_INPUT_V1 信封解析不了(实测形态是 -/// 输出被截断,字节级前缀与重投那次完全相同、只少了收尾的 `]}`)。它不消耗 -/// `repair_depth`,所以这里也不能说「唯一返工轮」——那既是假天花板,又会在做游戏 -/// 链路上误触发三个视觉角色的 `replaceExisting=true` 授权。 /// - `None`:普通委派,不加这一段。 pub(in crate::agent) enum StaticDelegateHopNote<'a> { None, Repair { original_delegation_id: &'a str, }, - EnvelopeRetry { - original_delegation_id: &'a str, - }, PlanClarification { original_delegation_id: &'a str, rounds_used: u32, @@ -245,11 +238,6 @@ impl StaticDelegateHopNote<'_> { StaticDelegateHopNote::Repair { original_delegation_id, } => format!("\n\n这是对已认领委派 {original_delegation_id} 的唯一返工轮。"), - StaticDelegateHopNote::EnvelopeRetry { - original_delegation_id, - } => format!( - "\n\n上一轮对已认领委派 {original_delegation_id} 的 AGC_NEEDS_USER_INPUT_V1 信封没能解析完整,多半是那一轮输出被截断。这是同一轮的重投,不是返工轮,不消耗返工深度,也不改变已用澄清轮次。请原样重出上一轮那个问题,并确保信封 JSON 完整闭合到最外层的 `]}}`。" - ), // 预算用尽:planning_coordinator 出卡时会用 // `current_round >= 3` 直接拒掉第四张卡,所以这里不能再邀请提问, // 只能要求收稿——语义上等价于原型的 INJ_MUST_DRAFT_ROUNDS。 @@ -934,13 +922,6 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked( } else { None }; - // 坏信封重投在谱系计数里不吃返工深度,任务正文就不能继续叫它「唯一返工轮」。 - // 读不到 delivery 时退回 Repair 文案:宁可保守,也不凭空宣布这是免费重投。 - let envelope_retry = repair_of_delegation_id.as_deref().is_some_and(|original| { - list_static_delegate_deliveries_at(root).is_ok_and(|deliveries| { - static_delegate_next_hop_is_free_envelope_retry(&deliveries, original) - }) - }); let hop_note = match ( repair_of_delegation_id.as_deref(), plan_clarification_rounds, @@ -952,11 +933,6 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked( rounds_limit, } } - (Some(original_delegation_id), None) if envelope_retry => { - StaticDelegateHopNote::EnvelopeRetry { - original_delegation_id, - } - } (Some(original_delegation_id), None) => StaticDelegateHopNote::Repair { original_delegation_id, }, @@ -2079,38 +2055,4 @@ mod tests { "预算用尽时不得再给出下一轮 header,那是一张永远递不上去的卡:{exhausted}" ); } - - /// 坏信封重投既不吃返工深度,任务正文也不能说它是返工轮——那句话同时是做游戏 - /// 链路三个视觉角色 `replaceExisting=true` 的授权信号。 - #[test] - fn envelope_retry_hop_note_is_neither_a_repair_round_nor_a_clarification_round() { - let retry = render_static_delegate_task_contract( - "任务", - "project-supervisor", - "run-1", - "delegation-new", - &["交付 game/fast_gdd.md".to_string()], - &["game/fast_gdd.md".to_string()], - StaticDelegateHopNote::EnvelopeRetry { - original_delegation_id: "delegation-old", - }, - ) - .expect("render envelope retry"); - assert!( - !retry.contains("唯一返工轮"), - "坏信封重投不是返工轮,复用这句话会误触发 replaceExisting 授权:{retry}" - ); - assert!( - !retry.contains("的澄清续跑"), - "重投是同一轮的重发,不得写成推进一轮的澄清续跑:{retry}" - ); - assert!( - retry.contains("不改变已用澄清轮次"), - "必须写明重投不推进轮次,否则子 Agent 会把 header 的 N 加一:{retry}" - ); - assert!( - retry.contains("delegation-old") && retry.contains("输出被截断"), - "重投必须指出原委派和真实原因,子 Agent 才知道要原样重出并闭合 JSON:{retry}" - ); - } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs index e88eac57d..e1505a4db 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -69,10 +69,6 @@ pub(crate) fn schema_max_clarification_envelope() -> String { // 从未承诺给它 3 轮预算,因此取 1;其它 source(包括 Project Supervisor 常规协作)取 3。 const STATIC_DELEGATE_CLARIFICATION_ROUND_LIMIT_DEFAULT: u32 = 3; const STATIC_DELEGATE_CLARIFICATION_ROUND_LIMIT_GAME_CHAT: u32 = 1; -// 一条谱系上最多给几次「坏信封重投」的免费额度。取 1:信封写坏一次通常是这一轮 -// 输出没写完,重投就能拿到同一份内容;连续两次多半是子 Agent 真的不会写这个契约, -// 那就该按质量返工计费,让既有的 repair_depth <= 1 把它兜住。 -const STATIC_DELEGATE_ENVELOPE_RETRY_LIMIT: u32 = 1; // 链上重放的防环 / 防越界上限,远大于设计允许的最大 7 跳,纯粹是安全阀。 const STATIC_DELEGATE_LINEAGE_MAX_HOPS: usize = 32; @@ -177,11 +173,6 @@ pub(crate) struct StaticDelegateStructuredResult { pub(crate) user_input_questions: Vec, #[serde(default)] pub(crate) user_input_questions_sha256: Option, - /// 本次终态带了 AGC_NEEDS_USER_INPUT_V1 首行但信封 JSON 解析不了。 - /// 这是「本轮输出没写完/写坏」,不是子 Agent 的方案质量问题,所以谱系计数 - /// 不把它当返工跳。旧记录反序列化为 false,按质量返工照常处理。 - #[serde(default)] - pub(crate) user_input_envelope_unparsable: bool, } impl Default for StaticDelegateStructuredResult { @@ -196,7 +187,6 @@ impl Default for StaticDelegateStructuredResult { error: None, user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, } } } @@ -440,7 +430,6 @@ pub(crate) fn mark_static_delegate_delivery_ready_at( error: (terminal_status != "completed").then(|| result_summary.to_string()), user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, }; mark_static_delegate_delivery_ready_with_result_at( root, @@ -1286,18 +1275,6 @@ fn static_delegate_original_is_user_revision_requested( }) } -/// 上一跳的终态带了信封前缀但 JSON 解析不了——实测的形态是输出被截断,字节级 -/// 前缀完全相同、只少了收尾的 `]}`。这属于「这一轮没写完」,重投一次就能拿到 -/// 同一份内容,不该按方案质量返工计费。 -pub(crate) fn static_delegate_original_envelope_was_unparsable( - delivery: &StaticDelegateDeliveryRecord, -) -> bool { - delivery - .structured_result - .as_ref() - .is_some_and(|result| result.user_input_envelope_unparsable) -} - /// 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. @@ -1334,31 +1311,14 @@ pub(crate) fn static_delegate_lineage_counters( chain.reverse(); let mut depth = 0u32; let mut round = 0u32; - let mut consecutive_envelope_retries = 0u32; for parent in &chain[..chain.len().saturating_sub(1)] { if static_delegate_original_is_awaiting_clarification(*parent) { round += 1; - consecutive_envelope_retries = 0; } else if static_delegate_original_is_user_revision_requested(*parent) { // 用户明确触发的修订不是 runaway-agent 返工;保留两个运行时派生计数。 - consecutive_envelope_retries = 0; - } else if static_delegate_original_envelope_was_unparsable(*parent) { - // 坏信封重投**永远不重置澄清轮次**:一次输出被截断既没有推进也没有否定 - // 任何已确认的决定,把 round 归零会让 planning_coordinator 回头要求 - // 「第1轮·关键决定」,子 Agent 按真实轮号写的 header 随即被拒,整条 - // 父 run 卡进 needs-reconciliation(实测复现过)。 - // - // 额度按**连续次数**算,不是整条谱系一个总额:分散在链上的多次截断 - // 各自独立,连续多次才说明子 Agent 真的不会写这个契约,那时才按质量 - // 返工计费,由既有的 `repair_depth >= 1` 兜住。 - consecutive_envelope_retries += 1; - if consecutive_envelope_retries > STATIC_DELEGATE_ENVELOPE_RETRY_LIMIT { - depth += 1; - } } else { depth += 1; round = 0; - consecutive_envelope_retries = 0; } } (depth, round) @@ -1381,26 +1341,6 @@ pub(crate) fn static_delegate_lineage_contains_unknown_contract_status( })) } -/// 以 `original_delegation_id` 为父的下一跳,是否还落在坏信封免费重投额度内。 -/// -/// 额度按连续次数算,所以只需要数**紧挨着链尾**的那串连续坏信封节点。跳类型 -/// 文案用它决定说「不消耗返工深度」还是退回质量返工文案——说反了就是撒谎。 -pub(crate) fn static_delegate_next_hop_is_free_envelope_retry( - deliveries: &[StaticDelegateDeliveryRecord], - original_delegation_id: &str, -) -> bool { - let Some(chain) = static_delegate_lineage_nodes(deliveries, original_delegation_id) else { - return false; - }; - // chain 是 [original .. 根],正好按「从链尾往回数」的顺序排列。 - let consecutive = chain - .iter() - .take_while(|node| static_delegate_original_envelope_was_unparsable(node)) - .count(); - consecutive > 0 - && u32::try_from(consecutive).unwrap_or(u32::MAX) <= STATIC_DELEGATE_ENVELOPE_RETRY_LIMIT -} - fn static_delegate_lineage_nodes<'a>( deliveries: &'a [StaticDelegateDeliveryRecord], delegation_id: &str, @@ -1804,9 +1744,6 @@ pub(crate) fn build_static_delegate_structured_result_at( Ok((questions, sha256)) => (questions, sha256, None), Err(parse_error) => (None, None, Some(parse_error)), }; - // parse_static_delegate_user_input_request 只在首行确实是信封前缀时才会报错, - // 所以这个标记不会被普通文本终态误置。 - let user_input_envelope_unparsable = user_input_parse_error.is_some(); let needs_user_input = completed && user_input_questions.is_some(); let contract_status = if needs_user_input { StaticDelegateContractStatus::NeedsUserInput @@ -1872,7 +1809,6 @@ pub(crate) fn build_static_delegate_structured_result_at( error: derived_error, user_input_questions: user_input_questions.unwrap_or_default(), user_input_questions_sha256, - user_input_envelope_unparsable, }) } @@ -3374,196 +3310,6 @@ mod tests { delivery } - /// 坏信封(首行是 AGC_NEEDS_USER_INPUT_V1 但 JSON 解析不了)重投不吃返工深度, - /// 但额度有限:连续第二次就落回质量返工,由既有的 depth<=1 门兜住。 - /// - /// 生产实测的形态是输出被截断——重投那次的正文与被截断那次逐字节相同,只多出 - /// 收尾的 `]}`。原实现把它记成质量返工,一次字节级截断就把整条委派唯一的返工 - /// 额度吃光,之后真出现方案质量问题时已经没有返工可用。 - #[test] - fn static_delegate_unparsable_envelope_retry_is_free_once_then_costs_repair_depth() { - let root = std::env::temp_dir().join(format!( - "genarrative-static-envelope-retry-{}-{}", - 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-envelope-retry", "坏信封重投计费测试") - .expect("project init"); - let parent_run_id = "m1c0-envelope-retry-parent-run"; - - let mut mark_unparsable = |delegation_id: &str, repair_of: Option<&str>| { - let mut delivery = claimed_static_delegate_for_lineage_test( - parent_run_id, - delegation_id, - repair_of, - StaticDelegateContractStatus::NeedsRepair, - ); - delivery - .structured_result - .as_mut() - .expect("structured result") - .user_input_envelope_unparsable = true; - write_static_delegate_delivery_at(&root, &delivery).expect("write delivery"); - delivery - }; - - let d0 = mark_unparsable("m1c0-envelope-retry-d0", None); - let d1 = mark_unparsable("m1c0-envelope-retry-d1", Some(&d0.delegation_id)); - let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); - assert_eq!( - static_delegate_lineage_counters(&deliveries, &d1.delegation_id), - (0, 0), - "第一次坏信封重投既不吃返工深度,也不动澄清轮次" - ); - validate_static_delegate_repair_request_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - parent_run_id, - "m1c0-envelope-retry-d2", - "design-director", - &[], - &[], - Some(&d1.delegation_id), - ) - .expect("first envelope retry must leave the quality-repair budget intact"); - - let d2 = claimed_static_delegate_for_lineage_test( - parent_run_id, - "m1c0-envelope-retry-d2", - Some(&d1.delegation_id), - StaticDelegateContractStatus::NeedsRepair, - ); - write_static_delegate_delivery_at(&root, &d2).expect("write d2"); - let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); - assert_eq!( - static_delegate_lineage_counters(&deliveries, &d2.delegation_id), - (1, 0), - "免费额度只有一次,第二次坏信封按质量返工计费" - ); - let blocked = validate_static_delegate_repair_request_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - parent_run_id, - "m1c0-envelope-retry-d3", - "design-director", - &[], - &[], - Some(&d2.delegation_id), - ) - .expect_err("repeated bad envelopes must fall back to the depth gate"); - assert!(blocked.contains("深度最多为 1"), "{blocked}"); - } - - /// 坏信封重投永远不重置澄清轮次,额度也按**连续**次数算而不是整条谱系一个总额。 - /// - /// 生产实测复现过:链是 坏信封 → 第1轮 → 第2轮 → 坏信封 → 目标,第二次截断把 - /// round 归零,planning_coordinator 回头要求「第1轮·关键决定」,子 Agent 按真实 - /// 轮号写的 header 被拒,父 run 卡进 needs-reconciliation。 - #[test] - fn static_delegate_envelope_retry_never_resets_the_clarification_round() { - let root = std::env::temp_dir().join(format!( - "genarrative-static-envelope-round-{}-{}", - 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-envelope-round", "坏信封不重置轮次测试") - .expect("project init"); - let parent_run_id = "m1c0-envelope-round-parent-run"; - - let write = |delegation_id: &str, - repair_of: Option<&str>, - status: StaticDelegateContractStatus, - unparsable: bool| { - let needs_user_input = status == StaticDelegateContractStatus::NeedsUserInput; - let mut delivery = claimed_static_delegate_for_lineage_test( - parent_run_id, - delegation_id, - repair_of, - status, - ); - let result = delivery - .structured_result - .as_mut() - .expect("structured result"); - result.user_input_envelope_unparsable = unparsable; - if needs_user_input { - // needs-user-input 的落盘校验要求问题非空且指纹匹配。 - let envelope = format!( - "{STATIC_DELEGATE_USER_INPUT_PREFIX}{}", - serde_json::json!({ - "questions": [{ - "id": "core_loop", - "header": "关键决定", - "question": "本轮要定什么?", - "options": [ - {"label": "A", "description": "方案 A"}, - {"label": "B", "description": "方案 B"} - ] - }] - }) - ); - let (questions, sha256) = parse_static_delegate_user_input_request(Some(&envelope)) - .expect("fixture clarification envelope"); - result.user_input_questions = questions.expect("fixture questions"); - result.user_input_questions_sha256 = sha256; - } - write_static_delegate_delivery_at(&root, &delivery).expect("write delivery"); - delivery - }; - - let d0 = write( - "m1c0-envelope-round-d0", - None, - StaticDelegateContractStatus::NeedsRepair, - true, - ); - let d1 = write( - "m1c0-envelope-round-d1", - Some(&d0.delegation_id), - StaticDelegateContractStatus::NeedsUserInput, - false, - ); - let d2 = write( - "m1c0-envelope-round-d2", - Some(&d1.delegation_id), - StaticDelegateContractStatus::NeedsUserInput, - false, - ); - let d3 = write( - "m1c0-envelope-round-d3", - Some(&d2.delegation_id), - StaticDelegateContractStatus::NeedsRepair, - true, - ); - let d4 = write( - "m1c0-envelope-round-d4", - Some(&d3.delegation_id), - StaticDelegateContractStatus::NeedsUserInput, - false, - ); - - let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); - assert_eq!( - static_delegate_lineage_counters(&deliveries, &d4.delegation_id), - (0, 2), - "两次截断被 2 轮澄清隔开,各自都在免费额度内,轮次必须保持 2" - ); - assert!( - static_delegate_next_hop_is_free_envelope_retry(&deliveries, &d3.delegation_id), - "链尾只有一个连续坏信封,下一跳仍是免费重投" - ); - assert!( - !static_delegate_next_hop_is_free_envelope_retry(&deliveries, &d2.delegation_id), - "父节点不是坏信封时不得声称是免费重投" - ); - } - #[test] fn static_delegate_user_revision_preserves_counters_and_bypasses_depth_gate() { let root = std::env::temp_dir().join(format!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/claims.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/claims.rs index efe8d653e..fa3e6a0bb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/claims.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/claims.rs @@ -1007,7 +1007,6 @@ fn project_supervisor_static_ready_receipt_over_budget_is_non_mutating() { error: None, user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, }; mark_static_delegate_delivery_ready_with_result_at( &root, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs index ed1cde8b9..cb0c43cfb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs @@ -1467,7 +1467,6 @@ fn canvas_replacement_requires_the_claimed_original_and_unique_repair_contract() error: None, user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, }); original.claimed_by_action_id = Some("canvas-replacement-claim-action".to_string()); create_or_read_static_delegate_delivery_at(&root, &original) @@ -1695,7 +1694,6 @@ async fn canvas_replacement_rejects_parent_run_that_terminates_during_external_r error: None, user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, }); original.claimed_by_action_id = Some("canvas-repair-claim-action".to_string()); create_or_read_static_delegate_delivery_at(&root, &original) diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs index 20b849e2f..43ca0195b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs @@ -6799,7 +6799,6 @@ fn legacy_pre_clarification_delivery_repair_counts_toward_depth() { error: Some("缺少 1 个预期产物".to_string()), user_input_questions: Vec::new(), user_input_questions_sha256: None, - user_input_envelope_unparsable: false, }); legacy_delivery.claimed_by_action_id = Some("legacy-delivery-claim-action".to_string()); write_static_delegate_delivery_at(&root, &legacy_delivery).expect("persist legacy delivery");