mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
feat(session)!: embed assistant streams in format v2
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user