Feat/design agent simple #305
@@ -43,7 +43,6 @@ struct PromptCompositions {
|
||||
/// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、
|
||||
/// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段
|
||||
/// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。
|
||||
supervisor_plan: Vec<String>,
|
||||
supervisor_chat: SupervisorChatComposition,
|
||||
}
|
||||
|
||||
@@ -99,10 +98,6 @@ struct ProviderFragments {
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AgentCatalog {
|
||||
supervisor: AgentGroup,
|
||||
/// 立项策划子 Agent。与 `supervisor` 平级、**不进 `groups`**:`specialist_nodes`
|
||||
/// 只从 `groups[].roles[]` 派生,因此它不参与 `build.rs` 与种子 DAG 的一致性
|
||||
/// 校验,「做游戏」的 16 任务 DAG 一行不动。详见技术方案第 3.1 节。
|
||||
planning: AgentGroup,
|
||||
groups: Vec<AgentGroup>,
|
||||
}
|
||||
|
||||
@@ -231,12 +226,6 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
§ions,
|
||||
&["$base", "$visualContract"],
|
||||
)?;
|
||||
validate_composition(
|
||||
"supervisorPlan",
|
||||
&manifest.compositions.supervisor_plan,
|
||||
§ions,
|
||||
&["$header"],
|
||||
)?;
|
||||
validate_section_reference(
|
||||
&manifest.compositions.supervisor_chat.identity,
|
||||
§ions,
|
||||
@@ -299,7 +288,6 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
.supervisor
|
||||
.roles
|
||||
.iter()
|
||||
.chain(manifest.agent_catalog.planning.roles.iter())
|
||||
.chain(
|
||||
manifest
|
||||
.agent_catalog
|
||||
@@ -348,7 +336,6 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
.runtime
|
||||
.iter()
|
||||
.chain(manifest.compositions.supervisor.iter())
|
||||
.chain(manifest.compositions.supervisor_plan.iter())
|
||||
.filter(|item| !item.starts_with('$'))
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
@@ -421,14 +408,6 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
{
|
||||
register("composition supervisor", section);
|
||||
}
|
||||
for section in manifest
|
||||
.compositions
|
||||
.supervisor_plan
|
||||
.iter()
|
||||
.filter(|section| !section.starts_with('$'))
|
||||
{
|
||||
register("composition supervisorPlan", section);
|
||||
}
|
||||
register("composition supervisorChat.identity", identity);
|
||||
register(
|
||||
"composition supervisorChat.finalReply",
|
||||
@@ -457,19 +436,9 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
"composition supervisor",
|
||||
"composition supervisorChat.identity",
|
||||
]);
|
||||
// plan 根 composition 是 Supervisor system prompt 的第二条 lane,不是另一种
|
||||
// 语义面。它按设计复用 runtime lane 的 `isolatedAgentContract`(`agent.delegate`
|
||||
// 的 expectedArtifacts/writeScopes 合同)和 supervisor lane 的 `supervisorRepair`
|
||||
// (返工必须逐字继承原合同)。除这两个方向外,跨所有者复用仍然是错误。
|
||||
let allowed_plan_runtime_owners =
|
||||
BTreeSet::from(["composition runtime", "composition supervisorPlan"]);
|
||||
let allowed_plan_supervisor_owners =
|
||||
BTreeSet::from(["composition supervisor", "composition supervisorPlan"]);
|
||||
for (section, section_owners) in owners {
|
||||
if section_owners.len() > 1
|
||||
&& !(section == identity && section_owners == allowed_identity_owners)
|
||||
&& section_owners != allowed_plan_runtime_owners
|
||||
&& section_owners != allowed_plan_supervisor_owners
|
||||
{
|
||||
return Err(format!(
|
||||
"Prompt section 跨语义所有者复用:{section} -> {section_owners:?}"
|
||||
@@ -706,16 +675,11 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
if catalog.supervisor.roles.len() != 1 {
|
||||
return Err("agentCatalog.supervisor 必须且只能包含一个 role".to_string());
|
||||
}
|
||||
if catalog.planning.roles.len() != 1 {
|
||||
return Err("agentCatalog.planning 必须且只能包含一个 role".to_string());
|
||||
}
|
||||
if catalog.groups.is_empty() {
|
||||
return Err("agentCatalog.groups 不能为空".to_string());
|
||||
}
|
||||
let mut group_brief_names = BTreeSet::new();
|
||||
for group in std::iter::once(&catalog.supervisor)
|
||||
.chain(std::iter::once(&catalog.planning))
|
||||
.chain(catalog.groups.iter())
|
||||
for group in std::iter::once(&catalog.supervisor).chain(catalog.groups.iter())
|
||||
{
|
||||
if !group_brief_names.insert(group.brief_path_name.as_str()) {
|
||||
return Err(format!(
|
||||
@@ -724,10 +688,7 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut generated_names = BTreeSet::from([
|
||||
"PROJECT_SUPERVISOR".to_string(),
|
||||
"PROJECT_PLANNING".to_string(),
|
||||
]);
|
||||
let mut generated_names = BTreeSet::from(["PROJECT_SUPERVISOR".to_string()]);
|
||||
for group in &catalog.groups {
|
||||
let generated = rust_identifier(&group.id);
|
||||
if !generated
|
||||
@@ -753,12 +714,6 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
&mut task_ids,
|
||||
&mut tool_ids,
|
||||
)?;
|
||||
validate_agent_group(
|
||||
&catalog.planning,
|
||||
&mut group_ids,
|
||||
&mut task_ids,
|
||||
&mut tool_ids,
|
||||
)?;
|
||||
for group in &catalog.groups {
|
||||
validate_agent_group(group, &mut group_ids, &mut task_ids, &mut tool_ids)?;
|
||||
}
|
||||
@@ -920,10 +875,6 @@ fn render_rust(manifest: &PromptBundleManifest, sections: &BTreeMap<String, Stri
|
||||
"RUNTIME_PROMPT_SUPERVISOR_COMPOSITION",
|
||||
&manifest.compositions.supervisor,
|
||||
));
|
||||
output.push_str(&render_string_slice_const(
|
||||
"RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION",
|
||||
&manifest.compositions.supervisor_plan,
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n",
|
||||
rust_literal(&manifest.compositions.supervisor_chat.identity),
|
||||
@@ -1012,26 +963,6 @@ fn render_agent_catalog(catalog: &AgentCatalog) -> String {
|
||||
"static PROJECT_SUPERVISOR_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
|
||||
render_group_value(&catalog.supervisor, "&PROJECT_SUPERVISOR_AGENT_ROLES")
|
||||
));
|
||||
let planning_role = &catalog.planning.roles[0];
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_AGENT_ID: &str = {};\n",
|
||||
rust_literal(&planning_role.task_id)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_MEMORY_PATH: &str = {};\n",
|
||||
rust_literal(&format!(
|
||||
"memory/agents/{}",
|
||||
catalog.planning.brief_path_name
|
||||
))
|
||||
));
|
||||
output.push_str(&render_role_array(
|
||||
"PROJECT_PLANNING_AGENT_ROLES",
|
||||
&catalog.planning.roles,
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"static PROJECT_PLANNING_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
|
||||
render_group_value(&catalog.planning, "&PROJECT_PLANNING_AGENT_ROLES")
|
||||
));
|
||||
for group in &catalog.groups {
|
||||
let roles_name = format!("{}_AGENT_ROLES", rust_identifier(&group.id));
|
||||
output.push_str(&render_role_array(&roles_name, &group.roles));
|
||||
|
||||
@@ -23,11 +23,7 @@
|
||||
"supervisorVisualWithEditor": "supervisor/visual-contract-with-editor.md",
|
||||
"supervisorPlaybook": "supervisor/playbook.md",
|
||||
"supervisorClaimGate": "supervisor/claim-gate.md",
|
||||
"supervisorRepair": "supervisor/repair.md",
|
||||
"projectPlanningRoleBrief": "roles/project-planning.md",
|
||||
"planCommon": "plan/common.md",
|
||||
"planSupervisorIdentity": "plan/supervisor-identity.md",
|
||||
"planSupervisorPlaybook": "plan/supervisor-playbook.md"
|
||||
"supervisorRepair": "supervisor/repair.md"
|
||||
},
|
||||
"compositions": {
|
||||
"runtime": [
|
||||
@@ -47,14 +43,6 @@
|
||||
"supervisorClaimGate",
|
||||
"supervisorRepair"
|
||||
],
|
||||
"supervisorPlan": [
|
||||
"$header",
|
||||
"planCommon",
|
||||
"isolatedAgentContract",
|
||||
"planSupervisorIdentity",
|
||||
"planSupervisorPlaybook",
|
||||
"supervisorRepair"
|
||||
],
|
||||
"supervisorChat": {
|
||||
"identity": "supervisorIdentityContract",
|
||||
"finalReply": "supervisorFinalReplyContract"
|
||||
@@ -70,12 +58,7 @@
|
||||
"editorUnavailable": "supervisorVisualWithoutEditor"
|
||||
}
|
||||
},
|
||||
"roleOverlays": [
|
||||
{
|
||||
"agentId": "project-planning",
|
||||
"sections": ["projectPlanningRoleBrief"]
|
||||
}
|
||||
],
|
||||
"roleOverlays": [],
|
||||
"providerFragments": {
|
||||
"isolatedToolContract": "providerIsolatedToolContract",
|
||||
"autonomousRunProfile": "providerAutonomousRunProfile",
|
||||
@@ -102,21 +85,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"planning": {
|
||||
"id": "planning",
|
||||
"label": "立项策划",
|
||||
"role": "Project Planning",
|
||||
"briefPathName": "project-planning.md",
|
||||
"roles": [
|
||||
{
|
||||
"id": "project-planning",
|
||||
"role": "Project Planning",
|
||||
"taskId": "project-planning",
|
||||
"toolId": "agent.runtime.project-planning",
|
||||
"briefPathName": "project-planning.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"id": "design",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
用户只描述玩法类型、机制或相似体验时,不代表授权复刻现有游戏。所有专业 Agent 必须创建原创标题、阵营、资源、单位名称、角色造型、界面术语和视觉语言;禁止沿用、翻译或近似改写现有游戏的专有角色、单位名、Logo、贴图、标志性布局与受保护视觉语言。除非用户明确提供有权使用的项目内素材,否则不得把 Sunflower、Peashooter、向日葵、豌豆射手、僵尸等知名塔防元素写入策划、记忆、代码、图片提示或正式产物。
|
||||
|
||||
静态委派协议:新 agent.delegate 必须提交 1-8 条 acceptanceCriteria、0-16 个精确项目内非私有 expectedArtifacts,以及 nullable repairOfDelegationId/runId/continuationOfDelegationId/questionsSha256/answersSha256,普通委派后三项传 null。专业 Agent 收到的 task 会携带完整合同。Supervisor 认领回执后必须区分 evidence-ready、needs-user-input 与 needs-repair;前者仍需语义验收,needs-repair 不能作为成功。专业 Agent 若缺少会实质改变结果的用户事实,不能调用 user.input_request,必须以最终回复首行 `AGC_NEEDS_USER_INPUT_V1`,下一行短 JSON `{"questions":[...]}` 返回 1-3 个结构化问题;Runtime 会把它作为内部回执交给 Supervisor。Supervisor 对每个原 delivery 逐一用现有 user.input_request 提问,收齐对应答案后最多创建一次 continuation 委派,并同时提交 continuationOfDelegationId、questionsSha256、answersSha256;Runtime 会自动派生稳定 continuation identity,不得把多个 delivery 的问题或答案混入同一 continuation。
|
||||
|
||||
每 6 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。
|
||||
|
||||
必须直接调用与当前请求广告的工具一一对应的动作函数,或在本阶段确实无事可做时调用 respond_to_user。本 run 不维护结构化计划,也没有 update_agent_plan 可调;工具结果会由 Runtime 作为 observation 返回,不要假装工具已执行,不要把动作或回复放进普通文本,不要 markdown,不要泄露密钥。
|
||||
@@ -1,7 +0,0 @@
|
||||
你是 Genarrative AI 游戏创作桌面 App 的 Project Supervisor。当前 run 是立项策划根 run(`source=project-supervisor-plan`),你是用户在本条链路里唯一的对话对象。
|
||||
|
||||
你不生产策划内容。本链路的全部策划工作——提问、取舍、撰写 GDD——都由 `project-planning` 子 Agent 完成。你只有四件事:冻结目标合同;发起与续跑对 `project-planning` 的委派;代子 Agent 向用户提问并把答案原样转达回去;在子 Agent 提交 GDD 后完成取证,把审批交给用户。
|
||||
|
||||
你不做的事:不自己提策划问题(`user.input_request` 只能用于转达子 Agent 的问题信封);不自己撰写、补写或改写 GDD 正文、决定台账与原型验证项;不替用户做产品决定;不写文件、不跑命令、不做预览、不生成素材、不查询任务图、不调度 ready 任务;不委派 `project-planning` 以外的任何 Agent,也不创建 isolated child 或启动构建。
|
||||
|
||||
`project-planning` 的消息和回执只是原目标的证据,不能替换原目标。contractStatus=evidence-ready 只代表客观证据齐全,你仍须按 acceptanceCriteria 逐条完成语义验收;needs-repair 不得忽略,同一原委派最多发起一轮显式返工。GDD 最终是否通过由用户在审批卡上决定,不由你代答。
|
||||
@@ -1,22 +0,0 @@
|
||||
【固定动作顺序,不得跳步】
|
||||
|
||||
1. 本 run 第一轮只调用一次 `agent.goal_contract` 冻结目标合同:outcome 概括用户原话意图,`preferences` 必须传空数组,`acceptanceNodes` 提交 Runtime 指定的固定单节点。这一轮不做任何其它调用。
|
||||
2. 冻结后立即用一次 `agent.delegate` 把任务委派给 `project-planning`,`expectedArtifacts` 写 `game/fast_gdd.md`,`repairOfDelegationId`、`runId`、`continuationOfDelegationId`、`questionsSha256`、`answersSha256` 全传 null。已有委派尚未收束时不要重复委派。
|
||||
3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。
|
||||
4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery。`questionsSha256`、`answersSha256`、`acceptanceCriteria`、`expectedArtifacts` 四个全传 null——Runtime 会从该原 delivery 补齐权威指纹和原委派合同,你不要自己抄。子 Agent 在 continuation 里**再次**以信封退出时,对那条新 delivery 重复同一动作:「仅创建一次」约束的是单条 delivery,不是整条链,澄清预算未用尽时这个循环继续。Runtime 会在委派 task 末尾写明已用轮次与上限,不需要你自己数,也不要替它宣布预算已尽。
|
||||
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着 `sourceAgentId` / `sourceRunId` / `sourceActionId` 三个字段,把它们原样抄成 evidence 的 `{agentId, runId, actionId}`,用一次 `agent.acceptance_update` 一并提交即可——evidence 是按这三个字段整体查回执的,回忆错任何一个都会被判成"缺少持久动作回执"。不要为了取这些字段再去查动作历史。取证完成前审批卡不会出现。
|
||||
6. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里。「同一原委派只能返工一次」约束的是单条 delivery,不是整条链:用户看过新稿再点一次修改,就对那条新 delivery 重复同一动作,这个循环没有次数上限——`repair_depth` 防的是 runaway agent,而每一轮修订都由用户亲手触发,人本身就是循环边界。不要替 Runtime 宣布「这是最后一次修改机会」,也不要因此把多条意见攒到一轮里改完。用户通过后只做一句简短收尾。
|
||||
|
||||
【转达的规则】
|
||||
|
||||
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] 第N轮问的是:{question 原文} | 候选项:{option1.label} / {option2.label} / {option3.label} → 用户答:{原文}`。**问题原文和三个选项标签必须带上**:`{header}` 只写到「第N轮·当前要决定:{主题}」这一层,答案落在选项上;子 Agent 每轮都是全新 run,除了这段正文什么都看不到,只给它主题和答案,「类似B」「B · 沙盒里程碑成长」这类答案就无从解读,它只能把同一件事再问一遍。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字和选项描述,绝不压缩用户答案、问题原文和选项标签。
|
||||
- 策划链路的澄清信封**恰好一题**,不是通用静态委派协议里的 1-3 题:`project-planning` 每轮只提一个主要决定,Runtime 也只接受一题,多于一题会在出卡时被拒。委派 task 里不要写“1-3 个结构化问题”。
|
||||
- 上一条格式里的三个选项标签就是决策卡上的 A、B 和“需要原型验证”,必须原样转述、一个都不能省;B 是用户确认的 `confirmed/user_option`,不能转成默认建议。用户后续自由填写推翻了更早的决定时,你只负责把两轮答案的原文都原样带到,并说明后者更晚;怎么记进决定台账由 `project-planning` 判断,不要替它裁定哪条作废。
|
||||
|
||||
【委派合同的边界】
|
||||
|
||||
委派 `project-planning` 时,acceptanceCriteria 只写产物形状、覆盖范围与红线(例如必须交付 `game/fast_gdd.md`、必须原创、必须只定义一个 MVP 闭环),**不得替用户预先裁定产品取舍**。用户没有指定的玩法规则、数值、关卡量级、美术方向和目标人群,一律留给策划子 Agent 按其 3 轮问询预算决定是提问还是按默认建议填写;不要写“未指定的标注为立项假设”“自行假设后继续”这类指令,那会把问询预算作废。平台事实(自包含 Web、desktop/mobile 双视口、keyboard/touch 双输入、本地 HTTP 预览)由 Runtime 固定注入,属于已定事实,不得要求标为待定、建议或开放项。
|
||||
|
||||
**本轮指令三选一。** 委派任务正文里,除了用户原始意图和已确认答案原文,你只能再写一句“本轮该做什么”,且必须是下面三个之一:**继续澄清**(默认,不附加任何前置条件)、**直接出稿**(仅当用户明确要求跳过问询)、**按意见修订**(仅审批返回修改或退回时)。不要自己描述“什么情况下才该提问”“若缺少会实质改变结果的事实则……”“否则直接提交完整 GDD”——那不在这三项里。提问预算怎么花,由 `project-planning` 按 Runtime 注入的判据决定。
|
||||
|
||||
不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。
|
||||
@@ -1,33 +0,0 @@
|
||||
你是“立项策划 Agent”(`agentId=project-planning`),由 Project Supervisor 通过静态 `agent.delegate` 委派。你的工作是把一句用户需求收敛成可审批的 MVP Fast GDD;你只负责玩法澄清、原型验证建议和最小 GDD,不负责完整游戏构建。
|
||||
|
||||
## 身份与边界
|
||||
|
||||
- 当前 run 固定为 `source=agent-delegate`、`profile=standard`,父 Agent 是 `project-supervisor`。不得伪造、改写或猜测这些 Runtime 身份。
|
||||
- 你不能委派或调度其他 Agent,不能创建 isolated child,不能调用 MCP、命令、进程、预览、画布、素材生成、写入/补丁/删除工具,也不能改变项目版本或审批事实。
|
||||
- 你的原生工具目录只应包含 `file.read`、`file.list` 以及 Runtime 协议控制函数 `update_agent_plan`、`respond_to_user`;`user.input_request` 不属于你的工具目录。若需要用户决定,必须以终态信封首行 `AGC_NEEDS_USER_INPUT_V1` 退出本轮,下一行给出严格 JSON 信封 `{"questions":[{ ... }]}`,交由 Supervisor 转发。`questions` 恰好一个元素;元素字段只能是 `id`、`header`、`question`、`options` 四个,多写任何字段(例如 `answerFormat`)或省掉 `questions` 外壳都会被 Runtime 拒收,整条委派随即作废。`id` 是唯一 snake_case(小写字母开头,只含小写字母、数字、下划线);`header` 是决策卡标题,写成 `第N轮·当前要决定:<主题>`,单行且不超过 60 字符;`question` 是决策卡正文,单行且不超过 400 字符;`options` 恰好 3 个 `{"label": ..., "description": ...}`,依次是 A、B、逐字“需要原型验证”(详见下文决策卡一段),label 单行不超过 60 字符、description 单行不超过 240 字符。不要另起一行写答题说明或把选项复述进 `question`,作答方式由 Runtime 自己呈现。
|
||||
- 只有 Runtime 广告并允许 `plan.submit_gdd` 时才可提交 GDD;不要假设未广告的工具存在,也不要把 GDD、审批或下游构建写进普通文本。
|
||||
|
||||
## 目标与轮次
|
||||
|
||||
- 最多进行 3 轮关键澄清;每轮是新 run、同一 session。你看得到自己的历史,但用户答案以 Supervisor 委派任务中的转述为准,缺失信息不能臆造。
|
||||
- **默认先澄清。** 出稿只有四个触发器,除此之外每轮都先做下面的字段差距检测再决定问不问:①任务正文出现“直接出稿”这四个字;②已完成第 3 轮澄清(任务正文写明的已用轮次已达上限);③剩余空白都能由默认建议覆盖,且不影响首个可玩闭环;④收到 Runtime 的活跃预算或超时提示。任务正文能改变流程的只有第 ① 条——它写的其它说明属于内容,不是出稿触发器。既定事实(用户答案、已确认决定)仍以任务正文为准。
|
||||
- 每轮提问前逐项对照 `plan-submit-gdd-input.v1` 的 `game` 字段做差距检测:用户明确提供的 = `confirmed`;有依据可推断的 = 按下面的默认建议填写并标 `default_pending`;无从判断的 = 空白。提问名额只花在**空白或存疑、且影响首个可玩闭环**的决定上;有默认建议兜底的字段优先用默认建议而不是提问——「有默认」不等于「不能问」,那条默认明显可能是错的、且选错就做不出首个可玩闭环时,它就是一个该问的存疑项。`title`、`oneLiner`、`mvpSystems`、`creatorTips` 由你生成并标 `default_pending`,不作为提问对象;`platformFacts` 禁问。
|
||||
- **默认建议**(一律 `answerSource=default`、`round=0`;只用于缩短对话,不覆盖用户明确输入):`targetUsers.sessionLength` 缺 → 10~20 分钟一局;`artStyle` 缺 → `visualType` 风格化、轮廓清楚,`keywords` 取自已确认的核心行为,`mvpArtBoundary` 写明 MVP 用占位资产、资产可复用;缺成长时 → 1 条成长线和 2~3 个选择;缺探索时 → 1 条主路线加 1 个有意义的岔路;缺构建时 → 高风险输出和稳健防御两种方向。清单之外的字段没有默认值兜底——`genre.fusion`、`targetUsers.coreUsers` / `preferences` / `referenceGames`、`outOfScope` 缺失时都算空白,该不该花一轮问它们由上面的判据决定,不要自己拍一个值填掉就当它已经定了。**`pillars` 与 `coreLoop` 没有默认建议**:它们就是首个可玩闭环本身,空白时属于该问的空白,不得用默认值填掉。
|
||||
- 优先顺序:核心行为与本局目标 → 重玩动力 → 制作边界与 MVP。每轮最多问一个主要决定。**已确认决定关掉的那条轴不得重问。** 任务正文里每条 `[已确认]` 都带着当轮的问题原文和三个选项标签,先照它判断哪些轴已经关闭,本轮的问题必须落在另一条还没关闭的轴上。把已确认答案换个说法再问一遍——例如用户已经选定“自由经营、靠成就和攒钱升级推进”,你又拿“短周期经营目标 vs 沙盒里程碑成长”去问——是白烧一轮预算。所有轴都已关闭时按出稿触发器③直接出稿。
|
||||
- 决策卡的 header 写成“第N轮·当前要决定:<主题>”,最多 60 字符。N 是 Runtime 从委派谱系派生的当前轮号,写错会被 Runtime 拒收:首轮恒为 1;之后每次续跑的任务正文都会写明已用轮次与上限,本轮该用的 N 就是“已用轮次 + 1”。`<主题>` 是这一轮真正要定的那件事本身(例如“塔的构筑方式”“每局变化来源”),一句话说完、不带状态标记——它会原样落进决定台账的 `topic`,也是你下一轮辨认哪些轴已经关掉的唯一线索,写成“关键决定”这类空话等于把它作废。正文只问尚未由平台事实或 MVP 规则排除的真实产品取舍,并说明为什么现在问;每张卡固定提供三个选项:A 是你的推荐方案(label 以 `A ·`、`A:`、`A:` 或 `A-` 开头并写明推荐、好处和代价),B 是形状不同且真实可行的平行备选(label 以 `B ·`、`B:`、`B:` 或 `B-` 开头并写明后果和代价),第三项逐字为“需要原型验证”,description 必须给出 30~90 分钟微型原型、试玩对象、观察信号和通过标准。自由输入按用户原话处理。
|
||||
|
||||
## 低幻觉与 GDD 约束
|
||||
|
||||
- 用户描述玩法类型、机制或“像某款游戏”时,不代表授权复刻该游戏。游戏名称、世界观、角色与单位名、阵营、资源、界面术语和视觉语言必须原创;不得沿用、翻译或近似改写现有游戏的专有名称、Logo、标志性布局与受保护视觉语言,也不得把它们写进 GDD 正文、决定台账或原型验证项。用户提到的相似作品只能作为抽象品类参考,`targetUsers.referenceGames` 同样不得填入受保护名称。你的工具面窄,但内容红线不因此放宽——GDD 是整条产线的上游。
|
||||
- 决定台账记录当前 GDD 的决定快照。澄清阶段的 A、B 或自由填写得到的用户决定标 `confirmed`,选择“需要原型验证”标 `prototype_pending`;未提问、由你按默认建议填写的字段标 `default_pending`、`answerSource=default`、`round=0`。审批阶段的用户修改意见是本轮最高优先级:由该意见新增或改写的决定使用 `answerSource=user_revision`、`round=0`,并按当前意见重新填写 `topic`、`state` 和 `answerSummary`。
|
||||
- 以当前 GDD 为基线,仅修改用户审批意见明确涉及的内容,以及为保持内部一致性所必需同步调整的派生内容。未被意见涉及的内容保持不变;如果意见与过去决定冲突,以最新意见为准。不要把用户未要求的其它方向自行扩展进本轮修订。提交时仍须提供完整 GDD 快照,但完整快照不代表可以任意重写未涉及内容。
|
||||
- `prototypeValidationItems` 是必填字段(没有就传空数组),与 `prototype_pending` 决定**一一对应**:每条 `prototype_pending` 决定必须有一个同 id 的验证项,每个验证项也必须对应一条 `prototype_pending` 决定,最多 3 项。除了用户亲选“需要原型验证”之外,你自己也可以主动标:手感、节奏、可读性、难度曲线这类你没问过、但选错就做不出首个可玩闭环的判断,标 `prototype_pending`(`answerSource=default`、`round=0`)比标 `default_pending` 诚实——那不是一个默认值,是一个没人验证过的假设。每项写清 30~90 分钟微型原型做什么、让谁试玩、观察什么信号、什么算通过。
|
||||
- 不得编造具体游戏的机制、数值、销量、人群规模、团队规模或来源。写 `targetUsers` 时按已确认的类型与核心行为描述典型玩家即可。
|
||||
- 只定义一个完整可玩闭环。MVP 不含多人、商城、服务器、开放世界、赛季、复杂社交、完整剧情或全量内容,除非用户明确改变范围。
|
||||
- GDD 至少覆盖:游戏名称与类型、一句话描述、2~4 条游戏支柱、核心循环、目标用户、美术方向、3~6 个最小 MVP 系统、先做/暂缓/验证/扩展条件、决定状态和审批请求。不要把 Runtime 注入的身份、时间、指纹、审批 receipt 或平台事实当作 Provider 输入字段。
|
||||
- 平台事实由 Runtime 固定注入为自包含 Web、desktop/mobile 双视口、keyboard/touch 双输入、本地 HTTP 预览;不得修改、删减或向用户询问。
|
||||
|
||||
## 输出纪律
|
||||
|
||||
- 澄清模式只返回 `AGC_NEEDS_USER_INPUT_V1` 终态信封,不再调用其他函数;成稿模式只在 `plan.submit_gdd` 被广告时调用它并等待 Runtime 校验;收到 revise/reject observation 后按同一 GDD 谱系修订,收到 approve 后只做简短收尾。
|
||||
- 必须直接调用当前请求广告的原生函数;不要输出 JSON、代码围栏或内部思考过程,不要假装已经写入文件、完成审批或启动构建。
|
||||
@@ -3635,8 +3635,6 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
request_slot: "direct-chat".to_string(),
|
||||
web_search_enabled: config.llm.web_search_enabled,
|
||||
allow_idle_context_compaction: false,
|
||||
// direct-codex 不是立项策划链路,没有 planning session 可绑定。
|
||||
planning_session_binding: None,
|
||||
};
|
||||
let api_kind =
|
||||
parse_game_creator_llm_api_kind(&config.llm.api_kind).map_err(|error| error.to_string())?;
|
||||
@@ -3844,8 +3842,6 @@ pub(crate) async fn direct_game_creator_home_codex_chat(
|
||||
request_slot: "direct-home-chat".to_string(),
|
||||
web_search_enabled: config.llm.web_search_enabled,
|
||||
allow_idle_context_compaction: false,
|
||||
// 直连 Codex 的首页对话不属于任何立项策划 session。
|
||||
planning_session_binding: None,
|
||||
};
|
||||
let api_kind =
|
||||
parse_game_creator_llm_api_kind(&config.llm.api_kind).map_err(|error| error.to_string())?;
|
||||
@@ -4275,7 +4271,6 @@ mod tests {
|
||||
request_slot: "slot-1".to_string(),
|
||||
web_search_enabled: false,
|
||||
allow_idle_context_compaction: false,
|
||||
planning_session_binding: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -337,9 +337,6 @@ pub(crate) fn agent_role_memory_relative_path_for_task(task_id: &str) -> Result<
|
||||
if task_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return Ok(GAME_CREATOR_PROJECT_SUPERVISOR_MEMORY_PATH.to_string());
|
||||
}
|
||||
if task_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return Ok(GAME_CREATOR_PROJECT_PLANNING_MEMORY_PATH.to_string());
|
||||
}
|
||||
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
for role in group.roles {
|
||||
if role.task_id == task_id {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,6 @@ pub(in crate::agent) use run_status_observation::*;
|
||||
pub(in crate::agent) use structured_plan::*;
|
||||
pub(in crate::agent) use tool_plan_protocol::*;
|
||||
|
||||
pub(crate) use crate::agent::runtime_protocol::plan_gdd_completion_blocker_at_locked;
|
||||
#[cfg(test)]
|
||||
pub(crate) use action_audit::agent_runtime_action_receipt_public_safe_detail_for_test;
|
||||
#[cfg(test)]
|
||||
@@ -74,7 +73,6 @@ pub(crate) use context_compaction::compact_game_creator_agent_runtime_session_at
|
||||
pub(crate) use parallel_ledger::{
|
||||
agent_runtime_confirmation_path_component, agent_runtime_parallel_read_batch_len,
|
||||
agent_runtime_tool_allowed_for_agent, agent_runtime_tool_is_parallel_safe_read,
|
||||
agent_runtime_tool_rejected_by_agent_identity,
|
||||
game_creator_agent_runtime_parallel_read_batch_path,
|
||||
game_creator_agent_runtime_pending_tool_action_path,
|
||||
game_creator_agent_runtime_provider_action_batch_path,
|
||||
@@ -116,7 +114,6 @@ pub(crate) use project_gates::{
|
||||
};
|
||||
pub(crate) use provider_action_batch::{
|
||||
prepare_game_creator_agent_runtime_provider_action_batch,
|
||||
prepare_game_creator_agent_runtime_provider_action_batch_with_planning_binding,
|
||||
update_game_creator_agent_runtime_provider_batch_member, AgentRuntimePendingToolAction,
|
||||
AgentRuntimeProviderActionBatch,
|
||||
};
|
||||
@@ -147,9 +144,6 @@ pub(crate) use tool_plan_protocol::parse_game_creator_agent_tool_plan_response;
|
||||
pub(crate) use tool_policy_snapshot::{
|
||||
agent_runtime_acceptance_evidence_tools,
|
||||
agent_runtime_autonomous_design_foundation_command_is_allowed, agent_runtime_executable_tools,
|
||||
agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools,
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage,
|
||||
agent_runtime_tool_policy_snapshot_for_run_at, plan_root_supervisor_stage_at,
|
||||
plan_root_supervisor_stage_at_locked, PlanRootSupervisorStage,
|
||||
AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS,
|
||||
agent_runtime_native_executable_tools, agent_runtime_tool_policy_snapshot_for_run_at,
|
||||
AGENT_RUNTIME_CANVAS_ASSET_KINDS,
|
||||
};
|
||||
|
||||
@@ -38,29 +38,6 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let tool = action.tool.trim();
|
||||
let relaxed_autonomous = autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false);
|
||||
if tool == PLAN_SUBMIT_GDD_TOOL && agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: "plan.submit_gdd 仅允许 project-planning Agent".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& !matches!(tool, "file.read" | "file.list")
|
||||
{
|
||||
// `plan.submit_gdd` is intentionally handled by the planning submit
|
||||
// branch in the Runtime main loop. If it ever reaches the generic
|
||||
// executor (including recovery or a stale pending record), fail
|
||||
// closed instead of treating the durable mutation as an ordinary
|
||||
// command action.
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "rejected".to_string(),
|
||||
summary: "当前 Agent 身份不允许执行该工具".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let action_fingerprint = pending_action
|
||||
.map(|pending| {
|
||||
agent_runtime_pending_tool_action_fingerprint(
|
||||
|
||||
@@ -25,11 +25,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
root,
|
||||
"runtime.context_compaction.build",
|
||||
)?;
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// Advance the immutable Provider-usage projection before any
|
||||
// request/source bytes are rebuilt from the plan session.
|
||||
fold_plan_provider_usage_before_new_request_at_locked(root, Some((agent_id, run_id)))?;
|
||||
}
|
||||
let source = build_game_creator_agent_runtime_context_compaction_source(
|
||||
root,
|
||||
agent_id,
|
||||
@@ -67,18 +62,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
let config_path = format!("agentLlm.{template_agent_id}");
|
||||
let mut request =
|
||||
build_game_creator_agent_runtime_context_compaction_request(&source, &llm)?;
|
||||
let planning_agent = agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID;
|
||||
if planning_agent {
|
||||
if allow_idle_context_compaction {
|
||||
return Err(
|
||||
"project-planning 不支持脱离 active run 的 idle context compaction".to_string(),
|
||||
);
|
||||
}
|
||||
let wire_bytes =
|
||||
capture_plan_provider_structured_injections_at(root, session_id, observations)?;
|
||||
let message = render_plan_provider_structured_injections_message(&wire_bytes)?;
|
||||
request.messages.insert(1, LlmMessage::user(message));
|
||||
}
|
||||
let estimated_request_tokens = estimate_game_creator_llm_request_tokens(&request)?;
|
||||
validate_game_creator_llm_request_context_budget(
|
||||
&llm,
|
||||
@@ -106,22 +89,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
applied_steer_cursor,
|
||||
)?
|
||||
};
|
||||
let snapshot = if planning_agent {
|
||||
let request_context_fingerprint =
|
||||
game_creator_agent_runtime_plan_provider_request_context_fingerprint(
|
||||
&llm, &request,
|
||||
)?;
|
||||
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
|
||||
let binding = capture_plan_provider_session_binding_for_snapshot(
|
||||
root,
|
||||
&runtime,
|
||||
&snapshot,
|
||||
&request_context_fingerprint,
|
||||
)?;
|
||||
snapshot.with_planning_session_binding(Some(binding))
|
||||
} else {
|
||||
snapshot
|
||||
};
|
||||
(snapshot, source, llm, config_path, request)
|
||||
};
|
||||
let handoff_identity =
|
||||
@@ -188,7 +155,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
|
||||
root,
|
||||
&base_request_id,
|
||||
snapshot.planning_session_binding.is_some(),
|
||||
)
|
||||
.map(|value| value.0)
|
||||
.unwrap_or(base_request_id);
|
||||
@@ -212,7 +178,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
|
||||
root,
|
||||
&base_request_id,
|
||||
snapshot.planning_session_binding.is_some(),
|
||||
)
|
||||
.map(|value| value.0)
|
||||
.unwrap_or(base_request_id);
|
||||
@@ -246,7 +211,6 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
||||
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
|
||||
root,
|
||||
&base_request_id,
|
||||
snapshot.planning_session_binding.is_some(),
|
||||
)
|
||||
.map(|value| value.0)
|
||||
.unwrap_or(base_request_id);
|
||||
|
||||
@@ -109,7 +109,6 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
|
||||
"agent.schedule_ready" => Some("agent.schedule_ready"),
|
||||
"agent.action_history" => Some("agent.audit"),
|
||||
"agent.run_status" => Some("agent.run_status"),
|
||||
PLAN_SUBMIT_GDD_TOOL => Some(PLAN_SUBMIT_GDD_TOOL),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -119,15 +118,7 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
|
||||
/// id (for example `project.search` and `file.read`); policy lookup alone must
|
||||
/// not turn that aliasing into an identity escalation for a restricted Agent.
|
||||
pub(crate) fn agent_runtime_tool_allowed_for_agent(agent_id: &str, tool: &str) -> bool {
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return matches!(
|
||||
tool.trim(),
|
||||
"file.read" | "file.list" | PLAN_SUBMIT_GDD_TOOL
|
||||
);
|
||||
}
|
||||
if tool.trim() == PLAN_SUBMIT_GDD_TOOL {
|
||||
return false;
|
||||
}
|
||||
let _ = agent_id;
|
||||
if tool.trim() == GAME_CREATOR_USER_INPUT_REQUEST_TOOL {
|
||||
// `user.input_request` is a protocol control handled by the main
|
||||
// loop, not by the command-id policy map. It remains available to
|
||||
@@ -137,95 +128,11 @@ pub(crate) fn agent_runtime_tool_allowed_for_agent(agent_id: &str, tool: &str) -
|
||||
game_creator_agent_runtime_tool_command_id(tool.trim()).is_some()
|
||||
}
|
||||
|
||||
/// 身份层面的**显式**拒绝:该 Agent 身份带 exact allowlist,且工具不在其中。
|
||||
///
|
||||
/// **未知工具名不属于本判据。** `agent_runtime_tool_allowed_for_agent` 对普通
|
||||
/// Agent 退化成「这个工具名是否已知」,用它做身份门会把「模型编了个不存在的
|
||||
/// 工具」这种普通协议错误误判成身份违规。协议错误的既有语义是:走到执行层产出
|
||||
/// 一条 `rejected` observation,run 继续,由下一轮 tool-plan 收束;升级成身份
|
||||
/// 拒绝会让整个 run 进 needs-reconciliation 而**不再发出 follow-up 请求**。
|
||||
///
|
||||
/// 因此凡是「命中即中断 run 或整体拒绝动作」的调用点都必须用本判据,不能直接
|
||||
/// 用 `agent_runtime_tool_allowed_for_agent`。
|
||||
pub(crate) fn agent_runtime_tool_rejected_by_agent_identity(agent_id: &str, tool: &str) -> bool {
|
||||
agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& !agent_runtime_tool_allowed_for_agent(agent_id, tool)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod identity_tests {
|
||||
use super::*;
|
||||
|
||||
/// 普通 Agent 编出来的未知工具名是**协议错误**,不是身份违规。
|
||||
///
|
||||
/// 判成身份违规会让 main_loop 把整个 run 打进 needs-reconciliation、不再发出
|
||||
/// follow-up tool-plan——曾导致 `background_agent_runtime_persists_receipts_for_rejected_actions`
|
||||
/// 在等待第二次 Provider 请求时超时。
|
||||
#[test]
|
||||
fn unknown_tool_on_ordinary_agent_is_not_an_identity_rejection() {
|
||||
assert!(!agent_runtime_tool_rejected_by_agent_identity(
|
||||
"design-director",
|
||||
"runtime.unknown"
|
||||
));
|
||||
assert!(!agent_runtime_tool_rejected_by_agent_identity(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"runtime.unknown"
|
||||
));
|
||||
assert!(!agent_runtime_tool_rejected_by_agent_identity(
|
||||
"design-director",
|
||||
"file.read"
|
||||
));
|
||||
}
|
||||
|
||||
/// planning 身份仍是 exact allowlist:未知工具与越权工具都算身份拒绝。
|
||||
#[test]
|
||||
fn planning_identity_still_rejects_unknown_and_out_of_scope_tools() {
|
||||
assert!(agent_runtime_tool_rejected_by_agent_identity(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"runtime.unknown"
|
||||
));
|
||||
assert!(agent_runtime_tool_rejected_by_agent_identity(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"file.write"
|
||||
));
|
||||
assert!(!agent_runtime_tool_rejected_by_agent_identity(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"file.read"
|
||||
));
|
||||
assert!(!agent_runtime_tool_rejected_by_agent_identity(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
PLAN_SUBMIT_GDD_TOOL
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_identity_does_not_inherit_project_search_alias() {
|
||||
assert!(agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"file.read"
|
||||
));
|
||||
assert!(agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"file.list"
|
||||
));
|
||||
assert!(!agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"project.search"
|
||||
));
|
||||
assert!(agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
PLAN_SUBMIT_GDD_TOOL
|
||||
));
|
||||
assert!(!agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
PLAN_SUBMIT_GDD_TOOL
|
||||
));
|
||||
assert!(agent_runtime_tool_allowed_for_agent(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"project.search"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_asset_library_uses_the_existing_read_only_asset_permission() {
|
||||
assert_eq!(
|
||||
|
||||
-27
@@ -336,33 +336,6 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&pending.run_profile);
|
||||
if !relaxed_autonomous {
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
match pending.planning_session_binding.as_ref() {
|
||||
Some(binding) => {
|
||||
validate_plan_provider_session_binding(binding)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| binding.agent_id != pending.agent_id
|
||||
|| binding.task_id != pending.task_id
|
||||
|| binding.session_id != pending.session_id
|
||||
|| binding.run_id != pending.run_id
|
||||
|| binding.source != pending.source
|
||||
|| binding.run_profile != pending.run_profile
|
||||
|| binding.run_profile_binding_fingerprint
|
||||
!= pending.run_profile_binding_fingerprint
|
||||
|| binding.applied_steer_cursor != pending.planned_steer_cursor
|
||||
{
|
||||
return Err(
|
||||
"planning submit standalone pending 与 frozen binding 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None if pending.provider_batch_plan_update.is_none() => {}
|
||||
None => {
|
||||
return Err(
|
||||
"非 planning standalone pending 不能携带 Provider batch planUpdate".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
|
||||
if pending.verification_gate_before.project_id
|
||||
!= game_creator_agent_runtime_context_project_id(root)?
|
||||
|
||||
@@ -1088,7 +1088,6 @@ pub(in crate::agent) fn agent_runtime_non_verification_completion_blocker_at_loc
|
||||
run_id: &str,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
provider_retry_completion_blocker_at_locked(root, agent_id, run_id)
|
||||
.or_else(|| plan_gdd_completion_blocker_at_locked(root, agent_id, run_id))
|
||||
.or_else(|| provider_action_batch_completion_blocker_at_locked(root, agent_id, run_id))
|
||||
.or_else(|| {
|
||||
supervisor_collaboration_policy_completion_blocker_at_locked(root, agent_id, run_id)
|
||||
|
||||
+6
-309
@@ -14,18 +14,6 @@ pub(crate) struct AgentRuntimePendingToolAction {
|
||||
pub(crate) run_profile: String,
|
||||
#[serde(default)]
|
||||
pub(crate) run_profile_binding_fingerprint: String,
|
||||
/// Planning submit actions carry the exact source-session snapshot that
|
||||
/// was captured before the Provider response was accepted. Other tools
|
||||
/// leave this field absent and retain the v1-v3 batch semantics.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) planning_session_binding: Option<PlanProviderSessionBindingV1>,
|
||||
/// The v4 planning batch identity covers the complete Provider plan,
|
||||
/// including an optional structured plan update. Persist that one
|
||||
/// batch-only field on the standalone submit anchor as recovery material;
|
||||
/// otherwise a surviving pending action cannot reproduce the original
|
||||
/// batch ID after the batch sidecar is lost.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) provider_batch_plan_update: Option<AgentRuntimePlanUpdate>,
|
||||
pub(crate) task: String,
|
||||
#[serde(default)]
|
||||
pub(crate) goal_id: Option<String>,
|
||||
@@ -90,8 +78,6 @@ pub(in crate::agent) struct AgentRuntimeParallelReadBatch {
|
||||
pub(crate) struct AgentRuntimeProviderActionBatch {
|
||||
pub(crate) schema_version: String,
|
||||
pub(crate) batch_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) provider_request_id: Option<String>,
|
||||
pub(crate) project_id: String,
|
||||
pub(crate) agent_id: String,
|
||||
pub(crate) task_id: String,
|
||||
@@ -102,8 +88,6 @@ pub(crate) struct AgentRuntimeProviderActionBatch {
|
||||
pub(crate) run_profile: String,
|
||||
#[serde(default)]
|
||||
pub(crate) run_profile_binding_fingerprint: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) planning_session_binding: Option<PlanProviderSessionBindingV1>,
|
||||
pub(crate) loop_iteration: u32,
|
||||
pub(crate) planned_steer_cursor: u64,
|
||||
pub(crate) status: String,
|
||||
@@ -123,8 +107,6 @@ pub(crate) struct AgentRuntimeProviderActionBatch {
|
||||
struct AgentRuntimeProviderActionBatchWire {
|
||||
schema_version: String,
|
||||
batch_id: String,
|
||||
#[serde(default)]
|
||||
provider_request_id: Option<String>,
|
||||
project_id: String,
|
||||
agent_id: String,
|
||||
task_id: String,
|
||||
@@ -135,8 +117,6 @@ struct AgentRuntimeProviderActionBatchWire {
|
||||
run_profile: String,
|
||||
#[serde(default)]
|
||||
run_profile_binding_fingerprint: String,
|
||||
#[serde(default)]
|
||||
planning_session_binding: Option<PlanProviderSessionBindingV1>,
|
||||
loop_iteration: u32,
|
||||
planned_steer_cursor: u64,
|
||||
status: String,
|
||||
@@ -160,7 +140,6 @@ impl<'de> Deserialize<'de> for AgentRuntimeProviderActionBatch {
|
||||
let batch = Self {
|
||||
schema_version: wire.schema_version,
|
||||
batch_id: wire.batch_id,
|
||||
provider_request_id: wire.provider_request_id,
|
||||
project_id: wire.project_id,
|
||||
agent_id: wire.agent_id,
|
||||
task_id: wire.task_id,
|
||||
@@ -169,7 +148,6 @@ impl<'de> Deserialize<'de> for AgentRuntimeProviderActionBatch {
|
||||
source: wire.source,
|
||||
run_profile: wire.run_profile,
|
||||
run_profile_binding_fingerprint: wire.run_profile_binding_fingerprint,
|
||||
planning_session_binding: wire.planning_session_binding,
|
||||
loop_iteration: wire.loop_iteration,
|
||||
planned_steer_cursor: wire.planned_steer_cursor,
|
||||
status: wire.status,
|
||||
@@ -217,7 +195,7 @@ impl AgentRuntimePendingToolAction {
|
||||
pub(in crate::agent) fn tool_plan(&self) -> AgentRuntimeToolPlan {
|
||||
AgentRuntimeToolPlan {
|
||||
thinking_summary: self.thinking_summary.clone(),
|
||||
plan_update: self.provider_batch_plan_update.clone(),
|
||||
plan_update: None,
|
||||
plan: self.plan.clone(),
|
||||
actions: Vec::new(),
|
||||
response: self.fallback_response.clone(),
|
||||
@@ -275,8 +253,6 @@ pub(in crate::agent) fn build_game_creator_agent_runtime_pending_tool_action(
|
||||
source: runtime.source.clone(),
|
||||
run_profile: runtime.run_profile.clone(),
|
||||
run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(),
|
||||
planning_session_binding: None,
|
||||
provider_batch_plan_update: None,
|
||||
task,
|
||||
goal_id: runtime.goal_id.clone(),
|
||||
goal_revision: runtime.goal_revision,
|
||||
@@ -312,110 +288,13 @@ pub(in crate::agent) fn build_game_creator_agent_runtime_pending_tool_action(
|
||||
})
|
||||
}
|
||||
|
||||
/// `plan.submit_gdd` is a transactional planning action rather than an
|
||||
/// ordinary provider action. It must be represented by one (and only one)
|
||||
/// durable batch member so that the main loop can establish the action
|
||||
/// identity before handing control to the planning submit handler.
|
||||
///
|
||||
/// Keep this check at the batch boundary as a second line of defence behind
|
||||
/// the native-tool parser. In particular, a text/JSON tool-plan or a stale
|
||||
/// caller must not be able to smuggle a submit action through the historical
|
||||
/// `< 2 actions => NotNeeded` fast path.
|
||||
fn validate_plan_submit_gdd_batch_shape_for_identity(
|
||||
agent_id: &str,
|
||||
source: &str,
|
||||
run_profile: &str,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
) -> Result<bool, String> {
|
||||
let submit_count = plan
|
||||
.actions
|
||||
.iter()
|
||||
.filter(|action| action.tool.trim() == PLAN_SUBMIT_GDD_TOOL)
|
||||
.count();
|
||||
if submit_count == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return Err("plan.submit_gdd 只能由 project-planning Agent 调用".to_string());
|
||||
}
|
||||
// The planning child is created through the ordinary delegate path. Keep
|
||||
// the source/profile check here even though the run-identity binder also
|
||||
// checks it: this prevents a forged/stale RuntimeState from turning the
|
||||
// sole-action exception into a generic batch.
|
||||
if source.trim() != "agent-delegate" || run_profile.trim() != AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
{
|
||||
return Err(
|
||||
"plan.submit_gdd 的 Runtime 身份必须是 source=agent-delegate、runProfile=standard"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if submit_count > 1 {
|
||||
return Err("plan.submit_gdd 在同一 Provider 响应中只能出现一次".to_string());
|
||||
}
|
||||
if plan.actions.len() != 1 {
|
||||
return Err("plan.submit_gdd 必须是 Provider 响应中的唯一 action".to_string());
|
||||
}
|
||||
if !plan.response.trim().is_empty() {
|
||||
return Err("plan.submit_gdd 不得与 respond_to_user 混批".to_string());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn validate_plan_submit_gdd_batch_shape(
|
||||
runtime: &AgentRuntimeState,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
) -> Result<bool, String> {
|
||||
validate_plan_submit_gdd_batch_shape_for_identity(
|
||||
&runtime.agent_id,
|
||||
&runtime.source,
|
||||
&runtime.run_profile,
|
||||
plan,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return whether a persisted v4 provider batch is the exact planning submit
|
||||
/// shape that is allowed to contain one action. The provider-batch ledger
|
||||
/// uses this narrow predicate when applying its normal two-action minimum;
|
||||
/// all non-plan batches retain the historical minimum unchanged.
|
||||
pub(in crate::agent) fn is_plan_submit_gdd_provider_action_batch(
|
||||
batch: &AgentRuntimeProviderActionBatch,
|
||||
) -> bool {
|
||||
batch.schema_version == AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
&& batch.agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& batch.source.trim() == "agent-delegate"
|
||||
&& batch.run_profile.trim() == AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
&& batch.collaboration_contract.is_none()
|
||||
&& batch.actions.len() == 1
|
||||
&& batch.plan.actions.len() == 1
|
||||
&& batch.plan.actions[0].tool.trim() == PLAN_SUBMIT_GDD_TOOL
|
||||
&& batch.actions[0].action.tool.trim() == PLAN_SUBMIT_GDD_TOOL
|
||||
&& batch.plan.response.trim().is_empty()
|
||||
&& batch.actions[0].action == batch.plan.actions[0]
|
||||
&& batch.planning_session_binding.is_some()
|
||||
&& batch.provider_request_id.as_deref()
|
||||
== batch
|
||||
.planning_session_binding
|
||||
.as_ref()
|
||||
.map(|binding| binding.provider_request_id.as_str())
|
||||
&& batch.actions[0].planning_session_binding == batch.planning_session_binding
|
||||
}
|
||||
|
||||
fn provider_action_batch_is_not_needed(
|
||||
action_count: usize,
|
||||
force_collaboration_batch: bool,
|
||||
is_plan_submit: bool,
|
||||
) -> bool {
|
||||
action_count < 2 && !force_collaboration_batch && !is_plan_submit
|
||||
action_count < 2 && !force_collaboration_batch
|
||||
}
|
||||
|
||||
/// Backwards-compatible entry point for the historical provider-batch callers.
|
||||
///
|
||||
/// Planning submit batches now need the frozen session binding captured while
|
||||
/// building the provider request. Callers that do not build a planning
|
||||
/// request (including the older test/support helpers) retain the old API and
|
||||
/// therefore pass no binding; the planning path uses the `_with_planning_binding`
|
||||
/// variant below.
|
||||
pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
@@ -425,34 +304,6 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
project_revision_before: &AgentRuntimeProjectRevision,
|
||||
planned_repository_context_fingerprint: &str,
|
||||
) -> Result<AgentRuntimeProviderActionBatchPreparation, String> {
|
||||
prepare_game_creator_agent_runtime_provider_action_batch_with_planning_binding(
|
||||
root,
|
||||
runtime,
|
||||
task,
|
||||
plan,
|
||||
observations,
|
||||
project_revision_before,
|
||||
planned_repository_context_fingerprint,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_with_planning_binding(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
task: &str,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
project_revision_before: &AgentRuntimeProjectRevision,
|
||||
planned_repository_context_fingerprint: &str,
|
||||
captured_planning_session_binding: Option<&PlanProviderSessionBindingV1>,
|
||||
) -> Result<AgentRuntimeProviderActionBatchPreparation, String> {
|
||||
// Validate against the complete provider plan before truncating the
|
||||
// historical action budget. Otherwise a mixed submit batch could hide a
|
||||
// `plan.submit_gdd` action beyond the truncation boundary and reach the
|
||||
// generic executor without a durable identity.
|
||||
let is_plan_submit = validate_plan_submit_gdd_batch_shape(runtime, plan)?;
|
||||
let mut batch_plan = plan.clone();
|
||||
batch_plan
|
||||
.actions
|
||||
@@ -554,7 +405,6 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
if provider_action_batch_is_not_needed(
|
||||
batch_plan.actions.len(),
|
||||
collaboration_preflight.force_durable_batch,
|
||||
is_plan_submit,
|
||||
) {
|
||||
return Ok(AgentRuntimeProviderActionBatchPreparation::NotNeeded);
|
||||
}
|
||||
@@ -577,23 +427,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
|
||||
None,
|
||||
)?;
|
||||
if action.tool.trim() == PLAN_SUBMIT_GDD_TOOL {
|
||||
pending.planning_session_binding = captured_planning_session_binding.cloned();
|
||||
pending.provider_batch_plan_update = batch_plan.plan_update.clone();
|
||||
if pending.planning_session_binding.is_none() {
|
||||
return Err(
|
||||
"planning submit action 缺少 Provider 请求前捕获的 session binding".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(action.tool.trim());
|
||||
let identity_block =
|
||||
agent_runtime_tool_rejected_by_agent_identity(&runtime.agent_id, action.tool.trim())
|
||||
.then(|| {
|
||||
AgentRuntimeToolPolicyBlock::Denied(
|
||||
"当前 Agent 身份不允许执行该原始工具".to_string(),
|
||||
)
|
||||
});
|
||||
let identity_block: Option<AgentRuntimeToolPolicyBlock> = None;
|
||||
let art_director_canvas_only_block =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(
|
||||
&runtime.agent_id,
|
||||
@@ -678,46 +513,7 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
} else {
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY
|
||||
};
|
||||
let planning_session_binding = if is_plan_submit {
|
||||
let binding = captured_planning_session_binding
|
||||
.or_else(|| {
|
||||
actions
|
||||
.first()
|
||||
.and_then(|pending| pending.planning_session_binding.as_ref())
|
||||
})
|
||||
.ok_or_else(|| "planning submit batch 缺少 frozen session binding".to_string())?;
|
||||
validate_plan_provider_session_binding_current_at(root, binding)?;
|
||||
if let Some(pending_binding) = actions
|
||||
.first()
|
||||
.and_then(|pending| pending.planning_session_binding.as_ref())
|
||||
{
|
||||
if pending_binding != binding {
|
||||
return Err(
|
||||
"planning submit pending 与 captured session binding 不一致".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(binding.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let batch_id = if let Some(binding) = planning_session_binding.as_ref() {
|
||||
agent_runtime_plan_provider_action_batch_id(
|
||||
&project_id,
|
||||
&runtime.agent_id,
|
||||
&runtime.task_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
runtime.loop_iteration,
|
||||
runtime.applied_steer_cursor,
|
||||
&batch_plan,
|
||||
project_revision_before,
|
||||
planned_repository_context_fingerprint,
|
||||
&actions,
|
||||
binding,
|
||||
)?
|
||||
} else {
|
||||
agent_runtime_provider_action_batch_id(
|
||||
let batch_id = agent_runtime_provider_action_batch_id(
|
||||
&project_id,
|
||||
&runtime.agent_id,
|
||||
&runtime.task_id,
|
||||
@@ -730,19 +526,11 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
planned_repository_context_fingerprint,
|
||||
&actions,
|
||||
collaboration_preflight.contract.as_ref(),
|
||||
)?
|
||||
};
|
||||
)?;
|
||||
let now = unix_timestamp();
|
||||
let batch = AgentRuntimeProviderActionBatch {
|
||||
schema_version: if is_plan_submit {
|
||||
AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION.to_string()
|
||||
} else {
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION.to_string()
|
||||
},
|
||||
schema_version: AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION.to_string(),
|
||||
batch_id,
|
||||
provider_request_id: planning_session_binding
|
||||
.as_ref()
|
||||
.map(|binding| binding.provider_request_id.clone()),
|
||||
project_id,
|
||||
agent_id: runtime.agent_id.clone(),
|
||||
task_id: runtime.task_id.clone(),
|
||||
@@ -751,7 +539,6 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
source: runtime.source.clone(),
|
||||
run_profile: runtime.run_profile.clone(),
|
||||
run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(),
|
||||
planning_session_binding,
|
||||
loop_iteration: runtime.loop_iteration,
|
||||
planned_steer_cursor: runtime.applied_steer_cursor,
|
||||
status: status.to_string(),
|
||||
@@ -1228,93 +1015,3 @@ pub(in crate::agent) fn update_game_creator_agent_runtime_provider_batch_paralle
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_submit_batch_shape_tests {
|
||||
use super::*;
|
||||
|
||||
fn action(tool: &str) -> AgentRuntimeToolAction {
|
||||
AgentRuntimeToolAction {
|
||||
tool: tool.to_string(),
|
||||
reason: Some("测试动作".to_string()),
|
||||
input: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan(actions: Vec<AgentRuntimeToolAction>, response: &str) -> AgentRuntimeToolPlan {
|
||||
AgentRuntimeToolPlan {
|
||||
thinking_summary: "测试 plan.submit_gdd 批次形状".to_string(),
|
||||
plan_update: None,
|
||||
plan: Vec::new(),
|
||||
actions,
|
||||
response: response.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(plan: &AgentRuntimeToolPlan) -> Result<bool, String> {
|
||||
validate_plan_submit_gdd_batch_shape_for_identity(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"agent-delegate",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
plan,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_submit_is_the_only_durable_action_and_allows_plan_control() {
|
||||
let mut submit = plan(vec![action(PLAN_SUBMIT_GDD_TOOL)], "");
|
||||
assert_eq!(validate(&submit), Ok(true));
|
||||
|
||||
submit.plan_update = Some(AgentRuntimePlanUpdate {
|
||||
explanation: "同步计划进度".to_string(),
|
||||
steps: Vec::new(),
|
||||
});
|
||||
assert_eq!(validate(&submit), Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_submit_rejects_mixed_or_duplicate_actions() {
|
||||
let mixed = plan(vec![action(PLAN_SUBMIT_GDD_TOOL), action("file.read")], "");
|
||||
let mixed_error = validate(&mixed).expect_err("submit + file.read must fail closed");
|
||||
assert!(mixed_error.contains("唯一 action"), "{mixed_error}");
|
||||
|
||||
let duplicate = plan(
|
||||
vec![action(PLAN_SUBMIT_GDD_TOOL), action(PLAN_SUBMIT_GDD_TOOL)],
|
||||
"",
|
||||
);
|
||||
let duplicate_error =
|
||||
validate(&duplicate).expect_err("duplicate submit actions must fail closed");
|
||||
assert!(
|
||||
duplicate_error.contains("只能出现一次"),
|
||||
"{duplicate_error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_submit_rejects_final_response_and_wrong_identity() {
|
||||
let with_response = plan(vec![action(PLAN_SUBMIT_GDD_TOOL)], "不能同时回复");
|
||||
let response_error =
|
||||
validate(&with_response).expect_err("submit + respond_to_user must fail closed");
|
||||
assert!(
|
||||
response_error.contains("respond_to_user"),
|
||||
"{response_error}"
|
||||
);
|
||||
|
||||
let wrong_agent = validate_plan_submit_gdd_batch_shape_for_identity(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"project-supervisor-plan",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
&plan(vec![action(PLAN_SUBMIT_GDD_TOOL)], ""),
|
||||
)
|
||||
.expect_err("non-planning identity must not receive submit exception");
|
||||
assert!(wrong_agent.contains("project-planning"), "{wrong_agent}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_single_action_keeps_not_needed_eligibility() {
|
||||
assert_eq!(validate(&plan(vec![action("file.read")], "")), Ok(false));
|
||||
assert!(provider_action_batch_is_not_needed(1, false, false));
|
||||
assert!(!provider_action_batch_is_not_needed(1, false, true));
|
||||
assert!(!provider_action_batch_is_not_needed(1, true, false));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-143
@@ -110,48 +110,6 @@ pub(in crate::agent) fn agent_runtime_provider_action_batch_id(
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(in crate::agent) fn agent_runtime_plan_provider_action_batch_id(
|
||||
project_id: &str,
|
||||
agent_id: &str,
|
||||
task_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
loop_iteration: u32,
|
||||
planned_steer_cursor: u64,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
project_revision_before: &AgentRuntimeProjectRevision,
|
||||
planned_repository_context_fingerprint: &str,
|
||||
actions: &[AgentRuntimePendingToolAction],
|
||||
planning_session_binding: &PlanProviderSessionBindingV1,
|
||||
) -> Result<String, String> {
|
||||
let action_ids = actions
|
||||
.iter()
|
||||
.map(|pending| pending.action_id.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let identity = serde_json::to_vec(&serde_json::json!({
|
||||
"schemaVersion": AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION,
|
||||
"projectId": project_id,
|
||||
"agentId": agent_id,
|
||||
"taskId": task_id,
|
||||
"sessionId": session_id,
|
||||
"runId": run_id,
|
||||
"loopIteration": loop_iteration,
|
||||
"plannedSteerCursor": planned_steer_cursor,
|
||||
"plan": plan,
|
||||
"projectRevisionBefore": project_revision_before,
|
||||
"plannedRepositoryContextFingerprint": planned_repository_context_fingerprint,
|
||||
"actionIds": action_ids,
|
||||
"planningSessionBinding": planning_session_binding,
|
||||
}))
|
||||
.map_err(|error| format!("序列化 Provider action 批次 v4 身份失败:{error}"))?;
|
||||
let fingerprint = format!("{:x}", Sha256::digest(identity));
|
||||
Ok(format!(
|
||||
"provider-action-v4-{}",
|
||||
fingerprint.chars().take(32).collect::<String>()
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batch(
|
||||
root: &Path,
|
||||
batch: &AgentRuntimeProviderActionBatch,
|
||||
@@ -168,8 +126,7 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
) -> Result<(), String> {
|
||||
if !matches!(
|
||||
batch.schema_version.as_str(),
|
||||
AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
| AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
| AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION
|
||||
| AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION
|
||||
) {
|
||||
@@ -206,48 +163,8 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
batch.status
|
||||
));
|
||||
}
|
||||
// `plan.submit_gdd` is intentionally a sole-action durable batch. It is
|
||||
// the only non-collaboration batch allowed to bypass the historical
|
||||
// two-action minimum; keep the exception tied to the complete identity
|
||||
// predicate so a forged one-action batch cannot widen the normal path.
|
||||
let plan_schema =
|
||||
batch.schema_version == AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION;
|
||||
let plan_submit_batch = is_plan_submit_gdd_provider_action_batch(batch);
|
||||
if plan_schema {
|
||||
if !plan_submit_batch {
|
||||
return Err(
|
||||
"planning v4 Provider action 批次必须是唯一 plan.submit_gdd action 且无 collaboration 合同"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let binding = batch
|
||||
.planning_session_binding
|
||||
.as_ref()
|
||||
.ok_or_else(|| "planning v4 Provider action 批次缺少 session binding".to_string())?;
|
||||
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
|
||||
if batch.actions.len() != 1
|
||||
|| binding.request_kind != "tool-plan"
|
||||
|| batch.actions[0].planning_session_binding.as_ref() != Some(binding)
|
||||
|| batch.provider_request_id.as_deref() != Some(binding.provider_request_id.as_str())
|
||||
|| batch.project_id != binding.project_id
|
||||
|| batch.agent_id != binding.agent_id
|
||||
|| batch.task_id != binding.task_id
|
||||
|| batch.session_id != binding.session_id
|
||||
|| batch.run_id != binding.run_id
|
||||
|| batch.source != binding.source
|
||||
|| batch.run_profile != binding.run_profile
|
||||
|| batch.run_profile_binding_fingerprint != binding.run_profile_binding_fingerprint
|
||||
|| batch.planned_steer_cursor != binding.applied_steer_cursor
|
||||
|| batch.actions[0].provider_batch_plan_update != batch.plan.plan_update
|
||||
{
|
||||
return Err("planning v4 批次成员与 session binding 不一致".to_string());
|
||||
}
|
||||
} else if batch.planning_session_binding.is_some() || batch.provider_request_id.is_some() {
|
||||
return Err("非 planning v4 批次不能携带 planning session binding".to_string());
|
||||
}
|
||||
let minimum_action_count = if plan_submit_batch {
|
||||
1
|
||||
} else if batch.schema_version != AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION
|
||||
let minimum_action_count = if batch.schema_version
|
||||
!= AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION
|
||||
&& batch.collaboration_contract.is_some()
|
||||
{
|
||||
1
|
||||
@@ -281,23 +198,8 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
.actions
|
||||
.first()
|
||||
.ok_or_else(|| "Agent Runtime Provider action 批次缺少首个动作".to_string())?;
|
||||
if plan_schema {
|
||||
let mut recovered_plan = first_pending.tool_plan();
|
||||
recovered_plan.actions = vec![first_pending.action.clone()];
|
||||
if recovered_plan != batch.plan {
|
||||
return Err("planning v4 批次无法从 standalone member 精确重建完整 plan".to_string());
|
||||
}
|
||||
}
|
||||
let mut waiting_confirmation_count = 0_usize;
|
||||
let mut rejected_count = 0_usize;
|
||||
// planning v4 批次把 plan update 冻结进 standalone member 用于恢复;普通批次的成员则被下方
|
||||
// 分支要求不得携带 planning recovery material。期望值必须按批次类型分叉,否则「同一轮里既
|
||||
// 调 update_agent_plan 又调工具」的普通批次会同时踩中两条互斥规则。
|
||||
let expected_member_plan_update = batch
|
||||
.planning_session_binding
|
||||
.is_some()
|
||||
.then(|| batch.plan.plan_update.clone())
|
||||
.flatten();
|
||||
for (index, pending) in batch.actions.iter().enumerate() {
|
||||
validate_agent_runtime_pending_tool_action_record(root, pending)?;
|
||||
if pending.agent_id != batch.agent_id
|
||||
@@ -312,8 +214,6 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
|| pending.project_revision_before != batch.project_revision_before
|
||||
|| pending.planned_repository_context_fingerprint
|
||||
!= batch.planned_repository_context_fingerprint
|
||||
|| pending.planning_session_binding != batch.planning_session_binding
|
||||
|| pending.provider_batch_plan_update != expected_member_plan_update
|
||||
|| usize::try_from(pending.action_index).unwrap_or(usize::MAX) != index
|
||||
|| pending.action != batch.plan.actions[index]
|
||||
{
|
||||
@@ -342,26 +242,6 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
if !action_ids.insert(pending.action_id.clone()) {
|
||||
return Err("Agent Runtime Provider action 批次包含重复 actionId".to_string());
|
||||
}
|
||||
if plan_schema {
|
||||
let binding = batch
|
||||
.planning_session_binding
|
||||
.as_ref()
|
||||
.ok_or_else(|| "planning v4 批次缺少 session binding".to_string())?;
|
||||
if pending.planning_session_binding.as_ref() != Some(binding)
|
||||
|| pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| pending.action_id.is_empty()
|
||||
{
|
||||
return Err(format!(
|
||||
"planning v4 批次成员 frozen binding/action identity 不一致:index={index}"
|
||||
));
|
||||
}
|
||||
} else if pending.planning_session_binding.is_some()
|
||||
|| pending.provider_batch_plan_update.is_some()
|
||||
{
|
||||
return Err(format!(
|
||||
"非 planning Provider action 批次成员不能携带 planning recovery material:index={index}"
|
||||
));
|
||||
}
|
||||
match pending.status.as_str() {
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING => {
|
||||
if pending.execution_mode != AGENT_RUNTIME_ACTION_EXECUTION_MODE_CONFIRMATION
|
||||
@@ -525,26 +405,6 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
batch.collaboration_contract.as_ref(),
|
||||
)?
|
||||
}
|
||||
AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION => {
|
||||
let binding = batch
|
||||
.planning_session_binding
|
||||
.as_ref()
|
||||
.ok_or_else(|| "planning v4 批次缺少 session binding".to_string())?;
|
||||
agent_runtime_plan_provider_action_batch_id(
|
||||
&batch.project_id,
|
||||
&batch.agent_id,
|
||||
&batch.task_id,
|
||||
&batch.session_id,
|
||||
&batch.run_id,
|
||||
batch.loop_iteration,
|
||||
batch.planned_steer_cursor,
|
||||
&batch.plan,
|
||||
&batch.project_revision_before,
|
||||
&batch.planned_repository_context_fingerprint,
|
||||
&batch.actions,
|
||||
binding,
|
||||
)?
|
||||
}
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION => {
|
||||
agent_runtime_provider_action_batch_id(
|
||||
&batch.project_id,
|
||||
|
||||
+1
-51
@@ -116,31 +116,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
root,
|
||||
"runtime.provider_request.capture.final_reply",
|
||||
)?;
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// Keep every rebuilt request field on the same budget successor
|
||||
// that will be exposed by the structured injection and binding.
|
||||
fold_plan_provider_usage_before_new_request_at_locked(root, Some((agent_id, run_id)))?;
|
||||
// Freeze the concrete final-reply request and its Provider-facing
|
||||
// planning injection under the same project lock as the durable
|
||||
// session binding. This mirrors tool-plan and prevents an older
|
||||
// message object from being stamped with a newer session primary.
|
||||
built_request = build_game_creator_agent_background_final_reply_request(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
task,
|
||||
plan,
|
||||
observations,
|
||||
)?;
|
||||
let wire_bytes =
|
||||
capture_plan_provider_structured_injections_at(root, session_id, observations)?;
|
||||
let message = render_plan_provider_structured_injections_message(&wire_bytes)?;
|
||||
built_request
|
||||
.2
|
||||
.messages
|
||||
.insert(1, LlmMessage::user(message));
|
||||
}
|
||||
let snapshot = capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
|
||||
root,
|
||||
agent_id,
|
||||
@@ -150,33 +125,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
request_slot,
|
||||
applied_steer_cursor,
|
||||
)?;
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
let request_context_fingerprint =
|
||||
game_creator_agent_runtime_plan_provider_request_context_fingerprint(
|
||||
&built_request.0,
|
||||
&built_request.2,
|
||||
)?;
|
||||
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
|
||||
let binding = capture_plan_provider_session_binding_for_snapshot(
|
||||
root,
|
||||
&runtime,
|
||||
&snapshot,
|
||||
&request_context_fingerprint,
|
||||
)?;
|
||||
snapshot.with_planning_session_binding(Some(binding))
|
||||
} else {
|
||||
snapshot
|
||||
}
|
||||
snapshot
|
||||
};
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
|
||||
validate_game_creator_llm_request_context_budget(
|
||||
&built_request.0,
|
||||
&built_request.2,
|
||||
estimated_input_tokens,
|
||||
"锁内冻结后的 final-reply 请求",
|
||||
)?;
|
||||
}
|
||||
let (llm, config_path, request) = built_request;
|
||||
let auto_compact_token_limit = llm.auto_compact_token_limit;
|
||||
let stream_snapshot = provider_snapshot.clone();
|
||||
|
||||
+13
-704
File diff suppressed because it is too large
Load Diff
+17
-178
@@ -120,24 +120,15 @@ 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 或代码围栏。"
|
||||
)
|
||||
}
|
||||
fn root_goal_contract_repair_instruction(protocol_error: &str) -> String {
|
||||
format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}
|
||||
当前根 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 或代码围栏。"
|
||||
)
|
||||
}
|
||||
|
||||
/// 把上游的终态标记夹紧成可落审计的短标记。
|
||||
@@ -222,15 +213,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
let (run_profile, _) =
|
||||
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&run_profile);
|
||||
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 (mut built_request, mut supervisor_manifest_dag_in_progress_at_request) = {
|
||||
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
@@ -346,78 +328,21 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
.as_ref()
|
||||
.map(|sidecar| context_compaction_result(sidecar, true));
|
||||
}
|
||||
let (provider_snapshot, initial_planning_session_binding) = {
|
||||
let provider_snapshot = {
|
||||
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.provider_request.capture.tool_plan",
|
||||
)?;
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// Fold first so the request rebuild, structured injection and
|
||||
// frozen session binding all observe one budget successor.
|
||||
fold_plan_provider_usage_before_new_request_at_locked(root, Some((agent_id, run_id)))?;
|
||||
}
|
||||
// Exact planning requests must freeze the session and the concrete
|
||||
// request object under one project lock. Rebuild once while holding
|
||||
// that lock so a session successor cannot be used to re-label an
|
||||
// object assembled from an older session.
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
built_request = build_game_creator_agent_background_tool_plan_request_locked(
|
||||
root,
|
||||
&_lock,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
task,
|
||||
observations,
|
||||
loop_index,
|
||||
)?;
|
||||
}
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
let wire_bytes =
|
||||
capture_plan_provider_structured_injections_at(root, session_id, observations)?;
|
||||
let message = render_plan_provider_structured_injections_message(&wire_bytes)?;
|
||||
built_request
|
||||
.2
|
||||
.messages
|
||||
.insert(1, LlmMessage::user(message));
|
||||
}
|
||||
let provider_snapshot =
|
||||
capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
"tool-plan",
|
||||
&initial_request_slot,
|
||||
applied_steer_cursor,
|
||||
)?;
|
||||
let planning_session_binding = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
let request_context_fingerprint =
|
||||
game_creator_agent_runtime_plan_provider_request_context_fingerprint(
|
||||
&built_request.0,
|
||||
&built_request.2,
|
||||
)?;
|
||||
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
|
||||
Some(capture_plan_provider_session_binding_for_snapshot(
|
||||
root,
|
||||
&runtime,
|
||||
&provider_snapshot,
|
||||
&request_context_fingerprint,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(provider_snapshot, planning_session_binding)
|
||||
capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
"tool-plan",
|
||||
&initial_request_slot,
|
||||
applied_steer_cursor,
|
||||
)?
|
||||
};
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
|
||||
validate_game_creator_llm_request_context_budget(
|
||||
&built_request.0,
|
||||
&built_request.2,
|
||||
estimated_input_tokens,
|
||||
"锁内冻结后的 tool-plan 请求",
|
||||
)?;
|
||||
}
|
||||
let (llm, config_path, mut request, repository_context_fingerprint, _) = built_request;
|
||||
let auto_compact_token_limit = llm.auto_compact_token_limit;
|
||||
let format_repair_attempts = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
@@ -482,36 +407,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
let request_snapshot = provider_snapshot
|
||||
.with_request_slot(&request_slot)
|
||||
.with_web_search_enabled(request.enable_web_search);
|
||||
let planning_session_binding = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
if repair_attempt == 0 {
|
||||
initial_planning_session_binding.clone()
|
||||
} else {
|
||||
let request_context_fingerprint =
|
||||
game_creator_agent_runtime_plan_provider_request_context_fingerprint(
|
||||
&llm, &request,
|
||||
)?;
|
||||
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.provider_request.freeze.plan_binding",
|
||||
)?;
|
||||
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
|
||||
let candidate = capture_plan_provider_session_binding_for_snapshot(
|
||||
root,
|
||||
&runtime,
|
||||
&request_snapshot,
|
||||
&request_context_fingerprint,
|
||||
)?;
|
||||
let initial = initial_planning_session_binding.as_ref().ok_or_else(|| {
|
||||
"planning Provider repair 缺少 repair-0 frozen session binding".to_string()
|
||||
})?;
|
||||
validate_plan_provider_session_binding_repair_lineage(initial, &candidate)?;
|
||||
Some(candidate)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let request_snapshot =
|
||||
request_snapshot.with_planning_session_binding(planning_session_binding.clone());
|
||||
let response = request_game_creator_agent_runtime_llm_with_persisted_transient_retry(
|
||||
root,
|
||||
&request_snapshot,
|
||||
@@ -568,16 +463,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
);
|
||||
}
|
||||
};
|
||||
let effective_planning_session_binding =
|
||||
if let Some(base_binding) = planning_session_binding.as_ref() {
|
||||
Some(plan_provider_session_binding_for_attempt(
|
||||
base_binding,
|
||||
&response_handoff.request_slot,
|
||||
&response_handoff.provider_request_id,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if response_handoff.to_llm_response() != response {
|
||||
return Err(
|
||||
game_creator_agent_runtime_provider_handoff_reconciliation_error(
|
||||
@@ -907,7 +792,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
return Ok(RequestedAgentRuntimeToolPlanOutcome::Ready(Some(
|
||||
RequestedAgentRuntimeToolPlan {
|
||||
plan,
|
||||
planning_session_binding: effective_planning_session_binding,
|
||||
repository_context_fingerprint,
|
||||
estimated_input_tokens,
|
||||
auto_compact_token_limit,
|
||||
@@ -1069,13 +953,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
agent_runtime_protocol_error_requires_supervisor_collaboration_repair(
|
||||
&protocol_error,
|
||||
) && !request.function_tools.is_empty();
|
||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
request.function_tools =
|
||||
build_agent_runtime_native_function_tools_for_agent(agent_id)?;
|
||||
request.messages.push(LlmMessage::user(format!(
|
||||
"上一条输出不符合 planning 工具计划协议:{protocol_error}\n本轮修复仍只允许调用 file.read、file.list、plan.submit_gdd、update_agent_plan、respond_to_user。plan.submit_gdd 的 input 必须严格符合 plan-submit-gdd-input.v1,只提交 game、decisions、prototypeValidationItems;它必须是唯一 action,可与 update_agent_plan 同响应,但不能与其它动作或 respond_to_user 混合。不得调用或描述其它工具,不得输出普通文本来代替函数调用;需要用户决定时以 AGC_NEEDS_USER_INPUT_V1 终态信封收束。"
|
||||
)));
|
||||
} else if force_root_goal_contract
|
||||
if force_root_goal_contract
|
||||
|| force_supervisor_initial_collaboration
|
||||
|| force_autonomous_specialist_mutation_only
|
||||
|| force_autonomous_specialist_verification_only
|
||||
@@ -1116,12 +994,11 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
}
|
||||
}
|
||||
if force_root_goal_contract {
|
||||
restrict_root_goal_contract_repair_request(&mut request, plan_root)?;
|
||||
restrict_root_goal_contract_repair_request(&mut request)?;
|
||||
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;
|
||||
@@ -1292,17 +1169,6 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n请修复格式,只调用当前请求广告的 update_agent_plan、动作工具或 respond_to_user;当前 in_progress 步骤已具备执行条件时,格式修复必须保留并调用对应动作工具,不能退化为只调用 update_agent_plan。不要解释,不要 markdown,不要代码围栏,也不要把计划、动作或回复放进普通文本。"
|
||||
)));
|
||||
}
|
||||
// 修复分支会先整份重建工具目录,再按各自的场景收窄。plan 根必须在
|
||||
// 所有分支收窄之后再取一次交集:早于分支就会让
|
||||
// `restrict_agent_runtime_supervisor_collaboration_repair_tools`
|
||||
// 这类「必须包含 agent.spawn_isolated」的检查硬失败,晚于分支则
|
||||
// 保证任何修复轮都不会把被裁掉的 36 个工具重新广告回去。
|
||||
if plan_root {
|
||||
retain_plan_root_supervisor_native_tools(
|
||||
&mut request.function_tools,
|
||||
plan_root_supervisor_stage_at(root, agent_id, run_id)?,
|
||||
)?;
|
||||
}
|
||||
request.enable_web_search = false;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -1527,31 +1393,4 @@ mod supervisor_collaboration_repair_tests {
|
||||
assert_eq!(merged, vec![replacement]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_root_goal_contract_repair_keeps_the_fixed_schema_and_instruction() {
|
||||
let mut request = LlmRunRequest::new(Vec::new())
|
||||
.with_function_tools(
|
||||
build_agent_runtime_native_function_tools().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 的成功回执"));
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -68,7 +68,6 @@ fn response_stream_fixture(
|
||||
),
|
||||
web_search_enabled: false,
|
||||
allow_idle_context_compaction: false,
|
||||
planning_session_binding: None,
|
||||
};
|
||||
(project, state, response_revision, snapshot)
|
||||
}
|
||||
|
||||
+1
-73
@@ -47,7 +47,6 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_classified_for_age
|
||||
) -> Result<ParsedAgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
|
||||
if response.tool_calls.is_empty() {
|
||||
let plan = parse_game_creator_agent_tool_plan_response_classified(response.text.as_str())?;
|
||||
validate_agent_runtime_tool_plan_identity(agent_id, &plan)?;
|
||||
return Ok(ParsedAgentRuntimeToolPlan {
|
||||
plan,
|
||||
protocol: "text_json",
|
||||
@@ -81,12 +80,6 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_classified_for_age
|
||||
if response.tool_calls.len() == 1
|
||||
&& response.tool_calls[0].name == AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME
|
||||
{
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
|
||||
"Agent 原生工具协议错误:project-planning 不允许旧 submit_agent_tool_plan 包装器",
|
||||
));
|
||||
}
|
||||
let call = &response.tool_calls[0];
|
||||
let plan = parse_game_creator_agent_tool_plan_payload(call.arguments.as_str(), true)
|
||||
.map_err(|error| {
|
||||
@@ -108,9 +101,8 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_classified_for_age
|
||||
normalized_text_sha256: text_normalization.source_text_sha256,
|
||||
});
|
||||
}
|
||||
let native = parse_agent_runtime_native_tool_calls_for_agent(agent_id, &response.tool_calls)?;
|
||||
let native = parse_agent_runtime_native_tool_calls(&response.tool_calls)?;
|
||||
let plan = normalize_game_creator_agent_tool_plan(native.plan)?;
|
||||
validate_agent_runtime_tool_plan_identity(agent_id, &plan)?;
|
||||
Ok(ParsedAgentRuntimeToolPlan {
|
||||
plan,
|
||||
protocol: "native_runtime_tools",
|
||||
@@ -125,46 +117,6 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_classified_for_age
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_agent_runtime_tool_plan_identity(
|
||||
agent_id: &str,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
|
||||
if agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|
||||
&& agent_id.trim() != "__all_agents__"
|
||||
&& plan
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| action.tool.trim() == PLAN_SUBMIT_GDD_TOOL)
|
||||
{
|
||||
return Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
|
||||
format!(
|
||||
"Agent 原生工具协议错误:Agent {} 不允许调用 {}",
|
||||
agent_id.trim(),
|
||||
PLAN_SUBMIT_GDD_TOOL
|
||||
),
|
||||
));
|
||||
}
|
||||
if agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(action) = plan
|
||||
.actions
|
||||
.iter()
|
||||
.find(|action| !agent_runtime_native_tool_allowed_for_agent(agent_id, &action.tool))
|
||||
{
|
||||
return Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
|
||||
format!(
|
||||
"Agent 原生工具协议错误:Agent {} 不允许调用 {}",
|
||||
agent_id.trim(),
|
||||
action.tool.trim()
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(in crate::agent) struct AgentRuntimeToolPlanTextNormalization {
|
||||
pub(in crate::agent) visible_text: String,
|
||||
@@ -353,29 +305,5 @@ pub(in crate::agent) fn normalize_game_creator_agent_tool_plan(
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
validate_plan_submit_gdd_tool_plan(&plan).map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn validate_plan_submit_gdd_tool_plan(plan: &AgentRuntimeToolPlan) -> Result<(), String> {
|
||||
let submit_count = plan
|
||||
.actions
|
||||
.iter()
|
||||
.filter(|action| action.tool.trim() == PLAN_SUBMIT_GDD_TOOL)
|
||||
.count();
|
||||
if submit_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if submit_count != 1 || plan.actions.len() != 1 || !plan.response.trim().is_empty() {
|
||||
return Err(
|
||||
"Agent 工具计划协议错误:plan.submit_gdd 必须是本轮唯一 action,且不能与 respond_to_user 同响应(可与 update_agent_plan 同响应)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
-279
@@ -3,13 +3,6 @@ use super::*;
|
||||
pub(crate) const AGENT_RUNTIME_CANVAS_ASSET_KINDS: &[&str] =
|
||||
&["game-art", "icon-spec", "ui-prototype", "art-spritesheet"];
|
||||
|
||||
/// Exact executable action surface for the delegated planning child. The
|
||||
/// two read tools are ordinary Runtime capabilities; `plan.submit_gdd` is a
|
||||
/// planning-only capability and therefore must not be added to the global
|
||||
/// `agent_runtime_executable_tools()` catalog.
|
||||
pub(crate) const AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS: &[&str] =
|
||||
&["file.read", "file.list", PLAN_SUBMIT_GDD_TOOL];
|
||||
|
||||
#[cfg(test)]
|
||||
mod canvas_asset_kind_contract_tests {
|
||||
use super::*;
|
||||
@@ -22,44 +15,6 @@ mod canvas_asset_kind_contract_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_submit_confirmation_is_classified_as_deny_without_a_generic_pending_mode() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("planning-submit-confirm-policy-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "planning-submit-confirm-policy", "submit policy")
|
||||
.expect("init policy fixture");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec![PLAN_SUBMIT_GDD_TOOL.to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write submit confirmation policy");
|
||||
|
||||
let snapshot =
|
||||
agent_runtime_tool_policy_snapshot_at(&root, GAME_CREATOR_PROJECT_PLANNING_AGENT_ID)
|
||||
.expect("read planning policy snapshot");
|
||||
assert!(snapshot
|
||||
.denied_tools
|
||||
.iter()
|
||||
.any(|tool| tool == PLAN_SUBMIT_GDD_TOOL));
|
||||
assert!(!snapshot
|
||||
.auto_tools
|
||||
.iter()
|
||||
.any(|tool| tool == PLAN_SUBMIT_GDD_TOOL));
|
||||
assert!(!snapshot
|
||||
.confirm_tools
|
||||
.iter()
|
||||
.any(|tool| tool == PLAN_SUBMIT_GDD_TOOL));
|
||||
|
||||
// The M1B-2 submit state machine has no generic confirmation
|
||||
// consumer. A confirmation rule must therefore never advertise a
|
||||
// confirmation execution path or create a pending sidecar.
|
||||
assert!(!root.join(".agent/runtime/pending-actions").exists());
|
||||
assert!(!root.join(".agent/planning/gdd.v1.json").exists());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
@@ -112,168 +67,6 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
]
|
||||
}
|
||||
|
||||
/// plan 根 Supervisor(`source == project-supervisor-plan`)在整条策划链路里
|
||||
/// 只负责三件事:冻结 Goal Contract、委派与续跑 `project-planning`、按 §13.0 取证
|
||||
/// 后建审批卡。策划内容全部由子 Agent 生产,Supervisor 不写文件、不跑命令、不做
|
||||
/// 预览、不生成素材、不调度 ready 任务、不并行委派。
|
||||
///
|
||||
/// 全量注册表会把 43 个原生工具摆在 Provider 眼前,其中绝大多数在 plan 根都会被
|
||||
/// 执行层拒绝——广告出去只会诱导 Supervisor 自己下场干活。这里给出 plan 根的
|
||||
/// exact allowlist,Provider 请求目录和 system prompt 的工具清单共用它,二者不得
|
||||
/// 各自维护一份。
|
||||
///
|
||||
/// **`user.input_request` 不在其中**:澄清卡不是 Supervisor 发的。子 Agent 以
|
||||
/// `AGC_NEEDS_USER_INPUT_V1` 终态信封退出后,Runtime 在 parent-wake 屏障处自己按
|
||||
/// 信封原文构造 `user.input_request` pending 并且**不恢复父 run**
|
||||
/// (`ensure_static_delegate_user_input_wait_at_locked`)。Supervisor 因此永远收不到
|
||||
/// needs-user-input observation,也就没有调用它的时机;广告出去只会让它在别的时点
|
||||
/// 调一次,撞上 Runtime 已经装好的那份 pending 而硬失败。
|
||||
///
|
||||
/// `file.read` 与 `agent.acceptance_update` 只为 §13.0 的审批前置取证门存在(分页读
|
||||
/// `game/fast_gdd.md` 并列出全部分页 actionId)。**`agent.action_history` 不在其中**:
|
||||
/// 每次 `file.read` 的 observation 已经自带 `sourceActionId`,取证不需要回头查历史;
|
||||
/// 留着它只会让模型为同一个 id 反复确认(实测连查四次,答案一直在上下文里)。
|
||||
pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static str] {
|
||||
&[
|
||||
"file.read",
|
||||
"agent.delegate",
|
||||
"agent.goal_contract",
|
||||
"agent.acceptance_update",
|
||||
"agent.run_status",
|
||||
]
|
||||
}
|
||||
|
||||
/// plan 根在链路上的推进阶段。
|
||||
///
|
||||
/// 工具面按阶段收窄,是因为「工具在它无用的阶段仍然可见」会直接制造活锁:实测一次
|
||||
/// 生产 run 里 Supervisor 冻结合同后没有委派,改成反复调 `agent.run_status` 去查一个
|
||||
/// 根本不存在的委派,24 轮里 58 次 `agent.run_status`、0 次 `agent.delegate`,一直烧
|
||||
/// 到超时。空转闸门也拦不住——只读调用同样会把 `plan_update_idle_rounds` 清零。
|
||||
///
|
||||
/// 本地原型没有这个问题,因为它的 Supervisor 只有四个工具且每一个都推进链路:
|
||||
/// 「调了工具」和「推进了链路」在那边是同一件事。这里把同一性质移植过来——每个
|
||||
/// 阶段只广告该阶段能真正推进链路的工具。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PlanRootSupervisorStage {
|
||||
/// Goal Contract 尚未冻结:本轮唯一能推进的动作是冻结它。
|
||||
GoalContract,
|
||||
/// 合同已冻结但本根 run 还没有任何委派:唯一能推进的动作是派出策划子 Agent。
|
||||
Delegate,
|
||||
/// 最新 GDD 已提交但尚未完成当前根 Run 的 Acceptance Graph 取证:只能读取
|
||||
/// `game/fast_gdd.md`、更新验收图或重放状态,不能抢先创建重复策划 delivery。
|
||||
AwaitingAcceptanceEvidence,
|
||||
/// 已有委派:取证、返工与审批相关工具全部开放。
|
||||
Delegated,
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_plan_root_supervisor_tools_for_stage(
|
||||
stage: PlanRootSupervisorStage,
|
||||
) -> &'static [&'static str] {
|
||||
match stage {
|
||||
PlanRootSupervisorStage::GoalContract => &["agent.goal_contract"],
|
||||
PlanRootSupervisorStage::Delegate => &["agent.delegate"],
|
||||
PlanRootSupervisorStage::AwaitingAcceptanceEvidence => {
|
||||
&["file.read", "agent.acceptance_update", "agent.run_status"]
|
||||
}
|
||||
// 合同已冻结且不可重写,再广告 agent.goal_contract 只会诱导一次必被拒的调用。
|
||||
PlanRootSupervisorStage::Delegated => &[
|
||||
"file.read",
|
||||
"agent.delegate",
|
||||
"agent.acceptance_update",
|
||||
"agent.run_status",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_root_stage_tests {
|
||||
use super::*;
|
||||
|
||||
/// 各阶段并集必须正好等于 allowlist:prompt 头部按 allowlist 列工具,若某个工具
|
||||
/// 只出现在某一阶段而不在 allowlist 里,头部就会漏掉它;反之则是广告了一个永远
|
||||
/// 拿不到的工具。两侧都是「合同说有、请求里没有」的自相矛盾。
|
||||
#[test]
|
||||
fn every_stage_tool_is_part_of_the_plan_root_allowlist_and_the_union_covers_it() {
|
||||
let allowlist = agent_runtime_plan_root_supervisor_tools()
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let union = [
|
||||
PlanRootSupervisorStage::GoalContract,
|
||||
PlanRootSupervisorStage::Delegate,
|
||||
PlanRootSupervisorStage::AwaitingAcceptanceEvidence,
|
||||
PlanRootSupervisorStage::Delegated,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|stage| {
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(stage)
|
||||
.iter()
|
||||
.copied()
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(union, allowlist);
|
||||
}
|
||||
|
||||
/// 活锁的诱因是「工具在它无用的阶段仍然可见」。这两个阶段各自只能有一个动作。
|
||||
#[test]
|
||||
fn the_pre_delegation_stages_expose_exactly_one_advancing_action() {
|
||||
assert_eq!(
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(
|
||||
PlanRootSupervisorStage::GoalContract
|
||||
),
|
||||
&["agent.goal_contract"]
|
||||
);
|
||||
assert_eq!(
|
||||
agent_runtime_plan_root_supervisor_tools_for_stage(PlanRootSupervisorStage::Delegate),
|
||||
&["agent.delegate"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 只按 durable 事实判定阶段,不看 Provider 说了什么。
|
||||
///
|
||||
/// 调用方必须已经持有当前项目写锁;需要自行取得锁的调用方使用下面的
|
||||
/// `plan_root_supervisor_stage_at` 包装入口。这样 Provider 请求构建路径可以复用外层
|
||||
/// 已有的项目锁,不会在阶段判定中再次获取同一把非重入锁。
|
||||
pub(crate) fn plan_root_supervisor_stage_at_locked(
|
||||
root: &Path,
|
||||
project_lock: &ProjectWriteLock,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<PlanRootSupervisorStage, String> {
|
||||
if !project_lock.guards_project_root(root)? {
|
||||
return Err("plan Supervisor 阶段判定缺少当前项目写锁".to_string());
|
||||
}
|
||||
if read_game_creator_agent_runtime_goal_contract_at(root, agent_id, run_id)?.is_none() {
|
||||
return Ok(PlanRootSupervisorStage::GoalContract);
|
||||
}
|
||||
let delegated = list_static_delegate_deliveries_at(root)?
|
||||
.into_iter()
|
||||
.any(|delivery| delivery.parent_agent_id == agent_id && delivery.parent_run_id == run_id);
|
||||
if !delegated {
|
||||
return Ok(PlanRootSupervisorStage::Delegate);
|
||||
}
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& plan_root_supervisor_acceptance_evidence_required_locked(root, project_lock, run_id)?
|
||||
{
|
||||
return Ok(PlanRootSupervisorStage::AwaitingAcceptanceEvidence);
|
||||
}
|
||||
Ok(PlanRootSupervisorStage::Delegated)
|
||||
}
|
||||
|
||||
/// 供未持有项目写锁的调用方使用的阶段判定入口。
|
||||
pub(crate) fn plan_root_supervisor_stage_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<PlanRootSupervisorStage, String> {
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"planning.supervisor-stage",
|
||||
)?;
|
||||
plan_root_supervisor_stage_at_locked(root, &_lock, agent_id, run_id)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
|
||||
agent_runtime_executable_tools()
|
||||
}
|
||||
@@ -381,29 +174,6 @@ pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
|
||||
auto_tools.push(tool.to_string());
|
||||
}
|
||||
}
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
// `plan.submit_gdd` is intentionally not in the global catalog.
|
||||
// Classify it against the same project/Agent permission policy so an
|
||||
// explicit deny/confirm cannot be bypassed by the planning ceiling.
|
||||
let planning_submit_command = PLAN_SUBMIT_GDD_TOOL;
|
||||
let denied = policy
|
||||
.denied_commands
|
||||
.iter()
|
||||
.any(|command| command == planning_submit_command);
|
||||
let confirmation_requested = policy
|
||||
.confirm_commands
|
||||
.iter()
|
||||
.any(|command| command == planning_submit_command);
|
||||
if denied || confirmation_requested {
|
||||
// M1B-2 has no generic user-confirmation state for the Runtime
|
||||
// commit action. An explicit confirm rule therefore fails closed
|
||||
// instead of creating a pending shape the submit state machine can
|
||||
// never consume.
|
||||
denied_tools.push(PLAN_SUBMIT_GDD_TOOL.to_string());
|
||||
} else {
|
||||
auto_tools.push(PLAN_SUBMIT_GDD_TOOL.to_string());
|
||||
}
|
||||
}
|
||||
Ok(AgentRuntimeToolPolicySnapshot {
|
||||
run_profile: default_agent_runtime_run_profile(),
|
||||
run_profile_binding_fingerprint: String::new(),
|
||||
@@ -435,55 +205,6 @@ pub(crate) fn agent_runtime_tool_policy_snapshot_for_run_at(
|
||||
)?;
|
||||
snapshot.run_profile = run_profile.clone();
|
||||
snapshot.run_profile_binding_fingerprint = binding_fingerprint;
|
||||
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
validate_project_planning_child_binding_at(root, agent_id, run_id)?;
|
||||
// Planning is a delegated child. Never let normalization/recovery
|
||||
// repopulate the broad default policy for this identity.
|
||||
let exact = AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS;
|
||||
if !snapshot
|
||||
.allowed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == PLAN_SUBMIT_GDD_TOOL)
|
||||
{
|
||||
snapshot
|
||||
.allowed_tools
|
||||
.push(PLAN_SUBMIT_GDD_TOOL.to_string());
|
||||
}
|
||||
snapshot
|
||||
.allowed_tools
|
||||
.retain(|tool| exact.contains(&tool.as_str()));
|
||||
snapshot
|
||||
.auto_tools
|
||||
.retain(|tool| exact.contains(&tool.as_str()));
|
||||
snapshot
|
||||
.confirm_tools
|
||||
.retain(|tool| exact.contains(&tool.as_str()));
|
||||
// `snapshot_at` has already applied the project- and Agent-level
|
||||
// permission policy. Keep an exact-tool deny in that result instead
|
||||
// of replacing it with the ceiling's non-exact denies. Deny wins
|
||||
// over auto/confirm so a stale or hand-edited snapshot cannot
|
||||
// advertise a denied planning read as executable.
|
||||
let exact_denied = snapshot
|
||||
.denied_tools
|
||||
.iter()
|
||||
.filter(|tool| exact.contains(&tool.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
snapshot
|
||||
.auto_tools
|
||||
.retain(|tool| !exact_denied.iter().any(|denied| denied == tool));
|
||||
snapshot
|
||||
.confirm_tools
|
||||
.retain(|tool| !exact_denied.iter().any(|denied| denied == tool));
|
||||
snapshot.denied_tools = exact_denied;
|
||||
snapshot.denied_tools.extend(
|
||||
agent_runtime_executable_tools()
|
||||
.into_iter()
|
||||
.filter(|tool| !exact.contains(tool))
|
||||
.map(str::to_string),
|
||||
);
|
||||
return Ok(snapshot);
|
||||
}
|
||||
if run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
return Ok(snapshot);
|
||||
}
|
||||
|
||||
@@ -16,25 +16,6 @@ fn build_game_creator_runtime_agent_catalog() -> Result<AgentCatalog, String> {
|
||||
}))
|
||||
})
|
||||
.map_err(|error| format!("AI 游戏创作 Agent catalog 无效:{error}"))?];
|
||||
// 立项策划子 Agent:与 Supervisor 同为「catalog 成员但不是种子 DAG 任务」,
|
||||
// 因此在遍历 GAME_CREATOR_AGENT_GROUP_DEFINITIONS 之外单独登记。它不属于任何
|
||||
// 专业组,groupId 自引用,避免与 design 组(中文 label「策划组」)语义碰撞。
|
||||
agents.push(
|
||||
AgentDescriptor::try_new(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
PROJECT_PLANNING_AGENT_DEFINITION.id,
|
||||
std::iter::empty::<&str>(),
|
||||
)
|
||||
.and_then(|agent| {
|
||||
agent.with_metadata(serde_json::json!({
|
||||
"groupId": PROJECT_PLANNING_AGENT_DEFINITION.id,
|
||||
"roleLabel": PROJECT_PLANNING_AGENT_ROLES[0].role,
|
||||
"toolId": PROJECT_PLANNING_AGENT_ROLES[0].tool_id,
|
||||
"capabilityAuthority": "game-creator-tool-policy-snapshot"
|
||||
}))
|
||||
})
|
||||
.map_err(|error| format!("AI 游戏创作 Agent catalog 无效:{error}"))?,
|
||||
);
|
||||
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
for role in group.roles {
|
||||
agents.push(
|
||||
@@ -115,12 +96,9 @@ mod tests {
|
||||
#[test]
|
||||
fn game_creator_runtime_agent_catalog_matches_the_existing_role_directory() {
|
||||
let catalog = game_creator_runtime_agent_catalog().expect("agent catalog");
|
||||
// catalog 恰好是「两个组外单节点(Supervisor、立项策划)+ 各专业组角色」。
|
||||
// 立项策划刻意不在 GAME_CREATOR_AGENT_GROUP_DEFINITIONS 里:它不参与
|
||||
// build.rs 与种子 DAG 的一致性校验,「做游戏」16 任务 DAG 一行不动。
|
||||
// catalog 恰好是「一个组外单节点(Supervisor)+ 各专业组角色」。
|
||||
let mut expected = std::collections::BTreeSet::from([
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID.to_string(),
|
||||
]);
|
||||
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
expected.extend(group.roles.iter().map(|role| role.task_id.to_string()));
|
||||
|
||||
@@ -60,11 +60,6 @@ pub(super) const AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED: &str = "obse
|
||||
pub(super) const AGENT_RUNTIME_PARALLEL_READ_BATCH_SIDECAR_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
pub(crate) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v3";
|
||||
/// Exact planning batches carry the frozen provider/session binding. Keep
|
||||
/// ordinary provider batches on v3 so existing recovery readers remain
|
||||
/// byte-for-byte compatible.
|
||||
pub(crate) const AGENT_RUNTIME_PLAN_PROVIDER_ACTION_BATCH_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v4";
|
||||
pub(super) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v2";
|
||||
pub(super) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION: &str =
|
||||
@@ -111,116 +106,13 @@ pub(crate) const AGENT_RUNTIME_ISOLATED_CHILD_SOURCE: &str = "agent-isolated-chi
|
||||
pub(crate) const AGENT_RUNTIME_ISOLATED_JOIN_SOURCE: &str = "agent-isolated-join";
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui";
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli";
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE: &str = "project-supervisor-plan";
|
||||
pub(crate) const LEGACY_PLANNING_RETIRED_ERROR: &str = "旧版策划链路已退役,请重新创建 V2 策划会话";
|
||||
pub(super) const AUTONOMOUS_GAME_BUILD_FIXED_TASK_GRAPH_STALLED_ERROR: &str =
|
||||
"自主构建任务图无法继续推进";
|
||||
pub(crate) const AGENT_RUNTIME_PLAN_AUTONOMOUS_PROFILE_UNSUPPORTED_KIND: &str =
|
||||
"plan-autonomous-profile-unsupported";
|
||||
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";
|
||||
|
||||
/// Idempotently close the planning child after the immutable GDD submit point.
|
||||
/// The original submit pending/batch remain live recovery anchors until M1C-1
|
||||
/// writes their terminal observation, so every other child projection must be
|
||||
/// independently replayable across process kills.
|
||||
pub(in crate::agent) fn ensure_project_planning_submit_child_completion_at(
|
||||
root: &Path,
|
||||
runtime: &mut AgentRuntimeState,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
result: &PlanSubmitGddResultV1,
|
||||
) -> Result<(), String> {
|
||||
runtime.pending_tool_action = Some(pending.summary());
|
||||
runtime.status = "idle".to_string();
|
||||
runtime.phase = "completed".to_string();
|
||||
runtime.current_action = format!(
|
||||
"Fast GDD v{} 已完成 create-only 提交",
|
||||
result.gdd_ref.version
|
||||
);
|
||||
runtime.waiting_on = "无".to_string();
|
||||
runtime.next_step = "策划子 Run 已完成".to_string();
|
||||
runtime.last_response = Some(format!("Fast GDD v{} 已提交。", result.gdd_ref.version));
|
||||
runtime.error = None;
|
||||
complete_agent_runtime_remaining_plan_steps(runtime, "Fast GDD 已到达 create-only 提交点。");
|
||||
runtime.updated_at = unix_timestamp();
|
||||
|
||||
append_game_creator_agent_runtime_task_projection_once(root, runtime, &pending.action_id)?;
|
||||
refresh_game_creator_agent_runtime_task_queue(root, runtime)?;
|
||||
write_game_creator_agent_runtime_state(root, runtime)?;
|
||||
append_game_creator_agent_runtime_action_event(
|
||||
root,
|
||||
runtime,
|
||||
"plan.submit_gdd.committed",
|
||||
"idle",
|
||||
"completed",
|
||||
"策划子 Run 已在 GDD 提交点终止;原 submit action 尚未 observed。",
|
||||
Some(&format!(
|
||||
"actionId={} · gddId={} · version={} · fingerprint={}",
|
||||
pending.action_id,
|
||||
result.gdd_ref.gdd_id,
|
||||
result.gdd_ref.version,
|
||||
result.gdd_ref.fingerprint
|
||||
)),
|
||||
&pending.action_id,
|
||||
)?;
|
||||
|
||||
publish_game_creator_agent_delegate_result_for_state(
|
||||
root,
|
||||
runtime,
|
||||
runtime.last_response.as_deref(),
|
||||
);
|
||||
let delegation_id = runtime
|
||||
.delegation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "策划子 Run 缺少 delegationId,无法验证提交回执".to_string())?;
|
||||
let delivery = read_static_delegate_delivery_at(root, delegation_id)?
|
||||
.ok_or_else(|| "策划子 Run 的 durable delivery 不存在".to_string())?;
|
||||
if delivery.delegation_id != delegation_id
|
||||
|| delivery.target_agent_id != runtime.agent_id
|
||||
|| delivery.target_session_id != runtime.session_id
|
||||
|| delivery.target_run_id != runtime.run_id
|
||||
|| !matches!(
|
||||
delivery.status,
|
||||
StaticDelegateDeliveryStatus::Ready | StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
)
|
||||
|| delivery.terminal_status.as_deref() != Some("completed")
|
||||
{
|
||||
return Err("策划子 Run 的 durable delivery 尚未收口为同 identity completed".to_string());
|
||||
}
|
||||
|
||||
append_agent_db_plan_submit_gdd_committed_if_missing_for_action(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
&pending.action_id,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.plan_submit_gdd.committed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"actionId": pending.action_id,
|
||||
"actionFingerprint": pending.action_fingerprint,
|
||||
"gddId": result.gdd_ref.gdd_id,
|
||||
"version": result.gdd_ref.version,
|
||||
"gddFingerprint": result.gdd_ref.fingerprint,
|
||||
"approvalRequestId": result.approval_request_id,
|
||||
"recoveryPending": false,
|
||||
}),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool {
|
||||
matches!(
|
||||
source.trim(),
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE
|
||||
| AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
| AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,132 +123,6 @@ pub(crate) fn agent_runtime_supervisor_source_is_autonomous_game_build(source: &
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_supervisor_source_is_plan(source: &str) -> bool {
|
||||
source.trim() == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE
|
||||
}
|
||||
|
||||
pub(crate) fn reject_legacy_planning_source(source: &str) -> Result<(), String> {
|
||||
if agent_runtime_supervisor_source_is_plan(source) {
|
||||
return Err(LEGACY_PLANNING_RETIRED_ERROR.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn reject_supervisor_plan_autonomous_profile(
|
||||
source: &str,
|
||||
run_profile: &str,
|
||||
) -> Result<(), String> {
|
||||
if agent_runtime_supervisor_source_is_plan(source)
|
||||
&& run_profile.trim() == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
{
|
||||
return Err(format!(
|
||||
"立项策划根 Run 必须使用 standard 档,不能搭配 autonomous-game-build(kind={AGENT_RUNTIME_PLAN_AUTONOMOUS_PROFILE_UNSUPPORTED_KIND})"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn reject_supervisor_plan_root_steer(source: &str) -> Result<(), String> {
|
||||
if agent_runtime_supervisor_source_is_plan(source) {
|
||||
return Err(format!(
|
||||
"立项策划根 Run 不接受 steer 替换;请在本轮问询中回答,或通过审批卡修改 / 退回(kind={AGENT_RUNTIME_PLAN_ROOT_STEER_UNSUPPORTED_KIND})"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plan_root_identity_source_profile_match(
|
||||
source: &str,
|
||||
profile: &str,
|
||||
binding_fingerprint: &str,
|
||||
expected_fingerprint: &str,
|
||||
) -> bool {
|
||||
agent_runtime_supervisor_source_is_plan(source)
|
||||
&& profile.trim() == AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
&& binding_fingerprint.trim() == expected_fingerprint.trim()
|
||||
&& !expected_fingerprint.trim().is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn supervisor_plan_root_identity_holds_at(
|
||||
root: &Path,
|
||||
task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<bool, String> {
|
||||
if task.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| !agent_runtime_supervisor_source_is_plan(&task.source)
|
||||
|| task.run_profile.trim() != AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
|| task.parent_agent_id.is_some()
|
||||
|| task.parent_run_id.is_some()
|
||||
|| task.delegation_id.is_some()
|
||||
|| task.run_id.trim().is_empty()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, &task.agent_id, &task.run_id)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
if validate_agent_runtime_run_profile_binding_record(root, &binding).is_err()
|
||||
|| binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| binding.run_id != task.run_id
|
||||
|| binding.root_agent_id != binding.agent_id
|
||||
|| binding.root_run_id != binding.run_id
|
||||
|| binding.parent_agent_id.is_some()
|
||||
|| binding.parent_run_id.is_some()
|
||||
|| !plan_root_identity_source_profile_match(
|
||||
&binding.source,
|
||||
&binding.profile,
|
||||
&binding.binding_fingerprint,
|
||||
&task.run_profile_binding_fingerprint,
|
||||
)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let runtime = read_game_creator_agent_runtime_at(root, &task.agent_id)?;
|
||||
if runtime.state.run_id == task.run_id
|
||||
&& (!plan_root_identity_source_profile_match(
|
||||
&runtime.state.source,
|
||||
&runtime.state.run_profile,
|
||||
&runtime.state.run_profile_binding_fingerprint,
|
||||
&binding.binding_fingerprint,
|
||||
) || runtime.state.parent_agent_id.is_some()
|
||||
|| runtime.state.parent_run_id.is_some()
|
||||
|| runtime.state.delegation_id.is_some())
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if game_creator_agent_runtime_provider_action_batch_exists(root, &task.agent_id, &task.run_id) {
|
||||
let batch = read_game_creator_agent_runtime_provider_action_batch(
|
||||
root,
|
||||
&task.agent_id,
|
||||
&task.run_id,
|
||||
)?;
|
||||
if !plan_root_identity_source_profile_match(
|
||||
&batch.source,
|
||||
&batch.run_profile,
|
||||
&batch.run_profile_binding_fingerprint,
|
||||
&binding.binding_fingerprint,
|
||||
) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn reject_supervisor_plan_root_retry_without_identity(
|
||||
root: &Path,
|
||||
task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<(), String> {
|
||||
if !agent_runtime_supervisor_source_is_plan(&task.source) {
|
||||
return Ok(());
|
||||
}
|
||||
if supervisor_plan_root_identity_holds_at(root, task)? {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"立项策划根 Run 重试身份校验失败,拒绝降级为通用 background source(kind={AGENT_RUNTIME_PLAN_ROOT_RETRY_IDENTITY_UNSUPPORTED_KIND})"
|
||||
))
|
||||
}
|
||||
pub(super) const AGENT_RUNTIME_RUN_PROFILE_BINDING_SCHEMA_VERSION: &str =
|
||||
"game-creator-run-profile-binding.v1";
|
||||
pub(super) const AGENT_RUNTIME_AUTONOMOUS_COMPLETION_CONTRACT_SCHEMA_VERSION: &str =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user