修复游戏智能体计划拒绝后的重复规划
Project CI / Repository checks (push) Successful in 1m5s
Project CI / Frontend tests (push) Successful in 3m22s
Project CI / Backend tests (push) Successful in 3m55s
Project CI / Native shell tests (push) Failing after 12m42s

在 runtime.plan_update 被拒绝后按角色收窄下一轮工具目录

恢复 Supervisor 首次项目修改的活性门禁

补充请求目录与活性门禁回归测试

同步更新游戏创作智能体实施规则
This commit is contained in:
2026-08-12 17:00:50 +08:00
parent 8abeb333bb
commit effdaefc63
3 changed files with 158 additions and 1 deletions
@@ -702,6 +702,21 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
.actions
.iter()
.any(|action| action.tool.trim() == "agent.route_manifest");
// A Supervisor without a mutation must still hit the same first-mutation
// liveness gate as every other autonomous Agent. Previously the presence
// of the Supervisor role alone could bypass this check.
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& loop_index > AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT
&& verification_gate.mutation_revision.is_none()
&& !has_mutation
&& !has_code_asset_route
&& plan.response.trim().is_empty()
&& !has_specialist_delegation
{
return Err(format!(
"{AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX}Supervisor 尚未提交首次项目 mutation 或有效协作动作,禁止继续只规划、读取、验证或空转"
));
}
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
if let Some(failed_playtest_revision) = verification_gate.failed_playtest_revision {
if project_revision < failed_playtest_revision {
@@ -1911,4 +1926,39 @@ mod tests {
.is_err());
}
}
#[test]
fn autonomous_supervisor_without_playtest_still_hits_pre_mutation_liveness_gate() {
let verification_gate = AgentRuntimeVerificationGate {
schema_version: "test".to_string(),
project_id: "test".to_string(),
agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
run_id: "supervisor-pre-mutation".to_string(),
requires_verification: false,
mutation_revision: None,
verified_revision: None,
last_mutation_tool: None,
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
failed_playtest_revision: None,
updated_at: 0,
};
let plan = AgentRuntimeToolPlan {
thinking_summary: "继续只读规划".to_string(),
..AgentRuntimeToolPlan::default()
};
let error = validate_agent_runtime_autonomous_plan_liveness(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1,
0,
&verification_gate,
&[],
&plan,
false,
false,
)
.expect_err("Supervisor without a playtest must not bypass pre-mutation liveness");
assert!(error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX));
}
}
@@ -316,6 +316,43 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
tool.name != goal_contract_function && tool.name != acceptance_update_function
});
}
// A rejected structured-plan update is a request-scoped liveness signal.
// The next Provider turn must perform the concrete mutation (or deliver a
// read-only result) instead of entering another planning loop.
let latest_plan_rejection = observations.iter().rposition(|observation| {
observation.tool == "runtime.plan_update" && observation.status == "rejected"
});
let plan_rejection_needs_repair = latest_plan_rejection.is_some_and(|index| {
!observations[index + 1..]
.iter()
.any(is_agent_runtime_project_mutation_observation)
});
if autonomous_game_build && plan_rejection_needs_repair {
let supervisor_orchestrator_repair = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
let policy = resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)?.policy;
let state = read_supervisor_collaboration_state_at(root, agent_id, run_id)?;
policy.orchestrator_only_after_delegation && state.has_collaboration()
} else {
false
};
let repair_tools: &[&str] = if supervisor_orchestrator_repair {
&["agent.delegate", "agent.run_status"]
} else {
&["file.write", "file.patch", "file.delete", "project.patchset", "project.restore", "canvas.asset_generate"]
};
let mut allowed_function_names = BTreeSet::from([AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string()]);
for tool in repair_tools {
if let Some(name) = native_runtime_function_name(tool) {
allowed_function_names.insert(name);
}
}
request.function_tools.retain(|tool| allowed_function_names.contains(&tool.name));
request.messages.push(LlmMessage::user(if supervisor_orchestrator_repair {
"上一轮 runtime.plan_update 被拒绝。本轮 Supervisor 已进入协作编排模式,只能调用 agent.run_status 或 agent.delegate 继续收束,或在证据足够时 respond_to_user;禁止再次规划、读取、搜索、验证或直接修改项目。"
} else {
"上一轮 runtime.plan_update 被拒绝。本轮必须立即提交当前 in_progress 步骤对应的实际项目 mutation,或在只读合同已满足时 respond_to_user;禁止再次规划、读取、搜索、验证、委派或普通文本解释。"
}));
}
if autonomous_game_build && !editor_api_key_is_configured() {
let canvas_function = native_runtime_function_name("canvas.asset_generate")
.ok_or_else(|| "无法生成画布素材工具函数名".to_string())?;
@@ -530,11 +567,14 @@ mod tests {
game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at,
provider_command_exec_contract, provider_command_start_contract,
required_runtime_prompt_section, resolve_agent_conversation_session_id_at,
start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolPlan,
start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolObservation,
AgentRuntimeToolPlan,
GameCreatorMcpCatalog, GameCreatorMcpCatalogTool,
AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
};
@@ -556,6 +596,71 @@ mod tests {
.collect()
}
#[test]
fn rejected_plan_update_forces_request_scoped_mutation_catalog() {
let directory = crate::tests::canonical_test_tempdir("provider-plan-rejection-repair-");
let root = directory.path().join("project");
init_local_game_project_at(&root, "plan-rejection-repair", "修复现有游戏")
.expect("project init");
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"plan-rejection-repair-root",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind root");
let state = start_game_creator_agent_runtime_task_at(
&root,
&binding.agent_id,
"修复现有游戏",
&binding.run_id,
&binding.source,
"执行当前计划中的项目修改",
vec!["立即修改 game/index.html".to_string()],
)
.expect("start task");
let catalog = GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
};
let rejected = AgentRuntimeToolObservation {
tool: "runtime.plan_update".to_string(),
status: "rejected".to_string(),
summary: "结构化计划更新被 Runtime 拒绝".to_string(),
detail: Some("计划状态回退".to_string()),
};
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&state.current_task,
&[rejected],
1,
&catalog,
)
.expect("build request");
let names = request
.function_tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<std::collections::BTreeSet<_>>();
assert!(names.contains(
crate::agent_native_tools::native_runtime_function_name("file.patch")
.expect("file.patch function")
.as_str()
));
assert!(names.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
assert!(!names.contains(AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME));
assert!(request
.messages
.iter()
.any(|message| message.content.contains("runtime.plan_update 被拒绝")));
}
fn build_request_system_prompt_for_root_source(
agent_id: &str,
root_source: &str,