import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle"; import type { Agent } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; import type { AssistantMessageItem, StreamItem, UserMessageItem } from "@/types/stream"; import { applyStreamEvent, flushHeadToTail, hydrateStreamState, isAgentToolCallItem, mergeAgentToolCallItem, reduceStreamUpdate, } from "@/types/stream"; const AGENT_STREAM_REDUCER_FLUSH_DELAY_MS = 16 * 3; // --------------------------------------------------------------------------- // Shared cursor type // --------------------------------------------------------------------------- export interface TimelineCursor { epoch: string; startSeq: number; endSeq: number; } // --------------------------------------------------------------------------- // Side-effect discriminated unions // --------------------------------------------------------------------------- export type TimelineReducerSideEffect = | { type: "catch_up"; cursor: { epoch: string; endSeq: number } } | { type: "flush_pending_updates" }; export interface AgentStreamReducerSideEffect { type: "catch_up"; cursor: { epoch: string; endSeq: number }; } // --------------------------------------------------------------------------- // processTimelineResponse // --------------------------------------------------------------------------- type TimelineDirection = "tail" | "before" | "after"; type InitRequestDirection = "tail" | "after"; type SessionTimelineSeqCursor = | { epoch: string; endSeq: number; } | null | undefined; type SessionTimelineSeqDecision = "accept" | "drop_stale" | "drop_epoch" | "gap" | "init"; interface TimelineSeqRange { startSeq: number; endSeq: number; } interface TimelineResponseEntry { seqStart: number; seqEnd: number; sourceSeqRanges?: TimelineSeqRange[]; collapsed?: string[]; provider: string; item: Record; timestamp: string; } export interface ProcessTimelineResponseInput { payload: { agentId: string; direction: TimelineDirection; reset: boolean; epoch: string; startCursor: { seq: number } | null; endCursor: { seq: number } | null; entries: TimelineResponseEntry[]; error: string | null; hasNewer: boolean; hasOlder: boolean; }; currentTail: StreamItem[]; currentHead: StreamItem[]; currentCursor: TimelineCursor | undefined; isInitializing: boolean; hasActiveInitDeferred: boolean; initRequestDirection: InitRequestDirection; } export interface ProcessTimelineResponseOutput { tail: StreamItem[]; head: StreamItem[]; cursor: TimelineCursor | null | undefined; cursorChanged: boolean; initResolution: "resolve" | "reject" | null; clearInitializing: boolean; error: string | null; sideEffects: TimelineReducerSideEffect[]; } interface TimelineUnit { seq: number; seqEnd: number; sourceSeqRanges: TimelineSeqRange[]; event: AgentStreamEventPayload; timestamp: Date; } interface TimelinePathResult { tail: StreamItem[]; head: StreamItem[]; cursor: TimelineCursor | null | undefined; cursorChanged: boolean; sideEffects: TimelineReducerSideEffect[]; } function classifySessionTimelineSeq({ cursor, epoch, seq, }: { cursor: SessionTimelineSeqCursor; epoch: string; seq: number; }): SessionTimelineSeqDecision { if (!cursor) { return "init"; } if (cursor.epoch !== epoch) { return "drop_epoch"; } if (seq <= cursor.endSeq) { return "drop_stale"; } if (seq === cursor.endSeq + 1) { return "accept"; } return "gap"; } function deriveBootstrapTailTimelinePolicy({ direction, reset, epoch, endCursor, isInitializing, hasActiveInitDeferred, }: { direction: TimelineDirection; reset: boolean; epoch: string; endCursor: { seq: number } | null; isInitializing: boolean; hasActiveInitDeferred: boolean; }): { replace: boolean; catchUpCursor: { epoch: string; endSeq: number } | null; } { if (reset) { return { replace: true, catchUpCursor: null }; } const isBootstrapTailInit = direction === "tail" && isInitializing && hasActiveInitDeferred; if (!isBootstrapTailInit) { return { replace: false, catchUpCursor: null }; } return { replace: true, catchUpCursor: endCursor ? { epoch, endSeq: endCursor.seq } : null, }; } function shouldResolveTimelineInit({ hasActiveInitDeferred, hasNewer, isInitializing, initRequestDirection, responseDirection, reset, }: { hasActiveInitDeferred: boolean; hasNewer: boolean; isInitializing: boolean; initRequestDirection: InitRequestDirection; responseDirection: TimelineDirection; reset: boolean; }): boolean { if (!hasActiveInitDeferred || !isInitializing) { return false; } if (reset) { return true; } if (responseDirection === "after" && hasNewer) { return false; } return responseDirection === initRequestDirection; } function deriveOptimisticLifecycleStatus( currentStatus: AgentLifecycleStatus, event: AgentStreamEventPayload, ): AgentLifecycleStatus | null { if (currentStatus !== "running") { return null; } switch (event.type) { case "turn_completed": return "idle"; case "turn_failed": return "error"; case "turn_canceled": // A canceled turn can be either a final user cancel or an interrupt before // a replacement turn starts. The daemon snapshot is authoritative here. return null; default: return null; } } function preserveReplacePathAssistantHead(params: { tail: StreamItem[]; currentHead: StreamItem[]; }): { tail: StreamItem[]; head: StreamItem[]; } { const { tail, currentHead } = params; const liveAssistant = currentHead.findLast( (item): item is Extract => item.kind === "assistant_message", ); if (!liveAssistant) { return { tail, head: [] }; } const tailAssistant = tail.at(-1); if (!tailAssistant || tailAssistant.kind !== "assistant_message") { return { tail, head: currentHead }; } if (!liveAssistant.text.startsWith(tailAssistant.text)) { return { tail, head: [] }; } return { tail: tail.slice(0, -1), head: [{ ...liveAssistant, text: tailAssistant.text }], }; } function applyTimelineReplacePath(args: { timelineUnits: TimelineUnit[]; payload: ProcessTimelineResponseInput["payload"]; bootstrapPolicy: ReturnType; currentTail: StreamItem[]; currentHead: StreamItem[]; toHydratedEvents: ( units: TimelineUnit[], ) => Array<{ event: AgentStreamEventPayload; timestamp: Date }>; }): TimelinePathResult { const { timelineUnits, payload, bootstrapPolicy, currentTail, currentHead, toHydratedEvents } = args; const hydratedTail = hydrateStreamState(toHydratedEvents(timelineUnits), { source: "canonical" }); const reconciledTail = reconcileLocalUserPresentationAfterReplace({ canonicalTail: hydratedTail, previousTail: currentTail, previousHead: currentHead, }); const { tail, head } = preserveReplacePathAssistantHead({ tail: reconciledTail, currentHead, }); const cursor: TimelineCursor | null = payload.startCursor && payload.endCursor ? { epoch: payload.epoch, startSeq: payload.startCursor.seq, endSeq: payload.endCursor.seq, } : null; const sideEffects: TimelineReducerSideEffect[] = []; if (bootstrapPolicy.catchUpCursor) { sideEffects.push({ type: "catch_up", cursor: bootstrapPolicy.catchUpCursor }); } return { tail, head, cursor, cursorChanged: true, sideEffects }; } function collectLocallyPresentedUserMessages(items: StreamItem[]): Array<{ ordinal: number; item: UserMessageItem; }> { const localUsers: Array<{ ordinal: number; item: UserMessageItem }> = []; let ordinal = 0; for (const item of items) { if (item.kind !== "user_message") { continue; } if (item.optimistic || item.images?.length || item.attachments?.length) { localUsers.push({ ordinal, item }); } ordinal += 1; } return localUsers; } function mergeCanonicalUserWithLocalPresentation( canonical: UserMessageItem, local: UserMessageItem, ): UserMessageItem { return { kind: "user_message", id: canonical.id, ...(canonical.clientMessageId ? { clientMessageId: canonical.clientMessageId } : {}), text: local.text, timestamp: local.timestamp, ...(local.images && local.images.length > 0 ? { images: local.images } : {}), ...(local.attachments && local.attachments.length > 0 ? { attachments: local.attachments } : {}), }; } interface CanonicalUserMessageIdentity { messageId?: string; clientMessageId?: string; text: string; } function matchesLocalUserMessageIdentity( canonical: CanonicalUserMessageIdentity, optimistic: UserMessageItem, ): boolean { if (canonical.clientMessageId !== undefined) { return canonical.clientMessageId === optimistic.id; } if (canonical.messageId === optimistic.id) { return true; } // COMPAT(userMessageClientId): added in v0.2.0, remove after 2027-01-20 once // the supported daemon floor emits clientMessageId on submitted user messages. return canonical.text.length > 0 && canonical.text === optimistic.text; } function reconcileLocalUserPresentationAfterReplace(params: { canonicalTail: StreamItem[]; previousTail: StreamItem[]; previousHead: StreamItem[]; }): StreamItem[] { const localUsers = collectLocallyPresentedUserMessages([ ...params.previousTail, ...params.previousHead, ]); if (localUsers.length === 0) { return params.canonicalTail; } const canonicalUserIndexes: number[] = []; params.canonicalTail.forEach((item, index) => { if (item.kind === "user_message") { canonicalUserIndexes.push(index); } }); const nextTail = [...params.canonicalTail]; const claimedCanonicalIndexes = new Set(); const unmatched: UserMessageItem[] = []; for (const local of localUsers) { const exactIndex = canonicalUserIndexes.find((index) => { if (claimedCanonicalIndexes.has(index)) return false; const canonical = params.canonicalTail[index]; return ( canonical?.kind === "user_message" && matchesLocalUserMessageIdentity( { messageId: canonical.id, clientMessageId: canonical.clientMessageId, text: canonical.text, }, local.item, ) ); }); const ordinalIndex = canonicalUserIndexes[local.ordinal]; const ordinalItem = ordinalIndex === undefined ? undefined : params.canonicalTail[ordinalIndex]; const canonicalIndex = exactIndex ?? (ordinalIndex !== undefined && !claimedCanonicalIndexes.has(ordinalIndex) && ordinalItem?.kind === "user_message" && ordinalItem.clientMessageId === undefined ? ordinalIndex : undefined); const canonicalItem = canonicalIndex === undefined ? undefined : nextTail[canonicalIndex]; if (canonicalIndex === undefined || !canonicalItem || canonicalItem.kind !== "user_message") { if (local.item.optimistic) { unmatched.push(local.item); } continue; } nextTail[canonicalIndex] = mergeCanonicalUserWithLocalPresentation(canonicalItem, local.item); claimedCanonicalIndexes.add(canonicalIndex); } for (const item of unmatched) { const insertionIndex = nextTail.findIndex( (canonical) => canonical.timestamp.getTime() > item.timestamp.getTime(), ); nextTail.splice(insertionIndex < 0 ? nextTail.length : insertionIndex, 0, item); } return nextTail; } interface IncrementalAcceptResult { acceptedUnits: TimelineUnit[]; cursor: TimelineCursor | undefined; gapCursor: { epoch: string; endSeq: number } | null; } function acceptIncrementalTimelineUnits(args: { timelineUnits: TimelineUnit[]; payload: ProcessTimelineResponseInput["payload"]; currentCursor: TimelineCursor | undefined; }): IncrementalAcceptResult { const { timelineUnits, payload, currentCursor } = args; const firstUnit = timelineUnits[0]; const lastUnit = timelineUnits[timelineUnits.length - 1]; const responseStartSeq = payload.startCursor?.seq ?? firstUnit?.seq; const responseEndSeq = payload.endCursor?.seq ?? lastUnit?.seqEnd; if (responseStartSeq === undefined || responseEndSeq === undefined) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: null }; } if (!currentCursor) { return { acceptedUnits: timelineUnits, cursor: { epoch: payload.epoch, startSeq: responseStartSeq, endSeq: responseEndSeq }, gapCursor: null, }; } if (currentCursor.epoch !== payload.epoch) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: null }; } if ( (!payload.startCursor || !payload.endCursor) && responseStartSeq <= currentCursor.endSeq && responseEndSeq > currentCursor.endSeq ) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq }, }; } if (responseEndSeq <= currentCursor.endSeq) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: null }; } if (responseStartSeq > currentCursor.endSeq + 1) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq }, }; } return { acceptedUnits: timelineUnits, cursor: { ...currentCursor, endSeq: responseEndSeq }, gapCursor: null, }; } function acceptOlderTimelineUnits(args: { timelineUnits: TimelineUnit[]; payload: ProcessTimelineResponseInput["payload"]; currentCursor: TimelineCursor | undefined; }): IncrementalAcceptResult { const { timelineUnits, payload, currentCursor } = args; if (!currentCursor || currentCursor.epoch !== payload.epoch) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: null }; } const firstUnit = timelineUnits[0]; const lastUnit = timelineUnits[timelineUnits.length - 1]; const responseStartSeq = payload.startCursor?.seq ?? firstUnit?.seq; const responseEndSeq = payload.endCursor?.seq ?? lastUnit?.seqEnd; if ( responseStartSeq === undefined || responseEndSeq === undefined || responseEndSeq >= currentCursor.startSeq ) { return { acceptedUnits: [], cursor: currentCursor, gapCursor: null }; } return { acceptedUnits: timelineUnits, cursor: { ...currentCursor, startSeq: responseStartSeq }, gapCursor: null, }; } function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: StreamItem[]) { if (olderTail.length === 0) { return currentTail; } if (currentTail.length === 0) { return olderTail; } const olderLast = olderTail.at(-1); const currentFirst = currentTail[0]; if ( olderLast && currentFirst && isAgentToolCallItem(olderLast) && isAgentToolCallItem(currentFirst) && olderLast.payload.data.callId === currentFirst.payload.data.callId ) { return [ ...olderTail.slice(0, -1), mergeAgentToolCallItem(olderLast, currentFirst.payload.data, currentFirst.timestamp), ...currentTail.slice(1), ]; } if (olderLast?.kind !== "assistant_message" || currentFirst?.kind !== "assistant_message") { return [...olderTail, ...currentTail]; } return [ ...olderTail.slice(0, -1), { ...olderLast, text: `${olderLast.text}${currentFirst.text}`, timestamp: currentFirst.timestamp, ...(currentFirst.timelineCursor ? { timelineCursor: currentFirst.timelineCursor } : {}), }, ...currentTail.slice(1), ]; } function replaceLiveAssistantWithProjectedText(params: { head: StreamItem[]; event: AgentStreamEventPayload; timestamp: Date; timelineCursor: { epoch: string; seq: number }; }): StreamItem[] | null { const { head, event, timestamp, timelineCursor } = params; if (event.type !== "timeline" || event.item.type !== "assistant_message") { return null; } const index = head.findLastIndex((item) => item.kind === "assistant_message"); const current = head[index]; if (!current || current.kind !== "assistant_message") { return null; } if (!event.item.text.startsWith(current.text)) { return null; } const next = [...head]; next[index] = { ...current, text: event.item.text, timestamp, timelineCursor, }; return next; } function reconcileOverlappingProjectedAssistant(params: { tail: StreamItem[]; head: StreamItem[]; unit: TimelineUnit; epoch: string; currentEndSeq: number; }): { tail: StreamItem[]; head: StreamItem[]; reconciled: boolean } { const { unit } = params; if ( unit.event.type !== "timeline" || unit.event.item.type !== "assistant_message" || !unit.sourceSeqRanges.some( (range) => range.startSeq <= params.currentEndSeq && range.endSeq > params.currentEndSeq, ) ) { return { tail: params.tail, head: params.head, reconciled: false }; } const projectedText = unit.event.item.text; const projectedMessageId = unit.event.item.messageId; const matches = (item: StreamItem) => { if (item.kind !== "assistant_message") return false; if (projectedMessageId && item.messageId) return item.messageId === projectedMessageId; return projectedText.startsWith(item.text); }; const findMatch = (items: StreamItem[]) => { const index = items.findLastIndex(matches); const current = items[index]; return current?.kind === "assistant_message" ? { current, index } : null; }; const headMatch = findMatch(params.head); const tailMatch = headMatch ? null : findMatch(params.tail); const match = headMatch ?? tailMatch; if (!match) { return { tail: params.tail, head: params.head, reconciled: false }; } const blockGroupId = match.current.blockGroupId; const messageId = projectedMessageId ?? match.current.messageId; const replacement: AssistantMessageItem = { kind: "assistant_message", id: blockGroupId ?? match.current.id, ...(messageId !== undefined ? { messageId } : {}), text: projectedText, timestamp: unit.timestamp, timelineCursor: { epoch: params.epoch, seq: unit.seqEnd }, }; const belongsToBlockGroup = (item: StreamItem) => blockGroupId !== undefined && item.kind === "assistant_message" && item.blockGroupId === blockGroupId; const removeBlockGroup = (items: StreamItem[]) => blockGroupId !== undefined ? items.filter((item) => !belongsToBlockGroup(item)) : items; const replaceMatch = (items: StreamItem[], index: number) => { if (!blockGroupId) { const next = [...items]; next[index] = replacement; return next; } const next: StreamItem[] = []; let inserted = false; for (const item of items) { if (!belongsToBlockGroup(item)) { next.push(item); } else if (!inserted) { next.push(replacement); inserted = true; } } return next; }; if (headMatch) { return { tail: removeBlockGroup(params.tail), head: replaceMatch(params.head, headMatch.index), reconciled: true, }; } return { tail: replaceMatch(params.tail, match.index), head: removeBlockGroup(params.head), reconciled: true, }; } function reconcileOverlappingProjectedReasoning(params: { tail: StreamItem[]; head: StreamItem[]; unit: TimelineUnit; currentEndSeq: number; }): { tail: StreamItem[]; head: StreamItem[]; reconciled: boolean } { const { unit } = params; if ( unit.event.type !== "timeline" || unit.event.item.type !== "reasoning" || !unit.sourceSeqRanges.some( (range) => range.startSeq <= params.currentEndSeq && range.endSeq > params.currentEndSeq, ) ) { return { tail: params.tail, head: params.head, reconciled: false }; } const projectedText = unit.event.item.text; const replaceIn = (items: StreamItem[]): StreamItem[] | null => { const index = items.findLastIndex( (item) => item.kind === "thought" && projectedText.startsWith(item.text), ); const current = items[index]; if (!current || current.kind !== "thought") return null; const next = [...items]; next[index] = { ...current, text: projectedText, timestamp: unit.timestamp, status: "loading", }; return next; }; const nextHead = replaceIn(params.head); if (nextHead) return { tail: params.tail, head: nextHead, reconciled: true }; const nextTail = replaceIn(params.tail); return nextTail ? { tail: nextTail, head: params.head, reconciled: true } : { tail: params.tail, head: params.head, reconciled: false }; } function reconcileOverlappingProjectedStreamItems(params: { tail: StreamItem[]; head: StreamItem[]; units: TimelineUnit[]; epoch: string; currentEndSeq: number | undefined; }): { tail: StreamItem[]; head: StreamItem[]; reconciledUnits: Set } { let tail = params.tail; let head = params.head; const reconciledUnits = new Set(); if (params.currentEndSeq === undefined) return { tail, head, reconciledUnits }; for (const unit of params.units) { let reconciled = reconcileOverlappingProjectedAssistant({ tail, head, unit, epoch: params.epoch, currentEndSeq: params.currentEndSeq, }); if (!reconciled.reconciled) { reconciled = reconcileOverlappingProjectedReasoning({ tail, head, unit, currentEndSeq: params.currentEndSeq, }); } tail = reconciled.tail; head = reconciled.head; if (reconciled.reconciled) reconciledUnits.add(unit); } return { tail, head, reconciledUnits }; } function applyCanonicalForwardUnit(params: { tail: StreamItem[]; head: StreamItem[]; unit: TimelineUnit; epoch: string; }): { tail: StreamItem[]; head: StreamItem[] } { const { event, timestamp, seqEnd } = params.unit; const timelineCursor = { epoch: params.epoch, seq: seqEnd }; if (params.head.length === 0) { return { tail: reduceStreamUpdate(params.tail, event, timestamp, { source: "canonical", timelineCursor, }), head: params.head, }; } const replacedHead = replaceLiveAssistantWithProjectedText({ head: params.head, event, timestamp, timelineCursor, }); if (replacedHead) return { tail: params.tail, head: replacedHead }; const activeAssistant = params.head.findLast( (item): item is Extract => item.kind === "assistant_message", ); if ( event.type === "timeline" && event.item.type === "assistant_message" && event.item.messageId !== undefined && event.item.messageId !== activeAssistant?.messageId ) { return { tail: flushHeadToTail(params.tail, params.head), head: reduceStreamUpdate([], event, timestamp, { source: "canonical", timelineCursor, }), }; } const applied = applyStreamEvent({ tail: params.tail, head: params.head, event, timestamp, source: "canonical", timelineCursor, }); return { tail: applied.tail, head: applied.head }; } function applyAcceptedForwardTimelineUnits(params: { units: TimelineUnit[]; epoch: string; currentTail: StreamItem[]; currentHead: StreamItem[]; currentEndSeq: number | undefined; }): { tail: StreamItem[]; head: StreamItem[] } { const reconciled = reconcileOverlappingProjectedStreamItems({ tail: params.currentTail, head: params.currentHead, units: params.units, epoch: params.epoch, currentEndSeq: params.currentEndSeq, }); let tail = reconciled.tail; let head = reconciled.head; for (const unit of params.units) { if (reconciled.reconciledUnits.has(unit)) continue; const applied = applyCanonicalForwardUnit({ tail, head, unit, epoch: params.epoch }); tail = applied.tail; head = applied.head; } return { tail, head }; } function applyTimelineIncrementalPath(args: { timelineUnits: TimelineUnit[]; payload: ProcessTimelineResponseInput["payload"]; currentTail: StreamItem[]; currentHead: StreamItem[]; currentCursor: TimelineCursor | undefined; }): TimelinePathResult { const { timelineUnits, payload, currentTail, currentHead, currentCursor } = args; let nextTail = currentTail; let nextHead = currentHead; let nextCursor: TimelineCursor | null | undefined = currentCursor; let cursorChanged = false; const sideEffects: TimelineReducerSideEffect[] = []; if (timelineUnits.length === 0) { return { tail: nextTail, head: nextHead, cursor: nextCursor, cursorChanged, sideEffects }; } const { acceptedUnits, cursor, gapCursor } = payload.direction === "before" ? acceptOlderTimelineUnits({ timelineUnits, payload, currentCursor, }) : acceptIncrementalTimelineUnits({ timelineUnits, payload, currentCursor, }); if (acceptedUnits.length > 0) { if (payload.direction === "before") { const olderTail = hydrateStreamState( acceptedUnits.map(({ event, timestamp, seqEnd }) => ({ event, timestamp, timelineCursor: { epoch: payload.epoch, seq: seqEnd }, })), { source: "canonical" }, ); nextTail = mergePrependedCanonicalTail(olderTail, currentTail); } else { const applied = applyAcceptedForwardTimelineUnits({ units: acceptedUnits, epoch: payload.epoch, currentTail: nextTail, currentHead: nextHead, currentEndSeq: currentCursor?.endSeq, }); nextTail = applied.tail; nextHead = applied.head; } } if ( cursor && (!currentCursor || currentCursor.epoch !== cursor.epoch || currentCursor.startSeq !== cursor.startSeq || currentCursor.endSeq !== cursor.endSeq) ) { nextCursor = cursor; cursorChanged = true; } if (gapCursor) { sideEffects.push({ type: "catch_up", cursor: gapCursor }); } return { tail: nextTail, head: nextHead, cursor: nextCursor, cursorChanged, sideEffects }; } export function processTimelineResponse( input: ProcessTimelineResponseInput, ): ProcessTimelineResponseOutput { const { payload, currentTail, currentHead, currentCursor, isInitializing, hasActiveInitDeferred, initRequestDirection, } = input; // ------------------------------------------------------------------ // Error path: reject init and leave stream state unchanged // ------------------------------------------------------------------ if (payload.error) { return { tail: currentTail, head: currentHead, cursor: currentCursor, cursorChanged: false, initResolution: hasActiveInitDeferred ? "reject" : null, clearInitializing: isInitializing, error: payload.error, sideEffects: [], }; } // ------------------------------------------------------------------ // Convert entries to timeline units // ------------------------------------------------------------------ const timelineUnits = payload.entries.map((entry) => ({ seq: entry.seqStart, seqEnd: entry.seqEnd, sourceSeqRanges: entry.sourceSeqRanges && entry.sourceSeqRanges.length > 0 ? entry.sourceSeqRanges : [{ startSeq: entry.seqStart, endSeq: entry.seqEnd }], event: { type: "timeline", provider: entry.provider, item: entry.item, } as AgentStreamEventPayload, timestamp: new Date(entry.timestamp), })); const toHydratedEvents = ( units: TimelineUnit[], ): Array<{ event: AgentStreamEventPayload; timestamp: Date; timelineCursor: { epoch: string; seq: number }; }> => units.map(({ event, timestamp, seqEnd }) => ({ event, timestamp, timelineCursor: { epoch: payload.epoch, seq: seqEnd }, })); // ------------------------------------------------------------------ // Derive bootstrap policy (replace vs incremental) // ------------------------------------------------------------------ const bootstrapPolicy = deriveBootstrapTailTimelinePolicy({ direction: payload.direction, reset: payload.reset, epoch: payload.epoch, endCursor: payload.endCursor, isInitializing, hasActiveInitDeferred, }); const replace = bootstrapPolicy.replace; const sideEffects: TimelineReducerSideEffect[] = []; const timelineResult = replace ? applyTimelineReplacePath({ timelineUnits, payload, bootstrapPolicy, currentTail, currentHead, toHydratedEvents, }) : applyTimelineIncrementalPath({ timelineUnits, payload, currentTail, currentHead, currentCursor, }); const nextTail = timelineResult.tail; const nextHead = timelineResult.head; const nextCursor = timelineResult.cursor; const cursorChanged = timelineResult.cursorChanged; sideEffects.push(...timelineResult.sideEffects); // ------------------------------------------------------------------ // Flush pending agent updates side effect // ------------------------------------------------------------------ sideEffects.push({ type: "flush_pending_updates" }); // ------------------------------------------------------------------ // Init resolution // ------------------------------------------------------------------ const shouldResolveDeferredInit = shouldResolveTimelineInit({ hasActiveInitDeferred, hasNewer: payload.hasNewer, isInitializing, initRequestDirection, responseDirection: payload.direction, reset: payload.reset, }); const timelineResponseComplete = payload.direction !== "after" || !payload.hasNewer; const clearInitializing = (shouldResolveDeferredInit || (isInitializing && !hasActiveInitDeferred)) && timelineResponseComplete; const initResolution: "resolve" | "reject" | null = shouldResolveDeferredInit ? "resolve" : null; return { tail: nextTail, head: nextHead, cursor: nextCursor, cursorChanged, initResolution, clearInitializing, error: null, sideEffects, }; } // --------------------------------------------------------------------------- // processAgentStreamEvent // --------------------------------------------------------------------------- export interface ProcessAgentStreamEventInput { event: AgentStreamEventPayload; seq: number | undefined; epoch: string | undefined; currentTail: StreamItem[]; currentHead: StreamItem[]; currentCursor: TimelineCursor | undefined; currentAgent: { status: AgentLifecycleStatus; updatedAt: Date; lastActivityAt: Date; } | null; timestamp: Date; } export interface AgentPatch { status: AgentLifecycleStatus; updatedAt: Date; lastActivityAt: Date; } export interface ProcessAgentStreamEventOutput { tail: StreamItem[]; head: StreamItem[]; changedTail: boolean; changedHead: boolean; cursor: TimelineCursor | null; cursorChanged: boolean; agent: AgentPatch | null; agentChanged: boolean; sideEffects: AgentStreamReducerSideEffect[]; } export interface AgentStreamReducerEvent { event: AgentStreamEventPayload; seq: number | undefined; epoch: string | undefined; timestamp: Date; } interface TimelineSequencingGateResult { shouldApplyStreamEvent: boolean; nextTimelineCursor: TimelineCursor | null; cursorChanged: boolean; resetLiveTimeline: boolean; sideEffects: AgentStreamReducerSideEffect[]; } export interface AgentStreamReducerAgentSnapshot { status: AgentLifecycleStatus; updatedAt: Date; lastActivityAt: Date; } export interface ProcessAgentStreamEventsInput { events: AgentStreamReducerEvent[]; currentTail: StreamItem[]; currentHead: StreamItem[]; currentCursor: TimelineCursor | undefined; currentAgent: AgentStreamReducerAgentSnapshot | null; } export type AgentStreamReducerSnapshot = Omit; export interface AgentStreamReducerQueue { enqueue: (agentId: string, event: AgentStreamReducerEvent) => void; flush: () => void; flushAgent: (agentId: string) => void; dispose: (options?: { flush?: boolean }) => void; } export interface CreateAgentStreamReducerQueueInput { getSnapshot: (agentId: string) => AgentStreamReducerSnapshot; commit: ( agentId: string, result: ProcessAgentStreamEventOutput, events: AgentStreamReducerEvent[], ) => void; handleSideEffects: (agentId: string, sideEffects: AgentStreamReducerSideEffect[]) => void; scheduleFlush: (callback: () => void) => number; cancelFlush: (id: number) => void; } function applyAgentPatch( currentAgent: AgentStreamReducerAgentSnapshot | null, patch: AgentPatch | null, ): AgentStreamReducerAgentSnapshot | null { if (!currentAgent || !patch) { return currentAgent; } return { status: patch.status, updatedAt: patch.updatedAt, lastActivityAt: patch.lastActivityAt, }; } function processTimelineSequencingGate(input: { event: AgentStreamEventPayload; seq: number | undefined; epoch: string | undefined; currentCursor: TimelineCursor | undefined; }): TimelineSequencingGateResult { const { event, seq, epoch, currentCursor } = input; const base: TimelineSequencingGateResult = { shouldApplyStreamEvent: true, nextTimelineCursor: null, cursorChanged: false, resetLiveTimeline: false, sideEffects: [], }; if (event.type !== "timeline" || typeof seq !== "number" || typeof epoch !== "string") { return base; } const decision = classifySessionTimelineSeq({ cursor: currentCursor ? { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq } : null, epoch, seq, }); if (decision === "init") { return { ...base, nextTimelineCursor: { epoch, startSeq: seq, endSeq: seq }, cursorChanged: true, }; } if (decision === "accept") { return { ...base, nextTimelineCursor: { ...(currentCursor ?? { epoch, startSeq: seq, endSeq: seq }), epoch, endSeq: seq, }, cursorChanged: true, }; } if (decision === "gap") { return { ...base, shouldApplyStreamEvent: false, sideEffects: currentCursor ? [ { type: "catch_up", cursor: { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq }, }, ] : [], }; } if (decision === "drop_epoch" && seq === 1) { return { ...base, nextTimelineCursor: { epoch, startSeq: seq, endSeq: seq }, cursorChanged: true, resetLiveTimeline: true, }; } return { ...base, shouldApplyStreamEvent: false, }; } export function processAgentStreamEvent( input: ProcessAgentStreamEventInput, ): ProcessAgentStreamEventOutput { const { event, seq, epoch, currentTail, currentHead, currentCursor, currentAgent, timestamp } = input; const sequencing = processTimelineSequencingGate({ event, seq, epoch, currentCursor }); const timelineCursor = event.type === "timeline" && seq !== undefined && epoch !== undefined ? { epoch, seq } : undefined; // ------------------------------------------------------------------ // Apply stream event to tail/head // ------------------------------------------------------------------ const { tail, head, changedTail, changedHead } = sequencing.shouldApplyStreamEvent ? applyStreamEvent({ tail: sequencing.resetLiveTimeline ? [] : currentTail, head: sequencing.resetLiveTimeline ? [] : currentHead, event, timestamp, source: "live", timelineCursor, }) : { tail: currentTail, head: currentHead, changedTail: false, changedHead: false, }; // ------------------------------------------------------------------ // Optimistic lifecycle status // ------------------------------------------------------------------ let agentPatch: AgentPatch | null = null; let agentChanged = false; if ( currentAgent && (event.type === "turn_completed" || event.type === "turn_canceled" || event.type === "turn_failed") ) { const optimisticStatus = deriveOptimisticLifecycleStatus(currentAgent.status, event); if (optimisticStatus) { const nextUpdatedAtMs = Math.max(currentAgent.updatedAt.getTime(), timestamp.getTime()); const nextLastActivityAtMs = Math.max( currentAgent.lastActivityAt.getTime(), timestamp.getTime(), ); agentPatch = { status: optimisticStatus, updatedAt: new Date(nextUpdatedAtMs), lastActivityAt: new Date(nextLastActivityAtMs), }; agentChanged = true; } } return { tail, head, changedTail, changedHead, cursor: sequencing.nextTimelineCursor, cursorChanged: sequencing.cursorChanged, agent: agentPatch, agentChanged, sideEffects: sequencing.sideEffects, }; } export function processAgentStreamEvents( input: ProcessAgentStreamEventsInput, ): ProcessAgentStreamEventOutput { let tail = input.currentTail; let head = input.currentHead; let cursor = input.currentCursor; let agent = input.currentAgent; let changedTail = false; let changedHead = false; let cursorChanged = false; let agentPatch: AgentPatch | null = null; let agentChanged = false; const sideEffects: AgentStreamReducerSideEffect[] = []; for (const reducerEvent of input.events) { const result = processAgentStreamEvent({ event: reducerEvent.event, seq: reducerEvent.seq, epoch: reducerEvent.epoch, currentTail: tail, currentHead: head, currentCursor: cursor, currentAgent: agent, timestamp: reducerEvent.timestamp, }); tail = result.tail; head = result.head; changedTail = changedTail || result.changedTail; changedHead = changedHead || result.changedHead; sideEffects.push(...result.sideEffects); if (result.cursorChanged) { cursor = result.cursor ?? undefined; cursorChanged = true; } if (result.agentChanged) { agentPatch = result.agent; agentChanged = true; agent = applyAgentPatch(agent, result.agent); } } return { tail, head, changedTail, changedHead, cursor: cursor ?? null, cursorChanged, agent: agentPatch, agentChanged, sideEffects, }; } export function createAgentStreamReducerQueue( input: CreateAgentStreamReducerQueueInput, ): AgentStreamReducerQueue { const pendingByAgentId = new Map(); let scheduledFlushId: number | null = null; const cancelScheduledFlush = () => { if (scheduledFlushId === null) { return; } input.cancelFlush(scheduledFlushId); scheduledFlushId = null; }; const flushAgent = (agentId: string) => { const events = pendingByAgentId.get(agentId); if (!events || events.length === 0) { return; } pendingByAgentId.delete(agentId); if (pendingByAgentId.size === 0) { cancelScheduledFlush(); } const result = processAgentStreamEvents({ events, ...input.getSnapshot(agentId), }); input.commit(agentId, result, events); if (result.sideEffects.length > 0) { input.handleSideEffects(agentId, result.sideEffects); } }; const flush = () => { const agentIds = Array.from(pendingByAgentId.keys()); for (const agentId of agentIds) { flushAgent(agentId); } }; const scheduleFlush = () => { if (scheduledFlushId !== null) { return; } scheduledFlushId = input.scheduleFlush(() => { scheduledFlushId = null; flush(); }); }; return { enqueue(agentId, event) { const pending = pendingByAgentId.get(agentId); if (pending) { pending.push(event); } else { pendingByAgentId.set(agentId, [event]); } scheduleFlush(); }, flush, flushAgent, dispose(options) { cancelScheduledFlush(); if (options?.flush) { flush(); } else { pendingByAgentId.clear(); } }, }; } interface StreamStatePatch { tail?: StreamItem[]; head?: StreamItem[]; } export interface CreateSessionAgentStreamReducerQueueInput { serverId: string; setAgentStreamState: (serverId: string, agentId: string, state: StreamStatePatch) => void; setAgentTimelineCursor: ( serverId: string, state: (prev: Map) => Map, ) => void; setAgents: (serverId: string, state: (prev: Map) => Map) => void; recoverTimelineGap: (agentId: string, cursor: { epoch: string; endSeq: number }) => void; } function scheduleAgentStreamReducerFlush(callback: () => void): number { return setTimeout(callback, AGENT_STREAM_REDUCER_FLUSH_DELAY_MS) as unknown as number; } function cancelAgentStreamReducerFlush(id: number) { clearTimeout(id); } export function createSessionAgentStreamReducerQueue( input: CreateSessionAgentStreamReducerQueueInput, ): AgentStreamReducerQueue { const { serverId, setAgentStreamState, setAgentTimelineCursor, setAgents, recoverTimelineGap } = input; return createAgentStreamReducerQueue({ getSnapshot: (agentId) => { const session = useSessionStore.getState().sessions[serverId]; const currentAgentEntry = session?.agents.get(agentId); return { currentTail: session?.agentStreamTail.get(agentId) ?? [], currentHead: session?.agentStreamHead.get(agentId) ?? [], currentCursor: session?.agentTimelineCursor.get(agentId), currentAgent: currentAgentEntry ? { status: currentAgentEntry.status, updatedAt: currentAgentEntry.updatedAt, lastActivityAt: currentAgentEntry.lastActivityAt, } : null, }; }, commit: (agentId, result, events) => { if (result.changedTail || result.changedHead) { setAgentStreamState(serverId, agentId, { ...(result.changedTail ? { tail: result.tail } : {}), ...(result.changedHead ? { head: result.head } : {}), }); } if (result.cursorChanged && result.cursor) { const nextCursor = result.cursor; const lastEvent = events.at(-1); setAgentTimelineCursor(serverId, (prev) => { const current = prev.get(agentId); if ( current && lastEvent && typeof lastEvent.seq === "number" && typeof lastEvent.epoch === "string" && current.epoch === lastEvent.epoch && lastEvent.seq >= current.startSeq && lastEvent.seq <= current.endSeq ) { return prev; } if ( current && current.epoch === nextCursor.epoch && current.startSeq === nextCursor.startSeq && current.endSeq === nextCursor.endSeq ) { return prev; } const next = new Map(prev); next.set(agentId, nextCursor); return next; }); } if (result.agentChanged && result.agent) { const nextAgent = result.agent; setAgents(serverId, (prev) => { const current = prev.get(agentId); if (!current) { return prev; } const next = new Map(prev); next.set(agentId, { ...current, status: nextAgent.status, updatedAt: nextAgent.updatedAt, lastActivityAt: nextAgent.lastActivityAt, }); return next; }); } }, handleSideEffects: (agentId, sideEffects) => { for (const effect of sideEffects) { if (effect.type === "catch_up") { recoverTimelineGap(agentId, effect.cursor); } } }, scheduleFlush: scheduleAgentStreamReducerFlush, cancelFlush: cancelAgentStreamReducerFlush, }); }