mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
# 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
141 lines
4.2 KiB
TypeScript
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
|
|
}
|
|
}
|