plan 根工具面按阶段开放,只读调用不再清零空转计数
run 18:Supervisor 冻结目标合同后一次也没委派,改成反复调 agent.run_status 去查
一个根本不存在的委派,24 轮里 58 次 agent.run_status、0 次 agent.delegate,烧到
超时。两道防线各自失效:
· 空转闸门(AGENT_RUNTIME_PLAN_UPDATE_IDLE_LIMIT=4)只在裸 update_agent_plan 且
步骤没变化时累加,而任意非空 actions 都清零。于是「裸计划更新被 blocked →
调一次只读工具 → 计数归零」是一个结构上永远关不上的闸。
· Runtime 其实有一条精准提示「调用 agent.delegate 派出策划子 Agent」,但它在
plan.actions.is_empty() 分支里——模型一调 run_status 就有了动作,提示不再出现,
取而代之的是一份看起来像进展的「已读取 11 个 Agent 状态」。
本地原型没有这个问题,不是因为阈值,是因为它的 Supervisor 只有四个工具且每一个都
推进链路:「调了工具」和「推进了链路」在那边是同一件事,按前者计数就是准的。生产
把两者拆开了却还在按前者计数,工具也是全程可见。
三处改动:
1. 工具面按 durable 阶段开放。合同未冻结 → 只有 agent.goal_contract;合同已冻结
但本根 run 尚无委派 → 只有 agent.delegate;已有委派 → 取证/返工/审批那几个。
run 18 卡死的正是第二档,那一档模型连 run_status 都调不出来。阶段只按 durable
事实判定,不看 Provider 说了什么。
2. 砍掉 user.input_request。这条链路上它是死的:子 Agent 以信封退出后,Runtime 在
parent-wake 屏障处自己按信封原文构造 pending 且不恢复父 run,Supervisor 永远收不
到 needs-user-input observation。广告出去只会让它在别的时点调一次,撞上 Runtime
已装好的那份 pending 而硬失败。playbook 第 4 步与转达提问那条一并改写。
3. 空转判据改成看「本轮有没有能推进 durable 状态的动作」,只读工具不清零。纯只读
调查不受影响——不发裸计划更新就根本不会累加。这条对做游戏那条链路同样有效。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,12 @@
|
||||
1. 本 run 第一轮只调用一次 `agent.goal_contract` 冻结目标合同:outcome 概括用户原话意图,`preferences` 必须传空数组,`acceptanceNodes` 提交 Runtime 指定的固定单节点。这一轮不做任何其它调用。
|
||||
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. 回执 contractStatus=needs-user-input 时,按 observation 给出的问题信封原样调用一次 `user.input_request`;答案收齐后为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery,并提交 observation 给出的 `questionsSha256`、`answersSha256`。
|
||||
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。取证完成前审批卡不会出现。
|
||||
6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
|
||||
|
||||
【转达的规则】
|
||||
|
||||
- 转达子 Agent 的问题时 `questions` 原样透传:不改写题干、不增删选项、不替用户预选。
|
||||
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] {header} → 用户答:{原文}`。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字,绝不压缩用户答案。
|
||||
- A、B、“需要原型验证”三项必须原样转述;B 是用户确认的 `confirmed/user_option`,不能转成默认建议。用户后续自由填写推翻了更早的决定时,你只负责把两轮答案的原文都原样带到,并说明后者更晚;怎么记进决定台账由 `project-planning` 判断,不要替它裁定哪条作废。
|
||||
|
||||
|
||||
@@ -620,7 +620,7 @@ fn game_creator_project_supervisor_plan_tool_plan_system_prompt() -> String {
|
||||
// 「合同说有、请求里没有」的自相矛盾。
|
||||
let tool_catalog = agent_runtime_plan_root_supervisor_tools().join("、");
|
||||
let prompt_header = format!(
|
||||
"你正在使用 Genarrative AI 游戏创作多智能体 Runtime。你必须直接调用当前请求广告的原生函数:复杂任务首次拆解、实际进度变化、steer 调整顺序或最终收束时调用 update_agent_plan,并提交 explanation 与完整 steps;无需更新时不要调用 update_agent_plan。steps 只允许 pending、in_progress、completed 且同时最多一个 in_progress;已完成步骤必须保留且不得回退,所有必要步骤 completed 前不得调用 respond_to_user,Runtime 不会按工具动作下标代替你更新进度。本 run 的原生可执行工具目录只有:{tool_catalog}。写入、补丁、删除、命令、预览、素材生成、任务图、记忆、黑板、isolated child 与 MCP 工具在本 run 都不存在,调用它们只会失败。"
|
||||
"你正在使用 Genarrative AI 游戏创作多智能体 Runtime。你必须直接调用当前请求广告的原生函数:复杂任务首次拆解、实际进度变化、steer 调整顺序或最终收束时调用 update_agent_plan,并提交 explanation 与完整 steps;无需更新时不要调用 update_agent_plan。steps 只允许 pending、in_progress、completed 且同时最多一个 in_progress;已完成步骤必须保留且不得回退,所有必要步骤 completed 前不得调用 respond_to_user,Runtime 不会按工具动作下标代替你更新进度。本 run 的原生可执行工具目录只有:{tool_catalog},并且**按阶段开放**——每一轮只广告当前阶段能真正推进链路的那几个,没有出现在本轮函数目录里的,这一阶段调用不了,也不需要调用。澄清卡不由你发:子 Agent 的问询信封由 Runtime 直接转成决策卡,你只会在用户答完之后被恢复。写入、补丁、删除、命令、预览、素材生成、任务图、记忆、黑板、isolated child 与 MCP 工具在本 run 都不存在,调用它们只会失败。"
|
||||
);
|
||||
// `$platform` 整段是 command.start/exec/poll/stdin/terminate 的用法合同,
|
||||
// plan 根一个 command 工具都没有;GDD 里的平台事实由 Runtime 另行注入,与
|
||||
@@ -1462,7 +1462,7 @@ mod tests {
|
||||
let allowlist = agent_runtime_plan_root_supervisor_tools();
|
||||
assert!(
|
||||
plan_prompt.contains(&format!(
|
||||
"本 run 的原生可执行工具目录只有:{}。",
|
||||
"本 run 的原生可执行工具目录只有:{},并且**按阶段开放**",
|
||||
allowlist.join("、")
|
||||
)),
|
||||
"plan 根 prompt 头部必须逐字列出 allowlist:{plan_prompt}"
|
||||
|
||||
@@ -155,6 +155,8 @@ pub(crate) use tool_policy_snapshot::{
|
||||
agent_runtime_acceptance_evidence_tools,
|
||||
agent_runtime_autonomous_design_foundation_command_is_allowed, agent_runtime_executable_tools,
|
||||
agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools,
|
||||
agent_runtime_tool_policy_snapshot_for_run_at, AGENT_RUNTIME_CANVAS_ASSET_KINDS,
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage,
|
||||
agent_runtime_tool_policy_snapshot_for_run_at, plan_root_supervisor_stage_at,
|
||||
PlanRootSupervisorStage, AGENT_RUNTIME_CANVAS_ASSET_KINDS,
|
||||
AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS,
|
||||
};
|
||||
|
||||
+9
-2
@@ -553,8 +553,15 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
)?)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
if plan_root {
|
||||
retain_plan_root_supervisor_native_tools(&mut request.function_tools)?;
|
||||
restrict_plan_root_goal_contract_schema(&mut request.function_tools)?;
|
||||
let stage = plan_root_supervisor_stage_at(root, agent_id, run_id)?;
|
||||
retain_plan_root_supervisor_native_tools(&mut request.function_tools, stage)?;
|
||||
// 固定单节点 schema 只对还在广告 agent.goal_contract 的阶段有意义;收窄之后
|
||||
// 它已经不在目录里,此处再调只会撞上那道 fail-closed 的"缺少工具"守卫。
|
||||
if agent_runtime_plan_root_supervisor_tools_for_stage(stage)
|
||||
.contains(&"agent.goal_contract")
|
||||
{
|
||||
restrict_plan_root_goal_contract_schema(&mut request.function_tools)?;
|
||||
}
|
||||
}
|
||||
if runtime_owner_artifact_validation_available {
|
||||
remove_autonomous_owner_manual_verification_tools(&mut request.function_tools)?;
|
||||
|
||||
+4
-1
@@ -1290,7 +1290,10 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
// 这类「必须包含 agent.spawn_isolated」的检查硬失败,晚于分支则
|
||||
// 保证任何修复轮都不会把被裁掉的 36 个工具重新广告回去。
|
||||
if plan_root {
|
||||
retain_plan_root_supervisor_native_tools(&mut request.function_tools)?;
|
||||
retain_plan_root_supervisor_native_tools(
|
||||
&mut request.function_tools,
|
||||
plan_root_supervisor_stage_at(root, agent_id, run_id)?,
|
||||
)?;
|
||||
}
|
||||
request.enable_web_search = false;
|
||||
}
|
||||
|
||||
+111
-5
@@ -112,20 +112,26 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
}
|
||||
|
||||
/// plan 根 Supervisor(`source == project-supervisor-plan`)在整条策划链路里
|
||||
/// 只负责四件事:冻结 Goal Contract、委派与续跑 `project-planning`、代子 Agent
|
||||
/// 向用户提问、按 §13.0 取证后建审批卡。策划内容全部由子 Agent 生产,Supervisor
|
||||
/// 不写文件、不跑命令、不做预览、不生成素材、不调度 ready 任务、不并行委派。
|
||||
/// 只负责三件事:冻结 Goal Contract、委派与续跑 `project-planning`、按 §13.0 取证
|
||||
/// 后建审批卡。策划内容全部由子 Agent 生产,Supervisor 不写文件、不跑命令、不做
|
||||
/// 预览、不生成素材、不调度 ready 任务、不并行委派。
|
||||
///
|
||||
/// 全量注册表会把 43 个原生工具摆在 Provider 眼前,其中约 36 个在 plan 根都会被
|
||||
/// 全量注册表会把 43 个原生工具摆在 Provider 眼前,其中绝大多数在 plan 根都会被
|
||||
/// 执行层拒绝——广告出去只会诱导 Supervisor 自己下场干活。这里给出 plan 根的
|
||||
/// exact allowlist,Provider 请求目录和 system prompt 的工具清单共用它,二者不得
|
||||
/// 各自维护一份。
|
||||
///
|
||||
/// **`user.input_request` 不在其中**:澄清卡不是 Supervisor 发的。子 Agent 以
|
||||
/// `AGC_NEEDS_USER_INPUT_V1` 终态信封退出后,Runtime 在 parent-wake 屏障处自己按
|
||||
/// 信封原文构造 `user.input_request` pending 并且**不恢复父 run**
|
||||
/// (`ensure_static_delegate_user_input_wait_at_locked`)。Supervisor 因此永远收不到
|
||||
/// needs-user-input observation,也就没有调用它的时机;广告出去只会让它在别的时点
|
||||
/// 调一次,撞上 Runtime 已经装好的那份 pending 而硬失败。
|
||||
///
|
||||
/// `file.read`、`agent.acceptance_update`、`agent.action_history` 只为 §13.0 的审批
|
||||
/// 前置取证门存在(分页读 `game/fast_gdd.md` 并列出全部分页 actionId)。
|
||||
pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static str] {
|
||||
&[
|
||||
"user.input_request",
|
||||
"file.read",
|
||||
"agent.delegate",
|
||||
"agent.goal_contract",
|
||||
@@ -135,6 +141,106 @@ pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static s
|
||||
]
|
||||
}
|
||||
|
||||
/// plan 根在链路上的推进阶段。
|
||||
///
|
||||
/// 工具面按阶段收窄,是因为「工具在它无用的阶段仍然可见」会直接制造活锁:实测一次
|
||||
/// 生产 run 里 Supervisor 冻结合同后没有委派,改成反复调 `agent.run_status` 去查一个
|
||||
/// 根本不存在的委派,24 轮里 58 次 `agent.run_status`、0 次 `agent.delegate`,一直烧
|
||||
/// 到超时。空转闸门也拦不住——只读调用同样会把 `plan_update_idle_rounds` 清零。
|
||||
///
|
||||
/// 本地原型没有这个问题,因为它的 Supervisor 只有四个工具且每一个都推进链路:
|
||||
/// 「调了工具」和「推进了链路」在那边是同一件事。这里把同一性质移植过来——每个
|
||||
/// 阶段只广告该阶段能真正推进链路的工具。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PlanRootSupervisorStage {
|
||||
/// Goal Contract 尚未冻结:本轮唯一能推进的动作是冻结它。
|
||||
GoalContract,
|
||||
/// 合同已冻结但本根 run 还没有任何委派:唯一能推进的动作是派出策划子 Agent。
|
||||
Delegate,
|
||||
/// 已有委派:取证、返工与审批相关工具全部开放。
|
||||
Delegated,
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_plan_root_supervisor_tools_for_stage(
|
||||
stage: PlanRootSupervisorStage,
|
||||
) -> &'static [&'static str] {
|
||||
match stage {
|
||||
PlanRootSupervisorStage::GoalContract => &["agent.goal_contract"],
|
||||
PlanRootSupervisorStage::Delegate => &["agent.delegate"],
|
||||
// 合同已冻结且不可重写,再广告 agent.goal_contract 只会诱导一次必被拒的调用。
|
||||
PlanRootSupervisorStage::Delegated => &[
|
||||
"file.read",
|
||||
"agent.delegate",
|
||||
"agent.acceptance_update",
|
||||
"agent.action_history",
|
||||
"agent.run_status",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_root_stage_tests {
|
||||
use super::*;
|
||||
|
||||
/// 各阶段并集必须正好等于 allowlist:prompt 头部按 allowlist 列工具,若某个工具
|
||||
/// 只出现在某一阶段而不在 allowlist 里,头部就会漏掉它;反之则是广告了一个永远
|
||||
/// 拿不到的工具。两侧都是「合同说有、请求里没有」的自相矛盾。
|
||||
#[test]
|
||||
fn every_stage_tool_is_part_of_the_plan_root_allowlist_and_the_union_covers_it() {
|
||||
let allowlist = agent_runtime_plan_root_supervisor_tools()
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let union = [
|
||||
PlanRootSupervisorStage::GoalContract,
|
||||
PlanRootSupervisorStage::Delegate,
|
||||
PlanRootSupervisorStage::Delegated,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|stage| {
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(stage)
|
||||
.iter()
|
||||
.copied()
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(union, allowlist);
|
||||
}
|
||||
|
||||
/// 活锁的诱因是「工具在它无用的阶段仍然可见」。这两个阶段各自只能有一个动作。
|
||||
#[test]
|
||||
fn the_pre_delegation_stages_expose_exactly_one_advancing_action() {
|
||||
assert_eq!(
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(
|
||||
PlanRootSupervisorStage::GoalContract
|
||||
),
|
||||
&["agent.goal_contract"]
|
||||
);
|
||||
assert_eq!(
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(PlanRootSupervisorStage::Delegate),
|
||||
&["agent.delegate"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 只按 durable 事实判定阶段,不看 Provider 说了什么。
|
||||
pub(crate) fn plan_root_supervisor_stage_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<PlanRootSupervisorStage, String> {
|
||||
if read_game_creator_agent_runtime_goal_contract_at(root, agent_id, run_id)?.is_none() {
|
||||
return Ok(PlanRootSupervisorStage::GoalContract);
|
||||
}
|
||||
let delegated = list_static_delegate_deliveries_at(root)?
|
||||
.into_iter()
|
||||
.any(|delivery| delivery.parent_agent_id == agent_id && delivery.parent_run_id == run_id);
|
||||
Ok(if delegated {
|
||||
PlanRootSupervisorStage::Delegated
|
||||
} else {
|
||||
PlanRootSupervisorStage::Delegate
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
||||
agent_runtime_executable_tools()
|
||||
.into_iter()
|
||||
|
||||
@@ -207,6 +207,30 @@ fn finish_plan_submit_business_rejection_limit_at(
|
||||
/// Runtime state 上,进程重启不能把一次活锁洗成新的无限 Provider 开销。
|
||||
const AGENT_RUNTIME_PLAN_UPDATE_IDLE_LIMIT: u32 = 4;
|
||||
|
||||
/// 纯只读工具不算「推进」。
|
||||
///
|
||||
/// 空转计数只在**裸 `update_agent_plan` 且步骤没有真实变化**时累加,早期实现却让
|
||||
/// 任意非空 actions 都把它清零。于是「裸计划更新被 blocked → 调一次只读工具 →
|
||||
/// 计数归零」构成一个闸门永远关不上的活锁:实测一次生产 run 24 轮里 58 次
|
||||
/// `agent.run_status`、0 次 `agent.delegate`,一路烧到超时。只读调用证明不了任何
|
||||
/// 推进,不该清零。
|
||||
///
|
||||
/// 纯只读调查不受影响:只要不发裸计划更新,计数根本不会累加,这里也就无事发生。
|
||||
fn agent_runtime_action_can_advance_progress(tool: &str) -> bool {
|
||||
!matches!(
|
||||
tool,
|
||||
"agent.run_status"
|
||||
| "agent.action_history"
|
||||
| "file.read"
|
||||
| "file.list"
|
||||
| "project.index"
|
||||
| "project.search"
|
||||
| "project.diff"
|
||||
| "git.inspect"
|
||||
| "command.output_read"
|
||||
)
|
||||
}
|
||||
|
||||
fn plan_update_idle_limit_reached(idle_rounds: u32) -> bool {
|
||||
idle_rounds >= AGENT_RUNTIME_PLAN_UPDATE_IDLE_LIMIT
|
||||
}
|
||||
@@ -215,6 +239,53 @@ fn plan_update_idle_limit_reached(idle_rounds: u32) -> bool {
|
||||
mod plan_update_idle_guard_tests {
|
||||
use super::*;
|
||||
|
||||
/// 只读工具不得把空转计数清零。这是 run 18 活锁的机制根因:Supervisor 冻结
|
||||
/// 合同后没有委派,改成「裸计划更新被 blocked → 调一次 agent.run_status →
|
||||
/// 计数归零」,24 轮里 58 次 run_status、0 次 delegate,闸门一次没响。
|
||||
#[test]
|
||||
fn read_only_tools_do_not_count_as_progress() {
|
||||
for tool in [
|
||||
"agent.run_status",
|
||||
"agent.action_history",
|
||||
"file.read",
|
||||
"file.list",
|
||||
"project.index",
|
||||
"project.search",
|
||||
"project.diff",
|
||||
"git.inspect",
|
||||
"command.output_read",
|
||||
] {
|
||||
assert!(
|
||||
!agent_runtime_action_can_advance_progress(tool),
|
||||
"{tool} 是只读工具,不该被当成推进"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 真正推进 durable 状态的动作照旧清零,否则合法的长链路会被误杀。
|
||||
#[test]
|
||||
fn state_advancing_tools_still_count_as_progress() {
|
||||
for tool in [
|
||||
"agent.delegate",
|
||||
"agent.goal_contract",
|
||||
"agent.acceptance_update",
|
||||
"file.write",
|
||||
"project.patchset",
|
||||
"command.exec",
|
||||
PLAN_SUBMIT_GDD_TOOL,
|
||||
] {
|
||||
assert!(
|
||||
agent_runtime_action_can_advance_progress(tool),
|
||||
"{tool} 会推进 durable 状态,必须清零空转计数"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_update_idle_guard_threshold_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn idle_guard_always_tries_self_repair_before_killing_the_run() {
|
||||
// 摘工具的阈值必须严格小于收束限额,否则 run 会在从没被逼过一次真动作
|
||||
@@ -2189,8 +2260,12 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
action_start_index = 0;
|
||||
}
|
||||
|
||||
if !plan.actions.is_empty() {
|
||||
// 本轮有真实动作,计划没有空转。
|
||||
if plan
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| agent_runtime_action_can_advance_progress(action.tool.trim()))
|
||||
{
|
||||
// 本轮至少有一个能推进 durable 状态的动作,计划没有空转。
|
||||
runtime.plan_update_idle_rounds = 0;
|
||||
}
|
||||
if plan.actions.is_empty() {
|
||||
|
||||
@@ -11,7 +11,8 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::agent::{
|
||||
agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools,
|
||||
AgentRuntimePlanUpdate, AgentRuntimeToolAction, AgentRuntimeToolPlan,
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage, AgentRuntimePlanUpdate,
|
||||
AgentRuntimeToolAction, AgentRuntimeToolPlan, PlanRootSupervisorStage,
|
||||
AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT, AGENT_RUNTIME_CANVAS_ASSET_KINDS,
|
||||
AGENT_RUNTIME_PLAN_STEP_LIMIT, PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION,
|
||||
PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE, PLAN_FAST_GDD_ACCEPTANCE_NODE_ID,
|
||||
@@ -375,12 +376,13 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
|
||||
/// all; that is a configuration error, so fail closed instead.
|
||||
pub(crate) fn retain_plan_root_supervisor_native_tools(
|
||||
functions: &mut Vec<LlmFunctionTool>,
|
||||
stage: PlanRootSupervisorStage,
|
||||
) -> Result<(), String> {
|
||||
let mut allowed = BTreeSet::from([
|
||||
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(),
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(),
|
||||
]);
|
||||
for tool in agent_runtime_plan_root_supervisor_tools() {
|
||||
for tool in agent_runtime_plan_root_supervisor_tools_for_stage(stage) {
|
||||
allowed.insert(
|
||||
native_runtime_function_name(tool)
|
||||
.ok_or_else(|| format!("无法生成 plan 根 Supervisor 工具函数名:{tool}"))?,
|
||||
@@ -2148,29 +2150,55 @@ mod tests {
|
||||
)
|
||||
.expect("build supervisor catalog");
|
||||
let before = functions.len();
|
||||
retain_plan_root_supervisor_native_tools(&mut functions).expect("retain plan root tools");
|
||||
let mut expected = vec![
|
||||
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(),
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(),
|
||||
];
|
||||
for tool in agent_runtime_plan_root_supervisor_tools() {
|
||||
expected.push(native_runtime_function_name(tool).expect("plan root function name"));
|
||||
// 逐个阶段都必须是精确集合:委派前那两档各自只剩一个推进动作,正是活锁
|
||||
// 的解药——模型在那一档连 agent.run_status 都调不出来。
|
||||
for stage in [
|
||||
PlanRootSupervisorStage::GoalContract,
|
||||
PlanRootSupervisorStage::Delegate,
|
||||
PlanRootSupervisorStage::Delegated,
|
||||
] {
|
||||
let mut staged = functions.clone();
|
||||
retain_plan_root_supervisor_native_tools(&mut staged, stage)
|
||||
.expect("retain plan root tools");
|
||||
let mut expected = vec![
|
||||
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(),
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(),
|
||||
];
|
||||
for tool in agent_runtime_plan_root_supervisor_tools_for_stage(stage) {
|
||||
expected.push(native_runtime_function_name(tool).expect("plan root function name"));
|
||||
}
|
||||
expected.sort();
|
||||
let mut actual = staged
|
||||
.iter()
|
||||
.map(|function| function.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
actual.sort();
|
||||
assert_eq!(actual, expected, "阶段 {stage:?} 的工具目录不是精确集合");
|
||||
assert!(
|
||||
before > staged.len(),
|
||||
"收窄必须真的裁掉工具,否则这条测试是空跑"
|
||||
);
|
||||
// MCP 前缀的动态工具同样不得残留:plan 根整条链路不调 MCP。
|
||||
assert!(!staged
|
||||
.iter()
|
||||
.any(|function| function.name.starts_with(AGENT_RUNTIME_NATIVE_MCP_PREFIX)));
|
||||
}
|
||||
// 广告层不得再出现 user.input_request:澄清卡由 Runtime 在 parent-wake 屏障处
|
||||
// 直接按信封原文构造,Supervisor 没有调用它的时机。
|
||||
let user_input_function =
|
||||
native_runtime_function_name("user.input_request").expect("user input function name");
|
||||
for stage in [
|
||||
PlanRootSupervisorStage::GoalContract,
|
||||
PlanRootSupervisorStage::Delegate,
|
||||
PlanRootSupervisorStage::Delegated,
|
||||
] {
|
||||
let mut staged = functions.clone();
|
||||
retain_plan_root_supervisor_native_tools(&mut staged, stage)
|
||||
.expect("retain plan root tools");
|
||||
assert!(!staged
|
||||
.iter()
|
||||
.any(|function| function.name == user_input_function));
|
||||
}
|
||||
expected.sort();
|
||||
let mut actual = functions
|
||||
.iter()
|
||||
.map(|function| function.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
actual.sort();
|
||||
assert_eq!(actual, expected);
|
||||
assert!(
|
||||
before > functions.len(),
|
||||
"收窄必须真的裁掉工具,否则这条测试是空跑"
|
||||
);
|
||||
// MCP 前缀的动态工具同样不得残留:plan 根整条链路不调 MCP。
|
||||
assert!(!functions
|
||||
.iter()
|
||||
.any(|function| function.name.starts_with(AGENT_RUNTIME_NATIVE_MCP_PREFIX)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user