From f35ec812d91c00602a22c01b1dae7cf469e0f1a3 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 3 Sep 2026 13:15:14 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=AD=96=E5=88=92=20V2=20?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E6=B5=8B=E8=AF=95=E4=B8=8E=E6=8E=A8=E6=96=AD?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复做方案首轮额外命名等待、处理中反馈和重复错误展示 补全 GDD 输出字段提示,避免模型生成非法 decisions 字段 统一 V2 assumption_pending 与 agent_inferred 语义并同步核心闭环问询策略 保持旧 Supervisor/V1 的 default_pending 与 default 语义不变 更新 Runtime V2 技术方案和项目决策记录 --- .../runtime_protocol/planning_policy_v2.rs | 138 +++++++++++++----- .../runtime_protocol/planning_session_v2.rs | 60 +++++++- apps/ai-game-creator-shell/src/App.tsx | 17 ++- apps/ai-game-creator-shell/src/app/types.ts | 8 +- .../src/features/agent-runtime/model.ts | 7 + .../app-shell/useHomeProjectCreation.ts | 4 +- .../project-workspace/GddApprovalCard.tsx | 1 + .../project-workspace/planningSessionV2.ts | 17 ++- .../tests/appSurface/home.suite.ts | 4 + .../shared-memory/decision-log.md | 9 ++ ...策划会话RuntimeV2接入与旧链路退役-2026-09-03.md | 10 +- 11 files changed, 220 insertions(+), 55 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs index 5ab2adc19..beb4bc9c8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs @@ -252,25 +252,48 @@ fn validate_question_v2(question: &PlanningQuestionV2) -> Result<(), String> { Ok(()) } +fn normalize_v2_decision_state(value: &str) -> Result { + match value.trim() { + "confirmed" => Ok("confirmed".to_string()), + "assumption_pending" => Ok("assumption_pending".to_string()), + "prototype_pending" => Ok("prototype_pending".to_string()), + value => Err(format!( + "PLANNING_INVALID_GDD: decision state 无效:{value}" + )), + } +} + fn normalize_answer_source(state: &str, source: Option) -> Result { - let fallback = match state { + let state = normalize_v2_decision_state(state)?; + let fallback = match state.as_str() { "confirmed" => "user_freeform".to_string(), - _ => "default".to_string(), + "assumption_pending" => "agent_inferred".to_string(), + "prototype_pending" => "user_option".to_string(), + _ => unreachable!("normalize_v2_decision_state returned an unknown state"), }; let source = source .map(|value| value.trim().to_string()) .filter(|value| { matches!( value.as_str(), - "user_option" | "user_freeform" | "user_revision" | "default" + "user_option" | "user_freeform" | "user_revision" | "agent_inferred" ) }) .unwrap_or(fallback); - Ok(source) + Ok(match state.as_str() { + // assumption_pending 的含义就是“不是用户明确决定,而是 Agent 推断”, + // 因此无论模型是否漏填/错填来源,都统一落成 agent_inferred。 + "assumption_pending" => "agent_inferred".to_string(), + // agent_inferred 不能与 confirmed 或 prototype_pending 自相矛盾;这两类 + // 状态分别回退到最接近的用户来源,但不把来源不一致当作阻断错误。 + "confirmed" if source == "agent_inferred" => "user_freeform".to_string(), + "prototype_pending" if source == "agent_inferred" => "user_option".to_string(), + _ => source, + }) } -fn planning_gdd_game_from_input(game: &PlanSubmitGame) -> PlanGddGame { - PlanGddGame { +fn planning_gdd_game_from_input(game: &PlanSubmitGame) -> Result { + Ok(PlanGddGame { title: game.title.clone(), genre: game.genre.clone(), art_style: game.art_style.clone(), @@ -278,32 +301,53 @@ fn planning_gdd_game_from_input(game: &PlanSubmitGame) -> PlanGddGame { pillars: game .pillars .iter() - .map(|value| PlanPillar { - name: value.name.clone(), - player_feel: value.player_feel.clone(), - mechanism: value.mechanism.clone(), - decision_state: value.decision_state.clone(), - basis: None, + .map(|value| { + Ok(PlanPillar { + name: value.name.clone(), + player_feel: value.player_feel.clone(), + mechanism: value.mechanism.clone(), + decision_state: normalize_v2_decision_state(&value.decision_state)?, + basis: None, + }) }) - .collect(), + .collect::, String>>()?, core_loop: game.core_loop.clone(), target_users: game.target_users.clone(), platform_facts: fixed_plan_platform_facts(), mvp_systems: game .mvp_systems .iter() - .map(|value| PlanMvpSystem { - system: value.system.clone(), - minimal_function: value.minimal_function.clone(), - why_required: value.why_required.clone(), - verify_method: value.verify_method.clone(), - decision_state: value.decision_state.clone(), - basis: None, + .map(|value| { + Ok(PlanMvpSystem { + system: value.system.clone(), + minimal_function: value.minimal_function.clone(), + why_required: value.why_required.clone(), + verify_method: value.verify_method.clone(), + decision_state: normalize_v2_decision_state(&value.decision_state)?, + basis: None, + }) }) - .collect(), + .collect::, String>>()?, out_of_scope: game.out_of_scope.clone(), creator_tips: game.creator_tips.clone(), + }) +} + +fn validate_v2_game(game: &PlanGddGame) -> Result<(), String> { + // `validate_plan_game` 属于旧存储模块;V2 先独立校验自己的状态枚举,再用 + // 中性的 confirmed 占位调用旧结构校验器。V2 输入、产物和 Markdown 永远使用 + // assumption_pending,不把 V1 的默认值语义带入 V2。 + let mut storage_compatible = game.clone(); + for pillar in &mut storage_compatible.pillars { + normalize_v2_decision_state(&pillar.decision_state)?; + pillar.decision_state = "confirmed".to_string(); } + for system in &mut storage_compatible.mvp_systems { + normalize_v2_decision_state(&system.decision_state)?; + system.decision_state = "confirmed".to_string(); + } + validate_plan_game(&storage_compatible) + .map_err(|error| format!("PLANNING_INVALID_GDD: {error}")) } fn validate_v2_decisions( @@ -339,14 +383,7 @@ fn validate_v2_decisions( 120, ) .map_err(|error| error.to_string())?; - if !matches!( - decision.state.as_str(), - "confirmed" | "default_pending" | "prototype_pending" - ) { - return Err(format!( - "PLANNING_INVALID_GDD: decisions[{index}].state 无效" - )); - } + normalize_v2_decision_state(&decision.state)?; normalize_answer_source(&decision.state, decision.answer_source.clone())?; validate_text( &decision.answer_summary, @@ -365,7 +402,7 @@ fn validate_v2_decisions( decision.id )); } - if decision.state == "prototype_pending" { + if normalize_v2_decision_state(&decision.state)? == "prototype_pending" { prototype_ids.insert(decision.id.as_str()); } } @@ -414,8 +451,8 @@ fn build_gdd_v2( input: PlanningGddInputV2, ) -> Result { validate_planning_policy_output_v2(&PlanningPolicyOutputV2::Gdd(input.clone()))?; - let game = planning_gdd_game_from_input(&input.game); - validate_plan_game(&game).map_err(|error| format!("PLANNING_INVALID_GDD: {}", error))?; + let game = planning_gdd_game_from_input(&input.game)?; + validate_v2_game(&game)?; let decisions = input .decisions .into_iter() @@ -423,7 +460,7 @@ fn build_gdd_v2( Ok(PlanDecision { id: value.id, topic: value.topic, - state: value.state.clone(), + state: normalize_v2_decision_state(&value.state)?, answer_source: normalize_answer_source(&value.state, value.answer_source)?, round: value.round, answer_summary: value.answer_summary, @@ -458,8 +495,8 @@ pub(crate) fn validate_planning_policy_output_v2( )); } validate_v2_decisions(&input.decisions, &input.prototype_validation_items)?; - let game = planning_gdd_game_from_input(&input.game); - validate_plan_game(&game).map_err(|error| format!("PLANNING_INVALID_GDD: {error}")) + let game = planning_gdd_game_from_input(&input.game)?; + validate_v2_game(&game) } } } @@ -491,7 +528,20 @@ fn validate_gdd_v2(value: &PlanningGddV2, project_id: &str) -> Result<(), String } validate_uuid_prefixed(&value.gdd_id, "gdd-", "gddId").map_err(|error| error.to_string())?; validate_timestamp(&value.created_at_utc, "createdAtUtc").map_err(|error| error.to_string())?; - validate_plan_game(&value.game).map_err(|error| error.to_string())?; + validate_v2_game(&value.game)?; + let decisions = value + .decisions + .iter() + .map(|decision| PlanningGddDecisionInputV2 { + id: decision.id.clone(), + topic: decision.topic.clone(), + state: decision.state.clone(), + answer_source: Some(decision.answer_source.clone()), + round: decision.round, + answer_summary: decision.answer_summary.clone(), + }) + .collect::>(); + validate_v2_decisions(&decisions, &value.prototype_validation_items)?; let expected = fingerprint_gdd_v2(value)?; if expected != value.fingerprint { return Err("PLANNING_INVALID_GDD: GDD fingerprint 不匹配".to_string()); @@ -1113,8 +1163,20 @@ mod tests { "user_freeform" ); assert_eq!( - normalize_answer_source("default_pending", None).unwrap(), - "default" + normalize_answer_source("assumption_pending", None).unwrap(), + "agent_inferred" + ); + assert_eq!( + normalize_answer_source("assumption_pending", Some("user_option".to_string())).unwrap(), + "agent_inferred" + ); + assert_eq!( + normalize_answer_source("prototype_pending", None).unwrap(), + "user_option" + ); + assert_eq!( + normalize_v2_decision_state("default_pending").is_err(), + true ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs index 57ecd9c8d..490f5501d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs @@ -571,11 +571,67 @@ fn build_provider_request_v2( session.question_count ), }; + // `question_limit` 是 Runtime 对“已展示问题数”的硬上限;模型提示词中的 + // “默认最多三轮”只是策略偏好,两者不要求数值一致。这里仅传当前已展示数, + // 不把硬上限数值直接广告给模型;达到硬上限时再明确禁止本次继续提问。 + let gdd_shape = r#" +GDD 输出必须严格使用下面的字段名和层级;不要增加其它字段,尤其不要把 decisions[] 中的字段写成 decision: +{ + "kind": "gdd", + "gdd": { + "schemaVersion": "plan-gdd.v2", + "game": { + "title": "中文标题", + "genre": {"primary": "类型", "fusion": null}, + "artStyle": { + "visualType": "视觉类型", + "keywords": ["关键词"], + "moodAndColor": "氛围与色彩", + "mvpArtBoundary": "MVP 美术边界" + }, + "oneLiner": "一句话概念", + "pillars": [ + {"name": "支柱名称", "playerFeel": "玩家感受", "mechanism": "实现机制", "decisionState": "confirmed"} + ], + "coreLoop": ["核心循环步骤"], + "targetUsers": { + "coreUsers": "核心用户", + "preferences": "用户偏好", + "sessionLength": "单局时长", + "referenceGames": [] + }, + "mvpSystems": [ + {"system": "系统名称", "minimalFunction": "最小功能", "whyRequired": "为什么必须有", "verifyMethod": "验证方式", "decisionState": "confirmed"} + ], + "outOfScope": ["暂不做的内容"], + "creatorTips": { + "doFirst": "先做什么", + "deferForNow": "暂缓什么", + "howToVerify": "如何验证", + "expandWhen": "何时扩展" + } + }, + "decisions": [ + { + "id": "initial-request", + "topic": "初始需求", + "state": "confirmed", + "answerSource": "user_freeform", + "round": 0, + "answerSummary": "用户的初始需求" + } + ], + "prototypeValidationItems": [] + } +} +decisions[] 每项只能有 id、topic、state、answerSource、round、answerSummary;answerSource 可以省略,但不能使用 decision、question、answer 或其它字段。state 只能是 confirmed、assumption_pending、prototype_pending;没有被用户明确决定、由 Agent 根据上下文补出的内容使用 assumption_pending,并将 answerSource 记为 agent_inferred。prototype_pending 决定必须在 prototypeValidationItems 中有同 id 的 question、microPrototype、observation、passCriterion。 +"#; let system = platform_llm::LlmMessage::system(format!( - "你是 Planning Session V2 的立项策划 Agent。当前会话 {},回合 {}。只返回一个合法 JSON object,不要 Markdown 代码围栏、解释文字、Supervisor、子 Agent、委派或验收协议。\n\n{}\n\n如果需要向用户确认,只返回 {{\"kind\":\"question\",\"question\":{{\"id\":\"snake_case_id\",\"header\":\"当前要决定:...\",\"question\":\"...\",\"options\":[{{\"label\":\"方案 A\",\"description\":\"...\"}},{{\"label\":\"方案 B\",\"description\":\"...\"}}]}}}}。如果信息足够或问题数已达到上限,只返回 {{\"kind\":\"gdd\",\"gdd\":{{\"schemaVersion\":\"plan-gdd.v2\",\"game\":{{...}},\"decisions\":[...],\"prototypeValidationItems\":[]}}}}。GDD 必须完整填写游戏、支柱、核心循环、目标用户、MVP 系统、制作边界和创作者提示;不要省略字段。", + "你是 Planning Session V2 的立项策划 Agent。当前会话 {},回合 {}。只返回一个合法 JSON object,不要 Markdown 代码围栏、解释文字、Supervisor、子 Agent、委派或验收协议。\n\n{}\n\n出稿前必须先确认三项核心闭环信息:玩家核心行为(玩家每一局反复做什么)、单局目标/核心循环(怎样算完成一局)、MVP 制作边界(首个可玩版本做什么、不做什么)。只要其中一项仍然只是 Agent 推断、没有出现在用户需求或用户回答中,就继续只问一个最关键的问题,不能用 assumption_pending 代替用户确认;主题包装、美术、数值和次要系统可以先使用 assumption_pending,并将 answerSource 记为 agent_inferred。不要重复已经回答的问题。\n\n如果需要向用户确认,只返回 {{\"kind\":\"question\",\"question\":{{\"id\":\"snake_case_id\",\"header\":\"当前要决定:...\",\"question\":\"...\",\"options\":[{{\"label\":\"方案 A\",\"description\":\"...\"}},{{\"label\":\"方案 B\",\"description\":\"...\"}}]}}}}。如果信息足够或问题数已达到上限,只返回上面完整形状的 {{\"kind\":\"gdd\",\"gdd\":...}}。GDD 必须完整填写所有字段,不要省略字段。\n\n{}", session.session_id, session.turn_index, - question_policy + question_policy, + gdd_shape )); let mut messages = vec![system]; messages.extend(context_messages); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index e5598b9cd..e61bfee65 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -632,7 +632,11 @@ export function App({ savedConversationProjectPathRef.current = localProjectPathRef.current; savedConversationCountRef.current = conversationMessages.length; latestMessagesRef.current = conversationMessages; - } else if (result.result && clientTurnId) { + } else if ( + result.result && + result.result.kind !== 'error' && + clientTurnId + ) { const displayText = planningResultDisplayText( result.result, result.currentArtifact, @@ -1588,7 +1592,16 @@ export function App({ if (!trackedTurn || payload.clientTurnId !== trackedTurn.clientTurnId) { return; } - if (payload.status === 'started' || payload.status === 'delta') { + if (payload.status === 'started') { + setPlanningV2TransientReply( + payload.accumulatedText || '正在生成策划方案…', + ); + setProjectSupervisorRuntimeError(''); + } + if ( + payload.status === 'delta' && + (payload.accumulatedText || payload.deltaText) + ) { setPlanningV2TransientReply( payload.accumulatedText || payload.deltaText, ); diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index a8d3a729f..c1c15cb76 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -198,6 +198,7 @@ export interface PlanGddStateViewV1 { decisionStateCounts: { confirmed: number; defaultPending: number; + assumptionPending?: number; prototypePending: number; }; } | null; @@ -283,11 +284,16 @@ export interface PlanGddStateViewV1 { decisions: Array<{ id: string; topic: string; - state: 'confirmed' | 'default_pending' | 'prototype_pending'; + state: + | 'confirmed' + | 'assumption_pending' + | 'default_pending' + | 'prototype_pending'; answerSource: | 'user_option' | 'user_freeform' | 'user_revision' + | 'agent_inferred' | 'default'; round: number; answerSummary: string; diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index d1c3be699..4e4746444 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -2097,6 +2097,13 @@ export function projectRuntimeVisibleError( ) { return `${subject} 保存运行记录失败,请检查项目目录后重试`; } + if ( + normalized.includes('planning_invalid') || + normalized.includes('gdd 结构无效') || + normalized.includes('策划输出格式') + ) { + return `${subject} 输出格式不符合当前 GDD 结构,请重试`; + } const containsInternalDiagnostics = normalized.includes('agentllm.') || /(?:^|[\s::])kind=/.test(normalized) || diff --git a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts index da4bbd7b2..99ec9ac5d 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts @@ -574,7 +574,9 @@ export function useHomeProjectCreation({ startMode: ProjectStartMode, ) { return createHomeDraftAutomaticallyWithOptions(draft, startMode, { - suggestName: true, + // 做方案的首轮还要调用一次策划 Provider;项目名称不是策划输入的 + // 前置条件,避免在进入工作区前再额外等待一次模型请求。 + suggestName: startMode !== 'planning', }); } diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx index 4049914b1..5deaaae93 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx @@ -260,6 +260,7 @@ export function PlanGddStageProgress({ const decisionStateLabels = { confirmed: '已确认', + assumption_pending: 'Agent 推断,待确认', default_pending: '待确认默认项', prototype_pending: '待原型验证', }; diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts b/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts index 7e837fee3..c3b867011 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts @@ -109,13 +109,12 @@ export type PlanningGddPayloadV2 = { decisions: Array<{ id: string; topic: string; - state: 'confirmed' | 'default_pending' | 'prototype_pending' | string; + state: 'confirmed' | 'assumption_pending' | 'prototype_pending'; answerSource: | 'user_option' | 'user_freeform' | 'user_revision' - | 'default' - | string; + | 'agent_inferred'; round: number; answerSummary: string; basis: null; @@ -312,7 +311,8 @@ export function planningMessagesToChatMessages( ( message, ): message is PlanningMessageV2 & { role: 'user' | 'assistant' } => - message.role === 'user' || message.role === 'assistant', + (message.role === 'user' || message.role === 'assistant') && + message.kind !== 'error', ) .map((message) => { const text = messageDisplayText(message); @@ -392,13 +392,13 @@ function gddDisplayFromArtifact(artifact: PlanningArtifactV2) { ...decision, state: decision.state as | 'confirmed' - | 'default_pending' + | 'assumption_pending' | 'prototype_pending', answerSource: decision.answerSource as | 'user_option' | 'user_freeform' | 'user_revision' - | 'default', + | 'agent_inferred', })), prototypeValidationItems: payload.prototypeValidationItems, } satisfies NonNullable; @@ -438,9 +438,10 @@ export function planningSessionToPlanGddState( displayGdd?.decisions.filter( (decision) => decision.state === 'confirmed', ).length ?? 0, - defaultPending: + defaultPending: 0, + assumptionPending: displayGdd?.decisions.filter( - (decision) => decision.state === 'default_pending', + (decision) => decision.state === 'assumption_pending', ).length ?? 0, prototypePending: displayGdd?.decisions.filter( diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 5493fb6cf..91446d49a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -1806,6 +1806,10 @@ export function registerHomeProjectCreationTests() { 'chat_with_game_creator_agent', expect.anything(), ); + expect(invoke).not.toHaveBeenCalledWith( + 'suggest_automatic_project_name', + expect.anything(), + ); expect( invoke.mock.calls.filter( ([command]) => command === 'create_automatic_local_game_project', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 293d9b841..23ed380ad 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -26,6 +26,15 @@ - 验证方式:按 `docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md` 的 P0~P5 阶段验收执行;至少覆盖第 8 个问题、上限后 question 抑制、Provider 失败、非法输出、批准/修改/退回、重启恢复、旧会话切换强制失败、迟到 Provider 结果丢弃和当前空能力快照。 - 关联文档:`docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 +## 2026-09-03 PlanningSessionRuntime V2 统一 Agent 推断语义 + +- 背景:旧 V1 使用 `default_pending` / `answerSource=default` 表示未提问时由 Agent 按默认建议补齐的字段;该语义会让 V2 的 Agent 推断看起来像产品默认值,也会造成原型与生产字段不一致。 +- 决策:V2 只使用 `confirmed`、`assumption_pending`、`prototype_pending` 三种决定状态;`assumption_pending` 的来源统一为 `agent_inferred`。`answerSource` 仍不是独立阻断项,缺失或不一致时按状态归一为 `user_freeform`、`user_option` 或 `agent_inferred`。V1 的 `default_pending` / `default` 校验和历史数据保持不动,不作为 V2 合同的一部分。 +- 问询策略:V2 出稿前必须确认玩家核心行为、单局目标/核心循环、MVP 制作边界;其中任一仅由 Agent 推断时继续问一个关键问题。`questionLimit` 是 Runtime 对已展示问题数的硬上限,提示词中的“默认最多三轮”只是策略偏好,不要求与硬上限数值一致。 +- 影响范围:V2 GDD 输入/产物、Provider system prompt、前端 V2 类型与决定状态展示;旧 Supervisor/V1 存储、校验和历史产物不变。 +- 验证方式:V2 解析 `assumption_pending` 不报错并落盘为 `assumption_pending/agent_inferred`;`default_pending` 不作为 V2 合法状态;核心三项未确认时提示词要求继续问询;相关 Rust/TS 定向测试、类型和编码检查通过。 +- 关联文档:`docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs`。 + ## 2026-09-03 AGC 登录 route event 使用 handler 已验证主体归属 - 背景:登录请求进入时尚未拥有 `AuthenticatedAccessToken`,通用 tracking middleware 无法从响应 extensions 归属登录成功用户;将 AGC marker 直接写入按用户/业务日幂等的 `daily_login` 又会受到不同来源登录顺序影响。 diff --git a/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md b/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md index 543987726..4414d8c8f 100644 --- a/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md +++ b/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md @@ -1,7 +1,7 @@ # 策划会话 Runtime V2 接入与旧链路退役方案 - 日期:2026-09-03 -- 状态:P0 合同冻结、P1 内核和 P2 产物闭环核心已完成;P3 入口与 UI 接入实施中;P4~P5 待实施;本文是新生产实现的目标方案与阶段验收合同 +- 状态:P0 合同冻结、P1 内核、P2 产物闭环和 P3 入口/UI 接入已完成;P4~P5 待实施;本文是新生产实现的目标方案与阶段验收合同 - 适用范围:AGC 桌面 App 的“做方案”入口、策划会话、GDD 产物与审批 > 本文规定新策划 Agent 的生产接入和旧链路退役方式。P3 开始修改正式 AGC 入口与工作台,但旧 `project-supervisor-plan` / `project-planning` 源码仍保留,直到 P5 完成退役;V2 切换时旧链路直接封存,所有未完成旧会话强制失败,旧 Fast GDD 文档之后只作为历史记录依据。 @@ -277,6 +277,8 @@ Provider 适配层输出 `planning-turn-result.v2`: 当前策略只要求 `options` 为 2~4 项、label/description 非空;不要求 A/B/“需要原型验证”固定顺序,也不把 `answerSource` 作为阻断条件。`question.id` 只作为当前问题标识,用户回答必须绑定当前 question 的 id。 +V2 决定状态使用 `confirmed | assumption_pending | prototype_pending`:`assumption_pending` 专门表示 Agent 根据上下文补出的、尚未被用户明确决定的内容,来源使用 `answerSource=agent_inferred`;`prototype_pending` 表示需要通过原型验证的决定,通常来源为用户选项或用户修改。V1 的 `default_pending` / `answerSource=default` 不属于 V2 语义。 + `artifact` payload 冻结为通用产物包: ```json @@ -656,7 +658,7 @@ hydrate_planning_session_v2 状态:核心实现已落地(2026-09-03)。当前已支持 question/GDD 解析、最多 8 个有效问题、达到上限后的单次强制出稿、`plan-gdd.v2` 版本文件、`game/fast_gdd.md` 投影和 `plan-approval.v2` 审批记录;审批修改后的下一轮仍由同一 V2 Session 继续。策略校验完成前的流式内容只在回合成功后对外转发,达到上限而被丢弃的 question、非法输出和重试内容不会泄露给调用方。等待审批时不能直接提交新的策划输入。P3 仍需把正式“做方案”入口和现有 UI 切到这些 command。 -已落地入口:`decide_planning_artifact_v2`。`answerSource` 缺失或未知值按当前状态回退为 `user_freeform` / `default`,不作为单独阻断项;GDD 结构、必填业务字段、版本、指纹和当前项目身份仍必须合法。 +已落地入口:`decide_planning_artifact_v2`。`answerSource` 缺失或未知值按当前状态回退为 `user_freeform` / `user_option` / `agent_inferred`,不作为单独阻断项;GDD 结构、必填业务字段、版本、指纹和当前项目身份仍必须合法。V2 不接受或生成 V1 的 `default_pending` / `default` 语义。 目标:把新版原型的策划行为落到生产 V2,不把旧 Supervisor 协议带回来。 @@ -713,7 +715,9 @@ hydrate_planning_session_v2 - 前端通过 V2 适配层复用现有聊天区、澄清卡、GDD 审批卡和阶段进度条;V2 Session 的 hydrate 结果额外携带 `conversation`,用于刷新/重启恢复历史消息。 - 监听 `planning-session-v2-stream`,将 Provider 回合的增量投影到现有实时回复区域;旧 Runtime 轮询、专业 Agent 轮询和旧 Runtime 事件在 V2 策划会话中关闭。 - 对已存在 V2 Session 的项目,打开项目时先 hydrate V2;没有 V2 authority 的旧项目继续走旧读取路径,避免误把旧项目数据当成 V2。 -- P3 尚未完成旧会话 `legacy_retired` 封存、入口彻底关闭和真实 Provider/UI 全链路回归,这些仍属于 P4/P5。 +- P3 已完成;旧会话 `legacy_retired` 封存、入口彻底关闭和真实 Provider/UI 全链路回归仍属于 P4/P5。 + +P3 首轮人工测试暴露的问题已在进入 P4 前修正:做方案创建工作区不再额外调用自动项目命名 Provider;策划等待态立即显示处理中提示;V2 GDD 提示词明确给出完整嵌套字段和 `decisions[]` 契约;失败结果不重复渲染,GDD 结构错误给出可操作的重试提示。严格解析和失败不落盘成功产物的规则保持不变。 ### P4:灰度、真实 Provider 与回归验收