修回归:sourceActionId 必须写在 summary 上,detail 到不了模型手里
上一笔把 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:
+21
-21
@@ -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 就不再追加第二个");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user