修复:收紧 game-chat 阶段投影与归档竞态
严格校验 game-chat root、单主和嵌套美术 lineage,并同步 Launcher 摘要与终态后代状态。 禁用声称 game-chat 动态美术谱系的通用重试控件,保留完整 DAG 直属美术重试。 冻结终态轮次的 Runtime 与 manifest 快照,覆盖延迟刷新期间快速开始下一轮的持久归档路径。 补齐纯函数、AppSurface 回归与 M0 状态文档。
This commit is contained in:
@@ -107,6 +107,7 @@ import {
|
||||
ensureProjectSupervisorActiveSessionId,
|
||||
isAgentFinalizationMessageId,
|
||||
isAgentRuntimeTerminalState,
|
||||
isGameChatSupervisorRoot,
|
||||
isMissingAgentRuntimeResumeCommandError,
|
||||
isRuntimeConfigMissingError,
|
||||
latestGameChatPlayableRevision,
|
||||
@@ -270,6 +271,7 @@ type PendingGameChatStageArchive = {
|
||||
rootRuntime: AgentRuntimeState;
|
||||
runtimeRecords: AgentRuntimeState[];
|
||||
manifestSnapshot: GameCreationAppManifest | null;
|
||||
awaitingConversationSync: boolean;
|
||||
};
|
||||
|
||||
function mergeGameChatRuntimeSnapshots(
|
||||
@@ -898,6 +900,9 @@ export function App({
|
||||
incoming,
|
||||
runtime,
|
||||
);
|
||||
if (gameChatOnly && !isGameChatSupervisorRoot(runtime)) {
|
||||
nextStream = null;
|
||||
}
|
||||
const candidateStream = nextStream;
|
||||
if (candidateStream?.status === 'ready' && gameChatOnly) {
|
||||
const text = candidateStream.accumulatedText.trim();
|
||||
@@ -978,7 +983,20 @@ export function App({
|
||||
return;
|
||||
}
|
||||
const syncKey = `${projectPath}\n${runtime.sessionId}\n${runtime.runId}`;
|
||||
if (projectSupervisorRuntimeSyncingRef.current.has(syncKey)) {
|
||||
const syncAlreadyStarted =
|
||||
projectSupervisorRuntimeSyncingRef.current.has(syncKey);
|
||||
if (gameChatOnly && isGameChatSupervisorRoot(runtime)) {
|
||||
// Freeze the old root and its currently known descendants before the
|
||||
// asynchronous conversation refresh. A fast next turn may replace the
|
||||
// current-by-agent map while that refresh is still in flight.
|
||||
appendGameChatStageRecord(
|
||||
projectPath,
|
||||
runtime,
|
||||
false,
|
||||
!syncAlreadyStarted,
|
||||
);
|
||||
}
|
||||
if (syncAlreadyStarted) {
|
||||
return;
|
||||
}
|
||||
const refreshConversation = projectSupervisorRefreshConversationRef.current;
|
||||
@@ -986,24 +1004,54 @@ export function App({
|
||||
return;
|
||||
}
|
||||
projectSupervisorRuntimeSyncingRef.current.add(syncKey);
|
||||
if (gameChatOnly && isGameChatSupervisorRoot(runtime)) {
|
||||
const archiveKey = `${projectPath}\n${runtime.runId}`;
|
||||
void invoke<GameCreationAppManifest>('get_local_game_manifest', {
|
||||
projectPath,
|
||||
})
|
||||
.then((capturedManifest) => {
|
||||
const pendingArchive =
|
||||
gameChatPendingStageRuntimesRef.current.get(archiveKey);
|
||||
if (
|
||||
pendingArchive &&
|
||||
gameChatManifestHasTerminalPrimaryTask(capturedManifest)
|
||||
) {
|
||||
pendingArchive.manifestSnapshot = capturedManifest;
|
||||
flushPendingGameChatStageRecords();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// The normal manifest refresh path can still supply the snapshot.
|
||||
});
|
||||
}
|
||||
void refreshConversation(
|
||||
invoke,
|
||||
projectPath,
|
||||
runtime.sessionId,
|
||||
runtime.runId,
|
||||
).catch((error) => {
|
||||
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
|
||||
if (
|
||||
localProjectPathRef.current === projectPath &&
|
||||
projectSupervisorSessionIdRef.current === runtime.sessionId
|
||||
) {
|
||||
setProjectSupervisorRuntimeError(
|
||||
`项目总控 Agent 对话刷新失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
)
|
||||
.then(() => {
|
||||
const archiveKey = `${projectPath}\n${runtime.runId}`;
|
||||
const pendingArchive =
|
||||
gameChatPendingStageRuntimesRef.current.get(archiveKey);
|
||||
if (pendingArchive) {
|
||||
pendingArchive.awaitingConversationSync = false;
|
||||
}
|
||||
flushPendingGameChatStageRecords();
|
||||
})
|
||||
.catch((error) => {
|
||||
projectSupervisorRuntimeSyncingRef.current.delete(syncKey);
|
||||
if (
|
||||
localProjectPathRef.current === projectPath &&
|
||||
projectSupervisorSessionIdRef.current === runtime.sessionId
|
||||
) {
|
||||
setProjectSupervisorRuntimeError(
|
||||
`项目总控 Agent 对话刷新失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flushPendingGameChatStageRecords() {
|
||||
@@ -1018,9 +1066,12 @@ export function App({
|
||||
gameChatPendingStageRuntimesRef.current.delete(archiveKey);
|
||||
continue;
|
||||
}
|
||||
if (pendingArchive.awaitingConversationSync) {
|
||||
continue;
|
||||
}
|
||||
pendingArchive.runtimeRecords = mergeGameChatRuntimeSnapshots(
|
||||
pendingArchive.runtimeRecords,
|
||||
Object.values(agentRuntimeById),
|
||||
Object.values(agentRuntimeByIdRef.current),
|
||||
);
|
||||
if (
|
||||
projectSupervisorRuntimeRef.current?.runId ===
|
||||
@@ -1080,7 +1131,7 @@ export function App({
|
||||
current.length,
|
||||
);
|
||||
}
|
||||
return [
|
||||
const nextMessages: ChatMessage[] = [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
@@ -1089,6 +1140,8 @@ export function App({
|
||||
updatedAt: pendingArchive.rootRuntime.updatedAt,
|
||||
},
|
||||
];
|
||||
latestMessagesRef.current = nextMessages;
|
||||
return nextMessages;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1097,6 +1150,9 @@ export function App({
|
||||
nextProjectPath: string,
|
||||
runtime: AgentRuntimeState,
|
||||
) {
|
||||
if (!isGameChatSupervisorRoot(runtime)) {
|
||||
return;
|
||||
}
|
||||
const eventMessages = gameChatRuntimeEventMessages(
|
||||
runtime,
|
||||
agentRuntimeByIdRef.current,
|
||||
@@ -1199,8 +1255,14 @@ export function App({
|
||||
function appendGameChatStageRecord(
|
||||
nextProjectPath: string,
|
||||
runtime: AgentRuntimeState,
|
||||
flush = true,
|
||||
awaitingConversationSync = false,
|
||||
) {
|
||||
if (!gameChatOnly || !gameChatRuntimeHasTerminalOutcome(runtime)) {
|
||||
if (
|
||||
!gameChatOnly ||
|
||||
!isGameChatSupervisorRoot(runtime) ||
|
||||
!gameChatRuntimeHasTerminalOutcome(runtime)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const archiveKey = `${nextProjectPath}\n${runtime.runId}`;
|
||||
@@ -1218,15 +1280,19 @@ export function App({
|
||||
: previous.rootRuntime,
|
||||
runtimeRecords: mergeGameChatRuntimeSnapshots(
|
||||
previous?.runtimeRecords ?? [],
|
||||
Object.values(agentRuntimeById),
|
||||
Object.values(agentRuntimeByIdRef.current),
|
||||
),
|
||||
manifestSnapshot:
|
||||
gameChatManifestHasTerminalPrimaryTask(manifest) &&
|
||||
projectSupervisorRuntimeRef.current?.runId === runtime.runId
|
||||
? manifest
|
||||
: (previous?.manifestSnapshot ?? null),
|
||||
awaitingConversationSync:
|
||||
awaitingConversationSync || previous?.awaitingConversationSync === true,
|
||||
});
|
||||
flushPendingGameChatStageRecords();
|
||||
if (flush) {
|
||||
flushPendingGameChatStageRecords();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -5735,6 +5801,38 @@ export function App({
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function capturePendingGameChatStageManifestBeforeNextRun(
|
||||
invoke: TauriInvoke,
|
||||
projectPath: string,
|
||||
) {
|
||||
const archivePrefix = `${projectPath}\n`;
|
||||
const pendingArchives = Array.from(
|
||||
gameChatPendingStageRuntimesRef.current.entries(),
|
||||
).filter(
|
||||
([archiveKey, pendingArchive]) =>
|
||||
archiveKey.startsWith(archivePrefix) &&
|
||||
!pendingArchive.manifestSnapshot,
|
||||
);
|
||||
for (const [, pendingArchive] of pendingArchives) {
|
||||
const currentRoot = projectSupervisorRuntimeRef.current;
|
||||
if (
|
||||
!isGameChatSupervisorRoot(currentRoot) ||
|
||||
currentRoot.runId !== pendingArchive.rootRuntime.runId
|
||||
) {
|
||||
throw new Error('上一轮阶段清单尚未冻结,请稍后重试');
|
||||
}
|
||||
const capturedManifest = await invoke<GameCreationAppManifest>(
|
||||
'get_local_game_manifest',
|
||||
{ projectPath },
|
||||
);
|
||||
if (!gameChatManifestHasTerminalPrimaryTask(capturedManifest)) {
|
||||
throw new Error('上一轮主阶段仍在收束,请稍后重试');
|
||||
}
|
||||
pendingArchive.manifestSnapshot = capturedManifest;
|
||||
}
|
||||
flushPendingGameChatStageRecords();
|
||||
}
|
||||
|
||||
async function executeChatAgentReply(prompt: string) {
|
||||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||||
@@ -5778,6 +5876,15 @@ export function App({
|
||||
'project-supervisor-game-chat',
|
||||
)
|
||||
: null;
|
||||
if (gameChatOnly && !steerRuntime) {
|
||||
await capturePendingGameChatStageManifestBeforeNextRun(
|
||||
invoke,
|
||||
nextProjectPath,
|
||||
);
|
||||
if (localProjectPathRef.current !== nextProjectPath) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let autoPreviewAfterRevision = 0;
|
||||
let autoPreviewAfterValidatedAt = 0;
|
||||
if (steerRuntime) {
|
||||
@@ -10304,9 +10411,13 @@ export function App({
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
setAgentRuntimeById((current) =>
|
||||
mergeAgentRuntimeStateIntoMap(current, runtime, false),
|
||||
const next = mergeAgentRuntimeStateIntoMap(
|
||||
agentRuntimeByIdRef.current,
|
||||
runtime,
|
||||
false,
|
||||
);
|
||||
agentRuntimeByIdRef.current = next;
|
||||
setAgentRuntimeById(next);
|
||||
}
|
||||
|
||||
async function refreshAgentRuntimes(
|
||||
@@ -10921,6 +11032,9 @@ export function App({
|
||||
|
||||
if (projectSupervisorOnly && gameChatOnly) {
|
||||
const gameChatProjectPath = localProject?.projectPath ?? '';
|
||||
const hasCurrentGameChatRoot = isGameChatSupervisorRoot(
|
||||
projectSupervisorRuntime,
|
||||
);
|
||||
return (
|
||||
<SupervisorChatOnlyView
|
||||
chatAgentBusy={chatAgentBusy}
|
||||
@@ -10956,11 +11070,17 @@ export function App({
|
||||
manifest={manifest}
|
||||
runtimeConfigOpen={runtimeConfigOpen}
|
||||
runtimeError={projectSupervisorRuntimeError}
|
||||
transientReply={projectSupervisorTransientReply}
|
||||
transientReply={
|
||||
hasCurrentGameChatRoot ? projectSupervisorTransientReply : ''
|
||||
}
|
||||
transientReplyUpdatedAt={projectSupervisorResponseStream?.updatedAt}
|
||||
hasConversationControls={projectSupervisorHasConversationControls}
|
||||
hasConversationControls={
|
||||
hasCurrentGameChatRoot && projectSupervisorHasConversationControls
|
||||
}
|
||||
hiddenConversationCount={hiddenConversationCount}
|
||||
needsUserInput={projectSupervisorNeedsUserInput}
|
||||
needsUserInput={
|
||||
hasCurrentGameChatRoot && projectSupervisorNeedsUserInput
|
||||
}
|
||||
visibleMessages={visibleMessages}
|
||||
workspaceStatus={workspaceStatus}
|
||||
expectedRunId={projectSupervisorExpectedRunId}
|
||||
|
||||
@@ -98,7 +98,7 @@ function distinctRuntimeRecords(
|
||||
|
||||
export function isGameChatSupervisorRoot(
|
||||
runtime: AgentRuntimeState | null | undefined,
|
||||
): boolean {
|
||||
): runtime is AgentRuntimeState {
|
||||
return Boolean(
|
||||
runtime &&
|
||||
runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
@@ -139,11 +139,26 @@ export function isCurrentGameChatDynamicArtRuntime(
|
||||
main.taskId === GAME_CHAT_PRIMARY_TASK_ID &&
|
||||
main.source === GAME_CHAT_MAIN_SOURCE &&
|
||||
candidate &&
|
||||
gameChatRuntimeClaimsDynamicArtLineage(candidate) &&
|
||||
candidate.parentRunId === main.runId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-closed UI classification for views that only loaded the selected child
|
||||
* Runtime and therefore cannot reconstruct its root binding. Exact lineage
|
||||
* consumers must still use `isCurrentGameChatDynamicArtRuntime`.
|
||||
*/
|
||||
export function gameChatRuntimeClaimsDynamicArtLineage(
|
||||
candidate: AgentRuntimeState | null | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
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.parentRunId) &&
|
||||
hasIdentity(candidate.sessionId) &&
|
||||
hasIdentity(candidate.runId),
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
projectSupervisorRuntimeStatusLabel,
|
||||
} from './model';
|
||||
import {
|
||||
gameChatRuntimeClaimsDynamicArtLineage,
|
||||
isCurrentGameChatDynamicArtRuntime,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
} from './gameChatRuntimeProjection';
|
||||
@@ -391,6 +392,7 @@ export function AgentRuntimeStatusPanel({
|
||||
const canRetry =
|
||||
Boolean(runtime.runId) &&
|
||||
agentRuntimeCanRetry(runtime.status) &&
|
||||
!gameChatRuntimeClaimsDynamicArtLineage(runtime) &&
|
||||
!agentGoalStatusIsPaused(runtime.goalStatus) &&
|
||||
!agentGoalStatusIsPaused(runtime.status) &&
|
||||
!agentGoalStatusIsPaused(runtime.phase) &&
|
||||
|
||||
@@ -36,7 +36,11 @@ import {
|
||||
formatAgentRecentRuntimeTask,
|
||||
formatAgentRuntimePlanStep,
|
||||
formatAgentRuntimeTaskQueue,
|
||||
GAME_CHAT_SUPERVISOR_SOURCE,
|
||||
gameChatLineageCollaboratingRuntimes,
|
||||
isAgentRuntimeTerminalState,
|
||||
isGameChatSupervisorRoot,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectRuntimeVisibleCurrentWork,
|
||||
taskRowsFromManifest,
|
||||
} from '../agent-runtime';
|
||||
@@ -228,6 +232,18 @@ export function projectAgentRuntimeSummaries(
|
||||
if (!supervisorRuntime?.runId) {
|
||||
return [];
|
||||
}
|
||||
const manifestTasks = taskRowsFromManifest(nextManifest);
|
||||
const gameChatRuntimes =
|
||||
supervisorRuntime.source === GAME_CHAT_SUPERVISOR_SOURCE
|
||||
? isGameChatSupervisorRoot(supervisorRuntime)
|
||||
? gameChatLineageCollaboratingRuntimes(
|
||||
projectCurrentGameChatRuntimeLineage(
|
||||
supervisorRuntime,
|
||||
runtimeByAgentId,
|
||||
),
|
||||
)
|
||||
: []
|
||||
: null;
|
||||
const groupConfigs = [
|
||||
{ group: 'design' as const, label: '策划 Agent' },
|
||||
{ group: 'art' as const, label: '美术 Agent' },
|
||||
@@ -260,27 +276,34 @@ export function projectAgentRuntimeSummaries(
|
||||
};
|
||||
|
||||
return groupConfigs.flatMap(({ group, label }) => {
|
||||
const runtimes = taskRowsFromManifest(nextManifest)
|
||||
.filter((task) => task.group === group)
|
||||
.map((task) => {
|
||||
const agentId = agentConversationId(task);
|
||||
return runtimeByAgentId[agentId] ?? runtimeByAgentId[task.id];
|
||||
})
|
||||
.filter((runtime): runtime is AgentRuntimeState =>
|
||||
Boolean(
|
||||
runtime &&
|
||||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||||
runtime.source,
|
||||
) &&
|
||||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
runtime.parentRunId === supervisorRuntime.runId,
|
||||
),
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
statusRank(right) - statusRank(left) ||
|
||||
right.updatedAt - left.updatedAt,
|
||||
);
|
||||
const runtimes = (
|
||||
gameChatRuntimes
|
||||
? gameChatRuntimes.filter((runtime) =>
|
||||
manifestTasks.some(
|
||||
(task) => task.id === runtime.taskId && task.group === group,
|
||||
),
|
||||
)
|
||||
: manifestTasks
|
||||
.filter((task) => task.group === group)
|
||||
.map((task) => {
|
||||
const agentId = agentConversationId(task);
|
||||
return runtimeByAgentId[agentId] ?? runtimeByAgentId[task.id];
|
||||
})
|
||||
.filter((runtime): runtime is AgentRuntimeState =>
|
||||
Boolean(
|
||||
runtime &&
|
||||
['agent-delegate', 'agent-delegate-retry'].includes(
|
||||
runtime.source,
|
||||
) &&
|
||||
runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
runtime.parentRunId === supervisorRuntime.runId,
|
||||
),
|
||||
)
|
||||
).sort(
|
||||
(left, right) =>
|
||||
statusRank(right) - statusRank(left) ||
|
||||
right.updatedAt - left.updatedAt,
|
||||
);
|
||||
const runtime = runtimes[0];
|
||||
if (!runtime) {
|
||||
return [];
|
||||
|
||||
+109
-61
@@ -27,6 +27,7 @@ import {
|
||||
GAME_CHAT_DYNAMIC_ART_AGENT_IDS,
|
||||
GAME_CHAT_PRIMARY_TASK_ID,
|
||||
isAgentRuntimeTerminalState,
|
||||
isGameChatSupervisorRoot,
|
||||
isMudPointInsufficientRuntimeError,
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
@@ -328,6 +329,9 @@ export function gameChatMudPointInterruptionText(
|
||||
runtime: AgentRuntimeState,
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||||
) {
|
||||
if (!isGameChatSupervisorRoot(runtime)) {
|
||||
return null;
|
||||
}
|
||||
const relatedRuntimes = [
|
||||
runtime,
|
||||
...projectSupervisorCollaboratingAgentRuntimes(runtime, runtimeByAgentId),
|
||||
@@ -453,7 +457,7 @@ export function buildGameChatProgressEvidence(
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||||
manifest: GameCreationAppManifest | null,
|
||||
): GameChatSupervisorProgress | null {
|
||||
if (!runtime?.runId) {
|
||||
if (!isGameChatSupervisorRoot(runtime)) {
|
||||
return null;
|
||||
}
|
||||
const lineage = projectCurrentGameChatRuntimeLineage(
|
||||
@@ -472,21 +476,20 @@ export function buildGameChatProgressEvidence(
|
||||
: '主阶段 0/1',
|
||||
plan.total > 0 ? `计划 ${plan.completed}/${plan.total}` : null,
|
||||
].filter(Boolean);
|
||||
const activeAgents = projectSupervisorCollaboratingAgentRuntimes(
|
||||
const collaboratingRuntimes = projectSupervisorCollaboratingAgentRuntimes(
|
||||
runtime,
|
||||
runtimeByAgentId,
|
||||
)
|
||||
.filter(
|
||||
(professionalRuntime) =>
|
||||
!isAgentRuntimeTerminalState(professionalRuntime),
|
||||
)
|
||||
.map((professionalRuntime) => {
|
||||
const parts = [
|
||||
projectProfessionalAgentLabel(professionalRuntime.agentId),
|
||||
projectRuntimeVisibleCurrentWork(professionalRuntime),
|
||||
].filter(Boolean);
|
||||
return compactProgressText(parts.join(' · '), 140);
|
||||
});
|
||||
);
|
||||
const activeProfessionalRuntimes = collaboratingRuntimes.filter(
|
||||
(professionalRuntime) => !isAgentRuntimeTerminalState(professionalRuntime),
|
||||
);
|
||||
const activeAgents = activeProfessionalRuntimes.map((professionalRuntime) => {
|
||||
const parts = [
|
||||
projectProfessionalAgentLabel(professionalRuntime.agentId),
|
||||
projectRuntimeVisibleCurrentWork(professionalRuntime),
|
||||
].filter(Boolean);
|
||||
return compactProgressText(parts.join(' · '), 140);
|
||||
});
|
||||
const events = runtimeEvidenceEvents(runtime, runtimeByAgentId);
|
||||
const mainRuntime = lineage?.main ?? null;
|
||||
const preview = latestEvidenceEvent(
|
||||
@@ -627,7 +630,14 @@ export function buildGameChatProgressEvidence(
|
||||
taskProgress:
|
||||
taskParts.join(' · ') || projectSupervisorChatRuntimeStatus(runtime),
|
||||
currentWork: formatGameChatRuntimeText(
|
||||
compactProgressText(projectRuntimeVisibleCurrentWork(runtime)),
|
||||
compactProgressText(
|
||||
projectRuntimeVisibleCurrentWork(
|
||||
isAgentRuntimeTerminalState(runtime) &&
|
||||
activeProfessionalRuntimes.length > 0
|
||||
? activeProfessionalRuntimes[0]!
|
||||
: runtime,
|
||||
),
|
||||
),
|
||||
),
|
||||
activeAgents,
|
||||
evidence,
|
||||
@@ -643,7 +653,7 @@ function collectGameChatRuntimeEventsInternal(
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||||
limit: number,
|
||||
) {
|
||||
const sources = runtime
|
||||
const sources = isGameChatSupervisorRoot(runtime)
|
||||
? [
|
||||
{ label: '项目总控 Agent', runtime },
|
||||
...projectSupervisorCollaboratingAgentRuntimes(
|
||||
@@ -949,17 +959,36 @@ export function SupervisorChatOnlyView({
|
||||
>([]);
|
||||
const [selectedResultImage, setSelectedResultImage] =
|
||||
useState<LoadedGameChatResultImagePreview | null>(null);
|
||||
const projectedRuntime =
|
||||
gameChatMode && !isGameChatSupervisorRoot(runtime) ? null : runtime;
|
||||
const synchronizingAcceptedRun = Boolean(
|
||||
expectedRunId && runtime?.runId !== expectedRunId,
|
||||
expectedRunId && projectedRuntime?.runId !== expectedRunId,
|
||||
);
|
||||
const collaboratingRuntimes = useMemo(
|
||||
() =>
|
||||
projectSupervisorCollaboratingAgentRuntimes(
|
||||
projectedRuntime,
|
||||
runtimeByAgentId,
|
||||
),
|
||||
[projectedRuntime, runtimeByAgentId],
|
||||
);
|
||||
const activeCollaboratingRuntimes = collaboratingRuntimes.filter(
|
||||
(candidate) => !isAgentRuntimeTerminalState(candidate),
|
||||
);
|
||||
const runtimeTerminal = Boolean(
|
||||
projectedRuntime && isAgentRuntimeTerminalState(projectedRuntime),
|
||||
);
|
||||
const descendantsStillActive = Boolean(
|
||||
gameChatMode && runtimeTerminal && activeCollaboratingRuntimes.length > 0,
|
||||
);
|
||||
const running = Boolean(
|
||||
chatAgentBusy ||
|
||||
synchronizingAcceptedRun ||
|
||||
(runtime && !isAgentRuntimeTerminalState(runtime)),
|
||||
(projectedRuntime && (!runtimeTerminal || descendantsStillActive)),
|
||||
);
|
||||
const gameChatInterruptionText =
|
||||
gameChatMode && runtime
|
||||
? gameChatMudPointInterruptionText(runtime, runtimeByAgentId)
|
||||
gameChatMode && projectedRuntime
|
||||
? gameChatMudPointInterruptionText(projectedRuntime, runtimeByAgentId)
|
||||
: null;
|
||||
const status =
|
||||
gameChatInterruptionText ||
|
||||
@@ -967,23 +996,37 @@ export function SupervisorChatOnlyView({
|
||||
? projectRuntimeVisibleError(runtimeError, '项目总控 Agent', true)
|
||||
: synchronizingAcceptedRun
|
||||
? '已投递,正在同步 Agent Runner'
|
||||
: runtime
|
||||
? projectSupervisorChatRuntimeStatus(runtime)
|
||||
: workspaceStatus);
|
||||
: descendantsStillActive
|
||||
? '专业 Agent 正在收束'
|
||||
: projectedRuntime
|
||||
? projectSupervisorChatRuntimeStatus(projectedRuntime)
|
||||
: workspaceStatus);
|
||||
const headerStatus = gameChatMode ? status : previewStatus || status;
|
||||
const runtimeEvents = useMemo(
|
||||
() => collectGameChatRuntimeEvents(runtime, runtimeByAgentId),
|
||||
[runtime, runtimeByAgentId],
|
||||
() => collectGameChatRuntimeEvents(projectedRuntime, runtimeByAgentId),
|
||||
[projectedRuntime, runtimeByAgentId],
|
||||
);
|
||||
const supervisorProgress = useMemo(
|
||||
() =>
|
||||
gameChatMode &&
|
||||
projectReady &&
|
||||
runtime &&
|
||||
!isAgentRuntimeTerminalState(runtime)
|
||||
? buildGameChatProgressEvidence(runtime, runtimeByAgentId, manifest)
|
||||
projectedRuntime &&
|
||||
(!runtimeTerminal || descendantsStillActive)
|
||||
? buildGameChatProgressEvidence(
|
||||
projectedRuntime,
|
||||
runtimeByAgentId,
|
||||
manifest,
|
||||
)
|
||||
: null,
|
||||
[gameChatMode, manifest, projectReady, runtime, runtimeByAgentId],
|
||||
[
|
||||
descendantsStillActive,
|
||||
gameChatMode,
|
||||
manifest,
|
||||
projectReady,
|
||||
projectedRuntime,
|
||||
runtimeByAgentId,
|
||||
runtimeTerminal,
|
||||
],
|
||||
);
|
||||
const resultImages = useMemo(
|
||||
() => (gameChatMode ? collectGameChatResultImages(manifest) : []),
|
||||
@@ -992,38 +1035,27 @@ export function SupervisorChatOnlyView({
|
||||
const resultImageKey = resultImages
|
||||
.map((image) => `${image.key}:${image.mediaType}`)
|
||||
.join('\n');
|
||||
const collaboratingRuntimes = useMemo(
|
||||
() =>
|
||||
projectSupervisorCollaboratingAgentRuntimes(runtime, runtimeByAgentId),
|
||||
[runtime, runtimeByAgentId],
|
||||
);
|
||||
const attentionAgentCount = collaboratingRuntimes.filter(
|
||||
(candidate) =>
|
||||
['failed', 'needs-reconciliation'].includes(candidate.status) ||
|
||||
['failed', 'needs-reconciliation'].includes(candidate.phase),
|
||||
).length;
|
||||
const runtimeTerminal = Boolean(
|
||||
runtime && isAgentRuntimeTerminalState(runtime),
|
||||
);
|
||||
const runtimeRunId = runtime?.runId ?? null;
|
||||
const latestParentActivityAt = runtime
|
||||
? gameChatRuntimeActivityTimes(runtime).reduce(
|
||||
const runtimeRunId = projectedRuntime?.runId ?? null;
|
||||
const latestParentActivityAt = projectedRuntime
|
||||
? gameChatRuntimeActivityTimes(projectedRuntime).reduce(
|
||||
(latest, candidate) => Math.max(latest, candidate),
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const latestActivityAt = runtime
|
||||
? [runtime, ...collaboratingRuntimes]
|
||||
const latestActivityAt = projectedRuntime
|
||||
? [projectedRuntime, ...collaboratingRuntimes]
|
||||
.flatMap(gameChatRuntimeActivityTimes)
|
||||
.reduce((latest, candidate) => Math.max(latest, candidate), 0)
|
||||
: 0;
|
||||
const activeCollaboratingRuntimes = collaboratingRuntimes.filter(
|
||||
(candidate) => !isAgentRuntimeTerminalState(candidate),
|
||||
);
|
||||
const activeRuntimeLanes = runtime
|
||||
const activeRuntimeLanes = projectedRuntime
|
||||
? activeCollaboratingRuntimes.length > 0
|
||||
? activeCollaboratingRuntimes
|
||||
: [runtime]
|
||||
: [projectedRuntime]
|
||||
: [];
|
||||
const nonWaitingRuntimeLaneActivity = activeRuntimeLanes
|
||||
.filter((candidate) => !gameChatRuntimeIsExpectedWait(candidate))
|
||||
@@ -1035,9 +1067,9 @@ export function SupervisorChatOnlyView({
|
||||
)
|
||||
.filter((activityAt) => activityAt > 0);
|
||||
const runStartedAt =
|
||||
runtime && runtimeRunId
|
||||
? (gameChatTimestampMilliseconds(runtime.startedAt) ??
|
||||
gameChatRuntimeActivityTimes(runtime).reduce(
|
||||
projectedRuntime && runtimeRunId
|
||||
? (gameChatTimestampMilliseconds(projectedRuntime.startedAt) ??
|
||||
gameChatRuntimeActivityTimes(projectedRuntime).reduce(
|
||||
(earliest, candidate) => Math.min(earliest, candidate),
|
||||
Number.POSITIVE_INFINITY,
|
||||
))
|
||||
@@ -1045,7 +1077,9 @@ export function SupervisorChatOnlyView({
|
||||
const elapsedRuntimeMs = Number.isFinite(runStartedAt)
|
||||
? Math.max(
|
||||
0,
|
||||
(runtimeTerminal && latestParentActivityAt > 0
|
||||
(runtimeTerminal &&
|
||||
!descendantsStillActive &&
|
||||
latestParentActivityAt > 0
|
||||
? latestParentActivityAt
|
||||
: runtimeClockNow) - runStartedAt,
|
||||
)
|
||||
@@ -1067,8 +1101,8 @@ export function SupervisorChatOnlyView({
|
||||
);
|
||||
const runtimeAppearsStalled = Boolean(
|
||||
running &&
|
||||
runtime &&
|
||||
!runtimeTerminal &&
|
||||
projectedRuntime &&
|
||||
(!runtimeTerminal || descendantsStillActive) &&
|
||||
!expectedRuntimeWait &&
|
||||
inactiveRuntimeMs !== null &&
|
||||
inactiveRuntimeMs > GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS,
|
||||
@@ -1092,13 +1126,22 @@ export function SupervisorChatOnlyView({
|
||||
}
|
||||
return attentionAgentCount > 0 ? '运行中 · 有异常' : '运行中';
|
||||
}
|
||||
if (runtime?.status === 'completed' || runtime?.phase === 'completed') {
|
||||
if (
|
||||
projectedRuntime?.status === 'completed' ||
|
||||
projectedRuntime?.phase === 'completed'
|
||||
) {
|
||||
return '本轮已完成';
|
||||
}
|
||||
if (runtime?.status === 'failed' || runtime?.phase === 'failed') {
|
||||
if (
|
||||
projectedRuntime?.status === 'failed' ||
|
||||
projectedRuntime?.phase === 'failed'
|
||||
) {
|
||||
return '本轮失败';
|
||||
}
|
||||
if (runtime?.status === 'cancelled' || runtime?.phase === 'cancelled') {
|
||||
if (
|
||||
projectedRuntime?.status === 'cancelled' ||
|
||||
projectedRuntime?.phase === 'cancelled'
|
||||
) {
|
||||
return '本轮已取消';
|
||||
}
|
||||
return '未运行';
|
||||
@@ -1114,7 +1157,8 @@ export function SupervisorChatOnlyView({
|
||||
? 'warning'
|
||||
: running || synchronizingAcceptedRun
|
||||
? 'active'
|
||||
: runtime?.status === 'completed' || runtime?.phase === 'completed'
|
||||
: projectedRuntime?.status === 'completed' ||
|
||||
projectedRuntime?.phase === 'completed'
|
||||
? 'complete'
|
||||
: 'idle';
|
||||
const embeddedPreviewUrl = preview
|
||||
@@ -1134,17 +1178,21 @@ export function SupervisorChatOnlyView({
|
||||
})();
|
||||
useLayoutEffect(() => {
|
||||
setShowRuntimeDetails(false);
|
||||
}, [projectPath, runtime?.runId]);
|
||||
}, [projectPath, projectedRuntime?.runId]);
|
||||
useEffect(() => {
|
||||
setRuntimeClockNow(Date.now());
|
||||
if (!gameChatMode || !runtimeRunId || runtimeTerminal) {
|
||||
if (
|
||||
!gameChatMode ||
|
||||
!runtimeRunId ||
|
||||
(runtimeTerminal && !descendantsStillActive)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const interval = window.setInterval(() => {
|
||||
setRuntimeClockNow(Date.now());
|
||||
}, GAME_CHAT_RUNTIME_CLOCK_INTERVAL_MS);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [gameChatMode, runtimeRunId, runtimeTerminal]);
|
||||
}, [descendantsStillActive, gameChatMode, runtimeRunId, runtimeTerminal]);
|
||||
useEffect(() => {
|
||||
if (!showRuntimeDetails) {
|
||||
return undefined;
|
||||
@@ -1387,7 +1435,7 @@ export function SupervisorChatOnlyView({
|
||||
{hasConversationControls ? (
|
||||
<div className="supervisor-chat-only-runtime-controls">
|
||||
<ProjectSupervisorRuntimeControls
|
||||
runtime={runtime}
|
||||
runtime={projectedRuntime}
|
||||
controlBusy={chatAgentBusy}
|
||||
onToolAction={onToolAction}
|
||||
onUserInput={onUserInput}
|
||||
|
||||
@@ -25,10 +25,12 @@ import {
|
||||
} from '../src/features/agent-runtime/model';
|
||||
import {
|
||||
canArchiveGameChatStage,
|
||||
gameChatRuntimeClaimsDynamicArtLineage,
|
||||
latestGameChatPlayableRevision,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectGameChatPrimaryProgress,
|
||||
} from '../src/features/agent-runtime/gameChatRuntimeProjection';
|
||||
import { projectAgentRuntimeSummaries } from '../src/features/project-summary/agentPresentation';
|
||||
|
||||
describe('Runtime-owned public statuses', () => {
|
||||
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
|
||||
@@ -812,6 +814,11 @@ describe('Game-chat source-aware runtime projection', () => {
|
||||
expect(lineage?.main).toBe(main);
|
||||
expect(lineage?.dynamicArtChildren).toEqual([artDirector, legacyRetry]);
|
||||
expect(lineage?.hasConflict).toBe(false);
|
||||
expect(gameChatRuntimeClaimsDynamicArtLineage(artDirector)).toBe(true);
|
||||
expect(gameChatRuntimeClaimsDynamicArtLineage(legacyRetry)).toBe(true);
|
||||
expect(gameChatRuntimeClaimsDynamicArtLineage(oldRootDirectArt)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
projectSupervisorCollaboratingAgentRuntimes(root, runtimeMap),
|
||||
).toEqual([main, artDirector, legacyRetry]);
|
||||
@@ -832,6 +839,49 @@ describe('Game-chat source-aware runtime projection', () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('projects current game-chat main and nested art into launcher summaries', () => {
|
||||
const root = gameChatProjectionRuntime();
|
||||
const main = gameChatProjectionMain(root);
|
||||
const art = gameChatProjectionRuntime({
|
||||
agentId: 'art-director',
|
||||
taskId: 'art-director',
|
||||
sessionId: 'launcher-art-session',
|
||||
runId: 'launcher-art-run',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'code-prototype',
|
||||
parentRunId: main.runId,
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
});
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'launcher-game-chat',
|
||||
'Launcher 单主摘要',
|
||||
);
|
||||
|
||||
expect(projectAgentRuntimeSummaries(manifest, root, { main, art })).toEqual(
|
||||
[
|
||||
expect.objectContaining({
|
||||
group: 'art',
|
||||
label: '美术 Agent',
|
||||
status: 'completed',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
group: 'code',
|
||||
label: '程序 Agent',
|
||||
status: 'running',
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
projectAgentRuntimeSummaries(
|
||||
manifest,
|
||||
{ ...root, parentAgentId: 'project-supervisor' },
|
||||
{ main, art },
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('uses only the current main for the 0/1 progress denominator', () => {
|
||||
const root = gameChatProjectionRuntime();
|
||||
const activeMain = gameChatProjectionMain(root);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import {
|
||||
latestGameChatPlayableRevision as projectLatestGameChatPlayableRevision,
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
AgentRuntimeStatusPanel,
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
} from '../../src/features/agent-runtime';
|
||||
@@ -3997,14 +3998,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
gameChatRuntimeEventMessages(supervisorContinuation, {}).map(
|
||||
(message) => message.text,
|
||||
),
|
||||
).toEqual([
|
||||
expect.stringContaining(
|
||||
`Supervisor ${continuation.kind} continuation started`,
|
||||
),
|
||||
expect.stringContaining(
|
||||
`Supervisor ${continuation.kind} continuation failed`,
|
||||
),
|
||||
]);
|
||||
).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4157,6 +4151,52 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('disables generic retry when a selected runtime claims nested game-chat art lineage', () => {
|
||||
const onRetryRuntimeTask = vi.fn();
|
||||
const nestedArt = gameChatRuntimeState({
|
||||
agentId: 'art-director',
|
||||
taskId: 'art-director',
|
||||
sessionId: 'selected-game-chat-art-session',
|
||||
runId: 'selected-game-chat-art-run',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'code-prototype',
|
||||
parentRunId: 'selected-game-chat-main-run',
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
});
|
||||
const rendered = render(
|
||||
React.createElement(AgentRuntimeStatusPanel, {
|
||||
runtime: nestedArt,
|
||||
onRetryRuntimeTask,
|
||||
}),
|
||||
);
|
||||
const retryButton = within(
|
||||
screen.getByLabelText('Agent Runtime 操作'),
|
||||
).getByRole('button', { name: '重试' }) as HTMLButtonElement;
|
||||
expect(retryButton.disabled).toBe(true);
|
||||
fireEvent.click(retryButton);
|
||||
expect(onRetryRuntimeTask).not.toHaveBeenCalled();
|
||||
|
||||
rendered.rerender(
|
||||
React.createElement(AgentRuntimeStatusPanel, {
|
||||
runtime: {
|
||||
...nestedArt,
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: 'full-dag-root-run',
|
||||
},
|
||||
onRetryRuntimeTask,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
within(screen.getByLabelText('Agent Runtime 操作')).getByRole(
|
||||
'button',
|
||||
{ name: '重试' },
|
||||
) as HTMLButtonElement
|
||||
).disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('persists professional final-reply streams with stable ids and does not duplicate them after hydration', async () => {
|
||||
const projectPath = '/tmp/game-chat-final-reply-hydration';
|
||||
const runId = 'game-chat-final-reply-hydration-run';
|
||||
@@ -4327,6 +4367,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
initialRuntime: {
|
||||
sessionId: 'supervisor-session-active',
|
||||
runId,
|
||||
source: 'project-supervisor-game-chat',
|
||||
status: 'running',
|
||||
phase: 'execution',
|
||||
recentEvents: events,
|
||||
@@ -5307,6 +5348,58 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
expect(document.body.textContent).not.toMatch(/第\s*\d+\s*轮/u);
|
||||
});
|
||||
|
||||
it('fails closed when the game-chat surface hydrates a non-game-chat Supervisor root', () => {
|
||||
const guiRuntime = gameChatRuntimeState({
|
||||
source: 'project-supervisor-gui',
|
||||
recentEvents: [
|
||||
gameChatRuntimeEvent({
|
||||
summary: '不应进入 game-chat 当前投影',
|
||||
updatedAt: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
renderGameChatStatus({ runtime: guiRuntime });
|
||||
|
||||
expect(screen.getByLabelText('最新状态').textContent).toContain('未运行');
|
||||
expect(screen.queryByText('不应进入 game-chat 当前投影')).toBeNull();
|
||||
expect(buildGameChatProgressEvidence(guiRuntime, {}, null)).toBeNull();
|
||||
expect(collectGameChatRuntimeEvents(guiRuntime, {})).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps game-chat running while an exact descendant is still active after root terminal projection', () => {
|
||||
const root = gameChatRuntimeState({
|
||||
runId: 'terminal-root-active-main',
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
updatedAt: 200,
|
||||
});
|
||||
const main = gameChatRuntimeState({
|
||||
agentId: 'code-prototype',
|
||||
taskId: 'code-prototype',
|
||||
sessionId: 'terminal-root-active-main-session',
|
||||
runId: 'terminal-root-active-main-run',
|
||||
source: 'agent-ready-task-scheduler',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: root.runId,
|
||||
status: 'running',
|
||||
phase: 'waiting-for-delegate-receipts',
|
||||
currentAction: '等待动态美术回执',
|
||||
updatedAt: 201,
|
||||
});
|
||||
|
||||
renderGameChatStatus({
|
||||
runtime: root,
|
||||
runtimeByAgentId: { 'code-prototype': main },
|
||||
});
|
||||
|
||||
const status = screen.getByLabelText('最新状态').textContent ?? '';
|
||||
expect(status).toContain('运行中');
|
||||
expect(status).toContain('主阶段 0/1');
|
||||
expect(status).toContain('正在等待专业 Agent 回执');
|
||||
expect(status).not.toContain('本轮已完成');
|
||||
});
|
||||
|
||||
it('shows and archives the explicit mud point interruption from a failed art child runtime', () => {
|
||||
const rootRunId = 'game-chat-mud-point-root-run';
|
||||
const runtime = gameChatRuntimeState({
|
||||
@@ -5899,6 +5992,155 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('freezes a terminal game-chat stage before a delayed conversation refresh and fast next turn', async () => {
|
||||
const projectPath = '/tmp/game-chat-stage-record-fast-next-turn';
|
||||
let professionalRuntimes: AgentRuntimeState[] = [];
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
runtimeMapLoader: async () => professionalRuntimes,
|
||||
});
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'game-chat-stage-record-fast-next-turn',
|
||||
'game-chat-stage-record-fast-next-turn',
|
||||
);
|
||||
manifest.tasks = manifest.tasks.map((task) =>
|
||||
task.id === 'code-prototype'
|
||||
? { ...task, status: 'completed' as const }
|
||||
: task,
|
||||
);
|
||||
const conversationGate = createDeferred<void>();
|
||||
const manifestGate = createDeferred<void>();
|
||||
let delayConversationRefresh = false;
|
||||
let delayManifestCapture = false;
|
||||
let delayedConversationReads = 0;
|
||||
let delayedManifestReads = 0;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
exists: true,
|
||||
isDirectory: true,
|
||||
isGameCreatorProject: true,
|
||||
projectName: 'game-chat-stage-record-fast-next-turn',
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
};
|
||||
}
|
||||
if (command === 'init_local_game_project') {
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}/.agent/manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'get_local_game_preview_status') {
|
||||
return { status: 'stopped', url: null, port: null, root: null };
|
||||
}
|
||||
if (command === 'get_local_game_manifest') {
|
||||
if (delayManifestCapture) {
|
||||
delayedManifestReads += 1;
|
||||
await manifestGate.promise;
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
if (command === 'read_local_conversation' && delayConversationRefresh) {
|
||||
delayedConversationReads += 1;
|
||||
await conversationGate.promise;
|
||||
}
|
||||
return harness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
render(
|
||||
React.createElement(App, {
|
||||
initialProjectPath: projectPath,
|
||||
projectSupervisorOnly: true,
|
||||
gameChatOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const surface = await screen.findByLabelText('游戏创作聊天');
|
||||
const composer = within(surface).getByLabelText(
|
||||
'项目总控对话内容',
|
||||
) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(composer.disabled).toBe(false));
|
||||
fireEvent.change(composer, { target: { value: '完成旧轮' } });
|
||||
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) =>
|
||||
command === 'start_game_creator_supervisor_runtime_task',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
const firstStart = invoke.mock.calls.find(
|
||||
([command]) => command === 'start_game_creator_supervisor_runtime_task',
|
||||
);
|
||||
const firstRunId = String(firstStart?.[1]?.runId ?? '');
|
||||
const main = gameChatPlayableMainRuntime({
|
||||
parentRunId: firstRunId,
|
||||
revision: 17,
|
||||
updatedAt: 1700,
|
||||
});
|
||||
professionalRuntimes = [main];
|
||||
act(() => harness.emitAgentRuntime(main));
|
||||
|
||||
delayConversationRefresh = true;
|
||||
delayManifestCapture = true;
|
||||
act(() => {
|
||||
harness.emitRuntime(
|
||||
harness.runtimeState({
|
||||
runId: firstRunId,
|
||||
source: 'project-supervisor-game-chat',
|
||||
status: 'completed',
|
||||
phase: 'completed',
|
||||
updatedAt: 4000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(delayedConversationReads).toBeGreaterThan(0);
|
||||
expect(delayedManifestReads).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
fireEvent.change(composer, { target: { value: '立即开始下一轮' } });
|
||||
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
|
||||
await waitFor(() => expect(delayedManifestReads).toBeGreaterThan(1));
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'start_game_creator_supervisor_runtime_task',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
manifestGate.resolve(undefined);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) =>
|
||||
command === 'start_game_creator_supervisor_runtime_task',
|
||||
),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
conversationGate.resolve(undefined);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command, args]) =>
|
||||
command === 'append_local_conversation_message' &&
|
||||
String(
|
||||
(args?.message as { content?: string } | undefined)?.content ??
|
||||
'',
|
||||
).startsWith('【Supervisor 阶段记录】'),
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('persists one terminal game-chat stage record and keeps it in the next round', async () => {
|
||||
const projectPath = '/tmp/game-chat-stage-record';
|
||||
let professionalRuntimes: AgentRuntimeState[] = [];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 立项策划 Agent(Fast GDD)技术方案
|
||||
|
||||
- 日期:2026-08-10
|
||||
- 状态:M0A-1 非交付阶段设计检查通过;M0A-2 / M0-3 合同对齐实施中,M1~M3 功能尚未实现
|
||||
- 状态:M0A-1、M0A-2、M0B-1、M0B-2 均已形成独立提交;合入状态以 Git / PR 为准,M1~M3 功能尚未实现
|
||||
- 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入
|
||||
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;合入本文只代表 M0A-1 设计检查点,不代表立项策划入口、审批 UI、Runtime 持久化或构建绑定已经可用
|
||||
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包只冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,不代表立项策划入口、审批 UI、Runtime 持久化或构建绑定已经可用
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
|
||||
Reference in New Issue
Block a user