完善策划 V2 人工测试与推断语义

修复做方案首轮额外命名等待、处理中反馈和重复错误展示

补全 GDD 输出字段提示,避免模型生成非法 decisions 字段

统一 V2 assumption_pending 与 agent_inferred 语义并同步核心闭环问询策略

保持旧 Supervisor/V1 的 default_pending 与 default 语义不变

更新 Runtime V2 技术方案和项目决策记录
This commit is contained in:
2026-09-03 13:15:14 +00:00
parent 64105f8259
commit f35ec812d9
11 changed files with 220 additions and 55 deletions
@@ -252,25 +252,48 @@ fn validate_question_v2(question: &PlanningQuestionV2) -> Result<(), String> {
Ok(())
}
fn normalize_v2_decision_state(value: &str) -> Result<String, String> {
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<String>) -> Result<String, String> {
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<PlanGddGame, String> {
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::<Result<Vec<_>, 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::<Result<Vec<_>, 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<PlanningGddV2, String> {
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::<Vec<_>>();
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
);
}
@@ -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、answerSummaryanswerSource 可以省略,但不能使用 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);
+15 -2
View File
@@ -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,
);
+7 -1
View File
@@ -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;
@@ -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) ||
@@ -574,7 +574,9 @@ export function useHomeProjectCreation({
startMode: ProjectStartMode,
) {
return createHomeDraftAutomaticallyWithOptions(draft, startMode, {
suggestName: true,
// 做方案的首轮还要调用一次策划 Provider;项目名称不是策划输入的
// 前置条件,避免在进入工作区前再额外等待一次模型请求。
suggestName: startMode !== 'planning',
});
}
@@ -260,6 +260,7 @@ export function PlanGddStageProgress({
const decisionStateLabels = {
confirmed: '已确认',
assumption_pending: 'Agent 推断,待确认',
default_pending: '待确认默认项',
prototype_pending: '待原型验证',
};
@@ -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<PlanGddStateViewV1['displayGdd']>;
@@ -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(
@@ -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',
@@ -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/【技术方案】立项策划AgentFast 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` 又会受到不同来源登录顺序影响。
@@ -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` 为 24 项、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 与回归验收