diff --git a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md index 42df25a2c..feef2a6a2 100644 --- a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md +++ b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/plan/supervisor-playbook.md @@ -3,9 +3,9 @@ 1. 本 run 第一轮只调用一次 `agent.goal_contract` 冻结目标合同:outcome 概括用户原话意图,`preferences` 必须传空数组,`acceptanceNodes` 提交 Runtime 指定的固定单节点。这一轮不做任何其它调用。 2. 冻结后立即用一次 `agent.delegate` 把任务委派给 `project-planning`,`expectedArtifacts` 写 `game/fast_gdd.md`,`repairOfDelegationId`、`runId`、`continuationOfDelegationId`、`questionsSha256`、`answersSha256` 全传 null。已有委派尚未收束时不要重复委派。 3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。 -4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery。`questionsSha256` 与 `answersSha256` 传 null——Runtime 会从该原 delivery 补齐权威指纹,你不要自己抄。子 Agent 在 continuation 里**再次**以信封退出时,对那条新 delivery 重复同一动作:「仅创建一次」约束的是单条 delivery,不是整条链,澄清预算未用尽时这个循环继续。Runtime 会在委派 task 末尾写明已用轮次与上限,不需要你自己数,也不要替它宣布预算已尽。 +4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery。`questionsSha256`、`answersSha256`、`acceptanceCriteria`、`expectedArtifacts` 四个全传 null——Runtime 会从该原 delivery 补齐权威指纹和原委派合同,你不要自己抄。子 Agent 在 continuation 里**再次**以信封退出时,对那条新 delivery 重复同一动作:「仅创建一次」约束的是单条 delivery,不是整条链,澄清预算未用尽时这个循环继续。Runtime 会在委派 task 末尾写明已用轮次与上限,不需要你自己数,也不要替它宣布预算已尽。 5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着 `sourceAgentId` / `sourceRunId` / `sourceActionId` 三个字段,把它们原样抄成 evidence 的 `{agentId, runId, actionId}`,用一次 `agent.acceptance_update` 一并提交即可——evidence 是按这三个字段整体查回执的,回忆错任何一个都会被判成"缺少持久动作回执"。不要为了取这些字段再去查动作历史。取证完成前审批卡不会出现。 -6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。 +6. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。 【转达的规则】 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 20f6b27e8..cb96ba65b 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 @@ -658,14 +658,36 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked( detail: None, }; } - let acceptance_criteria = agent_runtime_tool_input_string_list( + let mut acceptance_criteria = agent_runtime_tool_input_string_list( input, &["acceptanceCriteria", "acceptance_criteria", "criteria"], ); - let expected_artifacts = agent_runtime_tool_input_string_list( + let mut expected_artifacts = agent_runtime_tool_input_string_list( input, &["expectedArtifacts", "expected_artifacts", "artifacts"], ); + // 返工与澄清续跑必须逐字继承原委派的两个数组(下面 + // validate_static_delegate_repair_request_at 会逐项比对)。两个都空时由 Runtime + // 从原 delivery 补齐,省掉一次 agent.run_status 往返和一整轮手抄。只补齐"两个 + // 都空"这一种形态:只写了一半是有歧义的输入,仍旧交给既有比对报错。 + // 读不到原 delivery 时保持原样,让下游的既有错误如实说明问题。 + { + let original_delegation_id = agent_runtime_tool_input_text( + input, + &["repairOfDelegationId", "repair_of_delegation_id"], + ); + if !original_delegation_id.trim().is_empty() + && acceptance_criteria.is_empty() + && expected_artifacts.is_empty() + { + if let Ok(Some(original)) = + read_static_delegate_delivery_at(root, original_delegation_id.trim()) + { + acceptance_criteria = original.acceptance_criteria.clone(); + expected_artifacts = original.expected_artifacts.clone(); + } + } + } let explicit_contract = input.as_object().is_some_and(|object| { object.contains_key("acceptanceCriteria") || object.contains_key("acceptance_criteria") diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs index efe18dcf1..1b582897f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs @@ -735,20 +735,31 @@ fn validate_native_agent_delegate_input( } validate_native_delegate_string(object.get("agentId"), "agentId", 96, false)?; validate_native_delegate_string(object.get("task"), "task", 2_400, false)?; - validate_native_delegate_string_list( - object.get("acceptanceCriteria"), - "acceptanceCriteria", - 1, - 8, - 240, - )?; - validate_native_delegate_string_list( - object.get("expectedArtifacts"), - "expectedArtifacts", - 0, - 16, - 240, - )?; + // 返工/澄清续跑必须逐字继承原委派的两个数组,Runtime 从 repairOfDelegationId + // 指向的 delivery 直接读得到权威值。两个都传 null 时由 Runtime 补齐;手抄一遍 + // 不带来任何信息增益,只制造反复失败的返工委派。初次委派仍然必须自己写。 + let repair_hop = object + .get("repairOfDelegationId") + .is_some_and(Value::is_string); + let inherits_contract = repair_hop + && object.get("acceptanceCriteria").is_some_and(Value::is_null) + && object.get("expectedArtifacts").is_some_and(Value::is_null); + if !inherits_contract { + validate_native_delegate_string_list( + object.get("acceptanceCriteria"), + "acceptanceCriteria", + 1, + 8, + 240, + )?; + validate_native_delegate_string_list( + object.get("expectedArtifacts"), + "expectedArtifacts", + 0, + 16, + 240, + )?; + } validate_native_delegate_string( object.get("repairOfDelegationId"), "repairOfDelegationId", @@ -2068,6 +2079,39 @@ mod tests { .expect("legacy delegate input without clarification fields"); } + /// 返工/续跑的两个数组必须逐字继承原委派,Runtime 读得到权威值。要求 Supervisor + /// 手抄它们只会反复失败:实测一次两轮澄清的 plan run 里, + /// 「静态委派返工必须完整继承原 acceptanceCriteria 和 expectedArtifacts」 + /// 出现 4 次,每建一条 continuation 先白跑两轮工具调用。 + #[test] + fn native_agent_delegate_repair_may_inherit_the_original_contract() { + let mut inherit = valid_delegate_input(json!("delegation-id"), Value::Null); + let object = inherit.as_object_mut().expect("delegate input object"); + object.insert("acceptanceCriteria".to_string(), Value::Null); + object.insert("expectedArtifacts".to_string(), Value::Null); + + validate_native_agent_delegate_input(&inherit).expect("repair may inherit the contract"); + + // 初次委派没有可继承的原 delivery,两个数组仍然必须自己写。 + let mut initial = valid_delegate_input(Value::Null, json!("run-1")); + let object = initial.as_object_mut().expect("delegate input object"); + object.insert("acceptanceCriteria".to_string(), Value::Null); + object.insert("expectedArtifacts".to_string(), Value::Null); + assert!(validate_native_agent_delegate_input(&initial) + .expect_err("initial delegate must still carry its own contract") + .to_string() + .contains("acceptanceCriteria")); + + // 只省一半是有歧义的输入,不放行。 + let mut half = valid_delegate_input(json!("delegation-id"), Value::Null); + let object = half.as_object_mut().expect("delegate input object"); + object.insert("acceptanceCriteria".to_string(), Value::Null); + assert!(validate_native_agent_delegate_input(&half) + .expect_err("half-omitted contract must fail") + .to_string() + .contains("acceptanceCriteria")); + } + #[test] fn native_agent_delegate_accepts_clarification_continuation_without_fingerprints() { // 指纹由 Runtime 从原 delivery 补齐,Supervisor 只需要指出续跑的是哪个委派。 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 b5d2cb211..20b849e2f 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 @@ -4394,6 +4394,66 @@ fn clarification_continuation_chain_supports_multiple_rounds() { fs::remove_dir_all(root).ok(); } +/// 返工/澄清续跑的 acceptanceCriteria 与 expectedArtifacts 由 Runtime 从原 delivery +/// 继承:四个字段全传 null 时续跑照样建得起来,落盘值与原委派逐字相同。 +/// +/// 生产实测里 Supervisor 手抄这两个数组会反复抄错——一次两轮澄清的 plan run 中 +/// 「静态委派返工必须完整继承原 acceptanceCriteria 和 expectedArtifacts」出现 4 次, +/// 每建一条 continuation 先白跑两轮工具调用才成功。 +#[test] +fn planning_clarification_continuation_inherits_the_original_contract() { + let mut fixture = planning_clarification_fixture("inherit-contract"); + let answered = answer_planning_clarification_round( + &mut fixture, + 1, + PLAN_TEST_OPTION_A, + "inherit-contract", + ); + let original = answered.original_delivery.clone(); + assert!( + !original.acceptance_criteria.is_empty(), + "原委派合同为空会让下面的继承断言失去意义" + ); + + let continuation_id = clarification_continuation_delegation_id( + &fixture.root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &fixture.supervisor.run_id, + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + &original.delegation_id, + &answered.questions_sha256, + &answered.answers_sha256, + ); + let delegate_input = serde_json::json!({ + "agentId": GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + "task": "根据用户回答继续收敛 Fast GDD", + "acceptanceCriteria": serde_json::Value::Null, + "expectedArtifacts": serde_json::Value::Null, + "repairOfDelegationId": original.delegation_id, + "runId": serde_json::Value::Null, + "continuationOfDelegationId": original.delegation_id, + "questionsSha256": serde_json::Value::Null, + "answersSha256": serde_json::Value::Null, + }); + let observation = observe_agent_runtime_agent_delegate( + &fixture.root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &fixture.supervisor.run_id, + Some("planning-inherit-contract-continuation"), + &delegate_input, + ); + assert_eq!(observation.status, "ok", "{observation:?}"); + + let continuation = read_static_delegate_delivery_at(&fixture.root, &continuation_id) + .expect("read inherited continuation delivery") + .expect("inherited continuation delivery exists"); + assert_eq!( + continuation.acceptance_criteria, + original.acceptance_criteria + ); + assert_eq!(continuation.expected_artifacts, original.expected_artifacts); +} + #[test] fn planning_clarification_three_rounds_project_session_and_structured_injection() { let mut fixture = planning_clarification_fixture("three-rounds");