Files
deepseek-harness/packages/core/agent-loop/src/assistant-stream.ts
T
Tianyi Cui d58964a010 Merge commit 'be5db297c7' into worktree/session-format-05-v1-v2-chunk-migration
# Conflicts:
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	packages/api/session-controller/README.i18n.yaml
#	packages/api/session-controller/README.md
#	packages/api/session-controller/README.zh.md
#	packages/api/session-controller/src/assistant-stream.ts
#	packages/api/session-controller/src/client/sessions/assistant-stream.ts
#	packages/api/session-controller/src/types.ts
#	packages/api/session-controller/tests/assistant-stream.client.spec.ts
#	packages/api/session-controller/tests/session-history-journal.host.spec.ts
#	packages/api/session-controller/tests/sessions-service.client.spec.ts
#	packages/api/session-controller/tests/transport.client.spec.ts
#	packages/core/agent/README.i18n.yaml
#	packages/core/agent/README.md
#	packages/core/agent/README.zh.md
#	packages/extensions/tool-cordis/src/api-catalog.ts
#	scripts/package-dependency-policy.ts
2026-09-03 22:09:31 +08:00

141 lines
4.2 KiB
TypeScript

/** 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
}
}