合并隔离分支的M1C-1与M1C-2a改动
This commit is contained in:
@@ -500,6 +500,28 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
if observation.tool == "file.read" {
|
||||
let first_line = observation.detail.as_deref()?.lines().next()?.trim();
|
||||
let mut fields = first_line.split('·').map(str::trim);
|
||||
let path = normalize_relative_path(fields.next()?).ok()?;
|
||||
let sha_field = fields.next()?;
|
||||
let content_sha256 = sha_field.strip_prefix("sha256=")?;
|
||||
if content_sha256.len() != 64
|
||||
|| !content_sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let lines = fields.next()?.strip_prefix("lines ")?;
|
||||
if lines.is_empty() || path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
return serde_json::to_string(&serde_json::json!({
|
||||
"path": path,
|
||||
"contentSha256": content_sha256,
|
||||
"lines": lines,
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
if observation.tool == GAME_CREATOR_MCP_CALL_TOOL {
|
||||
return game_creator_mcp_public_result_metadata(
|
||||
observation.detail.as_deref().unwrap_or_default(),
|
||||
|
||||
+209
-9
@@ -128,10 +128,8 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
};
|
||||
let tool_policy =
|
||||
agent_runtime_tool_policy_snapshot_for_run_at(root, agent_id, run_id, None, None)?;
|
||||
let root_control_authority = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root, agent_id, run_id,
|
||||
)?
|
||||
.is_some_and(|binding| {
|
||||
let root_binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?;
|
||||
let root_control_authority = root_binding.as_ref().is_some_and(|binding| {
|
||||
binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& binding.root_agent_id == binding.agent_id
|
||||
&& binding.root_run_id == binding.run_id
|
||||
@@ -139,6 +137,18 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
&& binding.parent_run_id.is_none()
|
||||
&& agent_runtime_supervisor_source_is_trusted(&binding.source)
|
||||
});
|
||||
let plan_root_candidate = root_binding
|
||||
.as_ref()
|
||||
.is_some_and(|binding| binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
|
||||
let plan_root = if plan_root_candidate {
|
||||
// Source is the weak discriminator. Once it says "plan", every
|
||||
// parent/profile/root field must pass the shared strong predicate;
|
||||
// drift must not silently downgrade to the dynamic Goal Contract.
|
||||
validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let root_goal_contract_context =
|
||||
render_game_creator_agent_runtime_goal_contract_for_prompt_at(root, agent_id, run_id)?
|
||||
.unwrap_or_else(|| "null".to_string());
|
||||
@@ -301,7 +311,18 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
"{prompt}\n\n补充协议:project.verify 的 script 除 check、typecheck、test、lint、build 外,还可使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 script 与 expectedCommand 都必须原样来自项目根 package.json。{command_exec_contract};args 中的项目路径必须相对 cwd,禁止绝对路径、file URI、路径加行号以及把绝对路径嵌入脚本或说明文字。该工具默认需要精确确认,适合运行定向测试、构建检查和只读诊断。durable command.exec observation 会直接返回可复用的 sourceActionId;短 observation 不足以定位失败时,使用 command.output_read {{\"actionId\":\"该 sourceActionId\",\"startLine\":1,\"maxLines\":160}} 分页读取同一 Agent 的已清洗命令输出,并按 nextLine 继续,不要先猜 actionId 或为取得它额外查询动作历史,也不得仅凭输出尾部猜测。只有 cargo check/test/clippy/fmt/build、npm test 或命名为 check/typecheck/test/lint/build/verify/validate 的验证脚本,以及精确 node --test 测试文件可签发验证凭证;git、rg、cargo metadata 和普通 npm run 只作为诊断结果。每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须使用当前角色可用的 project.verify、可验证 command.exec 或 game.static_smoke 完成验证,才能调用 respond_to_user。文件回读不能替代验证,验证后再次修改必须重新验证。每 {AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT} 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。"
|
||||
)
|
||||
};
|
||||
let prompt = if root_control_authority {
|
||||
let prompt = if plan_root && root_goal_contract_required {
|
||||
format!(
|
||||
"{prompt}\n\n立项策划 Goal Contract 首轮协议:本轮必须且只能调用一次 agent.goal_contract。outcome、nonNegotiables、forbiddenAssumptions、openQuestions 必须来自你对当前用户意图的理解,preferences 必须提交空数组;acceptanceNodes 不得自定义,必须精确提交单个固定节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}。",
|
||||
PLAN_FAST_GDD_ACCEPTANCE_NODE_ID = PLAN_FAST_GDD_ACCEPTANCE_NODE_ID,
|
||||
PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION = PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION,
|
||||
PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE = PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE,
|
||||
)
|
||||
} else if plan_root {
|
||||
format!(
|
||||
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
|
||||
)
|
||||
} else if root_control_authority {
|
||||
format!(
|
||||
"{prompt}\n\n动态目标协议:agent.goal_contract 只允许当前可信根 Project Supervisor 调用,input 使用 {{\"outcome\":\"最终交付\",\"nonNegotiables\":[],\"preferences\":[],\"forbiddenAssumptions\":[],\"openQuestions\":[],\"acceptanceNodes\":[{{\"criterionId\":\"稳定短 ID\",\"criterion\":\"可核对标准\",\"required\":true,\"requiredEvidence\":[\"tool:project.verify\"],\"dependsOn\":[]}}]}}。该合同必须来自你对当前用户意图的理解;固定规则、关键词、资产探测和专家建议只能作为上下文,不能替你决定目标、工作流或实现方案。agent.acceptance_update 也只允许同一根 Supervisor 调用,input 使用 {{\"contractFingerprint\":\"当前合同指纹\",\"evaluations\":[{{\"criterionId\":\"节点 ID\",\"status\":\"passed|failed|not-observed\",\"evidence\":[{{\"agentId\":\"证据生产者\",\"runId\":\"证据 run\",\"actionId\":\"持久成功动作\"}}],\"summary\":\"结论\"}}]}}。只提交本轮实际重新验收的节点;未提交的 passed 节点保持不变。"
|
||||
)
|
||||
@@ -450,6 +471,9 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
mcp_catalog,
|
||||
)?)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
if plan_root {
|
||||
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)?;
|
||||
}
|
||||
@@ -530,9 +554,16 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
}
|
||||
if root_goal_contract_required {
|
||||
restrict_agent_runtime_root_goal_contract_tools(&mut request)?;
|
||||
request.messages.push(LlmMessage::user(
|
||||
"当前根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。",
|
||||
));
|
||||
let goal_contract_instruction = if plan_root {
|
||||
format!(
|
||||
"当前 plan 根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次。outcome 具体概括当前用户最终意图,nonNegotiables、forbiddenAssumptions、openQuestions 没有内容时传空数组,preferences 必须始终传空数组;acceptanceNodes 必须精确提交固定单节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。"
|
||||
)
|
||||
} else {
|
||||
"当前根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。".to_string()
|
||||
};
|
||||
request
|
||||
.messages
|
||||
.push(LlmMessage::user(goal_contract_instruction));
|
||||
}
|
||||
let mut request = apply_game_creator_llm_reasoning_effort(request, &llm)?;
|
||||
request = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
@@ -762,7 +793,9 @@ mod tests {
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME, GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
GAME_CREATOR_PROJECT_PLANNING_FINAL_REPLY_SYSTEM_PROMPT,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION,
|
||||
PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE, PLAN_FAST_GDD_ACCEPTANCE_NODE_ID,
|
||||
RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
|
||||
};
|
||||
|
||||
fn native_input_required_fields(
|
||||
@@ -1298,6 +1331,173 @@ mod tests {
|
||||
.contains("本轮唯一可用工具是 agent.goal_contract")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_root_goal_contract_prompt_and_schema_change_after_the_first_turn() {
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-plan-goal-control-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "plan-goal-control-project", "形成 Fast GDD")
|
||||
.expect("project init");
|
||||
let binding = bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"provider-plan-goal-control-root",
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind plan root");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&binding.agent_id,
|
||||
"形成 Fast GDD",
|
||||
&binding.run_id,
|
||||
&binding.source,
|
||||
"冻结立项目标",
|
||||
vec!["冻结 Goal Contract".to_string()],
|
||||
)
|
||||
.expect("start plan root");
|
||||
let catalog = GameCreatorMcpCatalog {
|
||||
fingerprint: String::new(),
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
|
||||
let (_, _, first, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&[],
|
||||
0,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build first plan root request");
|
||||
let goal_function =
|
||||
crate::agent_native_tools::native_runtime_function_name("agent.goal_contract")
|
||||
.expect("goal function name");
|
||||
let fixed_schema = first
|
||||
.function_tools
|
||||
.iter()
|
||||
.find(|function| function.name == goal_function)
|
||||
.expect("plan goal function")
|
||||
.parameters
|
||||
.pointer("/properties/input/properties/acceptanceNodes")
|
||||
.expect("fixed acceptance schema");
|
||||
let fixed_preferences = first
|
||||
.function_tools
|
||||
.iter()
|
||||
.find(|function| function.name == goal_function)
|
||||
.expect("plan goal function")
|
||||
.parameters
|
||||
.pointer("/properties/input/properties/preferences/maxItems")
|
||||
.expect("fixed preferences schema");
|
||||
assert_eq!(first.function_tools.len(), 1);
|
||||
assert_eq!(fixed_schema["maxItems"], serde_json::json!(1));
|
||||
assert_eq!(fixed_preferences, &serde_json::json!(0));
|
||||
assert_eq!(
|
||||
fixed_schema["items"]["properties"]["criterionId"]["enum"],
|
||||
serde_json::json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
|
||||
);
|
||||
let first_prompt = first
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(first_prompt.contains("Goal Contract 首轮协议"));
|
||||
assert!(!first_prompt.contains("acceptanceNodes 至少提交一项"));
|
||||
assert!(!first_prompt.contains("project.index 的成功回执"));
|
||||
|
||||
crate::agent::create_game_creator_agent_runtime_goal_contract_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&AgentRuntimeGoalContractDraft {
|
||||
outcome: "形成服务用户意图的 Fast GDD".to_string(),
|
||||
non_negotiables: Vec::new(),
|
||||
preferences: Vec::new(),
|
||||
forbidden_assumptions: Vec::new(),
|
||||
open_questions: Vec::new(),
|
||||
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
|
||||
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
|
||||
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
|
||||
required: true,
|
||||
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
|
||||
dependencies: Vec::new(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.expect("freeze plan contract");
|
||||
let (_, _, later, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&[],
|
||||
1,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build later plan root request");
|
||||
let later_prompt = later
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(later.function_tools.len() > 1);
|
||||
assert!(later_prompt.contains("Goal Contract 已冻结且不可重写"));
|
||||
assert!(later_prompt.contains("从 startLine=1 开始分页"));
|
||||
assert!(later_prompt.contains("直到 EOF"));
|
||||
assert!(later_prompt.contains("全部分页 file.read 的 actionId"));
|
||||
assert!(!later_prompt.contains("Goal Contract 首轮协议"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_source_profile_drift_cannot_fall_back_to_dynamic_goal_contract() {
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-plan-profile-drift-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "plan-profile-drift", "形成 Fast GDD")
|
||||
.expect("project init");
|
||||
let binding = bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"provider-plan-profile-drift-root",
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("bind deliberately drifted plan root");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&binding.agent_id,
|
||||
"形成 Fast GDD",
|
||||
&binding.run_id,
|
||||
&binding.source,
|
||||
"冻结立项目标",
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("start drifted plan root");
|
||||
let error = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&[],
|
||||
0,
|
||||
&GameCreatorMcpCatalog {
|
||||
fingerprint: String::new(),
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect_err("plan source must pass the shared strong predicate");
|
||||
assert!(error.contains("project-supervisor-plan standard 顶层根 Run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_request_source_does_not_patch_natural_language_with_replace_chains() {
|
||||
let source = include_str!("provider_request_builders.rs");
|
||||
|
||||
+72
-4
@@ -118,6 +118,28 @@ fn provider_collaboration_repair_instruction(protocol_error: &str, section_id: &
|
||||
)
|
||||
}
|
||||
|
||||
fn restrict_root_goal_contract_repair_request(
|
||||
request: &mut LlmRunRequest,
|
||||
plan_root: bool,
|
||||
) -> Result<(), String> {
|
||||
if plan_root {
|
||||
restrict_plan_root_goal_contract_schema(&mut request.function_tools)?;
|
||||
}
|
||||
restrict_agent_runtime_root_goal_contract_tools(request)
|
||||
}
|
||||
|
||||
fn root_goal_contract_repair_instruction(protocol_error: &str, plan_root: bool) -> String {
|
||||
if plan_root {
|
||||
format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前 plan 根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次。outcome 具体概括当前用户最终意图,nonNegotiables、forbiddenAssumptions、openQuestions 没有内容时传空数组,preferences 必须始终传空数组;acceptanceNodes 必须精确提交固定单节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn append_game_creator_agent_tool_plan_audit_idempotent(
|
||||
root: &Path,
|
||||
record: serde_json::Value,
|
||||
@@ -173,6 +195,15 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
let initial_request_slot = format!("loop-{loop_index}-repair-0");
|
||||
let (run_profile, _) =
|
||||
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
|
||||
let plan_root_candidate =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
.is_some_and(|binding| binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
|
||||
let plan_root = if plan_root_candidate {
|
||||
validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let mcp_catalog = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// Planning has a structurally empty MCP surface. Do not even resolve
|
||||
// the project MCP catalog here: doing so can start/connect required
|
||||
@@ -1033,10 +1064,13 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
}
|
||||
}
|
||||
if force_root_goal_contract {
|
||||
restrict_agent_runtime_root_goal_contract_tools(&mut request)?;
|
||||
request.messages.push(LlmMessage::user(format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
|
||||
)));
|
||||
restrict_root_goal_contract_repair_request(&mut request, plan_root)?;
|
||||
request
|
||||
.messages
|
||||
.push(LlmMessage::user(root_goal_contract_repair_instruction(
|
||||
&protocol_error,
|
||||
plan_root,
|
||||
)));
|
||||
} else if force_supervisor_initial_collaboration {
|
||||
supervisor_collaboration_repair_active = true;
|
||||
if let Some(actions) = supervisor_collaboration_candidate_actions.take() {
|
||||
@@ -1388,4 +1422,38 @@ mod supervisor_collaboration_repair_tests {
|
||||
|
||||
assert_eq!(merged, vec![replacement]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_root_goal_contract_repair_keeps_the_fixed_schema_and_instruction() {
|
||||
let catalog = GameCreatorMcpCatalog {
|
||||
fingerprint: String::new(),
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let mut request = LlmRunRequest::new(Vec::new())
|
||||
.with_function_tools(
|
||||
build_agent_runtime_native_function_tools(&catalog)
|
||||
.expect("build native function tools"),
|
||||
)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
|
||||
restrict_root_goal_contract_repair_request(&mut request, true)
|
||||
.expect("restrict plan goal repair");
|
||||
assert_eq!(request.function_tools.len(), 1);
|
||||
let fixed = request.function_tools[0]
|
||||
.parameters
|
||||
.pointer("/properties/input/properties/acceptanceNodes")
|
||||
.expect("fixed acceptance nodes schema");
|
||||
assert_eq!(fixed["maxItems"], serde_json::json!(1));
|
||||
assert_eq!(
|
||||
fixed["items"]["properties"]["criterionId"]["enum"],
|
||||
serde_json::json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
|
||||
);
|
||||
|
||||
let instruction = root_goal_contract_repair_instruction("test-error", true);
|
||||
assert!(instruction.contains("固定单节点"));
|
||||
assert!(instruction.contains(PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION));
|
||||
assert!(!instruction.contains("至少提交一项"));
|
||||
assert!(!instruction.contains("project.index 的成功回执"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,10 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
|
||||
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let blocker = if let Some(blocker) = structured_plan_completion_blocker(&state) {
|
||||
Some(blocker)
|
||||
} else if let Some(blocker) =
|
||||
plan_gdd_completion_blocker_at_locked(root, &journal.agent_id, &journal.run_id)
|
||||
{
|
||||
Some(blocker)
|
||||
} else if let Some(blocker) =
|
||||
game_creator_agent_goal_completion_blocker_at_locked(root, &state)
|
||||
{
|
||||
|
||||
@@ -1612,6 +1612,9 @@ mod orphaned_external_generation_recovery_tests {
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("恢复锚点同时缺失")));
|
||||
assert!(read_plan_gdd_approval_pending(root)
|
||||
.expect("read planning approval pending")
|
||||
.is_none());
|
||||
let audit_count = read_agent_db_records_bounded(root, 1024 * 1024)
|
||||
.expect("read anchor-missing audit")
|
||||
.0
|
||||
@@ -1628,6 +1631,9 @@ mod orphaned_external_generation_recovery_tests {
|
||||
);
|
||||
let _ = resume_game_creator_agent_background_tasks_at(root)
|
||||
.expect("second recovery scan may surface the existing reconciliation state");
|
||||
assert!(read_plan_gdd_approval_pending(root)
|
||||
.expect("reread planning approval pending")
|
||||
.is_none());
|
||||
let audit_count_after_second_scan = read_agent_db_records_bounded(root, 1024 * 1024)
|
||||
.expect("reread anchor-missing audit")
|
||||
.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+8
-17
@@ -224,25 +224,16 @@ fn plan_goal_contract_draft() -> AgentRuntimeGoalContractDraft {
|
||||
AgentRuntimeGoalContractDraft {
|
||||
outcome: "完成一份可审批的 Fast GDD".to_string(),
|
||||
non_negotiables: vec!["保留用户明确要求".to_string()],
|
||||
preferences: vec!["优先复用现有资源".to_string()],
|
||||
preferences: Vec::new(),
|
||||
forbidden_assumptions: vec!["不能把工具成功当作目标完成".to_string()],
|
||||
open_questions: vec!["最终视觉效果仍需观察".to_string()],
|
||||
acceptance_nodes: vec![
|
||||
AgentRuntimeGoalContractAcceptanceNodeDraft {
|
||||
criterion_id: "behavior".to_string(),
|
||||
criterion: "核心交互可运行".to_string(),
|
||||
required: true,
|
||||
required_evidence: vec!["tool:project.verify".to_string()],
|
||||
dependencies: Vec::new(),
|
||||
},
|
||||
AgentRuntimeGoalContractAcceptanceNodeDraft {
|
||||
criterion_id: "final-observation".to_string(),
|
||||
criterion: "最终结果符合用户目标".to_string(),
|
||||
required: true,
|
||||
required_evidence: vec!["tool:preview.validate".to_string()],
|
||||
dependencies: vec!["behavior".to_string()],
|
||||
},
|
||||
],
|
||||
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
|
||||
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
|
||||
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
|
||||
required: true,
|
||||
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
|
||||
dependencies: Vec::new(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ const AGENT_RUNTIME_GOAL_CONTRACT_NODE_ID_MAX_CHARS: usize = 160;
|
||||
const AGENT_RUNTIME_GOAL_CONTRACT_NODE_EVIDENCE_LIMIT: usize = 32;
|
||||
const AGENT_RUNTIME_GOAL_CONTRACT_NODE_DEPENDENCY_LIMIT: usize = 32;
|
||||
|
||||
/// Planning uses a fixed Fast GDD acceptance node. Only the intent-bearing
|
||||
/// Goal Contract fields vary with the project; the quality gate itself is a
|
||||
/// protocol invariant and is not Provider-authored.
|
||||
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_ID: &str = "fast-gdd-serves-intent";
|
||||
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION: &str =
|
||||
"当前 Fast GDD 服务已冻结的用户意图、不可协商约束与禁止假设";
|
||||
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE: &str = "tool:file.read";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeGoalContractAcceptanceNode {
|
||||
@@ -271,6 +279,30 @@ fn validate_goal_contract_acceptance_graph(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_plan_fast_gdd_acceptance_nodes(
|
||||
nodes: &[AgentRuntimeGoalContractAcceptanceNode],
|
||||
) -> Result<(), String> {
|
||||
if nodes.len() != 1 {
|
||||
return Err(
|
||||
"project-supervisor-plan 的 acceptanceNodes 必须是固定的单个 Fast GDD 验收节点"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let node = &nodes[0];
|
||||
if node.criterion_id != PLAN_FAST_GDD_ACCEPTANCE_NODE_ID
|
||||
|| node.criterion != PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION
|
||||
|| !node.required
|
||||
|| node.required_evidence != [PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE]
|
||||
|| !node.dependencies.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"project-supervisor-plan 的 acceptanceNodes 必须精确使用 Fast GDD 固定验收节点"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_goal_contract_record(
|
||||
root: &Path,
|
||||
contract: &AgentRuntimeGoalContract,
|
||||
@@ -340,6 +372,15 @@ fn validate_goal_contract_record(
|
||||
return Err("Goal Contract 语义字段未规范化".to_string());
|
||||
}
|
||||
validate_goal_contract_acceptance_graph(&contract.acceptance_nodes)?;
|
||||
if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
|
||||
if !contract.preferences.is_empty() {
|
||||
return Err(
|
||||
"project-supervisor-plan 的 preferences 必须为空;偏好不得进入固定 Fast GDD 验收合同"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
validate_plan_fast_gdd_acceptance_nodes(&contract.acceptance_nodes)?;
|
||||
}
|
||||
for node in &contract.acceptance_nodes {
|
||||
if node.revision != AGENT_RUNTIME_GOAL_CONTRACT_REVISION
|
||||
|| node.created_at != contract.created_at
|
||||
@@ -406,6 +447,9 @@ fn build_goal_contract(
|
||||
acceptance_nodes.push(node);
|
||||
}
|
||||
validate_goal_contract_acceptance_graph(&acceptance_nodes)?;
|
||||
if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
|
||||
validate_plan_fast_gdd_acceptance_nodes(&acceptance_nodes)?;
|
||||
}
|
||||
let mut contract = AgentRuntimeGoalContract {
|
||||
schema_version: AGENT_RUNTIME_GOAL_CONTRACT_SCHEMA_VERSION.to_string(),
|
||||
project_id: game_creator_agent_runtime_context_project_id(root)?,
|
||||
@@ -610,6 +654,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_goal_contract_draft(outcome: &str) -> AgentRuntimeGoalContractDraft {
|
||||
AgentRuntimeGoalContractDraft {
|
||||
outcome: outcome.to_string(),
|
||||
non_negotiables: vec!["保留用户明确要求".to_string()],
|
||||
preferences: Vec::new(),
|
||||
forbidden_assumptions: vec!["不能把提交当作审批".to_string()],
|
||||
open_questions: Vec::new(),
|
||||
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
|
||||
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
|
||||
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
|
||||
required: true,
|
||||
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
|
||||
dependencies: Vec::new(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn root_fixture(source: &str) -> (tempfile::TempDir, PathBuf, AgentRuntimeRunProfileBinding) {
|
||||
let temporary = crate::tests::canonical_test_tempdir("goal-contract-fixture-");
|
||||
let root = temporary.path().join("project");
|
||||
@@ -727,6 +788,56 @@ mod tests {
|
||||
assert!(error.contains("根 Project Supervisor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_goal_contract_acceptance_nodes_are_fixed_but_other_sources_stay_dynamic() {
|
||||
let (_temporary, root, binding) = root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
|
||||
create_game_creator_agent_runtime_goal_contract_at(
|
||||
&root,
|
||||
&binding.agent_id,
|
||||
&binding.run_id,
|
||||
"做一份 Fast GDD",
|
||||
&plan_goal_contract_draft("完成一份可审批的 Fast GDD"),
|
||||
)
|
||||
.expect("fixed plan contract");
|
||||
|
||||
let (_temporary, dynamic_root, dynamic_binding) =
|
||||
root_fixture(AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE);
|
||||
create_game_creator_agent_runtime_goal_contract_at(
|
||||
&dynamic_root,
|
||||
&dynamic_binding.agent_id,
|
||||
&dynamic_binding.run_id,
|
||||
"创建一个游戏",
|
||||
&goal_contract_draft("动态图仍可用"),
|
||||
)
|
||||
.expect("dynamic contract");
|
||||
|
||||
let (_temporary, invalid_root, invalid_binding) =
|
||||
root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
|
||||
let error = create_game_creator_agent_runtime_goal_contract_at(
|
||||
&invalid_root,
|
||||
&invalid_binding.agent_id,
|
||||
&invalid_binding.run_id,
|
||||
"做一份 Fast GDD",
|
||||
&goal_contract_draft("自定义节点应被拒绝"),
|
||||
)
|
||||
.expect_err("plan source must reject provider-authored nodes");
|
||||
assert!(error.contains("固定") || error.contains("acceptanceNodes"));
|
||||
|
||||
let (_temporary, preference_root, preference_binding) =
|
||||
root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
|
||||
let mut preference_draft = plan_goal_contract_draft("偏好不能进入 plan 合同");
|
||||
preference_draft.preferences = vec!["优先复用旧素材".to_string()];
|
||||
let error = create_game_creator_agent_runtime_goal_contract_at(
|
||||
&preference_root,
|
||||
&preference_binding.agent_id,
|
||||
&preference_binding.run_id,
|
||||
"做一份 Fast GDD",
|
||||
&preference_draft,
|
||||
)
|
||||
.expect_err("plan preferences must stay empty");
|
||||
assert!(error.contains("preferences 必须为空"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goal_contract_read_fails_closed_after_content_or_binding_tampering() {
|
||||
let (_temporary, root, binding) = root_fixture(AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE);
|
||||
|
||||
+405
-19
@@ -144,12 +144,33 @@ fn pending_identity_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGd
|
||||
&& pending.run_identity.run_id == gdd.created_by_run_id
|
||||
}
|
||||
|
||||
fn pending_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGddV1) -> bool {
|
||||
pub(crate) fn pending_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGddV1) -> bool {
|
||||
pending_identity_matches_gdd(pending, gdd)
|
||||
&& pending.status == "awaiting_decision"
|
||||
&& pending.observation.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn pending_matches_receipt(
|
||||
pending: &PlanGddApprovalPendingV1,
|
||||
gdd: &PlanGddV1,
|
||||
receipt: &PlanGddApprovalV1,
|
||||
) -> bool {
|
||||
if !pending_identity_matches_gdd(pending, gdd) {
|
||||
return false;
|
||||
}
|
||||
if pending_matches_gdd(pending, gdd) {
|
||||
return true;
|
||||
}
|
||||
let expected = approval_observation(receipt);
|
||||
pending.status == format!("observed_{}", receipt.action)
|
||||
&& pending.observation.as_ref().is_some_and(|observation| {
|
||||
observation.tool == expected.tool
|
||||
&& observation.status == expected.status
|
||||
&& observation.summary == expected.summary
|
||||
&& observation.detail == expected.detail
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct the independent planning pending projection after an external
|
||||
/// acceptance gate has succeeded. M1C-1 does not decide whether the gate
|
||||
/// passed; the caller must supply that fact and the exact GDD identity.
|
||||
@@ -240,6 +261,249 @@ pub(crate) fn create_plan_gdd_approval_pending_locked(
|
||||
write_plan_gdd_approval_pending_atomic_locked(root, &pending)
|
||||
}
|
||||
|
||||
/// Result of the M1C-2a acceptance gate. The gate is intentionally separate
|
||||
/// from `create_plan_gdd_approval_pending_locked`: a pending card is a
|
||||
/// projection that may only be created after the fixed Fast GDD acceptance
|
||||
/// graph has converged for the current Markdown revision.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PlanGddAcceptanceGateOutcome {
|
||||
NotApplicable,
|
||||
WaitingForDeliveryClaim {
|
||||
delegation_id: String,
|
||||
detail: String,
|
||||
},
|
||||
WaitingForEvidence {
|
||||
detail: String,
|
||||
},
|
||||
RepairRequired {
|
||||
repair_of_delegation_id: String,
|
||||
detail: String,
|
||||
},
|
||||
PendingCreated,
|
||||
PendingAlreadyPresent,
|
||||
AlreadyDecided,
|
||||
}
|
||||
|
||||
fn latest_plan_gdd_for_root<'a>(gdds: &'a [PlanGddV1], root_run_id: &str) -> Option<&'a PlanGddV1> {
|
||||
gdds.iter().rev().find(|gdd| {
|
||||
gdd.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& gdd.source == "agent-delegate"
|
||||
&& gdd.run_profile == AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
&& gdd.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& gdd.root_run_id == root_run_id
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<PlanGddAcceptanceGateOutcome, String> {
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
let Some(binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
else {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
};
|
||||
if binding.source != AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD {
|
||||
return Err("Fast GDD acceptance gate 的 plan 根 Run Profile 已漂移".to_string());
|
||||
}
|
||||
let gdds = read_plan_gdd_chain_locked(root).map_err(|error| error.to_string())?;
|
||||
let Some(gdd) = latest_plan_gdd_for_root(&gdds, run_id) else {
|
||||
// A contract may be updated before the planning child has submitted a
|
||||
// GDD. That is a normal intermediate state; it must never allocate a
|
||||
// user-facing approval card.
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
};
|
||||
// The planning storage is one immutable GDD lineage per project. A
|
||||
// replacement root may still have an older GDD in that lineage while a
|
||||
// newer root has already submitted a later version. Such an old root is
|
||||
// no longer eligible to expose an approval card; importantly, recovery
|
||||
// must skip it rather than turning the whole scan into reconciliation.
|
||||
let Some(global_latest) = gdds.last() else {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
};
|
||||
if global_latest.gdd_id != gdd.gdd_id
|
||||
|| global_latest.version != gdd.version
|
||||
|| global_latest.fingerprint != gdd.fingerprint
|
||||
{
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
validate_plan_gdd(gdd).map_err(|error| error.to_string())?;
|
||||
if gdd.root_run_id != run_id || gdd.source != "agent-delegate" {
|
||||
return Err(
|
||||
"Fast GDD acceptance gate 的 GDD root/source/profile identity 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let approvals = read_plan_gdd_approvals_locked(root).map_err(|error| error.to_string())?;
|
||||
validate_plan_gdd_approvals_against_gdds(&gdds, &approvals)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let receipt = read_plan_gdd_approval_for_version_locked(root, gdd.version)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let pending = read_plan_gdd_approval_pending_locked(root).map_err(|error| error.to_string())?;
|
||||
if let Some(receipt) = receipt.as_ref() {
|
||||
if pending
|
||||
.as_ref()
|
||||
.is_some_and(|pending| !pending_matches_receipt(pending, gdd, receipt))
|
||||
{
|
||||
return Err(
|
||||
"Fast GDD acceptance pending 与已提交 receipt 的 identity/observation 冲突"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(PlanGddAcceptanceGateOutcome::AlreadyDecided);
|
||||
}
|
||||
|
||||
// An exact awaiting pending is already the durable projection of a
|
||||
// passed gate. Recovery must preserve it without re-reading the file or
|
||||
// allocating a new approvalRequestId.
|
||||
if let Some(existing) = pending {
|
||||
if !pending_matches_gdd(&existing, gdd) {
|
||||
return Err("Fast GDD acceptance pending identity 与当前 GDD 不一致".to_string());
|
||||
}
|
||||
return Ok(PlanGddAcceptanceGateOutcome::PendingAlreadyPresent);
|
||||
}
|
||||
|
||||
let root_binding = validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
|
||||
if root_binding.project_id != gdd.project_id
|
||||
|| root_binding.source != PLAN_GDD_APPROVAL_SOURCE
|
||||
|| root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
{
|
||||
return Err("Fast GDD acceptance gate 的根 Run binding identity 不一致".to_string());
|
||||
}
|
||||
let child_binding = validate_project_planning_child_binding_at(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&gdd.created_by_run_id,
|
||||
)?;
|
||||
if child_binding.project_id != gdd.project_id
|
||||
|| child_binding.root_agent_id != gdd.root_agent_id
|
||||
|| child_binding.root_run_id != gdd.root_run_id
|
||||
|| child_binding.source != gdd.source
|
||||
|| child_binding.profile != gdd.run_profile
|
||||
|| child_binding.binding_fingerprint != gdd.run_profile_binding_fingerprint
|
||||
{
|
||||
return Err("Fast GDD acceptance gate 的策划子 Run binding identity 不一致".to_string());
|
||||
}
|
||||
let child_task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&gdd.created_by_run_id,
|
||||
)?;
|
||||
let child_task = child_task
|
||||
.ok_or_else(|| "Fast GDD acceptance gate 缺少策划子 Run task 记录".to_string())?;
|
||||
// Submit-anchor and child-completion recovery own every pre-terminal
|
||||
// projection. The approval gate must stay inert until that earlier
|
||||
// workflow has produced a completed task record.
|
||||
if game_creator_agent_runtime_terminal_status(&child_task).as_deref() != Some("completed") {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
if child_task.agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
|| child_task.run_id != gdd.created_by_run_id
|
||||
|| child_task.session_id != gdd.session_id
|
||||
|| child_task.parent_agent_id.as_deref() != Some(gdd.root_agent_id.as_str())
|
||||
|| child_task.parent_run_id.as_deref() != Some(gdd.root_run_id.as_str())
|
||||
|| child_task.source != gdd.source
|
||||
|| child_task.run_profile != gdd.run_profile
|
||||
|| child_task.run_profile_binding_fingerprint != gdd.run_profile_binding_fingerprint
|
||||
|| child_task.delegation_id.as_deref() != Some(gdd.delegation_id.as_str())
|
||||
{
|
||||
return Err(
|
||||
"Fast GDD acceptance gate 的策划子 Run task 与提交 delivery identity 不一致"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let delivery = read_static_delegate_delivery_at(root, &gdd.delegation_id)?;
|
||||
let delivery =
|
||||
delivery.ok_or_else(|| "Fast GDD acceptance gate 缺少原 planning delivery".to_string())?;
|
||||
if delivery.delegation_id != gdd.delegation_id
|
||||
|| delivery.parent_agent_id != gdd.root_agent_id
|
||||
|| delivery.parent_run_id != gdd.root_run_id
|
||||
|| delivery.target_agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
|| delivery.target_session_id != gdd.session_id
|
||||
|| delivery.target_run_id != gdd.created_by_run_id
|
||||
{
|
||||
return Err("Fast GDD acceptance gate 的 planning delivery identity 不一致".to_string());
|
||||
}
|
||||
if delivery.status == StaticDelegateDeliveryStatus::Dispatched {
|
||||
// Delegate-receipt recovery owns the Dispatched -> Ready transition.
|
||||
// The acceptance gate must remain inert until that durable handoff is
|
||||
// complete instead of treating an ordinary crash window as corruption.
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
if delivery.status == StaticDelegateDeliveryStatus::Suppressed {
|
||||
return Err("Fast GDD acceptance gate 的 planning delivery 已被 suppressed".to_string());
|
||||
}
|
||||
if delivery.terminal_status.as_deref() != Some("completed") {
|
||||
return Err("Fast GDD acceptance gate 的 planning delivery 终态不一致".to_string());
|
||||
}
|
||||
validate_active_plan_root_for_decision_locked(root, gdd).map_err(|error| error.to_string())?;
|
||||
let session = read_plan_session_with_recovery_locked(root)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| "Fast GDD acceptance gate 缺少 planning session".to_string())?;
|
||||
if !plan_gdd_session_matches_submission(&session, gdd) {
|
||||
return Err(
|
||||
"Fast GDD acceptance gate 的 planning session 尚未精确指向当前提交".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if delivery.status == StaticDelegateDeliveryStatus::Ready {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim {
|
||||
delegation_id: gdd.delegation_id.clone(),
|
||||
detail: format!(
|
||||
"planning delivery 尚未由当前 Supervisor 根 Run 认领;先通过 agent.run_status 认领 delegationId={},认领后才能按同一合同发起返工或创建审批 pending",
|
||||
gdd.delegation_id
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
match plan_fast_gdd_acceptance_status_at_locked(root, gdd)? {
|
||||
PlanFastGddAcceptanceStatus::NeedsEvidence => {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::WaitingForEvidence {
|
||||
detail: "Acceptance Graph 尚无当前 game/fast_gdd.md 的完整根 Run 取证;先从 startLine=1 分页无缺口读取到 EOF,并用全部 file.read actionId 更新验收节点;不得创建审批卡或提前返工".to_string(),
|
||||
});
|
||||
}
|
||||
PlanFastGddAcceptanceStatus::RepairRequired => {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::RepairRequired {
|
||||
repair_of_delegation_id: gdd.delegation_id.clone(),
|
||||
detail: format!(
|
||||
"Acceptance Graph 已基于当前 game/fast_gdd.md 明确判定未通过;不得创建审批卡;repairOfDelegationId={}",
|
||||
gdd.delegation_id
|
||||
),
|
||||
});
|
||||
}
|
||||
PlanFastGddAcceptanceStatus::Passed => {}
|
||||
}
|
||||
|
||||
create_plan_gdd_approval_pending_locked(root, gdd).map_err(|error| error.to_string())?;
|
||||
Ok(PlanGddAcceptanceGateOutcome::PendingCreated)
|
||||
}
|
||||
|
||||
/// Run the production acceptance gate after a successful root
|
||||
/// `agent.acceptance_update`. All identity and projection reads happen under
|
||||
/// the project lock; callers can safely retry this function after a crash.
|
||||
pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<PlanGddAcceptanceGateOutcome, String> {
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
|
||||
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
}
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"planning.acceptance-gate",
|
||||
)
|
||||
.map_err(|error| format!("取得 acceptance-gate 项目锁失败:{error}"))?;
|
||||
ensure_plan_gdd_approval_pending_after_acceptance_locked(root, agent_id, run_id)
|
||||
}
|
||||
|
||||
fn approval_terminal_observation_exists_locked(
|
||||
root: &Path,
|
||||
receipt: &PlanGddApprovalV1,
|
||||
@@ -776,20 +1040,51 @@ fn project_receipt_locked(
|
||||
}
|
||||
|
||||
/// Reconcile receipt-derived projections after a Runner restart. Receipts
|
||||
/// are the authority; this routine only repairs projections and never creates
|
||||
/// an approval pending without an explicit upstream acceptance-gate caller.
|
||||
/// are the authority; this routine also repairs the explicitly allowed
|
||||
/// acceptance-gate projection when the graph already passed. It never
|
||||
/// re-executes Provider or `file.read` evidence actions; before an exact
|
||||
/// pending/receipt exists it may re-read the current Markdown only to verify
|
||||
/// the persisted receipt hash. Recovery never changes any GDD/approval
|
||||
/// identity.
|
||||
pub(crate) fn reconcile_plan_gdd_approval_projections_at(
|
||||
root: &Path,
|
||||
) -> Result<bool, PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.approval-recovery")
|
||||
.map_err(|error| approval_error("PLAN_DURABILITY_FAILED", error))?;
|
||||
let gdds = read_plan_gdd_chain_locked(root)?;
|
||||
let plan_root_run_ids = gdds
|
||||
.iter()
|
||||
.filter(|gdd| {
|
||||
gdd.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& gdd.source == "agent-delegate"
|
||||
&& gdd.run_profile == AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
&& gdd.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
})
|
||||
.map(|gdd| gdd.root_run_id.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut recovery_pending = false;
|
||||
for root_run_id in plan_root_run_ids {
|
||||
match ensure_plan_gdd_approval_pending_after_acceptance_locked(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
root_run_id,
|
||||
)
|
||||
.map_err(|error| approval_error("PLAN_NEEDS_RECONCILIATION", error))?
|
||||
{
|
||||
PlanGddAcceptanceGateOutcome::PendingCreated => recovery_pending = true,
|
||||
PlanGddAcceptanceGateOutcome::NotApplicable
|
||||
| PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim { .. }
|
||||
| PlanGddAcceptanceGateOutcome::WaitingForEvidence { .. }
|
||||
| PlanGddAcceptanceGateOutcome::RepairRequired { .. }
|
||||
| PlanGddAcceptanceGateOutcome::PendingAlreadyPresent
|
||||
| PlanGddAcceptanceGateOutcome::AlreadyDecided => {}
|
||||
}
|
||||
}
|
||||
let approvals = read_plan_gdd_approvals_locked(root)?;
|
||||
if approvals.is_empty() {
|
||||
return Ok(false);
|
||||
return Ok(recovery_pending);
|
||||
}
|
||||
validate_plan_gdd_approvals_against_gdds(&gdds, &approvals)?;
|
||||
let mut recovery_pending = false;
|
||||
for receipt in &approvals {
|
||||
recovery_pending |= project_receipt_locked(root, &gdds, receipt)?;
|
||||
}
|
||||
@@ -1175,7 +1470,7 @@ fn plan_gdd_anchor_state_locked(
|
||||
Ok((pending_state, batch_state))
|
||||
}
|
||||
|
||||
fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
|
||||
fn plan_gdd_session_identity_matches_gdd(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
|
||||
session.project_id == gdd.project_id
|
||||
&& session.gdd_id == gdd.gdd_id
|
||||
&& session.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
@@ -1188,6 +1483,15 @@ fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1)
|
||||
&& session.session_id == gdd.session_id
|
||||
&& session.latest_submitted_ref.as_ref() == Some(&receipt_plan_ref_from_gdd(gdd))
|
||||
&& session.active_run_id.is_none()
|
||||
&& session.last_run_id == gdd.created_by_run_id
|
||||
}
|
||||
|
||||
fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
|
||||
plan_gdd_session_identity_matches_gdd(session, gdd)
|
||||
&& session.phase == "awaiting_gdd_approval"
|
||||
&& session.session_revision == gdd.source_session_revision.saturating_add(1)
|
||||
&& session.previous_fingerprint.as_deref() == Some(gdd.source_session_fingerprint.as_str())
|
||||
&& session.last_decision_ref.is_none()
|
||||
}
|
||||
|
||||
fn receipt_plan_ref_from_gdd(gdd: &PlanGddV1) -> PlanGddRef {
|
||||
@@ -1203,7 +1507,7 @@ fn plan_gdd_session_matches_receipt(
|
||||
gdd: &PlanGddV1,
|
||||
receipt: &PlanGddApprovalV1,
|
||||
) -> bool {
|
||||
if !plan_gdd_session_matches_submission(session, gdd) {
|
||||
if !plan_gdd_session_identity_matches_gdd(session, gdd) {
|
||||
return false;
|
||||
}
|
||||
let expected_phase = match receipt.action.as_str() {
|
||||
@@ -1320,7 +1624,25 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if plan_gdds.is_empty() {
|
||||
return None;
|
||||
return match read_game_creator_agent_runtime_goal_contract_at(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
) {
|
||||
Ok(Some(_)) => Some(plan_gdd_completion_blocker(
|
||||
"blocked",
|
||||
"当前立项策划根 Run 尚未提交 Fast GDD,不能收束任务",
|
||||
format!(
|
||||
"rootRunId={run_id} · nextRequiredAction=agent.delegate;上一根 Run 遗留的 game/fast_gdd.md 或 Acceptance Graph 不能代替本根提交"
|
||||
),
|
||||
)),
|
||||
Ok(None) => None,
|
||||
Err(error) => Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Goal Contract 无法读取,不能确认当前立项策划根 Run 的 GDD 提交状态",
|
||||
error,
|
||||
)),
|
||||
};
|
||||
}
|
||||
let latest = *plan_gdds.last().expect("non-empty plan GDD lineage");
|
||||
|
||||
@@ -1347,6 +1669,80 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
&& receipt.fingerprint == latest.fingerprint
|
||||
});
|
||||
|
||||
let pending = match read_plan_gdd_approval_pending_locked(root) {
|
||||
Ok(pending) => pending,
|
||||
Err(error) => {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Fast GDD approval pending 无法读取,不能收束任务",
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if receipt.is_none() {
|
||||
if let Some(pending) = pending.as_ref() {
|
||||
if !pending_matches_gdd(pending, latest) {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Fast GDD approval pending identity 与当前 GDD 不一致",
|
||||
format!("gddVersion={}", latest.version),
|
||||
));
|
||||
}
|
||||
}
|
||||
// An exact awaiting pending is proof that the acceptance gate already
|
||||
// passed. Do not re-read the Markdown after recovery or projection
|
||||
// repair; only the no-pending path needs the graph preflight.
|
||||
let pending_is_exact_awaiting = pending
|
||||
.as_ref()
|
||||
.is_some_and(|pending| pending_matches_gdd(pending, latest));
|
||||
if !pending_is_exact_awaiting {
|
||||
let contract = match read_game_creator_agent_runtime_goal_contract_at(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
) {
|
||||
Ok(contract) => contract,
|
||||
Err(error) => {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Goal Contract 无法读取,不能确认 Fast GDD 验收前置门",
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
if contract.is_some() {
|
||||
let global_latest_matches = gdds.last().is_some_and(|global_latest| {
|
||||
global_latest.gdd_id == latest.gdd_id
|
||||
&& global_latest.version == latest.version
|
||||
&& global_latest.fingerprint == latest.fingerprint
|
||||
});
|
||||
if !global_latest_matches {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"当前 Fast GDD 不是项目 lineage 最新版本,不能自动收束旧 plan 根",
|
||||
format!(
|
||||
"gddVersion={} · rootRunId={} · 需由当前 lineage 根 Run 继续处理",
|
||||
latest.version, run_id
|
||||
),
|
||||
));
|
||||
}
|
||||
match plan_fast_gdd_acceptance_passed_at_locked(root, latest) {
|
||||
Ok(true) => {}
|
||||
// Leave the not-yet-passed phase to the Acceptance Graph
|
||||
// blocker so the Supervisor can take file.read evidence.
|
||||
Ok(false) => return None,
|
||||
Err(error) => {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Fast GDD 验收前置门 identity 无法核对",
|
||||
error,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session = match read_plan_session_with_recovery_locked(root) {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => {
|
||||
@@ -1364,7 +1760,7 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if !plan_gdd_session_matches_submission(&session, latest) {
|
||||
if receipt.is_none() && !plan_gdd_session_matches_submission(&session, latest) {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"planning session 尚未收口到当前 Fast GDD 提交",
|
||||
@@ -1378,16 +1774,6 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
|
||||
let pending = match read_plan_gdd_approval_pending_locked(root) {
|
||||
Ok(pending) => pending,
|
||||
Err(error) => {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"needs-reconciliation",
|
||||
"Fast GDD approval pending 无法读取,不能收束任务",
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let (pending_anchor, batch_anchor) = match plan_gdd_anchor_state_locked(root, latest) {
|
||||
Ok(state) => state,
|
||||
Err(error) => {
|
||||
|
||||
+1062
-2
File diff suppressed because it is too large
Load Diff
@@ -148,23 +148,90 @@ pub(crate) fn observe_agent_runtime_acceptance_update(
|
||||
)
|
||||
})();
|
||||
match result {
|
||||
Ok(state) => AgentRuntimeToolObservation {
|
||||
tool: "agent.acceptance_update".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("Acceptance Graph 已更新到 revision {}", state.revision),
|
||||
detail: Some(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"contractFingerprint": state.contract_fingerprint,
|
||||
"acceptanceRevision": state.revision,
|
||||
"stateFingerprint": state.state_fingerprint,
|
||||
"evaluations": state.evaluations.iter().map(|evaluation| serde_json::json!({
|
||||
"criterionId": evaluation.criterion_id,
|
||||
"status": evaluation.status,
|
||||
})).collect::<Vec<_>>(),
|
||||
}))
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
Ok(state) => {
|
||||
let gate = ensure_plan_gdd_approval_pending_after_acceptance_at(root, agent_id, run_id);
|
||||
match gate {
|
||||
Err(error) => AgentRuntimeToolObservation {
|
||||
tool: "agent.acceptance_update".to_string(),
|
||||
status: "needs-reconciliation".to_string(),
|
||||
summary: format!(
|
||||
"Acceptance Graph 已更新到 revision {},但审批前置门需要人工核对",
|
||||
state.revision
|
||||
),
|
||||
detail: Some(redact_agent_runtime_project_paths(
|
||||
root,
|
||||
&format!(
|
||||
"Acceptance Graph 已落盘,但 acceptance-gate 未能安全收束:{error}"
|
||||
),
|
||||
800,
|
||||
)),
|
||||
},
|
||||
Ok(gate) => {
|
||||
let mut detail = serde_json::json!({
|
||||
"contractFingerprint": state.contract_fingerprint,
|
||||
"acceptanceRevision": state.revision,
|
||||
"stateFingerprint": state.state_fingerprint,
|
||||
"evaluations": state.evaluations.iter().map(|evaluation| serde_json::json!({
|
||||
"criterionId": evaluation.criterion_id,
|
||||
"status": evaluation.status,
|
||||
})).collect::<Vec<_>>(),
|
||||
});
|
||||
match gate {
|
||||
PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim {
|
||||
delegation_id,
|
||||
detail: gate_detail,
|
||||
} => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("not-created".to_string());
|
||||
detail["nextRequiredAction"] =
|
||||
serde_json::Value::String("agent.run_status".to_string());
|
||||
detail["delegationId"] = serde_json::Value::String(delegation_id);
|
||||
detail["gateDetail"] = serde_json::Value::String(gate_detail);
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::WaitingForEvidence {
|
||||
detail: gate_detail,
|
||||
} => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("not-created".to_string());
|
||||
detail["nextRequiredAction"] =
|
||||
serde_json::Value::String("file.read".to_string());
|
||||
detail["gateDetail"] = serde_json::Value::String(gate_detail);
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::RepairRequired {
|
||||
repair_of_delegation_id,
|
||||
detail: gate_detail,
|
||||
} => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("not-created".to_string());
|
||||
detail["nextRequiredAction"] =
|
||||
serde_json::Value::String("agent.delegate".to_string());
|
||||
detail["repairOfDelegationId"] =
|
||||
serde_json::Value::String(repair_of_delegation_id);
|
||||
detail["gateDetail"] = serde_json::Value::String(gate_detail);
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::PendingCreated => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("created".to_string());
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::PendingAlreadyPresent => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("already-present".to_string());
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::AlreadyDecided => {
|
||||
detail["approvalPending"] =
|
||||
serde_json::Value::String("already-decided".to_string());
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::NotApplicable => {}
|
||||
}
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "agent.acceptance_update".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("Acceptance Graph 已更新到 revision {}", state.revision),
|
||||
detail: Some(serde_json::to_string(&detail).unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => AgentRuntimeToolObservation {
|
||||
tool: "agent.acceptance_update".to_string(),
|
||||
status: "rejected".to_string(),
|
||||
|
||||
@@ -219,6 +219,48 @@ pub(crate) fn observe_agent_runtime_run_status(
|
||||
})();
|
||||
}
|
||||
|
||||
// `agent.run_status` already executes under the project snapshot lock.
|
||||
// Re-run the locked gate on every plan-root status observation, even
|
||||
// when no Ready delivery remains: an earlier run_status action may
|
||||
// have durably claimed this delivery before its gate could persist.
|
||||
let plan_gdd_acceptance_gate =
|
||||
match ensure_plan_gdd_approval_pending_after_acceptance_locked(
|
||||
root, agent_id, run_id,
|
||||
)? {
|
||||
PlanGddAcceptanceGateOutcome::NotApplicable => None,
|
||||
PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim { .. } => {
|
||||
return Err(
|
||||
"agent.run_status 已认领 planning delivery,但 acceptance gate 仍观察到 Ready 状态"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
PlanGddAcceptanceGateOutcome::WaitingForEvidence {
|
||||
detail: gate_detail,
|
||||
} => Some(serde_json::json!({
|
||||
"approvalPending": "not-created",
|
||||
"nextRequiredAction": "file.read",
|
||||
"gateDetail": gate_detail,
|
||||
})),
|
||||
PlanGddAcceptanceGateOutcome::RepairRequired {
|
||||
repair_of_delegation_id,
|
||||
detail: gate_detail,
|
||||
} => Some(serde_json::json!({
|
||||
"approvalPending": "not-created",
|
||||
"nextRequiredAction": "agent.delegate",
|
||||
"repairOfDelegationId": repair_of_delegation_id,
|
||||
"gateDetail": gate_detail,
|
||||
})),
|
||||
PlanGddAcceptanceGateOutcome::PendingCreated => Some(serde_json::json!({
|
||||
"approvalPending": "created",
|
||||
})),
|
||||
PlanGddAcceptanceGateOutcome::PendingAlreadyPresent => Some(serde_json::json!({
|
||||
"approvalPending": "already-present",
|
||||
})),
|
||||
PlanGddAcceptanceGateOutcome::AlreadyDecided => Some(serde_json::json!({
|
||||
"approvalPending": "already-decided",
|
||||
})),
|
||||
};
|
||||
|
||||
let claimed_delegate_deliveries = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
claimed_static_delegate_deliveries_at(root, agent_id, run_id)?
|
||||
} else {
|
||||
@@ -258,6 +300,11 @@ pub(crate) fn observe_agent_runtime_run_status(
|
||||
.map_err(|error| format!("序列化专业 Agent claimed contracts 失败:{error}"))?;
|
||||
detail = format!("claimedDelegateContracts: {payload}\n\n{detail}");
|
||||
}
|
||||
if let Some(gate) = plan_gdd_acceptance_gate {
|
||||
let payload = serde_json::to_string(&gate)
|
||||
.map_err(|error| format!("序列化 Fast GDD acceptance gate 结果失败:{error}"))?;
|
||||
detail = format!("planGddAcceptanceGate: {payload}\n\n{detail}");
|
||||
}
|
||||
|
||||
// Ready payloads are complete evidence. The ordinary status summary may be shortened,
|
||||
// but evidence must fit its budget before the corresponding claim is committed.
|
||||
|
||||
@@ -13,7 +13,8 @@ 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_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL,
|
||||
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,
|
||||
@@ -359,6 +360,53 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
|
||||
Ok(functions)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(crate) fn restrict_plan_root_goal_contract_schema(
|
||||
functions: &mut [LlmFunctionTool],
|
||||
) -> Result<(), String> {
|
||||
let goal_contract_function = native_runtime_function_name("agent.goal_contract")
|
||||
.ok_or_else(|| "无法生成 Goal Contract 工具函数名".to_string())?;
|
||||
let Some(function) = functions
|
||||
.iter_mut()
|
||||
.find(|function| function.name == goal_contract_function)
|
||||
else {
|
||||
return Err("根 plan 请求缺少 agent.goal_contract 工具".to_string());
|
||||
};
|
||||
function.parameters = action_function_parameters(json!({
|
||||
"type": "object",
|
||||
"required": ["outcome", "nonNegotiables", "preferences", "forbiddenAssumptions", "openQuestions", "acceptanceNodes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"outcome": { "type": "string", "minLength": 1, "maxLength": 4000 },
|
||||
"nonNegotiables": string_array_schema(16),
|
||||
"preferences": { "type": "array", "maxItems": 0, "items": { "type": "string" } },
|
||||
"forbiddenAssumptions": string_array_schema(16),
|
||||
"openQuestions": string_array_schema(16),
|
||||
"acceptanceNodes": {
|
||||
"type": "array", "minItems": 1, "maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["criterionId", "criterion", "required", "requiredEvidence", "dependsOn"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"criterionId": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_ID] },
|
||||
"criterion": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION] },
|
||||
"required": { "type": "boolean", "enum": [true] },
|
||||
"requiredEvidence": {
|
||||
"type": "array", "minItems": 1, "maxItems": 1,
|
||||
"items": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE] }
|
||||
},
|
||||
"dependsOn": { "type": "array", "maxItems": 0, "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_native_tool_allowed_for_agent(agent_id: &str, tool: &str) -> bool {
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// update_agent_plan/respond_to_user are protocol controls and are
|
||||
@@ -2068,6 +2116,7 @@ mod tests {
|
||||
])
|
||||
);
|
||||
assert_eq!(goal["properties"]["acceptanceNodes"]["minItems"], 1);
|
||||
assert_eq!(goal["properties"]["acceptanceNodes"]["maxItems"], 32);
|
||||
assert_eq!(
|
||||
goal["properties"]["acceptanceNodes"]["items"]["properties"]["requiredEvidence"]
|
||||
["items"]["pattern"],
|
||||
@@ -2098,6 +2147,23 @@ mod tests {
|
||||
["required"],
|
||||
json!(["agentId", "runId", "actionId"])
|
||||
);
|
||||
|
||||
let goal_function = native_runtime_function_name("agent.goal_contract")
|
||||
.expect("goal contract function name");
|
||||
let mut functions = vec![LlmFunctionTool::new(
|
||||
goal_function.clone(),
|
||||
"goal",
|
||||
action_function_parameters(runtime_tool_input_schema("agent.goal_contract")),
|
||||
)];
|
||||
restrict_plan_root_goal_contract_schema(&mut functions).expect("restrict plan schema");
|
||||
let fixed = &functions[0].parameters["properties"]["input"];
|
||||
assert_eq!(fixed["properties"]["acceptanceNodes"]["maxItems"], 1);
|
||||
assert_eq!(fixed["properties"]["preferences"]["maxItems"], 0);
|
||||
assert_eq!(
|
||||
fixed["properties"]["acceptanceNodes"]["items"]["properties"]["criterionId"]["enum"],
|
||||
json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
|
||||
);
|
||||
assert_eq!(functions[0].name, goal_function);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-15 M1C-2a 隔离工作树实现:固定 Goal Contract 与审批前置门
|
||||
|
||||
- **本轮范围**:只实现 Supervisor 根 run 的 Goal Contract / Acceptance Graph 与 Fast GDD 审批前置门;不接 `M1C-2b` 澄清中转、审批 UI、构建准入或下游完整构建。当前变更仍在隔离 worktree,尚未合回原分支。
|
||||
- **固定合同**:`project-supervisor-plan / standard` 的首轮与格式修复请求都必须且只能调用一次 `agent.goal_contract`。按项目变化的字段只有 `outcome / nonNegotiables / forbiddenAssumptions / openQuestions`;`preferences` 固定空数组,`acceptanceNodes` 固定为唯一 `fast-gdd-serves-intent` 节点,required evidence 固定 `tool:file.read`。其它 source 保持动态合同,即使使用同名 criterionId 也不套 Fast GDD 特例。
|
||||
- **证据合同**:Fast GDD evidence 只接受当前 Supervisor 根 run 自己读取 `game/fast_gdd.md` 的成功 action receipt。receipt 保存规范化路径、完整内容 SHA-256 与行覆盖摘要;多页必须从 `startLine=1` 无缺口、无重叠覆盖到 EOF,全部页同 hash/同总行数,`agent.acceptance_update.evidence` 必须列出所有分页 actionId。普通 Graph 读取只验 durable receipt 形状与完整覆盖;当前 Markdown hash 只在审批前且无 exact pending/receipt 时复核,审批后的状态投影不会让旧 Graph 损坏。
|
||||
- **取证与返工三态**:planning delivery 先由同一根 run 的 `agent.run_status` durable 认领。Graph 缺失、not-observed、project revision/hash 过期或证据不完整属于 `NeedsEvidence`,下一步是 `file.read`,不得提前返工;只有当前完整证据支撑的显式 failed 属 `RepairRequired`,才返回原 delivery 的 `repairOfDelegationId`;passed 才幂等创建 `gdd-approval` pending。`run_status` 在持有项目锁时调用 locked gate,并在 Ready 数为零时仍重放,封住旧 action 已认领但 gate 尚未落盘的崩溃窗口;GDD create 到 child/delivery 完成前保持惰性,不抢断 M1B-2 恢复。
|
||||
- **恢复与完成门**:acceptance update、delivery claim、Runner recovery、completion 与 finalization 都消费同一 gate。恢复不重新执行 Provider 或 `file.read` 动作,只复核 durable Graph、动作回执与当前 Markdown hash;missing pending 按原 approvalRequestId/identity 补建,exact pending 与 receipt 优先于 Graph/hash 复核,冲突则失败关闭。当前根没有自己的 GDD 时,上一根遗留 Markdown/Graph 不能绕过完成门。
|
||||
- **回归与门禁**:已新增真实 `file.read` receipt 的错路径、错误 actor、旧 hash、120/200 行部分覆盖、两页完整覆盖,claim 前后、零 Ready 重放、Dispatched 中间态、审批后 Markdown 改写、pending 二次 recovery 幂等、无本根 GDD、非 plan 同名 criterion 与 M1B-2 双锚缺失恢复顺序用例;终审另补 exact pending 优先于 stale Graph、exact receipt 优先于后来 failed Graph 及 pending/receipt 冲突仍失败关闭。`cargo fmt --check`、`cargo test goal_contract`(14 passed)、`cargo test m1c2a`(14 passed)、`cargo test acceptance_graph`(13 passed)、`cargo test planning_submit`(51 passed)、`cargo test planning_approval`(1 passed)、`cargo test finalization`(52 passed)及 M1B-2 双锚恢复回归(1 passed)均通过;`cargo check --all-targets`、`npm run check:encoding`(5403 files)与 `git diff --check` 通过。扩展 `cargo test autonomous_completion_contract` 在终审修复前曾整组 122 passed;最终代码下两次整组均为 121 passed,并由同一条无 plan 路径交集的 `game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress` 停在 `planning` 超时,精确单测随即 1 passed(9.49s)。判为并行负载时序抖动,修复边界不明确且超出 M1C-2a,按本轮缺陷策略保留、不改 game-chat。门禁期间另发现 receipt 已存在时 completion gate 仍错误要求提交前 session 状态,导致审批后无法继续核对 terminal observation;已收窄为仅无 receipt 时校验提交态,有 receipt 时由既有 receipt session 强判据收口,整组 `planning_submit` 复跑通过。
|
||||
|
||||
## 2026-08-15 M1C-1 隔离工作树收口:审批核心与专用完成门已落地,生产前置门保持后置
|
||||
|
||||
- **本轮落地**:在 `planning_storage.rs` 增加 `plan-gdd-approval.v1` receipt、`plan-gdd-approval-pending.v1` projection、comment/decision/receipt/pending 的 typed fingerprint 与 strict canonical 校验;审批 observation 固定校验 tool/status、版本摘要、detail 前缀和规范化 comment。`planning_approval.rs` 增加 receipt create-only、三动作幂等(`committed / replayed / already-decided`)、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复,以及只读的 plan 根专用 completion blocker;`commands.rs` 暴露 `decide_game_creator_plan_gdd`。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 立项策划 Agent(Fast GDD)技术方案
|
||||
|
||||
- 日期:2026-08-10
|
||||
- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。**当前隔离 worktree 已完成 M1C-1 的审批核心、receipt 投影/恢复和 plan 根专用 completion blocker;生产 acceptance-gate pending caller、验收图接线、审批 UI 与构建准入仍后置,完整交付未完成**(见第 23.6、23.8 节)。后续执行计划见第 23.6 节。
|
||||
- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。**当前隔离 worktree 已完成并通过 `M1C-2a` 本包门禁**:已接通固定 Goal Contract、Supervisor 根 run 的完整分页 `file.read` evidence、claim 后三态 acceptance gate、审批 pending 恢复及 completion/finalization 门;改动尚未合回。**`M1C-2b`、审批 UI、构建准入与下游完整构建仍后置,M1 整体不可交付**(见第 23.6、23.8 节)。后续执行计划见第 23.6 节。
|
||||
- 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入
|
||||
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 工作包已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径,当前隔离 worktree 另已补齐 M1C-1 receipt/审批核心、投影恢复和 plan 根完成门;生产 acceptance-gate pending caller、验收图接线、审批 UI、构建绑定和正式入口仍不可用
|
||||
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径,`M1C-1` 已提供 receipt/审批核心、投影恢复和 plan 根完成门;当前隔离 worktree 的 `M1C-2a` 已补齐固定 Goal Contract、验收图证据与生产 acceptance gate。`M1C-2b` 澄清中转、审批 UI、构建绑定和正式入口仍不可用
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
@@ -1206,7 +1206,11 @@ type PlanSubmitGddResult = {
|
||||
|
||||
此时用户以为已经定稿、系统却卡住,且 receipt 不可回滚(要改只能提交并决定新版本),这个状态对用户不可解释。
|
||||
|
||||
**固定顺序**:Supervisor 收到 `plan.submit_gdd` 的 `Submitted` 回执后,**必须先**以 `tool:file.read` 对 `game/fast_gdd.md` 取证并确认合同的全部 required 验收节点,**通过后才允许把 `gdd-approval` pending 暴露给用户**。取证未通过时**不创建审批卡**,改为发起返工委派(`repair_of` 指向原 delivery),由策划子 Agent 修订后重新提交。
|
||||
**固定顺序**:Supervisor 收到 `plan.submit_gdd` 的 `Submitted` 回执后,**必须先**以 `tool:file.read` 对 `game/fast_gdd.md` 取证并确认合同的全部 required 验收节点,**通过后才允许把 `gdd-approval` pending 暴露给用户**。尚未取证、分页未覆盖全文或回执 SHA-256 已不是当前 Markdown 时,下一步仍是由当前 Supervisor 根 run 重新读取并更新验收图,不能提前返工;只有当前根、当前 project revision、当前 Markdown 全文证据支撑的显式 `failed` 才表示“取证未通过”,此时**不创建审批卡**,改为发起返工委派(`repair_of` 指向原 delivery),由策划子 Agent 修订后重新提交。
|
||||
|
||||
M1C-2a 将该取证合同进一步冻结为:evidence 必须来自当前 `project-supervisor-plan / standard` 根 run 自己的 `file.read`,策划子 run 或同根其它 child 的回执不能替代 Supervisor 的语义判断;路径必须精确为 `game/fast_gdd.md`。读取从 `startLine=1` 开始,按工具上限分页,无缺口、无重叠地覆盖到 EOF;所有页必须报告同一完整内容 SHA-256 和总行数,`agent.acceptance_update.evidence` 必须列出全部分页 actionId。普通 Acceptance Graph durable 读取只复核回执形状、身份与完整覆盖,不把审批决定后 Markdown 状态投影造成的 hash 变化误判为 Graph 损坏;当前 Markdown hash 只在尚无 receipt/pending 的审批前置门复核。
|
||||
|
||||
原 planning delivery 必须先由同一 Supervisor 根 run 通过 `agent.run_status` durable 认领。`Ready` 只返回认领动作,不给 `repairOfDelegationId`、也不建审批卡;`ClaimedByParent` 后才根据上述三态产生“继续取证 / 指向原 delivery 的返工 / 建卡”。`plan.submit_gdd` 已 create 但 child 或 delivery 尚未完成的恢复窗口保持惰性,由 M1B-2 原恢复链先收口,不能被审批门抢先判错。
|
||||
|
||||
**为什么必须是这个方向**:两道检查的性质不同,混序会让它们互相抵消。
|
||||
|
||||
@@ -1218,7 +1222,7 @@ type PlanSubmitGddResult = {
|
||||
|
||||
机器判合格在前、人判满意在后,三道门各自的失败路径互不干扰;反序则会出现「人已经批了、机器才说不合格」这种没有出路的组合。schema 那道天然满足本约束——校验失败不产生版本,不产生版本就没有审批卡,结构上不可能反序。
|
||||
|
||||
**恢复语义**:取证已完成但 pending 尚未创建时,按既有 identity 幂等补建审批卡,不重新取证;pending 已创建则证明取证已通过,恢复路径不得再次要求取证。取证结果本身不落新的 durable 记录,它就是现役 Acceptance Graph 的 evaluation。
|
||||
**恢复语义**:取证已完成但 pending 尚未创建时,按既有 identity 幂等补建审批卡,不重新执行 Provider 或 `file.read` 动作;恢复只复核 durable Acceptance Graph、动作回执与当前 Markdown hash。pending 已创建则证明取证已通过,恢复路径不得再次要求取证,也不得因审批后的 Markdown 状态改写否定旧 Graph。取证结果本身不落新的平行记录,它就是现役 Acceptance Graph 的 evaluation。
|
||||
|
||||
### 13.1 `gdd-approval` pending
|
||||
|
||||
@@ -1827,7 +1831,7 @@ D9 之后,root 是未变的 Project Supervisor,它在 `standard` 下天然
|
||||
仍然成立、且与本项无关的是**通用 handoff 安全边界**:任何 Provider 成功响应都必须先过现役 storage 的大小、控制字符、敏感键、绝对路径、容量与 durable identity 门并落盘,才允许被消费;storage 门拒绝或 handoff 无法 durable 提交时,禁止保存不安全正文、补 lifecycle completed 或自动重发。这是现役机制,策划链路照用,不需要额外裁决。
|
||||
- ~~**plan source 与 Goal Contract 协议的关系**~~——**2026-08-13 已裁决**:`project-supervisor-plan` **进** `agent_runtime_supervisor_source_is_trusted`;做方案链路正常参与 Goal Contract 协议,不做豁免。原「裁决冻结前依赖 plan source 可信身份的 M1 代码不得合入」的硬门随本裁决解除。
|
||||
|
||||
*原阻塞理由已失效*:2026-08-12 排除该方向的依据是「出口门在验收阶段照卡且更难救——合同强制至少一个带 `requiredEvidence` 的 required 节点,而**策划 Agent 按设计一项证据工具都不该有**,`.agent/planning/**` 由 Runtime 写、不产生 Agent 回执,必然留下永不 `passed` 的节点」。该推理写于 D6/D9 拓扑,当时「plan run」就是策划 Agent 本身。D11 拆成两层后两个前提都不成立:① 策划子 Agent 的 exact allowlist 恰好含 `file.read` / `file.list`,二者都在 `agent_runtime_acceptance_evidence_tools()` 白名单内;② 更根本的是证据**不必由它出**——`validate_acceptance_evidence_identity_at`(`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs:152`)对证据来源的唯一要求是 `binding.root_agent_id/root_run_id` 等于合同的根,**不要求是根 Agent 自己的回执**;委派子 Agent 的 binding 根就是 Supervisor,因此两层的 `file.read` 回执都是合法验收证据。
|
||||
*原阻塞理由已失效*:2026-08-12 排除该方向的依据是「出口门在验收阶段照卡且更难救——合同强制至少一个带 `requiredEvidence` 的 required 节点,而**策划 Agent 按设计一项证据工具都不该有**,`.agent/planning/**` 由 Runtime 写、不产生 Agent 回执,必然留下永不 `passed` 的节点」。该推理写于 D6/D9 拓扑,当时「plan run」就是策划 Agent 本身。D11 拆成两层后,Goal Contract 与最终语义验收都属于 Supervisor 根 run;策划子 Agent 负责提交结构化 GDD,不负责替根判断“是否服务用户意图”。M1C-2a 因此把 Fast GDD 特例收紧为**只接受当前 Supervisor 根 run 自己的 `file.read` 回执**,不再沿用通用 Acceptance Graph 对同根 child evidence 的宽松能力。Supervisor 的 standard 工具面已有 `file.read` / `agent.acceptance_update`,固定节点可正常收敛,同时不扩大策划子 Agent 的工具面。
|
||||
|
||||
*三道门逐一可满足*:入口门只规定时序——合同不存在时本轮必须且只能是一个 `agent.goal_contract`,因此做方案链路的 turn 1 冻结合同、turn 2 才发委派,多一次 Provider 调用而已,不是阻塞;内容门要求 ≥1 个 required 节点且 `requiredEvidence` 命中白名单,`tool:file.read` 满足;出口门要求合同已冻结且验收图收敛,两者都能达成。
|
||||
|
||||
@@ -1838,7 +1842,7 @@ D9 之后,root 是未变的 Project Supervisor,它在 `standard` 下天然
|
||||
- **`plan.submit_gdd` 的 strict schema** 承担机器可判的字段存在性与字段约束(`oneLiner` 长度、`pillars` 条数、MVP 系统条数等,见第 8.2 / 8.3 节),这是硬校验;
|
||||
- **Goal Contract 验收图**承担「产物确实服务了用户这次的意图」,由 Supervisor 以 `tool:file.read` 对 `game/fast_gdd.md` 取证后确认节点。
|
||||
|
||||
Goal Contract 中**按项目变化**的只有 `outcome` / `nonNegotiables` / `forbiddenAssumptions` / `openQuestions` 四项——它们承载入口门 Prompt 要求的「自行理解用户真正要做的事」;`acceptanceNodes` 近乎固定,不算「照抄固定信号代替理解」,因为「什么算一份合格 GDD」本就不是本轮需要理解的东西。
|
||||
Goal Contract 中**按项目变化**的只有 `outcome` / `nonNegotiables` / `forbiddenAssumptions` / `openQuestions` 四项——它们承载入口门 Prompt 要求的「自行理解用户真正要做的事」;`preferences` 在该 source 下固定为空数组,`acceptanceNodes` 固定为唯一 `fast-gdd-serves-intent` 节点,不算「照抄固定信号代替理解」,因为「什么算一份合格 GDD」本就不是本轮需要理解的东西。其它可信 source 继续使用现役动态 Goal Contract,不因 criterionId 恰好同名而套用 Fast GDD 的路径、actor 或分页特例。
|
||||
|
||||
本条**不改变** D1(游戏支柱按项目生成 2~4 条,四个候选不固定套用)与 D8(GDD 不含引擎字段,平台事实由 Runtime 注入)已冻结的口径;验收图据以生成的合格标准以本文档第 5.3、8.2、8.3 节为准。
|
||||
- ~~**策划节点 `agentId` / source 命名**~~——**2026-08-13 已裁决冻结**:策划子 Agent `agentId=project-planning`;Supervisor 侧承载「做方案」入口的新可信 source 取全新常量名 `project-supervisor-plan`(不沿用历史 `project-supervisor-plan-chat` 字面,避免语义改变后接错旧代码路径)。第 1.1 节批二不再被本项阻塞,改由下方新增的 `build.rs` 一致性待裁决项阻塞。
|
||||
@@ -1921,7 +1925,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`
|
||||
| 三 | `project-planning` 的 agentCatalog 登记 | **已完成**(机制冻结见第 3.1 节;**代码亦已落地**,2026-08-13:manifest、prompt bundle、runtime adapter、`prompt.rs` 角色合成分支及四处 needs_change 全部合入) |
|
||||
| 四 | `M0A-3` 批二:拓扑与工具面部分 | **已完成**(2026-08-13),拆解见下 |
|
||||
| 四之余 | schema 与 golden vector 收口 | **已完成**(2026-08-13),拆解见下 |
|
||||
| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3`、`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0`、`M1C-0b` 已落地并合入**;当前隔离 worktree 已实现 M1C-1 审批核心、receipt 投影/恢复、terminal observation 完整性校验和 plan 根专用 completion blocker。**生产 acceptance-gate pending caller、验收图接线、审批 UI、M1C-2b 澄清中转与构建准入仍未完成**,因此 M1C-1 尚未合回、M1 整体不可交付。合入门见第 23.8 节 |
|
||||
| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3`、`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0`、`M1C-0b`、`M1C-1` 已落地并合入**;当前隔离 worktree 的 `M1C-2a` 已完成固定 Goal Contract、完整分页 `file.read` evidence、claim 后三态 acceptance gate、pending exactly-once 恢复与 completion/finalization 接线,并通过本包定向门禁,尚未合回。**`M1C-2b` 澄清中转、审批 UI、构建准入与下游完整构建仍未完成**,因此 M1 整体仍不可交付。合入门见第 23.8 节 |
|
||||
|
||||
批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**:
|
||||
|
||||
@@ -2009,8 +2013,8 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`
|
||||
| `M1B-2` | `plan.submit_gdd` 原生工具、exact planning Provider 请求绑定与 GDD 提交点 | `M1B-1` | **工作包已通过本包门禁并以 `27c3eb847` 合入本分支**。实现合同:四类 request kind 全部写 v3 lifecycle、required binding 与同一 dedicated structured-injection user message;只有 `tool-plan` 可生成 sole-submit v4 batch;提交点后只修复 index/Markdown/session successor、终止策划子 run,并保留 generic v5 standalone pending + v4 batch anchors,不创建 `gdd-approval` planning pending/审批等待。定向 Rust、`cargo check --offline`、格式、编码与 diff 门禁均已通过;完整审批链路与产品可交付仍留给后续工作包。**2026-08-15 订正:本包的定向门禁漏掉了两处跨包回归,全量 CI 才暴露**——① `validate_next_entry` 把 `same_tool_plan_repair_chain` 提到 `loop_iteration` 分支之外,使含本轮量(steer cursor / goal revision / planning binding)的判据作用于跨 loop 续跑,两条 `tests::goal` 与一条交接用例失败;② 新增的 `missing_plan_submit_anchor_candidate_at` 在 resume 最前面强读 runtime state,短路了下游对不可读 state 的 fail-closed 兜底。均已修,详见 `decision-log.md` 同日条裁决一/二。**教训已记入门禁**:新增或移动校验必须同时补一条「正向必须被接受」的回归,只钉拒绝挡不住作用域被放大 |
|
||||
| `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **已落地**:无审批写入方、是惰性路径;用户修订跳不增 `repair_depth` 也不重置 `clarification_round`,连续修订可通过;做游戏链路返工仍在 `depth=1` 被拒;原有三种状态与 `UserRevisionRequested` 的已知行为保持不变,未知 durable status 的前向兼容由已完成的 `M1C-0b` 显式承接,不在本包静默降级或改变 |
|
||||
| `M1C-0b` | 静态委派 durable enum 的前向兼容粒度:未知 `contract_status` 解析为显式 `Unknown` 并最大化阻塞 | `M1C-0` | **已完成**:纯读路径、无写入方、审批状态、pending、receipt 或 UI(不含 `M1C-1`)。四种已知 durable 值保持原 serde;未知字符串解析为 `Unknown(raw)`,非字符串仍拒绝,`Serialize` 及读-改-写均原样保留 raw。`Unknown` 计入 completion barrier 与 waiting blocker,返工入口无条件拒绝(含 `depth=0`),lineage 按“其它”最保守分类(`depth + 1`、`round = 0`);planning Provider、自治 liveness、终态扫描等既有读路径同步 fail closed。截断、非法 JSON、非 UTF-8、超过 128 KiB 的 sidecar 仍按整目录 fail closed,不做单条跳过。**不新增或改变 `M1B-*` 功能依赖(仅复核其既有读路径);不包含 `M1C-1` 的审批写入、receipt、UI 或构建准入** |
|
||||
| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status 与 plan 根完成门 | `M1B-2`、`M1C-0`(前向兼容粒度另见 `M1C-0b`) | **隔离 worktree 已实现核心**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;已补无 pending、awaiting、receipt 后 observation 缺失、完整收口、非 plan root scope 与 pending identity 漂移回归。**生产 acceptance-gate pending caller 与验收前置取证门后置到 `M1C-2a`,审批 UI / 澄清中转 / 构建准入仍未完成;本包尚未合回原分支**。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
|
||||
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1`、`M1A-3` | turn 1 只能是一个 `agent.goal_contract`;`acceptanceNodes` 不接受自定义;**第 13.0 节审批前置门:取证未通过时不出现审批卡、而是产生返工委派**(取证顺序是协议时序问题,归本包而非 `M1C-1`) |
|
||||
| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status 与 plan 根完成门 | `M1B-2`、`M1C-0`(前向兼容粒度另见 `M1C-0b`) | **已落地并合入**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;生产 acceptance-gate pending caller 与验收前置取证门按拆包纪律由 `M1C-2a` 承接。审批 UI / 澄清中转 / 构建准入仍未完成。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
|
||||
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1`、`M1A-3` | **当前隔离 worktree 已完成并通过本包门禁,尚未合回**:turn 1 的 request-scoped schema 与格式修复都只允许一个固定 `agent.goal_contract`;按项目变化的四项之外,`preferences=[]`、唯一验收节点及证据工具均冻结。Fast GDD evidence 只接受当前 Supervisor 根 run 对 `game/fast_gdd.md` 从第 1 行到 EOF 的同 hash 完整分页;无/旧证据先继续读取,显式 failed 才给原 delivery 的 `repairOfDelegationId`,passed 且 delivery 已认领才建 pending。pending/recovery/completion/finalization 均按同 identity 幂等,审批后 Markdown 改写不损坏 Graph。格式、Provider 强判据、M1C-2a、Acceptance Graph、planning submit/approval、finalization、all-targets、编码与 diff 门禁均通过;扩展 autonomous completion 整组的无关 game-chat 并行超时及精确复跑结果见 decision-log 同日条,不改该路径。不包含 `M1C-2b`、UI 或构建准入 |
|
||||
| `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | 3 轮上限;continuation 重放幂等不增加轮次;答案绑定冲突被拒 |
|
||||
| `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
|
||||
| `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 |
|
||||
|
||||
Reference in New Issue
Block a user