file.read observation 自带 sourceActionId,plan 根砍掉 agent.action_history
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 3m16s
Project CI / Native shell tests (pull_request) Successful in 15m31s

run 20 已经把裸计划更新那 5 轮清零(14 → 10 轮),但 agent.action_history 连调
四次:不带筛选拿到 4 条,带 tool=file.read 筛选拿到 1 条,然后又查全量、又筛一次。
GDD 只有 134 行、一页读完,本来就只有一个 actionId,它反复确认了四遍。

查过了,这些结果一直在它上下文里:观察逐轮累积进 prompt,agent.action_history 的
detail 还有 8000 字符的专属额度(context_window.rs 的 sanitize 分支),run 20 全程
compaction=0、上下文从 30507 涨到 33820。所以它不是失明,是对"全部分页"这个词较真
——拿到 1 条怀疑漏了分页,拿到全量又怀疑混进别的工具。这种"不敢信"加 prompt 约束
没用,得把取 id 这件事从工具变成事实。

command.exec 早就是这么做的:durable observation 直接返回可复用的 sourceActionId,
合同里明写「不要为取得它额外查询动作历史」。这里把同一条路铺给 file.read——成功读
取时在 detail 首行末尾追加 sourceActionId,然后把 agent.action_history 从 plan 根的
工具面整个拿掉。

追加是安全的:agent_runtime_action_receipt_safe_detail 解析 file.read 时只取前三个
`·` 字段(path / sha256 / lines),多出来的字段不参与,durable receipt 与 §13.0 的
取证解析都不受影响。

plan 根现在是五个工具:agent.goal_contract / agent.delegate / file.read /
agent.acceptance_update / agent.run_status,其中 run_status 只在 Delegated 阶段开放,
供审批返工时按原 delegationId 取回权威委派合同。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 08:53:46 +00:00
parent 2bdf6a732b
commit 0ab887c157
4 changed files with 127 additions and 16 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` 末尾取证,用 `agent.action_history` 取回全部分页 actionId,再用一次 `agent.acceptance_update` 把它们完整写进 evidence。取证完成前审批卡不会出现。
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行分页读到 `game/fast_gdd.md` 末尾取证。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。
6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
【转达的规则】
@@ -283,16 +283,20 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
false,
|| observe_agent_runtime_file_list(root, &action.input),
),
"file.read" => observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
run_id,
action,
&action_fingerprint,
pending_action,
false,
|| observe_agent_runtime_file(root, &action.input),
),
"file.read" => {
let mut observation = observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
run_id,
action,
&action_fingerprint,
pending_action,
false,
|| observe_agent_runtime_file(root, &action.input),
);
append_agent_runtime_file_read_source_action_id(&mut observation, action_id);
observation
}
"file.write" => observe_agent_runtime_file_write(
root,
agent_id,
@@ -513,6 +517,47 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
}
}
/// 让 `file.read` 的 observation 自带 durable actionId。
///
/// §13.0 的审批前置取证要求把每一页 `file.read` 的 actionId 放进
/// `agent.acceptance_update.evidence`,而 observation 结构里只有
/// `{tool,status,summary,detail}`,模型没有第二条途径拿到它——只能回头查
/// `agent.action_history`。实测它会为了一个 actionId 连查四次:拿到 1 条怀疑漏了
/// 分页,拿到全量又怀疑混进了别的工具,而这些结果自始至终都在它上下文里
/// `agent.action_history` 的 observation detail 有 8000 字符的专属额度,不会被裁)。
/// 加 prompt 约束对这种"不敢信"没用,把取 id 这件事从工具变成事实才有用。
///
/// `command.exec` 早就是这么做的:durable observation 直接返回可复用的
/// `sourceActionId`,合同里明写「不要先猜 actionId 或为取得它额外查询动作历史」。
/// 这里把同一条路铺给 `file.read`。
///
/// 追加在 detail 首行末尾是安全的:`agent_runtime_action_safe_detail_value` 解析
/// `file.read` 时只取前三个 `·` 字段(path / sha256 / lines),多出来的字段不参与,
/// durable receipt 与既有的取证解析都不受影响。
fn append_agent_runtime_file_read_source_action_id(
observation: &mut AgentRuntimeToolObservation,
action_id: Option<&str>,
) {
if observation.status != "ok" {
return;
}
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=") {
return;
}
let rest = detail[first.len()..].to_string();
let first = first.to_string();
observation.detail = Some(format!("{first} · sourceActionId={action_id}{rest}"));
}
pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock<F>(
root: &Path,
agent_id: &str,
@@ -863,3 +908,69 @@ mod canvas_only_execution_tests {
);
}
}
#[cfg(test)]
mod file_read_source_action_id_tests {
use super::*;
fn file_read_observation() -> AgentRuntimeToolObservation {
AgentRuntimeToolObservation {
tool: "file.read".to_string(),
status: "ok".to_string(),
summary: "已读取 game/fast_gdd.md 第 1-134 行(共 134 行)".to_string(),
detail: Some(format!(
"game/fast_gdd.md · sha256={} · lines 1-134 of 134
第一行内容",
"a".repeat(64)
)),
}
}
/// 追加的 sourceActionId 必须能被模型看见,且不能破坏 durable receipt 的
/// safe_detail 解析——取证门就是从那里读 path / contentSha256 / lines 的。
#[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(
&mut observation,
Some("action-0123456789abcdef01234567"),
);
let detail = observation.detail.clone().expect("detail");
assert!(detail
.lines()
.next()
.expect("first line")
.ends_with("· sourceActionId=action-0123456789abcdef01234567"));
assert!(detail.contains("第一行内容"), "首行之后的内容必须保留");
let root = std::env::temp_dir();
let safe_detail = crate::agent::runtime_actions::action_audit::
agent_runtime_action_receipt_public_safe_detail_for_test(&root, &observation)
.expect("safe detail still parses");
let value = serde_json::from_str::<serde_json::Value>(&safe_detail).expect("json");
assert_eq!(value["path"], "game/fast_gdd.md");
assert_eq!(value["lines"], "1-134 of 134");
assert_eq!(value["contentSha256"], "a".repeat(64));
}
/// 失败的读取、缺 actionId、以及重复调用都不得改写 detail。
#[test]
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();
append_agent_runtime_file_read_source_action_id(&mut failed, Some("action-1"));
assert_eq!(failed.detail, before);
let mut missing = file_read_observation();
let before = missing.detail.clone();
append_agent_runtime_file_read_source_action_id(&mut missing, None);
assert_eq!(missing.detail, 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();
append_agent_runtime_file_read_source_action_id(&mut twice, Some("action-2"));
assert_eq!(twice.detail, 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,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
)
} else if root_control_authority {
format!(
@@ -128,15 +128,16 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
/// needs-user-input observation,也就没有调用它的时机;广告出去只会让它在别的时点
/// 调一次,撞上 Runtime 已经装好的那份 pending 而硬失败。
///
/// `file.read``agent.acceptance_update`、`agent.action_history` 只为 §13.0 的审批
/// 前置取证门存在(分页读 `game/fast_gdd.md` 并列出全部分页 actionId)。
/// `file.read``agent.acceptance_update` 只为 §13.0 的审批前置取证门存在(分页读
/// `game/fast_gdd.md` 并列出全部分页 actionId)。**`agent.action_history` 不在其中**
/// 每次 `file.read` 的 observation 已经自带 `sourceActionId`,取证不需要回头查历史;
/// 留着它只会让模型为同一个 id 反复确认(实测连查四次,答案一直在上下文里)。
pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static str] {
&[
"file.read",
"agent.delegate",
"agent.goal_contract",
"agent.acceptance_update",
"agent.action_history",
"agent.run_status",
]
}
@@ -172,7 +173,6 @@ pub(crate) fn agent_runtime_plan_root_supervisor_tools_for_stage(
"file.read",
"agent.delegate",
"agent.acceptance_update",
"agent.action_history",
"agent.run_status",
],
}