完善策划 V2 人工测试与推断语义
修复做方案首轮额外命名等待、处理中反馈和重复错误展示 补全 GDD 输出字段提示,避免模型生成非法 decisions 字段 统一 V2 assumption_pending 与 agent_inferred 语义并同步核心闭环问询策略 保持旧 Supervisor/V1 的 default_pending 与 default 语义不变 更新 Runtime V2 技术方案和项目决策记录
This commit is contained in:
+100
-38
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+58
-2
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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/【技术方案】立项策划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` 又会受到不同来源登录顺序影响。
|
||||
|
||||
@@ -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 与回归验收
|
||||
|
||||
|
||||
Reference in New Issue
Block a user