立项策划:落地 M1A-4,plan 根 run 只能委派策划子 Agent
补 M1A-2 的反方向。原先只做了「目标是 project-planning 时要求父是 plan 根」,反过来「父是 plan 根时目标必须是 project-planning」没做, 也没记为 deferred。后果是 plan 根 run 可以委派任意专业 Agent,而被 委派者拿常规 standard 工具面(能写文件、跑命令),第 24 节「策划全程 零构建」当时只由 Prompt 兜底。 执行层:observe_agent_runtime_agent_delegate 补对称分支; observe_agent_runtime_agent_spawn_isolated 对 plan 根 run 一律拒——它是 第二条造子 Agent 的通道,只堵 delegate 等于留后门。两条共用 typed kind=plan-root-child-target-unsupported。 强弱判据分工与 M1A-3 一致:弱判据从 task journal 读 source 决定是否 管辖,强判据 validate_project_supervisor_plan_root_binding_at 决定是否 合法,其 Err 永远落进拒绝分支。不可写成 is_ok() 当作「不是 plan 根」, 那会在 binding 损坏时放行任意子 Agent 创建。 上下文层:plan source 下不拼 supervisorIntro 与 $visualContract。不改 .md 内容,不新增 composition key。 三条有意保留的取舍已冻结进 decision-log,非待办: $isolatedAgentTemplates 仍列出专业角色名(被执行层硬拒后的死文本, 上下文层收窄边界到此为止);task journal 读取失败对所有 source fail closed(改成放行是新的 fail-open);plan 根 prompt 断言偏弱 (执行层是该场景唯一保障)。 同步方案 §22 证据表、§23.8 PR 表、§24 不变量,并订正 M1A-2 决策条里 含糊的「Supervisor 根 run 继续使用现役工具面」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -525,6 +525,7 @@ fn game_creator_art_asset_plan_tool_plan_prompt(
|
||||
|
||||
pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
agent_id: &str,
|
||||
source: &str,
|
||||
) -> String {
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return game_creator_project_planning_tool_plan_system_prompt();
|
||||
@@ -548,7 +549,7 @@ pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return prompt;
|
||||
}
|
||||
game_creator_project_supervisor_tool_plan_prompt(&prompt, editor_api_key_is_configured())
|
||||
game_creator_project_supervisor_tool_plan_prompt(&prompt, editor_api_key_is_configured(), source)
|
||||
}
|
||||
|
||||
/// The planning child has an exact native allowlist. Do not reuse the broad
|
||||
@@ -564,10 +565,20 @@ fn game_creator_project_planning_tool_plan_system_prompt() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// plan 根 run(`source == project-supervisor-plan`)的策划链路合同是「全程零
|
||||
/// 构建、零文件写」,只允许委派 project-planning 这一个子 Agent(见 M1A-4)。
|
||||
/// 因此它的 Supervisor system prompt 不拼 `$visualContract`(点名
|
||||
/// art-director/design-foundation/art-asset-plan/code-prototype 的视觉产物
|
||||
/// 合同)和 `supervisorIntro`(要求「自行查看静态角色目录,选择最匹配的不同
|
||||
/// 专业 Agent」)——两段都在暗示存在可并行委派的专业组,而 plan 根 run 没有
|
||||
/// 这个能力。除 plan 根以外的一切 source(gui/cli/game-chat/未知)都必须逐字
|
||||
/// 保留既有合成结果,不能被这里的分支误伤。
|
||||
fn game_creator_project_supervisor_tool_plan_prompt(
|
||||
prompt: &str,
|
||||
editor_api_key_is_configured: bool,
|
||||
source: &str,
|
||||
) -> String {
|
||||
let plan_root = agent_runtime_supervisor_source_is_plan(source);
|
||||
let visual_section = if editor_api_key_is_configured {
|
||||
RUNTIME_PROMPT_VISUAL_EDITOR_SECTION
|
||||
} else {
|
||||
@@ -577,7 +588,12 @@ fn game_creator_project_supervisor_tool_plan_prompt(
|
||||
RUNTIME_PROMPT_SUPERVISOR_COMPOSITION,
|
||||
|marker| match marker {
|
||||
"$base" => Some(prompt),
|
||||
"$visualContract" => Some(required_runtime_prompt_section(visual_section)),
|
||||
"$visualContract" => Some(if plan_root {
|
||||
""
|
||||
} else {
|
||||
required_runtime_prompt_section(visual_section)
|
||||
}),
|
||||
"supervisorIntro" if plan_root => Some(""),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
@@ -597,19 +613,25 @@ pub(crate) fn required_runtime_prompt_section(section_id: &str) -> &'static str
|
||||
.unwrap_or_else(|| panic!("生成的 Prompt Bundle 缺少 section:{section_id}"))
|
||||
}
|
||||
|
||||
/// 每个 composition item 先交给 `dynamic` 闭包,让调用方有机会按运行时条件
|
||||
/// (如 M1A-4 的 plan 根 source)覆盖或整段跳过(返回 `Some("")`,会在
|
||||
/// `render_runtime_prompt_sections` 里被 trim 后过滤掉)。`dynamic` 返回
|
||||
/// `None` 表示"这个 item 不关心":`$` 开头的动态 marker 必须有人接管,找不到
|
||||
/// 就是 Prompt Bundle 配置错误,直接 panic;普通 section id 则退回既有的
|
||||
/// `required_runtime_prompt_section` 查找。既有调用方的闭包对不认识的普通
|
||||
/// section id 一律返回 None,因此这次改动不改变它们的既有输出。
|
||||
fn render_runtime_prompt_composition<'a>(
|
||||
composition: &[&str],
|
||||
mut dynamic: impl FnMut(&str) -> Option<&'a str>,
|
||||
) -> String {
|
||||
let sections = composition
|
||||
.iter()
|
||||
.map(|item| {
|
||||
if item.starts_with('$') {
|
||||
dynamic(item)
|
||||
.unwrap_or_else(|| panic!("Prompt composition 缺少动态 marker:{item}"))
|
||||
} else {
|
||||
required_runtime_prompt_section(item)
|
||||
.map(|item| match dynamic(item) {
|
||||
Some(value) => value,
|
||||
None if item.starts_with('$') => {
|
||||
panic!("Prompt composition 缺少动态 marker:{item}")
|
||||
}
|
||||
None => required_runtime_prompt_section(item),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
render_runtime_prompt_sections(§ions)
|
||||
@@ -1135,6 +1157,7 @@ mod tests {
|
||||
let prompt = game_creator_project_supervisor_tool_plan_prompt(
|
||||
"shared runtime contract",
|
||||
editor_api_key_is_configured,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
let sections = [
|
||||
"shared runtime contract",
|
||||
@@ -1159,8 +1182,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn supervisor_editor_prompt_has_one_art_asset_plan_owner_contract() {
|
||||
let prompt = game_creator_project_supervisor_tool_plan_prompt("", true);
|
||||
let without_editor = game_creator_project_supervisor_tool_plan_prompt("", false);
|
||||
let prompt = game_creator_project_supervisor_tool_plan_prompt(
|
||||
"",
|
||||
true,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
let without_editor = game_creator_project_supervisor_tool_plan_prompt(
|
||||
"",
|
||||
false,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
|
||||
assert!(prompt.contains(
|
||||
"art-asset-plan 只声明 assets/manifest.art.json 与 assets/art-spritesheet.png"
|
||||
@@ -1174,12 +1205,94 @@ mod tests {
|
||||
assert!(!without_editor.contains("assets/art-spritesheet.png;不得把 UI 与图集合并"));
|
||||
}
|
||||
|
||||
/// M1A-4:plan 根 run(source == project-supervisor-plan)的 Supervisor
|
||||
/// system prompt 不拼 supervisorIntro 与 $visualContract 这两个 section
|
||||
/// ——两段都在暗示存在可并行委派的专业组,而策划链路的合同是只能委派
|
||||
/// project-planning 一个子 Agent。这里用两段各自最短的、只出现在该 section
|
||||
/// 里的独有句子做断言,不用裸的 agent-id 字面量:`$base`(共享 runtime
|
||||
/// 合同)里的 isolated agent 模板目录本就会列出 art-director /
|
||||
/// design-foundation / art-asset-plan / code-prototype 等全部模板 taskId
|
||||
/// 供 agent.spawn_isolated 使用,这与本次改动无关,也不构成越权——plan 根
|
||||
/// 已经在执行层被 A2 无条件拒绝 agent.spawn_isolated(见 delegation.rs),
|
||||
/// 所以这份共享目录残留只是死文本,不是可利用的委派入口。
|
||||
#[test]
|
||||
fn plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections() {
|
||||
for editor_api_key_is_configured in [false, true] {
|
||||
let plan_prompt = game_creator_project_supervisor_tool_plan_prompt(
|
||||
"shared runtime contract",
|
||||
editor_api_key_is_configured,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
);
|
||||
assert!(
|
||||
!plan_prompt.contains("静态角色目录"),
|
||||
"plan 根 prompt 不应再要求查看静态角色目录:{plan_prompt}"
|
||||
);
|
||||
assert!(
|
||||
!plan_prompt.contains("选择最匹配的不同专业 Agent"),
|
||||
"plan 根 prompt 不应再包含 supervisorIntro 的并行委派指令"
|
||||
);
|
||||
assert!(
|
||||
!plan_prompt.contains("视觉产物始终按 owner 隔离"),
|
||||
"plan 根 prompt 不应再包含 with-editor 视觉合同"
|
||||
);
|
||||
assert!(
|
||||
!plan_prompt.contains("当前未配置 External Editor API Key,art-director 只交付视觉方向文档"),
|
||||
"plan 根 prompt 不应再包含 without-editor 视觉合同"
|
||||
);
|
||||
assert!(
|
||||
plan_prompt.contains("shared runtime contract"),
|
||||
"plan 根 prompt 必须保留 $base"
|
||||
);
|
||||
assert!(plan_prompt.contains(required_runtime_prompt_section("supervisorPlaybook").trim()));
|
||||
assert!(plan_prompt.contains(required_runtime_prompt_section("supervisorClaimGate").trim()));
|
||||
assert!(plan_prompt.contains(required_runtime_prompt_section("supervisorRepair").trim()));
|
||||
}
|
||||
}
|
||||
|
||||
/// gui/cli/game-chat 等非 plan source 的 Supervisor system prompt 必须与
|
||||
/// M1A-4 之前逐字相同:直接用既有 section 常量手工拼出改动前的合成结果,
|
||||
/// 逐字比对,防止上面的 plan 根分支误伤这条现役路径。
|
||||
#[test]
|
||||
fn non_plan_supervisor_prompt_stays_byte_identical_to_the_original_composition() {
|
||||
for editor_api_key_is_configured in [false, true] {
|
||||
let visual_section = if editor_api_key_is_configured {
|
||||
RUNTIME_PROMPT_VISUAL_EDITOR_SECTION
|
||||
} else {
|
||||
RUNTIME_PROMPT_VISUAL_NO_EDITOR_SECTION
|
||||
};
|
||||
let expected = render_runtime_prompt_sections(&[
|
||||
"shared runtime contract",
|
||||
required_runtime_prompt_section("supervisorIdentityContract"),
|
||||
required_runtime_prompt_section("supervisorIntro"),
|
||||
required_runtime_prompt_section(visual_section),
|
||||
required_runtime_prompt_section("supervisorPlaybook"),
|
||||
required_runtime_prompt_section("supervisorClaimGate"),
|
||||
required_runtime_prompt_section("supervisorRepair"),
|
||||
]);
|
||||
for source in [
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
"",
|
||||
"project-supervisor-plan-forged",
|
||||
] {
|
||||
let actual = game_creator_project_supervisor_tool_plan_prompt(
|
||||
"shared runtime contract",
|
||||
editor_api_key_is_configured,
|
||||
source,
|
||||
);
|
||||
assert_eq!(actual, expected, "source={source} 不应被 plan 根收窄逻辑改变");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_prompt_bundle_uses_only_native_function_protocol_terms() {
|
||||
for editor_api_key_is_configured in [false, true] {
|
||||
let prompt = game_creator_project_supervisor_tool_plan_prompt(
|
||||
required_runtime_prompt_section("common"),
|
||||
editor_api_key_is_configured,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
for legacy_term in [
|
||||
"最终 response",
|
||||
@@ -1309,8 +1422,10 @@ mod tests {
|
||||
#[test]
|
||||
fn agent_prompt_other_agents_keep_the_shared_runtime_contract() {
|
||||
let shared_prompt = game_creator_agent_runtime_tool_plan_system_prompt();
|
||||
let code_agent_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent("code-prototype");
|
||||
let code_agent_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"code-prototype",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
|
||||
assert_eq!(code_agent_prompt, shared_prompt);
|
||||
assert!(code_agent_prompt.contains("你正在使用 Genarrative AI 游戏创作多智能体 Runtime"));
|
||||
|
||||
+18
-2
@@ -345,8 +345,10 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
prompt
|
||||
};
|
||||
if planning_agent {
|
||||
// project-planning 分支在 game_creator_agent_runtime_tool_plan_system_prompt_for_agent
|
||||
// 内部按 agent_id 提前返回固定的 planning 合同,不看 source,这里传空串即可。
|
||||
let mut planning_system_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent(agent_id);
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent(agent_id, "");
|
||||
let role_brief = game_creator_agent_runtime_role_overlay_prompt(agent_id, None);
|
||||
if !role_brief.is_empty() {
|
||||
planning_system_prompt.push_str("\n\n");
|
||||
@@ -369,7 +371,21 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
.with_web_search(false);
|
||||
return Ok((llm, config_path, request, repository_context_fingerprint));
|
||||
}
|
||||
let mut system_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(agent_id);
|
||||
// M1A-4:Supervisor 的 plan 根 run 需要在合成 system prompt 时收窄
|
||||
// supervisorIntro / $visualContract 两段(见 prompt.rs),其余角色的 prompt
|
||||
// 不看 source,这里读取当前 run 自身的 binding 只是为了拿它的 source 字段;
|
||||
// 缺失 binding 时按空串处理,等价于既有(非 plan)行为,不改变现有输出。
|
||||
let supervisor_prompt_source = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
.map(|binding| binding.source)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut system_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
agent_id,
|
||||
&supervisor_prompt_source,
|
||||
);
|
||||
if autonomous_game_build {
|
||||
system_prompt.push_str("\n\n");
|
||||
system_prompt.push_str(required_runtime_prompt_section(
|
||||
|
||||
@@ -114,6 +114,8 @@ pub(crate) const AGENT_RUNTIME_PLAN_ROOT_STEER_UNSUPPORTED_KIND: &str =
|
||||
"plan-root-steer-unsupported";
|
||||
pub(crate) const AGENT_RUNTIME_PLAN_ROOT_RETRY_IDENTITY_UNSUPPORTED_KIND: &str =
|
||||
"plan-root-retry-identity-unsupported";
|
||||
pub(crate) const AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND: &str =
|
||||
"plan-root-child-target-unsupported";
|
||||
pub(super) const GAME_CHAT_FIXED_TASK_GRAPH_STALLED_ERROR: &str =
|
||||
"game-chat 首版固定任务图无法继续推进,拒绝回退到普通 Provider 协作波";
|
||||
|
||||
|
||||
@@ -476,6 +476,41 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
};
|
||||
}
|
||||
}
|
||||
// M1A-4 补上 D11 的对称方向:父 Run 若在 task journal 里自称
|
||||
// project-supervisor-plan(弱判据,取自 task record 而不是 binding,binding
|
||||
// 缺失或损坏时也能取到),就只能委派 project-planning。绝不能把
|
||||
// validate_project_supervisor_plan_root_binding_at 返回 Err 误判成"父根本不是
|
||||
// plan 根"从而放行——那会在 binding 损坏时对任意子 Agent 创建 fail-open。判定
|
||||
// 必须是:弱判据为假→不管;弱判据为真+强判据 validate 通过→只放行
|
||||
// target==project-planning;弱判据为真+强判据不通过→拒绝创建任何子 Agent。
|
||||
let parent_task_for_plan_root_symmetry =
|
||||
match read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, parent_run_id) {
|
||||
Ok(task) => task,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
if parent_task_for_plan_root_symmetry
|
||||
.is_some_and(|task| agent_runtime_supervisor_source_is_plan(&task.source))
|
||||
{
|
||||
let plan_root_binding_validated =
|
||||
validate_project_supervisor_plan_root_binding_at(root, agent_id, parent_run_id).is_ok();
|
||||
if !plan_root_binding_validated || target_agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: format!(
|
||||
"立项策划根 Run 只能委派 project-planning,已拒绝创建子 Agent(kind={AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND})"
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
let action_identity = action_id
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(str::to_string)
|
||||
@@ -1302,6 +1337,43 @@ pub(crate) fn observe_agent_runtime_agent_spawn_isolated(
|
||||
detail: None,
|
||||
};
|
||||
};
|
||||
// M1A-4:策划链路没有临时并行检查的场景,父 Run 若在 task journal 里自称
|
||||
// project-supervisor-plan(弱判据,取自 task record,binding 缺失或损坏时
|
||||
// 也能取到)就一律拒绝创建动态隔离 child,不区分强判据是否通过——弱判据为
|
||||
// 真时无论 binding 是否能被 validate_project_supervisor_plan_root_binding_at
|
||||
// 验证,agent.spawn_isolated 对策划根 Run 都不是合法通道,必须 fail closed。
|
||||
let parent_task_for_plan_root_isolation =
|
||||
match read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
) {
|
||||
Ok(task) => task,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.spawn_isolated".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(
|
||||
root,
|
||||
&format!("无法核对动态隔离父 Run task journal,已拒绝创建 child:{error}"),
|
||||
240,
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
if parent_task_for_plan_root_isolation
|
||||
.is_some_and(|task| agent_runtime_supervisor_source_is_plan(&task.source))
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.spawn_isolated".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: format!(
|
||||
"立项策划根 Run 不支持动态隔离子 Agent(kind={AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND})"
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let binding = match read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
parent_agent_id,
|
||||
|
||||
@@ -495,8 +495,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"visual-prompt-test-key"}}"#.to_string(),
|
||||
);
|
||||
let design_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent("design-foundation");
|
||||
let design_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"design-foundation",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
for expected in [
|
||||
"文本策划只是中间结果",
|
||||
"canvas.asset_generate",
|
||||
@@ -532,8 +534,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
||||
);
|
||||
}
|
||||
|
||||
let director_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent("art-director");
|
||||
let director_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"art-director",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
for expected in [
|
||||
"assets/art-spec.png",
|
||||
"assetKind=icon-spec",
|
||||
@@ -550,7 +554,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
||||
);
|
||||
}
|
||||
|
||||
let art_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent("art-asset-plan");
|
||||
let art_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"art-asset-plan",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
for expected in [
|
||||
"资产清单和美术计划只是中间结果",
|
||||
"canvas.asset_generate",
|
||||
@@ -581,8 +588,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
||||
);
|
||||
}
|
||||
|
||||
let ordinary_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent("quality-review");
|
||||
let ordinary_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"quality-review",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
assert!(!ordinary_prompt.contains("assets/ui-prototype.png"));
|
||||
assert!(!ordinary_prompt.contains("assets/art-spritesheet.png"));
|
||||
}
|
||||
@@ -590,18 +599,24 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
||||
#[test]
|
||||
fn visual_prompts_degrade_to_text_contracts_without_editor_api_key() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let design_prompt =
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent("design-foundation");
|
||||
let design_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"design-foundation",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
assert!(design_prompt.contains("当前未配置 External Editor API Key"));
|
||||
assert!(design_prompt.contains("memory/project.md 与 game/game_design.md"));
|
||||
assert!(design_prompt.contains("不调用 canvas.asset_generate"));
|
||||
|
||||
let art_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent("art-asset-plan");
|
||||
let art_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
"art-asset-plan",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
assert!(art_prompt.contains("assets/manifest.art.json"));
|
||||
assert!(art_prompt.contains("不伪造 assets/art-spritesheet.png"));
|
||||
|
||||
let supervisor_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
);
|
||||
assert!(supervisor_prompt.contains("当前未配置 External Editor API Key"));
|
||||
assert!(supervisor_prompt.contains("不得要求调用 canvas.asset_generate"));
|
||||
@@ -616,6 +631,7 @@ async fn project_supervisor_prompts_are_total_control_and_reject_isolated_templa
|
||||
);
|
||||
game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
)
|
||||
};
|
||||
for expected in [
|
||||
|
||||
@@ -358,6 +358,300 @@ fn planning_tool_policy_snapshot_keeps_exact_permission_decisions() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// M1A-4 A1/A2 正向路径:plan 根 run 只能委派 project-planning,委派其它任意
|
||||
/// 专业 Agent(含 code-prototype、art-director)必须被拒绝且带新 typed kind;
|
||||
/// 委派 project-planning 本身必须继续通过,不能误伤 M1A-2 已落地的正向路径;
|
||||
/// agent.spawn_isolated 对 plan 根 run 一律拒绝。
|
||||
#[test]
|
||||
fn plan_root_delegate_rejects_non_planning_targets_but_keeps_planning_path() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "plan-root-delegate-symmetry", "策划根委派对称收口")
|
||||
.expect("project init");
|
||||
let parent_run_id = "plan-root-delegate-symmetry-run";
|
||||
start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"策划立项",
|
||||
parent_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
"准备委派策划",
|
||||
vec!["委派 project-planning".to_string()],
|
||||
)
|
||||
.expect("start plan root task");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind plan root");
|
||||
|
||||
for non_planning_target in ["code-prototype", "art-director", "design-director"] {
|
||||
let observation = observe_agent_runtime_agent_delegate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
Some(&format!("plan-root-reject-{non_planning_target}")),
|
||||
&serde_json::json!({
|
||||
"agentId": non_planning_target,
|
||||
"task": "策划根 run 不应能委派专业 Agent",
|
||||
"acceptanceCriteria": ["不应通过"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
);
|
||||
assert_eq!(observation.status, "failed", "{observation:?}");
|
||||
assert!(
|
||||
observation
|
||||
.summary
|
||||
.contains(AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND),
|
||||
"{observation:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let isolated_observation = observe_agent_runtime_agent_spawn_isolated(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
Some("plan-root-reject-spawn-isolated"),
|
||||
&serde_json::json!({
|
||||
"children": [{
|
||||
"templateAgentId": "code-prototype",
|
||||
"task": "策划根 run 不应能创建隔离 child",
|
||||
"acceptanceCriteria": ["不应通过"],
|
||||
"expectedArtifacts": [],
|
||||
"writeScopes": ["game/**"]
|
||||
}],
|
||||
"joinMode": "all"
|
||||
}),
|
||||
);
|
||||
assert_eq!(isolated_observation.status, "failed", "{isolated_observation:?}");
|
||||
assert!(
|
||||
isolated_observation
|
||||
.summary
|
||||
.contains(AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND),
|
||||
"{isolated_observation:?}"
|
||||
);
|
||||
|
||||
let planning_lock =
|
||||
try_acquire_game_creator_agent_runtime_task_lock(&root, GAME_CREATOR_PROJECT_PLANNING_AGENT_ID)
|
||||
.expect("acquire planning lane")
|
||||
.expect("planning lane available");
|
||||
let planning_observation = observe_agent_runtime_agent_delegate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
Some("plan-root-allow-planning"),
|
||||
&serde_json::json!({
|
||||
"agentId": GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"task": "输出 Fast GDD",
|
||||
"acceptanceCriteria": ["必须给出可审批的 Fast GDD"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
);
|
||||
assert_eq!(planning_observation.status, "ok", "{planning_observation:?}");
|
||||
drop(planning_lock);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// M1A-4 最关键的回归:plan 根 run 的 task record 自称
|
||||
/// project-supervisor-plan(弱判据为真),但 durable Run Profile 绑定缺失或
|
||||
/// 与该 source 不一致(强判据不通过)时,agent.delegate 与
|
||||
/// agent.spawn_isolated 两条通道都必须 fail closed——不能因为
|
||||
/// `validate_project_supervisor_plan_root_binding_at` 返回 Err 就被误判成
|
||||
/// "根本不是 plan 根" 从而放行任意子 Agent 创建。
|
||||
#[test]
|
||||
fn plan_root_delegate_and_spawn_isolated_fail_closed_when_binding_missing_or_corrupted() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "plan-root-fail-closed", "策划根绑定损坏拒绝")
|
||||
.expect("project init");
|
||||
|
||||
// 变体一:task record 自称 plan,但从未绑定过 durable binding(缺失)。
|
||||
let missing_binding_run_id = "plan-root-fail-closed-missing-binding-run";
|
||||
start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"策划立项",
|
||||
missing_binding_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
"准备委派策划",
|
||||
vec!["委派 project-planning".to_string()],
|
||||
)
|
||||
.expect("start plan root task without binding");
|
||||
|
||||
// 变体二:task record 自称 plan,但该 run_id 实际绑定的 source 是 gui
|
||||
// (即 binding 与 task 的自称身份不一致,validate_...(..) 必须失败)。
|
||||
let mismatched_binding_run_id = "plan-root-fail-closed-mismatched-binding-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
mismatched_binding_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind mismatched gui binding");
|
||||
start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"策划立项",
|
||||
mismatched_binding_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
"准备委派策划",
|
||||
vec!["委派 project-planning".to_string()],
|
||||
)
|
||||
.expect("start plan root task with mismatched binding");
|
||||
|
||||
for run_id in [missing_binding_run_id, mismatched_binding_run_id] {
|
||||
for target in ["code-prototype", GAME_CREATOR_PROJECT_PLANNING_AGENT_ID] {
|
||||
let observation = observe_agent_runtime_agent_delegate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
Some(&format!("plan-root-fail-closed-{run_id}-{target}")),
|
||||
&serde_json::json!({
|
||||
"agentId": target,
|
||||
"task": "binding 损坏时任何子 Agent 创建都不得通过",
|
||||
"acceptanceCriteria": ["不应通过"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
observation.status, "failed",
|
||||
"run_id={run_id} target={target}: {observation:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let isolated_observation = observe_agent_runtime_agent_spawn_isolated(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
Some(&format!("plan-root-fail-closed-spawn-{run_id}")),
|
||||
&serde_json::json!({
|
||||
"children": [{
|
||||
"templateAgentId": "code-prototype",
|
||||
"task": "binding 损坏时不得创建隔离 child",
|
||||
"acceptanceCriteria": ["不应通过"],
|
||||
"expectedArtifacts": [],
|
||||
"writeScopes": ["game/**"]
|
||||
}],
|
||||
"joinMode": "all"
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
isolated_observation.status, "failed",
|
||||
"run_id={run_id}: {isolated_observation:?}"
|
||||
);
|
||||
assert!(
|
||||
isolated_observation
|
||||
.summary
|
||||
.contains(AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND),
|
||||
"{isolated_observation:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// 补上最关键的单点:非 project-planning 目标必须由新 typed kind 拒绝
|
||||
// (而不是被 M1A-2 现役的 target==project-planning 专属分支恰好挡住)。
|
||||
let non_planning_observation = observe_agent_runtime_agent_delegate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
missing_binding_run_id,
|
||||
Some("plan-root-fail-closed-kind-check"),
|
||||
&serde_json::json!({
|
||||
"agentId": "code-prototype",
|
||||
"task": "binding 缺失时委派专业 Agent 必须带新 kind",
|
||||
"acceptanceCriteria": ["不应通过"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
);
|
||||
assert!(
|
||||
non_planning_observation
|
||||
.summary
|
||||
.contains(AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND),
|
||||
"{non_planning_observation:?}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 对照组:gui 根 run 的委派与 agent.spawn_isolated 行为必须逐字不变。
|
||||
#[test]
|
||||
fn gui_root_delegate_and_spawn_isolated_are_unaffected_by_plan_root_symmetry() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "gui-root-delegate-control", "对照组:gui 根不受影响")
|
||||
.expect("project init");
|
||||
let parent_run_id = "gui-root-delegate-control-run";
|
||||
start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"普通游戏立项",
|
||||
parent_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"准备委派专业组",
|
||||
vec!["委派专业组".to_string()],
|
||||
)
|
||||
.expect("start gui root task");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind gui root");
|
||||
|
||||
let design_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director")
|
||||
.expect("acquire design lane")
|
||||
.expect("design lane available");
|
||||
let design_observation = observe_agent_runtime_agent_delegate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
Some("gui-root-control-delegate"),
|
||||
&serde_json::json!({
|
||||
"agentId": "design-director",
|
||||
"task": "输出核心循环",
|
||||
"acceptanceCriteria": ["必须给出一句可执行核心循环"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
}),
|
||||
);
|
||||
assert_eq!(design_observation.status, "ok", "{design_observation:?}");
|
||||
drop(design_lock);
|
||||
|
||||
let isolated_observation = observe_agent_runtime_agent_spawn_isolated(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
Some("gui-root-control-spawn-isolated"),
|
||||
&serde_json::json!({
|
||||
"children": [{
|
||||
"templateAgentId": "code-prototype",
|
||||
"task": "完成 feature-a 子任务",
|
||||
"acceptanceCriteria": ["game/feature-a/output.txt 存在"],
|
||||
"expectedArtifacts": ["game/feature-a/output.txt"],
|
||||
"writeScopes": ["game/feature-a/**"]
|
||||
}],
|
||||
"joinMode": "all"
|
||||
}),
|
||||
);
|
||||
assert_eq!(isolated_observation.status, "ok", "{isolated_observation:?}");
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_asset_generation_respects_project_policy() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -65,7 +65,8 @@ pub(super) use crate::{
|
||||
game_creator_agent_runtime_task_lock_is_available, game_creator_agent_runtime_task_path,
|
||||
game_creator_agent_runtime_tool_policy_rule_for_run, init_local_game_project_at,
|
||||
invalidate_agent_runtime_project_verification_after_preview_failure_at,
|
||||
native_runtime_function_name, observe_agent_runtime_agent_message,
|
||||
native_runtime_function_name, observe_agent_runtime_agent_delegate,
|
||||
observe_agent_runtime_agent_message, observe_agent_runtime_agent_spawn_isolated,
|
||||
plan_game_creation_agent_pass, prepare_agent_runtime_project_mutation_locked,
|
||||
prepare_game_creator_agent_runtime_provider_action_batch,
|
||||
project_verification_completion_blocker_at, read_all_game_creator_agent_runtime_tasks,
|
||||
@@ -107,9 +108,11 @@ pub(super) use crate::{
|
||||
AGENT_RUNTIME_PENDING_ACTION_SCHEMA_VERSION, AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED, AGENT_RUNTIME_PLAN_STATUS_COMPLETED,
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME, AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
AGENT_RUNTIME_PLAN_ROOT_CHILD_TARGET_UNSUPPORTED_KIND, AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD, AGENT_RUNTIME_SCHEMA_VERSION,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION,
|
||||
AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND,
|
||||
AGENT_RUNTIME_UI_PROTOTYPE_LEGACY_VALIDATION_PROFILE, AGENT_RUNTIME_UI_PROTOTYPE_PATH,
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-14 M1A-4:plan 根 run 的子 Agent 创建面收窄,并冻结三条已知残留
|
||||
|
||||
- **补的是 `M1A-2` 的反方向。** `M1A-2` 只做了「目标是 `project-planning` → 要求父是 plan 根」这一半;反过来「父是 plan 根 → 目标必须是 `project-planning`」当时没做,也没记为 deferred。后果是 plan 根 run 可以委派任意专业 Agent,而被委派者拿的是常规 `standard` 工具面(能写文件、跑命令),第 24 节「策划全程零构建」当时只由 Prompt 兜底、不是机制保证。
|
||||
- 落地:`observe_agent_runtime_agent_delegate` 补对称分支;`observe_agent_runtime_agent_spawn_isolated` 对 plan 根 run 一律拒绝(**它是第二条造子 Agent 的通道,只堵 delegate 等于留后门**)。两条通道共用 typed `kind=plan-root-child-target-unsupported`。Supervisor 根 run 的 prompt 在 plan source 下不拼 `supervisorIntro` 与 `$visualContract` 两段;不改 `.md` 内容、不新增 composition key(第 4.2 节)。
|
||||
- **强弱判据分工与 `M1A-3` 一致,不得合并**:弱判据(`task.source` 自称是 plan,经 task journal 读取而非 binding)决定「本约束是否管辖这条 run」;强判据 `validate_project_supervisor_plan_root_binding_at` 决定「它是否合法」。强判据 `Err` **永远落进拒绝分支**,绝不可写成 `else if validate(..).is_ok() { 拒 }`——那会把「binding 损坏」误归为「不是 plan 根」,恰好在 durable 状态损坏时放行任意子 Agent 创建。
|
||||
- 回归:`plan_root_delegate_rejects_non_planning_targets_but_keeps_planning_path`(含 `project-planning` 正向路径仍通过,避免把功能整个焊死)、`plan_root_delegate_and_spawn_isolated_fail_closed_when_binding_missing_or_corrupted`、`gui_root_delegate_and_spawn_isolated_are_unaffected_by_plan_root_symmetry`、`plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections`、`non_plan_supervisor_prompt_stays_byte_identical_to_the_original_composition`。
|
||||
|
||||
**以下三条是经复核后有意保留的取舍,不是待办。后续 PR 不得在未重新裁决的情况下「顺手修掉」。**
|
||||
|
||||
1. **`$isolatedAgentTemplates` 仍会向 plan 根 run 列出全部专业角色名。** `art-director` / `design-foundation` / `art-asset-plan` / `code-prototype` 等名字来自 `$base`(`RUNTIME_PROMPT_RUNTIME_COMPOSITION` 的 `$isolatedAgentTemplates` 段,由 `GAME_CREATOR_AGENT_GROUP_DEFINITIONS` 运行时渲染),用途是广告 `agent.spawn_isolated` 的合法模板 id,**不在本次裁掉的两段之内**。裁掉它要动 `$base` 与隔离模板目录,波及全部 Agent。保留的依据是:`A2` 已对 plan 根 run 硬拒 `spawn_isolated`,该目录对 plan 根 run 是**死文本**,不构成可利用面。**结论:上下文层的收窄边界到此为止;「plan 根 run 的上下文里不出现其它 Agent 名」这一目标 M1 不成立,不要据此写验收句。**
|
||||
2. **plan 之外的 source 多了一个失败面。** 两个 observe 函数新增的 task journal 读取,其 `Err` 分支位于弱判据之前,对 `project-supervisor-gui/cli/game-chat` 同样生效。正常状态不触发(仅 journal I/O 损坏等异常)。保留的依据是:读不到 task 就无法判断这条 run 是不是 plan 根,此时 fail closed 是正确取向;改成「读失败就放行」会引入一个新的 fail-open。**这是对第 19 节「做游戏链路逐字不变」的一处有意例外,仅限异常路径。**
|
||||
3. **上下文层回归是弱断言。** `plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections` 用「不含某几条独有短句」断言,不是对照组那种字节级 `assert_eq`。已实测非永真。已知漏报场景:若将来 `$visualContract` / `supervisorIntro` 被替换成措辞不同但仍暗示专业组扇出的新文本,这条不会报警。**执行层的 `A1`/`A2` 是该场景的唯一保障**——这也是本包把硬门排在裁段之前的原因。
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 4.3、22、23.8、24 节。
|
||||
|
||||
## 2026-08-13 M1A-2:planning 子 Agent 两层工具面与角色 brief 注入
|
||||
|
||||
- 落地范围:在 `M1A-1` 的 `project-supervisor-plan` source 基础上,收口两层工具面。Supervisor 根 run 继续使用 `standard` 的现役工具面;`project-planning` 只接受 `source=agent-delegate`、`profile=standard`、父 Agent 为 `project-supervisor` 的静态委派身份。
|
||||
- 落地范围:在 `M1A-1` 的 `project-supervisor-plan` source 基础上,收口两层工具面。Supervisor 根 run 继续使用 `standard` 的现役工具面;`project-planning` 只接受 `source=agent-delegate`、`profile=standard`、父 Agent 为 `project-supervisor` 的静态委派身份。**(2026-08-14 订正:「Supervisor 根 run 继续使用现役工具面」这句读起来像决定,实际是要求丢失——本包工作项原本还要求「按 source 收窄 Supervisor 侧委派面:做方案链路只应产生一条指向 `project-planning` 的委派」,该半未实现也未记为 deferred。已由 `M1A-4` 补齐,见本文件同名条。)**
|
||||
- planning 子 Agent 的当前 native action exact allowlist 只有 `file.read`、`file.list`;`update_agent_plan` / `respond_to_user` 是协议控制函数,不计入 action capability。MCP catalog 强制为空,`webSearchEnabled=false`,`collaborationPolicy=null`。`plan.submit_gdd` 刻意未注册、未广告、未执行,留给后续 `M1B-2`,因此本条不代表 GDD 提交、版本存储或审批闭环已完成。
|
||||
- Prompt:Prompt Bundle 新增并登记 `project-planning` role brief,standard planning child 的初始请求与 repair/rebuild 请求均注入同一 brief;Supervisor 和其它 Agent 不注入该 section。brief 只描述 Fast GDD 澄清、终态 `AGC_NEEDS_USER_INPUT_V1`、三轮边界、平台事实与低幻觉约束,不授予任何写入、命令、MCP、预览、生成或审批能力。
|
||||
- 纵深拒绝:广告层不再向 planning child 暴露 `user.input_request`;Provider parser、action batch/pending、并行只读、执行层和状态恢复均按原始 tool identity 再校验。伪造写入/命令/MCP、`project.search` 等映射为 `file.read` 的 alias、`user.input_request` 都 fail-closed;恢复旧快照不得把 planning 工具面扩回全量目录。委派子 Agent 原有 `validate_user_input_action_owner` 执行层拒绝继续保留。
|
||||
|
||||
@@ -1717,6 +1717,7 @@ M0 文档 PR 本身最低验证:Markdown 结构与三张 Mermaid 图可解析
|
||||
| JSON sidecar | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/json_sidecar.rs:44-104,122-250` | 现有 writer 可覆盖;不可变文件必须新增 no-replace helper |
|
||||
| 项目锁 | `apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs:85-138`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs:1467-1505` | 所有 planning mutation 在同一项目锁内重读事实 |
|
||||
| completion blocker | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs:808-860,934-948`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs:1792-1832` | **2026-08-13 收窄**:现役 collaboration blocker 对**策划子 Agent**返回不适用;Supervisor 根 run 侧不再整体豁免(见第 4.3 节)。仍需新增 plan GDD 专用完成门,检查提问/审批等待、receipt、observation、session 与 recovery 状态 |
|
||||
| plan 根 run 子 Agent 创建面 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs`(`observe_agent_runtime_agent_delegate` / `observe_agent_runtime_agent_spawn_isolated`)、`agent/runtime_protocol/run_configuration.rs`(`validate_project_supervisor_plan_root_binding_at`)、`agent/prompt.rs`(`game_creator_project_supervisor_tool_plan_prompt`) | **2026-08-14 `M1A-4` 已落地**:plan 根 run 只能委派 `project-planning`,`agent.spawn_isolated` 一律拒,两条通道共用 typed `kind=plan-root-child-target-unsupported`;plan source 下不拼 `supervisorIntro` 与 `$visualContract`。**已知残留(有意保留,见 decision-log 2026-08-14 `M1A-4` 条)**:`$base` 的 `$isolatedAgentTemplates` 段仍会向 plan 根 run 列出全部专业角色名——那是 `agent.spawn_isolated` 的模板目录,因执行层硬拒而成为死文本;因此**不得**写「plan 根 run 上下文不出现其它 Agent 名」这类验收句 |
|
||||
| plan retry | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs`(`resolve_game_creator_agent_runtime_retry_configuration_at`)、`runtime_driver.rs`(`supervisor_plan_root_identity_holds_at`) | **2026-08-13 `M1A-3` 已落地 source 保源**:`task.source == project-supervisor-plan` 时先走强判据,通过则保留该 source,失败 `kind=plan-root-retry-identity-unsupported`、不降级。gui/cli 仍走 `agent-background-task`。plan-session revision / `gddId` / 按 `gdd-approval` kind 禁 retry 仍属后续包(现役已拒 `waiting-*`) |
|
||||
| planning hydrate command | `apps/ai-game-creator-shell/src-tauri/src/commands.rs:856-875`、`apps/ai-game-creator-shell/src-tauri/src/main.rs:2178-2180`、`apps/ai-game-creator-shell/src/App.tsx:1550,1650,2957,3364,3702` | 新增单一 hydrate command/注册与前端生命周期调用;不把 runtime polling 当 GDD authority |
|
||||
| agent.db | `apps/ai-game-creator-shell/src-tauri/src/project/agent_db.rs:955-1034,1551-1605,1995-2088` | 普通 append 不满足 decision 幂等;新增专用保留入口 |
|
||||
@@ -1961,6 +1962,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3`
|
||||
| `M1A-1` | source 常量 `project-supervisor-plan` + 进可信 matcher + steer 独立否决 | — | **已落地**。消费点复核见 decision-log 2026-08-13 `M1A-1` 条;steer `kind=plan-root-steer-unsupported`,独立于 matcher |
|
||||
| `M1A-2` | 两层工具面 + `project-planning` 角色 brief | `M1A-1` | **已落地**:Supervisor 根 run 保持 standard 工具面;planning 子 Agent 按 `agent-delegate`/`standard`/Supervisor parent 身份构建 exact native allowlist(`file.read`、`file.list` 与两个协议控制函数),MCP 为空、web search 关闭、collaboration policy 为 `null`;Prompt Bundle brief 只注入 `project-planning`,repair/rebuild 与初始请求一致;伪造写入、命令、MCP、`project.search` alias、`user.input_request` 等原始工具在广告/解析/执行/恢复路径均 fail-closed。**不包含 `plan.submit_gdd` 注册或 GDD 提交/审批。** |
|
||||
| `M1A-3` | plan 根 run 强判据函数 + retry 保源(本节下方「`M1A-3` 的由来」,规格见第 4.1 节第 5、7 段) | `M1A-1` | **已落地**(source 保源与强判据)。`kind=plan-root-retry-identity-unsupported`;gui/cli 兜底不变。第 4.1 节第 7 段里依赖 plan-session / `gddId` / `gdd-approval` kind 的部分仍后置。必须早于 `M1C-2a` |
|
||||
| `M1A-4` | plan 根 run 的子 Agent 创建面收窄:`agent.delegate` 目标必须是 `project-planning`、`agent.spawn_isolated` 一律拒;plan source 下不拼 `supervisorIntro` 与 `$visualContract` | `M1A-2` | 执行层与上下文层**都要做且不可互相替代**(同第 19 节第 2 条纪律);`agent.spawn_isolated` 是第二条造子 Agent 的通道,只堵 `agent.delegate` 视为未完成;强判据 `Err` 必须落进拒绝分支而非当作「不是 plan 根」;`project-supervisor-gui/cli` 的委派与 spawn 行为正常路径不变。**必须早于 `M1D-2` 与 `M1E`** |
|
||||
| `M1B-1` | `.agent/planning/` 存储层、strict schema、typed 指纹、版本链;含 `.agent/planning/**` 只挡写判据 | `M1A-2` | **第 9.1 节 golden vector 逐字节相等且指纹相等**(先于其它测试);create-only 与等前缀不可变 |
|
||||
| `M1B-2` | `plan.submit_gdd` 原生工具与提交点 | `M1B-1` | 全部拒绝分支;提交点前后强杀恢复;同 submissionId replay 不产生 vN+1 |
|
||||
| `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **本 PR 无写入方、是惰性路径,行为零变化**;做游戏链路返工仍在 `depth=1` 被拒;无该变体的历史记录分类不变 |
|
||||
@@ -1982,6 +1984,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3`
|
||||
## 24. 最终不变量摘要
|
||||
|
||||
- 策划与构建是两个不同 profile/source 的 run,由用户动作连接,不由 Agent 自行升级。
|
||||
- **(2026-08-14 由 `M1A-4` 落为机制)** plan 根 run 与策划子 Agent 互为唯一对应:`source=project-supervisor-plan` 的根 run 只能委派 `project-planning`,且一律不得 `agent.spawn_isolated`;身份判据失败(binding 缺失/损坏/漂移)时拒绝创建**任何**子 Agent,不得放行。因此「策划全程零构建、零文件写」是执行层保证,不再依赖 Prompt。上下文层的裁段是纵深防御的第二层、不可替代第一层——`$base` 的隔离模板目录仍会列出专业角色名,那是被硬拒后的死文本(见 decision-log 2026-08-14 `M1A-4` 条)。
|
||||
- **(2026-08-13 按 D11 改写)** 每轮的线性化点是**答案绑回 delivery**(`answersSha256` 原子写入,首次或逐字相同则幂等、不同答案则拒绝),不是 session CAS;轮次由委派链上的 `clarification_round` 派生,session 不保存计数字段。同一组问答只能派生同一个 continuation,因此同一回答永远不能产生两轮。
|
||||
- 每个 plan Provider request 的实际 messages、工具、结构化注入与 durable binding 来自同一 captured session;stale 输出不能重绑到新 session。
|
||||
- GDD durable create 是提交点;approval receipt durable create 是用户决定线性化点。
|
||||
|
||||
Reference in New Issue
Block a user