bcbf398b65
删除旧 GDD 审批卡与策划窄条的孤儿 CSS 更新聊天布局测试以覆盖现役 DirectCodex 选择器 保留设计 Agent 和通用项目工作台表现
2286 lines
71 KiB
TypeScript
2286 lines
71 KiB
TypeScript
import type {
|
||
GameCreationAppManifest,
|
||
GameCreationAppTaskState,
|
||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||
import {
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
PROJECT_SUPERVISOR_PLAN_SOURCE,
|
||
seedManifest,
|
||
} from '../../app/constants';
|
||
import type {
|
||
AgentConversationSessionListResult,
|
||
AgentConversationSessionRecord,
|
||
AgentGoalRecord,
|
||
AgentRuntimeEventRecord,
|
||
AgentRuntimePendingToolActionSummary,
|
||
AgentRuntimePlanStep,
|
||
AgentRuntimeResponseStream,
|
||
AgentRuntimeResult,
|
||
AgentRuntimeState,
|
||
AgentRuntimeSteerResult,
|
||
AgentRuntimeTaskQueueSummary,
|
||
AgentRuntimeTaskRecord,
|
||
ChatMessage,
|
||
LocalConversationMessageRecord,
|
||
TauriInvoke,
|
||
} from '../../app/types';
|
||
const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX = 'runtime-public-status-';
|
||
const AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX = 'runtime-task-';
|
||
const AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX = 'agent-steer-';
|
||
const AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN = /^[0-9a-f]{32}$/;
|
||
|
||
export type ProjectSupervisorRuntimeSubmission = {
|
||
runProfile: 'standard' | 'autonomous-game-build';
|
||
source: 'project-supervisor-gui' | typeof PROJECT_SUPERVISOR_PLAN_SOURCE;
|
||
};
|
||
|
||
export function resolveProjectSupervisorRuntimeSubmission({
|
||
workspaceProjectKind,
|
||
orchestrationMode,
|
||
supervisorChatOnly,
|
||
planningEntry = false,
|
||
}: {
|
||
workspaceProjectKind: 'web' | 'godot' | 'cocos';
|
||
orchestrationMode: 'single-supervisor' | 'professional-dag';
|
||
supervisorChatOnly: boolean;
|
||
planningEntry?: boolean;
|
||
}): ProjectSupervisorRuntimeSubmission {
|
||
// 立项策划入口独立成链:命中时固定 standard + plan source,不参与下面按
|
||
// godot / chat / DAG 的分流,也不改动做游戏与做素材的既有路由。
|
||
if (planningEntry && workspaceProjectKind === 'web') {
|
||
return {
|
||
runProfile: 'standard',
|
||
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
|
||
};
|
||
}
|
||
if (workspaceProjectKind === 'godot' || supervisorChatOnly) {
|
||
return {
|
||
runProfile: 'standard',
|
||
source: 'project-supervisor-gui',
|
||
};
|
||
}
|
||
if (orchestrationMode !== 'single-supervisor') {
|
||
return {
|
||
runProfile: 'autonomous-game-build',
|
||
source: 'project-supervisor-gui',
|
||
};
|
||
}
|
||
return {
|
||
runProfile: 'standard',
|
||
source: 'project-supervisor-gui',
|
||
};
|
||
}
|
||
|
||
function agentRuntimeMessageCorrelationId(
|
||
messageId: string | null | undefined,
|
||
) {
|
||
const normalized = messageId?.trim() ?? '';
|
||
const prefix = normalized.startsWith(
|
||
AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX,
|
||
)
|
||
? AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX
|
||
: normalized.startsWith(AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX)
|
||
? AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX
|
||
: normalized.startsWith(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX)
|
||
? AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX
|
||
: null;
|
||
if (!prefix) {
|
||
return null;
|
||
}
|
||
const correlationId = normalized.slice(prefix.length).split('-', 1)[0] ?? '';
|
||
return AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN.test(correlationId)
|
||
? correlationId
|
||
: null;
|
||
}
|
||
|
||
export function createLocalConversationDraftMessage(
|
||
content: string,
|
||
updatedAt = Date.now(),
|
||
): LocalConversationMessageRecord {
|
||
return {
|
||
schemaVersion: 'game-creator-conversation.v1',
|
||
role: 'assistant',
|
||
content,
|
||
agentId: null,
|
||
updatedAt,
|
||
};
|
||
}
|
||
|
||
export function createAgentChatRunId(prefix: string) {
|
||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
}
|
||
|
||
export function parseAgentGoalItems(value: string) {
|
||
return value
|
||
.split(/\r?\n/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
export function formatAgentConversationSessionMeta(
|
||
session: AgentConversationSessionRecord,
|
||
sessions: AgentConversationSessionRecord[],
|
||
activeSessionId: string | null,
|
||
) {
|
||
const details = [String(session.messageCount)];
|
||
if (session.sessionId === activeSessionId) {
|
||
details.push('活动');
|
||
}
|
||
if (session.forkedFromSessionId) {
|
||
const source = sessions.find(
|
||
(candidate) => candidate.sessionId === session.forkedFromSessionId,
|
||
);
|
||
const sourceLabel = source?.title ?? session.forkedFromSessionId;
|
||
details.push(
|
||
session.forkedMessageCount === null ||
|
||
session.forkedMessageCount === undefined
|
||
? `分支自 ${sourceLabel}`
|
||
: `分支自 ${sourceLabel}(${session.forkedMessageCount} 条)`,
|
||
);
|
||
}
|
||
return details.join(' · ');
|
||
}
|
||
|
||
export function agentRuntimePlanStepsFromPlan(
|
||
plan: string[],
|
||
): AgentRuntimePlanStep[] {
|
||
return plan
|
||
.filter((item) => item.trim().length > 0)
|
||
.slice(0, 8)
|
||
.map((title, index) => ({
|
||
step: title,
|
||
index,
|
||
title,
|
||
status: index === 0 ? 'active' : 'pending',
|
||
detail: null,
|
||
updatedAt: 0,
|
||
}));
|
||
}
|
||
|
||
export function agentRuntimePlanStepText(step: AgentRuntimePlanStep) {
|
||
return (step.step ?? step.title ?? '').trim();
|
||
}
|
||
|
||
export function normalizeAgentRuntimePlanStep(
|
||
step: AgentRuntimePlanStep,
|
||
fallbackIndex: number,
|
||
): AgentRuntimePlanStep | null {
|
||
const text = agentRuntimePlanStepText(step);
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
const index =
|
||
typeof step.index === 'number' &&
|
||
Number.isFinite(step.index) &&
|
||
step.index >= 0
|
||
? Math.trunc(step.index)
|
||
: fallbackIndex;
|
||
return {
|
||
...step,
|
||
step: text,
|
||
index,
|
||
title: step.title?.trim() || text,
|
||
status: step.status?.trim() || 'pending',
|
||
detail: step.detail ?? null,
|
||
updatedAt:
|
||
typeof step.updatedAt === 'number' && Number.isFinite(step.updatedAt)
|
||
? step.updatedAt
|
||
: 0,
|
||
};
|
||
}
|
||
|
||
export function normalizeAgentRuntimePlanStepList(
|
||
steps: AgentRuntimePlanStep[],
|
||
) {
|
||
return steps
|
||
.map((step, index) => normalizeAgentRuntimePlanStep(step, index))
|
||
.filter((step): step is AgentRuntimePlanStep => step !== null);
|
||
}
|
||
|
||
export function normalizeAgentRuntimePlanSteps(
|
||
state: AgentRuntimeState,
|
||
previous?: AgentRuntimeState | null,
|
||
) {
|
||
if (state.planSteps && state.planSteps.length > 0) {
|
||
return normalizeAgentRuntimePlanStepList(state.planSteps);
|
||
}
|
||
if (state.planSteps !== undefined && state.planRevision !== undefined) {
|
||
return [];
|
||
}
|
||
if (previous?.planSteps && previous.planSteps.length > 0) {
|
||
return normalizeAgentRuntimePlanStepList(previous.planSteps);
|
||
}
|
||
return agentRuntimePlanStepsFromPlan(state.plan ?? []);
|
||
}
|
||
|
||
export function normalizeAgentRuntimeActivePlanStepIndex(
|
||
state: AgentRuntimeState,
|
||
planSteps: AgentRuntimePlanStep[],
|
||
previous?: AgentRuntimeState | null,
|
||
) {
|
||
if (state.activePlanStepIndex !== undefined) {
|
||
if (state.activePlanStepIndex === null) {
|
||
return null;
|
||
}
|
||
return Number.isFinite(state.activePlanStepIndex) &&
|
||
state.activePlanStepIndex >= 0
|
||
? Math.trunc(state.activePlanStepIndex)
|
||
: null;
|
||
}
|
||
if (previous?.activePlanStepIndex !== undefined) {
|
||
return previous.activePlanStepIndex;
|
||
}
|
||
const activeStepIndex = planSteps.findIndex((step) =>
|
||
['active', 'in_progress', 'running'].includes(step.status),
|
||
);
|
||
if (activeStepIndex < 0) {
|
||
return null;
|
||
}
|
||
return planSteps[activeStepIndex]?.index ?? activeStepIndex;
|
||
}
|
||
|
||
export function sameAgentRuntimeRun(
|
||
state: AgentRuntimeState,
|
||
previous?: AgentRuntimeState | null,
|
||
) {
|
||
return Boolean(
|
||
previous &&
|
||
previous.agentId === state.agentId &&
|
||
previous.sessionId === state.sessionId &&
|
||
previous.runId === state.runId,
|
||
);
|
||
}
|
||
|
||
export function normalizeAgentRuntimePlanRevision(
|
||
revision: number | undefined,
|
||
previousRevision: number | undefined,
|
||
) {
|
||
if (
|
||
typeof revision === 'number' &&
|
||
Number.isFinite(revision) &&
|
||
revision >= 0
|
||
) {
|
||
return Math.trunc(revision);
|
||
}
|
||
return previousRevision;
|
||
}
|
||
|
||
export function shouldReplaceAgentRuntimePlan(
|
||
state: AgentRuntimeState,
|
||
previous?: AgentRuntimeState | null,
|
||
) {
|
||
if (!previous) {
|
||
return true;
|
||
}
|
||
const previousRevision = normalizeAgentRuntimePlanRevision(
|
||
previous.planRevision,
|
||
undefined,
|
||
);
|
||
if (previousRevision === undefined) {
|
||
return true;
|
||
}
|
||
const nextRevision = normalizeAgentRuntimePlanRevision(
|
||
state.planRevision,
|
||
undefined,
|
||
);
|
||
return nextRevision !== undefined && nextRevision >= previousRevision;
|
||
}
|
||
|
||
export function normalizeAgentRuntimeState(
|
||
state: AgentRuntimeState,
|
||
previous?: AgentRuntimeState | null,
|
||
): AgentRuntimeState {
|
||
const previousPlanState = sameAgentRuntimeRun(state, previous)
|
||
? previous
|
||
: null;
|
||
const replacePlan = shouldReplaceAgentRuntimePlan(state, previousPlanState);
|
||
const planState = replacePlan ? state : previousPlanState!;
|
||
const planFallbackState = replacePlan ? previousPlanState : null;
|
||
const planSteps = normalizeAgentRuntimePlanSteps(
|
||
planState,
|
||
planFallbackState,
|
||
);
|
||
return {
|
||
...state,
|
||
currentGoal: state.currentGoal ?? state.currentTask ?? '',
|
||
goalId:
|
||
state.goalId !== undefined
|
||
? state.goalId
|
||
: (previousPlanState?.goalId ?? null),
|
||
goalRevision:
|
||
state.goalRevision !== undefined
|
||
? state.goalRevision
|
||
: (previousPlanState?.goalRevision ?? 0),
|
||
goalStatus:
|
||
state.goalStatus !== undefined
|
||
? state.goalStatus
|
||
: (previousPlanState?.goalStatus ?? null),
|
||
goalOutcome:
|
||
state.goalOutcome !== undefined
|
||
? state.goalOutcome
|
||
: (previousPlanState?.goalOutcome ?? null),
|
||
goalConstraints:
|
||
state.goalConstraints !== undefined
|
||
? state.goalConstraints
|
||
: (previousPlanState?.goalConstraints ?? []),
|
||
goalVerification:
|
||
state.goalVerification !== undefined
|
||
? state.goalVerification
|
||
: (previousPlanState?.goalVerification ?? []),
|
||
waitingOn: state.waitingOn ?? agentRuntimeWaitingOnFromPhase(state.phase),
|
||
nextStep: state.nextStep ?? agentRuntimeNextStepFromPhase(state.phase),
|
||
loopIteration: state.loopIteration ?? previous?.loopIteration ?? 0,
|
||
startedAt:
|
||
state.startedAt ??
|
||
(previousPlanState?.startedAt && previousPlanState.startedAt > 0
|
||
? previousPlanState.startedAt
|
||
: undefined),
|
||
maxLoopIterations:
|
||
state.maxLoopIterations ?? previous?.maxLoopIterations ?? 3,
|
||
toolActionBudget: state.toolActionBudget ?? previous?.toolActionBudget ?? 3,
|
||
contextUsage: state.contextUsage ??
|
||
previous?.contextUsage ?? {
|
||
estimatedInputTokens: 0,
|
||
autoCompactTokenLimit: 0,
|
||
lastPromptTokens: null,
|
||
lastCompletionTokens: null,
|
||
lastTotalTokens: null,
|
||
compactionRevision: 0,
|
||
compactionCount: 0,
|
||
lastCompactionTrigger: null,
|
||
lastCompactedAt: null,
|
||
},
|
||
plan: planState.plan ?? planFallbackState?.plan ?? [],
|
||
planRevision: normalizeAgentRuntimePlanRevision(
|
||
planState.planRevision,
|
||
planFallbackState?.planRevision,
|
||
),
|
||
planExplanation:
|
||
planState.planExplanation ?? planFallbackState?.planExplanation,
|
||
planSteps,
|
||
activePlanStepIndex: normalizeAgentRuntimeActivePlanStepIndex(
|
||
planState,
|
||
planSteps,
|
||
planFallbackState,
|
||
),
|
||
recentToolCalls: state.recentToolCalls ?? previous?.recentToolCalls ?? [],
|
||
userInputRequest:
|
||
state.userInputRequest !== undefined
|
||
? state.userInputRequest
|
||
: (previousPlanState?.userInputRequest ?? null),
|
||
toolPolicy: state.toolPolicy ??
|
||
previous?.toolPolicy ?? {
|
||
allowedTools: state.allowedTools ?? [],
|
||
autoTools: [],
|
||
confirmTools: [],
|
||
deniedTools: [],
|
||
updatedAt: 0,
|
||
},
|
||
taskQueue: {
|
||
...(state.taskQueue ??
|
||
previousPlanState?.taskQueue ?? {
|
||
total: 0,
|
||
pending: 0,
|
||
running: 0,
|
||
waitingForConfirmation: 0,
|
||
waitingForUserInput: 0,
|
||
paused: 0,
|
||
cancelled: 0,
|
||
completed: 0,
|
||
failed: 0,
|
||
latestRunId: null,
|
||
updatedAt: 0,
|
||
}),
|
||
paused:
|
||
state.taskQueue?.paused ?? previousPlanState?.taskQueue?.paused ?? 0,
|
||
waitingForUserInput:
|
||
state.taskQueue?.waitingForUserInput ??
|
||
previousPlanState?.taskQueue?.waitingForUserInput ??
|
||
0,
|
||
},
|
||
recentEvents: state.recentEvents ?? previous?.recentEvents ?? [],
|
||
recentTasks: state.recentTasks ?? previous?.recentTasks ?? [],
|
||
};
|
||
}
|
||
|
||
export function mergeAgentGoalRecordFromRuntime(
|
||
goal: AgentGoalRecord | null,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
if (
|
||
!goal ||
|
||
!runtime.goalId ||
|
||
goal.goalId !== runtime.goalId ||
|
||
goal.agentId !== runtime.agentId ||
|
||
goal.sessionId !== runtime.sessionId
|
||
) {
|
||
return goal;
|
||
}
|
||
return {
|
||
...goal,
|
||
revision: runtime.goalRevision ?? goal.revision,
|
||
status: runtime.goalStatus ?? goal.status,
|
||
outcome: runtime.goalOutcome ?? goal.outcome,
|
||
constraints: runtime.goalConstraints ?? goal.constraints,
|
||
verification: runtime.goalVerification ?? goal.verification,
|
||
updatedAt: Math.max(goal.updatedAt, runtime.updatedAt),
|
||
};
|
||
}
|
||
|
||
export function mergeAgentRuntimeStateIntoMap(
|
||
current: Record<string, AgentRuntimeState | undefined>,
|
||
incoming: AgentRuntimeState,
|
||
preserveNewerRun: boolean,
|
||
) {
|
||
const previous = Object.values(current).find(
|
||
(runtime) => runtime?.agentId === incoming.agentId,
|
||
);
|
||
const mergedRuntime =
|
||
preserveNewerRun &&
|
||
previous &&
|
||
(sameAgentRuntimeRun(incoming, previous)
|
||
? previous.updatedAt > incoming.updatedAt
|
||
: previous.updatedAt >= incoming.updatedAt)
|
||
? previous
|
||
: normalizeAgentRuntimeState(incoming, previous);
|
||
const next = { ...current };
|
||
for (const [key, runtime] of Object.entries(next)) {
|
||
if (runtime?.agentId === incoming.agentId) {
|
||
delete next[key];
|
||
}
|
||
}
|
||
next[mergedRuntime.agentId] = mergedRuntime;
|
||
next[mergedRuntime.taskId] = mergedRuntime;
|
||
return next;
|
||
}
|
||
|
||
export function agentRuntimeStateFromResult(
|
||
result: AgentRuntimeResult,
|
||
previous?: AgentRuntimeState | null,
|
||
): AgentRuntimeState {
|
||
const acceptedRunId = result.acceptedRunId?.trim();
|
||
const acceptedTask = acceptedRunId
|
||
? (result.recentTasks ?? result.state.recentTasks ?? []).find(
|
||
(task) => task.runId === acceptedRunId,
|
||
)
|
||
: null;
|
||
const state =
|
||
acceptedTask && result.state.runId !== acceptedRunId
|
||
? {
|
||
...result.state,
|
||
agentId: acceptedTask.agentId,
|
||
taskId: acceptedTask.taskId,
|
||
sessionId: acceptedTask.sessionId,
|
||
runId: acceptedTask.runId,
|
||
source: acceptedTask.source,
|
||
parentAgentId: acceptedTask.parentAgentId ?? null,
|
||
parentRunId: acceptedTask.parentRunId ?? null,
|
||
delegationId: acceptedTask.delegationId ?? null,
|
||
goalId: acceptedTask.goalId ?? null,
|
||
goalRevision: acceptedTask.goalRevision ?? 0,
|
||
goalStatus: acceptedTask.goalStatus ?? null,
|
||
currentTask: acceptedTask.task,
|
||
currentGoal: acceptedTask.task,
|
||
status: acceptedTask.status,
|
||
phase: acceptedTask.phase,
|
||
currentAction: acceptedTask.currentAction,
|
||
waitingOn: agentRuntimeWaitingOnFromPhase(acceptedTask.phase),
|
||
nextStep: agentRuntimeNextStepFromPhase(acceptedTask.phase),
|
||
plan: [],
|
||
planRevision: undefined,
|
||
planExplanation: undefined,
|
||
planSteps: [],
|
||
activePlanStepIndex: null,
|
||
observations: [],
|
||
recentToolCalls: [],
|
||
pendingToolAction: null,
|
||
userInputRequest: null,
|
||
lastResponse: null,
|
||
error: acceptedTask.error,
|
||
startedAt: acceptedTask.updatedAt,
|
||
updatedAt: acceptedTask.updatedAt,
|
||
}
|
||
: result.state;
|
||
return normalizeAgentRuntimeState(
|
||
{
|
||
...state,
|
||
taskQueue: result.taskQueue ?? state.taskQueue,
|
||
recentEvents: result.recentEvents ?? state.recentEvents,
|
||
recentTasks: result.recentTasks ?? state.recentTasks,
|
||
userInputRequest:
|
||
result.userInputRequest !== undefined
|
||
? result.userInputRequest
|
||
: state.userInputRequest,
|
||
},
|
||
previous,
|
||
);
|
||
}
|
||
|
||
export function normalizeProjectSupervisorResponseStream(
|
||
stream: AgentRuntimeResponseStream | null | undefined,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
if (!stream) {
|
||
return null;
|
||
}
|
||
const integerFields = [
|
||
stream.appliedSteerCursor,
|
||
stream.responseRevision,
|
||
stream.sequence,
|
||
stream.startedAt,
|
||
stream.updatedAt,
|
||
];
|
||
if (
|
||
stream.schemaVersion !== 'game-creator-runtime-response-stream.v1' ||
|
||
stream.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
stream.agentId !== runtime.agentId ||
|
||
stream.taskId !== runtime.taskId ||
|
||
stream.sessionId !== runtime.sessionId ||
|
||
stream.runId !== runtime.runId ||
|
||
stream.requestKind !== 'final-reply' ||
|
||
!stream.requestSlot.trim() ||
|
||
integerFields.some(
|
||
(value) =>
|
||
typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0,
|
||
) ||
|
||
stream.startedAt <= 0 ||
|
||
stream.updatedAt < stream.startedAt ||
|
||
typeof stream.accumulatedText !== 'string' ||
|
||
Array.from(stream.accumulatedText).length > 32_000 ||
|
||
/<\/?think>/i.test(stream.accumulatedText)
|
||
) {
|
||
return null;
|
||
}
|
||
if (
|
||
typeof runtime.appliedSteerCursor === 'number' &&
|
||
stream.appliedSteerCursor !== runtime.appliedSteerCursor
|
||
) {
|
||
return null;
|
||
}
|
||
if ((runtime.queuedSteerCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
if (
|
||
typeof runtime.loopIteration === 'number' &&
|
||
Number.isSafeInteger(runtime.loopIteration) &&
|
||
stream.requestSlot !==
|
||
`final-reply-loop-${runtime.loopIteration}-revision-${stream.responseRevision}`
|
||
) {
|
||
return null;
|
||
}
|
||
if (
|
||
(stream.status === 'streaming' || stream.status === 'ready') &&
|
||
(runtime.status !== 'running' ||
|
||
!['response', 'finalizing'].includes(runtime.phase))
|
||
) {
|
||
return null;
|
||
}
|
||
if (stream.status === 'ready' && !stream.accumulatedText.trim()) {
|
||
return null;
|
||
}
|
||
return stream;
|
||
}
|
||
|
||
export function sameProjectSupervisorResponseStream(
|
||
left: AgentRuntimeResponseStream,
|
||
right: AgentRuntimeResponseStream,
|
||
) {
|
||
return (
|
||
left.agentId === right.agentId &&
|
||
left.taskId === right.taskId &&
|
||
left.sessionId === right.sessionId &&
|
||
left.runId === right.runId &&
|
||
left.requestKind === right.requestKind &&
|
||
left.requestSlot === right.requestSlot &&
|
||
left.appliedSteerCursor === right.appliedSteerCursor &&
|
||
left.responseRevision === right.responseRevision
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Durable identity for one final-reply response. The response text is
|
||
* intentionally excluded so two turns with identical wording remain
|
||
* distinct messages in the conversation.
|
||
*/
|
||
export function projectSupervisorResponseStreamIdentity(
|
||
stream: Pick<
|
||
AgentRuntimeResponseStream,
|
||
'runId' | 'requestSlot' | 'responseRevision'
|
||
>,
|
||
) {
|
||
return [stream.runId, stream.requestSlot, stream.responseRevision].join(
|
||
'\u001f',
|
||
);
|
||
}
|
||
|
||
export function mergeProjectSupervisorResponseStream(
|
||
current: AgentRuntimeResponseStream | null,
|
||
incoming: AgentRuntimeResponseStream | null | undefined,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
const currentForRuntime = normalizeProjectSupervisorResponseStream(
|
||
current,
|
||
runtime,
|
||
);
|
||
if (!incoming) {
|
||
return null;
|
||
}
|
||
const next = normalizeProjectSupervisorResponseStream(incoming, runtime);
|
||
if (!next) {
|
||
return currentForRuntime;
|
||
}
|
||
if (next.status !== 'streaming' && next.status !== 'ready') {
|
||
return null;
|
||
}
|
||
if (
|
||
!currentForRuntime ||
|
||
!sameProjectSupervisorResponseStream(currentForRuntime, next)
|
||
) {
|
||
return next;
|
||
}
|
||
if (next.sequence <= currentForRuntime.sequence) {
|
||
return currentForRuntime;
|
||
}
|
||
return next;
|
||
}
|
||
|
||
export function conversationContainsProjectSupervisorResponseStream(
|
||
messages: LocalConversationMessageRecord[],
|
||
stream: AgentRuntimeResponseStream | null | undefined,
|
||
) {
|
||
return Boolean(
|
||
stream?.accumulatedText.trim() &&
|
||
messages.some(
|
||
(message) =>
|
||
message.role === 'assistant' &&
|
||
message.content === stream.accumulatedText &&
|
||
message.updatedAt >= stream.startedAt,
|
||
),
|
||
);
|
||
}
|
||
|
||
export function agentRuntimeWaitingOnFromPhase(phase: string) {
|
||
switch (phase) {
|
||
case 'planning':
|
||
return 'Agent 输出计划或回复';
|
||
case 'llm':
|
||
return 'Agent LLM 回复';
|
||
case 'action':
|
||
return '工具观察结果';
|
||
case 'waiting-for-confirmation':
|
||
return '开发者确认 Agent 工具动作';
|
||
case 'waiting-for-user-input':
|
||
return '你的澄清回答';
|
||
case 'cancelling':
|
||
return '当前 LLM 或工具调用返回';
|
||
case 'pausing':
|
||
return '当前动作到达安全边界';
|
||
case 'paused':
|
||
return '开发者恢复持久目标';
|
||
case 'response':
|
||
return 'Agent 整理最终回复';
|
||
case 'completed':
|
||
case 'idle':
|
||
return '开发者下一轮输入';
|
||
case 'cancelled':
|
||
return '开发者下一轮输入';
|
||
case 'failed':
|
||
return '开发者处理失败';
|
||
default:
|
||
return '当前任务推进';
|
||
}
|
||
}
|
||
|
||
export function agentRuntimeNextStepFromPhase(phase: string) {
|
||
switch (phase) {
|
||
case 'planning':
|
||
return '等待 Agent 输出计划或回复';
|
||
case 'action':
|
||
return '等待工具观察结果';
|
||
case 'waiting-for-confirmation':
|
||
return '等待开发者确认工具动作';
|
||
case 'waiting-for-user-input':
|
||
return '提交全部回答后继续同一 Run';
|
||
case 'cancelling':
|
||
return '取消完成后可重试该任务或提交新任务';
|
||
case 'pausing':
|
||
return '等待持久目标暂停';
|
||
case 'paused':
|
||
return '恢复持久目标后继续同一 Run';
|
||
case 'response':
|
||
return '等待 Agent 整理最终回复';
|
||
case 'completed':
|
||
case 'idle':
|
||
return '等待下一轮输入';
|
||
case 'cancelled':
|
||
return '可重试该后台任务或提交新任务';
|
||
case 'failed':
|
||
return '等待开发者处理失败';
|
||
default:
|
||
return '继续推进当前任务';
|
||
}
|
||
}
|
||
|
||
export function agentRuntimeStartStatus(result: AgentRuntimeResult) {
|
||
const pendingTask = (result.recentTasks ?? []).find(
|
||
(task) => task.status === 'pending',
|
||
);
|
||
return pendingTask
|
||
? `已加入后台队列:${pendingTask.runId}`
|
||
: `已启动后台任务:${result.state.runId}`;
|
||
}
|
||
|
||
export function agentRuntimeStartedRunId(
|
||
result: AgentRuntimeResult,
|
||
requestedRunId: string,
|
||
) {
|
||
if (result.acceptedRunId?.trim()) {
|
||
return result.acceptedRunId.trim();
|
||
}
|
||
if (
|
||
result.recentTasks?.some((task) => task.runId === requestedRunId) ||
|
||
result.state.runId === requestedRunId
|
||
) {
|
||
return requestedRunId;
|
||
}
|
||
return (
|
||
result.taskQueue?.latestRunId ??
|
||
result.state.taskQueue?.latestRunId ??
|
||
result.state.runId ??
|
||
requestedRunId
|
||
);
|
||
}
|
||
|
||
export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) {
|
||
return (
|
||
['completed', 'failed', 'cancelled', 'needs-reconciliation'].includes(
|
||
runtime.phase,
|
||
) ||
|
||
[
|
||
'completed',
|
||
'failed',
|
||
'cancelled',
|
||
'idle',
|
||
'needs-reconciliation',
|
||
].includes(runtime.status)
|
||
);
|
||
}
|
||
|
||
export function isAgentRuntimeSteerableState(runtime: AgentRuntimeState) {
|
||
if (isAgentRuntimeTerminalState(runtime)) {
|
||
return false;
|
||
}
|
||
const blockedStates = [
|
||
'cancelling',
|
||
'finalizing',
|
||
'needs-reconciliation',
|
||
'waiting-for-user-input',
|
||
];
|
||
if (
|
||
blockedStates.includes(runtime.status) ||
|
||
blockedStates.includes(runtime.phase)
|
||
) {
|
||
return false;
|
||
}
|
||
return (
|
||
['running', 'waiting-for-confirmation'].includes(runtime.status) ||
|
||
[
|
||
'planning',
|
||
'llm',
|
||
'action',
|
||
'observation',
|
||
'waiting-for-confirmation',
|
||
'waiting-for-isolated-join',
|
||
'waiting-for-delegate-receipts',
|
||
'waiting-for-manifest-tasks',
|
||
'response',
|
||
].includes(runtime.phase)
|
||
);
|
||
}
|
||
|
||
export function agentRuntimeNeedsUserInput(
|
||
runtime: AgentRuntimeState | null | undefined,
|
||
) {
|
||
return Boolean(
|
||
runtime?.userInputRequest ||
|
||
runtime?.status === 'waiting-for-user-input' ||
|
||
runtime?.phase === 'waiting-for-user-input',
|
||
);
|
||
}
|
||
|
||
export const AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL = 'user.input_request';
|
||
export const AGENT_RUNTIME_PLAN_SUBMIT_GDD_TOOL = 'plan.submit_gdd';
|
||
|
||
// 有两个 pending 动作不是「等待批准的动作」,而是某张专用卡的载体:Runtime 把内容停在
|
||
// pending 上,真正的交互面是另一个组件。
|
||
//
|
||
// - `user.input_request`:子 Agent 以 needs-user-input 终态退出后,Runtime 在 parent-wake
|
||
// 屏障处按信封原文构造这个 pending(`ensure_static_delegate_user_input_wait_at_locked`),
|
||
// 同一份问题再投影成 `userInputRequest`,交互面是问答卡。
|
||
// - `plan.submit_gdd`:策划子 Agent 提交 Fast GDD 后,`planning/pending.json` 的
|
||
// 的批准 / 修改 / 退回。
|
||
//
|
||
// 两者都没有「确认 / 拒绝」语义:`user.input_request` 从来不在
|
||
// `GAME_CREATION_APP_COMMANDS` 的 confirm 集合里;`plan.submit_gdd` 更是被运行时策略明确
|
||
// 拒绝转成通用确认 pending(「Runtime-owned create-only 提交」),点确认必然失败。
|
||
//
|
||
// 通用待确认卡按 `tool` + `inputSummary` 渲染,套到它们身上就是把同一个请求画两遍——标题
|
||
// 是原始工具名,副标题是 `questionCount=… · questionsSha256=…` 这种给日志看的取证摘要,
|
||
// 还会把真正该看的那张卡压在下面。所以这里统一把通用确认面判掉。
|
||
const AGENT_RUNTIME_DEDICATED_CARD_TOOLS = new Set([
|
||
AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL,
|
||
AGENT_RUNTIME_PLAN_SUBMIT_GDD_TOOL,
|
||
]);
|
||
|
||
export function agentRuntimePendingActionHasDedicatedCard(
|
||
action: AgentRuntimePendingToolActionSummary | null | undefined,
|
||
) {
|
||
return Boolean(action && AGENT_RUNTIME_DEDICATED_CARD_TOOLS.has(action.tool));
|
||
}
|
||
|
||
export function matchingAgentRuntimeForSteer(
|
||
runtimes: Array<AgentRuntimeState | null | undefined>,
|
||
agentId: string,
|
||
sessionId: string | null,
|
||
requestedRunProfile: NonNullable<
|
||
AgentRuntimeState['runProfile']
|
||
> = 'standard',
|
||
requestedSource?: string,
|
||
) {
|
||
if (!sessionId) {
|
||
return null;
|
||
}
|
||
return (
|
||
runtimes.find(
|
||
(runtime) =>
|
||
runtime?.agentId === agentId &&
|
||
runtime.sessionId === sessionId &&
|
||
(runtime.runProfile ?? 'standard') === requestedRunProfile &&
|
||
(!requestedSource || runtime.source === requestedSource) &&
|
||
isAgentRuntimeSteerableState(runtime),
|
||
) ?? null
|
||
);
|
||
}
|
||
|
||
export function projectSupervisorActiveSessionIdFromList(
|
||
sessionList: AgentConversationSessionListResult,
|
||
) {
|
||
if (
|
||
!sessionList ||
|
||
sessionList.agentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
!Array.isArray(sessionList.sessions)
|
||
) {
|
||
throw new Error('项目总控 Agent 会话列表返回格式无效');
|
||
}
|
||
const activeSession = sessionList.activeSessionId
|
||
? sessionList.sessions.find(
|
||
(session) =>
|
||
session.sessionId === sessionList.activeSessionId &&
|
||
session.archivedAt === null,
|
||
)
|
||
: null;
|
||
return (
|
||
activeSession?.sessionId ||
|
||
sessionList.sessions.find((session) => session.archivedAt === null)
|
||
?.sessionId ||
|
||
null
|
||
);
|
||
}
|
||
|
||
export async function readProjectSupervisorActiveSessionId(
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
) {
|
||
try {
|
||
const sessionList = await invoke<AgentConversationSessionListResult>(
|
||
'list_game_creator_agent_sessions',
|
||
{
|
||
projectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
},
|
||
);
|
||
return projectSupervisorActiveSessionIdFromList(sessionList);
|
||
} catch (error) {
|
||
if (isMissingAgentSessionCommandError(error)) {
|
||
return null;
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function ensureProjectSupervisorActiveSessionId(
|
||
invoke: TauriInvoke,
|
||
projectPath: string,
|
||
) {
|
||
const existingSessionId = await readProjectSupervisorActiveSessionId(
|
||
invoke,
|
||
projectPath,
|
||
);
|
||
if (existingSessionId) {
|
||
return existingSessionId;
|
||
}
|
||
const sessionList = await invoke<AgentConversationSessionListResult>(
|
||
'create_game_creator_agent_session',
|
||
{
|
||
projectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
title: '项目总控',
|
||
},
|
||
);
|
||
const sessionId = projectSupervisorActiveSessionIdFromList(sessionList);
|
||
if (!sessionId) {
|
||
throw new Error('项目总控 Agent active Session 创建失败');
|
||
}
|
||
return sessionId;
|
||
}
|
||
|
||
export async function submitProjectSupervisorRuntimeTask({
|
||
invoke,
|
||
projectPath,
|
||
sessionId,
|
||
prompt,
|
||
runtime,
|
||
runProfile,
|
||
source,
|
||
}: {
|
||
invoke: TauriInvoke;
|
||
projectPath: string;
|
||
sessionId: string;
|
||
prompt: string;
|
||
runtime: AgentRuntimeState | null;
|
||
runProfile: 'standard' | 'autonomous-game-build';
|
||
source: ProjectSupervisorRuntimeSubmission['source'];
|
||
}) {
|
||
const steerRuntime = matchingAgentRuntimeForSteer(
|
||
[runtime],
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
runProfile,
|
||
source,
|
||
);
|
||
if (steerRuntime) {
|
||
const steer = await invoke<AgentRuntimeSteerResult>(
|
||
'steer_game_creator_agent_runtime_task',
|
||
{
|
||
projectPath,
|
||
agentId: PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
runId: steerRuntime.runId,
|
||
steerId: createAgentChatRunId('project-supervisor-steer'),
|
||
instruction: prompt,
|
||
runProfile,
|
||
source,
|
||
},
|
||
);
|
||
return {
|
||
mode: 'steer' as const,
|
||
runtimeResult: steer.runtime,
|
||
acceptedRunId: steerRuntime.runId,
|
||
};
|
||
}
|
||
const requestedRunId = createAgentChatRunId('project-supervisor-task');
|
||
const runtimeResult = await invoke<AgentRuntimeResult>(
|
||
'start_game_creator_supervisor_runtime_task',
|
||
{
|
||
projectPath,
|
||
sessionId,
|
||
task: prompt,
|
||
runId: requestedRunId,
|
||
runProfile,
|
||
source,
|
||
},
|
||
);
|
||
return {
|
||
mode: 'start' as const,
|
||
runtimeResult,
|
||
acceptedRunId: agentRuntimeStartedRunId(runtimeResult, requestedRunId),
|
||
};
|
||
}
|
||
|
||
export function agentRuntimeSteerStatus(result: AgentRuntimeSteerResult) {
|
||
const runId = result.runtime.state.runId;
|
||
if (result.status === 'restarted') {
|
||
return `目标已变化,正在新 Run 重新理解并执行:${runId}`;
|
||
}
|
||
if (result.providerInterrupted) {
|
||
return `智能服务已根据追加指令调整,旧请求已安全中断:${runId}`;
|
||
}
|
||
if (result.interruptDecision === false) {
|
||
return `智能服务已回复且判定无需中断,当前 Run 继续:${runId}`;
|
||
}
|
||
if (result.interruptDecision === true) {
|
||
return `智能服务已判定需要改向;旧请求已结束或新规划已开始:${runId}`;
|
||
}
|
||
if (result.status === 'applied') {
|
||
return `追加指令已应用,当前 Run 正在继续:${runId}`;
|
||
}
|
||
return `追加指令已排队,等待当前 Run 应用:${runId}`;
|
||
}
|
||
|
||
export function agentRuntimeCancelStatus(
|
||
runtime: AgentRuntimeState,
|
||
runId: string,
|
||
) {
|
||
return runtime.status === 'cancelling' || runtime.phase === 'cancelling'
|
||
? `正在取消后台任务:${runId}`
|
||
: `已取消后台任务:${runId}`;
|
||
}
|
||
|
||
function agentRuntimeProviderRetryStatus(runtime: AgentRuntimeState) {
|
||
if (runtime.phase !== 'waiting-for-provider-retry') {
|
||
return null;
|
||
}
|
||
const currentAction = runtime.currentAction?.trim();
|
||
const waitingOn = runtime.waitingOn?.trim();
|
||
const safeCurrentAction =
|
||
currentAction &&
|
||
/^(?:Goal 已恢复,)?Provider (?:上游返回 HTTP \d{3}|瞬态故障),准备自动重试 \d+\/\d+$/.test(
|
||
currentAction,
|
||
)
|
||
? currentAction
|
||
: null;
|
||
const safeWaitingOn =
|
||
waitingOn && /^预计 \d+ 秒后重试$/.test(waitingOn) ? waitingOn : null;
|
||
if (safeCurrentAction && safeWaitingOn) {
|
||
return `${safeCurrentAction.replaceAll('Provider', '智能服务')};${safeWaitingOn}`;
|
||
}
|
||
if (safeCurrentAction) {
|
||
return safeCurrentAction.replaceAll('Provider', '智能服务');
|
||
}
|
||
|
||
const legacyAttempt = currentAction?.match(
|
||
/^(?:等待 Provider 瞬态重试|Goal 已恢复,继续等待 Provider 瞬态重试) (\d+)\/(\d+)$/,
|
||
);
|
||
const retryProgress =
|
||
legacyAttempt?.[1] && legacyAttempt[2]
|
||
? `,准备自动重试 ${legacyAttempt[1]}/${legacyAttempt[2]}`
|
||
: ',正在准备自动重试';
|
||
const safeFallback = `智能服务暂时不可用${retryProgress}`;
|
||
return safeWaitingOn ? `${safeFallback};${safeWaitingOn}` : safeFallback;
|
||
}
|
||
|
||
export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
||
if (isAgentRuntimeTerminalState(runtime)) {
|
||
if (
|
||
runtime.status === 'needs-reconciliation' ||
|
||
runtime.phase === 'needs-reconciliation'
|
||
) {
|
||
return 'Agent 运行状态需要核对,正在同步记录';
|
||
}
|
||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||
return 'Agent 运行失败,正在同步错误记录';
|
||
}
|
||
if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') {
|
||
return 'Agent 任务已取消,正在同步对话';
|
||
}
|
||
return 'Agent 已完成,正在同步回复';
|
||
}
|
||
if (runtime.status === 'pending' || runtime.phase === 'queued') {
|
||
return 'Agent 任务已排队,正在等待执行';
|
||
}
|
||
if (
|
||
runtime.status === 'waiting-for-user-input' ||
|
||
runtime.phase === 'waiting-for-user-input'
|
||
) {
|
||
return 'Agent 需要你补充信息';
|
||
}
|
||
if (
|
||
runtime.status === 'pausing' ||
|
||
runtime.phase === 'pausing' ||
|
||
runtime.goalStatus === 'pause-requested'
|
||
) {
|
||
return '持久目标正在暂停';
|
||
}
|
||
if (
|
||
runtime.status === 'paused' ||
|
||
runtime.phase === 'paused' ||
|
||
runtime.goalStatus === 'paused'
|
||
) {
|
||
return '持久目标已暂停';
|
||
}
|
||
const providerRetryStatus = agentRuntimeProviderRetryStatus(runtime);
|
||
if (providerRetryStatus) {
|
||
return providerRetryStatus;
|
||
}
|
||
const waitingOn =
|
||
runtime.waitingOn ?? agentRuntimeWaitingOnFromPhase(runtime.phase);
|
||
return waitingOn ? `Agent 正在运行,等待${waitingOn}` : 'Agent 正在运行';
|
||
}
|
||
|
||
export function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) {
|
||
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
||
return '等待输入';
|
||
}
|
||
if (
|
||
runtime.status === 'needs-reconciliation' ||
|
||
runtime.phase === 'needs-reconciliation'
|
||
) {
|
||
return runtime.error
|
||
? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true)
|
||
: '项目总控 Agent 运行状态需要核对,请打开运行详情后重试';
|
||
}
|
||
if (isAgentRuntimeTerminalState(runtime)) {
|
||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||
return runtime.error
|
||
? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true)
|
||
: 'Agent 运行失败';
|
||
}
|
||
if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') {
|
||
return '本轮已取消';
|
||
}
|
||
return '本轮已完成';
|
||
}
|
||
return agentRuntimeConversationStatus(runtime);
|
||
}
|
||
|
||
export function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) {
|
||
const isFailureEvent = [
|
||
'error',
|
||
'turn.failed',
|
||
'turn.budget_exhausted',
|
||
].includes(event.eventType);
|
||
const visibleDetail = isFailureEvent ? null : event.detail;
|
||
const publicFailureText =
|
||
isFailureEvent && typeof event.publicText === 'string'
|
||
? event.publicText.trim()
|
||
: '';
|
||
const summary =
|
||
publicFailureText ||
|
||
event.summary ||
|
||
visibleDetail ||
|
||
(isFailureEvent ? 'Agent Runtime 本轮处理失败。' : event.runId);
|
||
const detail =
|
||
visibleDetail && visibleDetail !== summary ? ` · ${visibleDetail}` : '';
|
||
return `${event.eventType} · ${event.status} / ${event.phase} · ${summary}${detail}`;
|
||
}
|
||
|
||
export function formatAgentRuntimeTaskQueue(
|
||
queue: AgentRuntimeTaskQueueSummary | null | undefined,
|
||
) {
|
||
if (!queue || queue.total <= 0) {
|
||
return null;
|
||
}
|
||
const parts = [
|
||
`pending ${queue.pending}`,
|
||
`running ${queue.running}`,
|
||
`waiting ${queue.waitingForConfirmation ?? 0}`,
|
||
`needsInput ${queue.waitingForUserInput ?? 0}`,
|
||
];
|
||
if ((queue.paused ?? 0) > 0) {
|
||
parts.push(`paused ${queue.paused}`);
|
||
}
|
||
parts.push(
|
||
`cancelled ${queue.cancelled ?? 0}`,
|
||
`completed ${queue.completed}`,
|
||
`failed ${queue.failed}`,
|
||
`total ${queue.total}`,
|
||
);
|
||
if (queue.latestRunId) {
|
||
parts.push(`latest ${queue.latestRunId}`);
|
||
}
|
||
return `任务队列:${parts.join(' · ')}`;
|
||
}
|
||
|
||
export function formatAgentRuntimeLoopProgress(
|
||
runtime: Pick<
|
||
AgentRuntimeState,
|
||
'loopIteration' | 'maxLoopIterations' | 'toolActionBudget'
|
||
>,
|
||
) {
|
||
const loopIteration = runtime.loopIteration ?? 0;
|
||
if (loopIteration <= 0) {
|
||
return null;
|
||
}
|
||
return `Loop:${loopIteration}/${runtime.maxLoopIterations ?? 3} · 工具预算 ${
|
||
runtime.toolActionBudget ?? 3
|
||
}`;
|
||
}
|
||
|
||
export function formatAgentRuntimePlanStep(
|
||
step: AgentRuntimePlanStep,
|
||
fallbackIndex = 0,
|
||
) {
|
||
const index = step.index ?? fallbackIndex;
|
||
return `#${index + 1} ${step.status} · ${agentRuntimePlanStepText(step)}${
|
||
step.detail ? ` · ${step.detail}` : ''
|
||
}`;
|
||
}
|
||
|
||
export function agentRuntimeActivePlanStep(
|
||
runtime: Pick<AgentRuntimeState, 'planSteps' | 'activePlanStepIndex'>,
|
||
) {
|
||
const steps = runtime.planSteps ?? [];
|
||
const activePlanStepIndex = runtime.activePlanStepIndex;
|
||
if (activePlanStepIndex !== null && activePlanStepIndex !== undefined) {
|
||
const indexedStep =
|
||
steps.find((step) => step.index === activePlanStepIndex) ??
|
||
steps[activePlanStepIndex];
|
||
if (indexedStep) {
|
||
return indexedStep;
|
||
}
|
||
}
|
||
return (
|
||
steps.find((step) =>
|
||
['active', 'in_progress', 'running'].includes(step.status),
|
||
) ??
|
||
steps.find((step) => step.status === 'pending') ??
|
||
null
|
||
);
|
||
}
|
||
|
||
export function agentRuntimeCanCancel(status: string) {
|
||
return [
|
||
'pending',
|
||
'running',
|
||
'waiting-for-confirmation',
|
||
'waiting-for-user-input',
|
||
].includes(status);
|
||
}
|
||
|
||
export function agentRuntimeCanRetry(status: string) {
|
||
return ['cancelled', 'failed', 'completed', 'idle'].includes(status);
|
||
}
|
||
|
||
export function agentGoalStatusIsPaused(status: string | null | undefined) {
|
||
return ['pause-requested', 'pausing', 'paused'].includes(status ?? '');
|
||
}
|
||
|
||
export function agentGoalStatusIsTerminal(status: string | null | undefined) {
|
||
return ['completed', 'cleared'].includes(status ?? '');
|
||
}
|
||
|
||
export function agentRuntimeCanConfirm(status: string) {
|
||
return status === 'waiting-for-confirmation';
|
||
}
|
||
|
||
export function formatAgentRuntimeDelegationSource(runtime: {
|
||
source: string;
|
||
parentAgentId?: string | null;
|
||
parentRunId?: string | null;
|
||
delegationId?: string | null;
|
||
}) {
|
||
if (runtime.source === 'agent-delegate-receipt') {
|
||
return [
|
||
'来源:委派回执',
|
||
runtime.delegationId ? `委派:${runtime.delegationId}` : null,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' · ');
|
||
}
|
||
const parts = [
|
||
runtime.parentAgentId ? `委派自:${runtime.parentAgentId}` : null,
|
||
runtime.parentRunId ? `父 run:${runtime.parentRunId}` : null,
|
||
runtime.delegationId ? `委派:${runtime.delegationId}` : null,
|
||
].filter(Boolean);
|
||
return parts.length > 0 ? parts.join(' · ') : null;
|
||
}
|
||
|
||
export function createAgentRuntimeUserInputResponseId() {
|
||
const entropy =
|
||
typeof globalThis.crypto?.randomUUID === 'function'
|
||
? globalThis.crypto.randomUUID()
|
||
: Math.random().toString(36).slice(2);
|
||
return `app-user-input-${Date.now().toString(36)}-${entropy}`.slice(0, 160);
|
||
}
|
||
|
||
export function isMissingAgentSessionCommandError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
const normalized = message.toLowerCase();
|
||
return (
|
||
normalized.includes('list_game_creator_agent_sessions') &&
|
||
(normalized.includes('not found') ||
|
||
normalized.includes('unknown command') ||
|
||
normalized.includes('unexpected invoke') ||
|
||
normalized.includes('unexpected command') ||
|
||
normalized.includes('不存在') ||
|
||
normalized.includes('未找到'))
|
||
);
|
||
}
|
||
|
||
export function isMissingAgentRuntimeResumeCommandError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
const normalized = message.toLowerCase();
|
||
return (
|
||
normalized.includes('resume_game_creator_agent_runtime_tasks') &&
|
||
(normalized.includes('not found') ||
|
||
normalized.includes('unknown command') ||
|
||
normalized.includes('unexpected invoke') ||
|
||
normalized.includes('unexpected command') ||
|
||
normalized.includes('不存在') ||
|
||
normalized.includes('未找到'))
|
||
);
|
||
}
|
||
|
||
export function isMissingAgentGoalCommandError(error: unknown) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
const normalized = message.toLowerCase();
|
||
return (
|
||
normalized.includes('game_creator_agent_goal') &&
|
||
(normalized.includes('not found') ||
|
||
normalized.includes('unknown command') ||
|
||
normalized.includes('unexpected invoke') ||
|
||
normalized.includes('unexpected command') ||
|
||
normalized.includes('不存在') ||
|
||
normalized.includes('未找到'))
|
||
);
|
||
}
|
||
|
||
export function createDefaultChatMessages(): ChatMessage[] {
|
||
return [
|
||
{
|
||
role: 'assistant',
|
||
text: '想做什么游戏?',
|
||
},
|
||
];
|
||
}
|
||
|
||
export function isRuntimeConfigMissingError(message: string) {
|
||
return (
|
||
message.includes('LLM 未配置') ||
|
||
message.includes('LLM base_url 未配置') ||
|
||
message.includes('LLM model 未配置') ||
|
||
message.includes('editorApi.apiKey')
|
||
);
|
||
}
|
||
|
||
export function projectNameFromPath(projectPath: string) {
|
||
return (
|
||
projectPath
|
||
.split(/[\\/]/)
|
||
.map((part) => part.trim())
|
||
.filter(Boolean)
|
||
.pop() || '未命名游戏原型'
|
||
);
|
||
}
|
||
|
||
export function projectWorkspaceStatusForDisplay(workspaceStatus: string) {
|
||
const openedPrefix = '已打开:';
|
||
if (!workspaceStatus.startsWith(openedPrefix)) {
|
||
return workspaceStatus;
|
||
}
|
||
const projectPath = workspaceStatus.slice(openedPrefix.length).trim();
|
||
if (!/[\\/]/u.test(projectPath)) {
|
||
return workspaceStatus;
|
||
}
|
||
return `${openedPrefix}${projectNameFromPath(projectPath)}`;
|
||
}
|
||
|
||
export function mergeProjectSupervisorConversation(
|
||
projectRecords: LocalConversationMessageRecord[],
|
||
supervisorRecords: LocalConversationMessageRecord[],
|
||
): ChatMessage[] {
|
||
const supervisorRecordIndexByCorrelation = new Map<string, number>();
|
||
supervisorRecords.forEach((record, index) => {
|
||
if (record.role !== 'user') {
|
||
return;
|
||
}
|
||
const correlationId = agentRuntimeMessageCorrelationId(record.messageId);
|
||
if (
|
||
correlationId &&
|
||
!supervisorRecordIndexByCorrelation.has(correlationId)
|
||
) {
|
||
supervisorRecordIndexByCorrelation.set(correlationId, index);
|
||
}
|
||
});
|
||
const records = [
|
||
...projectRecords.map((record, index) => {
|
||
const runtimeOwned = Boolean(
|
||
record.messageId
|
||
?.trim()
|
||
.startsWith(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX),
|
||
);
|
||
const accepted =
|
||
runtimeOwned &&
|
||
record.content === '任务已接收,项目总控 Agent 正在启动处理。';
|
||
const correlationId = agentRuntimeMessageCorrelationId(record.messageId);
|
||
const supervisorRecordIndex = correlationId
|
||
? supervisorRecordIndexByCorrelation.get(correlationId)
|
||
: undefined;
|
||
const runtimeRecordIndex =
|
||
supervisorRecordIndex ?? supervisorRecords.length + index;
|
||
return {
|
||
record,
|
||
runtimeOwned,
|
||
sameSecondLane: runtimeOwned ? 1 : 0,
|
||
sameSecondOrder: runtimeOwned
|
||
? runtimeRecordIndex * 4 + (accepted ? 1 : 2)
|
||
: index,
|
||
stableIndex: index,
|
||
};
|
||
}),
|
||
...supervisorRecords.map((record, index) => {
|
||
const userMessage = record.role === 'user';
|
||
return {
|
||
record,
|
||
runtimeOwned: true,
|
||
sameSecondLane: 1,
|
||
// Conversation timestamps have second precision. Runtime task and
|
||
// public status IDs carry the same opaque run correlation digest, so
|
||
// status ordering follows its actual task record instead of guessing
|
||
// from independent user/accepted/terminal category counters.
|
||
sameSecondOrder: index * 4 + (userMessage ? 0 : 3),
|
||
stableIndex: projectRecords.length + index,
|
||
};
|
||
}),
|
||
]
|
||
.filter(
|
||
({ record }) => record.role === 'user' || record.role === 'assistant',
|
||
)
|
||
.sort(
|
||
(left, right) =>
|
||
left.record.updatedAt - right.record.updatedAt ||
|
||
left.sameSecondLane - right.sameSecondLane ||
|
||
left.sameSecondOrder - right.sameSecondOrder ||
|
||
left.stableIndex - right.stableIndex,
|
||
);
|
||
const seen = new Set<string>();
|
||
const messages: ChatMessage[] = [];
|
||
for (const { record, runtimeOwned } of records) {
|
||
const messageId = record.messageId?.trim();
|
||
const identity = messageId
|
||
? `message:${messageId}`
|
||
: `identity:${JSON.stringify([
|
||
record.role,
|
||
record.agentId,
|
||
record.updatedAt,
|
||
record.content,
|
||
])}`;
|
||
if (seen.has(identity)) {
|
||
continue;
|
||
}
|
||
seen.add(identity);
|
||
messages.push({
|
||
role: record.role as ChatMessage['role'],
|
||
text: record.content,
|
||
messageId: messageId ?? null,
|
||
agentId: record.agentId,
|
||
updatedAt: record.updatedAt,
|
||
runtimeOwned,
|
||
});
|
||
}
|
||
return messages.length > 0 ? messages : createDefaultChatMessages();
|
||
}
|
||
|
||
export function projectSupervisorRuntimeStatusLabel(
|
||
runtime: AgentRuntimeState | null,
|
||
runtimeError: string,
|
||
) {
|
||
if (
|
||
runtime?.status === 'needs-reconciliation' ||
|
||
runtime?.phase === 'needs-reconciliation'
|
||
) {
|
||
return '待核对';
|
||
}
|
||
if (
|
||
runtimeError ||
|
||
runtime?.status === 'failed' ||
|
||
runtime?.phase === 'failed'
|
||
) {
|
||
return '失败';
|
||
}
|
||
if (!runtime) {
|
||
return null;
|
||
}
|
||
if (
|
||
runtime.userInputRequest ||
|
||
runtime.status === 'waiting-for-user-input' ||
|
||
runtime.phase === 'waiting-for-user-input'
|
||
) {
|
||
return '需要回答';
|
||
}
|
||
if (
|
||
runtime.pendingToolAction ||
|
||
runtime.status === 'waiting-for-confirmation' ||
|
||
runtime.phase === 'waiting-for-confirmation'
|
||
) {
|
||
return '等待确认';
|
||
}
|
||
if (
|
||
runtime.phase === 'waiting-for-delegate-receipts' ||
|
||
runtime.phase === 'waiting-for-isolated-join'
|
||
) {
|
||
return '等待专业 Agent';
|
||
}
|
||
if (runtime.phase === 'waiting-for-manifest-tasks') {
|
||
return '等待项目任务';
|
||
}
|
||
if (
|
||
['action', 'observation', 'executing'].includes(runtime.phase) ||
|
||
runtime.status === 'executing'
|
||
) {
|
||
return '执行';
|
||
}
|
||
if (runtime.phase === 'response' || runtime.phase === 'finalizing') {
|
||
return '回复中';
|
||
}
|
||
if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') {
|
||
return '已取消';
|
||
}
|
||
if (runtime.status === 'completed' || runtime.phase === 'completed') {
|
||
return '已完成';
|
||
}
|
||
if (runtime.status === 'idle') {
|
||
return null;
|
||
}
|
||
return '分析';
|
||
}
|
||
|
||
export function projectSupervisorCollaboratingAgentRuntimes(
|
||
supervisorRuntime: AgentRuntimeState | null,
|
||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
|
||
) {
|
||
if (!supervisorRuntime?.runId) {
|
||
return [];
|
||
}
|
||
const runtimesByAgentId = new Map<string, AgentRuntimeState>();
|
||
for (const runtime of Object.values(runtimeByAgentId)) {
|
||
const isVisibleChildSource = [
|
||
'agent-delegate',
|
||
'agent-delegate-retry',
|
||
].includes(runtime?.source ?? '');
|
||
if (
|
||
!runtime ||
|
||
runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID ||
|
||
!isVisibleChildSource ||
|
||
runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID ||
|
||
runtime.parentRunId !== supervisorRuntime.runId
|
||
) {
|
||
continue;
|
||
}
|
||
const previous = runtimesByAgentId.get(runtime.agentId);
|
||
if (!previous || runtime.updatedAt >= previous.updatedAt) {
|
||
runtimesByAgentId.set(runtime.agentId, runtime);
|
||
}
|
||
}
|
||
return Array.from(runtimesByAgentId.values()).sort((left, right) =>
|
||
left.agentId.localeCompare(right.agentId),
|
||
);
|
||
}
|
||
|
||
export function projectProfessionalAgentLabel(agentId: string) {
|
||
if (agentId === 'project-planning') {
|
||
return '立项策划 Agent';
|
||
}
|
||
if (agentId === 'design-foundation') {
|
||
return '玩法策划 Agent';
|
||
}
|
||
if (agentId === 'art-asset-plan') {
|
||
return '美术资源计划 Agent';
|
||
}
|
||
if (agentId === 'code-prototype') {
|
||
return '程序原型 Agent';
|
||
}
|
||
if (agentId.includes('balance')) {
|
||
return '数值 Agent';
|
||
}
|
||
if (agentId.includes('design')) {
|
||
return '设计实现 Agent';
|
||
}
|
||
if (agentId.includes('art')) {
|
||
return '美术 Agent';
|
||
}
|
||
if (agentId.includes('code')) {
|
||
return '程序 Agent';
|
||
}
|
||
if (agentId.includes('audio')) {
|
||
return '音频 Agent';
|
||
}
|
||
if (
|
||
agentId.includes('publish') ||
|
||
agentId.includes('quality') ||
|
||
agentId.includes('preview')
|
||
) {
|
||
return '发布 Agent';
|
||
}
|
||
return agentId;
|
||
}
|
||
|
||
export function isAgentFinalizationMessageId(
|
||
messageId: string | null | undefined,
|
||
) {
|
||
return /^agent-finalization-[0-9a-f]{32}$/u.test(messageId ?? '');
|
||
}
|
||
|
||
export function projectRuntimeStatusPresentation(runtime: AgentRuntimeState) {
|
||
if (runtime.phase === 'needs-reconciliation') {
|
||
return { label: '待核对', tone: 'failed' };
|
||
}
|
||
if (
|
||
runtime.userInputRequest ||
|
||
runtime.status === 'waiting-for-user-input' ||
|
||
runtime.phase === 'waiting-for-user-input'
|
||
) {
|
||
return { label: '待回答', tone: 'waiting' };
|
||
}
|
||
if (
|
||
runtime.pendingToolAction ||
|
||
runtime.status === 'waiting-for-confirmation' ||
|
||
runtime.phase === 'waiting-for-confirmation'
|
||
) {
|
||
return { label: '待确认', tone: 'waiting' };
|
||
}
|
||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||
return { label: '失败', tone: 'failed' };
|
||
}
|
||
if (runtime.status === 'completed' || runtime.phase === 'completed') {
|
||
return { label: '已完成', tone: 'completed' };
|
||
}
|
||
if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') {
|
||
return { label: '已取消', tone: 'idle' };
|
||
}
|
||
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
||
return { label: '等待中', tone: 'idle' };
|
||
}
|
||
if (runtime.phase === 'planning') {
|
||
return { label: '分析中', tone: 'running' };
|
||
}
|
||
if (
|
||
runtime.phase === 'waiting-for-delegate-receipts' ||
|
||
runtime.phase === 'waiting-for-isolated-join'
|
||
) {
|
||
return { label: '协作中', tone: 'running' };
|
||
}
|
||
if (runtime.phase === 'waiting-for-manifest-tasks') {
|
||
return {
|
||
label: '项目任务中',
|
||
tone: 'running',
|
||
};
|
||
}
|
||
return { label: '执行中', tone: 'running' };
|
||
}
|
||
|
||
export function projectRuntimePlanProgress(runtime: AgentRuntimeState) {
|
||
const steps = (runtime.planSteps ?? []).filter(
|
||
(step) => agentRuntimePlanStepText(step).length > 0,
|
||
);
|
||
return {
|
||
completed: steps.filter((step) => step.status === 'completed').length,
|
||
total: steps.length,
|
||
active: agentRuntimeActivePlanStep(runtime),
|
||
};
|
||
}
|
||
|
||
export function projectRuntimeVisibleCurrentWork(runtime: AgentRuntimeState) {
|
||
const providerRetryStatus = agentRuntimeProviderRetryStatus(runtime);
|
||
if (providerRetryStatus) {
|
||
return providerRetryStatus;
|
||
}
|
||
const activePlanStep = agentRuntimeActivePlanStep(runtime);
|
||
if (activePlanStep) {
|
||
const stepText = agentRuntimePlanStepText(activePlanStep);
|
||
if (stepText) {
|
||
return stepText;
|
||
}
|
||
}
|
||
if (
|
||
runtime.pendingToolAction ||
|
||
runtime.status === 'waiting-for-confirmation' ||
|
||
runtime.phase === 'waiting-for-confirmation'
|
||
) {
|
||
return '等待确认后继续工作';
|
||
}
|
||
if (
|
||
runtime.userInputRequest ||
|
||
runtime.status === 'waiting-for-user-input' ||
|
||
runtime.phase === 'waiting-for-user-input'
|
||
) {
|
||
return '等待回答后继续工作';
|
||
}
|
||
if (runtime.phase === 'planning') {
|
||
return '正在分析需求并制定计划';
|
||
}
|
||
if (
|
||
runtime.phase === 'waiting-for-delegate-receipts' ||
|
||
runtime.phase === 'waiting-for-isolated-join'
|
||
) {
|
||
return '正在等待专业 Agent 回执';
|
||
}
|
||
if (runtime.phase === 'waiting-for-manifest-tasks') {
|
||
return '正在等待项目专业任务完成';
|
||
}
|
||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||
return '本轮工作执行失败';
|
||
}
|
||
if (runtime.status === 'completed' || runtime.phase === 'completed') {
|
||
return '本轮工作已完成';
|
||
}
|
||
const currentTask = runtime.currentTask?.trim();
|
||
if (currentTask) {
|
||
return currentTask;
|
||
}
|
||
return '正在执行当前任务';
|
||
}
|
||
|
||
export const MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE =
|
||
'泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。';
|
||
|
||
export function isMudPointInsufficientRuntimeError(message: string) {
|
||
const normalized = message.trim().toLowerCase();
|
||
return (
|
||
message.includes('泥点余额不足') ||
|
||
message.includes('可消费泥点不足:') ||
|
||
normalized.includes('kind=mud-points-insufficient')
|
||
);
|
||
}
|
||
|
||
const DIRECT_FAILURE_SENSITIVE_ASSIGNMENT_PATTERN =
|
||
/(?:^|[^\w])(?:proxy-authorization|authorization|set-cookie|cookie|access[_ -]?token|accesstoken|refresh[_ -]?token|refreshtoken|oauth[_ -]?token|id[_ -]?token|auth[_ -]?token|authtoken|client[_ -]?secret|clientsecret|private[_ -]?key|privatekey|secret[_ -]?key|secretkey|x-api-key|x_api_key|api[_ -]?key|apikey|credentials?|password|token|secret|bearer)\s*["']?\s*[:=]>?\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|(?:Bearer\s+(?:\[[^\]]+\]|<[^>]+>|[^\s,;,;&}\]]+)|\[[^\]]+\]|<[^>]+>|[^\s,;,;&}\]]+))/gi;
|
||
|
||
const DIRECT_FAILURE_BEARER_PATTERN =
|
||
/(?:^|[^\w])Bearer\s+(\[[^\]]+\]|<[^>]+>|[^\s,;,;&}\]]+)/gi;
|
||
|
||
function isRedactedDirectFailureValue(value: string) {
|
||
const normalized = value.trim();
|
||
return (
|
||
normalized.length === 0 ||
|
||
/^(?:Bearer\s+)?(?:\[redacted-secret\]|\[redacted-sensitive-field\]|\[已隐藏凭据\]|\[已隐藏敏感字段\]|\[已隐藏链接\]|\[已隐藏路径\]|\[已隐藏配置\]|\[已隐藏敏感信息\]|<redacted-secret>|<redacted-url>|<absolute-path>|\[redacted-config\]|\[redacted sensitive context\])$/i.test(
|
||
normalized,
|
||
)
|
||
);
|
||
}
|
||
|
||
function containsUnredactedDirectFailureSecret(value: string) {
|
||
// Provider payloads are sometimes embedded as escaped JSON in a single
|
||
// diagnostic line (`{\"token\":\"...\"}`). Normalize only the quote
|
||
// escapes for the detector; the displayed value still goes through the
|
||
// backend's redaction and marker conversion unchanged.
|
||
const normalizedValue = value.replace(/\\(["'])/g, '$1');
|
||
DIRECT_FAILURE_SENSITIVE_ASSIGNMENT_PATTERN.lastIndex = 0;
|
||
let match: RegExpExecArray | null;
|
||
while (
|
||
(match =
|
||
DIRECT_FAILURE_SENSITIVE_ASSIGNMENT_PATTERN.exec(normalizedValue)) !==
|
||
null
|
||
) {
|
||
const rawCandidate = (match[1] ?? '').trim();
|
||
const candidate =
|
||
rawCandidate.length >= 2 &&
|
||
((rawCandidate.startsWith('"') && rawCandidate.endsWith('"')) ||
|
||
(rawCandidate.startsWith("'") && rawCandidate.endsWith("'")))
|
||
? rawCandidate.slice(1, -1).trim()
|
||
: rawCandidate;
|
||
if (!isRedactedDirectFailureValue(candidate)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
DIRECT_FAILURE_BEARER_PATTERN.lastIndex = 0;
|
||
while (
|
||
(match = DIRECT_FAILURE_BEARER_PATTERN.exec(normalizedValue)) !== null
|
||
) {
|
||
if (!isRedactedDirectFailureValue(match[1] ?? '')) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function redactDirectFailureMarkers(value: string) {
|
||
return value
|
||
.replace(/<redacted-url>/gi, '[已隐藏链接]')
|
||
.replace(/\$PROJECT_ROOT|<absolute-path>/g, '[已隐藏路径]')
|
||
.replace(/\[redacted-secret\]/gi, '[已隐藏凭据]')
|
||
.replace(/\[redacted-sensitive-field\]/gi, '[已隐藏敏感字段]')
|
||
.replace(/\[redacted-config\]/gi, '[已隐藏配置]')
|
||
.replace(/\[redacted sensitive context\]/gi, '[已隐藏敏感信息]');
|
||
}
|
||
|
||
function directCodexDiagnosticFailureDetail(message: string) {
|
||
const trimmed = message.trim();
|
||
const match =
|
||
/^direct-codex-failure:v1 stage=(request|art-preparation|code-generation|browser-validation|version-registration) retryable=(true|false) summary=(.+?);建议:(.+?);(?:已保存脱敏项目诊断|未能保存项目诊断)$/u.exec(
|
||
trimmed,
|
||
);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
const [, stage, retryable, rawSummary, rawHint] = match;
|
||
if (!stage || !retryable || !rawSummary || !rawHint) {
|
||
return null;
|
||
}
|
||
const summary = redactDirectFailureMarkers(rawSummary)
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 320);
|
||
const hint = redactDirectFailureMarkers(rawHint)
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 180);
|
||
const combined = `${summary} ${hint}`;
|
||
if (
|
||
containsUnredactedDirectFailureSecret(combined) ||
|
||
/https?:\/\/|(?:^|[\s::])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\/i.test(
|
||
combined,
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
const stageLabel = {
|
||
request: '智能创作请求失败',
|
||
'art-preparation': '平台资源准备失败',
|
||
'code-generation': '代码生成失败',
|
||
'browser-validation': '真实试玩未通过',
|
||
'version-registration': '项目版本登记失败',
|
||
}[stage];
|
||
if (!stageLabel) {
|
||
return null;
|
||
}
|
||
if (!summary || !hint) {
|
||
return null;
|
||
}
|
||
return `${stageLabel}:${summary}。${hint}${
|
||
retryable === 'true' ? '(可直接重试)' : ''
|
||
}`;
|
||
}
|
||
|
||
function directPlatformFailureDetail(message: string) {
|
||
const trimmed = message.trim();
|
||
if (!trimmed) {
|
||
return null;
|
||
}
|
||
const lower = trimmed.toLowerCase();
|
||
if (
|
||
!(
|
||
lower.includes('陶泥儿美术包生成失败') ||
|
||
lower.includes('平台图片生成任务失败') ||
|
||
lower.includes('external editor') ||
|
||
lower.includes('透明美术图集') ||
|
||
lower.includes('图集切片')
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
if (
|
||
containsUnredactedDirectFailureSecret(trimmed) ||
|
||
/https?:\/\/|(?:^|[\\s::])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\/i.test(
|
||
trimmed,
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
const safe = redactDirectFailureMarkers(trimmed)
|
||
.replace(/(?:;|;|\s)operationId\s*=\s*[^;;\s]+/gi, '')
|
||
.replace(/(?:;|;)\s*externalGenerationJobId\s*=\s*[^;;\s]+/gi, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 280);
|
||
return safe || null;
|
||
}
|
||
|
||
function directCodexFailureDetail(message: string) {
|
||
const trimmed = message.trim();
|
||
const marker = 'direct-codex-error:';
|
||
const markerIndex = trimmed.indexOf(marker);
|
||
if (markerIndex < 0) {
|
||
return null;
|
||
}
|
||
const detail = trimmed.slice(markerIndex + marker.length).trim();
|
||
if (!detail) {
|
||
return null;
|
||
}
|
||
if (
|
||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie)/i.test(
|
||
detail,
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
const safe = detail
|
||
.replace(/https?:\/\/[^\s]+/gi, '[已隐藏链接]')
|
||
.replace(
|
||
/(?:[A-Z]:\\|\\\\|\/(?:Users|home|var|tmp|private)\/)[^\s]+/gi,
|
||
'[已隐藏路径]',
|
||
)
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 280);
|
||
return safe || null;
|
||
}
|
||
|
||
function directRuntimeFailureDetail(message: string) {
|
||
const trimmed = message.trim();
|
||
if (
|
||
!(
|
||
trimmed.startsWith('Codex 已返回,但客户端登记生成产物失败:') ||
|
||
trimmed.includes('陶泥儿美术包不完整,已终止代码生成') ||
|
||
trimmed.includes('陶泥儿规范图生成后未形成可用平台合同') ||
|
||
trimmed.includes('陶泥儿美术包生成返回后未形成可用的已登记平台素材合同')
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
if (
|
||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie|https?:\/\/|(?:^|[\\s::])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\)/i.test(
|
||
trimmed,
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
const safe = trimmed.replace(/\s+/g, ' ').trim().slice(0, 320);
|
||
return safe || null;
|
||
}
|
||
|
||
export function projectRuntimeVisibleError(
|
||
message: string,
|
||
subject: string,
|
||
preservePublicMessage = false,
|
||
) {
|
||
const visibleMessage = message.trim();
|
||
const normalized = visibleMessage.toLowerCase();
|
||
if (isRuntimeConfigMissingError(message)) {
|
||
return '运行时配置未完成,请先打开配置';
|
||
}
|
||
if (isMudPointInsufficientRuntimeError(message)) {
|
||
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
|
||
}
|
||
const directDiagnosticDetail = directCodexDiagnosticFailureDetail(message);
|
||
if (directDiagnosticDetail) {
|
||
return `${subject}:${directDiagnosticDetail}`;
|
||
}
|
||
const directFailureDetail = directPlatformFailureDetail(message);
|
||
if (directFailureDetail) {
|
||
return `${subject}:${directFailureDetail}`;
|
||
}
|
||
const codexAppServerKind = visibleMessage.match(
|
||
/(?:^|[\s::])kind=codex-app-server-([a-z-]+)(?=\s|$)/,
|
||
)?.[1];
|
||
if (codexAppServerKind) {
|
||
const detail = {
|
||
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
|
||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
|
||
unauthorized: '智能服务鉴权失败,请重新登录后重试',
|
||
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||
'bad-request': '智能创作请求无效,请稍后重试',
|
||
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
|
||
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
|
||
'thread-rollback-failed': '智能创作会话恢复失败,请新建任务后重试',
|
||
'active-turn-not-steerable':
|
||
'当前智能创作任务无法追加指令,请等待结束后重试',
|
||
other: '智能创作执行失败,请查看运行详情后重试',
|
||
}[codexAppServerKind];
|
||
if (detail) {
|
||
return `${subject} ${detail}`;
|
||
}
|
||
}
|
||
const directCodexAppServerKind = visibleMessage.match(
|
||
/codex-app-server-error:([a-z-]+)/,
|
||
)?.[1];
|
||
if (directCodexAppServerKind) {
|
||
const detail = {
|
||
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
|
||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
|
||
unauthorized: '鉴权失败,请重新登录后重试',
|
||
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||
'bad-request': '请求无效,请稍后重试',
|
||
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
|
||
'sandbox-error': '工作区隔离启动失败,请检查项目目录后重试',
|
||
other: '未完成本次执行,请查看项目文件是否已修改后再重试',
|
||
}[directCodexAppServerKind];
|
||
if (detail) {
|
||
return `${subject} ${detail}`;
|
||
}
|
||
}
|
||
if (visibleMessage.includes('codex-app-server-terminal-unknown:')) {
|
||
return `${subject}服务或网络在执行中断开,最终状态未知;请先检查项目文件是否已修改,再决定是否重试`;
|
||
}
|
||
const directCodexDetail = directCodexFailureDetail(visibleMessage);
|
||
if (directCodexDetail) {
|
||
return `${subject}:智能服务执行失败:${directCodexDetail}`;
|
||
}
|
||
const directRuntimeDetail = directRuntimeFailureDetail(visibleMessage);
|
||
if (directRuntimeDetail) {
|
||
return `${subject}:${directRuntimeDetail}`;
|
||
}
|
||
const exhaustedUpstreamRetry = visibleMessage.match(
|
||
/(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
|
||
);
|
||
if (exhaustedUpstreamRetry) {
|
||
const [, kindStatusText, httpStatusText, retryAttemptText, maxRetriesText] =
|
||
exhaustedUpstreamRetry;
|
||
if (
|
||
!kindStatusText ||
|
||
!httpStatusText ||
|
||
!retryAttemptText ||
|
||
!maxRetriesText
|
||
) {
|
||
return `${subject} 执行失败,请稍后重试`;
|
||
}
|
||
const httpStatus = Number.parseInt(httpStatusText, 10);
|
||
const retryAttempt = Number.parseInt(retryAttemptText, 10);
|
||
const maxRetries = Number.parseInt(maxRetriesText, 10);
|
||
if (
|
||
kindStatusText === httpStatusText &&
|
||
httpStatus >= 500 &&
|
||
httpStatus <= 599 &&
|
||
retryAttempt === maxRetries &&
|
||
maxRetries >= 0 &&
|
||
maxRetries <= 4_294_967_295
|
||
) {
|
||
return `${subject} 上游服务返回 HTTP ${httpStatus};自动重试已耗尽(${retryAttempt}/${maxRetries})`;
|
||
}
|
||
}
|
||
if (
|
||
normalized.includes('kind=transport') ||
|
||
normalized.includes('transport') ||
|
||
normalized.includes('connect') ||
|
||
normalized.includes('network') ||
|
||
normalized.includes('tls')
|
||
) {
|
||
return `${subject} 服务连接失败,请稍后重试`;
|
||
}
|
||
if (normalized.includes('timeout') || normalized.includes('超时')) {
|
||
return `${subject} 响应超时,请稍后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('unauthorized') ||
|
||
normalized.includes('api key') ||
|
||
normalized.includes('401') ||
|
||
normalized.includes('鉴权')
|
||
) {
|
||
return `${subject} 鉴权失败,请检查运行时配置`;
|
||
}
|
||
if (
|
||
normalized.includes('rate limit') ||
|
||
normalized.includes('429') ||
|
||
normalized.includes('quota') ||
|
||
normalized.includes('额度') ||
|
||
normalized.includes('限流')
|
||
) {
|
||
return `${subject} 服务繁忙,请稍后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('needs-reconciliation') ||
|
||
normalized.includes('result-unknown') ||
|
||
normalized.includes('终态未知') ||
|
||
normalized.includes('需要人工核对')
|
||
) {
|
||
return `${subject} 运行状态需要核对,请打开运行详情后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('budget-exhausted') ||
|
||
normalized.includes('预算耗尽') ||
|
||
normalized.includes('预算已耗尽')
|
||
) {
|
||
return `${subject} 本轮预算已耗尽,请缩小任务范围后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('missing expected artifact') ||
|
||
normalized.includes('expected artifact') ||
|
||
normalized.includes('缺少预期产物') ||
|
||
normalized.includes('缺少 expected artifact')
|
||
) {
|
||
return `${subject} 未生成要求的产物,请查看任务要求后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('verification') ||
|
||
normalized.includes('project.verify') ||
|
||
normalized.includes('preview.validate') ||
|
||
normalized.includes('验证未通过') ||
|
||
normalized.includes('验证失败')
|
||
) {
|
||
return `${subject} 项目验证未通过,请查看运行详情并修复后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('policy') ||
|
||
normalized.includes('permission') ||
|
||
normalized.includes('拒绝') ||
|
||
normalized.includes('禁止') ||
|
||
normalized.includes('不允许')
|
||
) {
|
||
return `${subject} 被项目权限或安全策略阻止,请检查审批配置`;
|
||
}
|
||
if (
|
||
normalized.includes('落盘失败') ||
|
||
normalized.includes('持久化失败') ||
|
||
normalized.includes('写入失败')
|
||
) {
|
||
return `${subject} 保存运行记录失败,请检查项目目录后重试`;
|
||
}
|
||
if (
|
||
normalized.includes('planning_invalid') ||
|
||
normalized.includes('gdd 结构无效') ||
|
||
normalized.includes('策划输出格式')
|
||
) {
|
||
return `${subject} 输出格式不符合当前 GDD 结构,请重试`;
|
||
}
|
||
const containsInternalDiagnostics =
|
||
normalized.includes('agentllm.') ||
|
||
/(?:^|[\s::])kind=/.test(normalized) ||
|
||
/(?:^|\s)(?:fingerprint|chars|contentchars|contentbytes|sha256)=/.test(
|
||
normalized,
|
||
) ||
|
||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie)/i.test(
|
||
visibleMessage,
|
||
) ||
|
||
/(?:https?:\/\/|(?:^|\s)\/(?:home|Users|var|tmp)\/)/.test(visibleMessage);
|
||
const isKnownPublicMessage =
|
||
visibleMessage === 'LLM 服务暂时不可用,请检查配置后重试' ||
|
||
visibleMessage === '请先回答项目总控 Agent 当前的澄清问题' ||
|
||
visibleMessage === '待回答请求已变更,请刷新 Runtime 状态' ||
|
||
/^回答提交失败:[^\r\n]{1,80}$/.test(visibleMessage);
|
||
if (
|
||
preservePublicMessage &&
|
||
isKnownPublicMessage &&
|
||
!containsInternalDiagnostics
|
||
) {
|
||
return visibleMessage;
|
||
}
|
||
return `${subject} 执行失败,请稍后重试`;
|
||
}
|
||
|
||
export function projectSupervisorVisibleConversationText(
|
||
message: string,
|
||
role: ChatMessage['role'] | 'tool' = 'assistant',
|
||
subject = '项目总控 Agent',
|
||
) {
|
||
const failurePrefix = '后台任务失败:';
|
||
if (role !== 'assistant' || !message.startsWith(failurePrefix)) {
|
||
return message;
|
||
}
|
||
return projectRuntimeVisibleError(
|
||
message.slice(failurePrefix.length),
|
||
subject,
|
||
true,
|
||
);
|
||
}
|
||
|
||
export function projectSupervisorChatMessageText(
|
||
message: Pick<ChatMessage, 'text' | 'role'>,
|
||
) {
|
||
return message.role === 'assistant'
|
||
? projectSupervisorVisibleConversationText(message.text, message.role)
|
||
: message.text;
|
||
}
|
||
|
||
export function projectRuntimeVisibleToolSummary(summary: string) {
|
||
return summary
|
||
.split('·')
|
||
.map((part) => part.trim())
|
||
.filter(
|
||
(part) =>
|
||
part.length > 0 &&
|
||
!/^(contentChars|contentBytes|sha256|fingerprint)=/i.test(part),
|
||
)
|
||
.join(' · ');
|
||
}
|
||
|
||
export function projectSupervisorPendingActionPresentation(
|
||
action: AgentRuntimePendingToolActionSummary,
|
||
) {
|
||
if (action.tool === 'agent.delegate') {
|
||
const agentId =
|
||
action.inputSummary?.match(/(?:^|·)\s*agentId=([^·]+)/)?.[1]?.trim() ??
|
||
'';
|
||
const agentLabel = agentId
|
||
? projectProfessionalAgentLabel(agentId)
|
||
: '专业 Agent';
|
||
const isRepair =
|
||
action.inputSummary?.includes('repairOf=') &&
|
||
!action.inputSummary.includes('repairOf=null');
|
||
return {
|
||
title: isRepair ? `安排${agentLabel}返工` : `安排${agentLabel}执行任务`,
|
||
detail: isRepair
|
||
? '确认后将在当前项目创建一轮带原验收合同的返工任务'
|
||
: '确认后将在当前项目启动新的专业任务',
|
||
};
|
||
}
|
||
return {
|
||
title: action.tool,
|
||
detail: action.inputSummary
|
||
? projectRuntimeVisibleToolSummary(action.inputSummary)
|
||
: '确认后继续当前项目任务',
|
||
};
|
||
}
|
||
|
||
export function projectSupervisorPendingRepairMatchesProfessional(
|
||
action: AgentRuntimePendingToolActionSummary | null | undefined,
|
||
runtime: AgentRuntimeState,
|
||
) {
|
||
const summary = action?.inputSummary ?? '';
|
||
return Boolean(
|
||
action?.tool === 'agent.delegate' &&
|
||
runtime.delegationId &&
|
||
summary.includes(`agentId=${runtime.agentId}`) &&
|
||
summary.includes(`repairOf=${runtime.delegationId}`),
|
||
);
|
||
}
|
||
|
||
export function formatProjectRuntimeUpdatedAt(updatedAt: number) {
|
||
if (!Number.isFinite(updatedAt) || updatedAt <= 0) {
|
||
return '更新时间未知';
|
||
}
|
||
const milliseconds =
|
||
updatedAt < 1_000_000_000_000 ? updatedAt * 1000 : updatedAt;
|
||
return `${new Date(milliseconds).toLocaleTimeString('zh-CN', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
})} 更新`;
|
||
}
|
||
|
||
export function formatProjectSupervisorCompactProgress(
|
||
runtime: AgentRuntimeState,
|
||
collaboratingAgentCount: number,
|
||
) {
|
||
const planSteps = (runtime.planSteps ?? []).filter(
|
||
(step) => agentRuntimePlanStepText(step).length > 0,
|
||
);
|
||
const completedPlanStepCount = planSteps.filter(
|
||
(step) => step.status === 'completed',
|
||
).length;
|
||
const activePlanStep = agentRuntimeActivePlanStep(runtime);
|
||
const currentPlanStep = activePlanStep
|
||
? agentRuntimePlanStepText(activePlanStep)
|
||
: planSteps.length > 0 && completedPlanStepCount === planSteps.length
|
||
? '已完成'
|
||
: '暂无';
|
||
const waitingOn =
|
||
runtime.waitingOn?.trim() || agentRuntimeWaitingOnFromPhase(runtime.phase);
|
||
const nextStep =
|
||
runtime.nextStep?.trim() || agentRuntimeNextStepFromPhase(runtime.phase);
|
||
return [
|
||
`计划完成:${completedPlanStepCount}/${planSteps.length}`,
|
||
`当前步骤:${currentPlanStep}`,
|
||
`等待:${waitingOn}`,
|
||
`下一步:${nextStep}`,
|
||
`专业 Agent 协作:${collaboratingAgentCount}`,
|
||
].join(' · ');
|
||
}
|
||
|
||
export function taskRowsFromManifest(
|
||
manifest: GameCreationAppManifest,
|
||
): GameCreationAppTaskState[] {
|
||
return manifest.tasks.length > 0 ? manifest.tasks : seedManifest.tasks;
|
||
}
|
||
|
||
export function agentConversationId(task: GameCreationAppTaskState) {
|
||
return task.id;
|
||
}
|
||
|
||
export function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) {
|
||
const delegationSource = formatAgentRuntimeDelegationSource(task);
|
||
const goalSource = task.goalId
|
||
? `Goal ${task.goalStatus ?? '-'} · revision ${task.goalRevision ?? 0}`
|
||
: null;
|
||
const failed =
|
||
task.status === 'failed' ||
|
||
['failed', 'budget-exhausted', 'needs-reconciliation'].includes(task.phase);
|
||
const failureDetail = failed
|
||
? task.terminalDetail?.trim() || task.error?.trim() || null
|
||
: null;
|
||
const failureSummary = failureDetail
|
||
? projectRuntimeVisibleError(
|
||
failureDetail,
|
||
projectProfessionalAgentLabel(task.agentId),
|
||
true,
|
||
)
|
||
: null;
|
||
return `${task.status} / ${task.phase} · ${
|
||
task.task || task.currentAction || task.runId
|
||
}${goalSource ? ` · ${goalSource}` : ''}${
|
||
delegationSource ? ` · ${delegationSource}` : ''
|
||
}${failureSummary ? ` · 失败原因:${failureSummary}` : ''}`;
|
||
}
|