/** Process-local assistant attempt framing and durable stream accumulation. */ import { AssistantStreamAccumulator, BlockAssembler, LlmAttemptId, type AssistantStreamRecord, type ContentBlock, type FinishReason, type ReplayEnvelope, type StreamChunk, type TokenUsage, } from '@deepseek-ai/dsh-llm' import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent' import type { SessionEventMap, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' /** Folds one model attempt into one compact stream plus ordered transient frames. */ export class AssistantStreamAttempt { private readonly accumulator = new AssistantStreamAccumulator() private readonly assembler = new BlockAssembler() private index = 0 private terminal = false /** Attempt identity unique within this Agent lifecycle. */ readonly attemptId: LlmAttemptId /** Whether this started attempt has emitted its terminal frame. */ get ended(): boolean { return this.terminal } /** * @param sessionId - identity embedded only in the Agent-lifecycle-local attempt id. * @param attempt - attached-Session-local attempt counter. * @param nextRevision - allocates the next emitted frame revision. * @param turn - durable turn owning the request. * @param step - durable step owning the request. * @param emit - agent-scoped notification publisher. */ constructor( sessionId: SessionId, attempt: number, private readonly nextRevision: () => number, readonly turn: number, readonly step: number, private readonly emit: (frame: AssistantStreamFrame) => void, ) { this.attemptId = LlmAttemptId(`${sessionId}:${attempt}`) } /** Publish the opening marker before the first delivered chunk. */ start(): void { this.emit({ type: 'start', attemptId: this.attemptId, revision: this.nextRevision(), turn: this.turn, step: this.step, }) } /** Snapshot one chunk once, then feed durable compaction, assembly, and live publication. */ push(chunk: StreamChunk): void { const timed = this.accumulator.push({ time: Date.now(), chunk }) this.assembler.push(timed.chunk) this.emit({ type: 'chunk', attemptId: this.attemptId, revision: this.nextRevision(), index: this.index++, time: timed.time, chunk: timed.chunk, }) } /** * Publish terminal settlement after the matching durable event commits. * @param eventType - durable settlement type. * @param append - synchronous durable append returning its committed seq. */ settle( eventType: 'assistant/message' | 'assistant/attempt', append: () => SessionSeq, ): void { let seq: SessionSeq try { seq = append() } catch (error: unknown) { this.abandon() throw error } this.terminal = true this.emit({ type: 'end', attemptId: this.attemptId, revision: this.nextRevision(), index: this.index, outcome: { kind: 'committed', eventType, seq }, }) } /** Publish abandonment when no durable attempt event can be committed. */ abandon(): void { this.terminal = true this.emit({ type: 'end', attemptId: this.attemptId, revision: this.nextRevision(), index: this.index, outcome: { kind: 'abandoned' }, }) } /** Exact compact stream for the final durable event. */ get stream(): SessionEventMap['assistant/attempt']['stream'] { return [...this.accumulator.snapshot()] as AssistantStreamRecord[] } /** Canonical completed-message blocks from the same chunks. */ blocks(): ContentBlock[] { return this.assembler.blocks() } /** Safe visible prefix when cancellation interrupts the attempt. */ interruptedBlocks(): ContentBlock[] { return this.assembler.interruptedBlocks() } /** Latest adapter-reported usage in the stream. */ get usage(): TokenUsage | undefined { return this.assembler.usage } /** Terminal reason, defaulting to stop when the stream omitted one. */ get finish(): FinishReason { return this.assembler.finish } /** Replay state carried by the terminal finish record. */ get replayState(): ReplayEnvelope | undefined { return this.assembler.replayState } }