feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,796 @@
|
||||
import type {
|
||||
ChatTimelineItem,
|
||||
ChatTimelineKnowledgeHit,
|
||||
ChatTimelineMessageItem,
|
||||
} from '@easyflow/common-ui';
|
||||
import {ChatTimelineBuilder} from '@easyflow/common-ui';
|
||||
|
||||
interface AgentTryoutRuntimeEvent {
|
||||
createdAt: number;
|
||||
domain: string;
|
||||
payload: Record<string, unknown>;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type AgentTryoutRoundStatus = 'completed' | 'error' | 'running';
|
||||
|
||||
interface AgentTryoutRawVariant {
|
||||
createdAt: number;
|
||||
runtimeEvents: AgentTryoutRuntimeEvent[];
|
||||
status: AgentTryoutRoundStatus;
|
||||
updatedAt: number;
|
||||
variantIndex: number;
|
||||
}
|
||||
|
||||
interface AgentTryoutRawRound {
|
||||
createdAt: number;
|
||||
prompt: string;
|
||||
roundId: string;
|
||||
selectedVariantIndex: number;
|
||||
status: AgentTryoutRoundStatus;
|
||||
updatedAt: number;
|
||||
variants: AgentTryoutRawVariant[];
|
||||
}
|
||||
|
||||
interface AgentTryoutRawSessionRecord {
|
||||
rounds: AgentTryoutRawRound[];
|
||||
sessionId: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const STORAGE_VERSION = 1;
|
||||
const MAX_ROUNDS = 50;
|
||||
const MAX_VARIANTS = 10;
|
||||
const STORAGE_PREFIX = 'easyflow:agent-tryout-raw-rounds';
|
||||
const PERSIST_DEBOUNCE_MS = 300;
|
||||
const memorySessions = new Map<string, AgentTryoutRawRound[]>();
|
||||
|
||||
function createRoundId() {
|
||||
return `round-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function asText(value: unknown) {
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase() === 'true';
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
function normalizeToolName(value: unknown) {
|
||||
return asText(value).trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isHiddenToolName(value: unknown) {
|
||||
const normalizedName = normalizeToolName(value);
|
||||
return normalizedName === 'retrieve_knowledge' || normalizedName === '__fragment__';
|
||||
}
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function storageKey(mode: string, sessionId: string) {
|
||||
return `${STORAGE_PREFIX}:${mode}:${sessionId}`;
|
||||
}
|
||||
|
||||
function safeSessionStorage() {
|
||||
try {
|
||||
return globalThis.sessionStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createVariant(variantIndex: number): AgentTryoutRawVariant {
|
||||
const now = Date.now();
|
||||
return {
|
||||
createdAt: now,
|
||||
runtimeEvents: [],
|
||||
status: 'running',
|
||||
updatedAt: now,
|
||||
variantIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRuntimeEvent(value: any): AgentTryoutRuntimeEvent | undefined {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const domain = asText(value.domain).toUpperCase();
|
||||
const type = asText(value.type).toUpperCase();
|
||||
if (!domain || !type) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
createdAt: Number(value.createdAt || Date.now()),
|
||||
domain,
|
||||
payload: asRecord(value.payload),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVariant(value: any, index: number) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return createVariant(index);
|
||||
}
|
||||
const runtimeEvents = Array.isArray(value.runtimeEvents)
|
||||
? value.runtimeEvents
|
||||
.map((item: any) => normalizeRuntimeEvent(item))
|
||||
.filter(
|
||||
(item: AgentTryoutRuntimeEvent | undefined): item is AgentTryoutRuntimeEvent =>
|
||||
Boolean(item),
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
createdAt: Number(value.createdAt || Date.now()),
|
||||
runtimeEvents,
|
||||
status:
|
||||
value.status === 'completed' || value.status === 'error'
|
||||
? value.status
|
||||
: 'running',
|
||||
updatedAt: Number(value.updatedAt || value.createdAt || Date.now()),
|
||||
variantIndex: index,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRound(value: any): AgentTryoutRawRound | undefined {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const prompt = asText(value.prompt);
|
||||
const roundId = asText(value.roundId);
|
||||
if (!prompt || !roundId) {
|
||||
return undefined;
|
||||
}
|
||||
const variants = Array.isArray(value.variants)
|
||||
? value.variants
|
||||
.slice(-MAX_VARIANTS)
|
||||
.map((item: any, index: number) => normalizeVariant(item, index + 1))
|
||||
: [];
|
||||
if (variants.length === 0) {
|
||||
variants.push(createVariant(1));
|
||||
}
|
||||
const selectedVariantIndex = Math.min(
|
||||
Math.max(Number(value.selectedVariantIndex || variants.length), 1),
|
||||
variants.length,
|
||||
);
|
||||
return {
|
||||
createdAt: Number(value.createdAt || Date.now()),
|
||||
prompt,
|
||||
roundId,
|
||||
selectedVariantIndex,
|
||||
status:
|
||||
value.status === 'completed' || value.status === 'error'
|
||||
? value.status
|
||||
: 'running',
|
||||
updatedAt: Number(value.updatedAt || value.createdAt || Date.now()),
|
||||
variants,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreSession(mode: string, sessionId: string) {
|
||||
const key = storageKey(mode, sessionId);
|
||||
const memoryRecords = memorySessions.get(key);
|
||||
if (memoryRecords) {
|
||||
return memoryRecords.map((item) => clone(item));
|
||||
}
|
||||
const storage = safeSessionStorage();
|
||||
if (!storage) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const raw = storage.getItem(key);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw) as AgentTryoutRawSessionRecord;
|
||||
if (parsed.sessionId !== sessionId || parsed.version !== STORAGE_VERSION) {
|
||||
return [];
|
||||
}
|
||||
const rounds = Array.isArray(parsed.rounds)
|
||||
? parsed.rounds
|
||||
.map((item) => normalizeRound(item))
|
||||
.filter((item): item is AgentTryoutRawRound => Boolean(item))
|
||||
: [];
|
||||
memorySessions.set(key, rounds.map((item) => clone(item)));
|
||||
return rounds;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistSession(
|
||||
mode: string,
|
||||
sessionId: string,
|
||||
rounds: AgentTryoutRawRound[],
|
||||
) {
|
||||
const key = storageKey(mode, sessionId);
|
||||
const snapshot: AgentTryoutRawSessionRecord = {
|
||||
rounds: rounds.slice(-MAX_ROUNDS).map((item) => clone(item)),
|
||||
sessionId,
|
||||
version: STORAGE_VERSION,
|
||||
};
|
||||
memorySessions.set(key, snapshot.rounds.map((item) => clone(item)));
|
||||
const storage = safeSessionStorage();
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
storage.setItem(key, JSON.stringify(snapshot));
|
||||
} catch {
|
||||
// 试运行缓存失败不影响当前聊天主流程。
|
||||
}
|
||||
}
|
||||
|
||||
function removeStoredSession(mode: string, sessionId: string) {
|
||||
const key = storageKey(mode, sessionId);
|
||||
memorySessions.delete(key);
|
||||
const storage = safeSessionStorage();
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
storage.removeItem(key);
|
||||
} catch {
|
||||
// 清理缓存失败不影响界面重置。
|
||||
}
|
||||
}
|
||||
|
||||
function selectedVariant(round: AgentTryoutRawRound) {
|
||||
return (
|
||||
round.variants.find(
|
||||
(variant) => variant.variantIndex === round.selectedVariantIndex,
|
||||
) || round.variants.at(-1)
|
||||
);
|
||||
}
|
||||
|
||||
function visibleText(item: ChatTimelineMessageItem) {
|
||||
return item.parts
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.content)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function isUserMessage(item: ChatTimelineItem): item is ChatTimelineMessageItem {
|
||||
return item.type === 'message' && item.role === 'user';
|
||||
}
|
||||
|
||||
function isAssistantMessage(
|
||||
item: ChatTimelineItem,
|
||||
): item is ChatTimelineMessageItem {
|
||||
return item.type === 'message' && item.role === 'assistant';
|
||||
}
|
||||
|
||||
function findRoundResponseRange(items: ChatTimelineItem[], roundId: string) {
|
||||
const userIndex = items.findIndex(
|
||||
(item) => isUserMessage(item) && item.roundId === roundId,
|
||||
);
|
||||
if (userIndex < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const nextUserIndex = items.findIndex(
|
||||
(item, index) => index > userIndex && isUserMessage(item),
|
||||
);
|
||||
return {
|
||||
end: nextUserIndex >= 0 ? nextUserIndex : items.length,
|
||||
start: userIndex + 1,
|
||||
};
|
||||
}
|
||||
|
||||
function assistantSegmentIndex(items: ChatTimelineItem[], roundId: string) {
|
||||
return items.filter(
|
||||
(item) => isAssistantMessage(item) && item.roundId === roundId,
|
||||
).length;
|
||||
}
|
||||
|
||||
function nextAssistantId(
|
||||
items: ChatTimelineItem[],
|
||||
roundId: string,
|
||||
variantIndex: number,
|
||||
) {
|
||||
const last = items[items.length - 1];
|
||||
if (
|
||||
last &&
|
||||
isAssistantMessage(last) &&
|
||||
last.roundId === roundId &&
|
||||
last.status !== 'done'
|
||||
) {
|
||||
return last.id;
|
||||
}
|
||||
return `assistant-${roundId}-${variantIndex}-${assistantSegmentIndex(items, roundId) + 1}`;
|
||||
}
|
||||
|
||||
function normalizeAssistantPartIds(
|
||||
items: ChatTimelineItem[],
|
||||
roundId: string,
|
||||
variantIndex: number,
|
||||
) {
|
||||
const segment = assistantSegmentIndex(items, roundId);
|
||||
const latest = [...items]
|
||||
.reverse()
|
||||
.find((item): item is ChatTimelineMessageItem => isAssistantMessage(item) && item.roundId === roundId);
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
latest.id = `assistant-${roundId}-${variantIndex}-${segment}`;
|
||||
latest.parts.forEach((part, index) => {
|
||||
part.id = `${part.type}-${roundId}-${variantIndex}-${segment}-${index + 1}`;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLatestItemId(
|
||||
items: ChatTimelineItem[],
|
||||
prefix: string,
|
||||
roundId: string,
|
||||
variantIndex: number,
|
||||
) {
|
||||
const last = items[items.length - 1];
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
last.id = `${prefix}-${roundId}-${variantIndex}`;
|
||||
}
|
||||
|
||||
function markRoundCompleted(
|
||||
items: ChatTimelineItem[],
|
||||
roundId: string,
|
||||
variant: AgentTryoutRawVariant,
|
||||
variantCount: number,
|
||||
selectedVariantIndex: number,
|
||||
) {
|
||||
const range = findRoundResponseRange(items, roundId);
|
||||
const source = range ? items.slice(range.start, range.end) : items;
|
||||
const latest = [...source]
|
||||
.reverse()
|
||||
.find((item): item is ChatTimelineMessageItem => isAssistantMessage(item));
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
latest.roundCompleted = true;
|
||||
latest.status = latest.status === 'error' ? 'error' : 'done';
|
||||
latest.regenerable = true;
|
||||
latest.switchable = variantCount > 1;
|
||||
latest.variantCount = variantCount;
|
||||
latest.variantIndex = variant.variantIndex;
|
||||
latest.selectedVariantIndex = selectedVariantIndex;
|
||||
}
|
||||
|
||||
function normalizeKnowledgeItems(payload: Record<string, unknown>) {
|
||||
const source =
|
||||
payload.items ||
|
||||
payload.hits ||
|
||||
payload.documents ||
|
||||
payload.knowledgeResults ||
|
||||
[];
|
||||
if (!Array.isArray(source)) {
|
||||
return [];
|
||||
}
|
||||
const topLevelKnowledgeType = asText(payload.knowledgeType);
|
||||
const topLevelFaqCollection =
|
||||
payload.faqCollection === undefined
|
||||
? topLevelKnowledgeType.toUpperCase() === 'FAQ'
|
||||
: asBoolean(payload.faqCollection);
|
||||
return source.map((item: any) => {
|
||||
const metadata = asRecord(item.metadata);
|
||||
const sourceFileName = asText(
|
||||
item.sourceFileName ?? metadata.sourceFileName,
|
||||
);
|
||||
const documentName = asText(
|
||||
item.documentName ?? item.documentTitle ?? item.title,
|
||||
);
|
||||
const chunkId = asText(item.chunkId ?? metadata.chunkId ?? item.id);
|
||||
const documentId = asText(
|
||||
item.documentId ?? metadata.documentId ?? item.id,
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
id: asText(item.id || chunkId || documentId),
|
||||
knowledgeId: asText(item.knowledgeId ?? payload.knowledgeId),
|
||||
knowledgeName: asText(item.knowledgeName ?? payload.knowledgeName),
|
||||
knowledgeType: asText(item.knowledgeType ?? payload.knowledgeType),
|
||||
faqCollection:
|
||||
item.faqCollection === undefined
|
||||
? topLevelFaqCollection
|
||||
: asBoolean(item.faqCollection),
|
||||
documentId,
|
||||
documentName,
|
||||
chunkId,
|
||||
score: item.score ?? item.similarity,
|
||||
source: item.source,
|
||||
sourceFileName,
|
||||
sourceUri: asText(item.sourceUri ?? metadata.sourceUri),
|
||||
metadata,
|
||||
chunkContent: asText(
|
||||
item.chunkContent ?? item.content ?? item.text ?? item.summary,
|
||||
),
|
||||
content: asText(item.content ?? item.text ?? item.summary),
|
||||
title: documentName || sourceFileName || item.source,
|
||||
} satisfies ChatTimelineKnowledgeHit;
|
||||
});
|
||||
}
|
||||
|
||||
function statusKeyForProjection(
|
||||
payload: Record<string, unknown>,
|
||||
roundId: string,
|
||||
variantIndex: number,
|
||||
fallback = 'status',
|
||||
) {
|
||||
const statusKey = asText(payload.statusKey) || fallback;
|
||||
return `${statusKey}:${roundId}:${variantIndex}`;
|
||||
}
|
||||
|
||||
function projectEventToTimeline(
|
||||
items: ChatTimelineItem[],
|
||||
event: AgentTryoutRuntimeEvent,
|
||||
roundId: string,
|
||||
variantIndex: number,
|
||||
) {
|
||||
const { domain, payload, type } = event;
|
||||
if (domain === 'LLM' && type === 'MESSAGE') {
|
||||
ChatTimelineBuilder.appendMessageDelta(items, payload.delta, {
|
||||
id: nextAssistantId(items, roundId, variantIndex),
|
||||
roundId,
|
||||
});
|
||||
normalizeAssistantPartIds(items, roundId, variantIndex);
|
||||
return;
|
||||
}
|
||||
if (domain === 'LLM' && type === 'THINKING') {
|
||||
const text = asText(payload.reasoning ?? payload.delta ?? payload.text);
|
||||
ChatTimelineBuilder.appendThinkingDelta(items, text, {
|
||||
id: nextAssistantId(items, roundId, variantIndex),
|
||||
roundId,
|
||||
});
|
||||
normalizeAssistantPartIds(items, roundId, variantIndex);
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REQUEST') {
|
||||
ChatTimelineBuilder.appendToolApproval(items, {
|
||||
expiresAt: asText(payload.expiresAt),
|
||||
input: payload.input,
|
||||
metadata: payload.metadata,
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
|
||||
toolDisplayName: asText(payload.toolDisplayName),
|
||||
toolName: asText(payload.toolName),
|
||||
toolType: asText(payload.toolType),
|
||||
});
|
||||
if (items[items.length - 1]?.type === 'tool') {
|
||||
normalizeLatestItemId(items, 'tool-approval', roundId, variantIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_APPROVING') {
|
||||
ChatTimelineBuilder.markToolApproving(items, {
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REJECTED') {
|
||||
ChatTimelineBuilder.markToolRejected(items, {
|
||||
reason: asText(payload.reason),
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
|
||||
const rawToolName = asText(payload.toolName ?? payload.name);
|
||||
const normalizedToolName = normalizeToolName(rawToolName);
|
||||
if (!normalizedToolName && type === 'TOOL_CALL') {
|
||||
return;
|
||||
}
|
||||
const displayToolName = asText(
|
||||
payload.toolDisplayName ?? rawToolName ?? '工具',
|
||||
);
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: payload.input ?? payload.toolInput,
|
||||
output: payload.output ?? payload.result ?? payload.text,
|
||||
status: type === 'TOOL_RESULT' ? 'success' : 'running',
|
||||
statusKey: statusKeyForProjection(
|
||||
payload,
|
||||
roundId,
|
||||
variantIndex,
|
||||
'knowledge-retrieval',
|
||||
),
|
||||
toolCallId: asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id),
|
||||
toolName: isHiddenToolName(rawToolName) ? rawToolName : displayToolName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'CITATIONS') {
|
||||
const itemsToAppend = normalizeKnowledgeItems(payload);
|
||||
if (itemsToAppend.length > 0) {
|
||||
ChatTimelineBuilder.appendKnowledge(items, itemsToAppend);
|
||||
if (items[items.length - 1]?.type === 'knowledge') {
|
||||
normalizeLatestItemId(items, 'knowledge', roundId, variantIndex);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'STATUS') {
|
||||
if (asText(payload.statusKey) === 'memory-compression') {
|
||||
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
|
||||
compressed:
|
||||
typeof payload.compressed === 'boolean'
|
||||
? payload.compressed
|
||||
: undefined,
|
||||
label: asText(payload.label),
|
||||
phase: asText(payload.phase),
|
||||
status: asText(payload.status),
|
||||
statusKey: statusKeyForProjection(payload, roundId, variantIndex),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (asText(payload.statusKey) === 'knowledge-retrieval') {
|
||||
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
||||
items,
|
||||
asText(payload.status) === 'running' ? 'running' : 'done',
|
||||
statusKeyForProjection(payload, roundId, variantIndex),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === 'ERROR' || domain === 'ERROR') {
|
||||
ChatTimelineBuilder.appendError(
|
||||
items,
|
||||
payload.message ?? payload.error ?? '试运行失败',
|
||||
);
|
||||
normalizeLatestItemId(items, 'error', roundId, variantIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function sortedRounds(rounds: Map<string, AgentTryoutRawRound>) {
|
||||
return [...rounds.values()].sort(
|
||||
(first, second) =>
|
||||
(first.createdAt || first.updatedAt) -
|
||||
(second.createdAt || second.updatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
export function useAgentTryoutRawRounds(options: {
|
||||
mode: 'draft';
|
||||
sessionId: string;
|
||||
}) {
|
||||
const rounds = new Map<string, AgentTryoutRawRound>();
|
||||
let persistTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
for (const round of restoreSession(options.mode, options.sessionId)) {
|
||||
rounds.set(round.roundId, round);
|
||||
}
|
||||
|
||||
function persistNow() {
|
||||
if (persistTimer) {
|
||||
clearTimeout(persistTimer);
|
||||
persistTimer = undefined;
|
||||
}
|
||||
const overflow = rounds.size - MAX_ROUNDS;
|
||||
if (overflow > 0) {
|
||||
for (const round of sortedRounds(rounds).slice(0, overflow)) {
|
||||
rounds.delete(round.roundId);
|
||||
}
|
||||
}
|
||||
persistSession(options.mode, options.sessionId, [...rounds.values()]);
|
||||
}
|
||||
|
||||
function schedulePersist() {
|
||||
const key = storageKey(options.mode, options.sessionId);
|
||||
memorySessions.set(key, [...rounds.values()].map((item) => clone(item)));
|
||||
if (persistTimer) {
|
||||
return;
|
||||
}
|
||||
persistTimer = setTimeout(() => {
|
||||
persistNow();
|
||||
}, PERSIST_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
rounds.clear();
|
||||
if (persistTimer) {
|
||||
clearTimeout(persistTimer);
|
||||
persistTimer = undefined;
|
||||
}
|
||||
removeStoredSession(options.mode, options.sessionId);
|
||||
}
|
||||
|
||||
function createRound(prompt: string) {
|
||||
const now = Date.now();
|
||||
const roundId = createRoundId();
|
||||
rounds.set(roundId, {
|
||||
createdAt: now,
|
||||
prompt,
|
||||
roundId,
|
||||
selectedVariantIndex: 1,
|
||||
status: 'running',
|
||||
updatedAt: now,
|
||||
variants: [createVariant(1)],
|
||||
});
|
||||
persistNow();
|
||||
return roundId;
|
||||
}
|
||||
|
||||
function regenerateRound(roundId: string) {
|
||||
const round = rounds.get(roundId);
|
||||
if (!round) {
|
||||
return undefined;
|
||||
}
|
||||
const nextVariantIndex = Math.min(round.variants.length + 1, MAX_VARIANTS);
|
||||
round.variants.push(createVariant(round.variants.length + 1));
|
||||
if (round.variants.length > MAX_VARIANTS) {
|
||||
round.variants.splice(0, round.variants.length - MAX_VARIANTS);
|
||||
round.variants.forEach((variant, index) => {
|
||||
variant.variantIndex = index + 1;
|
||||
});
|
||||
}
|
||||
round.selectedVariantIndex = nextVariantIndex;
|
||||
round.status = 'running';
|
||||
round.updatedAt = Date.now();
|
||||
persistNow();
|
||||
return round.roundId;
|
||||
}
|
||||
|
||||
function getPrompt(roundId?: string) {
|
||||
return roundId ? rounds.get(roundId)?.prompt || '' : '';
|
||||
}
|
||||
|
||||
function currentVariant(roundId: string) {
|
||||
const round = rounds.get(roundId);
|
||||
return round ? selectedVariant(round) : undefined;
|
||||
}
|
||||
|
||||
function recordEvent(
|
||||
roundId: string,
|
||||
event: {
|
||||
domain: string;
|
||||
payload?: Record<string, unknown>;
|
||||
type: string;
|
||||
},
|
||||
) {
|
||||
const round = rounds.get(roundId);
|
||||
const variant = round && selectedVariant(round);
|
||||
if (!round || !variant) {
|
||||
return;
|
||||
}
|
||||
const runtimeEvent: AgentTryoutRuntimeEvent = {
|
||||
createdAt: Date.now(),
|
||||
domain: event.domain.toUpperCase(),
|
||||
payload: event.payload || {},
|
||||
type: event.type.toUpperCase(),
|
||||
};
|
||||
variant.runtimeEvents.push(runtimeEvent);
|
||||
if (runtimeEvent.domain === 'SYSTEM' && runtimeEvent.type === 'DONE') {
|
||||
variant.status = 'completed';
|
||||
round.status = 'completed';
|
||||
}
|
||||
if (runtimeEvent.type === 'ERROR' || runtimeEvent.domain === 'ERROR') {
|
||||
variant.status = 'error';
|
||||
round.status = 'error';
|
||||
}
|
||||
variant.updatedAt = Date.now();
|
||||
round.updatedAt = variant.updatedAt;
|
||||
if (
|
||||
(runtimeEvent.domain === 'SYSTEM' && runtimeEvent.type === 'DONE') ||
|
||||
runtimeEvent.type === 'ERROR' ||
|
||||
runtimeEvent.domain === 'ERROR'
|
||||
) {
|
||||
persistNow();
|
||||
return;
|
||||
}
|
||||
schedulePersist();
|
||||
}
|
||||
|
||||
function completeRound(roundId: string) {
|
||||
const round = rounds.get(roundId);
|
||||
const variant = round && selectedVariant(round);
|
||||
if (!round || !variant) {
|
||||
return;
|
||||
}
|
||||
if (variant.status === 'error' || round.status === 'error') {
|
||||
persistNow();
|
||||
return;
|
||||
}
|
||||
variant.status = 'completed';
|
||||
round.status = 'completed';
|
||||
variant.updatedAt = Date.now();
|
||||
round.updatedAt = variant.updatedAt;
|
||||
persistNow();
|
||||
}
|
||||
|
||||
function buildTimelineItems() {
|
||||
const items: ChatTimelineItem[] = [];
|
||||
for (const round of sortedRounds(rounds)) {
|
||||
ChatTimelineBuilder.appendUserMessage(items, round.prompt, {
|
||||
id: `user-${round.roundId}`,
|
||||
roundId: round.roundId,
|
||||
});
|
||||
const variant = selectedVariant(round);
|
||||
if (!variant) {
|
||||
continue;
|
||||
}
|
||||
for (const event of variant.runtimeEvents) {
|
||||
projectEventToTimeline(items, event, round.roundId, variant.variantIndex);
|
||||
}
|
||||
if (variant.status === 'completed' || variant.status === 'error') {
|
||||
ChatTimelineBuilder.finalize(items);
|
||||
markRoundCompleted(
|
||||
items,
|
||||
round.roundId,
|
||||
variant,
|
||||
round.variants.length,
|
||||
round.selectedVariantIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function selectVariant(
|
||||
roundId: string,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
const round = rounds.get(roundId);
|
||||
if (!round) {
|
||||
return;
|
||||
}
|
||||
const next =
|
||||
direction === 'previous'
|
||||
? round.selectedVariantIndex - 1
|
||||
: round.selectedVariantIndex + 1;
|
||||
if (next < 1 || next > round.variants.length) {
|
||||
return;
|
||||
}
|
||||
round.selectedVariantIndex = next;
|
||||
round.updatedAt = Date.now();
|
||||
persistNow();
|
||||
}
|
||||
|
||||
function canSwitch(
|
||||
item: ChatTimelineMessageItem,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
const current = Number(item.selectedVariantIndex || item.variantIndex || 1);
|
||||
const total = Number(item.variantCount || 1);
|
||||
return direction === 'previous' ? current > 1 : current < total;
|
||||
}
|
||||
|
||||
function copyText(item: ChatTimelineMessageItem) {
|
||||
return visibleText(item);
|
||||
}
|
||||
|
||||
return {
|
||||
buildTimelineItems,
|
||||
canSwitch,
|
||||
clear,
|
||||
completeRound,
|
||||
copyText,
|
||||
createRound,
|
||||
currentVariant,
|
||||
getPrompt,
|
||||
recordEvent,
|
||||
regenerateRound,
|
||||
selectVariant,
|
||||
flush: persistNow,
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
AgentTryoutRawRound,
|
||||
AgentTryoutRawVariant,
|
||||
AgentTryoutRuntimeEvent,
|
||||
};
|
||||
Reference in New Issue
Block a user