澄清转述 pending 不再被自己的任务身份校验判成 needs-reconciliation
D11 里 Runtime 代 Supervisor 汇总子 Agent 澄清问题时,pending 动作的 task 字段 存的不是本 run 的任务,而是一段 Runtime 生成的转述指令(provider_recovery.rs 的「子 Agent 需要用户澄清后才能继续。delegationId=…」),delegationId 就编码在 前缀之后,另有四处按这个前缀反解绑定关系。 但 validate_agent_runtime_pending_context 会拿 pending.task 与 runtime .current_task 做相等比较,而落等待态的 persist_game_creator_agent_user_input_ wait 只改 status/phase/waiting_on,从不同步 current_task。其余五个 pending 创建 点传的都是 run 的真实 task,相等断言对它们成立;转述这一处永不可能成立。 后果是必现而非抖动:用户答完问题、观察已落盘(status=observed-approved)后, resume 立刻被判 needs-reconciliation,整条立项策划链在第一次澄清就断。 实测证据(run 14): runtime.currentTask = 做个横版像素解谜小游戏,主角是个能操控自己影子的小机器人。(29 字符) pending.task = 子 Agent 需要用户澄清后才能继续。delegationId=delegation-ec059abd…(355 字符) 任务日志 27 条全部是前者;plan 根没有 autonomous completion contract, autonomous_effective_root_task_at 两侧都走 fallback,比较退化成裸文本相等。 这条路径此前从未执行过——澄清信封在 run 11/12/13 一次都没触发,缺陷因此一直 藏在 0 轮问询后面。 只豁免文本相等这一条。agent/task_id/session/run/source 五项身份检查在它之前已 全部通过,轮次检查在它之后继续执行,转述 pending 本身也只能由 Runtime 在本 run 内生成,所以不放开任何跨 run 或跨身份的重放面。没有选择改 pending 的持久格式: task 字段被当载荷用是既成事实,四处反解加在途 pending 迁移,风险与收益不成比例。 顺带把散落五处的同一段 strip_prefix 链收成 user_input.rs 的一个共用判据。抄本 之间一旦漂移,转述路径会静默失去与原 delivery 的绑定,那是比本次更难查的故障。 回归测试双向变异验证:恢复严格比较则转述被拒(红),整条删掉则被改写 task 的 pending 不再被拒(红)。受影响测试组单线程 124 passed / 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 需要用户澄清后才能继续。delegationId={};{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。",
|
||||
delivery.delegation_id
|
||||
);
|
||||
// Re-entry after a wake or restart may only reuse the exact request that
|
||||
|
||||
@@ -173,7 +173,20 @@ pub(in crate::agent) fn validate_agent_runtime_pending_context(
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作身份与当前状态不匹配".to_string());
|
||||
}
|
||||
validate_agent_runtime_context_task_parameter(root, runtime, &pending.task)?;
|
||||
// 转述 pending 是唯一一处 `task` 字段存的不是本 run 任务的动作:Runtime 代
|
||||
// Supervisor 汇总子 Agent 澄清问题时,把「回答后创建唯一 continuation 委派」
|
||||
// 那段指令连同 delegationId 写在这里(provider_recovery.rs),而 run 的
|
||||
// current_task 始终是用户原始请求。其余五个 pending 创建点传的都是 run 的真实
|
||||
// task,所以这条相等断言对它们成立,对转述则永不可能成立——D11 澄清路径实测
|
||||
// 每一次都会在 resume 时被判成 needs-reconciliation,用户答案已落盘却无法继续。
|
||||
//
|
||||
// 这里只豁免文本相等这一条。pending 与 runtime 的 agent/task_id/session/run/
|
||||
// source 五项身份检查在上面已经全部通过,轮次检查在下面继续执行,转述 pending
|
||||
// 本身也只能由 Runtime 在本 run 内生成,所以豁免不放开任何跨 run 或跨身份的
|
||||
// 重放面。
|
||||
if !agent_runtime_pending_is_delegate_clarification_relay(pending) {
|
||||
validate_agent_runtime_context_task_parameter(root, runtime, &pending.task)?;
|
||||
}
|
||||
if pending.loop_iteration != runtime.loop_iteration {
|
||||
return Err(format!(
|
||||
"Agent Runtime 待确认动作轮次与当前状态不匹配:pending={} state={}",
|
||||
@@ -820,6 +833,88 @@ mod tests {
|
||||
use super::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// D11 转述路径的回归锚点。
|
||||
///
|
||||
/// Runtime 代 Supervisor 汇总子 Agent 澄清问题时,pending 的 `task` 存的是转述
|
||||
/// 指令(含 delegationId),而 run 的 current_task 始终是用户原始请求。此前这条
|
||||
/// 相等断言对转述 pending 永不可能成立:用户答完后 resume 立刻被判
|
||||
/// needs-reconciliation,答案已落盘却无法继续,整条立项策划链在第一次澄清就断。
|
||||
/// 生产 run 11/12/13 没暴露,只是因为澄清信封从来没触发过。
|
||||
#[test]
|
||||
fn delegate_clarification_relay_pending_survives_the_task_identity_check() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock should be after epoch")
|
||||
.as_nanos();
|
||||
let temporary = crate::tests::canonical_test_tempdir(&format!(
|
||||
"clarification-relay-task-{}-{unique}-",
|
||||
std::process::id()
|
||||
));
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "project-1", "澄清转述任务身份项目")
|
||||
.expect("project init");
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"做个横版像素解谜小游戏",
|
||||
"clarification-relay-run",
|
||||
"agent-background-task",
|
||||
"转述澄清问题",
|
||||
vec!["等待用户回答后创建唯一 continuation 委派".to_string()],
|
||||
)
|
||||
.expect("start supervisor runtime state");
|
||||
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: GAME_CREATOR_USER_INPUT_REQUEST_TOOL.to_string(),
|
||||
reason: Some("代 Supervisor 汇总子 Agent 的澄清问题".to_string()),
|
||||
input: serde_json::json!({"questions": []}),
|
||||
};
|
||||
let relay_task = format!(
|
||||
"{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}delegation-abc;questionsSha256=fixture。请回答以下问题。"
|
||||
);
|
||||
let build = |task: &str, action: &AgentRuntimeToolAction| {
|
||||
build_game_creator_agent_runtime_pending_tool_action(
|
||||
&root,
|
||||
&runtime,
|
||||
task,
|
||||
&AgentRuntimeToolPlan::default(),
|
||||
&[],
|
||||
&read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read project revision"),
|
||||
&build_repository_startup_context_at(&root)
|
||||
.expect("repository context")
|
||||
.fingerprint,
|
||||
action,
|
||||
0,
|
||||
AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO,
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT,
|
||||
None,
|
||||
)
|
||||
.expect("build pending action")
|
||||
};
|
||||
|
||||
let relay = build(&relay_task, &action);
|
||||
assert!(agent_runtime_pending_is_delegate_clarification_relay(
|
||||
&relay
|
||||
));
|
||||
assert_ne!(relay.task, runtime.current_task);
|
||||
validate_agent_runtime_pending_context(&root, &runtime, &relay)
|
||||
.expect("转述 pending 必须能通过身份校验,否则用户答案落盘后无法续跑");
|
||||
|
||||
// 豁免只覆盖转述本身。同一工具、同一 run,但 task 不是 Runtime 生成的
|
||||
// 转述指令时,原有的相等断言必须照旧拒绝——否则这就是个洞而不是修复。
|
||||
let forged = build("被改写的任务", &action);
|
||||
assert!(!agent_runtime_pending_is_delegate_clarification_relay(
|
||||
&forged
|
||||
));
|
||||
assert!(validate_agent_runtime_pending_context(&root, &runtime, &forged).is_err());
|
||||
|
||||
// 身份五项仍然照查:换一个 run_id 的转述 pending 不得被放行。
|
||||
let mut cross_run = relay.clone();
|
||||
cross_run.run_id = format!("{}-other", cross_run.run_id);
|
||||
assert!(validate_agent_runtime_pending_context(&root, &runtime, &cross_run).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_runtime_context_bundle_restores_pre_checkpoint_window_boundary() {
|
||||
let unique = SystemTime::now()
|
||||
|
||||
+2
-12
@@ -710,12 +710,7 @@ pub(crate) fn plan_clarification_pending_requires_project_lock_at(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<bool, String> {
|
||||
let Some(delegation_id) = pending
|
||||
.task
|
||||
.strip_prefix("子 Agent 需要用户澄清后才能继续。delegationId=")
|
||||
.and_then(|value| value.split(';').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
let Some(delegation_id) = agent_runtime_delegate_clarification_delegation_id(&pending.task)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -730,12 +725,7 @@ fn validate_plan_clarification_answer_for_pending_at_unlocked(
|
||||
questions: &[AgentRuntimeUserInputQuestion],
|
||||
answers: &BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
let Some(delegation_id) = pending
|
||||
.task
|
||||
.strip_prefix("子 Agent 需要用户澄清后才能继续。delegationId=")
|
||||
.and_then(|value| value.split(';').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
let Some(delegation_id) = agent_runtime_delegate_clarification_delegation_id(&pending.task)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -1631,12 +1631,7 @@ pub(crate) fn static_delegate_clarification_pending_matches_delivery_at(
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(delegation_id) = pending
|
||||
.task
|
||||
.strip_prefix("子 Agent 需要用户澄清后才能继续。delegationId=")
|
||||
.and_then(|value| value.split(';').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
let Some(delegation_id) = agent_runtime_delegate_clarification_delegation_id(&pending.task)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -2564,7 +2564,7 @@ fn claimed_needs_user_input_delivery_becomes_one_supervisor_wait() {
|
||||
);
|
||||
let mut spoofed = first.clone();
|
||||
spoofed.task = format!(
|
||||
"子 Agent 需要用户澄清后才能继续。delegationId={delegation_id};questionsSha256={}。",
|
||||
"{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}{delegation_id};questionsSha256={}。",
|
||||
"a".repeat(64)
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -2,6 +2,29 @@ use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub(crate) const GAME_CREATOR_USER_INPUT_REQUEST_TOOL: &str = "user.input_request";
|
||||
/// Runtime 代 Supervisor 汇总子 Agent 澄清问题时,pending 动作的 `task` 字段存的
|
||||
/// 不是本 run 的任务,而是这段由 Runtime 生成的转述指令,delegationId 编码在前缀
|
||||
/// 之后。系统里另外五处都要按这个前缀反解,此前各自抄了一份 `strip_prefix` 链;
|
||||
/// 抄本之间一旦漂移,转述路径会静默失去与原 delivery 的绑定,所以收成一处。
|
||||
pub(crate) const AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX: &str =
|
||||
"子 Agent 需要用户澄清后才能继续。delegationId=";
|
||||
|
||||
/// 从转述 pending 的 `task` 里取回它绑定的原 delegationId;不是转述 task 返回 None。
|
||||
pub(crate) fn agent_runtime_delegate_clarification_delegation_id(task: &str) -> Option<&str> {
|
||||
task.strip_prefix(AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX)
|
||||
.and_then(|value| value.split(';').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
/// 该 pending 是否由 Runtime 代 Supervisor 生成的子 Agent 澄清转述。
|
||||
pub(crate) fn agent_runtime_pending_is_delegate_clarification_relay(
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> bool {
|
||||
pending.action.tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL
|
||||
&& agent_runtime_delegate_clarification_delegation_id(&pending.task).is_some()
|
||||
}
|
||||
|
||||
pub(crate) const AGENT_RUNTIME_USER_INPUT_SCHEMA_VERSION: &str =
|
||||
"game-creator-runtime-user-input.v1";
|
||||
pub(crate) const AGENT_RUNTIME_USER_INPUT_STATUS_PENDING: &str = "pending";
|
||||
@@ -998,12 +1021,7 @@ fn bind_user_input_record_to_static_delegate_at(
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
record: &AgentRuntimeUserInputRecord,
|
||||
) -> Result<(), String> {
|
||||
let Some(delegation_id) = pending
|
||||
.task
|
||||
.strip_prefix("子 Agent 需要用户澄清后才能继续。delegationId=")
|
||||
.and_then(|value| value.split(';').next())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
let Some(delegation_id) = agent_runtime_delegate_clarification_delegation_id(&pending.task)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user