修复:统一 game-chat 单主阶段投影
新增精确绑定 root、main 与动态美术 child 的 game-chat 谱系投影。 统一单主进度、可玩 revision、终态归档与跨轮恢复界面语义。 收紧 final-reply、通用 retry 与异常根身份的失败关闭边界。 补齐普通 DAG 回归、谱系冲突及事件证据契约测试。 同步技术方案、架构裁决与已知陷阱文档。
This commit is contained in:
@@ -99,6 +99,7 @@ import {
|
||||
agentRuntimeStartStatus,
|
||||
agentRuntimeStateFromResult,
|
||||
agentRuntimeSteerStatus,
|
||||
canArchiveGameChatStage,
|
||||
conversationContainsProjectSupervisorResponseStream,
|
||||
createAgentChatRunId,
|
||||
createDefaultChatMessages,
|
||||
@@ -108,12 +109,16 @@ import {
|
||||
isAgentRuntimeTerminalState,
|
||||
isMissingAgentRuntimeResumeCommandError,
|
||||
isRuntimeConfigMissingError,
|
||||
latestGameChatPlayableRevision,
|
||||
matchingAgentRuntimeForSteer,
|
||||
mergeAgentRuntimeStateIntoMap,
|
||||
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
||||
mergeProjectSupervisorConversation,
|
||||
mergeProjectSupervisorResponseStream,
|
||||
normalizeAgentRuntimeState,
|
||||
gameChatRuntimeBelongsToLineage,
|
||||
gameChatRuntimeIdentity,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectNameFromPath,
|
||||
projectProfessionalAgentLabel,
|
||||
projectSupervisorPendingRepairMatchesProfessional,
|
||||
@@ -122,6 +127,7 @@ import {
|
||||
sameAgentRuntimeRun,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
taskRowsFromManifest,
|
||||
type GameChatPlayableRevision,
|
||||
} from './features/agent-runtime';
|
||||
import {
|
||||
isDeveloperMode,
|
||||
@@ -260,37 +266,45 @@ type GameChatAutoPreviewAuthorization = {
|
||||
runId: string;
|
||||
};
|
||||
|
||||
type GameChatPlayableRevision = {
|
||||
runId: string;
|
||||
revision: number;
|
||||
validatedAt: number;
|
||||
type PendingGameChatStageArchive = {
|
||||
rootRuntime: AgentRuntimeState;
|
||||
runtimeRecords: AgentRuntimeState[];
|
||||
manifestSnapshot: GameCreationAppManifest | null;
|
||||
};
|
||||
|
||||
type GameChatPreviewValidationCandidate = GameChatPlayableRevision & {
|
||||
eventOrder: number;
|
||||
playable: boolean;
|
||||
};
|
||||
function mergeGameChatRuntimeSnapshots(
|
||||
previous: AgentRuntimeState[],
|
||||
incoming: Iterable<AgentRuntimeState | null | undefined>,
|
||||
) {
|
||||
const merged = new Map(
|
||||
previous.map((runtime) => [gameChatRuntimeIdentity(runtime), runtime]),
|
||||
);
|
||||
for (const runtime of incoming) {
|
||||
if (!runtime) {
|
||||
continue;
|
||||
}
|
||||
const key = gameChatRuntimeIdentity(runtime);
|
||||
const existing = merged.get(key);
|
||||
if (!existing || runtime.updatedAt >= existing.updatedAt) {
|
||||
merged.set(key, runtime);
|
||||
}
|
||||
}
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
|
||||
const GAME_CHAT_STAGE_TASK_IDS = [
|
||||
'design-director',
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
'code-director',
|
||||
'code-prototype',
|
||||
'preview-readiness',
|
||||
'preview-playtest',
|
||||
] as const;
|
||||
|
||||
function gameChatManifestHasTerminalStageTasks(
|
||||
function gameChatManifestHasTerminalPrimaryTask(
|
||||
manifest: GameCreationAppManifest | null,
|
||||
) {
|
||||
if (!manifest) {
|
||||
return false;
|
||||
}
|
||||
return GAME_CHAT_STAGE_TASK_IDS.every((taskId) => {
|
||||
const status = manifest.tasks.find((task) => task.id === taskId)?.status;
|
||||
return status === 'completed' || status === 'failed';
|
||||
});
|
||||
const tasks =
|
||||
manifest?.tasks.filter((task) => task.id === 'code-prototype') ?? [];
|
||||
return (
|
||||
tasks.length === 1 &&
|
||||
(tasks[0]?.status === 'completed' || tasks[0]?.status === 'failed')
|
||||
);
|
||||
}
|
||||
|
||||
function gameChatStageRecordMessageId(runId: string) {
|
||||
return `game-chat-stage-record:${encodeURIComponent(runId)}`;
|
||||
}
|
||||
|
||||
function gameChatRuntimeHasTerminalOutcome(runtime: AgentRuntimeState) {
|
||||
@@ -335,99 +349,6 @@ function gameChatPlayableRevisionIsAfterAuthorization(
|
||||
);
|
||||
}
|
||||
|
||||
export function latestGameChatPlayableRevision(
|
||||
runtime: AgentRuntimeState | null,
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||||
): GameChatPlayableRevision | null {
|
||||
if (!runtime?.runId) {
|
||||
return null;
|
||||
}
|
||||
const playtestChildren = new Map<string, AgentRuntimeState>();
|
||||
for (const child of Object.values(runtimeByAgentId)) {
|
||||
if (
|
||||
child?.agentId !== 'preview-playtest' ||
|
||||
child.taskId !== 'preview-playtest' ||
|
||||
child.source !== 'agent-ready-task-scheduler' ||
|
||||
child.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||||
child.parentRunId !== runtime.runId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
playtestChildren.set(
|
||||
`${child.agentId}\n${child.sessionId}\n${child.runId}`,
|
||||
child,
|
||||
);
|
||||
}
|
||||
let latest: GameChatPreviewValidationCandidate | null = null;
|
||||
let eventOrder = 0;
|
||||
for (const child of playtestChildren.values()) {
|
||||
for (const event of child.recentEvents ?? []) {
|
||||
eventOrder += 1;
|
||||
if (
|
||||
event.agentId !== child.agentId ||
|
||||
event.taskId !== child.taskId ||
|
||||
event.sessionId !== child.sessionId ||
|
||||
event.runId !== child.runId ||
|
||||
event.eventType !== 'observation' ||
|
||||
!Number.isSafeInteger(event.updatedAt) ||
|
||||
event.updatedAt < 0 ||
|
||||
!event.summary.startsWith('preview.validate:') ||
|
||||
!event.detail?.trim().startsWith('{')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const detail = JSON.parse(event.detail) as {
|
||||
passed?: unknown;
|
||||
playtestPassed?: unknown;
|
||||
revision?: unknown;
|
||||
};
|
||||
const playable =
|
||||
event.summary.startsWith('preview.validate:ok') &&
|
||||
detail.passed === true &&
|
||||
detail.playtestPassed === true;
|
||||
if (
|
||||
typeof detail.passed !== 'boolean' ||
|
||||
typeof detail.revision !== 'number' ||
|
||||
!Number.isSafeInteger(detail.revision) ||
|
||||
detail.revision <= 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const shouldReplace =
|
||||
!latest ||
|
||||
detail.revision > latest.revision ||
|
||||
(detail.revision === latest.revision &&
|
||||
event.updatedAt > latest.validatedAt) ||
|
||||
(detail.revision === latest.revision &&
|
||||
event.updatedAt === latest.validatedAt &&
|
||||
((latest.playable && !playable) ||
|
||||
(latest.playable === playable &&
|
||||
eventOrder > latest.eventOrder)));
|
||||
if (!shouldReplace) {
|
||||
continue;
|
||||
}
|
||||
latest = {
|
||||
eventOrder,
|
||||
playable,
|
||||
runId: runtime.runId,
|
||||
revision: detail.revision,
|
||||
validatedAt: event.updatedAt,
|
||||
};
|
||||
} catch {
|
||||
// Ignore malformed or truncated public evidence and wait for a valid revision.
|
||||
}
|
||||
}
|
||||
}
|
||||
return latest?.playable
|
||||
? {
|
||||
runId: latest.runId,
|
||||
revision: latest.revision,
|
||||
validatedAt: latest.validatedAt,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
function readStoredGameChatAutoPreviewAuthorization(): GameChatAutoPreviewAuthorization | null {
|
||||
try {
|
||||
window.localStorage.removeItem(
|
||||
@@ -656,7 +577,7 @@ export function App({
|
||||
const gameChatObservedRunKeysRef = useRef(new Set<string>());
|
||||
const gameChatArchivedRunKeysRef = useRef(new Set<string>());
|
||||
const gameChatPendingStageRuntimesRef = useRef(
|
||||
new Map<string, AgentRuntimeState>(),
|
||||
new Map<string, PendingGameChatStageArchive>(),
|
||||
);
|
||||
const gameChatCommittedResponseStreamKeysRef = useRef(new Set<string>());
|
||||
const initialSupervisorMessageLatchRef = useRef({
|
||||
@@ -1086,36 +1007,65 @@ export function App({
|
||||
}
|
||||
|
||||
function flushPendingGameChatStageRecords() {
|
||||
if (!gameChatOnly || !gameChatManifestHasTerminalStageTasks(manifest)) {
|
||||
if (!gameChatOnly) {
|
||||
return;
|
||||
}
|
||||
for (const [
|
||||
archiveKey,
|
||||
pendingRuntime,
|
||||
pendingArchive,
|
||||
] of gameChatPendingStageRuntimesRef.current) {
|
||||
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
|
||||
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
|
||||
continue;
|
||||
}
|
||||
pendingArchive.runtimeRecords = mergeGameChatRuntimeSnapshots(
|
||||
pendingArchive.runtimeRecords,
|
||||
Object.values(agentRuntimeById),
|
||||
);
|
||||
if (
|
||||
projectSupervisorRuntimeRef.current?.runId ===
|
||||
pendingArchive.rootRuntime.runId &&
|
||||
gameChatManifestHasTerminalPrimaryTask(manifest)
|
||||
) {
|
||||
pendingArchive.manifestSnapshot = manifest;
|
||||
}
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(
|
||||
pendingArchive.rootRuntime,
|
||||
pendingArchive.runtimeRecords,
|
||||
);
|
||||
if (!canArchiveGameChatStage(lineage, pendingArchive.manifestSnapshot)) {
|
||||
continue;
|
||||
}
|
||||
const runtimeSnapshot = Object.fromEntries(
|
||||
pendingArchive.runtimeRecords.map((runtime) => [
|
||||
gameChatRuntimeIdentity(runtime),
|
||||
runtime,
|
||||
]),
|
||||
);
|
||||
const progress = buildGameChatProgressEvidence(
|
||||
pendingRuntime,
|
||||
agentRuntimeById,
|
||||
manifest,
|
||||
pendingArchive.rootRuntime,
|
||||
runtimeSnapshot,
|
||||
pendingArchive.manifestSnapshot,
|
||||
);
|
||||
if (!progress) {
|
||||
continue;
|
||||
}
|
||||
const text = formatGameChatStageRecord(
|
||||
pendingRuntime,
|
||||
pendingArchive.rootRuntime,
|
||||
progress,
|
||||
collectGameChatResultImages(manifest),
|
||||
collectGameChatResultImages(pendingArchive.manifestSnapshot),
|
||||
);
|
||||
const messageId = gameChatStageRecordMessageId(
|
||||
pendingArchive.rootRuntime.runId,
|
||||
);
|
||||
gameChatArchivedRunKeysRef.current.add(archiveKey);
|
||||
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
|
||||
setMessages((current) => {
|
||||
if (
|
||||
current.some(
|
||||
(message) => message.role === 'assistant' && message.text === text,
|
||||
(message) =>
|
||||
message.role === 'assistant' &&
|
||||
(message.messageId === messageId || message.text === text),
|
||||
)
|
||||
) {
|
||||
return current;
|
||||
@@ -1130,7 +1080,15 @@ export function App({
|
||||
current.length,
|
||||
);
|
||||
}
|
||||
return [...current, { role: 'assistant', text }];
|
||||
return [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text,
|
||||
messageId,
|
||||
updatedAt: pendingArchive.rootRuntime.updatedAt,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1141,7 +1099,7 @@ export function App({
|
||||
) {
|
||||
const eventMessages = gameChatRuntimeEventMessages(
|
||||
runtime,
|
||||
agentRuntimeById,
|
||||
agentRuntimeByIdRef.current,
|
||||
);
|
||||
if (eventMessages.length === 0) {
|
||||
return;
|
||||
@@ -1174,27 +1132,36 @@ export function App({
|
||||
if (!gameChatOnly || runtimeResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
// A professional Runtime is only part of the active game-chat turn when
|
||||
// it was delegated by the current Project Supervisor run. This prevents
|
||||
// a stale child Runtime (or a different app mode) from leaking into the
|
||||
// project transcript after a restart.
|
||||
const supervisorRunId =
|
||||
projectSupervisorRuntimeRef.current?.runId ??
|
||||
runtimeResults.find(
|
||||
(result) => result.state.agentId === PROJECT_SUPERVISOR_AGENT_ID,
|
||||
)?.state.runId;
|
||||
if (!supervisorRunId) {
|
||||
const runtimeStates = runtimeResults.map((result) =>
|
||||
agentRuntimeStateFromResult(result),
|
||||
);
|
||||
const root =
|
||||
projectSupervisorRuntimeRef.current ??
|
||||
runtimeStates.find(
|
||||
(candidate) => candidate.agentId === PROJECT_SUPERVISOR_AGENT_ID,
|
||||
) ??
|
||||
null;
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(root, [
|
||||
...Object.values(agentRuntimeByIdRef.current),
|
||||
...runtimeStates,
|
||||
]);
|
||||
if (!lineage?.main) {
|
||||
return;
|
||||
}
|
||||
const messages = runtimeResults.flatMap((result) => {
|
||||
const runtime = agentRuntimeStateFromResult(result);
|
||||
const messages = runtimeResults.flatMap((result, index) => {
|
||||
const runtime = runtimeStates[index]!;
|
||||
const stream = result.responseStream;
|
||||
if (
|
||||
runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||||
runtime.parentRunId !== supervisorRunId
|
||||
!gameChatRuntimeBelongsToLineage(lineage, runtime) ||
|
||||
!stream ||
|
||||
stream.agentId !== runtime.agentId ||
|
||||
stream.taskId !== runtime.taskId ||
|
||||
stream.sessionId !== runtime.sessionId ||
|
||||
stream.runId !== runtime.runId
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return gameChatFinalReplyMessages([result.responseStream]);
|
||||
return gameChatFinalReplyMessages([stream]);
|
||||
});
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
@@ -1243,7 +1210,22 @@ export function App({
|
||||
if (gameChatArchivedRunKeysRef.current.has(archiveKey)) {
|
||||
return;
|
||||
}
|
||||
gameChatPendingStageRuntimesRef.current.set(archiveKey, runtime);
|
||||
const previous = gameChatPendingStageRuntimesRef.current.get(archiveKey);
|
||||
gameChatPendingStageRuntimesRef.current.set(archiveKey, {
|
||||
rootRuntime:
|
||||
!previous || runtime.updatedAt >= previous.rootRuntime.updatedAt
|
||||
? runtime
|
||||
: previous.rootRuntime,
|
||||
runtimeRecords: mergeGameChatRuntimeSnapshots(
|
||||
previous?.runtimeRecords ?? [],
|
||||
Object.values(agentRuntimeById),
|
||||
),
|
||||
manifestSnapshot:
|
||||
gameChatManifestHasTerminalPrimaryTask(manifest) &&
|
||||
projectSupervisorRuntimeRef.current?.runId === runtime.runId
|
||||
? manifest
|
||||
: (previous?.manifestSnapshot ?? null),
|
||||
});
|
||||
flushPendingGameChatStageRecords();
|
||||
}
|
||||
|
||||
@@ -1802,8 +1784,10 @@ export function App({
|
||||
}
|
||||
const currentSupervisor = projectSupervisorRuntimeRef.current;
|
||||
let playableRevision = latestGameChatPlayableRevision(
|
||||
currentSupervisor,
|
||||
agentRuntimeByIdRef.current,
|
||||
projectCurrentGameChatRuntimeLineage(
|
||||
currentSupervisor,
|
||||
agentRuntimeByIdRef.current,
|
||||
),
|
||||
);
|
||||
if (playableRevision) {
|
||||
const currentRevision = await readCurrentProjectRevision();
|
||||
@@ -5798,8 +5782,10 @@ export function App({
|
||||
let autoPreviewAfterValidatedAt = 0;
|
||||
if (steerRuntime) {
|
||||
const playableAtSubmission = latestGameChatPlayableRevision(
|
||||
runtimeAtSubmission,
|
||||
agentRuntimeByIdRef.current,
|
||||
projectCurrentGameChatRuntimeLineage(
|
||||
runtimeAtSubmission,
|
||||
agentRuntimeByIdRef.current,
|
||||
),
|
||||
);
|
||||
autoPreviewAfterRevision = Math.max(
|
||||
gameChatPreviewRevisionRef.current ?? 0,
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { PROJECT_SUPERVISOR_AGENT_ID } from '../../app/constants';
|
||||
import type {
|
||||
AgentRuntimeEventRecord,
|
||||
AgentRuntimeState,
|
||||
} from '../../app/types';
|
||||
|
||||
export const GAME_CHAT_SUPERVISOR_SOURCE =
|
||||
'project-supervisor-game-chat' as const;
|
||||
export const GAME_CHAT_PRIMARY_TASK_ID = 'code-prototype' as const;
|
||||
export const GAME_CHAT_DYNAMIC_ART_AGENT_IDS: ReadonlySet<string> = new Set([
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
]);
|
||||
|
||||
const GAME_CHAT_MAIN_SOURCE = 'agent-ready-task-scheduler';
|
||||
const GAME_CHAT_DYNAMIC_ART_SOURCES = new Set([
|
||||
'agent-delegate',
|
||||
'agent-delegate-retry',
|
||||
]);
|
||||
const GAME_CHAT_TERMINAL_STATES = new Set(['completed', 'failed', 'cancelled']);
|
||||
const GAME_CHAT_RECONCILIATION_STATE = 'needs-reconciliation';
|
||||
|
||||
export type GameChatRuntimeLineage = {
|
||||
root: AgentRuntimeState;
|
||||
main: AgentRuntimeState | null;
|
||||
dynamicArtChildren: AgentRuntimeState[];
|
||||
hasConflict: boolean;
|
||||
};
|
||||
|
||||
export type GameChatPrimaryProgress = {
|
||||
completed: 0 | 1;
|
||||
total: 1;
|
||||
status:
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'needs-reconciliation';
|
||||
};
|
||||
|
||||
export type GameChatPlayableRevision = {
|
||||
runId: string;
|
||||
mainRunId: string;
|
||||
revision: number;
|
||||
validatedAt: number;
|
||||
};
|
||||
|
||||
type OrderedRuntimeEvent = {
|
||||
event: AgentRuntimeEventRecord;
|
||||
order: number;
|
||||
};
|
||||
|
||||
type PreviewValidationCandidate = GameChatPlayableRevision & {
|
||||
order: number;
|
||||
playable: boolean;
|
||||
};
|
||||
|
||||
function hasIdentity(value: string | null | undefined) {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
export function gameChatRuntimeIdentity(runtime: AgentRuntimeState) {
|
||||
return [runtime.agentId, runtime.sessionId, runtime.runId].join('\u001f');
|
||||
}
|
||||
|
||||
export function sameGameChatRuntimeIdentity(
|
||||
left: AgentRuntimeState,
|
||||
right: AgentRuntimeState,
|
||||
) {
|
||||
return gameChatRuntimeIdentity(left) === gameChatRuntimeIdentity(right);
|
||||
}
|
||||
|
||||
function distinctRuntimeRecords(
|
||||
runtimeRecords:
|
||||
| Iterable<AgentRuntimeState | null | undefined>
|
||||
| Record<string, AgentRuntimeState | null | undefined>,
|
||||
) {
|
||||
const records =
|
||||
Symbol.iterator in Object(runtimeRecords)
|
||||
? Array.from(
|
||||
runtimeRecords as Iterable<AgentRuntimeState | null | undefined>,
|
||||
)
|
||||
: Object.values(runtimeRecords);
|
||||
const distinct = new Map<string, AgentRuntimeState>();
|
||||
for (const runtime of records) {
|
||||
if (!runtime) {
|
||||
continue;
|
||||
}
|
||||
const key = gameChatRuntimeIdentity(runtime);
|
||||
const previous = distinct.get(key);
|
||||
if (!previous || runtime.updatedAt >= previous.updatedAt) {
|
||||
distinct.set(key, runtime);
|
||||
}
|
||||
}
|
||||
return Array.from(distinct.values());
|
||||
}
|
||||
|
||||
export function isGameChatSupervisorRoot(
|
||||
runtime: AgentRuntimeState | null | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
runtime &&
|
||||
runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
runtime.taskId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
runtime.source === GAME_CHAT_SUPERVISOR_SOURCE &&
|
||||
hasIdentity(runtime.sessionId) &&
|
||||
hasIdentity(runtime.runId) &&
|
||||
!hasIdentity(runtime.parentAgentId) &&
|
||||
!hasIdentity(runtime.parentRunId),
|
||||
);
|
||||
}
|
||||
|
||||
export function isCurrentGameChatMainRuntime(
|
||||
root: AgentRuntimeState | null | undefined,
|
||||
candidate: AgentRuntimeState | null | undefined,
|
||||
): boolean {
|
||||
if (!root || !isGameChatSupervisorRoot(root) || !candidate) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(
|
||||
candidate.agentId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
candidate.taskId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
candidate.source === GAME_CHAT_MAIN_SOURCE &&
|
||||
candidate.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
candidate.parentRunId === root.runId &&
|
||||
hasIdentity(candidate.sessionId) &&
|
||||
hasIdentity(candidate.runId),
|
||||
);
|
||||
}
|
||||
|
||||
export function isCurrentGameChatDynamicArtRuntime(
|
||||
main: AgentRuntimeState | null | undefined,
|
||||
candidate: AgentRuntimeState | null | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
main &&
|
||||
main.agentId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
main.taskId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
main.source === GAME_CHAT_MAIN_SOURCE &&
|
||||
candidate &&
|
||||
GAME_CHAT_DYNAMIC_ART_AGENT_IDS.has(candidate.agentId) &&
|
||||
candidate.taskId === candidate.agentId &&
|
||||
GAME_CHAT_DYNAMIC_ART_SOURCES.has(candidate.source) &&
|
||||
candidate.parentAgentId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
candidate.parentRunId === main.runId &&
|
||||
hasIdentity(candidate.sessionId) &&
|
||||
hasIdentity(candidate.runId),
|
||||
);
|
||||
}
|
||||
|
||||
export function isGameChatRuntimeTerminalState(
|
||||
runtime: AgentRuntimeState | null | undefined,
|
||||
) {
|
||||
return Boolean(
|
||||
runtime &&
|
||||
(GAME_CHAT_TERMINAL_STATES.has(runtime.status) ||
|
||||
GAME_CHAT_TERMINAL_STATES.has(runtime.phase)),
|
||||
);
|
||||
}
|
||||
|
||||
export function isGameChatRuntimeInReconciliation(
|
||||
runtime: AgentRuntimeState | null | undefined,
|
||||
) {
|
||||
return Boolean(
|
||||
runtime &&
|
||||
(runtime.status === GAME_CHAT_RECONCILIATION_STATE ||
|
||||
runtime.phase === GAME_CHAT_RECONCILIATION_STATE),
|
||||
);
|
||||
}
|
||||
|
||||
export function projectCurrentGameChatRuntimeLineage(
|
||||
root: AgentRuntimeState | null | undefined,
|
||||
runtimeRecords:
|
||||
| Iterable<AgentRuntimeState | null | undefined>
|
||||
| Record<string, AgentRuntimeState | null | undefined>,
|
||||
): GameChatRuntimeLineage | null {
|
||||
if (!root || !isGameChatSupervisorRoot(root)) {
|
||||
return null;
|
||||
}
|
||||
const records = distinctRuntimeRecords(runtimeRecords);
|
||||
const mainCandidates = records.filter((candidate) =>
|
||||
isCurrentGameChatMainRuntime(root, candidate),
|
||||
);
|
||||
const main = mainCandidates.length === 1 ? mainCandidates[0]! : null;
|
||||
const dynamicArtChildren = main
|
||||
? records
|
||||
.filter((candidate) =>
|
||||
isCurrentGameChatDynamicArtRuntime(main, candidate),
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.updatedAt - right.updatedAt ||
|
||||
left.agentId.localeCompare(right.agentId) ||
|
||||
left.runId.localeCompare(right.runId),
|
||||
)
|
||||
: [];
|
||||
const activeArtChildren = dynamicArtChildren.filter(
|
||||
(candidate) => !isGameChatRuntimeTerminalState(candidate),
|
||||
);
|
||||
return {
|
||||
root,
|
||||
main,
|
||||
dynamicArtChildren,
|
||||
hasConflict: mainCandidates.length > 1 || activeArtChildren.length > 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function gameChatLineageCollaboratingRuntimes(
|
||||
lineage: GameChatRuntimeLineage | null,
|
||||
) {
|
||||
return lineage?.main ? [lineage.main, ...lineage.dynamicArtChildren] : [];
|
||||
}
|
||||
|
||||
export function gameChatRuntimeBelongsToLineage(
|
||||
lineage: GameChatRuntimeLineage | null,
|
||||
candidate: AgentRuntimeState | null | undefined,
|
||||
) {
|
||||
return Boolean(
|
||||
candidate &&
|
||||
gameChatLineageCollaboratingRuntimes(lineage).some((runtime) =>
|
||||
sameGameChatRuntimeIdentity(runtime, candidate),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function manifestPrimaryTaskStatus(manifest: GameCreationAppManifest | null) {
|
||||
const tasks =
|
||||
manifest?.tasks.filter((task) => task.id === GAME_CHAT_PRIMARY_TASK_ID) ??
|
||||
[];
|
||||
return tasks.length === 1 ? tasks[0]!.status : null;
|
||||
}
|
||||
|
||||
export function projectGameChatPrimaryProgress(
|
||||
manifest: GameCreationAppManifest | null,
|
||||
lineage: GameChatRuntimeLineage | null,
|
||||
): GameChatPrimaryProgress {
|
||||
if (!lineage) {
|
||||
return { completed: 0, total: 1, status: 'pending' };
|
||||
}
|
||||
if (lineage.hasConflict || isGameChatRuntimeInReconciliation(lineage.root)) {
|
||||
return { completed: 0, total: 1, status: 'needs-reconciliation' };
|
||||
}
|
||||
if (!lineage.main) {
|
||||
return { completed: 0, total: 1, status: 'pending' };
|
||||
}
|
||||
const relatedRuntimes = gameChatLineageCollaboratingRuntimes(lineage);
|
||||
if (relatedRuntimes.some(isGameChatRuntimeInReconciliation)) {
|
||||
return { completed: 0, total: 1, status: 'needs-reconciliation' };
|
||||
}
|
||||
const main = lineage.main;
|
||||
if (
|
||||
main.status === 'failed' ||
|
||||
main.phase === 'failed' ||
|
||||
main.status === 'cancelled' ||
|
||||
main.phase === 'cancelled'
|
||||
) {
|
||||
return { completed: 0, total: 1, status: 'failed' };
|
||||
}
|
||||
if (!isGameChatRuntimeTerminalState(main)) {
|
||||
return { completed: 0, total: 1, status: 'running' };
|
||||
}
|
||||
const manifestStatus = manifestPrimaryTaskStatus(manifest);
|
||||
if (manifestStatus === 'completed') {
|
||||
return { completed: 1, total: 1, status: 'completed' };
|
||||
}
|
||||
if (manifestStatus === 'failed') {
|
||||
return { completed: 0, total: 1, status: 'failed' };
|
||||
}
|
||||
return { completed: 0, total: 1, status: 'pending' };
|
||||
}
|
||||
|
||||
function runtimeOwnOrderedEvents(runtime: AgentRuntimeState) {
|
||||
return (runtime.recentEvents ?? []).flatMap((event, order) =>
|
||||
event.agentId === runtime.agentId &&
|
||||
event.taskId === runtime.taskId &&
|
||||
event.sessionId === runtime.sessionId &&
|
||||
event.runId === runtime.runId &&
|
||||
event.source === runtime.source &&
|
||||
Number.isSafeInteger(event.updatedAt) &&
|
||||
event.updatedAt >= 0
|
||||
? [{ event, order }]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
function staticSmokeObservationPassed(summary: string) {
|
||||
if (
|
||||
!summary.startsWith('command.run_limited:') ||
|
||||
!/(?:^|[\s·])game\.static_smoke(?:$|[\s,。;])/u.test(summary)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return summary.startsWith('command.run_limited:ok') ? true : false;
|
||||
}
|
||||
|
||||
function orderedEventAtOrBefore(
|
||||
left: OrderedRuntimeEvent,
|
||||
right: OrderedRuntimeEvent,
|
||||
) {
|
||||
return (
|
||||
left.event.updatedAt < right.event.updatedAt ||
|
||||
(left.event.updatedAt === right.event.updatedAt &&
|
||||
left.order <= right.order)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldReplacePreviewCandidate(
|
||||
current: PreviewValidationCandidate | null,
|
||||
candidate: PreviewValidationCandidate,
|
||||
) {
|
||||
if (!current) {
|
||||
return true;
|
||||
}
|
||||
if (candidate.revision !== current.revision) {
|
||||
return candidate.revision > current.revision;
|
||||
}
|
||||
if (candidate.validatedAt !== current.validatedAt) {
|
||||
return candidate.validatedAt > current.validatedAt;
|
||||
}
|
||||
if (candidate.playable !== current.playable) {
|
||||
return !candidate.playable;
|
||||
}
|
||||
return candidate.order > current.order;
|
||||
}
|
||||
|
||||
export function latestGameChatPlayableRevision(
|
||||
lineage: GameChatRuntimeLineage | null,
|
||||
): GameChatPlayableRevision | null {
|
||||
if (!lineage?.main || lineage.hasConflict) {
|
||||
return null;
|
||||
}
|
||||
const orderedEvents = runtimeOwnOrderedEvents(lineage.main);
|
||||
const staticSmokeEvents = orderedEvents.filter(
|
||||
({ event }) =>
|
||||
event.eventType === 'observation' &&
|
||||
staticSmokeObservationPassed(event.summary) !== null,
|
||||
);
|
||||
const latestStaticSmoke =
|
||||
staticSmokeEvents.reduce<OrderedRuntimeEvent | null>(
|
||||
(latest, candidate) => {
|
||||
if (
|
||||
!latest ||
|
||||
candidate.event.updatedAt > latest.event.updatedAt ||
|
||||
(candidate.event.updatedAt === latest.event.updatedAt &&
|
||||
candidate.order > latest.order)
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
return latest;
|
||||
},
|
||||
null,
|
||||
);
|
||||
if (
|
||||
!latestStaticSmoke ||
|
||||
staticSmokeObservationPassed(latestStaticSmoke.event.summary) !== true
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let latestPreview: PreviewValidationCandidate | null = null;
|
||||
for (const orderedEvent of orderedEvents) {
|
||||
const { event, order } = orderedEvent;
|
||||
if (
|
||||
event.eventType !== 'observation' ||
|
||||
!event.summary.startsWith('preview.validate:') ||
|
||||
!event.detail?.trim().startsWith('{')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const detail = JSON.parse(event.detail) as {
|
||||
passed?: unknown;
|
||||
playtestPassed?: unknown;
|
||||
revision?: unknown;
|
||||
};
|
||||
if (
|
||||
typeof detail.revision !== 'number' ||
|
||||
!Number.isSafeInteger(detail.revision) ||
|
||||
detail.revision <= 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const candidate: PreviewValidationCandidate = {
|
||||
runId: lineage.root.runId,
|
||||
mainRunId: lineage.main.runId,
|
||||
revision: detail.revision,
|
||||
validatedAt: event.updatedAt,
|
||||
order,
|
||||
playable:
|
||||
event.summary.startsWith('preview.validate:ok') &&
|
||||
detail.passed === true &&
|
||||
detail.playtestPassed === true,
|
||||
};
|
||||
if (shouldReplacePreviewCandidate(latestPreview, candidate)) {
|
||||
latestPreview = candidate;
|
||||
}
|
||||
} catch {
|
||||
// Malformed public evidence cannot establish a playable revision.
|
||||
}
|
||||
}
|
||||
if (!latestPreview?.playable) {
|
||||
return null;
|
||||
}
|
||||
const previewEvent = orderedEvents.find(
|
||||
({ event, order }) =>
|
||||
event.updatedAt === latestPreview?.validatedAt &&
|
||||
order === latestPreview.order,
|
||||
);
|
||||
if (
|
||||
!previewEvent ||
|
||||
!orderedEventAtOrBefore(latestStaticSmoke, previewEvent)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
runId: latestPreview.runId,
|
||||
mainRunId: latestPreview.mainRunId,
|
||||
revision: latestPreview.revision,
|
||||
validatedAt: latestPreview.validatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function canArchiveGameChatStage(
|
||||
lineage: GameChatRuntimeLineage | null,
|
||||
manifest: GameCreationAppManifest | null,
|
||||
) {
|
||||
if (!lineage?.main || lineage.hasConflict) {
|
||||
return false;
|
||||
}
|
||||
const relatedRuntimes = [
|
||||
lineage.root,
|
||||
lineage.main,
|
||||
...lineage.dynamicArtChildren,
|
||||
];
|
||||
if (
|
||||
relatedRuntimes.some(isGameChatRuntimeInReconciliation) ||
|
||||
relatedRuntimes.some((runtime) => !isGameChatRuntimeTerminalState(runtime))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return ['completed', 'failed'].includes(
|
||||
manifestPrimaryTaskStatus(manifest) ?? '',
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './gameChatRuntimeProjection';
|
||||
export * from './model';
|
||||
export * from './panels';
|
||||
|
||||
@@ -20,6 +20,12 @@ import type {
|
||||
LocalConversationMessageRecord,
|
||||
TauriInvoke,
|
||||
} from '../../app/types';
|
||||
import {
|
||||
GAME_CHAT_SUPERVISOR_SOURCE,
|
||||
gameChatLineageCollaboratingRuntimes,
|
||||
isGameChatSupervisorRoot,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
} from './gameChatRuntimeProjection';
|
||||
|
||||
const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX = 'runtime-public-status-';
|
||||
const AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX = 'runtime-task-';
|
||||
@@ -1412,14 +1418,22 @@ export function projectSupervisorCollaboratingAgentRuntimes(
|
||||
if (!supervisorRuntime?.runId) {
|
||||
return [];
|
||||
}
|
||||
if (supervisorRuntime.source === GAME_CHAT_SUPERVISOR_SOURCE) {
|
||||
return isGameChatSupervisorRoot(supervisorRuntime)
|
||||
? gameChatLineageCollaboratingRuntimes(
|
||||
projectCurrentGameChatRuntimeLineage(
|
||||
supervisorRuntime,
|
||||
runtimeByAgentId,
|
||||
),
|
||||
)
|
||||
: [];
|
||||
}
|
||||
const runtimesByAgentId = new Map<string, AgentRuntimeState>();
|
||||
for (const runtime of Object.values(runtimeByAgentId)) {
|
||||
const isVisibleChildSource =
|
||||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||||
runtime?.source ?? '',
|
||||
) ||
|
||||
(supervisorRuntime.source === 'project-supervisor-game-chat' &&
|
||||
runtime?.source === 'agent-ready-task-scheduler');
|
||||
const isVisibleChildSource = [
|
||||
'agent-delegate',
|
||||
'agent-delegate-retry',
|
||||
].includes(runtime?.source ?? '');
|
||||
if (
|
||||
!runtime ||
|
||||
runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID ||
|
||||
|
||||
@@ -46,6 +46,10 @@ import {
|
||||
projectSupervisorPendingActionPresentation,
|
||||
projectSupervisorRuntimeStatusLabel,
|
||||
} from './model';
|
||||
import {
|
||||
isCurrentGameChatDynamicArtRuntime,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
} from './gameChatRuntimeProjection';
|
||||
|
||||
export function AgentGoalStatusPanel({
|
||||
goal,
|
||||
@@ -721,6 +725,10 @@ export function ProjectSupervisorRuntimePanel({
|
||||
runtime,
|
||||
runtimeByAgentId,
|
||||
);
|
||||
const gameChatLineage = projectCurrentGameChatRuntimeLineage(
|
||||
runtime,
|
||||
runtimeByAgentId,
|
||||
);
|
||||
const visibleSnapshotKey = [
|
||||
runtime?.runId ?? '',
|
||||
runtime?.status ?? '',
|
||||
@@ -975,8 +983,14 @@ export function ProjectSupervisorRuntimePanel({
|
||||
professionalRuntime.pendingToolAction ?? null;
|
||||
const professionalResult =
|
||||
professionalResultsByAgentId[professionalRuntime.agentId];
|
||||
const dynamicArtRetryUnsupported =
|
||||
isCurrentGameChatDynamicArtRuntime(
|
||||
gameChatLineage?.main,
|
||||
professionalRuntime,
|
||||
);
|
||||
const canRetryProfessional =
|
||||
!supervisorIsTerminal &&
|
||||
!dynamicArtRetryUnsupported &&
|
||||
!professionalPendingAction &&
|
||||
(professionalRuntime.status === 'failed' ||
|
||||
professionalRuntime.phase === 'failed') &&
|
||||
@@ -1169,7 +1183,9 @@ export function ProjectSupervisorRuntimePanel({
|
||||
(professionalRuntime.status === 'failed' ||
|
||||
professionalRuntime.phase === 'failed') ? (
|
||||
<small className="project-runtime-retry-feedback">
|
||||
请先重试项目总控,再由新总控继续安排此任务
|
||||
{dynamicArtRetryUnsupported
|
||||
? '请继续 game-chat 对话,由下一轮程序原型 Agent 重新审计缺口后委派'
|
||||
: '请先重试项目总控,再由新总控继续安排此任务'}
|
||||
</small>
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
+38
-48
@@ -24,9 +24,13 @@ import type {
|
||||
} from '../../app/types';
|
||||
import {
|
||||
formatAgentRuntimeEvent,
|
||||
GAME_CHAT_DYNAMIC_ART_AGENT_IDS,
|
||||
GAME_CHAT_PRIMARY_TASK_ID,
|
||||
isAgentRuntimeTerminalState,
|
||||
isMudPointInsufficientRuntimeError,
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectGameChatPrimaryProgress,
|
||||
projectNameFromPath,
|
||||
projectProfessionalAgentLabel,
|
||||
projectRuntimePlanProgress,
|
||||
@@ -62,13 +66,8 @@ export type GameChatRuntimeEvent = {
|
||||
const GAME_CHAT_RUNTIME_EVENT_MESSAGE_PREFIX = 'game-chat-runtime-event:';
|
||||
const GAME_CHAT_FINAL_REPLY_MESSAGE_PREFIX = 'game-chat-final-reply:';
|
||||
const GAME_CHAT_FINAL_REPLY_AGENT_IDS = new Set([
|
||||
'design-director',
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
'code-director',
|
||||
'code-prototype',
|
||||
'preview-readiness',
|
||||
'preview-playtest',
|
||||
GAME_CHAT_PRIMARY_TASK_ID,
|
||||
...GAME_CHAT_DYNAMIC_ART_AGENT_IDS,
|
||||
]);
|
||||
const GAME_CHAT_INTERNAL_RUNTIME_EVENT_TYPES = new Set([
|
||||
'tool.request',
|
||||
@@ -329,18 +328,10 @@ export function gameChatMudPointInterruptionText(
|
||||
runtime: AgentRuntimeState,
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||||
) {
|
||||
const childRuntimes = Object.values(runtimeByAgentId).filter(
|
||||
(childRuntime): childRuntime is AgentRuntimeState =>
|
||||
childRuntime !== undefined &&
|
||||
childRuntime.parentAgentId === runtime.agentId &&
|
||||
childRuntime.parentRunId === runtime.runId &&
|
||||
[
|
||||
'agent-delegate',
|
||||
'agent-delegate-retry',
|
||||
'agent-ready-task-scheduler',
|
||||
].includes(childRuntime.source),
|
||||
);
|
||||
const relatedRuntimes = [runtime, ...childRuntimes];
|
||||
const relatedRuntimes = [
|
||||
runtime,
|
||||
...projectSupervisorCollaboratingAgentRuntimes(runtime, runtimeByAgentId),
|
||||
];
|
||||
return relatedRuntimes.some(
|
||||
(relatedRuntime) =>
|
||||
relatedRuntime.error &&
|
||||
@@ -442,9 +433,11 @@ function runtimeEvidenceEvents(
|
||||
|
||||
function latestEvidenceEvent(
|
||||
events: ReturnType<typeof runtimeEvidenceEvents>,
|
||||
predicate: (event: AgentRuntimeEventRecord) => boolean,
|
||||
predicate: (
|
||||
item: ReturnType<typeof runtimeEvidenceEvents>[number],
|
||||
) => boolean,
|
||||
) {
|
||||
return events.find(({ event }) => predicate(event)) ?? null;
|
||||
return events.find(predicate) ?? null;
|
||||
}
|
||||
|
||||
export function formatGameChatRuntimeText(text: string) {
|
||||
@@ -463,29 +456,20 @@ export function buildGameChatProgressEvidence(
|
||||
if (!runtime?.runId) {
|
||||
return null;
|
||||
}
|
||||
const fastPathTaskIds = new Set([
|
||||
'design-director',
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
'code-director',
|
||||
'code-prototype',
|
||||
'preview-readiness',
|
||||
'preview-playtest',
|
||||
]);
|
||||
const tasks =
|
||||
manifest?.tasks.filter((task) => fastPathTaskIds.has(task.id)) ?? [];
|
||||
const completedTasks = tasks.filter(
|
||||
(task) => task.status === 'completed',
|
||||
).length;
|
||||
const runningTasks = tasks.filter((task) =>
|
||||
['running', 'waiting-for-confirmation'].includes(task.status),
|
||||
).length;
|
||||
const failedTasks = tasks.filter((task) => task.status === 'failed').length;
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(
|
||||
runtime,
|
||||
runtimeByAgentId,
|
||||
);
|
||||
const primaryProgress = projectGameChatPrimaryProgress(manifest, lineage);
|
||||
const plan = projectRuntimePlanProgress(runtime);
|
||||
const taskParts = [
|
||||
tasks.length > 0 ? `任务图 ${completedTasks}/${tasks.length}` : null,
|
||||
runningTasks > 0 ? `进行中 ${runningTasks}` : null,
|
||||
failedTasks > 0 ? `失败 ${failedTasks}` : null,
|
||||
primaryProgress.status === 'completed'
|
||||
? '主阶段 1/1'
|
||||
: primaryProgress.status === 'failed'
|
||||
? '主阶段失败'
|
||||
: primaryProgress.status === 'needs-reconciliation'
|
||||
? '主阶段待核对'
|
||||
: '主阶段 0/1',
|
||||
plan.total > 0 ? `计划 ${plan.completed}/${plan.total}` : null,
|
||||
].filter(Boolean);
|
||||
const activeAgents = projectSupervisorCollaboratingAgentRuntimes(
|
||||
@@ -504,33 +488,39 @@ export function buildGameChatProgressEvidence(
|
||||
return compactProgressText(parts.join(' · '), 140);
|
||||
});
|
||||
const events = runtimeEvidenceEvents(runtime, runtimeByAgentId);
|
||||
const mainRuntime = lineage?.main ?? null;
|
||||
const preview = latestEvidenceEvent(
|
||||
events,
|
||||
(event) =>
|
||||
({ event, source }) =>
|
||||
source === mainRuntime &&
|
||||
event.eventType === 'observation' &&
|
||||
event.summary.startsWith('preview.validate:'),
|
||||
);
|
||||
const staticCheck = latestEvidenceEvent(
|
||||
events,
|
||||
(event) =>
|
||||
({ event, source }) =>
|
||||
source === mainRuntime &&
|
||||
event.eventType === 'observation' &&
|
||||
event.summary.startsWith('command.run_limited:'),
|
||||
event.summary.startsWith('command.run_limited:') &&
|
||||
/(?:^|[\s·])game\.static_smoke(?:$|[\s,。;])/u.test(event.summary),
|
||||
);
|
||||
const delegate = latestEvidenceEvent(
|
||||
events,
|
||||
(event) =>
|
||||
({ event, source }) =>
|
||||
source === mainRuntime &&
|
||||
event.eventType === 'action' &&
|
||||
event.summary === '调用工具 agent.delegate',
|
||||
);
|
||||
const imageInspection = latestEvidenceEvent(
|
||||
events,
|
||||
(event) =>
|
||||
({ event }) =>
|
||||
event.eventType === 'observation' &&
|
||||
event.summary.startsWith('image.inspect:'),
|
||||
);
|
||||
const mutation = latestEvidenceEvent(
|
||||
events,
|
||||
(event) =>
|
||||
({ event, source }) =>
|
||||
source === mainRuntime &&
|
||||
event.eventType === 'observation' &&
|
||||
(event.summary.startsWith('file.patch:') ||
|
||||
event.summary.startsWith('file.write:')),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
import type {
|
||||
AgentRuntimeEventRecord,
|
||||
AgentRuntimeResponseStream,
|
||||
AgentRuntimeResult,
|
||||
AgentRuntimeState,
|
||||
@@ -15,10 +18,17 @@ import {
|
||||
projectRuntimeVisibleCurrentWork,
|
||||
projectRuntimeVisibleError,
|
||||
projectSupervisorChatRuntimeStatus,
|
||||
projectSupervisorCollaboratingAgentRuntimes,
|
||||
projectSupervisorResponseStreamIdentity,
|
||||
projectSupervisorVisibleConversationText,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
import {
|
||||
canArchiveGameChatStage,
|
||||
latestGameChatPlayableRevision,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectGameChatPrimaryProgress,
|
||||
} from '../src/features/agent-runtime/gameChatRuntimeProjection';
|
||||
|
||||
describe('Runtime-owned public statuses', () => {
|
||||
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
|
||||
@@ -669,3 +679,407 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
expect(emptySummaryEvent).not.toContain(fingerprint);
|
||||
});
|
||||
});
|
||||
|
||||
function gameChatProjectionRuntime(
|
||||
overrides: Partial<AgentRuntimeState> = {},
|
||||
): AgentRuntimeState {
|
||||
return {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'project-supervisor',
|
||||
taskId: 'project-supervisor',
|
||||
sessionId: 'game-chat-root-session',
|
||||
runId: 'game-chat-root-run',
|
||||
source: 'project-supervisor-game-chat',
|
||||
status: 'running',
|
||||
phase: 'execution',
|
||||
currentTask: '完成当前游戏创作轮次',
|
||||
currentAction: '等待单主 Agent 完成',
|
||||
plan: [],
|
||||
observations: [],
|
||||
allowedTools: [],
|
||||
lastResponse: null,
|
||||
error: null,
|
||||
updatedAt: 100,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function gameChatProjectionMain(
|
||||
root: AgentRuntimeState,
|
||||
overrides: Partial<AgentRuntimeState> = {},
|
||||
) {
|
||||
return gameChatProjectionRuntime({
|
||||
agentId: 'code-prototype',
|
||||
taskId: 'code-prototype',
|
||||
sessionId: 'game-chat-main-session',
|
||||
runId: 'game-chat-main-run',
|
||||
source: 'agent-ready-task-scheduler',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: root.runId,
|
||||
currentTask: '审计、接入并验收当前原型',
|
||||
updatedAt: 110,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function gameChatProjectionEvent(
|
||||
runtime: AgentRuntimeState,
|
||||
overrides: Partial<AgentRuntimeEventRecord>,
|
||||
): AgentRuntimeEventRecord {
|
||||
return {
|
||||
schemaVersion: 'game-creator-runtime-event.v1',
|
||||
agentId: runtime.agentId,
|
||||
taskId: runtime.taskId,
|
||||
sessionId: runtime.sessionId,
|
||||
runId: runtime.runId,
|
||||
source: runtime.source,
|
||||
eventType: 'observation',
|
||||
status: runtime.status,
|
||||
phase: 'observation',
|
||||
summary: 'observation',
|
||||
detail: null,
|
||||
updatedAt: runtime.updatedAt,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Game-chat source-aware runtime projection', () => {
|
||||
test('projects only the current scheduler main and its nested art children', () => {
|
||||
const root = gameChatProjectionRuntime();
|
||||
const main = gameChatProjectionMain(root);
|
||||
const artDirector = gameChatProjectionRuntime({
|
||||
agentId: 'art-director',
|
||||
taskId: 'art-director',
|
||||
sessionId: 'art-director-session',
|
||||
runId: 'art-director-run',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'code-prototype',
|
||||
parentRunId: main.runId,
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
updatedAt: 120,
|
||||
});
|
||||
const legacyRetry = gameChatProjectionRuntime({
|
||||
agentId: 'art-asset-plan',
|
||||
taskId: 'art-asset-plan',
|
||||
sessionId: 'art-asset-plan-retry-session',
|
||||
runId: 'art-asset-plan-retry-run',
|
||||
source: 'agent-delegate-retry',
|
||||
parentAgentId: 'code-prototype',
|
||||
parentRunId: main.runId,
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
updatedAt: 130,
|
||||
});
|
||||
const oldMain = gameChatProjectionMain(root, {
|
||||
sessionId: 'old-main-session',
|
||||
runId: 'old-main-run',
|
||||
parentRunId: 'old-root-run',
|
||||
updatedAt: 999,
|
||||
});
|
||||
const oldRootDirectArt = gameChatProjectionRuntime({
|
||||
agentId: 'art-director',
|
||||
taskId: 'art-director',
|
||||
sessionId: 'old-root-art-session',
|
||||
runId: 'old-root-art-run',
|
||||
source: 'agent-ready-task-scheduler',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: root.runId,
|
||||
updatedAt: 998,
|
||||
});
|
||||
const fullDagPlaytest = gameChatProjectionRuntime({
|
||||
agentId: 'preview-playtest',
|
||||
taskId: 'preview-playtest',
|
||||
sessionId: 'full-dag-playtest-session',
|
||||
runId: 'full-dag-playtest-run',
|
||||
source: 'agent-ready-task-scheduler',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: root.runId,
|
||||
updatedAt: 997,
|
||||
});
|
||||
|
||||
const runtimeMap = {
|
||||
main,
|
||||
'main-task-alias': main,
|
||||
artDirector,
|
||||
legacyRetry,
|
||||
oldMain,
|
||||
oldRootDirectArt,
|
||||
fullDagPlaytest,
|
||||
};
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(root, runtimeMap);
|
||||
|
||||
expect(lineage?.main).toBe(main);
|
||||
expect(lineage?.dynamicArtChildren).toEqual([artDirector, legacyRetry]);
|
||||
expect(lineage?.hasConflict).toBe(false);
|
||||
expect(
|
||||
projectSupervisorCollaboratingAgentRuntimes(root, runtimeMap),
|
||||
).toEqual([main, artDirector, legacyRetry]);
|
||||
|
||||
const malformedGameChatRoot = {
|
||||
...root,
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: 'ancestor-run',
|
||||
};
|
||||
expect(
|
||||
projectSupervisorCollaboratingAgentRuntimes(malformedGameChatRoot, {
|
||||
directDelegate: {
|
||||
...artDirector,
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: malformedGameChatRoot.runId,
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('uses only the current main for the 0/1 progress denominator', () => {
|
||||
const root = gameChatProjectionRuntime();
|
||||
const activeMain = gameChatProjectionMain(root);
|
||||
const manifest = createGameCreationAppManifest('project-1', '单主进度');
|
||||
manifest.tasks = manifest.tasks.map((task) =>
|
||||
task.id === 'code-prototype'
|
||||
? { ...task, status: 'completed' as const }
|
||||
: task,
|
||||
);
|
||||
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(root, {}),
|
||||
),
|
||||
).toEqual({ completed: 0, total: 1, status: 'pending' });
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(root, [activeMain]),
|
||||
),
|
||||
).toEqual({ completed: 0, total: 1, status: 'running' });
|
||||
|
||||
const completedMain = {
|
||||
...activeMain,
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
};
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(root, [completedMain]),
|
||||
),
|
||||
).toEqual({ completed: 1, total: 1, status: 'completed' });
|
||||
|
||||
const failedMain = {
|
||||
...activeMain,
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
};
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(root, [failedMain]),
|
||||
),
|
||||
).toEqual({ completed: 0, total: 1, status: 'failed' });
|
||||
|
||||
const reconciliationRoot = {
|
||||
...root,
|
||||
status: 'needs-reconciliation',
|
||||
phase: 'needs-reconciliation',
|
||||
};
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(reconciliationRoot, []),
|
||||
),
|
||||
).toEqual({
|
||||
completed: 0,
|
||||
total: 1,
|
||||
status: 'needs-reconciliation',
|
||||
});
|
||||
|
||||
const conflictingMain = gameChatProjectionMain(root, {
|
||||
sessionId: 'conflicting-main-session',
|
||||
runId: 'conflicting-main-run',
|
||||
});
|
||||
expect(
|
||||
projectGameChatPrimaryProgress(
|
||||
manifest,
|
||||
projectCurrentGameChatRuntimeLineage(root, [
|
||||
activeMain,
|
||||
conflictingMain,
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
completed: 0,
|
||||
total: 1,
|
||||
status: 'needs-reconciliation',
|
||||
});
|
||||
});
|
||||
|
||||
test('derives a playable revision only from current-main smoke then structured preview evidence', () => {
|
||||
const root = gameChatProjectionRuntime();
|
||||
const main = gameChatProjectionMain(root, { recentEvents: [] });
|
||||
const smoke = gameChatProjectionEvent(main, {
|
||||
summary: 'command.run_limited:ok · game.static_smoke 已完成',
|
||||
updatedAt: 200,
|
||||
});
|
||||
const previewPassed = gameChatProjectionEvent(main, {
|
||||
summary: 'preview.validate:ok · 浏览器验证已通过',
|
||||
detail: JSON.stringify({
|
||||
passed: true,
|
||||
playtestPassed: true,
|
||||
revision: 7,
|
||||
}),
|
||||
updatedAt: 201,
|
||||
});
|
||||
main.recentEvents = [smoke, previewPassed];
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(root, [main]);
|
||||
|
||||
expect(latestGameChatPlayableRevision(lineage)).toEqual({
|
||||
runId: root.runId,
|
||||
mainRunId: main.runId,
|
||||
revision: 7,
|
||||
validatedAt: 201,
|
||||
});
|
||||
|
||||
const laterFailure = gameChatProjectionEvent(main, {
|
||||
summary: 'preview.validate:failed · 后续回归',
|
||||
detail: JSON.stringify({
|
||||
passed: false,
|
||||
playtestPassed: false,
|
||||
revision: 7,
|
||||
}),
|
||||
updatedAt: 202,
|
||||
});
|
||||
main.recentEvents = [smoke, previewPassed, laterFailure];
|
||||
expect(
|
||||
latestGameChatPlayableRevision(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main]),
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
main.recentEvents = [previewPassed];
|
||||
expect(
|
||||
latestGameChatPlayableRevision(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main]),
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
const oldPlaytest = gameChatProjectionRuntime({
|
||||
agentId: 'preview-playtest',
|
||||
taskId: 'preview-playtest',
|
||||
sessionId: 'old-playtest-session',
|
||||
runId: 'old-playtest-run',
|
||||
source: 'agent-ready-task-scheduler',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: root.runId,
|
||||
recentEvents: [previewPassed],
|
||||
});
|
||||
expect(
|
||||
latestGameChatPlayableRevision(
|
||||
projectCurrentGameChatRuntimeLineage(root, [oldPlaytest]),
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
main.recentEvents = [previewPassed, { ...smoke, updatedAt: 202 }];
|
||||
expect(
|
||||
latestGameChatPlayableRevision(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main]),
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
main.recentEvents = [smoke, { ...previewPassed, updatedAt: -1 }];
|
||||
expect(
|
||||
latestGameChatPlayableRevision(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main]),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('archives only after root, main, dynamic children and manifest have converged', () => {
|
||||
const root = gameChatProjectionRuntime({
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
});
|
||||
const main = gameChatProjectionMain(root, {
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
});
|
||||
const art = gameChatProjectionRuntime({
|
||||
agentId: 'art-director',
|
||||
taskId: 'art-director',
|
||||
sessionId: 'archive-art-session',
|
||||
runId: 'archive-art-run',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'code-prototype',
|
||||
parentRunId: main.runId,
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
});
|
||||
const manifest = createGameCreationAppManifest('project-2', '归档门');
|
||||
manifest.tasks = manifest.tasks.map((task) =>
|
||||
task.id === 'code-prototype'
|
||||
? { ...task, status: 'completed' as const }
|
||||
: task,
|
||||
);
|
||||
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main, art]),
|
||||
manifest,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const activeArt = { ...art, status: 'running', phase: 'execution' };
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main, activeArt]),
|
||||
manifest,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const waitingMain = {
|
||||
...main,
|
||||
status: 'running',
|
||||
phase: 'waiting-for-delegate-receipts',
|
||||
};
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(root, [waitingMain, art]),
|
||||
manifest,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const activeRoot = { ...root, status: 'running', phase: 'execution' };
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(activeRoot, [
|
||||
{ ...main, parentRunId: activeRoot.runId },
|
||||
art,
|
||||
]),
|
||||
manifest,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const reconciliationArt = {
|
||||
...art,
|
||||
status: 'needs-reconciliation',
|
||||
phase: 'needs-reconciliation',
|
||||
};
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main, reconciliationArt]),
|
||||
manifest,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const pendingManifest = createGameCreationAppManifest(
|
||||
'project-2',
|
||||
'归档门未收敛',
|
||||
);
|
||||
expect(
|
||||
canArchiveGameChatStage(
|
||||
projectCurrentGameChatRuntimeLineage(root, [main, art]),
|
||||
pendingManifest,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-11 game-chat 单主前端投影统一使用 source-aware lineage
|
||||
|
||||
- 投影身份:game-chat 前端只认 `project-supervisor` 且 source=`project-supervisor-game-chat` 的无父根 Run;其唯一 main 必须是 parent 指向该根、source=`agent-ready-task-scheduler` 的 `code-prototype`;动态美术 child 只允许 parent 指向该 main 的 `art-director | art-asset-plan`,source 为 `agent-delegate | agent-delegate-retry`。旧根直属美术、固定 DAG 节点、错误 source/parent/run 和并发冲突一律不进入当前轮投影。
|
||||
- 消费者边界:game-chat 主进度固定为 `0/1`、`1/1`、失败或待核对;专业 final-reply 同时要求角色 allowlist 与精确 run 谱系;可玩 revision 只由当前 main 的成功 `game.static_smoke` 之后、`passed=true + playtestPassed=true` 的结构化 `preview.validate` 建立,同 revision 后续失败优先。完整 16 任务 DAG 的投影与试玩 owner 不变。
|
||||
- retry 投影:动态美术 child,包括遗留 `agent-delegate-retry`,只作诊断展示且不提供通用 retry 控件;根终态时恢复提示固定引导用户继续 game-chat,由下一轮 main 重新审计缺口后首次委派。普通 DAG child 和非美术委派 retry 不受影响。
|
||||
- 归档边界:`【Supervisor 阶段记录】` 只有在 root、唯一 main、当前动态美术 children 与 manifest `code-prototype` 全部终态且无冲突/reconciliation 时才可写入。等待期间按完整 `agent/session/run` 身份保存 root-scoped Runtime 快照,避免 current-by-agent map 被新 root 覆盖后把新旧证据串线;消息 ID 固定绑定 root run,重载与 hydration 幂等。
|
||||
- 范围:M0B-2 只改前端纯投影、接线、文案与回归,不修改后端 DTO/schema/delivery/route,不迁移历史 manifest,也不提前实现 M1~M3。
|
||||
|
||||
## 2026-08-11 game-chat 动态美术 child 通用 retry 失败关闭
|
||||
|
||||
- 裁决:game-chat 动态美术 child(`art-director` / `art-asset-plan`,parent 为单主 `code-prototype`、root 绑定 `project-supervisor-game-chat` source)不允许走通用 Agent Runtime retry。retry 入口识别该类 child 后,在创建 successor run、delegationId、task journal 或其它 durable 副作用前直接返回类型化错误,引导用户继续 game-chat 对话:下一轮 main `code-prototype` 重新完成 `asset.list` 审计后,按仍存在的缺口建立新的 durable 美术委派。委派去重以 parent run 为键,跨轮自然放行;新委派产生新的 delegationId 与 durable delivery,沿用现行严格 lineage 判定,不新增 delivery 换绑、续发或可变机制。
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 2026-08-11 game-chat 阶段归档不能只保存 current-by-agent map
|
||||
|
||||
- 现象:前端 Runtime map 以 Agent ID 保存当前记录。新一轮仍会复用 `code-prototype`、`art-director`、`art-asset-plan` 这些 Agent ID;若旧 root 已终态但 main/美术 child 或 manifest 仍在收口,直接从 live map 归档会在新 root 接管后丢失旧后代,或把新轮证据误接到旧阶段记录。
|
||||
- 原因:Agent ID 只是当前视图索引,不是跨 root 的运行身份。game-chat 的正式投影身份至少需要 `agentId + sessionId + runId + source + parentAgentId + parentRunId`,而阶段记录还必须绑定 source-bound root。
|
||||
- 修复:根进入终态时按完整 `agent/session/run` 身份保存 root-scoped Runtime 快照,后续只合并同一稳定身份的更新;manifest 快照只在该 root 仍为当前 root 时捕获。归档前重新执行严格 lineage、全终态、单 main/单 active art 与 reconciliation 门禁,并用 root run 派生稳定 message ID。
|
||||
- 防回归:测试必须覆盖 root 先终态、main 或 child 仍 active、main 仍等待 delegate receipts、manifest hydration 滞后、initial hydration 和历史阶段记录去重。完整 DAG 继续使用自己的投影,不能把 game-chat selector 反向推广为全局任务图真相。
|
||||
|
||||
> 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
|
||||
|
||||
## 记录格式
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
- 对话与事件:窗口固定使用 `project-supervisor + autonomous-game-build`,继续复用 active Session、External Runner、持久 conversation、流式回复、same-run steer、工具确认与用户追问。以 `/` 开头的输入必须继续走现有内置命令解析,例如 `/preview` 只能生成 `preview.start` 确认卡,不得作为自主构建任务投递给 Supervisor。game-chat 的自主链路中,Supervisor 持久化意图后只有 `code-prototype` 是主 Agent;它可能临时委派一个受限美术 child,后者只写 `assets/**`,回执返回同一主 Run 后由主 Agent 接入与验收。界面聚合当前 Supervisor 父 run、单主 Agent 及其直接美术 child 的最新原始事件,按时间倒序稳定去重并标注 Agent;默认显示 4 条,可展开至最新 20 条。原始 `summary / detail` 仍只作 Runtime 状态投影,不直接写入 conversation。需要进入聊天的事件必须由 Rust 同步生成唯一 `eventId` 与安全 `publicText`;前端只按这两个字段形成独立 assistant 消息,无 `eventId`、空 `publicText`、legacy 事件和内部 tool / Provider / Runner 协议一律忽略。
|
||||
- 公开消息硬门:模型仍负责 Supervisor / 专业 Agent 回复的业务语义,Runtime 不根据 tool 或 Provider 事件自行补写业务结论;但用户直接投递的 Project Supervisor 根后台任务必须先落为不可执行的 `preparing / public-status-pending`,再以 `runtime-public-status-*` 稳定 message ID 把“任务已接收,正在启动处理”写入项目 conversation,成功后才转为 `pending / queued`;恢复预检只读,只能在验证到同 run accepted 消息后把该任务临时分类为可恢复,真实 resume 持有 Agent 锁后才可持久提升为 `pending / queued`;写入失败则落为 `failed / public-status-write-failed`,不得继续执行。这些 Runtime 公开状态只供 UI 展示,prompt 构建器必须按稳定前缀排除。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,必须在 task、event、state 等其它终态投影之前先幂等写入一条脱敏、用户可理解的失败消息;专业 Agent 命中该全局硬期限时,也必须通过权威 Run Profile 和根 task 将同一根终态写入项目 conversation,同时保留 child 私有 Session 状态;状态文件本身写坏也不能导致零公开结果。当前 Runtime 自称根 agent/run 时,其 session 和两个 parent 字段必须与权威根 task 一致;任一身份冲突必须失败关闭,不得以另一 session 派生第二条项目终态。前端把该前缀识别为 Runtime-owned,同秒时排在触发它的 Supervisor 用户消息之后,不二次持久化;仅根 Supervisor 的 `turn.started / turn.failed / turn.budget_exhausted` 只保留在 Runtime 详情和进度投影中,不能再生成第二条聊天消息,专业 Agent 的公开启动事件仍可见。该硬门不改变 final-reply 的唯一性;非 Supervisor 专业 Agent 的失败消息继续留在对应 Agent Session,不把私有诊断写进项目 conversation。
|
||||
- 启动恢复和续跑边界:本条取代上一条中“只有 accepted 才可恢复”的窄口径。若进程在 Supervisor 用户消息已持久、accepted 未持久之间崩溃,只读 preflight 可以把该 `preparing` 识别为可恢复,但不改写 task/conversation;真实 resume 持有 Agent 锁后必须先幂等补写 accepted,再提升为 `pending / queued`。用户消息或 accepted conversation 已落盘而辅助审计失败时,以 conversation 为公开真相继续入队,不留下“已接收但永不执行”的任务;根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。receipt / isolated-join 等带 parent 的 Supervisor continuation 不再另写 Session 终态,只保留单一后端公开事件;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,秒级时间戳下多个连续任务必须按实际 run 对应的 `user -> accepted -> terminal` 顺序交错展示。
|
||||
- Supervisor 进度播报:聊天消息流内保留且只保留一条当前 run 的 Runtime-owned 播报卡,由客户端从 manifest 任务图、Supervisor 结构化计划、`loopIteration`、当前动作、直接委派专业 Agent 及其持久事件确定性整理;显示当前轮次、任务 / 计划进度、活跃 Agent、最近试玩与静态检查、返工决定、代码修改和截图检查证据。同一 run 原位更新,切换 run 时替换,不调用额外模型、不追加持久 conversation,也不改变最终 assistant 回复的唯一性;任意详情必须有界且不展示绝对路径、Provider 元数据或内部指纹。运行详情弹窗在项目或 run 身份切换的同次提交中同步关闭,不能由延迟 effect 关闭用户在新 run 状态可见后刚打开的弹窗。
|
||||
- Supervisor 进度播报:聊天消息流内保留且只保留一条当前 run 的 Runtime-owned 播报卡。game-chat 客户端只从当前 source-bound 根、唯一 `code-prototype` scheduler main、该 main 的动态美术 child、Supervisor 结构化计划和对应持久事件确定性整理,主阶段分母固定为 `0/1` 或 `1/1`;不再读取完整 DAG 的固定七节点,也不把旧根直属美术、错误 parent/source 或 `preview-playtest` child 混入当前轮。播报显示任务 / 计划进度、活跃 Agent、最近试玩与静态检查、返工决定、代码修改和截图检查证据;同一 run 原位更新,切换 run 时替换,不调用额外模型、不追加持久 conversation,也不改变最终 assistant 回复的唯一性。任意详情必须有界且不展示绝对路径、Provider 元数据或内部指纹。运行详情弹窗在项目或 run 身份切换的同次提交中同步关闭,不能由延迟 effect 关闭用户在新 run 状态可见后刚打开的弹窗。
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task manifest 漂移:父 Supervisor 必须分别判断“能否调度新节点”和“是否存在必须等待的工作”。派生视觉需要父规划修复时不再调度新 child,但当前最新且活跃的根 Run 下,只要存在确定性 runId、scheduler source、正确父绑定且 durable journal 为 queued/running 的 ready child,父 Run 就保持 `waiting-for-manifest-tasks`,不能因旧 hydration 快照把 manifest running 覆盖成 pending 而提前 fixed-graph-stalled。game-chat child 可在相同严格身份下容忍 pending 漂移;正式产物、Canvas、revision、`game.static_smoke` 与 `preview.validate` 门禁不放宽。GUI/CLI、旧父 Run、终态、确认/用户输入/reconciliation、伪造绑定或非确定性 runId 全部失败关闭;更新根 Run 后旧 child 不得继续维持新 DAG 或投影完成。
|
||||
- Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
||||
@@ -42,12 +42,12 @@
|
||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||
- 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。
|
||||
- Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-<HTTP status>` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”;其它错误使用固定安全摘要。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。
|
||||
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待唯一 `code-prototype` 主 Run 及其所有必要美术委派都已形成真实终态,再把本轮、主 Agent 进度、是否复用/补齐素材、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 child 或 manifest 仍在 hydration 时不得以陈旧快照提前归档,要暂存终态 Runtime 并在状态刷新后重试。页面初始 hydration 若直接读到缺少阶段记录的真实终态 run,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
||||
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待唯一 `code-prototype` 主 Run、该 main 的全部动态美术 child 与 manifest 中 `code-prototype` 都形成终态,且全链没有冲突或 `needs-reconciliation`,再把本轮、主 Agent 进度、是否复用/补齐素材、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 child 或 manifest 仍在 hydration 时不得以陈旧快照提前归档;客户端以完整 `agent/session/run` 身份保留 root-scoped Runtime 快照并在状态刷新后重试,不能继续依赖会被新 run 覆盖的 current-by-agent map。页面初始 hydration 若直接读到缺少阶段记录的真实终态全链,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”使用稳定 message ID 最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
||||
- 图片成果:当前 manifest 新增或恢复已登记的 PNG / JPEG / WebP 资源时,聊天消息流同步显示 Runtime-owned “Supervisor 成果图片”卡,最多展示最新 4 张并随 manifest 原位更新。图片必须通过现有 `read_local_project_image_preview` 读取,只允许当前授权项目中 `assets/` 下的已登记资源,继续执行 `file.read` auto 权限、真实格式、大小、尺寸、普通文件、祖先目录和项目根边界校验;前端只接受返回路径、媒体类型和 `data:` 前缀与请求完全一致的结果。缩略图点击后使用独立模态查看器,支持按钮与滚轮缩放、指针拖拽、双击 / 按钮复位、Esc / 按钮 / 遮罩关闭,移动端占满视口;不得在聊天卡下方追加展开区。图片卡不写入 conversation,不解析 assistant 文本中的任意 Markdown / 绝对路径,也不开放 `.agent` 验收截图读取。
|
||||
- Run 接管:External Runner 模式下首次提交可能返回“旧 canonical state + 新 `acceptedRunId`”;页面必须以 `acceptedRunId` 作为本轮权威身份,在 state 尚未切换时显示“已投递,正在同步 Agent Runner”,并允许该 run 的 Tauri event 或轮询结果接管。不得把旧 idle state 当作本轮结果、过滤新 run 事件,自动预览授权也必须绑定 `acceptedRunId`。
|
||||
- 运行容器:当前项目没有由 Tauri 客户端 `PreviewRegistry` 返回的有效 `running` 预览时,页面只渲染聊天,不显示游戏区域或占位文案,顶部运行状态必须明确显示“预览未启动”,不得再使用含义不明的“未启动”;预览运行后自动显示 iframe,桌面端按“游戏 2 / 聊天 1”分栏,移动端改为上下布局。预览停止、失败或切换项目后立即移除 iframe。运行容器继续只接受当前授权项目的 `http://127.0.0.1:*`,复用现有 CSP、iframe sandbox、autoplay、fullscreen 和 gamepad 约束;远程 URL、`file://`、手填地址或陈旧 manifest 状态均不得显示。
|
||||
- 预览进程归属:External Runner 与 Tauri 客户端位于不同进程,双方 `PreviewRegistry`、server 子进程句柄和 running 状态严格进程内隔离;Runner 为 `preview.validate` 持有或回收的预览进程不能作为用户可见预览,也不能据此伪造 Tauri registry 的 running 状态。game-chat 用户可见 preview 必须由 Tauri 客户端启动、持有和停止,iframe 只使用同一 Tauri registry 返回的 loopback URL。
|
||||
- 自动启动门禁:客户端只接受当前 accepted Supervisor 父 run 下真实 `preview-playtest` scheduler child 的结构化 `preview.validate` 事件,同 revision 采用最新事件且同时间失败优先;成功证据的 revision 必须精确等于当前项目原子 sidecar revision。客户端向 Tauri `preview.start` 传入 `expectedRevision`,后端在取得项目写锁后再次比对再启动,以闭合检查 / 启动 TOCTOU。same-run steer 的一次性授权使用带 revision / validation cursor 和唯一 generation ID 的 v2 记录,旧事件和旧异步 attempt 不得消费后续授权。旧 attempt 返回后的补偿清理按完整 preview identity 原子停止 registry server,不能让项目 stop policy 或写锁竞争造成后台 server 泄漏;若同项目新 server 已接管,则不能把其持久状态覆盖为 stopped。
|
||||
- 自动启动门禁:客户端只接受当前 accepted、source-bound Supervisor 父 run 下唯一 `code-prototype` scheduler main 本人的证据序列:该 main 先成功执行 `game.static_smoke`,随后产生结构化 `preview.validate`,且 detail 同时满足 `passed=true`、`playtestPassed=true` 和正整数 revision。同 revision 采用最新事件且失败优先;错误 main/source/parent、旧固定 `preview-playtest` child、根 Supervisor、动态美术 child 和 smoke 之前的 preview 都不能建立可玩 revision。成功证据的 revision 必须精确等于当前项目原子 sidecar revision。客户端向 Tauri `preview.start` 传入 `expectedRevision`,后端在取得项目写锁后再次比对再启动,以闭合检查 / 启动 TOCTOU。same-run steer 的一次性授权使用带 revision / validation cursor 和唯一 generation ID 的 v2 记录,旧事件和旧异步 attempt 不得消费后续授权。旧 attempt 返回后的补偿清理按完整 preview identity 原子停止 registry server,不能让项目 stop policy 或写锁竞争造成后台 server 泄漏;若同项目新 server 已接管,则不能把其持久状态覆盖为 stopped。
|
||||
- 固定试玩契约:`generic-v1` 初始状态必须为 `ready` 且 `level > 0`;点击 start 后 sequence 必须推进、phase 必须进入 `playing`,并先持续观察 2 秒、取得至少 8 个实际样本,期间保持 `playing`,以确认玩家获得正常操作机会。随后必须点击唯一可见、启用且真实可交互的 `data-playtest-id="primary-action"` 控件;该控件必须映射游戏的真实主要玩法操作,并以 sequence 相对点击前严格推进证明操作已被接受。玩家获得这次正常操作机会之前进入 `won | lost` 属于过早结束并失败;操作被接受后的单次 `lost` 是合法游戏结局,但不能成为所有受控尝试的唯一结果;若主要操作后仍为 `playing`,则继续观察 3 秒并取得至少 12 个实际样本,`won` 可提前证明非失败推进。点击 restart 后 sequence 必须再次推进并恢复到 `ready | playing`,随后持续观察 3 秒且取得至少 12 个实际样本。若首轮结果为 `lost`,重开稳定后必须再执行一次必要的 start、2 秒 / 8 样本操作机会和真实 primary-action;第二次必须进入或保持 `playing`(再观察 3 秒 / 12 样本且不得转为 `lost`)或进入 `won`,两次都固定 `lost` 代表无法正常推进的恶性 bug,必须失败。各观察窗口内 sequence 不得回退,restart 窗口只能保持 `ready | playing`;样本数门槛不能替代时长门槛,窗口末端必须强制再读取一次有效状态,不能只在前段快速取得足够样本后提前通过。控件 selector、观察时长、最少样本数、终态边界、非失败推进、末端覆盖、sequence 单调 / 严格推进规则及完整 required assertions 都进入 scenario fingerprint。读取旧 fingerprint 回执和检查 plan liveness 时,把合同升级造成的 fingerprint 不匹配视为 stale missing,允许同一 run 重新执行 `preview.validate` 自愈;身份、路径、digest 或内容完整性篡改仍失败关闭。最终完成门每次按当前合同重算 fingerprint,并严格拒绝旧 fingerprint、旧 assertion 集或仅保存历史 `passed=true` 的证据。
|
||||
- 试玩证据展示:game-chat 的进度卡、可玩 revision 和自动预览只接受结构化 `preview.validate` detail 同时满足 `passed=true` 与 `playtestPassed=true`;工具 summary 中的 `:ok` 不能作为兜底。`image.inspect` 的 `status=ok` 只代表工具执行成功,不代表视觉验收通过;结构化 `passed=null` 或缺少布尔结论时,UI 必须以中性“截图分析完成”展示,只有显式 `passed=true` 才能显示“截图检查通过”。
|
||||
- 一次性自动预览授权:用户在该入口成功提交本轮自主生成需求,即视为对“当前项目 + 当前 Supervisor 父 run”的一次 `preview.start` 授权。授权以仅含项目路径与 accepted parent runId 的客户端本地记录持久化,App / WebView 重启后仍可恢复,但项目或 run 身份不匹配时不得使用。只有当前 accepted parent run 成功完成 `preview.validate` 且给出有效 revision 后,客户端才可消费授权,由 Tauri 首次启动并自动展示该 revision 的用户可见预览;一次授权最多成功启动一个 Tauri preview server,并必须继续走现有权限、项目写锁、审计和客户端 `PreviewRegistry` 链路。项目或 Agent 策略的显式 deny 始终优先,不得被此授权绕过。启动成功、显式 deny、非瞬时失败、父 run 在首版验证前终止或切换项目后授权失效;`preview.start` 恰逢项目写锁竞争属于瞬时失败,不消费授权,释放写锁后由同一轮询链路重试。
|
||||
|
||||
@@ -1428,6 +1428,7 @@ M0 文档 PR 本身最低验证:Markdown 结构与三张 Mermaid 图可解析
|
||||
| owner 产物验证 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs` | M0-3 / M0A-2 让四个 pre-code fixed owner 复用 canonical 路径映射,由 Runtime 内部验固定产物;Provider 不见验证工具,code / preview 节点继续真实 smoke |
|
||||
| approvedGddRef | 当前仓库无匹配实现 | M2 从 command 到 task/run/completion/context 全链新增 |
|
||||
| game-chat retry 边界 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs:7-39,120-134`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs:740-772,885-905` | M0-4 / 工作包 M0B-1 统一 delegate/retry assets-only lineage |
|
||||
| game-chat 前端投影 | `apps/ai-game-creator-shell/src/features/agent-runtime/gameChatRuntimeProjection.ts`、`src/features/agent-runtime/model.ts`、`src/features/project-workspace/SupervisorChatOnlyView.tsx`、`src/App.tsx` | M0-4 / 工作包 M0B-2 只认 root → 单主 `code-prototype` → 动态美术 child 的 source-aware lineage;进度、final-reply、可玩 revision、retry 控件与阶段归档共用该身份边界 |
|
||||
|
||||
## 23. 分期门禁与完成定义
|
||||
|
||||
@@ -1450,6 +1451,8 @@ M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可
|
||||
|
||||
`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的安全边界:首次 `agent-delegate` 继续按严格 delivery/route/Canvas lineage 只写 `assets/**`;该类 child 的通用 retry 在入队前返回 `kind=game-chat-dynamic-art-retry-unsupported`,遗留或伪造的 `agent-delegate-retry` 只读且全部 mutation 失败关闭。恢复只走跨轮:用户继续 game-chat 对话,由下一轮 main `code-prototype` 重新审计并按仍存在的缺口创建新的首次委派;同一 main run 对同一 target 的第二次委派继续拒绝,新一轮以新 parent run、新 delegationId、targetRunId 与 delivery 全链恢复。`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。
|
||||
|
||||
2026-08-11 的 M0B-2 落地固定为 presentation-only:新增纯投影模块,结构身份严格限定为 `project-supervisor-game-chat` root → `agent-ready-task-scheduler` 的唯一 `code-prototype` main → 该 main 下 `agent-delegate | agent-delegate-retry` 的 `art-director | art-asset-plan`;retry source 只作遗留诊断展示,不恢复通用 retry 控件。game-chat 进度只显示主阶段 `0/1`、`1/1`、失败或待核对;final-reply 除角色 allowlist 外还必须匹配精确 run 谱系;可玩 revision 只由当前 main 成功 smoke 后的结构化双视口试玩通过建立,同 revision 后续失败优先;阶段记录须等待 root、main、动态 child 和 manifest 主任务终态且无 reconciliation,并使用 root-scoped Runtime 快照与稳定 root-run message ID 幂等归档。该工作包不修改后端 DTO/schema/delivery/route,不迁移历史 manifest,也不实现 M1~M3。
|
||||
|
||||
## 24. 最终不变量摘要
|
||||
|
||||
- 策划与构建是两个不同 profile/source 的 run,由用户动作连接,不由 Agent 自行升级。
|
||||
|
||||
Reference in New Issue
Block a user