file.read observation 自带 sourceActionId,plan 根砍掉 agent.action_history
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:
@@ -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 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
|
||||
|
||||
【转达的规则】
|
||||
|
||||
+121
-10
@@ -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 就不再追加第二个");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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!(
|
||||
|
||||
+4
-4
@@ -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",
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user