feat(session)!: embed assistant streams in format v2

This commit is contained in:
Tianyi Cui
2026-09-02 04:00:01 +08:00
parent 0bb7bba015
commit f99b06eaed
387 changed files with 9491 additions and 4625 deletions
@@ -1,31 +1,30 @@
/** Process-local assistant state retained for reconnecting Web followers. */
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type {
SessionAssistantStreamAttempt,
SessionAssistantStreamBaseline,
} from './types.ts'
type ChunkFrame = Extract<AssistantStreamFrame, { type: 'chunk' }>
interface MutableAttempt {
readonly attemptId: SessionAssistantStreamAttempt['attemptId']
readonly startedTime: number
readonly turn: number
readonly step: number
readonly chunks: ChunkFrame[]
readonly legacyChunkSeqs: number[]
readonly stream: AssistantStreamAccumulator
nextIndex: number
}
const EMPTY_BASELINE: SessionAssistantStreamBaseline = { revision: 0, attempts: [] }
const EMPTY_BASELINE: SessionAssistantStreamBaseline = { revision: 0 }
/**
* Folds dense Agent frames and materializes one shared immutable reconnect
* baseline per accepted revision.
*/
export class SessionAssistantStreamAccumulator {
private readonly attempts = new Map<string, MutableAttempt>()
private activeAttempt: MutableAttempt | undefined
private revision = 0
private snapshotValue: SessionAssistantStreamBaseline = EMPTY_BASELINE
private dirty = false
@@ -36,11 +35,11 @@ export class SessionAssistantStreamAccumulator {
*/
accept(frame: AssistantStreamFrame): void {
if (frame.type === 'start' && frame.revision === 1 && this.revision !== 0) {
this.attempts.clear()
this.activeAttempt = undefined
this.revision = 0
}
if (frame.revision !== this.revision + 1) {
this.attempts.clear()
this.activeAttempt = undefined
this.revision = frame.revision
this.dirty = true
return
@@ -48,27 +47,29 @@ export class SessionAssistantStreamAccumulator {
this.revision = frame.revision
switch (frame.type) {
case 'start':
this.attempts.set(String(frame.attemptId), {
this.activeAttempt = {
attemptId: frame.attemptId,
startedTime: frame.startedTime,
turn: frame.turn,
step: frame.step,
chunks: [],
legacyChunkSeqs: [],
})
stream: new AssistantStreamAccumulator(),
nextIndex: 0,
}
break
case 'chunk': {
const attempt = this.attempts.get(String(frame.attemptId))
if (attempt === undefined || frame.index !== attempt.chunks.length) {
this.attempts.clear()
const attempt = this.activeAttempt
if (attempt === undefined
|| attempt.attemptId !== frame.attemptId
|| frame.index !== attempt.nextIndex) {
this.activeAttempt = undefined
break
}
attempt.chunks.push(frame)
attempt.legacyChunkSeqs.push(frame.legacyChunkSeq)
attempt.stream.push({ time: frame.time, chunk: frame.chunk })
attempt.nextIndex += 1
break
}
case 'end':
this.attempts.delete(String(frame.attemptId))
this.activeAttempt = undefined
break
}
this.dirty = true
@@ -82,14 +83,16 @@ export class SessionAssistantStreamAccumulator {
if (!this.dirty) return this.snapshotValue
this.snapshotValue = {
revision: this.revision,
attempts: [...this.attempts.values()].map(attempt => ({
attemptId: attempt.attemptId,
startedTime: attempt.startedTime,
turn: attempt.turn,
step: attempt.step,
chunks: attempt.chunks.map(frame => frame.chunk as JsonValue),
legacyChunkSeqs: [...attempt.legacyChunkSeqs],
})),
...this.activeAttempt === undefined ? {} : {
activeAttempt: {
attemptId: this.activeAttempt.attemptId,
startedTime: this.activeAttempt.startedTime,
turn: this.activeAttempt.turn,
step: this.activeAttempt.step,
nextIndex: this.activeAttempt.nextIndex,
stream: this.activeAttempt.stream.snapshot() as unknown as readonly JsonValue[],
},
},
}
this.dirty = false
return this.snapshotValue
@@ -1,18 +1,33 @@
/** Observable contiguous Session event window consumed by domain assemblers. */
import { notifySubscribers, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { LlmAttemptId, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type { ChunkRowEvent } from '../../types.ts'
/** Standard Session event or compact historical Assistant run. */
export type SessionEventLike = SessionEvent | ChunkRowEvent
/** Client-only live chunk presentation; `seq` orders the transient row between durable Session seqs. */
export interface AssistantLiveChunkEvent {
readonly type: 'assistant/live-chunk'
readonly seq: number
readonly time: number
readonly data: {
readonly attemptId: LlmAttemptId
readonly turn: number
readonly step: number
readonly chunk: StreamChunk
}
}
/** Current durable Session event or one client-only live chunk presentation. */
export type SessionEventLike = SessionEvent | AssistantLiveChunkEvent
/** Client history entry retaining its coarse transport discriminator. */
export type SessionEventLikeEntry =
| { readonly type: 'event'; readonly event: SessionEvent }
| { readonly type: 'chunks'; readonly event: ChunkRowEvent }
| { readonly type: 'transient'; readonly event: AssistantLiveChunkEvent }
/** Scalar live entry accepted by append-only Client paths. */
export type SessionLiveEventEntry = Extract<SessionEventLikeEntry, { readonly type: 'event' }>
/** Client-only Assistant frame admitted outside durable cursor algebra. */
export type SessionTransientEventEntry = Extract<SessionEventLikeEntry, { readonly type: 'transient' }>
interface EventWindowLeaf {
readonly kind: 'leaf'
@@ -78,7 +93,7 @@ function windowSnapshot(
export type SessionEventChange =
| { readonly kind: 'replace'; readonly entries: readonly SessionEventLikeEntry[] }
| { readonly kind: 'prepend'; readonly entries: readonly SessionEventLikeEntry[] }
| { readonly kind: 'append'; readonly entries: readonly SessionLiveEventEntry[] }
| { readonly kind: 'append'; readonly entries: readonly SessionEventLikeEntry[] }
/** Current contiguous event window and its latest synchronous delta. */
export interface SessionEventWindow {
@@ -139,7 +154,7 @@ export class MutableSessionEventSource implements SessionEventSource {
* Append one contiguous live entry.
* @param entry - live tail entry.
*/
append(entry: SessionLiveEventEntry): void {
append(entry: SessionEventLikeEntry): void {
const entries = [entry]
this.window = concat(this.window, leaf(entries))
this.publish(this.snapshot.hasMore, {
@@ -1,154 +1,191 @@
/** Web presentation fold joining durable v1 events with transient assistant frames. */
/** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
import type {
SessionAssistantStreamBaseline,
SessionAssistantStreamFrame,
} from '../../types.ts'
import { expandAssistantStream } from '@deepseek-ai/dsh-llm/assistant-stream'
import type { AssistantStreamRecord } from '@deepseek-ai/dsh-llm/assistant-stream'
import type {
SessionEventLikeEntry,
SessionLiveEventEntry,
SessionTransientEventEntry,
} from '../contract/events.ts'
interface ActiveAttempt {
readonly attemptId: string
readonly startedTime: number
readonly turn: number
readonly step: number
readonly legacyChunkSeqs: Set<number>
nextIndex: number
}
/** One Web publication decision from the assistant stream fold. */
export type ClientAssistantStreamResult =
| { readonly type: 'publish'; readonly entry: SessionLiveEventEntry }
| { readonly type: 'transient'; readonly entry: SessionTransientEventEntry }
| { readonly type: 'rebaseline' }
| undefined
function positionKey(turn: number, step: number): string {
return `${String(turn)}:${String(step)}`
}
function sameSeqs(left: readonly number[], right: readonly number[]): boolean {
return left.length === right.length && left.every((seq, index) => seq === right[index])
}
/**
* Keeps transient Assistant presentation behind one small interface. Durable
* chunks and final messages publish only at their matching live frame.
*/
/** Keeps transient Assistant presentation behind one settlement-aware interface. */
export class ClientAssistantStream {
private readonly attempts = new Map<string, ActiveAttempt>()
private readonly pendingChunks = new Map<number, SessionLiveEventEntry>()
private readonly pendingMessages = new Map<string, SessionLiveEventEntry>()
private activeAttempt: ActiveAttempt | undefined
private readonly pending = new Map<number, SessionLiveEventEntry>()
private publishedSeqs = new Set<number>()
private durableCursor = -1
private transientInGap = 0
/**
* Replace the durable Web window and adopt an optional reconnect baseline.
* @param entries - complete event window from the journal replacement.
* @param baseline - active process-local attempts for a follow opening.
* @returns the same durable window; baseline seqs suppress later duplicate live appends.
* @param entries - durable entries in the replacement window.
* @param baseline - compact prefix for an Assistant attempt that is still live.
* @returns immediately visible durable and reconstructed transient entries, with an active settlement withheld.
*/
replace(
entries: readonly SessionEventLikeEntry[],
baseline?: SessionAssistantStreamBaseline,
): readonly SessionEventLikeEntry[] {
this.pendingChunks.clear()
this.pendingMessages.clear()
this.attempts.clear()
if (baseline !== undefined) {
for (const attempt of baseline.attempts) {
this.attempts.set(String(attempt.attemptId), {
startedTime: attempt.startedTime,
turn: attempt.turn,
step: attempt.step,
legacyChunkSeqs: new Set(attempt.legacyChunkSeqs),
nextIndex: attempt.chunks.length,
})
this.pending.clear()
this.transientInGap = 0
this.activeAttempt = undefined
const opening = baseline?.activeAttempt
if (opening !== undefined) {
this.activeAttempt = {
attemptId: String(opening.attemptId),
startedTime: opening.startedTime,
turn: opening.turn,
step: opening.step,
nextIndex: opening.nextIndex,
}
}
const visible = entries
const pending = opening === undefined
? undefined
: entries.findLast(entry => entry.type === 'event'
&& this.attemptForSettlement(entry.event) !== undefined)
if (pending?.type === 'event') this.pending.set(pending.event.seq, pending)
const visible: SessionEventLikeEntry[] = pending === undefined
? [...entries]
: entries.filter(entry => entry !== pending)
this.publishedSeqs = new Set(visible.map(entry => entry.event.seq))
this.durableCursor = visible.reduce((cursor, entry) => Math.max(cursor, entry.event.seq), -1)
if (opening !== undefined) {
for (const [index, member] of expandAssistantStream(
opening.stream as unknown as readonly AssistantStreamRecord[],
).entries()) {
this.transientInGap += 1
visible.push({
type: 'transient',
event: {
type: 'assistant/live-chunk',
seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
time: member.time,
data: {
attemptId: opening.attemptId,
turn: opening.turn,
step: opening.step,
chunk: member.chunk,
},
},
})
if (index + 1 >= opening.nextIndex) break
}
}
return visible
}
/**
* Stage one durable tail event when an active attempt owns its publication.
* @param entry - next cursor-validated durable event.
* @returns the entry for immediate publication, or undefined while staged.
* Stage one durable v2 settlement while its matching live attempt is open.
* @param entry - newly followed durable entry.
* @returns a publication decision, or `undefined` when no entry becomes visible.
*/
acceptDurable(entry: SessionLiveEventEntry): ClientAssistantStreamResult {
const event = entry.event
if (event.type === 'assistant/chunk') {
const attempt = this.attemptFor(event.data.turn, event.data.step)
if (attempt === undefined) return this.publish(entry)
if (attempt.legacyChunkSeqs.has(event.seq)) return this.publish(entry)
this.pendingChunks.set(event.seq, entry)
return undefined
}
if (event.type === 'assistant/message') {
if (event.surfaceOp !== 'append') return this.publish(entry)
const attempt = this.attemptFor(event.data.turn, event.data.step)
if (attempt === undefined) return this.publish(entry)
this.pendingMessages.set(positionKey(event.data.turn, event.data.step), entry)
this.durableCursor = Math.max(this.durableCursor, event.seq)
this.transientInGap = 0
if (this.attemptForSettlement(event) !== undefined) {
this.pending.set(event.seq, entry)
return undefined
}
return this.publish(entry)
}
/**
* Fold one validated transient frame and release its matching durable event.
* @param frame - next dense process-local frame.
* @returns one durable event whose Web publication commits at this frame.
* Fold one dense transient frame and release its named durable settlement.
* @param frame - next Assistant stream frame received by the follow connection.
* @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
*/
acceptFrame(frame: SessionAssistantStreamFrame): ClientAssistantStreamResult {
switch (frame.type) {
case 'start':
this.attempts.set(String(frame.attemptId), {
this.pending.clear()
this.activeAttempt = {
attemptId: String(frame.attemptId),
startedTime: frame.startedTime,
turn: frame.turn,
step: frame.step,
legacyChunkSeqs: new Set(),
nextIndex: 0,
})
}
return undefined
case 'chunk': {
const attempt = this.attempts.get(String(frame.attemptId))
if (attempt === undefined || frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
const attempt = this.activeAttempt
if (attempt === undefined
|| attempt.attemptId !== String(frame.attemptId)
|| frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
attempt.nextIndex += 1
attempt.legacyChunkSeqs.add(frame.legacyChunkSeq)
if (this.publishedSeqs.has(frame.legacyChunkSeq)) return undefined
const entry = this.pendingChunks.get(frame.legacyChunkSeq)
if (entry === undefined) return { type: 'rebaseline' }
this.pendingChunks.delete(frame.legacyChunkSeq)
return this.publish(entry)
this.transientInGap += 1
return {
type: 'transient',
entry: {
type: 'transient',
event: {
type: 'assistant/live-chunk',
seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
time: frame.time,
data: {
attemptId: frame.attemptId,
turn: attempt.turn,
step: attempt.step,
chunk: frame.chunk as never,
},
},
},
}
}
case 'end': {
const attempt = this.attempts.get(String(frame.attemptId))
this.attempts.delete(String(frame.attemptId))
if (attempt === undefined) return { type: 'rebaseline' }
const attempt = this.activeAttempt
this.activeAttempt = undefined
if (attempt === undefined || attempt.attemptId !== String(frame.attemptId)) {
return { type: 'rebaseline' }
}
if (frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
if (!sameSeqs([...attempt.legacyChunkSeqs], frame.legacyChunkSeqs)) {
if (frame.outcome.kind === 'abandoned') {
return this.pending.size === 0 ? undefined : { type: 'rebaseline' }
}
if (this.publishedSeqs.has(frame.outcome.seq)) return undefined
const entry = this.pending.get(frame.outcome.seq)
if (entry === undefined
|| entry.event.type !== frame.outcome.eventType
|| entry.event.data.turn !== attempt.turn
|| entry.event.data.step !== attempt.step) {
return { type: 'rebaseline' }
}
const key = positionKey(attempt.turn, attempt.step)
const entry = this.pendingMessages.get(key)
if (entry === undefined) {
return frame.outcome === 'aborted' ? undefined : { type: 'rebaseline' }
}
if (entry.event.type !== 'assistant/message') return { type: 'rebaseline' }
const sourceEventSeqs = entry.event.sourceEventSeqs
if (sourceEventSeqs === undefined || !sameSeqs(sourceEventSeqs, frame.legacyChunkSeqs)) {
return { type: 'rebaseline' }
}
this.pendingMessages.delete(key)
this.pending.delete(frame.outcome.seq)
return this.publish(entry)
}
}
}
private attemptFor(turn: number, step: number): ActiveAttempt | undefined {
return [...this.attempts.values()].find(attempt => (
attempt.turn === turn && attempt.step === step
))
private attemptForSettlement(
event: SessionLiveEventEntry['event'],
): ActiveAttempt | undefined {
const attempt = this.activeAttempt
if (attempt === undefined
|| (event.type !== 'assistant/message' && event.type !== 'assistant/attempt')
|| (event.type === 'assistant/message' && event.surfaceOp !== 'append')
|| event.time < attempt.startedTime
|| attempt.turn !== event.data.turn
|| attempt.step !== event.data.step) return undefined
return attempt
}
private publish(entry: SessionLiveEventEntry): ClientAssistantStreamResult {
@@ -18,7 +18,7 @@ export function historyEntries(
/**
* Read the first logical sequence represented by one wire record.
* @param record - validated scalar event or packed Assistant delta run.
* @param record - validated Session event.
* @returns inclusive first Session sequence.
*/
export function historyRecordFirstSeq(record: SessionHistoryRecord): number {
@@ -27,13 +27,9 @@ export function historyRecordFirstSeq(record: SessionHistoryRecord): number {
/**
* Read the final logical sequence represented by one wire record.
* @param record - validated scalar event or packed Assistant delta run.
* @param record - validated Session event.
* @returns inclusive final Session sequence.
*/
export function historyRecordLastSeq(record: SessionHistoryRecord): number {
if (record.type === 'event') return record.event.seq
const length = record.event.type === 'chunkrow/tool-call-chunks'
? record.event.data.args.length
: record.event.data.texts.length
return record.event.seq + length - 1
return record.event.seq
}
@@ -651,7 +651,7 @@ export class Session implements SessionFace {
// attempts makes a held notification reopen follow once for an atomic
// page/baseline pair instead of applying it to an unrelated repair cut.
const visible = this.assistantStream.replace(entries, assistantStream)
this.baseSeq = SessionLogOffset(visible[0]?.event.seq ?? 0)
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0)
this.hasMore = hasMore
if (visible.some(entry => entry.event.type === 'turn/start')) this.firstPromptPendingTurn = false
if (projections !== undefined) this.projections.seed(projections)
@@ -670,6 +670,9 @@ export class Session implements SessionFace {
}
if (result?.type === 'publish' && this.appendLive(result.entry)) {
this.notifier.markDirty()
} else if (result?.type === 'transient') {
this.eventSource.append(result.entry)
this.notifier.markDirty()
}
}
@@ -66,13 +66,6 @@ function toSessionJournalChange(
case 'prepend':
return { ...change, entries: historyEntries(change.entries) }
case 'append': {
if (change.entry.type !== 'event') {
throw new RemoteError(
'gateway/internal',
'session live stream emitted a packed history record',
{},
)
}
return {
type: 'append',
entry: change.entry as unknown as SessionLiveEventEntry,
@@ -7,7 +7,7 @@ import type { Agent, ModelSelection as AgentModelSelection } from '@deepseek-ai/
import { AttachmentError, admitPromptContent } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import {
ReasoningEffortId, createUserMessage, freezeMessage,
ReasoningEffortId, createUserMessage, expandAssistantStream, freezeMessage,
} from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
@@ -525,7 +525,6 @@ function imageInEvent(
readonly content?: unknown
readonly message?: { readonly content?: unknown }
readonly inserted?: readonly { readonly content?: unknown }[]
readonly chunk?: { readonly type?: unknown; readonly block?: unknown }
}
const direct = imageBlockIn(data.content, match)
if (direct !== undefined) return direct
@@ -535,9 +534,14 @@ function imageInEvent(
const found = imageBlockIn(inserted.content, match)
if (found !== undefined) return found
}
return event.type === 'assistant/chunk' && data.chunk?.type === 'block-end'
? imageBlockIn([data.chunk.block], match)
: undefined
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
for (const { chunk } of expandAssistantStream(event.data.stream)) {
if (chunk.type !== 'block-end') continue
const found = imageBlockIn([chunk.block], match)
if (found !== undefined) return found
}
}
return undefined
}
function referencedImage(
+6 -37
View File
@@ -8,7 +8,6 @@ import {
SessionLogOffset,
SessionSeq,
} from '@deepseek-ai/dsh-session'
import { isChunkRow, packChunkRuns, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type {
SessionEvent,
SessionHeader,
@@ -23,7 +22,6 @@ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type {
SessionAddress,
SessionAssistantStreamFrame,
SessionChunkRun,
SessionEventEntry,
SessionFollowRequest,
SessionFollowFrame,
@@ -183,7 +181,7 @@ export class SessionHistoryController {
snapshotCursor = cursor
const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES)
const assistantStream = request.assistantStream === true
? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0, attempts: [] }
? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 }
: undefined
// The accumulator snapshot and this watermark are synchronous. Frames
// through the cut are represented or superseded by that baseline,
@@ -192,7 +190,7 @@ export class SessionHistoryController {
const assistantStreamOrdinalCut = assistantStreamOrdinal
yield {
type: 'snapshot',
header: wireHeader(source.header, source.inheritedEventCount),
header: wireHeader(source.header),
cursor,
records: pageRecords(page.events),
hasMore: page.hasMore,
@@ -402,16 +400,9 @@ function paginate(
return { events: events.slice(cut, end), hasMore: cut > 0 }
}
/** Translate logical Session metadata to the unchanged v0 browser wire. */
function wireHeader(
header: SessionHeader,
inheritedEventCount: SessionLogOffsetType,
): SessionWireHeader {
const { isSeeded, ...wire } = header
return {
...wire,
...isSeeded ? { seedLength: inheritedEventCount } : {},
}
/** Translate current logical Session metadata to the browser wire. */
function wireHeader(header: SessionHeader): SessionWireHeader {
return { ...header }
}
function entryFor(event: SessionEvent): SessionEventEntry {
@@ -422,29 +413,7 @@ function entryFor(event: SessionEvent): SessionEventEntry {
}
}
function chunkEntryFor(row: ChunkRow): SessionChunkRun {
switch (row.type) {
case 'text-chunks':
return {
type: 'chunks',
event: { type: 'chunkrow/text-chunks', seq: row.seq0, time: row.time0, data: row.data },
}
case 'reasoning-chunks':
return {
type: 'chunks',
event: { type: 'chunkrow/reasoning-chunks', seq: row.seq0, time: row.time0, data: row.data },
}
case 'tool-call-chunks':
return {
type: 'chunks',
event: { type: 'chunkrow/tool-call-chunks', seq: row.seq0, time: row.time0, data: row.data },
}
}
}
/** Encode one bounded logical page without changing its pagination cut. */
function pageRecords(events: readonly SessionEvent[]): SessionHistoryRecord[] {
return packChunkRuns(events).map(record => isChunkRow(record)
? chunkEntryFor(record)
: entryFor(record))
return events.map(entryFor)
}
+19 -30
View File
@@ -5,8 +5,7 @@ import type {
} from '@deepseek-ai/dsh-attachment'
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { LlmAttemptId, MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
@@ -384,15 +383,15 @@ export interface SessionEventEntry {
readonly event: SessionWireEvent
}
/** v0-compatible Session metadata carried on the browser wire. */
/** Current logical Session metadata carried on the browser wire. */
export interface SessionWireHeader {
readonly version: number
readonly id: SessionId
readonly createdAt: number
readonly cwd?: string
readonly parentSession?: SessionId
/** Exact inherited prefix length; absent for an unseeded Session. */
readonly seedLength?: number
/** Whether the Session contains a fork-inherited prefix. */
readonly isSeeded: boolean
readonly origin?: 'subagent'
readonly delegationDepth?: number
readonly agentPreset?: string
@@ -403,24 +402,8 @@ export type SessionWireSurfaceOp =
| 'append'
| { readonly op: 'replace'; readonly start: number; readonly end: number }
/** Event-shaped wire representation of one packed chunk row. */
export type ChunkRowEvent = {
[Kind in ChunkRow['type']]: {
readonly type: `chunkrow/${Kind}`
readonly seq: number
readonly time: number
readonly data: Extract<ChunkRow, { readonly type: Kind }>['data']
}
}[ChunkRow['type']]
/** One lossless run of consecutive Assistant delta events in a history page. */
export interface SessionChunkRun {
readonly type: 'chunks'
readonly event: ChunkRowEvent
}
/** One history-page record: a raw event or a packed Assistant delta run. */
export type SessionHistoryRecord = SessionEventEntry | SessionChunkRun
/** One history-page record. V2 embeds compact Assistant streams inside events. */
export type SessionHistoryRecord = SessionEventEntry
/** Session event wire form; durable readers own recognition of merge-extensible event names. */
export interface SessionWireEvent {
@@ -457,15 +440,16 @@ export interface SessionAssistantStreamAttempt {
readonly startedTime: number
readonly turn: number
readonly step: number
readonly chunks: readonly JsonValue[]
/** Exact durable v1 chunk records already represented by {@link chunks}. */
readonly legacyChunkSeqs: readonly number[]
/** Dense position expected for the next live chunk frame. */
readonly nextIndex: number
/** Compact detached stream accumulated at this opening revision. */
readonly stream: readonly JsonValue[]
}
/** Complete process-local assistant state at one follow opening. */
export interface SessionAssistantStreamBaseline {
readonly revision: number
readonly attempts: readonly SessionAssistantStreamAttempt[]
readonly activeAttempt?: SessionAssistantStreamAttempt
}
/** Browser wire form of one process-local assistant frame. */
@@ -483,8 +467,8 @@ export type SessionAssistantStreamFrame =
readonly attemptId: LlmAttemptId
readonly revision: number
readonly index: number
readonly time: number
readonly chunk: JsonValue
readonly legacyChunkSeq: number
}
| {
readonly type: 'end'
@@ -492,8 +476,13 @@ export type SessionAssistantStreamFrame =
readonly revision: number
/** Number of chunk frames represented by this terminal marker. */
readonly index: number
readonly outcome: 'committed' | 'aborted'
readonly legacyChunkSeqs: readonly number[]
readonly outcome:
| {
readonly kind: 'committed'
readonly eventType: 'assistant/message' | 'assistant/attempt'
readonly seq: number
}
| { readonly kind: 'abandoned' }
}
/** One contiguous backwards page of a Session log. */