修回归:sourceActionId 必须写在 summary 上,detail 到不了模型手里
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 2m57s
Project CI / Native shell tests (pull_request) Successful in 15m41s

上一笔把 sourceActionId 追加进 file.read 的 observation detail,同时砍掉了
agent.action_history。但 file.read 的 detail 在投影给模型和事件流之前会被换成
durable receipt 的 safe_detail(只有 path / contentSha256 / lines 三个字段),
追加的字段根本到不了模型手里。

于是 run 22 变成:模型照着新指令满世界找 sourceActionId,一次也没看到;
agent.action_history 又已经被拿掉,它没有任何途径取得 actionId,只能猜;猜错被
acceptance_update 拒,然后判断"此前回执中的读取动作未被验收持久层接受",重读全文
再猜。29 轮里 28 次 file.read(全是同一页 1-132)、5 次 acceptance_update、3 次拒绝。

summary 是原样保留到模型和事件流的(日志里 `|` 左边那句就是
observe_agent_runtime_file 构造的原文),也没有任何解析方依赖它的形状。改成写在
summary 末尾。用例同步改成断言 summary 带 id、detail 保持原样。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 09:24:20 +00:00
parent 95d6068860
commit 9be673513e
@@ -544,18 +544,16 @@ fn append_agent_runtime_file_read_source_action_id(
let Some(action_id) = action_id.map(str::trim).filter(|value| !value.is_empty()) else {
return;
};
let Some(detail) = observation.detail.as_deref() else {
return;
};
let Some(first) = detail.lines().next() else {
return;
};
if first.contains("sourceActionId=") {
if observation.summary.contains("sourceActionId=") {
return;
}
let rest = detail[first.len()..].to_string();
let first = first.to_string();
observation.detail = Some(format!("{first} · sourceActionId={action_id}{rest}"));
// 写进 **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);
}
pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock<F>(
@@ -935,12 +933,14 @@ mod file_read_source_action_id_tests {
&mut observation,
Some("action-0123456789abcdef01234567"),
);
assert!(
observation
.summary
.ends_with("· sourceActionId=action-0123456789abcdef01234567"),
"actionId 必须写在 summary 上——detail 会被换成 safe_detail,到不了模型手里"
);
let detail = observation.detail.clone().expect("detail");
assert!(detail
.lines()
.next()
.expect("first line")
.ends_with("· sourceActionId=action-0123456789abcdef01234567"));
assert!(!detail.contains("sourceActionId="), "detail 保持原样");
assert!(detail.contains("第一行内容"), "首行之后的内容必须保留");
let root = std::env::temp_dir();
@@ -958,19 +958,19 @@ mod file_read_source_action_id_tests {
fn appending_is_a_no_op_without_a_successful_read_or_an_action_id() {
let mut failed = file_read_observation();
failed.status = "failed".to_string();
let before = failed.detail.clone();
let before = failed.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut failed, Some("action-1"));
assert_eq!(failed.detail, before);
assert_eq!(failed.summary, before);
let mut missing = file_read_observation();
let before = missing.detail.clone();
let before = missing.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut missing, None);
assert_eq!(missing.detail, before);
assert_eq!(missing.summary, before);
let mut twice = file_read_observation();
append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-1"));
let once = twice.detail.clone();
let once = twice.summary.clone();
append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-2"));
assert_eq!(twice.detail, once, "已经带了 id 就不再追加第二个");
assert_eq!(twice.summary, once, "已经带了 id 就不再追加第二个");
}
}