/* * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010403 API契约 draft-2026-06-18-r1. * 职责: Workbench durable facts store adapter。统一读取 session/message/turn/trace projection facts,不在 GET 中推进 AgentRun facts。 */ import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { safeConversationId, safeSessionId, safeTraceId } from "./server-http-utils.ts"; export function createWorkbenchFactsStore(options = {}, actor = null) { const accessStore = options.accessController?.store ?? options.accessController ?? null; const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const runtimeStore = options.runtimeStore ?? null; const results = options.codeAgentChatResults ?? null; async function getSessionById(sessionId) { const safeId = safeSessionId(sessionId); if (!safeId) return null; const session = await accessStore?.getAgentSession?.(safeId) ?? null; return canActorReadSession(session, actor) ? session : null; } async function getSessionByRouteId(routeId) { const sessionId = safeSessionId(routeId); if (sessionId) return getSessionById(sessionId); const conversationId = safeConversationId(routeId); if (!conversationId) return null; const listed = await accessStore?.listAgentSessionsForUser?.({ ownerUserId: actor?.id, actorRole: actor?.role, ownerScoped: true, conversationId, limit: 1, includeArchived: false }) ?? []; const matched = listed.find((session) => canActorReadSession(session, actor)) ?? null; if (matched) return matched; const suffix = conversationId.slice("cnv_".length); const deterministicSessionId = safeSessionId(`ses_${suffix}`); return deterministicSessionId ? getSessionById(deterministicSessionId) : null; } async function getSessionByTraceId(traceId) { const safeId = safeTraceId(traceId); if (!safeId) return null; const session = await accessStore?.getAgentSessionByTraceId?.(safeId) ?? null; return canActorReadSession(session, actor) ? session : null; } async function listSessions({ limit, offset = 0, includeRouteId } = {}) { let sessions = await accessStore?.listAgentSessionsForUser?.({ ownerUserId: actor?.id, actorRole: actor?.role, ownerScoped: true, limit, offset, includeArchived: false }) ?? []; const includeSessionId = safeSessionId(includeRouteId); if (includeRouteId && !sessions.some((session) => session.id === includeSessionId)) { const included = await getSessionByRouteId(includeRouteId); if (included) sessions = [included, ...sessions]; } return sessions.filter((session) => canActorReadSession(session, actor)); } function resultForTrace(traceId) { const safeId = safeTraceId(traceId); return safeId ? results?.get?.(safeId) ?? null : null; } function traceSnapshotSync(traceId) { return traceStore.snapshot(traceId); } async function traceSnapshot(traceId) { const memory = traceSnapshotSync(traceId); if (hasTraceProjection(memory) && TERMINAL_STATUSES.has(normalizeStatus(memory.status))) return memory; const durable = await durableTraceSnapshot(runtimeStore, traceId); if (durable && shouldPreferDurableTrace(memory, durable)) return durable; if (hasTraceProjection(memory)) return memory; return durable ?? memory; } function subscribeTrace(traceId, listener) { return traceStore.subscribe(traceId, listener); } return { getSessionById, getSessionByRouteId, getSessionByTraceId, listSessions, resultForTrace, traceSnapshot, traceSnapshotSync, subscribeTrace, canReadOwner: (ownerUserId) => canActorReadOwner(ownerUserId, actor) }; } async function durableTraceSnapshot(runtimeStore, traceId) { if (!runtimeStore || typeof runtimeStore.queryAgentTraceEvents !== "function") return null; let result; try { result = await runtimeStore.queryAgentTraceEvents({ traceId }); } catch { return null; } const events = Array.isArray(result?.events) ? result.events : []; if (events.length === 0) return null; const normalizedEvents = events.map((event, index) => ({ ...event, traceId, seq: eventSeq(event, index) })); const firstEvent = normalizedEvents[0] ?? null; const lastEvent = normalizedEvents.at(-1) ?? null; const status = durableTraceStatus(normalizedEvents); return { traceId, status, createdAt: firstEvent?.createdAt ?? null, updatedAt: lastEvent?.createdAt ?? firstEvent?.createdAt ?? null, startedAt: firstEvent?.createdAt ?? null, finishedAt: TERMINAL_STATUSES.has(status) ? lastEvent?.createdAt ?? null : null, eventCount: normalizedEvents.length, events: normalizedEvents, assistantStreams: [], eventLabels: normalizedEvents.map((event) => event.label).filter(Boolean), lastEvent, outputTruncated: normalizedEvents.some((event) => event.outputTruncated === true), valuesPrinted: false }; } const TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled", "idle"]); const RUNNING_STATUSES = new Set(["running", "pending", "queued", "accepted", "dispatching", "streaming", "active"]); function hasTraceProjection(snapshot) { return Boolean(snapshot?.status !== "missing" && (Number(snapshot?.eventCount ?? 0) > 0 || snapshot?.lastEvent)); } function shouldPreferDurableTrace(memory, durable) { if (!durable) return false; if (!hasTraceProjection(memory)) return true; const memoryStatus = normalizeStatus(memory?.status); const durableStatus = normalizeStatus(durable?.status); if (TERMINAL_STATUSES.has(durableStatus) && !TERMINAL_STATUSES.has(memoryStatus)) return true; return traceLastSeq(durable) > traceLastSeq(memory); } function traceLastSeq(snapshot) { const events = Array.isArray(snapshot?.events) ? snapshot.events : []; const lastEvent = snapshot?.lastEvent ?? events.at(-1) ?? null; const indexedMax = events.reduce((max, event, index) => Math.max(max, eventSeq(event, index)), 0); return Math.max(indexedMax, lastEvent ? eventSeq(lastEvent, Math.max(0, events.length - 1)) : 0); } function durableTraceStatus(events) { const lastEvent = events.at(-1) ?? null; const normalized = normalizeStatus(lastEvent?.status ?? lastEvent?.type); if (lastEvent?.terminal === true) { if (normalized === "observed" || normalized === "event" || normalized === "unknown") return "completed"; return normalized; } if (TERMINAL_STATUSES.has(normalized) || RUNNING_STATUSES.has(normalized)) return normalized; if (["failed", "error", "timeout"].includes(normalized)) return normalized; return "running"; } function canActorReadSession(session, actor) { if (!session || !actor) return false; if (normalizeStatus(session.status) === "archived") return false; if (actor.role === "admin") return true; return session.ownerUserId === actor.id; } function canActorReadOwner(ownerUserId, actor) { if (!ownerUserId || !actor) return true; if (actor.role === "admin") return true; return ownerUserId === actor.id; } function eventSeq(event, index) { const seq = Number(event?.seq); return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1; } function normalizeStatus(value) { const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); if (text === "cancelled") return "canceled"; return text || "unknown"; }