diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/provider_recovery.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/provider_recovery.rs index 085408e7a..321a6acfb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/provider_recovery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/provider_recovery.rs @@ -391,7 +391,7 @@ pub(crate) fn ensure_static_delegate_user_input_wait_at_locked( let question_binding = game_creator_agent_user_input_action_input_summary(&action.input) .unwrap_or_else(|| "questionsSha256=unavailable".to_string()); let task = format!( - "{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}{};{question_binding}。请回答以下问题;回答完成后只创建一次 agent.delegate continuation,并将 repairOfDelegationId 与 continuationOfDelegationId 指向该原 delegation,同时提交 questionsSha256/answersSha256。", + "{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}{};{question_binding}。请回答以下问题;回答完成后只创建一次 agent.delegate continuation,并将 repairOfDelegationId 与 continuationOfDelegationId 指向该原 delegation。questionsSha256/answersSha256 由 Runtime 从原 delivery 补齐,你不要自己填写。", delivery.delegation_id ); // Re-entry after a wake or restart may only reuse the exact request that 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 2acbe61e7..19612be70 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 @@ -785,28 +785,27 @@ fn validate_native_agent_delegate_input( "Agent 原生工具协议错误:agent.delegate 返工委派时 runId 必须为 JSON null", )); } - let continuation_fields = [ - "continuationOfDelegationId", - "questionsSha256", - "answersSha256", - ] - .iter() - .filter(|field| object.get(**field).is_some_and(Value::is_string)) - .count(); - let continuation_present = [ - "continuationOfDelegationId", - "questionsSha256", - "answersSha256", - ] - .iter() - .filter(|field| object.contains_key(**field)) - .count(); - if (continuation_present != 0 && continuation_present != 3) - || (continuation_fields != 0 && continuation_fields != 3) - { + // 澄清 continuation 的锚点是 continuationOfDelegationId;两个指纹可以整体省略, + // 由 Runtime 从原 delivery 取权威值补齐。省略是为了不让 Supervisor 手抄 128 个 + // 十六进制字符——抄错会打到硬失败,而抄对也不带来任何 Runtime 不知道的信息。 + // 三个字段一律按“JSON null 等同于缺省”处理,与本函数其余可空字段一致。 + let continuation_anchor = object + .get("continuationOfDelegationId") + .is_some_and(Value::is_string); + let digest_strings = ["questionsSha256", "answersSha256"] + .iter() + .filter(|field| object.get(**field).is_some_and(Value::is_string)) + .count(); + if digest_strings == 1 { return Err(protocol_error( AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema, - "Agent 原生工具协议错误:agent.delegate 澄清 continuation 字段必须同时提供", + "Agent 原生工具协议错误:agent.delegate 澄清 continuation 指纹必须成对提供,或整体省略交给 Runtime 补齐", + )); + } + if digest_strings == 2 && !continuation_anchor { + return Err(protocol_error( + AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema, + "Agent 原生工具协议错误:agent.delegate 澄清 continuation 指纹必须同时提供 continuationOfDelegationId", )); } for field in ["questionsSha256", "answersSha256"] { @@ -2070,19 +2069,43 @@ mod tests { } #[test] - fn native_agent_delegate_rejects_partial_or_invalid_clarification_binding() { - let mut partial = valid_delegate_input(json!("delegation-id"), Value::Null); - partial + fn native_agent_delegate_accepts_clarification_continuation_without_fingerprints() { + // 指纹由 Runtime 从原 delivery 补齐,Supervisor 只需要指出续跑的是哪个委派。 + let mut anchor_only = valid_delegate_input(json!("delegation-id"), Value::Null); + anchor_only .as_object_mut() .expect("delegate input object") .insert( "continuationOfDelegationId".to_string(), json!("delegation-id"), ); - assert!(validate_native_agent_delegate_input(&partial) - .expect_err("partial continuation binding must fail") + + validate_native_agent_delegate_input(&anchor_only) + .expect("clarification continuation without fingerprints"); + } + + #[test] + fn native_agent_delegate_rejects_partial_or_invalid_clarification_binding() { + let mut single_digest = valid_delegate_input(json!("delegation-id"), Value::Null); + let object = single_digest.as_object_mut().expect("delegate input object"); + object.insert( + "continuationOfDelegationId".to_string(), + json!("delegation-id"), + ); + object.insert("questionsSha256".to_string(), json!("a".repeat(64))); + assert!(validate_native_agent_delegate_input(&single_digest) + .expect_err("single continuation fingerprint must fail") .to_string() - .contains("必须同时提供")); + .contains("必须成对提供")); + + let mut orphan_digests = valid_delegate_input(json!("delegation-id"), Value::Null); + let object = orphan_digests.as_object_mut().expect("delegate input object"); + object.insert("questionsSha256".to_string(), json!("a".repeat(64))); + object.insert("answersSha256".to_string(), json!("b".repeat(64))); + assert!(validate_native_agent_delegate_input(&orphan_digests) + .expect_err("continuation fingerprints without anchor must fail") + .to_string() + .contains("必须同时提供 continuationOfDelegationId")); let mut invalid_sha = valid_delegate_input(json!("delegation-id"), Value::Null); let object = invalid_sha.as_object_mut().expect("delegate input object"); 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 e15f04363..e1505a4db 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -1553,7 +1553,13 @@ pub(crate) fn validate_static_delegate_clarification_continuation_at( if continuation_of != repair_of_delegation_id { return Err("澄清 continuation 必须绑定原 delegationId".to_string()); } - if input_questions_sha != questions_sha256 || input_answers_sha != answers_sha256 { + // 两个指纹是可选的:原 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!( 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 3f59a1cb8..c1f1a4e61 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 @@ -298,6 +298,18 @@ fn clarification_continuation_delegation_id( ) .expect("validate continuation binding") .expect("derived continuation identity"); + // 省略两个指纹时 Runtime 必须从原 delivery 补齐权威值,并得到与手填时完全相同的身份。 + // 这条断言挂在共享 helper 上,所有澄清 continuation 用例都会顺带覆盖到。 + let identity_without_fingerprints = validate_static_delegate_clarification_continuation_at( + root, + parent_agent_id, + parent_run_id, + &serde_json::json!({ "continuationOfDelegationId": original_delegation_id }), + Some(original_delegation_id), + ) + .expect("validate continuation binding without fingerprints") + .expect("derived continuation identity without fingerprints"); + assert_eq!(identity, identity_without_fingerprints); agent_runtime_delegation_id(parent_agent_id, parent_run_id, target_agent_id, &identity) }