import type { AgentTranscriptSnapshot } from '../ops/operation'; import type { TranscriptAttachment } from '../model/attachment'; import type { TranscriptFrame, TranscriptUserOrigin } from '../model/frame'; import type { TranscriptItem, TranscriptMarker } from '../model/item'; import type { TurnOrigin } from '../model/turn'; import { daemonFileRefFromPairingPart } from '../contract/mediaRef'; import { projectTranscriptUserOrigin, projectTranscriptUserTurnOrigin } from '../contract/origin'; export type HistoryMediaSource = | { readonly kind: 'url'; readonly url: string } | { readonly kind: 'base64'; readonly media_type: string; readonly data: string } | { readonly kind: 'file' | 'session_media'; readonly file_id: string }; export type HistoryContentPart = | { readonly type: 'text'; readonly text: string } | { readonly type: 'think'; readonly think: string; readonly hidden?: boolean } | { readonly type: 'image' | 'video' | 'audio'; readonly source: HistoryMediaSource; readonly name?: string } | { readonly type: 'file'; readonly file_id: string; readonly name: string; readonly media_type: string; readonly size: number; } | { readonly type: string }; export interface HistoryToolCall { readonly id: string; readonly name: string; readonly arguments: string | null; } export interface HistoryMessage { readonly id?: string; readonly role: string; readonly content?: readonly HistoryContentPart[]; readonly toolCalls?: readonly HistoryToolCall[]; readonly toolCallId?: string; readonly isError?: boolean; readonly origin?: { readonly kind: string }; } interface TurnDraft { turnId: string; ordinal: number; triggerPromptId?: string; origin: TurnOrigin; prompt?: string; attachmentIds?: string[]; steps: StepDraft[]; } interface StepDraft { stepId: string; ordinal: number; frames: TranscriptFrame[]; } const HIDDEN_USER_ORIGINS = new Set(['injection', 'system_trigger', 'retry']); const TURN_OPENING_SYSTEM_TRIGGERS = new Set(['goal_continuation', 'subagent']); const MARKER_USER_ORIGINS: Readonly> = { skill_activation: 'skill', plugin_command: 'skill', compaction_summary: 'compaction', }; const FALLBACK_ORIGIN: TurnOrigin = { kind: 'other' }; export function groupMessagesIntoSnapshot( messages: readonly HistoryMessage[], options?: { readonly taskOriginTurnTaskIds?: ReadonlySet; readonly steeredContents?: ReadonlyMap>; }, ): AgentTranscriptSnapshot { const items: TranscriptItem[] = []; const attachments: TranscriptAttachment[] = []; const steeredContents = new Map( [...(options?.steeredContents ?? [])].map(([key, byKind]) => [key, new Map(byKind)]), ); let turn: TurnDraft | undefined; let pendingNotificationFrames: { text: string; taskId: string | undefined; attachmentIds?: string[]; promptIds?: readonly string[]; origin?: TranscriptUserOrigin; steered?: boolean; }[] = []; let nextOrdinal = 0; let markerCount = 0; const foldTurnOpeningInput = ( message: HistoryMessage, ): { text: string; attachmentIds?: string[] } => { const parts = message.content ?? []; const ids: string[] = []; const texts: string[] = []; for (const part of parts) { if (part.type === 'text' && 'text' in part && typeof part.text === 'string') { texts.push(part.text); continue; } if (part.type === 'image' || part.type === 'video' || part.type === 'audio') { if (!('source' in part) || part.source === undefined) continue; const source = part.source as HistoryMediaSource; const entity: TranscriptAttachment = { attachmentId: `att_${attachments.length + 1}`, mediaType: source.kind === 'base64' ? source.media_type : `${part.type}/*`, name: part.name, source: source.kind === 'url' ? { kind: 'url', url: source.url } : source.kind === 'file' || source.kind === 'session_media' ? { kind: source.kind, fileId: source.file_id } : undefined, }; attachments.push(entity); ids.push(entity.attachmentId); continue; } if (part.type === 'file' && 'file_id' in part) { const entity: TranscriptAttachment = { attachmentId: `att_${attachments.length + 1}`, mediaType: part.media_type as string, name: part.name as string, size: part.size as number, source: { kind: 'file', fileId: part.file_id as string }, }; attachments.push(entity); ids.push(entity.attachmentId); continue; } const ref = daemonFileRefFromPairingPart(part); if (ref !== undefined) { const entity: TranscriptAttachment = { attachmentId: `att_${attachments.length + 1}`, mediaType: `${ref.kind}/*`, name: ref.name, source: { kind: 'session_media', fileId: ref.ref.fileId }, }; attachments.push(entity); ids.push(entity.attachmentId); } } for (const attachment of originFileAttachments(message)) { const entity: TranscriptAttachment = { attachmentId: `att_${attachments.length + 1}`, mediaType: attachment.mediaType, name: attachment.name, size: attachment.size, }; attachments.push(entity); ids.push(entity.attachmentId); } return { text: texts.join(''), attachmentIds: ids.length > 0 ? ids : undefined }; }; const ensureTurn = (origin: TurnOrigin = FALLBACK_ORIGIN): TurnDraft => { if (!turn) { const ordinal = nextOrdinal; nextOrdinal += 1; turn = { turnId: `t${ordinal}`, ordinal, origin, steps: [] }; items.push(draftToTurnItem(turn)); } return turn; }; const flushSteeredLeftovers = (): void => { const leftovers = pendingNotificationFrames.filter((pending) => pending.steered); if (leftovers.length === 0) return; pendingNotificationFrames = pendingNotificationFrames.filter((pending) => !pending.steered); for (const pending of leftovers) { const targetTurn = turn ?? startTurn({ kind: 'user' }); let lastStep = targetTurn.steps.at(-1); if (lastStep === undefined) { const ordinal = targetTurn.steps.length + 1; lastStep = { stepId: `${targetTurn.turnId}.${ordinal}`, ordinal, frames: [], }; targetTurn.steps.push(lastStep); } lastStep.frames.push({ kind: 'text', frameId: `${lastStep.stepId}.f${lastStep.frames.length + 1}`, role: 'user', text: pending.text, attachmentIds: pending.attachmentIds, promptIds: pending.promptIds, origin: pending.origin, }); syncTurnItem(items, targetTurn); } }; const startTurn = ( origin: TurnOrigin, prompt?: string, attachmentIds?: string[], triggerPromptId?: string, ): TurnDraft => { flushSteeredLeftovers(); const ordinal = nextOrdinal; nextOrdinal += 1; pendingNotificationFrames = []; turn = { turnId: `t${ordinal}`, ordinal, triggerPromptId, origin, prompt, attachmentIds, steps: [] }; items.push(draftToTurnItem(turn)); return turn; }; const pushMarker = (marker: string, payload?: unknown): void => { markerCount += 1; const item: TranscriptMarker = { kind: 'marker', markerId: `m${markerCount}`, marker, payload }; items.push(item); }; let prevNonTaskRole: string | undefined; for (const message of messages) { if (message.role === 'system') continue; const originKind = message.origin?.kind; const isTaskOrigin = originKind === 'task' || originKind === 'background_task' || originKind === 'task_notification'; const prevRoleAtEntry = prevNonTaskRole; if (!isTaskOrigin) prevNonTaskRole = message.role; if (message.role === 'user') { if (originKind !== undefined && HIDDEN_USER_ORIGINS.has(originKind)) { if (opensOwnTurn(message)) { const opening = (message.origin as { name?: unknown }).name === 'subagent' ? foldTurnOpeningInput(message) : undefined; startTurn(mapOrigin(message), opening?.text || undefined, opening?.attachmentIds); } continue; } const markerKey = originKind !== undefined ? MARKER_USER_ORIGINS[originKind] : undefined; if (markerKey !== undefined && !isUserSlashPrompt(message)) { pushMarker(markerKey, { text: textOf(message), origin: message.origin }); continue; } const contentKey = JSON.stringify(message.content ?? []); const steerKind = originKind ?? 'user'; const steeredByKind = steeredContents.get(contentKey); const steeredRemaining = steeredByKind?.get(steerKind) ?? 0; if (steeredByKind !== undefined && steeredRemaining > 0) { steeredByKind.set(steerKind, steeredRemaining - 1); const bundled = bundledSkillActivations(message); const parts = message.content ?? []; bundled.forEach((activation, index) => { const block = parts[index]; pushMarker('skill', { text: block !== undefined && block.type === 'text' && 'text' in block ? block.text : '', origin: { kind: 'skill_activation', trigger: 'user-slash', ...activation }, }); }); const opening = foldTurnOpeningInput({ ...message, content: parts.slice(bundled.length) }); pendingNotificationFrames.push({ text: opening.text, taskId: undefined, attachmentIds: opening.attachmentIds, origin: projectTranscriptUserOrigin(message.origin), steered: true, }); continue; } if (markerKey !== undefined) { const opening = isUserSlashPrompt(message) ? foldTurnOpeningInput(message) : undefined; pushMarker(markerKey, { text: opening?.text ?? textOf(message), origin: message.origin }); if (opening !== undefined) { startTurn(mapOrigin(message), opening.text, opening.attachmentIds, triggerPromptIdOf(message)); } continue; } if (isTaskOrigin) { const origin = message.origin as { taskId?: unknown } | undefined; const taskId = typeof origin?.taskId === 'string' ? origin.taskId : undefined; const opensOwn = options?.taskOriginTurnTaskIds === undefined ? prevRoleAtEntry !== 'assistant' && prevRoleAtEntry !== 'tool' : taskId === undefined || options.taskOriginTurnTaskIds.has(taskId) || originKind === 'background_task'; if (opensOwn) { const opening = foldTurnOpeningInput(message); startTurn(mapOrigin(message), opening.text, opening.attachmentIds); continue; } pendingNotificationFrames.push({ text: notificationFrameText(textOf(message)), taskId }); continue; } const bundled = bundledSkillActivations(message); if (bundled.length > 0) { const parts = message.content ?? []; bundled.forEach((activation, index) => { const block = parts[index]; pushMarker('skill', { text: block !== undefined && block.type === 'text' && 'text' in block ? block.text : '', origin: { kind: 'skill_activation', trigger: 'user-slash', ...activation }, }); }); const callerMessage = { ...message, content: parts.slice(bundled.length) }; const opening = foldTurnOpeningInput(callerMessage); startTurn(mapOrigin(message), opening.text, opening.attachmentIds, triggerPromptIdOf(message)); continue; } const opening = foldTurnOpeningInput(message); startTurn(mapOrigin(message), opening.text, opening.attachmentIds, triggerPromptIdOf(message)); continue; } if (message.role === 'assistant') { const current = ensureTurn(); const stepOrdinal = current.steps.length + 1; const step: StepDraft = { stepId: `${current.turnId}.${stepOrdinal}`, ordinal: stepOrdinal, frames: [], }; current.steps.push(step); let frameCount = 0; const nextFrameId = (): string => { frameCount += 1; return `${step.stepId}.f${frameCount}`; }; for (const pending of pendingNotificationFrames) { step.frames.push({ kind: 'text', frameId: nextFrameId(), role: 'user', text: pending.text, taskId: pending.taskId, attachmentIds: pending.attachmentIds, promptIds: pending.promptIds, origin: pending.origin, }); } pendingNotificationFrames = []; for (const part of message.content ?? []) { if (part.type === 'text' && 'text' in part && typeof part.text === 'string' && part.text.length > 0) { step.frames.push({ kind: 'text', frameId: nextFrameId(), role: 'assistant', text: part.text }); } else if (part.type === 'think' && 'think' in part && typeof part.think === 'string' && part.think.length > 0 && part.hidden !== true) { step.frames.push({ kind: 'thinking', frameId: nextFrameId(), text: part.think }); } } for (const call of message.toolCalls ?? []) { step.frames.push({ kind: 'tool', frameId: `${step.stepId}.${call.id}`, toolCallId: call.id, name: call.name, state: 'running', input: parseArguments(call.arguments), }); } syncTurnItem(items, current); continue; } if (message.role === 'tool') { const frame = currentTurnToolFrame(turn, message.toolCallId); if (frame && frame.kind === 'tool') { const output = textOf(message); const patched: TranscriptFrame = { ...frame, state: message.isError ? 'error' : 'done', output, error: message.isError ? output : undefined, }; replaceToolFrame(turn!, message.toolCallId!, patched); syncTurnItem(items, turn!); } } } flushSteeredLeftovers(); return { items, tasks: [], interactions: [], attachments, todos: [], prompts: [], meta: {} }; } function notificationFrameText(text: string): string { if (!text.startsWith(''); const closingStart = text.lastIndexOf(''); if (openingEnd === -1 || closingStart <= openingEnd) return text; const inner = text.slice(openingEnd + 1, closingStart); const lines = inner.split('\n'); let headerEnd = 0; while (headerEnd < lines.length && lines[headerEnd]!.trim() === '') headerEnd += 1; let title = ''; let bodyStart = headerEnd; const titleLine = lines[bodyStart]; if (titleLine !== undefined && titleLine.startsWith('Title: ')) { title = titleLine.slice('Title: '.length); bodyStart += 1; } const severityLine = lines[bodyStart]; if (severityLine !== undefined && severityLine.startsWith('Severity: ')) { bodyStart += 1; } const bodyLines = lines.slice(bodyStart); const childStart = bodyLines.findIndex((line) => { const trimmed = line.trimStart(); return ( trimmed.startsWith(' 0 && body.length > 0) return `${title}\n${body}`; return title.length > 0 ? title : body.length > 0 ? body : text; } function opensOwnTurn(message: HistoryMessage): boolean { const origin = message.origin as { kind?: unknown; name?: unknown } | undefined; return ( origin?.kind === 'system_trigger' && typeof origin.name === 'string' && TURN_OPENING_SYSTEM_TRIGGERS.has(origin.name) ); } function isUserSlashPrompt(message: HistoryMessage): boolean { const origin = message.origin as { kind?: unknown; trigger?: unknown } | undefined; return ( (origin?.kind === 'skill_activation' || origin?.kind === 'plugin_command') && origin.trigger === 'user-slash' ); } function triggerPromptIdOf(message: HistoryMessage): string | undefined { if (typeof message.id !== 'string' || message.id.length === 0) return undefined; const origin = message.origin as { kind?: unknown; trigger?: unknown } | undefined; if (origin?.kind === undefined || origin.kind === 'user') return message.id; return ( (origin.kind === 'skill_activation' || origin.kind === 'plugin_command') && origin.trigger === 'user-slash' ) ? message.id : undefined; } function mapOrigin(message: HistoryMessage): TurnOrigin { const origin = message.origin; switch (origin?.kind) { case 'cron_job': case 'cron_missed': { const jobId = (origin as { jobId?: unknown }).jobId; return { kind: 'cron', taskId: typeof jobId === 'string' ? jobId : undefined, payload: origin }; } case 'task': case 'background_task': { const taskId = (origin as { taskId?: unknown }).taskId; return taskId !== undefined && typeof taskId === 'string' ? { kind: 'task', taskId, payload: origin } : { kind: 'other', payload: origin }; } case 'hook_result': return { kind: 'hook', payload: origin }; case 'shell_command': return { kind: 'user', payload: origin }; case 'user': return projectTranscriptUserTurnOrigin(origin); case undefined: return { kind: 'user' }; default: return { kind: 'other', payload: origin }; } } interface BundledSkillActivation { readonly activationId: string; readonly skillName: string; readonly skillArgs?: string; readonly skillType?: string; readonly skillPath?: string; readonly skillSource?: string; } function bundledSkillActivations(message: HistoryMessage): readonly BundledSkillActivation[] { if (message.origin?.kind !== 'user') return []; const activations = (message.origin as { readonly skillActivations?: unknown }).skillActivations; if (!Array.isArray(activations)) return []; return activations.filter( (activation): activation is BundledSkillActivation => typeof activation === 'object' && activation !== null && typeof (activation as { activationId?: unknown }).activationId === 'string' && typeof (activation as { skillName?: unknown }).skillName === 'string', ); } interface OriginFileAttachment { readonly name: string; readonly mediaType: string; readonly size: number; readonly path: string; } function originFileAttachments(message: HistoryMessage): readonly OriginFileAttachment[] { if (message.origin?.kind !== 'user' && message.origin?.kind !== 'skill_activation') return []; const attachments = (message.origin as { readonly attachments?: unknown }).attachments; if (!Array.isArray(attachments)) return []; return attachments.filter( (attachment): attachment is OriginFileAttachment => typeof attachment === 'object' && attachment !== null && typeof (attachment as { name?: unknown }).name === 'string' && typeof (attachment as { mediaType?: unknown }).mediaType === 'string' && typeof (attachment as { size?: unknown }).size === 'number' && typeof (attachment as { path?: unknown }).path === 'string', ); } function textOf(message: HistoryMessage): string { return (message.content ?? []) .filter((part): part is { readonly type: 'text'; readonly text: string } => part.type === 'text' && 'text' in part) .map((part) => part.text) .join(''); } function parseArguments(raw: string | null): unknown { if (!raw) return undefined; try { return JSON.parse(raw) as unknown; } catch { return raw; } } function draftToTurnItem(draft: TurnDraft): TranscriptItem { return { kind: 'turn', turnId: draft.turnId, triggerPromptId: draft.triggerPromptId, ordinal: draft.ordinal, state: 'completed', origin: draft.origin, prompt: draft.prompt, attachmentIds: draft.attachmentIds, steps: draft.steps.map((step) => ({ kind: 'step' as const, stepId: step.stepId, turnId: draft.turnId, ordinal: step.ordinal, state: 'completed' as const, frames: step.frames, })), }; } function syncTurnItem(items: TranscriptItem[], draft: TurnDraft): void { const index = items.findIndex((entry) => entry.kind === 'turn' && entry.turnId === draft.turnId); if (index >= 0) items[index] = draftToTurnItem(draft); } function currentTurnToolFrame(turn: TurnDraft | undefined, toolCallId: string | undefined): TranscriptFrame | undefined { if (!turn || toolCallId === undefined) return undefined; for (let s = turn.steps.length - 1; s >= 0; s -= 1) { const frames = turn.steps[s]?.frames ?? []; for (let f = frames.length - 1; f >= 0; f -= 1) { const frame = frames[f]; if (frame?.kind === 'tool' && frame.toolCallId === toolCallId) return frame; } } return undefined; } function replaceToolFrame(turn: TurnDraft, toolCallId: string, next: TranscriptFrame): void { for (let s = turn.steps.length - 1; s >= 0; s -= 1) { const step = turn.steps[s]; if (!step) continue; const index = step.frames.findIndex((frame) => frame.kind === 'tool' && frame.toolCallId === toolCallId); if (index >= 0) { step.frames[index] = next; return; } } }