c2ef8631f8
统一余额不足分类与稳定原因码 阻止泥点不足进入重试和对账状态 在游戏聊天与阶段记录展示固定安全说明 补齐 API、Runtime 与前端回归测试 同步技术方案与项目决策记录
1737 lines
51 KiB
TypeScript
1737 lines
51 KiB
TypeScript
import type {
|
||
GameCreationAppManifest,
|
||
GameCreationAppTaskState,
|
||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||
import { PROJECT_SUPERVISOR_AGENT_ID, seedManifest } from '../../app/constants';
|
||
import type {
|
||
AgentConversationSessionListResult,
|
||
AgentConversationSessionRecord,
|
||
AgentGoalRecord,
|
||
AgentRuntimeEventRecord,
|
||
AgentRuntimePendingToolActionSummary,
|
||
AgentRuntimePlanStep,
|
||
AgentRuntimeResponseStream,
|
||
AgentRuntimeResult,
|
||
AgentRuntimeState,
|
||
AgentRuntimeSteerResult,
|
||
AgentRuntimeTaskQueueSummary,
|
||
AgentRuntimeTaskRecord,
|
||
ChatMessage,
|
||
LocalConversationMessageRecord,
|
||
TauriInvoke,
|
||
} from '../../app/types';
|
||
|
||
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,
|
||
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 {
|
||
return normalizeAgentRuntimeState(
|
||
{
|
||
...result.state,
|
||
taskQueue: result.taskQueue ?? result.state.taskQueue,
|
||
recentEvents: result.recentEvents ?? result.state.recentEvents,
|
||
recentTasks: result.recentTasks ?? result.state.recentTasks,
|
||
userInputRequest:
|
||
result.userInputRequest !== undefined
|
||
? result.userInputRequest
|
||
: result.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 game-chat.
|
||
*/
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Keep final responses that were committed into the game-chat window while
|
||
* conversation history is being hydrated. Runtime responses have a durable
|
||
* local id; matching by text or timestamp would collapse two different runs
|
||
* that happen to produce the same wording.
|
||
*/
|
||
export function mergeGameChatRuntimeResponseMessagesIntoHistory(
|
||
historyMessages: ChatMessage[],
|
||
currentMessages: ChatMessage[],
|
||
) {
|
||
const historyMessageIds = new Set(
|
||
historyMessages
|
||
.map((message) => message.messageId?.trim())
|
||
.filter((messageId): messageId is string => Boolean(messageId)),
|
||
);
|
||
const addedMessageIds = new Set<string>();
|
||
const responsesToKeep = currentMessages.filter((message) => {
|
||
const messageId = message.messageId?.trim();
|
||
if (
|
||
!message.runtimeOwned ||
|
||
message.role !== 'assistant' ||
|
||
!messageId?.startsWith('runtime-response:') ||
|
||
historyMessageIds.has(messageId) ||
|
||
addedMessageIds.has(messageId)
|
||
) {
|
||
return false;
|
||
}
|
||
addedMessageIds.add(messageId);
|
||
return true;
|
||
});
|
||
if (responsesToKeep.length === 0) {
|
||
return historyMessages;
|
||
}
|
||
return [...historyMessages, ...responsesToKeep].sort(
|
||
(left, right) => (left.updatedAt ?? 0) - (right.updatedAt ?? 0),
|
||
);
|
||
}
|
||
|
||
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'].includes(runtime.phase) ||
|
||
['completed', 'failed', 'cancelled', 'idle'].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 function matchingAgentRuntimeForSteer(
|
||
runtimes: Array<AgentRuntimeState | null | undefined>,
|
||
agentId: string,
|
||
sessionId: string | null,
|
||
requestedRunProfile: NonNullable<
|
||
AgentRuntimeState['runProfile']
|
||
> = 'standard',
|
||
) {
|
||
if (!sessionId) {
|
||
return null;
|
||
}
|
||
return (
|
||
runtimes.find(
|
||
(runtime) =>
|
||
runtime?.agentId === agentId &&
|
||
runtime.sessionId === sessionId &&
|
||
(runtime.runProfile ?? 'standard') === requestedRunProfile &&
|
||
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?: string;
|
||
}) {
|
||
const steerRuntime = matchingAgentRuntimeForSteer(
|
||
[runtime],
|
||
PROJECT_SUPERVISOR_AGENT_ID,
|
||
sessionId,
|
||
runProfile,
|
||
);
|
||
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 ? { 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 ? { source } : {}),
|
||
},
|
||
);
|
||
return {
|
||
mode: 'start' as const,
|
||
runtimeResult,
|
||
acceptedRunId: agentRuntimeStartedRunId(runtimeResult, requestedRunId),
|
||
};
|
||
}
|
||
|
||
export function agentRuntimeSteerStatus(result: AgentRuntimeSteerResult) {
|
||
const runId = result.runtime.state.runId;
|
||
if (result.providerInterrupted) {
|
||
return `已中断 Provider,正在同一 Run 重新规划:${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};${safeWaitingOn}`;
|
||
}
|
||
if (safeCurrentAction) {
|
||
return safeCurrentAction;
|
||
}
|
||
|
||
const legacyAttempt = currentAction?.match(
|
||
/^(?:等待 Provider 瞬态重试|Goal 已恢复,继续等待 Provider 瞬态重试) (\d+)\/(\d+)$/,
|
||
);
|
||
const retryProgress =
|
||
legacyAttempt?.[1] && legacyAttempt[2]
|
||
? `,准备自动重试 ${legacyAttempt[1]}/${legacyAttempt[2]}`
|
||
: ',正在准备自动重试';
|
||
const safeFallback = `Provider 上游服务暂时不可用${retryProgress}`;
|
||
return safeWaitingOn ? `${safeFallback};${safeWaitingOn}` : safeFallback;
|
||
}
|
||
|
||
export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
||
if (isAgentRuntimeTerminalState(runtime)) {
|
||
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 (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 containsInternalFailureDiagnostics = Boolean(
|
||
event.detail &&
|
||
/(?:errorSha256|errorChars|fingerprint|chars|retryAttempt|retryState)=|<(?:absolute-path|redacted-url)>|\[redacted(?:[- ]secret| sensitive context)\]/i.test(
|
||
event.detail,
|
||
),
|
||
);
|
||
const visibleDetail =
|
||
isFailureEvent && containsInternalFailureDiagnostics ? null : event.detail;
|
||
const summary =
|
||
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 mergeProjectSupervisorConversation(
|
||
projectRecords: LocalConversationMessageRecord[],
|
||
supervisorRecords: LocalConversationMessageRecord[],
|
||
): ChatMessage[] {
|
||
const records = [
|
||
...projectRecords.map((record, index) => ({
|
||
record,
|
||
runtimeOwned: false,
|
||
stableIndex: index,
|
||
})),
|
||
...supervisorRecords.map((record, index) => ({
|
||
record,
|
||
runtimeOwned: true,
|
||
stableIndex: projectRecords.length + index,
|
||
})),
|
||
]
|
||
.filter(
|
||
({ record }) => record.role === 'user' || record.role === 'assistant',
|
||
)
|
||
.sort(
|
||
(left, right) =>
|
||
left.record.updatedAt - right.record.updatedAt ||
|
||
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 (
|
||
runtimeError ||
|
||
runtime?.status === 'failed' ||
|
||
runtime?.phase === 'failed' ||
|
||
runtime?.phase === 'needs-reconciliation'
|
||
) {
|
||
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)) {
|
||
if (
|
||
!runtime ||
|
||
runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID ||
|
||
!['agent-delegate', 'agent-delegate-retry'].includes(runtime.source) ||
|
||
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 === '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')
|
||
);
|
||
}
|
||
|
||
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 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} 服务繁忙,请稍后重试`;
|
||
}
|
||
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'] = 'assistant',
|
||
) {
|
||
const failurePrefix = '后台任务失败:';
|
||
if (role !== 'assistant' || !message.startsWith(failurePrefix)) {
|
||
return message;
|
||
}
|
||
return projectRuntimeVisibleError(
|
||
message.slice(failurePrefix.length),
|
||
'项目总控 Agent',
|
||
true,
|
||
);
|
||
}
|
||
|
||
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;
|
||
return `${task.status} / ${task.phase} · ${
|
||
task.task || task.currentAction || task.runId
|
||
}${goalSource ? ` · ${goalSource}` : ''}${
|
||
delegationSource ? ` · ${delegationSource}` : ''
|
||
}`;
|
||
}
|