修通审批卡「修改/退回」路线:claim 重放冲突、park 死锁、返工额度串用
生产实测:出 GDD 后点「修改」而不是「批准」,修订委派从头到尾建不出来。盘上现场 (gameagent-c6948da6 / gameagent-347c9786)逐层剥出三个独立缺陷,前两个叠在一起, 修掉上层才露出下层。 1. claim 快照与 delivery 全等比较 claim 的 structuredResult 是「父 Agent 当时观察到了什么」的冻结快照;审批把 delivery 从 EvidenceReady 原地改写成 UserRevisionRequested 后,快照仍停在 EvidenceReady。claim 提交(含幂等重放)按全等判,于是 agent.run_status 每次 重放都 failed,Supervisor 永远拿不到回执。实测空转 43 轮,报「静态委派 claim 与 delivery 身份或结果冲突」。 改为「delivery 是不是 receipt 的合法后继」:除 contractStatus 外每字段逐字 不变、且方向唯一(EvidenceReady → UserRevisionRequested)才放行。原型没有 claim 这层快照,单一真相就地改,结构上不存在这个冲突。 2. park 死锁(被 1 掩盖) 修掉 1 之后 barrier 能正常求值了,userRevisionPending=1 却被 has_waiting() 计成「有在等的委派」,main_loop 于是 park 成「等待专业 Agent 委派回执 / 回执全部 ready 后自动唤醒当前父 run」——那条回执只能来自本 run 自己要创建的修订委派,等的是自己。实测 8 分钟零事件。 has_waiting() 里那个计数是承重的:三处自动恢复路径靠它挡住自动唤醒, runtime_tools/delivery.rs 现场还有 debug_assert 钉着这份依赖。所以不动它, 另加 has_external_wait()(只计 waitingDelegations / unknownContractStatus), 两处 park 决策点改用它。两个谓词问的是相反的问题:自动恢复该不该收手 vs 当前这轮该不该 park。落进 main_loop 本来就写好的 user_revision_pending 分支 (phase=planning、next_step=调用 agent.delegate)即可。 3. 用户修订与质量返工共用「唯一返工轮」文案 用户修订跳带 repairOfDelegationId 但不是澄清续跑,落进 Repair 分支拿到 「这是对已认领委派 X 的唯一返工轮」——用户第一次点修改就被告知只能改这一次。 counter 层早就正确(lineage_counters 对该跳原样继承 depth/round),错的只有 这句话。新增 StaticDelegateHopNote::UserRevision,判据是原 delivery 的 contractStatus,并同澄清分支一样加 target == project-planning 门,做游戏 / 做素材的返工跳逐字保持 Repair——那句话是它们 replaceExisting=true 的唯一授权 信号。playbook 第 6 条同步改成「约束的是单条 delivery 不是整条链」。 原型对应的是 USER_REVISION_SOFT_LIMIT = 16,且超过只提示不拒绝。 守门三条,都验过非空转: - revise 端到端补上此前缺失的那一步——mark 与 dispatch 之间的 claim 重放,正是 生产上唯一会失败的地方;并断言 !is_clear() && !has_external_wait() && has_waiting() 三者同时成立。把比较改回全等即复现生产原文错误。 - 128 种计数组合的 detail↔barrier 等价性测试里写死两个谓词的分叉点,合并回一个 就会炸。 - user_revision hop note 不得含「唯一返工轮」,须含「不消耗返工深度」「不是最后一轮」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。
|
||||
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. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
|
||||
6. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里。「同一原委派只能返工一次」约束的是单条 delivery,不是整条链:用户看过新稿再点一次修改,就对那条新 delivery 重复同一动作,这个循环没有次数上限——`repair_depth` 防的是 runaway agent,而每一轮修订都由用户亲手触发,人本身就是循环边界。不要替 Runtime 宣布「这是最后一次修改机会」,也不要因此把多条意见攒到一轮里改完。用户通过后只做一句简短收尾。
|
||||
|
||||
【转达的规则】
|
||||
|
||||
|
||||
@@ -1017,6 +1017,25 @@ pub(in crate::agent) fn static_delegate_barrier_has_waiting_deliveries(detail: &
|
||||
waiting || user_revision_pending || unknown_contract_status
|
||||
}
|
||||
|
||||
/// `StaticDelegateCompletionBarrier::has_external_wait()` 的 detail 侧等价物。
|
||||
///
|
||||
/// 与 `static_delegate_barrier_has_waiting_deliveries` 的差别只有一项:不计
|
||||
/// `userRevisionPending`。park 决策必须用这个——用户修订没有任何外部事件可等,
|
||||
/// park 住就是等自己派出的委派,必然死锁。自动唤醒侧仍然用前者收手。
|
||||
pub(in crate::agent) fn static_delegate_barrier_has_external_wait(detail: &str) -> bool {
|
||||
let waiting = detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("waitingDelegations="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
let unknown_contract_status = detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("unknownContractStatus="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
waiting || unknown_contract_status
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) -> bool {
|
||||
detail
|
||||
.split_whitespace()
|
||||
@@ -1910,6 +1929,21 @@ mod static_delegate_barrier_detail_gate_tests {
|
||||
barrier.user_revision_pending_count > 0,
|
||||
"userRevisionPending 往返失真:{barrier:?}\ndetail={detail}"
|
||||
);
|
||||
assert_eq!(
|
||||
static_delegate_barrier_has_external_wait(&detail),
|
||||
barrier.has_external_wait(),
|
||||
"has_external_wait() 与 detail 解析必须等价:{barrier:?}\ndetail={detail}"
|
||||
);
|
||||
// 两个谓词只能在「仅 userRevisionPending」这一种情形上分叉,别的组合必须一致。
|
||||
// 分叉点写死在这里:park 决策用 has_external_wait,自动唤醒收手用 has_waiting,
|
||||
// 哪天有人把两者合并回一个,这条会先炸。
|
||||
assert_eq!(
|
||||
barrier.has_waiting() && !barrier.has_external_wait(),
|
||||
barrier.user_revision_pending_count > 0
|
||||
&& barrier.waiting_count == 0
|
||||
&& barrier.unknown_contract_status_count == 0,
|
||||
"两个等待谓词只应在「仅用户修订待办」时分叉:{barrier:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -681,8 +681,11 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
if let Some(blocker) =
|
||||
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
||||
{
|
||||
// park 只在真有外部事件可等时才对。用户修订待办不是外部事件——那条回执
|
||||
// 只能来自本 run 自己创建的修订委派,park 住就是等自己。下面 1700 行附近
|
||||
// 的 `user_revision_pending` 分支才是它该去的地方。
|
||||
let waits_for_delivery = blocker.detail.as_deref().is_some_and(|detail| {
|
||||
static_delegate_barrier_has_waiting_deliveries(detail)
|
||||
static_delegate_barrier_has_external_wait(detail)
|
||||
|| static_delegate_barrier_requires_user_input(detail)
|
||||
});
|
||||
if waits_for_delivery {
|
||||
@@ -1792,7 +1795,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
AgentBackgroundTaskOutcome::WaitingForIsolatedJoin,
|
||||
))
|
||||
} else if observation.tool == "runtime.delegate_receipts"
|
||||
&& (static_delegate_barrier_has_waiting_deliveries(detail)
|
||||
&& (static_delegate_barrier_has_external_wait(detail)
|
||||
|| static_delegate_barrier_requires_user_input(detail))
|
||||
{
|
||||
Some((
|
||||
|
||||
@@ -214,12 +214,21 @@ pub(crate) fn observe_agent_runtime_agent_message(
|
||||
/// 平坦的 depth <= 1 门,那时「唯一返工轮」对澄清跳也成立;本仓库改成按谱系分类后
|
||||
/// 把预算抬到 3,这句话就变成了假天花板——生产实测 4 次澄清续跑全部命中它,命中后
|
||||
/// 全部直接出稿,没有任何一个 run 走到第 2 轮。
|
||||
/// - `UserRevision`:用户在审批卡上点「修改 / 退回」后的修订轮。它同样带
|
||||
/// `repairOfDelegationId`,但 `repair_depth` 防的是 runaway agent,而这一跳每一轮
|
||||
/// 都由人触发——人本身就是循环边界,所以 `static_delegate_lineage_counters` 早就
|
||||
/// 把 depth/round 原样继承了。缺的是这句话:走 `Repair` 分支时用户第一次点修改就
|
||||
/// 会被告知「这是唯一返工轮」,和澄清跳当初那个假天花板是同一个错误。原型对应的是
|
||||
/// `USER_REVISION_SOFT_LIMIT = 16`,且超过只提示、不拒绝。
|
||||
/// - `None`:普通委派,不加这一段。
|
||||
pub(in crate::agent) enum StaticDelegateHopNote<'a> {
|
||||
None,
|
||||
Repair {
|
||||
original_delegation_id: &'a str,
|
||||
},
|
||||
UserRevision {
|
||||
original_delegation_id: &'a str,
|
||||
},
|
||||
PlanClarification {
|
||||
original_delegation_id: &'a str,
|
||||
rounds_used: u32,
|
||||
@@ -238,6 +247,11 @@ impl StaticDelegateHopNote<'_> {
|
||||
StaticDelegateHopNote::Repair {
|
||||
original_delegation_id,
|
||||
} => format!("\n\n这是对已认领委派 {original_delegation_id} 的唯一返工轮。"),
|
||||
StaticDelegateHopNote::UserRevision {
|
||||
original_delegation_id,
|
||||
} => format!(
|
||||
"\n\n这是对已认领委派 {original_delegation_id} 的用户修订轮,由用户在审批卡上提出,不是质量返工,不消耗返工深度,也不重置澄清轮次。按任务正文里的用户意见原文修订同一份 GDD 谱系后重新提交;用户看过新稿还可以再次提出修改,这不是最后一轮,不要因此压缩改动或提前收尾。"
|
||||
),
|
||||
// 预算用尽:planning_coordinator 出卡时会用
|
||||
// `current_round >= 3` 直接拒掉第四张卡,所以这里不能再邀请提问,
|
||||
// 只能要求收稿——语义上等价于原型的 INJ_MUST_DRAFT_ROUNDS。
|
||||
@@ -651,6 +665,29 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// 用户修订跳同样带 repairOfDelegationId,但它是人触发的,不该拿到「唯一返工轮」
|
||||
// 那句话。判据用原 delivery 的 contractStatus,并同样只作用于立项策划链路:
|
||||
// `mark_static_delegate_delivery_user_revision_requested_at` 只由策划审批调用,
|
||||
// 这里再加一道 target 门,做游戏 / 做素材的返工跳逐字保持 Repair 分支。
|
||||
let user_revision_hop = match repair_of_delegation_id.as_deref() {
|
||||
Some(original)
|
||||
if plan_clarification_rounds.is_none()
|
||||
&& target_agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID =>
|
||||
{
|
||||
match static_delegate_original_awaits_user_revision_at(root, original) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let hop_note = match (
|
||||
repair_of_delegation_id.as_deref(),
|
||||
plan_clarification_rounds,
|
||||
@@ -662,6 +699,11 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked(
|
||||
rounds_limit,
|
||||
}
|
||||
}
|
||||
(Some(original_delegation_id), None) if user_revision_hop => {
|
||||
StaticDelegateHopNote::UserRevision {
|
||||
original_delegation_id,
|
||||
}
|
||||
}
|
||||
(Some(original_delegation_id), None) => StaticDelegateHopNote::Repair {
|
||||
original_delegation_id,
|
||||
},
|
||||
@@ -1688,6 +1730,40 @@ mod tests {
|
||||
/// 「你只剩这一轮」——这正是生产上 4 次澄清续跑之后无一走到第 2 轮的原因。
|
||||
/// 同时钉住轮号:`planning_coordinator` 出卡时按 `rounds_used + 1` 校验 header,
|
||||
/// 这里写进 task 的必须是同一个数,否则第 2 轮信封会当场被拒。
|
||||
/// 用户修订轮同样不能套返工文案。
|
||||
///
|
||||
/// 「唯一返工轮」防的是 runaway agent,而这一跳由用户在审批卡上亲手点出来——人本身
|
||||
/// 就是循环边界,`static_delegate_lineage_counters` 早就把 depth/round 原样继承了。
|
||||
/// 套用返工文案就是告诉策划子 Agent「用户只能改这一次」,和澄清跳当初那个假天花板
|
||||
/// 是同一个错误。原型对应的是软阈值 16 次、超过只提示不拒绝。
|
||||
#[test]
|
||||
fn user_revision_hop_note_is_not_the_repair_round_note() {
|
||||
let revision = render_static_delegate_task_contract(
|
||||
"任务",
|
||||
"project-supervisor",
|
||||
"run-1",
|
||||
"delegation-new",
|
||||
&["交付 game/fast_gdd.md".to_string()],
|
||||
&["game/fast_gdd.md".to_string()],
|
||||
StaticDelegateHopNote::UserRevision {
|
||||
original_delegation_id: "delegation-old",
|
||||
},
|
||||
)
|
||||
.expect("render user revision hop note");
|
||||
assert!(
|
||||
!revision.contains("唯一返工轮"),
|
||||
"用户修订轮不得复用返工文案,否则子 Agent 以为用户只能改这一次:{revision}"
|
||||
);
|
||||
assert!(
|
||||
revision.contains("不消耗返工深度"),
|
||||
"必须写明它不吃返工额度:{revision}"
|
||||
);
|
||||
assert!(
|
||||
revision.contains("不是最后一轮"),
|
||||
"必须写明用户还能再改,否则子 Agent 会把多条意见攒到一轮改完:{revision}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_clarification_hop_note_is_not_the_repair_round_note() {
|
||||
let repair = render_static_delegate_task_contract(
|
||||
|
||||
@@ -278,12 +278,33 @@ impl StaticDelegateCompletionBarrier {
|
||||
&& 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,或升级客户端后再继续",
|
||||
@@ -497,6 +518,42 @@ pub(crate) fn mark_static_delegate_delivery_ready_with_result_at(
|
||||
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
|
||||
@@ -1097,7 +1154,10 @@ fn commit_static_delegate_claim_with_locks_with_budget_at(
|
||||
|| delivery.acceptance_criteria != receipt.acceptance_criteria
|
||||
|| delivery.expected_artifacts != receipt.expected_artifacts
|
||||
|| delivery.repair_of_delegation_id != receipt.repair_of_delegation_id
|
||||
|| delivery.structured_result != receipt.structured_result
|
||||
|| !static_delegate_structured_result_follows_claim_snapshot(
|
||||
receipt.structured_result.as_ref(),
|
||||
delivery.structured_result.as_ref(),
|
||||
)
|
||||
{
|
||||
return Err(format!(
|
||||
"静态委派 claim 与 delivery 身份或结果冲突:{}",
|
||||
@@ -1216,6 +1276,21 @@ fn static_delegate_original_is_awaiting_clarification(
|
||||
///
|
||||
/// 该状态只由后续审批工作包写入;本包只让 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 {
|
||||
|
||||
@@ -4426,6 +4426,42 @@ fn planning_clarification_user_revision_after_answer_preserves_round_for_revise_
|
||||
&submitted_delivery.delegation_id,
|
||||
)
|
||||
.expect("mark submitted delivery user-revision-requested");
|
||||
|
||||
// 审批改写 delivery 之后、派发修订委派之前,Supervisor 必然先调一次
|
||||
// `agent.run_status`,它会把这条已 observed 的 claim 整个重放一遍。claim 里的
|
||||
// structuredResult 仍是审批前那份 EvidenceReady 快照,而 delivery 已经是
|
||||
// UserRevisionRequested——这一步按全等判就会报「claim 与 delivery 身份或结果
|
||||
// 冲突」,Supervisor 从此拿不到回执,也就永远建不出下面那条修订委派。
|
||||
//
|
||||
// 生产实测正是卡在这里:run_status 连续 failed、空转到第 43 轮。此前这个用例
|
||||
// 从 mark 直接跳到 dispatch,跳过的恰好是唯一会失败的那一步。
|
||||
let barrier = static_delegate_completion_barrier_at(
|
||||
&fixture.root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&fixture.supervisor.run_id,
|
||||
)
|
||||
.expect("审批改写 delivery 后,claim 重放必须仍然成立");
|
||||
|
||||
// 屏障必须同时说出两件事:任务还不能收束,但**没有外部事件可等**。
|
||||
//
|
||||
// 只判「不能收束」不够——修复前它正是这样:main_loop 把用户修订待办当成
|
||||
// 「有在等的委派」,park 成「等待专业 Agent 委派回执 / 回执全部 ready 后自动
|
||||
// 唤醒当前父 run」,而那条回执只能来自下面这条还没派出去的修订委派。生产实测
|
||||
// 8 分钟零事件。has_external_wait() 为假才能让本轮落进 user_revision 分支去
|
||||
// 调 agent.delegate。
|
||||
assert!(
|
||||
!barrier.is_clear(),
|
||||
"用户修订待办没派出续作前,父 run 不能被判为可收束"
|
||||
);
|
||||
assert!(
|
||||
!barrier.has_external_wait(),
|
||||
"用户修订待办没有任何外部事件可等,park 住就是等自己派出的委派"
|
||||
);
|
||||
assert!(
|
||||
barrier.has_waiting(),
|
||||
"自动恢复路径仍须收手:续作派出前不得跨过这条 Supervisor 决策边界"
|
||||
);
|
||||
|
||||
let revision_action_id = format!("planning-user-{action}-continuation");
|
||||
let revision = dispatch_static_delegate_plain_repair(
|
||||
&fixture.root,
|
||||
|
||||
Reference in New Issue
Block a user