evidence 引用整份下发,拒绝理由报全三元组

run 23 那次 agent.acceptance_update 被拒的真实理由是
「Acceptance Graph evidence 缺少持久动作回执:action-2976e1b70fc2b12177c8f2d4」——
而同一个 actionId 下一轮就通过了,回执也早在 9 秒前落盘。

原因是 evidence 引用是 {agentId, runId, actionId} 三元组,
acceptance_evidence_tools_at 按三元组整体做 key 查回执,三者任一对不上都落空。
observation 只给了 sourceActionId 一个碎片,另外两个字段要模型自己回忆,它第一次
回忆错了。而错误信息只打印 identity.2,把「runId 写错」报成「这个 actionId 没有
回执」——模型据此得出"我的读取没落盘",排查的人也会先去查时序和落盘顺序。

两处都改:

1. file.read 的 observation summary 给出整份三元组
   (sourceAgentId / sourceRunId / sourceActionId)。反正
   validate_fast_gdd_evidence_identity 只接受当前根 run 的回执,合法取值唯一,
   本来就不该让模型猜。
2. 缺回执与回执未成功两条错误都报全 agentId / runId / actionId。

取证指令与 playbook 第 5 步同步改成「三个字段原样照抄,不要自己回忆 agentId 或
runId」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 09:50:59 +00:00
parent 9be673513e
commit 397e9dc3fc
4 changed files with 71 additions and 18 deletions
@@ -4,7 +4,7 @@
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,并提交 observation 给出的 `questionsSha256``answersSha256`
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。
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 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
【转达的规则】
@@ -294,7 +294,12 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
false,
|| observe_agent_runtime_file(root, &action.input),
);
append_agent_runtime_file_read_source_action_id(&mut observation, action_id);
append_agent_runtime_file_read_evidence_ref(
&mut observation,
agent_id,
run_id,
action_id,
);
observation
}
"file.write" => observe_agent_runtime_file_write(
@@ -534,8 +539,10 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
/// 追加在 detail 首行末尾是安全的:`agent_runtime_action_safe_detail_value` 解析
/// `file.read` 时只取前三个 `·` 字段(path / sha256 / lines),多出来的字段不参与,
/// durable receipt 与既有的取证解析都不受影响。
fn append_agent_runtime_file_read_source_action_id(
fn append_agent_runtime_file_read_evidence_ref(
observation: &mut AgentRuntimeToolObservation,
agent_id: &str,
run_id: &str,
action_id: Option<&str>,
) {
if observation.status != "ok" {
@@ -547,13 +554,26 @@ fn append_agent_runtime_file_read_source_action_id(
if observation.summary.contains("sourceActionId=") {
return;
}
let agent_id = agent_id.trim();
let run_id = run_id.trim();
if agent_id.is_empty() || run_id.is_empty() {
return;
}
// 写进 **summary** 而不是 detail`file.read` 的 detail 在投影给模型和事件流之前
// 会被换成 durable receipt 的 safe_detail(只有 path / contentSha256 / lines 三个
// 字段),追加在 detail 上的字段到不了模型手里。实测一次 run 里模型照着新指令
// 满世界找 sourceActionId、一次也没看到,只能反复重读全文再猜 actionId,28 次
// file.read、5 次 acceptance_update 才收敛。summary 是原样保留的,也没有任何
// 解析方依赖它的形状。
observation.summary = format!("{} · sourceActionId={action_id}", observation.summary);
// 给的是**整个三元组**,不是一个碎片。`agent.acceptance_update` 的 evidence 引用
// 要求 {agentId, runId, actionId} 三个字段,回执查找也按三元组整体做 key。早期
// 只给 actionId,另外两个靠模型回忆——实测它第一次就把其中一个记错,白吃一次
// 拒绝。反正 `validate_fast_gdd_evidence_identity` 只接受当前根 run 的回执,
// 合法取值唯一,本来就不该让它猜。
observation.summary = format!(
"{} · sourceAgentId={agent_id} · sourceRunId={run_id} · sourceActionId={action_id}",
observation.summary
);
}
pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock<F>(
@@ -929,15 +949,17 @@ mod file_read_source_action_id_tests {
#[test]
fn appending_the_source_action_id_keeps_the_receipt_safe_detail_parseable() {
let mut observation = file_read_observation();
append_agent_runtime_file_read_source_action_id(
append_agent_runtime_file_read_evidence_ref(
&mut observation,
"project-supervisor",
"run-1",
Some("action-0123456789abcdef01234567"),
);
assert!(
observation
.summary
.ends_with("· sourceActionId=action-0123456789abcdef01234567"),
"actionId 必须写在 summary 上——detail 会被换成 safe_detail,到不了模型手里"
observation.summary.ends_with(
"· sourceAgentId=project-supervisor · sourceRunId=run-1 · sourceActionId=action-0123456789abcdef01234567"
),
"evidence 三元组必须整份写在 summary 上——detail 会被换成 safe_detail,到不了模型手里;只给 actionId 则另外两个字段要靠模型回忆,实测会记错",
);
let detail = observation.detail.clone().expect("detail");
assert!(!detail.contains("sourceActionId="), "detail 保持原样");
@@ -959,18 +981,38 @@ mod file_read_source_action_id_tests {
let mut failed = file_read_observation();
failed.status = "failed".to_string();
let before = failed.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut failed, Some("action-1"));
append_agent_runtime_file_read_evidence_ref(
&mut failed,
"project-supervisor",
"run-1",
Some("action-1"),
);
assert_eq!(failed.summary, before);
let mut missing = file_read_observation();
let before = missing.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut missing, None);
append_agent_runtime_file_read_evidence_ref(
&mut missing,
"project-supervisor",
"run-1",
None,
);
assert_eq!(missing.summary, before);
let mut twice = file_read_observation();
append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-1"));
append_agent_runtime_file_read_evidence_ref(
&mut twice,
"project-supervisor",
"run-1",
Some("action-1"),
);
let once = twice.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-2"));
append_agent_runtime_file_read_evidence_ref(
&mut twice,
"project-supervisor",
"run-1",
Some("action-2"),
);
assert_eq!(twice.summary, once, "已经带了 id 就不再追加第二个");
}
}
@@ -392,7 +392,7 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
)
} else if plan_root {
format!(
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,每次都用 maxLines=240(上限)以尽量一页读完、少分页,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,每次都用 maxLines=240(上限)以尽量一页读完、少分页,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中逐条提交全部分页 file.read 的 {{agentId, runId, actionId}}——每次 file.read 的 observation 末尾都带着自己的 sourceAgentId / sourceRunId / sourceActionId,三个字段原样照抄,不要自己回忆 agentId 或 runId,也不要为取得它们额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
)
} else if root_control_authority {
format!(
@@ -1689,7 +1689,9 @@ mod tests {
assert!(later_prompt.contains("Goal Contract 已冻结且不可重写"));
assert!(later_prompt.contains("从 startLine=1 开始分页"));
assert!(later_prompt.contains("直到 EOF"));
assert!(later_prompt.contains("全部分页 file.read 的 actionId"));
assert!(later_prompt.contains("全部分页 file.read 的 {agentId, runId, actionId}"));
// evidence 是三元组,不是一个 actionId;指令必须点名另外两个字段从哪来。
assert!(later_prompt.contains("sourceAgentId / sourceRunId / sourceActionId"));
assert!(!later_prompt.contains("Goal Contract 首轮协议"));
}
@@ -198,11 +198,20 @@ fn acceptance_evidence_tools_at<'a>(
for identity in identities {
let record = receipts
.get(&identity)
.ok_or_else(|| format!("Acceptance Graph evidence 缺少持久动作回执:{}", identity.2))?;
// 查找按 (agentId, runId, actionId) 整体做 key,三者任一对不上都会落空。
// 早期这句只打印 actionId,于是一次「runId 写错」被报成「这个 actionId 没有
// 回执」——实测把模型带进了「我的读取没落盘」的错误结论,重读全文再猜;
// 排查的人也会先去查时序和落盘顺序。报全三元组才指得对方向。
.ok_or_else(|| {
format!(
"Acceptance Graph evidence 缺少持久动作回执:agentId={} · runId={} · actionId={}",
identity.0, identity.1, identity.2
)
})?;
if agent_db_record_text(record, "status") != Some("ok") {
return Err(format!(
"Acceptance Graph evidence 动作未成功:{}",
identity.2
"Acceptance Graph evidence 动作未成功:agentId={} · runId={} · actionId={}",
identity.0, identity.1, identity.2
));
}
let tool = agent_db_record_text(record, "tool")