| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- /**
- * Surface layer on top of the session event log: an ordered view of events
- * that produce LLM messages. The append-only log remains the source of truth.
- *
- * Browser-safe: web clients consume this subpath export, so it must stay free
- * of `node:` imports (they break the vite bundle).
- *
- * @module @deepseek-ai/dsh-session/surface
- */
- import type { SessionEvent, SurfaceEvent, SurfaceEventType, SurfaceOp } from './types.ts'
- /** Runtime counterpart of the message-producing event union. */
- const SURFACE_EVENT_TYPES = new Set<string>([
- 'user/message',
- 'assistant/message',
- 'tool/result',
- 'steering/message',
- ])
- /**
- * Whether an event type can join the model-visible surface.
- * @param type - event type to test.
- * @returns true for one of the four message-producing event types.
- */
- export function isSurfaceEligibleType(type: string): boolean {
- return SURFACE_EVENT_TYPES.has(type)
- }
- /**
- * Narrow an event to a surface-eligible event carrying its required marker.
- * @param event - event to test.
- * @returns true when both the type and marker identify a surface event.
- */
- export function isSurfaceEvent(event: SessionEvent): event is SurfaceEvent {
- if (!SURFACE_EVENT_TYPES.has(event.type)) return false
- return (event as SessionEvent<SurfaceEventType>).surfaceOp !== undefined
- }
- /**
- * Narrow an event to an append-origin surface event: one that entered the
- * surface at its own log position and was never itself a replacement copy.
- *
- * The model-visible surface deliberately shadows replaced ranges, so it is the
- * wrong source for a human transcript — a landed replacement would erase
- * conversation the user already saw. Append-origin events are that transcript's
- * durable source material; replacement copies stay model-only.
- * @param event - event to test.
- * @returns true when the event appended to the surface tail.
- */
- export function isAppendSurfaceEvent(
- event: SessionEvent,
- ): event is SurfaceEvent & { surfaceOp: 'append' } {
- return isSurfaceEvent(event) && event.surfaceOp === 'append'
- }
- /**
- * Narrow an event to a surface replacement: a node that shadowed an existing
- * surface range instead of appending to the tail. The counterpart of
- * {@link isAppendSurfaceEvent} over the two {@link SurfaceOp} variants.
- * @param event - event to test.
- * @returns true when the event replaced a surface range.
- */
- export function isReplacementSurfaceEvent(
- event: SessionEvent,
- ): event is SurfaceEvent & { surfaceOp: Extract<SurfaceOp, { op: 'replace' }> } {
- return isSurfaceEvent(event) && event.surfaceOp !== 'append'
- }
- /** One replacement operation observed while folding a session surface. */
- export interface SurfaceFoldReplacement {
- /** Seq of the event that replaced the prior surface range. */
- seq: number
- /** Declared inclusive start seq of the replaced surface range. */
- start: number
- /** Declared inclusive end seq of the replaced surface range. */
- end: number
- /** Actual surface entries removed by the operation, in surface order. */
- shadowedSeqs: number[]
- }
- /** Complete result of replaying the surface operations in a session log. */
- export interface SurfaceFoldResult {
- /** Current surface event sequences in model-visible order. */
- nodes: number[]
- /** Replacement operations in event order. */
- replacements: SurfaceFoldReplacement[]
- }
- /** Readonly live projection of the message-producing session events. */
- export interface SessionSurface {
- /** Current surface event sequences in model-visible order. */
- readonly nodes: readonly number[]
- /** Monotonic count of committed positional replacements. */
- readonly replaceGeneration: number
- }
- /** Mutable state shared by complete and incremental folds. */
- interface SurfaceFoldState {
- nodes: number[]
- replaceGeneration: number
- }
- /** A validated replacement transition that has not mutated fold state yet. */
- interface SurfaceReplacePlan extends SurfaceFoldReplacement {
- kind: 'replace'
- startIdx: number
- endIdx: number
- }
- /** One validated surface transition that has not mutated fold state yet. */
- type SurfacePlan =
- | { kind: 'append'; seq: number }
- | SurfaceReplacePlan
- /** Create an empty surface fold state. */
- function createFoldState(): SurfaceFoldState {
- return { nodes: [], replaceGeneration: 0 }
- }
- /** Whether a runtime value is a non-negative safe event sequence. */
- function isEventSeq(value: unknown): value is number {
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
- }
- /** Whether a runtime value is the exact positional-replacement shape. */
- function isReplaceOp(value: object): value is Extract<SurfaceOp, { op: 'replace' }> {
- const op = value as Record<string, unknown>
- return Object.keys(op).length === 3
- && Object.hasOwn(op, 'op')
- && Object.hasOwn(op, 'start')
- && Object.hasOwn(op, 'end')
- && op['op'] === 'replace'
- && isEventSeq(op['start'])
- && isEventSeq(op['end'])
- }
- /** Validate event-local surface eligibility and return its operation. */
- function surfaceOpOf(event: SessionEvent): SurfaceOp | undefined {
- const raw = event as SessionEvent & { surfaceOp?: unknown; sourceEventSeqs?: unknown }
- if (!isSurfaceEligibleType(event.type)) {
- if (raw.surfaceOp !== undefined) {
- throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`)
- }
- if (raw.sourceEventSeqs !== undefined) {
- throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`)
- }
- return
- }
- const op = raw.surfaceOp
- if (op === undefined) {
- throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`)
- }
- if (op === 'append') return op
- if (op === null || typeof op !== 'object' || Array.isArray(op)) {
- throw new Error(`session event "${event.type}" carries an invalid surfaceOp`)
- }
- if (!isReplaceOp(op)) {
- throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`)
- }
- return op
- }
- /** Validate provenance against prior log entries and the replacement range. */
- function assertProvenance(
- event: SessionEvent,
- shadowedSeqs: readonly number[],
- ): void {
- const raw = (event as SessionEvent & { sourceEventSeqs?: unknown }).sourceEventSeqs
- const sources = new Set<number>()
- if (raw !== undefined) {
- if (!Array.isArray(raw)) {
- throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`)
- }
- if (raw.length === 0 && event.type !== 'assistant/message') {
- throw new Error('sourceEventSeqs must not be empty except on assistant/message')
- }
- let nonEarlierSource: number | undefined
- for (const source of raw) {
- if (!isEventSeq(source)) {
- throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`)
- }
- sources.add(source)
- if (nonEarlierSource === undefined && source >= event.seq) nonEarlierSource = source
- }
- if (sources.size !== raw.length) {
- throw new Error('sourceEventSeqs must not contain duplicates')
- }
- if (nonEarlierSource !== undefined) {
- throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`)
- }
- }
- const missing = shadowedSeqs.filter(seq => !sources.has(seq))
- if (missing.length > 0) {
- throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`)
- }
- }
- /** Locate one replacement range without mutating the current fold state. */
- function replacementRange(
- state: SurfaceFoldState,
- op: Extract<SurfaceOp, { op: 'replace' }>,
- ): Pick<SurfaceReplacePlan, 'startIdx' | 'endIdx' | 'shadowedSeqs'> {
- const startIdx = state.nodes.indexOf(op.start)
- if (startIdx === -1) {
- throw new Error(`surface replace: start seq ${op.start} not found in surface`)
- }
- const endIdx = state.nodes.indexOf(op.end)
- if (endIdx === -1) {
- throw new Error(`surface replace: end seq ${op.end} not found in surface`)
- }
- if (startIdx > endIdx) {
- throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`)
- }
- return {
- startIdx,
- endIdx,
- shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1),
- }
- }
- /**
- * Deep structural equality over the session-event JSON value domain
- * (null/boolean/number/string, arrays, plain objects). Replaces
- * `node:util`'s isDeepStrictEqual to keep this module browser-safe.
- */
- function isDeepEqualJson(a: unknown, b: unknown): boolean {
- if (a === b) return true
- if (Array.isArray(a) || Array.isArray(b)) {
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
- return a.every((item, i) => isDeepEqualJson(item, b[i]))
- }
- if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
- const aKeys = Object.keys(a)
- const bRecord = b as Record<string, unknown>
- if (aKeys.length !== Object.keys(b).length) return false
- return aKeys.every(key => Object.hasOwn(b, key) && isDeepEqualJson((a as Record<string, unknown>)[key], bRecord[key]))
- }
- /** Restrict a tool-result replacement to one current result's content. */
- function assertToolResultRewrite(
- event: SessionEvent,
- shadowedSeqs: readonly number[],
- events: readonly SessionEvent[],
- ): void {
- if (event.type !== 'tool/result') return
- if (shadowedSeqs.length !== 1) {
- throw new Error('tool/result surface replacement must rewrite exactly one current node')
- }
- for (const originalSeq of shadowedSeqs) {
- const original = events[originalSeq]
- if (original?.type !== 'tool/result') {
- throw new Error('tool/result surface replacement must target a current tool/result')
- }
- const originalRest = { ...original.data } as Record<string, unknown>
- const replacementRest = { ...event.data } as Record<string, unknown>
- const originalResult = original.data.message.content[0]
- const replacementResult = event.data.message.content[0]
- originalRest['message'] = {
- ...original.data.message,
- content: [{ ...originalResult, content: null }],
- }
- replacementRest['message'] = {
- ...event.data.message,
- content: [{ ...replacementResult, content: null }],
- }
- if (!isDeepEqualJson(originalRest, replacementRest)) {
- throw new Error('tool/result surface replacement may change only content')
- }
- }
- }
- /** Validate one event at its replay boundary and prepare its atomic fold transition. */
- function planSurfaceEvent(
- state: SurfaceFoldState,
- event: SessionEvent,
- expectedSeq: number,
- events: readonly SessionEvent[],
- ): SurfacePlan | undefined {
- if (event.seq !== expectedSeq) {
- throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`)
- }
- const surfaceOp = surfaceOpOf(event)
- if (surfaceOp === undefined) return
- if (surfaceOp === 'append') {
- assertProvenance(event, [])
- return { kind: 'append', seq: event.seq }
- }
- const range = replacementRange(state, surfaceOp)
- assertProvenance(event, range.shadowedSeqs)
- assertToolResultRewrite(event, range.shadowedSeqs, events)
- return {
- kind: 'replace',
- seq: event.seq,
- start: surfaceOp.start,
- end: surfaceOp.end,
- ...range,
- }
- }
- /** Apply one event and return replacement metadata only when one occurred. */
- function applySurfaceEvent(
- state: SurfaceFoldState,
- event: SessionEvent,
- expectedSeq: number,
- events: readonly SessionEvent[],
- ): SurfaceFoldReplacement | undefined {
- const plan = planSurfaceEvent(state, event, expectedSeq, events)
- if (plan?.kind === 'append') {
- state.nodes.push(plan.seq)
- } else if (plan?.kind === 'replace') {
- state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq)
- state.replaceGeneration += 1
- }
- if (plan?.kind !== 'replace') return
- return {
- seq: plan.seq,
- start: plan.start,
- end: plan.end,
- shadowedSeqs: plan.shadowedSeqs,
- }
- }
- /**
- * Replay a complete session log through the canonical surface fold.
- * @param events - session events in contiguous seq order.
- * @returns detached current sequences and replacement history.
- * @throws when an event violates surface metadata, provenance, range, or tool-result rewrite rules.
- */
- export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult {
- const state = createFoldState()
- const replacements: SurfaceFoldReplacement[] = []
- for (const [index, event] of events.entries()) {
- const replacement = applySurfaceEvent(state, event, index, events)
- if (replacement !== undefined) replacements.push(replacement)
- }
- return { nodes: [...state.nodes], replacements }
- }
- /** Incremental ordered surface view and append-boundary validator. */
- export class SurfaceManager implements SessionSurface {
- /** Shared transition state; replacement history is not retained. */
- private _state = createFoldState()
- /** Last processed seq; -1 folds a seeded log on first access. */
- private _lastProcessedSeq = -1
- constructor(private log: readonly SessionEvent[]) {}
- /**
- * Validate the next candidate without mutating the committed surface.
- * @param event - candidate event that has not entered the log yet.
- */
- validateNext(event: SessionEvent): void {
- if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
- planSurfaceEvent(this._state, event, this.log.length, this.log)
- }
- /** Monotonic count of folded positional replacements. */
- get replaceGeneration(): number {
- if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
- return this._state.replaceGeneration
- }
- /** Surface event sequences in model-visible order. */
- get nodes(): readonly number[] {
- if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
- return this._state.nodes
- }
- /** Fold events appended since the previous access. */
- private _processDelta(): void {
- for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) {
- // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
- applySurfaceEvent(this._state, this.log[i]!, i, this.log)
- this._lastProcessedSeq = i
- }
- }
- }
|