立项策划根 run 的工具面收窄成 7 个原生工具的 exact allowlist
plan 根 Supervisor 此前拿到的是未按身份收窄的全量注册表:43 个原生工具外加 项目 MCP 目录。其中约 36 个在这条链路上根本执行不了——写入、补丁、删除、 命令、预览、画布、任务图、记忆、黑板、agent.spawn_isolated、 agent.route_manifest、agent.schedule_ready 都会被执行层拒绝。 广告一个执行不了的工具不是中性的。M1A-4 已经实测到同类后果:上下文里残留的 专业角色目录让 Supervisor 照着发起 agent.delegate,被拒后再没能自行改回 project-planning,整个 run 空转到预算耗尽。工具面同理——本链路的策划内容全部 由 project-planning 子 Agent 生产,Supervisor 手里多一个写文件或跑命令的入口, 就多一条它自己下场干活的诱导路径。 allowlist 定为 user.input_request、file.read、agent.delegate、 agent.goal_contract、agent.acceptance_update、agent.action_history、 agent.run_status,加上两个协议控制函数。后三个原生工具只为 §13.0 的审批前置 取证门存在(分页读 game/fast_gdd.md 并列出全部分页 actionId);file.list 一并 砍掉,取证路径是固定的,不需要列目录。 实现沿用 restrict_plan_root_goal_contract_schema 已有的后置收窄形状,而不是给 build_agent_runtime_native_function_tools_for_agent 加参数:两处调用点本来就在 `if plan_root` 里,改动面更小,也不会波及其它 lane 的目录构建。 收窄写成交集而非断言。协议修复路径会先整份重建目录再按场景收窄,其中 restrict_agent_runtime_supervisor_collaboration_repair_tools 硬要求目录里存在 agent.spawn_isolated;plan 根这一遍必须排在所有分支收窄之后取交集,早于分支 会让那条检查失败,晚于分支才能保证任何修复轮都不把被裁掉的 36 个工具重新广告 回去。 回归断言写成精确集合而不是「不包含某几个」,将来往注册表加工具不会静默漏进 plan 根;同时断言 MCP 前缀工具不残留、收窄确实裁掉了东西(否则测试是空跑)。 已变异验证。 本次只改广告目录。提示词头部仍按全量注册表拼工具清单,二者的一致性由紧随其后 的 prompt 提交收口——单独看这一笔存在「合同说有、请求里没有」的过渡态。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,7 @@ pub(crate) use tool_plan_protocol::{
|
||||
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_tool_policy_snapshot_for_run_at,
|
||||
AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PROJECT_PLANNING_ACTION_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_PROJECT_PLANNING_ACTION_TOOLS,
|
||||
};
|
||||
|
||||
+1
@@ -553,6 +553,7 @@ 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)?;
|
||||
}
|
||||
if runtime_owner_artifact_validation_available {
|
||||
|
||||
@@ -1284,6 +1284,14 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n请修复格式,只调用当前请求广告的 update_agent_plan、动作工具或 respond_to_user;当前 in_progress 步骤已具备执行条件时,格式修复必须保留并调用对应动作工具,不能退化为只调用 update_agent_plan。不要解释,不要 markdown,不要代码围栏,也不要把计划、动作或回复放进普通文本。"
|
||||
)));
|
||||
}
|
||||
// 修复分支会先整份重建工具目录,再按各自的场景收窄。plan 根必须在
|
||||
// 所有分支收窄之后再取一次交集:早于分支就会让
|
||||
// `restrict_agent_runtime_supervisor_collaboration_repair_tools`
|
||||
// 这类「必须包含 agent.spawn_isolated」的检查硬失败,晚于分支则
|
||||
// 保证任何修复轮都不会把被裁掉的 36 个工具重新广告回去。
|
||||
if plan_root {
|
||||
retain_plan_root_supervisor_native_tools(&mut request.function_tools)?;
|
||||
}
|
||||
request.enable_web_search = false;
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
+24
@@ -111,6 +111,30 @@ 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 任务、不并行委派。
|
||||
///
|
||||
/// 全量注册表会把 43 个原生工具摆在 Provider 眼前,其中约 36 个在 plan 根都会被
|
||||
/// 执行层拒绝——广告出去只会诱导 Supervisor 自己下场干活。这里给出 plan 根的
|
||||
/// exact allowlist,Provider 请求目录和 system prompt 的工具清单共用它,二者不得
|
||||
/// 各自维护一份。
|
||||
///
|
||||
/// `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",
|
||||
"agent.acceptance_update",
|
||||
"agent.action_history",
|
||||
"agent.run_status",
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
||||
agent_runtime_executable_tools()
|
||||
.into_iter()
|
||||
|
||||
@@ -10,11 +10,12 @@ use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::agent::{
|
||||
agent_runtime_native_executable_tools, AgentRuntimePlanUpdate, AgentRuntimeToolAction,
|
||||
AgentRuntimeToolPlan, 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, PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL,
|
||||
agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools,
|
||||
AgentRuntimePlanUpdate, AgentRuntimeToolAction, AgentRuntimeToolPlan,
|
||||
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,
|
||||
PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL,
|
||||
};
|
||||
use crate::mcp::{
|
||||
validate_game_creator_mcp_tool_arguments, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool,
|
||||
@@ -360,6 +361,38 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
|
||||
Ok(functions)
|
||||
}
|
||||
|
||||
/// Narrow the plan root Supervisor's advertised function catalog down to
|
||||
/// `agent_runtime_plan_root_supervisor_tools()` plus the two protocol controls.
|
||||
///
|
||||
/// This is an intersection, not an assertion: the caller may already have
|
||||
/// narrowed the catalog further for a protocol-repair turn (for example
|
||||
/// `restrict_agent_runtime_supervisor_collaboration_repair_tools`), and this
|
||||
/// pass must never widen it back. MCP function tools carry a different prefix
|
||||
/// and are dropped here too — the plan lane never calls MCP.
|
||||
///
|
||||
/// An empty result means the repair-branch allowlist and the plan-root
|
||||
/// allowlist are disjoint, which would send a request with no callable tool at
|
||||
/// all; that is a configuration error, so fail closed instead.
|
||||
pub(crate) fn retain_plan_root_supervisor_native_tools(
|
||||
functions: &mut Vec<LlmFunctionTool>,
|
||||
) -> 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() {
|
||||
allowed.insert(
|
||||
native_runtime_function_name(tool)
|
||||
.ok_or_else(|| format!("无法生成 plan 根 Supervisor 工具函数名:{tool}"))?,
|
||||
);
|
||||
}
|
||||
functions.retain(|function| allowed.contains(&function.name));
|
||||
if functions.is_empty() {
|
||||
return Err("plan 根 Supervisor 工具目录收窄后为空".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Narrow only the request-scoped Goal Contract schema used by the plan root.
|
||||
/// The capability registry itself must remain dynamic: game-chat and ordinary
|
||||
/// Supervisor runs still author their own acceptance graph.
|
||||
@@ -2101,6 +2134,45 @@ mod tests {
|
||||
assert!(description.contains("runId 必须为 null"));
|
||||
}
|
||||
|
||||
/// plan 根 Supervisor 只广告 7 个原生工具 + 2 个协议控制。全量注册表里其余
|
||||
/// 约 36 个在这条链路上全部会被执行层拒绝,广告出去只会诱导 Supervisor 自己
|
||||
/// 下场写文件、跑命令、查任务图。断言写成精确集合而不是「不包含某几个」,
|
||||
/// 这样将来往注册表里加工具不会静默漏进 plan 根。
|
||||
#[test]
|
||||
fn plan_root_supervisor_tool_catalog_is_an_exact_allowlist() {
|
||||
let mcp_catalog =
|
||||
native_mcp_catalog(json!({"type": "object", "additionalProperties": false}));
|
||||
let mut functions = build_agent_runtime_native_function_tools_for_agent(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&mcp_catalog,
|
||||
)
|
||||
.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"));
|
||||
}
|
||||
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]
|
||||
fn native_goal_contract_and_acceptance_update_expose_dynamic_graph_contract() {
|
||||
let goal = runtime_tool_input_schema("agent.goal_contract");
|
||||
|
||||
Reference in New Issue
Block a user