| import type { RuntimeEvent } from '@maka/core'; |
| import type { StoredMessage } from '@maka/core'; |
| import { |
| isHardRuntimeEventReadModelDiagnostic, |
| projectRuntimeEventsToStoredMessages, |
| type CanonicalPermissionOutcomeReader, |
| type CanonicalPermissionOutcomeRecord, |
| } from '@maka/runtime'; |
| import type { ExecutionStoresWriter } from '@maka/storage/execution-stores'; |
| import type { TurnSnapshot } from '../protocol/index.js'; |
| import type { ReadSessionTranscript } from './session-continuity-coordinator.js'; |
| |
| const PERMISSION_OUTCOME_READ_CONCURRENCY = 8; |
| |
| export function createSessionTranscriptReader(input: { |
| stores: ExecutionStoresWriter<'interactive'>; |
| canonicalPermissionOutcomes: CanonicalPermissionOutcomeReader; |
| }): ReadSessionTranscript { |
| return async (sessionId, rootTurn) => { |
| const stored = await input.stores.sessionStore.readMessagesSnapshot(sessionId); |
| if (!rootTurn || isTerminalTurn(rootTurn)) return stored; |
| |
| const [run, events] = await Promise.all([ |
| input.stores.agentRunStore.readRun(sessionId, rootTurn.runId), |
| input.stores.runtimeEventStore.readRuntimeEvents(sessionId, rootTurn.runId), |
| ]); |
| const canonicalPermissionOutcomes = await readCanonicalPermissionOutcomes( |
| events, |
| input.canonicalPermissionOutcomes, |
| ); |
| const projected = projectRuntimeEventsToStoredMessages(activePresentationEvents(events), { |
| runHeaders: [run], |
| canonicalPermissionOutcomes, |
| }); |
| if (projected.diagnostics.some(isHardRuntimeEventReadModelDiagnostic)) { |
| throw new Error('Active RuntimeEvent transcript projection is incomplete'); |
| } |
| return mergeMessageUpserts(stored, projected.messages); |
| }; |
| } |
| |
| async function readCanonicalPermissionOutcomes( |
| events: readonly RuntimeEvent[], |
| reader: CanonicalPermissionOutcomeReader, |
| ): Promise<ReadonlyMap<string, CanonicalPermissionOutcomeRecord>> { |
| const requestIds = new Set( |
| events.flatMap((event) => { |
| const requestId = event.actions?.permissionAnswerAccepted?.requestId; |
| return requestId ? [requestId] : []; |
| }), |
| ); |
| const outcomes = new Map<string, CanonicalPermissionOutcomeRecord>(); |
| const ids = [...requestIds]; |
| for (let index = 0; index < ids.length; index += PERMISSION_OUTCOME_READ_CONCURRENCY) { |
| const batch = await Promise.all( |
| ids.slice(index, index + PERMISSION_OUTCOME_READ_CONCURRENCY).map(async (requestId) => ({ |
| requestId, |
| outcome: await reader.readPermissionOutcome(requestId), |
| })), |
| ); |
| for (const item of batch) { |
| if (item.outcome) outcomes.set(item.requestId, item.outcome); |
| } |
| } |
| return outcomes; |
| } |
| |
| function activePresentationEvents(events: readonly RuntimeEvent[]): RuntimeEvent[] { |
| const textMessages = new Set<string>(); |
| const lastThinkingByMessage = new Map<string, RuntimeEvent>(); |
| |
| for (const event of events) { |
| const content = event.content; |
| if (event.role !== 'model' || (content?.kind !== 'text' && content?.kind !== 'thinking')) { |
| continue; |
| } |
| const messageKey = activeMessageKey(event); |
| if (content.kind === 'text') textMessages.add(messageKey); |
| else lastThinkingByMessage.set(messageKey, event); |
| } |
| |
| const syntheticAfter = new Map<RuntimeEvent, RuntimeEvent[]>(); |
| for (const [messageKey, thinking] of lastThinkingByMessage) { |
| if (textMessages.has(messageKey)) continue; |
| const existing = syntheticAfter.get(thinking) ?? []; |
| existing.push(emptyAssistantText(thinking)); |
| syntheticAfter.set(thinking, existing); |
| } |
| |
| const presented: RuntimeEvent[] = []; |
| for (const event of events) { |
| presented.push(presentationEvent(event)); |
| const synthetic = syntheticAfter.get(event); |
| if (synthetic) presented.push(...synthetic); |
| } |
| return presented; |
| } |
| |
| function activeMessageKey(event: RuntimeEvent): string { |
| const messageId = event.refs?.providerEventId ?? event.refs?.storedMessageId ?? event.id; |
| return `${event.runId}\0${messageId}`; |
| } |
| |
| function presentationEvent(event: RuntimeEvent): RuntimeEvent { |
| const content = event.content; |
| return event.partial && |
| event.role === 'model' && |
| (content?.kind === 'text' || content?.kind === 'thinking') |
| ? { ...event, partial: false } |
| : event; |
| } |
| |
| function emptyAssistantText(thinking: RuntimeEvent): RuntimeEvent { |
| return { |
| ...thinking, |
| id: `${thinking.id}:active-transcript-empty-text`, |
| partial: false, |
| content: { kind: 'text', text: '' }, |
| }; |
| } |
| |
| function mergeMessageUpserts( |
| stored: readonly StoredMessage[], |
| active: readonly StoredMessage[], |
| ): StoredMessage[] { |
| const merged = stored.map((message) => structuredClone(message)); |
| const indices = new Map(merged.map((message, index) => [message.id, index])); |
| for (const message of active) { |
| const index = indices.get(message.id); |
| if (index === undefined) { |
| indices.set(message.id, merged.length); |
| merged.push(structuredClone(message)); |
| } else { |
| merged[index] = structuredClone(message); |
| } |
| } |
| return merged; |
| } |
| |
| function isTerminalTurn(turn: TurnSnapshot): boolean { |
| return turn.status === 'completed' || turn.status === 'failed' || turn.status === 'cancelled'; |
| } |