补齐Agent Runtime工具计划强杀恢复门禁

新增真实 Provider 工具计划交接 checkpoint、Runner 强杀恢复与零重放验收。

收紧 Supervisor 首波协作、返工 runId 和持久 delivery 合同。

扩展敏感正文扫描、跨平台安全回归和 E2E 自测。

注册两级 npm 命令并同步 Runtime 验收文档与当前证据。
This commit is contained in:
AIGameCreator App
2026-07-20 15:48:07 +08:00
parent 791a50ffe8
commit 0fbb76fb94
11 changed files with 3012 additions and 51 deletions
+1
View File
@@ -20,6 +20,7 @@
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
"agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-final-reply-transient-retry",
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -492,7 +492,18 @@ fn validate_native_agent_delegate_input(
160,
true,
)?;
validate_native_delegate_string(object.get("runId"), "runId", 160, true)
validate_native_delegate_string(object.get("runId"), "runId", 160, true)?;
if object
.get("repairOfDelegationId")
.is_some_and(Value::is_string)
&& !object.get("runId").is_some_and(Value::is_null)
{
return Err(protocol_error(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 原生工具协议错误:agent.delegate 返工委派时 runId 必须为 JSON null",
));
}
Ok(())
}
fn validate_native_delegate_string(
@@ -685,7 +696,9 @@ fn runtime_tool_description(tool: &str) -> &'static str {
"canvas.asset_generate" => "通过已配置平台生成并登记首版美术素材。",
"blackboard.write" => "向项目级共享黑板追加稳定结论。",
"agent.message" => "向一个目标 Agent 写入定向上下文消息。",
"agent.delegate" => "用持久验收合同把边界清晰的后台任务委派给另一个 Agent。",
"agent.delegate" => {
"用持久验收合同把边界清晰的后台任务委派给另一个 Agent;返工时 repairOfDelegationId 指向原 delivery,且 runId 必须为 null。"
}
"agent.spawn_isolated" => "创建最多三个写范围互不重叠的隔离子 Agent。",
"agent.schedule_ready" => "调度依赖已完成的 ready manifest 任务。",
"agent.action_history" => "查询当前 Agent 的持久终态动作历史。",
@@ -1035,3 +1048,66 @@ fn project_patchset_input_schema() -> Value {
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_delegate_input(repair_of_delegation_id: Value, run_id: Value) -> Value {
json!({
"agentId": "specialist",
"task": "完成委派任务",
"acceptanceCriteria": ["定向测试通过"],
"expectedArtifacts": [],
"repairOfDelegationId": repair_of_delegation_id,
"runId": run_id,
})
}
#[test]
fn native_agent_delegate_repair_rejects_string_run_id() {
let repair_id = "delegation-value-must-not-leak";
let run_id = "run-value-must-not-leak";
let error = validate_native_agent_delegate_input(&valid_delegate_input(
json!(repair_id),
json!(run_id),
))
.expect_err("repair delegate must not accept a string runId");
assert_eq!(
error.kind(),
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema
);
let detail = error.to_string();
assert_eq!(
detail,
"Agent 原生工具协议错误:agent.delegate 返工委派时 runId 必须为 JSON null"
);
assert!(!detail.contains(repair_id));
assert!(!detail.contains(run_id));
}
#[test]
fn native_agent_delegate_repair_accepts_null_run_id() {
let input = valid_delegate_input(json!("delegation-id"), Value::Null);
validate_native_agent_delegate_input(&input)
.expect("repair delegate should accept a null runId");
}
#[test]
fn native_agent_delegate_initial_accepts_string_run_id() {
let input = valid_delegate_input(Value::Null, json!("initial-run-id"));
validate_native_agent_delegate_input(&input)
.expect("initial delegate should accept a valid string runId");
}
#[test]
fn native_agent_delegate_description_explains_repair_run_identity() {
let description = runtime_tool_description("agent.delegate");
assert!(description.contains("repairOfDelegationId 指向原 delivery"));
assert!(description.contains("runId 必须为 null"));
}
}
@@ -778,12 +778,26 @@ pub(crate) fn preflight_supervisor_collaboration_plan(
policy: &SupervisorCollaborationPolicy,
state: &SupervisorCollaborationState,
) -> Result<SupervisorCollaborationPreflight, String> {
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || actions.is_empty() {
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
return Ok(SupervisorCollaborationPreflight::default());
}
let policy = normalize_supervisor_collaboration_policy(policy.clone())?;
let summary = summarize_supervisor_collaboration_actions(actions)?;
let initial_wave = !state.has_collaboration();
if actions.is_empty() {
if initial_wave && supervisor_collaboration_policy_has_initial_requirements(&policy) {
return Ok(SupervisorCollaborationPreflight {
violation: Some(SupervisorCollaborationViolation {
summary: "Project Supervisor 首批协作不能停留在计划更新".to_string(),
detail: "当前父 run 尚无协作事实,且项目 policy 明确要求首批专业协作;首批不能停留在计划更新,必须在同一 Provider 批次完整提交协作。".to_string(),
}),
..SupervisorCollaborationPreflight::default()
});
}
return Ok(SupervisorCollaborationPreflight::default());
}
let summary = summarize_supervisor_collaboration_actions(actions)?;
let has_collaboration_action = summary.has_collaboration_action();
if !initial_wave
@@ -1363,6 +1377,58 @@ mod tests {
assert!(error.contains("minIsolatedGroupsBeforeClaim 不能超过 16"));
}
#[test]
fn supervisor_collaboration_policy_blocks_empty_required_initial_wave() {
let result = preflight_supervisor_collaboration_plan(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&[],
&mixed_policy(),
&SupervisorCollaborationState::default(),
)
.expect("preflight empty required initial wave");
let violation = result
.violation
.expect("required initial wave must reject empty actions");
assert_eq!(
violation.summary,
"Project Supervisor 首批协作不能停留在计划更新"
);
assert!(violation.detail.contains("首批不能停留在计划更新"));
assert!(violation
.detail
.contains("必须在同一 Provider 批次完整提交协作"));
assert!(result.contract.is_none());
assert!(!result.force_durable_batch);
}
#[test]
fn supervisor_collaboration_policy_allows_empty_actions_after_collaboration() {
let state = SupervisorCollaborationState {
initial_static_agent_ids: vec!["design-director".to_string()],
..SupervisorCollaborationState::default()
};
let result = preflight_supervisor_collaboration_plan(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&[],
&mixed_policy(),
&state,
)
.expect("preflight empty actions after collaboration");
assert_eq!(result, SupervisorCollaborationPreflight::default());
}
#[test]
fn supervisor_collaboration_policy_allows_empty_actions_without_initial_requirements() {
let result = preflight_supervisor_collaboration_plan(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&[],
&SupervisorCollaborationPolicy::default(),
&SupervisorCollaborationState::default(),
)
.expect("preflight empty actions without initial requirements");
assert_eq!(result, SupervisorCollaborationPreflight::default());
}
#[test]
fn supervisor_collaboration_policy_blocks_partial_mixed_wave() {
let result = preflight_supervisor_collaboration_plan(
File diff suppressed because it is too large Load Diff