接入策划会话 Runtime V2 入口与界面
做方案入口改用 start/continue/decide_planning_session_v2 命令 新增 V2 Session、问题、GDD 和审批结果的前端适配层 接通 hydrate 会话历史、流式回复、问询卡、审批卡和处理耗时展示 保持做游戏、做素材及无 V2 旧项目读取路径不变 同步策划入口回归测试、Runtime V2 技术方案和项目决策记录
This commit is contained in:
@@ -131,6 +131,8 @@ pub(crate) struct PlanningSessionCommandResultV2 {
|
||||
pub result: Option<PlanningTurnResultV2>,
|
||||
pub current_artifact: Option<Value>,
|
||||
pub replayed: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation: Option<Vec<PlanningMessageV2>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -705,6 +707,7 @@ where
|
||||
result: Some(result),
|
||||
current_artifact: current_planning_artifact_v2(root)?,
|
||||
replayed: true,
|
||||
conversation: None,
|
||||
});
|
||||
}
|
||||
let session_id = start.session.session_id.clone();
|
||||
@@ -765,6 +768,7 @@ where
|
||||
}),
|
||||
current_artifact: current_planning_artifact_v2(root)?,
|
||||
replayed: false,
|
||||
conversation: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -828,6 +832,7 @@ where
|
||||
}),
|
||||
current_artifact: current_planning_artifact_v2(root)?,
|
||||
replayed: false,
|
||||
conversation: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -878,6 +883,7 @@ where
|
||||
result: Some(persisted.result),
|
||||
current_artifact: persisted.current_artifact,
|
||||
replayed: false,
|
||||
conversation: None,
|
||||
})
|
||||
}
|
||||
Err(detail) => {
|
||||
@@ -909,6 +915,7 @@ where
|
||||
}),
|
||||
current_artifact: current_planning_artifact_v2(root)?,
|
||||
replayed: false,
|
||||
conversation: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1021,6 +1028,7 @@ pub(crate) fn hydrate_planning_session_v2(
|
||||
result: None,
|
||||
current_artifact: current_planning_artifact_v2(&root)?,
|
||||
replayed: false,
|
||||
conversation: Some(read_planning_messages_v2(&root)?),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -185,6 +185,7 @@ export interface PlanGddStateViewV1 {
|
||||
| 'rejected'
|
||||
| 'recovery_required';
|
||||
clarificationRound: number;
|
||||
questionLimit?: number | null;
|
||||
repairDepth: number;
|
||||
accumulatedAgentMillis: number;
|
||||
activeRunId: string | null;
|
||||
|
||||
@@ -141,14 +141,16 @@ export function PlanGddStageProgress({
|
||||
state.versions[state.versions.length - 1]?.gddRef.version ??
|
||||
null;
|
||||
const answeredRounds = state.session?.clarificationRound ?? 0;
|
||||
// `clarificationRound` 是 0-indexed 的「已答轮数」:`static_delegate_lineage_counters`
|
||||
// 排除目标自身、只数祖先里的澄清跳数,后端判上限用的也是 `current_round + 1`。所以直接
|
||||
// 按「轮次 X/3」渲染会整体差一格——问最后一轮时显示「轮次 2/3」,字面暗示还剩一轮。
|
||||
// 等待回答时 `latestDelegationId` 就是当前那条 delivery,+1 恰好是正在问的轮次;其余
|
||||
// 状态(含 `awaitingAnswerFor` 为 null 的恢复态)退回「已完成」表述,不去猜当前轮。
|
||||
const questionLimit = state.session?.questionLimit ?? 3;
|
||||
const questionLimitLabel =
|
||||
questionLimit === null ? '不限' : String(questionLimit);
|
||||
// `clarificationRound` 是已完成的问询数;等待回答时加一表示当前正在展示的问题,
|
||||
// 避免把「已答 N 个」误显示成当前第 N 轮。旧状态没有 questionLimit 时沿用旧 UI 的 3 轮
|
||||
// 文案,V2 状态直接使用会话快照里的策略值。
|
||||
const roundLabel = state.session?.awaitingAnswerFor
|
||||
? `第 ${answeredRounds + 1} 轮 / 共 3 轮`
|
||||
: `已完成 ${answeredRounds}/3 轮澄清`;
|
||||
? `第 ${answeredRounds + 1} 轮 / 共 ${questionLimitLabel} 轮`
|
||||
: `已完成 ${answeredRounds}/${questionLimitLabel} 轮澄清`;
|
||||
const processingSeconds = (state.session?.accumulatedAgentMillis ?? 0) / 1000;
|
||||
// 批准之后审批卡整张收掉,交付出口就落在这条标题栏上:GDD 的 Markdown 已经在项目
|
||||
// 里(提交时渲染、审批回执重渲染带上 approved 头),这里只是把它指出来并交给系统
|
||||
// 打开。恢复态不给出口——那时权威投影还没收敛,路径上的内容可能不是用户批的那版。
|
||||
@@ -170,6 +172,9 @@ export function PlanGddStageProgress({
|
||||
? '当前版本:草稿'
|
||||
: `当前版本:v${latestVersion}`}
|
||||
</span>
|
||||
{processingSeconds > 0 ? (
|
||||
<span>{`处理耗时:${processingSeconds.toFixed(1)} 秒`}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{deliveredGdd ? (
|
||||
<div
|
||||
|
||||
@@ -63,6 +63,7 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
planGddHydrateBusy: boolean;
|
||||
planGddDecisionBusy: boolean;
|
||||
planGddError: string | null;
|
||||
planningLane?: boolean;
|
||||
onPlanGddRefresh: () => void;
|
||||
onPlanGddDecision: (
|
||||
action: PlanGddDecisionAction,
|
||||
@@ -98,11 +99,14 @@ export function ProjectSupervisorView({
|
||||
planGddHydrateBusy,
|
||||
planGddDecisionBusy,
|
||||
planGddError,
|
||||
planningLane = false,
|
||||
onPlanGddRefresh,
|
||||
onPlanGddDecision,
|
||||
onMakeGameFromApprovedGdd,
|
||||
...runtimePanelProps
|
||||
}: ProjectSupervisorViewProps) {
|
||||
const planningSurfaceActive =
|
||||
planningLane || isPlanningLaneRuntime(runtimePanelProps.runtime);
|
||||
const submitLabel = needsUserInput
|
||||
? '等待回答'
|
||||
: runtimePanelProps.controlBusy
|
||||
@@ -117,7 +121,7 @@ export function ProjectSupervisorView({
|
||||
<div className="project-supervisor-conversation">
|
||||
<PlanGddSurface
|
||||
state={planGddState}
|
||||
active={isPlanningLaneRuntime(runtimePanelProps.runtime)}
|
||||
active={planningSurfaceActive}
|
||||
projectPath={projectPath}
|
||||
hydrateBusy={planGddHydrateBusy}
|
||||
decisionBusy={planGddDecisionBusy}
|
||||
@@ -181,9 +185,7 @@ export function ProjectSupervisorView({
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{directCodex ? null : isPlanningLaneRuntime(
|
||||
runtimePanelProps.runtime,
|
||||
) ? (
|
||||
{directCodex ? null : planningSurfaceActive ? (
|
||||
<PlanningLaneRuntimeStrip
|
||||
runtime={runtimePanelProps.runtime}
|
||||
error={runtimePanelProps.error}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -458,6 +458,8 @@ function createProjectSupervisorRuntimeHarness({
|
||||
initialProjectRevision = 0,
|
||||
runtimeMapLoader,
|
||||
expectedRunProfile = 'autonomous-game-build',
|
||||
planningV2Result = null,
|
||||
planningV2StartResult = null,
|
||||
}: {
|
||||
projectPath?: string;
|
||||
sessionId?: string;
|
||||
@@ -469,6 +471,8 @@ function createProjectSupervisorRuntimeHarness({
|
||||
initialProjectRevision?: number;
|
||||
runtimeMapLoader?: () => Promise<Array<Record<string, unknown>>>;
|
||||
expectedRunProfile?: 'standard' | 'autonomous-game-build';
|
||||
planningV2Result?: Record<string, unknown> | null;
|
||||
planningV2StartResult?: Record<string, unknown> | null;
|
||||
} = {}) {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
@@ -516,6 +520,8 @@ function createProjectSupervisorRuntimeHarness({
|
||||
let currentPlanGddState: PlanGddStateViewV1 | null = null;
|
||||
let planGddDecisionError: string | null = null;
|
||||
let planGddHydrateCount = 0;
|
||||
let currentPlanningV2Result = planningV2Result;
|
||||
let currentPlanningV2StartResult = planningV2StartResult;
|
||||
const planGddDecisionCalls: Array<Record<string, unknown>> = [];
|
||||
let runtimeUpdateHandler:
|
||||
| ((event: {
|
||||
@@ -809,6 +815,29 @@ function createProjectSupervisorRuntimeHarness({
|
||||
}
|
||||
return currentPlanGddState;
|
||||
}
|
||||
if (command === 'hydrate_planning_session_v2') {
|
||||
return currentPlanningV2Result;
|
||||
}
|
||||
if (
|
||||
command === 'start_planning_session_v2' ||
|
||||
command === 'continue_planning_session_v2'
|
||||
) {
|
||||
const nextResult =
|
||||
command === 'start_planning_session_v2'
|
||||
? (currentPlanningV2StartResult ?? currentPlanningV2Result)
|
||||
: currentPlanningV2Result;
|
||||
if (!nextResult) {
|
||||
throw new Error('PLANNING_V2_RESULT_NOT_CONFIGURED');
|
||||
}
|
||||
currentPlanningV2Result = nextResult;
|
||||
return nextResult;
|
||||
}
|
||||
if (command === 'decide_planning_artifact_v2') {
|
||||
if (!currentPlanningV2Result) {
|
||||
throw new Error('PLANNING_V2_RESULT_NOT_CONFIGURED');
|
||||
}
|
||||
return currentPlanningV2Result;
|
||||
}
|
||||
if (command === 'decide_game_creator_plan_gdd') {
|
||||
planGddDecisionCalls.push({ ...(args ?? {}) });
|
||||
if (planGddDecisionError) {
|
||||
@@ -885,6 +914,12 @@ function createProjectSupervisorRuntimeHarness({
|
||||
setPlanGddState(state: PlanGddStateViewV1 | null) {
|
||||
currentPlanGddState = state;
|
||||
},
|
||||
setPlanningV2Result(state: Record<string, unknown> | null) {
|
||||
currentPlanningV2Result = state;
|
||||
},
|
||||
setPlanningV2StartResult(state: Record<string, unknown> | null) {
|
||||
currentPlanningV2StartResult = state;
|
||||
},
|
||||
failNextPlanGddDecision(message: string) {
|
||||
planGddDecisionError = message;
|
||||
},
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { PROJECT_SUPERVISOR_PLAN_SOURCE } from '../../src/app/constants';
|
||||
import type { ProjectSupervisorComponentProps } from '../../src/features/app-shell/model';
|
||||
import { useHomeProjectCreation } from '../../src/features/app-shell/useHomeProjectCreation';
|
||||
import { WorkspaceLauncherShell } from '../../src/features/app-shell/WorkspaceLauncher';
|
||||
import type { LauncherView } from '../../src/view/layout';
|
||||
import {
|
||||
act,
|
||||
agentRuntimeUserInputRequest,
|
||||
App,
|
||||
cleanup,
|
||||
createGameCreationAppManifest,
|
||||
@@ -29,6 +27,33 @@ import {
|
||||
within,
|
||||
} from './harness';
|
||||
|
||||
function emptyPlanningV2StartResult(projectId = 'local-project-draft') {
|
||||
return {
|
||||
session: {
|
||||
schemaVersion: 'planning-session.v2',
|
||||
engine: 'planning-session-v2',
|
||||
sessionId: 'home-planning-v2-session',
|
||||
projectId,
|
||||
mode: 'gdd',
|
||||
status: 'planning',
|
||||
turnIndex: 1,
|
||||
questionCount: 0,
|
||||
questionLimit: 8,
|
||||
revisionCount: 0,
|
||||
currentArtifactVersion: null,
|
||||
currentQuestion: null,
|
||||
capabilities: { tools: [], skills: [] },
|
||||
processingSeconds: 0.5,
|
||||
createdAtUtc: '2026-09-03T00:00:00Z',
|
||||
updatedAtUtc: '2026-09-03T00:00:01Z',
|
||||
lastError: null,
|
||||
},
|
||||
result: null,
|
||||
currentArtifact: null,
|
||||
replayed: false,
|
||||
};
|
||||
}
|
||||
|
||||
function ApprovedGddStartHarness() {
|
||||
const [, setLauncherView] = React.useState<LauncherView>(
|
||||
'project-development',
|
||||
@@ -1700,11 +1725,11 @@ export function registerHomeProjectCreationTests() {
|
||||
});
|
||||
|
||||
it.each([
|
||||
['做方案', false, 'standard', PROJECT_SUPERVISOR_PLAN_SOURCE],
|
||||
['做方案', true, 'standard', PROJECT_SUPERVISOR_PLAN_SOURCE],
|
||||
['做方案', false],
|
||||
['做方案', true],
|
||||
] as const)(
|
||||
'routes %s %s creation to the expected root run',
|
||||
async (modeLabel, automatic, runProfile, source) => {
|
||||
'routes %s %s creation to Planning Session V2',
|
||||
async (modeLabel, automatic) => {
|
||||
const projectPath = `/tmp/home-${modeLabel}-${automatic ? 'enter' : 'submit'}`;
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
@@ -1712,7 +1737,7 @@ export function registerHomeProjectCreationTests() {
|
||||
);
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
expectedRunProfile: runProfile,
|
||||
planningV2StartResult: emptyPlanningV2StartResult(),
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
@@ -1752,29 +1777,23 @@ export function registerHomeProjectCreationTests() {
|
||||
} else {
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: source ? '进入立项策划' : '开启创作',
|
||||
name: '进入立项策划',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'start_game_creator_supervisor_runtime_task',
|
||||
'start_planning_session_v2',
|
||||
expect.objectContaining({
|
||||
projectPath,
|
||||
runProfile,
|
||||
...(source ? { source } : {}),
|
||||
mode: 'gdd',
|
||||
}),
|
||||
);
|
||||
});
|
||||
const startCall = invoke.mock.calls.find(
|
||||
([command]) => command === 'start_game_creator_supervisor_runtime_task',
|
||||
([command]) => command === 'start_planning_session_v2',
|
||||
);
|
||||
if (source) {
|
||||
expect(startCall?.[1]).toMatchObject({ source });
|
||||
} else {
|
||||
expect(startCall?.[1]).not.toHaveProperty('source');
|
||||
}
|
||||
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
||||
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
|
||||
'本轮用户附件',
|
||||
@@ -1803,7 +1822,9 @@ export function registerHomeProjectCreationTests() {
|
||||
);
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
expectedRunProfile: 'standard',
|
||||
planningV2StartResult: emptyPlanningV2StartResult(
|
||||
'home-planning-attachment',
|
||||
),
|
||||
});
|
||||
const fileBytes = Array.from(new TextEncoder().encode('png'));
|
||||
const invoke = vi.fn(
|
||||
@@ -1856,10 +1877,10 @@ export function registerHomeProjectCreationTests() {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'start_game_creator_supervisor_runtime_task',
|
||||
'start_planning_session_v2',
|
||||
expect.objectContaining({
|
||||
projectPath,
|
||||
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
|
||||
mode: 'gdd',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1870,7 +1891,7 @@ export function registerHomeProjectCreationTests() {
|
||||
bytes: fileBytes,
|
||||
});
|
||||
const startCall = invoke.mock.calls.find(
|
||||
([command]) => command === 'start_game_creator_supervisor_runtime_task',
|
||||
([command]) => command === 'start_planning_session_v2',
|
||||
);
|
||||
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
||||
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain('本轮用户附件');
|
||||
@@ -1901,7 +1922,52 @@ export function registerHomeProjectCreationTests() {
|
||||
projectPath,
|
||||
expectedRunProfile: 'standard',
|
||||
});
|
||||
let planRootRunId = '';
|
||||
supervisorHarness.setPlanningV2StartResult({
|
||||
session: {
|
||||
schemaVersion: 'planning-session.v2',
|
||||
engine: 'planning-session-v2',
|
||||
sessionId: 'home-planning-v2-session',
|
||||
projectId: 'local-project-draft',
|
||||
mode: 'gdd',
|
||||
status: 'awaiting_user',
|
||||
turnIndex: 1,
|
||||
questionCount: 1,
|
||||
questionLimit: 8,
|
||||
revisionCount: 0,
|
||||
currentArtifactVersion: null,
|
||||
currentQuestion: {
|
||||
id: 'visual_direction',
|
||||
header: '当前要决定:首版美术方向',
|
||||
question: '首版角色规范图采用哪种美术方向?',
|
||||
options: [
|
||||
{ label: '像素', description: '低成本像素风。' },
|
||||
{ label: '扁平', description: '清晰的扁平插画风。' },
|
||||
],
|
||||
},
|
||||
capabilities: { tools: [], skills: [] },
|
||||
processingSeconds: 1,
|
||||
createdAtUtc: '2026-09-03T00:00:00Z',
|
||||
updatedAtUtc: '2026-09-03T00:00:01Z',
|
||||
lastError: null,
|
||||
},
|
||||
result: {
|
||||
schemaVersion: 'planning-turn-result.v2',
|
||||
kind: 'question',
|
||||
payload: {
|
||||
question: {
|
||||
id: 'visual_direction',
|
||||
header: '当前要决定:首版美术方向',
|
||||
question: '首版角色规范图采用哪种美术方向?',
|
||||
options: [
|
||||
{ label: '像素', description: '低成本像素风。' },
|
||||
{ label: '扁平', description: '清晰的扁平插画风。' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
currentArtifact: null,
|
||||
replayed: false,
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'pick_local_project_directory') {
|
||||
@@ -1917,33 +1983,7 @@ export function registerHomeProjectCreationTests() {
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'start_game_creator_supervisor_runtime_task') {
|
||||
planRootRunId = String(args?.runId ?? '');
|
||||
}
|
||||
const result = await supervisorHarness.invoke(command, args);
|
||||
if (command !== 'read_game_creator_agent_runtime' || !planRootRunId) {
|
||||
return result;
|
||||
}
|
||||
// 后端此刻的真实形态:pending 是 user.input_request,读命令把澄清请求投影在
|
||||
// 结果的**顶层**(`AgentRuntimeResult.user_input_request`,与 `state` 平级),
|
||||
// 前端的 agentRuntimeStateFromResult 也优先读顶层。放进 state 会被顶层的 null
|
||||
// 盖掉,那是 fixture 写错,不是产品缺陷。
|
||||
const runtimeResult = result as { state: Record<string, unknown> };
|
||||
return {
|
||||
...runtimeResult,
|
||||
state: {
|
||||
...runtimeResult.state,
|
||||
status: 'waiting-for-user-input',
|
||||
phase: 'waiting-for-user-input',
|
||||
},
|
||||
userInputRequest: agentRuntimeUserInputRequest({
|
||||
agentId: 'project-supervisor',
|
||||
sessionId: supervisorHarness.sessionId,
|
||||
runId: planRootRunId,
|
||||
requestId: 'request-plan-round-1',
|
||||
actionId: 'action-plan-round-1',
|
||||
}),
|
||||
};
|
||||
return supervisorHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
@@ -1963,8 +2003,8 @@ export function registerHomeProjectCreationTests() {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'start_game_creator_supervisor_runtime_task',
|
||||
expect.objectContaining({ source: PROJECT_SUPERVISOR_PLAN_SOURCE }),
|
||||
'start_planning_session_v2',
|
||||
expect.objectContaining({ prompt: '2D射击游戏', mode: 'gdd' }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { resolve } from 'node:path';
|
||||
|
||||
import { PlanGddStageProgress } from '../../src/features/project-workspace/GddApprovalCard';
|
||||
import {
|
||||
agentRuntimeUserInputRequest,
|
||||
App,
|
||||
createPlanGddStateView,
|
||||
createProjectSupervisorRuntimeHarness,
|
||||
@@ -20,37 +19,140 @@ import {
|
||||
|
||||
function mountFormalSupervisor(
|
||||
harness: ReturnType<typeof createProjectSupervisorRuntimeHarness>,
|
||||
planningStartMode = false,
|
||||
) {
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
orchestrationMode: 'single-supervisor',
|
||||
planningStartMode: true,
|
||||
planningStartMode,
|
||||
projectSupervisorOnly: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function planningV2ApprovalResult() {
|
||||
const state = createPlanGddStateView();
|
||||
const displayGdd = state.displayGdd;
|
||||
const session = state.session;
|
||||
if (!displayGdd || !session) {
|
||||
throw new Error('fixture 应当带 GDD 和 session');
|
||||
}
|
||||
return {
|
||||
session: {
|
||||
schemaVersion: 'planning-session.v2',
|
||||
engine: 'planning-session-v2',
|
||||
sessionId: session.sessionId,
|
||||
projectId: state.projectId,
|
||||
mode: 'gdd',
|
||||
status: 'awaiting_approval',
|
||||
turnIndex: 3,
|
||||
questionCount: session.clarificationRound,
|
||||
questionLimit: 8,
|
||||
revisionCount: 0,
|
||||
currentArtifactVersion: displayGdd.version,
|
||||
currentQuestion: null,
|
||||
capabilities: { tools: [], skills: [] },
|
||||
processingSeconds: session.accumulatedAgentMillis / 1000,
|
||||
createdAtUtc: displayGdd.createdAtUtc,
|
||||
updatedAtUtc: displayGdd.createdAtUtc,
|
||||
lastError: null,
|
||||
},
|
||||
result: null,
|
||||
currentArtifact: {
|
||||
artifactId: displayGdd.gddId,
|
||||
kind: 'gdd',
|
||||
version: displayGdd.version,
|
||||
status: 'ready_for_approval',
|
||||
fingerprint: displayGdd.fingerprint,
|
||||
payload: {
|
||||
schemaVersion: 'plan-gdd.v2',
|
||||
projectId: displayGdd.projectId,
|
||||
gddId: displayGdd.gddId,
|
||||
version: displayGdd.version,
|
||||
createdAtUtc: displayGdd.createdAtUtc,
|
||||
game: displayGdd.game,
|
||||
decisions: displayGdd.decisions,
|
||||
prototypeValidationItems: displayGdd.prototypeValidationItems,
|
||||
fingerprint: displayGdd.fingerprint,
|
||||
},
|
||||
},
|
||||
replayed: false,
|
||||
conversation: [],
|
||||
};
|
||||
}
|
||||
|
||||
function planningV2QuestionResult() {
|
||||
return {
|
||||
session: {
|
||||
schemaVersion: 'planning-session.v2',
|
||||
engine: 'planning-session-v2',
|
||||
sessionId: 'plan-session-v2-question',
|
||||
projectId: 'local-project-draft',
|
||||
mode: 'gdd',
|
||||
status: 'awaiting_user',
|
||||
turnIndex: 1,
|
||||
questionCount: 1,
|
||||
questionLimit: 8,
|
||||
revisionCount: 0,
|
||||
currentArtifactVersion: null,
|
||||
currentQuestion: {
|
||||
id: 'core_loop',
|
||||
header: '当前要决定:核心循环',
|
||||
question: '玩家在一局中主要反复做什么?',
|
||||
options: [
|
||||
{ label: '持续闪避', description: '保持移动并躲避来袭弹幕。' },
|
||||
{ label: '规划路线', description: '观察弹幕并选择安全路线。' },
|
||||
],
|
||||
},
|
||||
capabilities: { tools: [], skills: [] },
|
||||
processingSeconds: 1.2,
|
||||
createdAtUtc: '2026-09-03T00:00:00Z',
|
||||
updatedAtUtc: '2026-09-03T00:00:01Z',
|
||||
lastError: null,
|
||||
},
|
||||
result: {
|
||||
schemaVersion: 'planning-turn-result.v2',
|
||||
kind: 'question',
|
||||
payload: {
|
||||
question: {
|
||||
id: 'core_loop',
|
||||
header: '当前要决定:核心循环',
|
||||
question: '玩家在一局中主要反复做什么?',
|
||||
options: [
|
||||
{ label: '持续闪避', description: '保持移动并躲避来袭弹幕。' },
|
||||
{ label: '规划路线', description: '观察弹幕并选择安全路线。' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
currentArtifact: null,
|
||||
replayed: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function mountApprovalCard(
|
||||
harness: ReturnType<typeof createProjectSupervisorRuntimeHarness>,
|
||||
planningStartMode = false,
|
||||
) {
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
mountFormalSupervisor(harness);
|
||||
mountFormalSupervisor(harness, planningStartMode);
|
||||
return await screen.findByLabelText('GDD 审批卡');
|
||||
}
|
||||
|
||||
async function mountPlanningSurface(
|
||||
harness: ReturnType<typeof createProjectSupervisorRuntimeHarness>,
|
||||
planningStartMode = false,
|
||||
) {
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
mountFormalSupervisor(harness);
|
||||
mountFormalSupervisor(harness, planningStartMode);
|
||||
return await screen.findByLabelText('立项策划阶段进度');
|
||||
}
|
||||
|
||||
@@ -502,77 +604,51 @@ export function registerPlanGddApprovalTests() {
|
||||
});
|
||||
|
||||
it('still surfaces the clarification card on the planning lane', async () => {
|
||||
// 澄清卡是策划链路唯一需要用户动手的交互面之一,瘦身不能把它一起收掉。
|
||||
const supervisorRunId = 'plan-root-clarifying-run';
|
||||
const request = agentRuntimeUserInputRequest({
|
||||
agentId: 'project-supervisor',
|
||||
sessionId: 'supervisor-session-active',
|
||||
runId: supervisorRunId,
|
||||
requestId: 'request-plan-round-1',
|
||||
actionId: 'action-plan-round-1',
|
||||
});
|
||||
// 澄清卡是 V2 策划链路唯一需要用户动手的交互面之一。
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
expectedRunProfile: 'standard',
|
||||
initialRuntime: {
|
||||
runId: supervisorRunId,
|
||||
source: 'project-supervisor-plan',
|
||||
runProfile: 'standard',
|
||||
status: 'waiting-for-user-input',
|
||||
phase: 'waiting-for-user-input',
|
||||
currentTask: '剧情向恋爱养成游戏',
|
||||
currentAction: '等待用户回答第 1 轮澄清',
|
||||
userInputRequest: request,
|
||||
updatedAt: 7100,
|
||||
},
|
||||
planningV2Result: planningV2QuestionResult(),
|
||||
});
|
||||
harness.setPlanGddState(
|
||||
draftPlanGddState({
|
||||
clarificationRound: 0,
|
||||
awaitingAnswerFor: {
|
||||
delegationId: 'delegation-0001',
|
||||
requestId: 'request-plan-round-1',
|
||||
questionId: 'visual_direction',
|
||||
round: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await mountPlanningSurface(harness);
|
||||
await mountPlanningSurface(harness, true);
|
||||
|
||||
const strip = await screen.findByLabelText('立项策划运行状态');
|
||||
expect(within(strip).getByLabelText('Needs input')).not.toBeNull();
|
||||
expect(
|
||||
within(strip).getByText('首版角色规范图采用哪种美术方向?'),
|
||||
within(strip).getByText('玩家在一局中主要反复做什么?'),
|
||||
).not.toBeNull();
|
||||
expectSupervisorRuntimePanelAbsent();
|
||||
expect(screen.queryByText('等待用户回答第 1 轮澄清')).toBeNull();
|
||||
expect(screen.queryByText(/agent\.delegate/)).toBeNull();
|
||||
});
|
||||
|
||||
it('offers a restart on the planning lane once the planning run has failed', async () => {
|
||||
// 失败是另一处需要用户动手的时刻。恢复入口跟着搬进窄条,而不是随面板一起消失。
|
||||
it('surfaces a V2 provider failure on the planning lane', async () => {
|
||||
// V2 provider_failed 由同一策划会话承接,错误显示在窄条中,用户可用输入框重新提交。
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
expectedRunProfile: 'standard',
|
||||
initialRuntime: {
|
||||
runId: 'plan-root-failed-run',
|
||||
source: 'project-supervisor-plan',
|
||||
runProfile: 'standard',
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
currentTask: '剧情向恋爱养成游戏',
|
||||
currentAction: '策划子 Run 退出',
|
||||
error: '项目总控 Agent Codex 执行失败,请查看运行详情后重试',
|
||||
updatedAt: 7200,
|
||||
planningV2Result: {
|
||||
...planningV2QuestionResult(),
|
||||
session: {
|
||||
...planningV2QuestionResult().session,
|
||||
status: 'provider_failed',
|
||||
currentQuestion: null,
|
||||
lastError: {
|
||||
code: 'PROVIDER_FAILED',
|
||||
summary: 'Planning V2 Provider 调用失败',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
schemaVersion: 'planning-turn-result.v2',
|
||||
kind: 'error',
|
||||
payload: {
|
||||
code: 'PROVIDER_FAILED',
|
||||
summary: 'Planning V2 Provider 调用失败',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
harness.setPlanGddState(draftPlanGddState());
|
||||
await mountPlanningSurface(harness);
|
||||
await mountPlanningSurface(harness, true);
|
||||
|
||||
const recovery = await screen.findByLabelText('立项策划失败恢复');
|
||||
expect(
|
||||
within(recovery).getByRole('button', { name: '重新启动策划' }),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByRole('alert').textContent).toContain('执行失败');
|
||||
const strip = await screen.findByLabelText('立项策划运行状态');
|
||||
expect(within(strip).getByRole('alert').textContent).toContain('执行失败');
|
||||
expectSupervisorRuntimePanelAbsent();
|
||||
expect(screen.queryByText('策划子 Run 退出')).toBeNull();
|
||||
expect(screen.queryByText(/project-planning/)).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a stylesheet rule for every class the planning components reference', () => {
|
||||
@@ -661,4 +737,66 @@ export function registerPlanGddApprovalTests() {
|
||||
/\.game-workbench-chat\s+\.plan-gdd-surface--with-card\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\)/s,
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a formal planning entry through Runtime V2 commands', async () => {
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
planningV2Result: planningV2ApprovalResult(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
orchestrationMode: 'single-supervisor',
|
||||
planningStartMode: true,
|
||||
projectSupervisorOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await screen.findByLabelText('GDD 审批卡');
|
||||
const planningHydrateCall = harness.invoke.mock.calls.find(
|
||||
([command]) => command === 'hydrate_planning_session_v2',
|
||||
);
|
||||
expect(planningHydrateCall).toBeDefined();
|
||||
fireEvent.click(screen.getByRole('button', { name: '批准 v1' }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
harness.invoke.mock.calls.some(
|
||||
([command]) => command === 'decide_planning_artifact_v2',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('starts a new V2 planning session and renders its question card', async () => {
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
planningV2StartResult: planningV2QuestionResult(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
initialSupervisorMessage: '做一个2D弹幕射击游戏',
|
||||
orchestrationMode: 'single-supervisor',
|
||||
planningStartMode: true,
|
||||
projectSupervisorOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await screen.findByLabelText('Needs input');
|
||||
expect(
|
||||
harness.invoke.mock.calls.some(
|
||||
([command]) => command === 'start_planning_session_v2',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(screen.getByText('玩家在一局中主要反复做什么?')).not.toBeNull();
|
||||
expect(screen.queryByText(/agent\.delegate/)).toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- 背景:现有“做方案”依赖 `project-supervisor-plan` 根 Run、`project-planning` 子 Run、静态委派、delivery、Acceptance Graph 和审批前 evidence。新策划 Agent 只需要单 Agent 会话、问询、GDD 和审批;继续在旧 Runtime 上逐条放宽会保留身份/编排耦合。未来策划 Agent 可能支持无限多轮、MCP 和 Skill,需要避免把当前 8 题/GDD/no-tools 固化为 Runtime 根结构。
|
||||
- 决策:新增独立 `PlanningSessionRuntime`,复用 Provider/流式、会话持久化、项目锁、原子写和基础错误恢复;当前启用 `mode=gdd`、最多展示 8 个有效问题、GDD 审批和用户修改。新建“做方案”会话不创建 Supervisor root、planning child、delegation 或 acceptance evidence。V2 使用独立 `.agent/planning-v2/` 与 V2 schema,继续输出 `game/fast_gdd.md`;不自动转换旧会话。
|
||||
- 兼容性:Session 保存 `mode`、可空 `questionLimit`、`capabilities.tools/skills`;完整会话记录与 Provider 请求上下文分离;消息模型预留 tool/skill 事件类型但本期不执行 MCP/Skill。无限问询、上下文摘要、多产物和能力执行以后作为策略/能力层扩展,不重新引入 Supervisor 身份模型。
|
||||
- 当前进度:P0 合同冻结、P1 会话内核和 P2 GDD/审批核心已落地;P3 正式入口/UI 接入与 P5 旧链路退役尚未开始。
|
||||
- 当前进度:P0 合同冻结、P1 会话内核和 P2 GDD/审批核心已落地;P3 正式入口/UI 接入已开始,P5 旧链路退役尚未开始。
|
||||
- 退役:V2 切换时旧链路直接封存;所有未完成旧会话投影为 `legacy_retired` 失败,禁止继续问询、审批、恢复或 continuation。旧 GDD、approval、conversation 和 `.agent/planning` 文件只读保留;旧入口 caller 关闭,但不删除旧代码、旧测试或旧数据。
|
||||
- 影响范围:AGC 做方案入口、Rust/Tauri planning session/Provider adapter、GDD/审批 V2、前端 planning lane、阶段任务与 BDD 验收;做游戏/做素材 DirectProject 不变。
|
||||
- 验证方式:按 `docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md` 的 P0~P5 阶段验收执行;至少覆盖第 8 个问题、上限后 question 抑制、Provider 失败、非法输出、批准/修改/退回、重启恢复、旧会话切换强制失败、迟到 Provider 结果丢弃和当前空能力快照。
|
||||
@@ -7982,3 +7982,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- V2 新增 `GddPlanningPolicy`:Provider 输出只接受合法 question 或完整 GDD JSON;question 在 `questionCount < questionLimit` 时保存并展示,第 8 个问题仍可展示,第 8 个之后再次返回 question 时只允许一次内部强制出稿重试,额外 question 不落盘、不进入用户等待态。
|
||||
- V2 GDD 使用独立 `plan-gdd.v2`,只保留项目身份、版本、游戏内容、决定、原型验证项和指纹,不带旧 Supervisor/Run/delegation 字段。每个版本写入 `.agent/planning-v2/gdd.vN.json`,同步更新 V2 index 和 `game/fast_gdd.md`;旧版本不可覆盖。
|
||||
- 新增 `decide_planning_artifact_v2` 与 `plan-approval.v2`。批准、修改、退回均绑定当前 Session、artifact、version 和 fingerprint;修改/退回必须有意见,修改后 Session 回到 `revision_requested`,下一轮由同一 V2 Session 继续。`answerSource` 缺失或未知值回退,不成为单独阻断项。
|
||||
|
||||
## 2026-09-03 策划会话 Runtime V2 P3 入口与 UI 接入开始
|
||||
|
||||
- 正式 AGC“做方案”入口在 `planningStartMode` 下直接调用 `start_planning_session_v2`、`continue_planning_session_v2` 和 `decide_planning_artifact_v2`,不再为新策划回合创建 Supervisor root、child Run 或 delegation。
|
||||
- 前端以适配层复用现有聊天区、澄清输入卡、GDD 审批卡和阶段进度条;V2 hydrate 返回 `conversation` 消息,用于页面刷新和重启后恢复可见历史。
|
||||
- V2 会话使用独立 `planning-session-v2-stream` 事件,旧 Supervisor Runtime 轮询、专业 Agent 轮询和旧 Runtime 事件不会介入 V2 会话。
|
||||
- 没有 V2 authority 的项目仍按旧读取路径打开;旧链路封存、未完成旧会话强制失败和旧入口彻底关闭仍留在 P5。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 策划会话 Runtime V2 接入与旧链路退役方案
|
||||
|
||||
- 日期:2026-09-03
|
||||
- 状态:P0 合同冻结、P1 内核和 P2 产物闭环核心已完成;P3~P5 待实施;本文是新生产实现的目标方案与阶段验收合同
|
||||
- 状态:P0 合同冻结、P1 内核和 P2 产物闭环核心已完成;P3 入口与 UI 接入实施中;P4~P5 待实施;本文是新生产实现的目标方案与阶段验收合同
|
||||
- 适用范围:AGC 桌面 App 的“做方案”入口、策划会话、GDD 产物与审批
|
||||
|
||||
> 本文只规定新策划 Agent 的生产接入和旧链路退役方式,不修改当前生产代码。现有 `project-supervisor-plan` / `project-planning` 链路在 V2 切换前仍是存量实现;V2 切换时旧链路直接封存,所有未完成旧会话强制失败,旧 Fast GDD 文档之后只作为历史记录依据。
|
||||
> 本文规定新策划 Agent 的生产接入和旧链路退役方式。P3 开始修改正式 AGC 入口与工作台,但旧 `project-supervisor-plan` / `project-planning` 源码仍保留,直到 P5 完成退役;V2 切换时旧链路直接封存,所有未完成旧会话强制失败,旧 Fast GDD 文档之后只作为历史记录依据。
|
||||
|
||||
## 1. 决策摘要
|
||||
|
||||
@@ -263,8 +263,14 @@ Provider 适配层输出 `planning-turn-result.v2`:
|
||||
"header": "当前要决定:核心回路",
|
||||
"question": "玩家每一局主要反复做什么?",
|
||||
"options": [
|
||||
{"label": "工作台设计迭代", "description": "先设计,再验证并回到编辑器修改。"},
|
||||
{"label": "直接战斗验证", "description": "先进入战斗,再根据结果调整设计。"}
|
||||
{
|
||||
"label": "工作台设计迭代",
|
||||
"description": "先设计,再验证并回到编辑器修改。"
|
||||
},
|
||||
{
|
||||
"label": "直接战斗验证",
|
||||
"description": "先进入战斗,再根据结果调整设计。"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -295,12 +301,12 @@ Provider 适配层输出 `planning-turn-result.v2`:
|
||||
|
||||
四个 command 的输入/输出边界冻结如下:
|
||||
|
||||
| command | 必要输入 | 返回 |
|
||||
|---|---|---|
|
||||
| `start_planning_session_v2` | `projectPath`、`clientTurnId`、`prompt`、可选 `mode` | V2 Session 及本次回合结果 |
|
||||
| `continue_planning_session_v2` | `projectPath`、`sessionId`、`clientTurnId`、`input` | 更新后的 Session 及本次回合结果 |
|
||||
| `decide_planning_artifact_v2` | `projectPath`、`sessionId`、`artifactId`、`version`、`fingerprint`、`decisionId`、`action`、可选 `comment` | 审批结果和最新 Session 状态 |
|
||||
| `hydrate_planning_session_v2` | `projectPath`、可选 `sessionId` | 只读 V2 Session、当前 question、当前产物和错误摘要 |
|
||||
| command | 必要输入 | 返回 |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `start_planning_session_v2` | `projectPath`、`clientTurnId`、`prompt`、可选 `mode` | V2 Session 及本次回合结果 |
|
||||
| `continue_planning_session_v2` | `projectPath`、`sessionId`、`clientTurnId`、`input` | 更新后的 Session 及本次回合结果 |
|
||||
| `decide_planning_artifact_v2` | `projectPath`、`sessionId`、`artifactId`、`version`、`fingerprint`、`decisionId`、`action`、可选 `comment` | 审批结果和最新 Session 状态 |
|
||||
| `hydrate_planning_session_v2` | `projectPath`、可选 `sessionId` | 只读 V2 Session、当前 question、当前产物、错误摘要和完整 `conversation` 消息 |
|
||||
|
||||
`input` 冻结为 `option`、`freeform`、`direct_draft`、`revision` 四种用户意图:
|
||||
|
||||
@@ -383,12 +389,12 @@ normalize_turn_result()
|
||||
|
||||
P0 冻结适配器的四个边界对象:
|
||||
|
||||
| 对象 | Runtime 可见字段 | 约束 |
|
||||
|---|---|---|
|
||||
| `PlanningProviderRequestV2` | `sessionId`、`turnIndex`、`mode`、`policyHint`、`messages`、`capabilities` | 不携带 Supervisor/child/delegation 身份;具体 API kind、URL 和凭据由 Provider substrate 持有 |
|
||||
| `PlanningProviderStreamEventV2` | `type=text_delta\|completed\|failed`、可选 `text`/`result`/`error` | Provider 原始协议在 adapter 内归一;Runtime 不解析 OpenAI/Anthropic 私有字段 |
|
||||
| `ContextBuildResultV2` | `messages`、`estimatedTokens`、`overflow` | 完整会话记录不等于请求上下文;`overflow=true` 时显式失败,不静默丢历史 |
|
||||
| `CapabilitySnapshotV2` | `tools`、`skills` | 稳定排序、去重;当前必须为空,由宿主注入,模型不能修改 |
|
||||
| 对象 | Runtime 可见字段 | 约束 |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `PlanningProviderRequestV2` | `sessionId`、`turnIndex`、`mode`、`policyHint`、`messages`、`capabilities` | 不携带 Supervisor/child/delegation 身份;具体 API kind、URL 和凭据由 Provider substrate 持有 |
|
||||
| `PlanningProviderStreamEventV2` | `type=text_delta\|completed\|failed`、可选 `text`/`result`/`error` | Provider 原始协议在 adapter 内归一;Runtime 不解析 OpenAI/Anthropic 私有字段 |
|
||||
| `ContextBuildResultV2` | `messages`、`estimatedTokens`、`overflow` | 完整会话记录不等于请求上下文;`overflow=true` 时显式失败,不静默丢历史 |
|
||||
| `CapabilitySnapshotV2` | `tools`、`skills` | 稳定排序、去重;当前必须为空,由宿主注入,模型不能修改 |
|
||||
|
||||
Provider adapter 只负责“请求、流式事件、稳定错误、用量/耗时”;是否接受 question/GDD、是否计入问题数和是否生成审批由 `PlanningPolicy` 决定。
|
||||
|
||||
@@ -490,13 +496,13 @@ P0 冻结审批记录绑定 `version + fingerprint`,并使用 `plan-approval.v
|
||||
|
||||
同一项目只允许一个活跃策划权威。V2 切换包含一次项目级“旧链路封存”操作,路由规则如下:
|
||||
|
||||
| 项目状态 | 新请求路由 |
|
||||
|---|---|
|
||||
| 已有活跃 V2 会话 | 继续 V2;同一会话只允许一个在途 Provider 回合 |
|
||||
| 项目状态 | 新请求路由 |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| 已有活跃 V2 会话 | 继续 V2;同一会话只允许一个在途 Provider 回合 |
|
||||
| 只有旧 `project-supervisor-plan` 活跃会话 | 切换时将旧会话投影为 `legacy_retired` 失败;用户需重新创建 V2,不再继续旧链路 |
|
||||
| 没有活跃策划会话 | 创建 V2 |
|
||||
| 旧会话已终态、用户明确重新做方案 | 创建 V2,不改写旧目录 |
|
||||
| 同时发现旧/V2 活跃会话 | 旧会话优先封存为 `legacy_retired`,只保留 V2 推进 |
|
||||
| 没有活跃策划会话 | 创建 V2 |
|
||||
| 旧会话已终态、用户明确重新做方案 | 创建 V2,不改写旧目录 |
|
||||
| 同时发现旧/V2 活跃会话 | 旧会话优先封存为 `legacy_retired`,只保留 V2 推进 |
|
||||
|
||||
不做旧 session → V2 的自动转换。原因是两套身份、计数、审批和 GDD schema 不同,自动转换会把旧 pending 或旧 approval receipt 混入 V2。
|
||||
|
||||
@@ -593,11 +599,11 @@ hydrate_planning_session_v2
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P0-1 | 冻结 Session、消息、回合结果、产物和审批 DTO | V2 schema、字段枚举和版本策略 |
|
||||
| P0-2 | 冻结状态机、`questionLimit` 语义和重试规则 | 状态转移表、错误边界 |
|
||||
| P0-3 | 冻结新旧并存与同项目单权威规则 | 路由/恢复决策表 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | -------------------------------------------- | ----------------------------------- |
|
||||
| P0-1 | 冻结 Session、消息、回合结果、产物和审批 DTO | V2 schema、字段枚举和版本策略 |
|
||||
| P0-2 | 冻结状态机、`questionLimit` 语义和重试规则 | 状态转移表、错误边界 |
|
||||
| P0-3 | 冻结新旧并存与同项目单权威规则 | 路由/恢复决策表 |
|
||||
| P0-4 | 冻结 Provider/ContextBuilder/Capability 插槽 | provider-neutral adapter 和模块边界 |
|
||||
|
||||
阶段验收:
|
||||
@@ -626,14 +632,14 @@ hydrate_planning_session_v2
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P1-1 | 新建 V2 Session 生命周期与单项目并发控制 | `planning_session_v2` Rust 模块 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | ------------------------------------------ | --------------------------------------- |
|
||||
| P1-1 | 新建 V2 Session 生命周期与单项目并发控制 | `planning_session_v2` Rust 模块 |
|
||||
| P1-2 | 接入现有 Provider substrate 和流式事件归一 | provider-neutral request/stream adapter |
|
||||
| P1-3 | 实现消息 JSONL、回合身份和幂等写入 | `conversation.jsonl` 及 turn identity |
|
||||
| P1-4 | 实现 ContextBuilder 初版 | 有界历史构建;超限显式失败 |
|
||||
| P1-5 | 实现 Provider 失败/中断/重启恢复 | Session 不丢消息、不伪造成功 |
|
||||
| P1-6 | 记录 `turnIndex`、处理耗时和安全错误摘要 | Session 快照、诊断字段 |
|
||||
| P1-3 | 实现消息 JSONL、回合身份和幂等写入 | `conversation.jsonl` 及 turn identity |
|
||||
| P1-4 | 实现 ContextBuilder 初版 | 有界历史构建;超限显式失败 |
|
||||
| P1-5 | 实现 Provider 失败/中断/重启恢复 | Session 不丢消息、不伪造成功 |
|
||||
| P1-6 | 记录 `turnIndex`、处理耗时和安全错误摘要 | Session 快照、诊断字段 |
|
||||
|
||||
阶段验收:
|
||||
|
||||
@@ -656,14 +662,14 @@ hydrate_planning_session_v2
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P2-1 | 实现 question / GDD 结果解析 | 只接受当前策略需要的结果 |
|
||||
| P2-2 | 实现最多 8 个有效问题的策略计数 | `questionCount` 与 `turnIndex` 分离 |
|
||||
| P2-3 | 实现达到上限后的单次强制出稿重试 | 不保存/展示额外 question |
|
||||
| P2-4 | 实现 V2 GDD schema、版本链和 `fast_gdd.md` | `.agent/planning-v2/**` 与 Markdown |
|
||||
| P2-5 | 实现审批、修改、退回 | `plan-approval.v2` 与新版本生成 |
|
||||
| P2-6 | 实现轻量输入归一化 | A/B/编号/完整 label/“按第一个选项做”映射 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | ------------------------------------------ | ---------------------------------------- |
|
||||
| P2-1 | 实现 question / GDD 结果解析 | 只接受当前策略需要的结果 |
|
||||
| P2-2 | 实现最多 8 个有效问题的策略计数 | `questionCount` 与 `turnIndex` 分离 |
|
||||
| P2-3 | 实现达到上限后的单次强制出稿重试 | 不保存/展示额外 question |
|
||||
| P2-4 | 实现 V2 GDD schema、版本链和 `fast_gdd.md` | `.agent/planning-v2/**` 与 Markdown |
|
||||
| P2-5 | 实现审批、修改、退回 | `plan-approval.v2` 与新版本生成 |
|
||||
| P2-6 | 实现轻量输入归一化 | A/B/编号/完整 label/“按第一个选项做”映射 |
|
||||
|
||||
阶段验收:
|
||||
|
||||
@@ -677,19 +683,19 @@ hydrate_planning_session_v2
|
||||
|
||||
依赖:P1。
|
||||
|
||||
### P3:生产入口与 UI 接入
|
||||
### P3:生产入口与 UI 接入(实施中)
|
||||
|
||||
目标:让用户从正式“做方案”入口使用 V2,同时保持现有页面可用。
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P3-1 | 新增 V2 Tauri command 注册和前端 invoke 封装 | 命令可启动/恢复/审批 |
|
||||
| P3-2 | 将 `planningStartMode` 路由到 V2 | 新项目不创建旧 Supervisor root |
|
||||
| P3-3 | 复用审批卡并切换到 V2 hydrate 状态 | GDD 展示、版本和审批按钮正常 |
|
||||
| P3-4 | 加入当前运行态、流式回复和耗时展示 | 页面可见状态与 Session 一致 |
|
||||
| P3-5 | 识别旧项目并准备封存投影 | 存量旧会话不被误路由到 V2 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | -------------------------------------------- | ------------------------------ |
|
||||
| P3-1 | 新增 V2 Tauri command 注册和前端 invoke 封装 | 命令可启动/恢复/审批 |
|
||||
| P3-2 | 将 `planningStartMode` 路由到 V2 | 新项目不创建旧 Supervisor root |
|
||||
| P3-3 | 复用审批卡并切换到 V2 hydrate 状态 | GDD 展示、版本和审批按钮正常 |
|
||||
| P3-4 | 加入当前运行态、流式回复和耗时展示 | 页面可见状态与 Session 一致 |
|
||||
| P3-5 | 识别旧项目并准备封存投影 | 存量旧会话不被误路由到 V2 |
|
||||
|
||||
阶段验收:
|
||||
|
||||
@@ -701,19 +707,27 @@ hydrate_planning_session_v2
|
||||
|
||||
依赖:P2。
|
||||
|
||||
当前实现进度(2026-09-03):
|
||||
|
||||
- 正式 `planningStartMode` 首轮提交已改走 `start_planning_session_v2`,后续回答改走 `continue_planning_session_v2`,审批改走 `decide_planning_artifact_v2`;不再为新策划回合创建 Supervisor Run。
|
||||
- 前端通过 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。
|
||||
|
||||
### P4:灰度、真实 Provider 与回归验收
|
||||
|
||||
目标:证明 V2 的正常路径和关键失败路径可用,再关闭旧入口新建能力。
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P4-1 | 离线状态/结构定向测试 | Session、策略、schema、审批测试 |
|
||||
| P4-2 | 真实 Provider 测试 | 流式、问题、GDD、失败恢复 |
|
||||
| P4-3 | 前端组件/工作台测试 | 路由、审批卡、恢复显示 |
|
||||
| P4-4 | 旧链路切换封存测试 | 未完成旧 session 强制失败、V2 不误读旧目录 |
|
||||
| P4-5 | 安全与编码门禁 | `npm run check:encoding`、`git diff --check` 及相关 Rust/TS 检查 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | --------------------- | ---------------------------------------------------------------- |
|
||||
| P4-1 | 离线状态/结构定向测试 | Session、策略、schema、审批测试 |
|
||||
| P4-2 | 真实 Provider 测试 | 流式、问题、GDD、失败恢复 |
|
||||
| P4-3 | 前端组件/工作台测试 | 路由、审批卡、恢复显示 |
|
||||
| P4-4 | 旧链路切换封存测试 | 未完成旧 session 强制失败、V2 不误读旧目录 |
|
||||
| P4-5 | 安全与编码门禁 | `npm run check:encoding`、`git diff --check` 及相关 Rust/TS 检查 |
|
||||
|
||||
阶段验收:
|
||||
|
||||
@@ -731,12 +745,12 @@ hydrate_planning_session_v2
|
||||
|
||||
任务:
|
||||
|
||||
| ID | 任务 | 产出 |
|
||||
|---|---|---|
|
||||
| P5-1 | 关闭新建和继续旧 `project-supervisor-plan` 的 caller | 入口门禁/路由变更 |
|
||||
| P5-2 | 执行旧 session 一次性封存 | `legacy-cutover.json` 与失败投影 |
|
||||
| P5-3 | 保留旧产物只读展示,禁止旧交互继续推进 | 历史查看能力 |
|
||||
| P5-4 | 更新生产文档和运维说明 | 旧链路退役状态、回滚边界 |
|
||||
| ID | 任务 | 产出 |
|
||||
| ---- | ---------------------------------------------------- | -------------------------------- |
|
||||
| P5-1 | 关闭新建和继续旧 `project-supervisor-plan` 的 caller | 入口门禁/路由变更 |
|
||||
| P5-2 | 执行旧 session 一次性封存 | `legacy-cutover.json` 与失败投影 |
|
||||
| P5-3 | 保留旧产物只读展示,禁止旧交互继续推进 | 历史查看能力 |
|
||||
| P5-4 | 更新生产文档和运维说明 | 旧链路退役状态、回滚边界 |
|
||||
|
||||
阶段验收:
|
||||
|
||||
@@ -875,32 +889,32 @@ hydrate_planning_session_v2
|
||||
|
||||
## 11. 测试映射
|
||||
|
||||
| 场景/规则 | 测试层级 | 计划目标 |
|
||||
|---|---|---|
|
||||
| Session 状态、questionCount/turnIndex 分离 | Rust unit | `planning_session_v2` |
|
||||
| Provider 失败、幂等回合、恢复 | Rust integration | V2 runtime/provider adapter tests |
|
||||
| GDD schema、版本链、fingerprint | Rust unit/integration | V2 artifact/approval tests |
|
||||
| 第 8 个问题与上限后重试 | Rust unit | `GddPlanningPolicy` tests |
|
||||
| 审批批准/修改/退回/过期审批 | Rust integration | V2 approval tests |
|
||||
| 输入“按 A 做/按第一个选项做” | Rust/TS unit | input normalization tests |
|
||||
| 做方案入口路由 | frontend integration | `App`/planning lane tests |
|
||||
| 问题卡、GDD 卡和恢复态展示 | component | `ProjectSupervisorView`/`GddApprovalCard` tests |
|
||||
| 真实 Provider 流式链路 | real provider smoke | P4 独立脚本或现有 real-e2e harness |
|
||||
| 旧会话切换强制失败、旧目录不被 V2 读取 | Rust integration | legacy cutover/recovery tests |
|
||||
| 中文编码和文档 diff | repository gate | `npm run check:encoding`、`git diff --check` |
|
||||
| 场景/规则 | 测试层级 | 计划目标 |
|
||||
| ------------------------------------------ | --------------------- | ----------------------------------------------- |
|
||||
| Session 状态、questionCount/turnIndex 分离 | Rust unit | `planning_session_v2` |
|
||||
| Provider 失败、幂等回合、恢复 | Rust integration | V2 runtime/provider adapter tests |
|
||||
| GDD schema、版本链、fingerprint | Rust unit/integration | V2 artifact/approval tests |
|
||||
| 第 8 个问题与上限后重试 | Rust unit | `GddPlanningPolicy` tests |
|
||||
| 审批批准/修改/退回/过期审批 | Rust integration | V2 approval tests |
|
||||
| 输入“按 A 做/按第一个选项做” | Rust/TS unit | input normalization tests |
|
||||
| 做方案入口路由 | frontend integration | `App`/planning lane tests |
|
||||
| 问题卡、GDD 卡和恢复态展示 | component | `ProjectSupervisorView`/`GddApprovalCard` tests |
|
||||
| 真实 Provider 流式链路 | real provider smoke | P4 独立脚本或现有 real-e2e harness |
|
||||
| 旧会话切换强制失败、旧目录不被 V2 读取 | Rust integration | legacy cutover/recovery tests |
|
||||
| 中文编码和文档 diff | repository gate | `npm run check:encoding`、`git diff --check` |
|
||||
|
||||
未接入 Cucumber/Playwright runner 前,以上 BDD 先作为 Markdown 验收合同;不为本方案新增独立 BDD 测试框架。
|
||||
|
||||
## 12. 风险与处理原则
|
||||
|
||||
| 风险 | 处理 |
|
||||
|---|---|
|
||||
| 继续复用旧 `planning_submit.rs` 导致 Supervisor 身份回流 | V2 使用独立 artifact/approval 模块;只复用通用文件/锁能力 |
|
||||
| 新旧都写 `game/fast_gdd.md` | 同一项目单活跃策划权威;V2/旧路径均使用项目锁和原子写 |
|
||||
| 无限会话导致上下文无限膨胀 | 当前先分离完整记录和 ContextBuilder;超预算显式失败,后续再加摘要 |
|
||||
| 未来 MCP/Skill 侵入 GDD 策略 | 能力快照和消息类型在 Runtime 层预留,当前策略不广告、不执行 |
|
||||
| 强制失败导致旧 pending/receipt 不再可继续 | 这是本次明确的退役语义;旧文件只读保留,不迁移、不删除 |
|
||||
| 前端组件名继续叫 Supervisor 造成误解 | 第一阶段只切数据源;后续独立重命名,不把命名重构当接入前置 |
|
||||
| 风险 | 处理 |
|
||||
| -------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| 继续复用旧 `planning_submit.rs` 导致 Supervisor 身份回流 | V2 使用独立 artifact/approval 模块;只复用通用文件/锁能力 |
|
||||
| 新旧都写 `game/fast_gdd.md` | 同一项目单活跃策划权威;V2/旧路径均使用项目锁和原子写 |
|
||||
| 无限会话导致上下文无限膨胀 | 当前先分离完整记录和 ContextBuilder;超预算显式失败,后续再加摘要 |
|
||||
| 未来 MCP/Skill 侵入 GDD 策略 | 能力快照和消息类型在 Runtime 层预留,当前策略不广告、不执行 |
|
||||
| 强制失败导致旧 pending/receipt 不再可继续 | 这是本次明确的退役语义;旧文件只读保留,不迁移、不删除 |
|
||||
| 前端组件名继续叫 Supervisor 造成误解 | 第一阶段只切数据源;后续独立重命名,不把命名重构当接入前置 |
|
||||
|
||||
## 13. 完成定义
|
||||
|
||||
|
||||
Reference in New Issue
Block a user