| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- /**
- * Surface retention selection and the log-recorded compaction transaction.
- *
- * @module @deepseek-ai/dsh-compact-basic/region
- */
- import { isDeepStrictEqual } from 'node:util'
- import {
- COMPACT_CHECKPOINT_SOURCE,
- toolPairingBalancedAfter,
- toolPairingBalancedBefore,
- } from '@deepseek-ai/dsh-compact'
- import type { CompactionResult } from '@deepseek-ai/dsh-compact'
- import type { Message } from '@deepseek-ai/dsh-llm'
- import type { TokenMeasurement, TokenMeterService } from '@deepseek-ai/dsh-token-meter'
- import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
- import type { Agent } from '@deepseek-ai/dsh-agent'
- import { frameSummary } from './summarizer.ts'
- import type { SummarizationInput, SummaryResult } from './summarizer.ts'
- interface RegionDependencies {
- readonly meter: TokenMeterService
- summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>
- }
- /**
- * Resolve the next head-anchored range while retaining a priced recent tail
- * and never splitting an assistant tool-call/result pair.
- * @param session - session supplying authoritative current surface positions.
- * @param measurement - unified pressure and surface measurement from the conversation meter.
- * @param retainTokens - minimum recent tail budget retained verbatim.
- * @returns the inclusive positional seq range to compact, or `null`.
- */
- export function selectCompactableRange(
- session: Session,
- measurement: TokenMeasurement,
- retainTokens: number,
- ): { start: number; end: number } | null {
- const pricedNodes = measurement.nodes
- if (pricedNodes.length === 0) return null
- const surfaceNodes = session.surface.nodes
- if (surfaceNodes.length !== pricedNodes.length
- || surfaceNodes.some((seq, index) => seq !== pricedNodes[index]?.seq)) {
- throw new Error('compaction: token-meter surface does not match the current session surface')
- }
- let accumulated = 0
- let keepFromIdx = pricedNodes.length
- for (let index = pricedNodes.length - 1; index >= 0; index -= 1) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- accumulated += pricedNodes[index]!.tokens
- keepFromIdx = index
- if (accumulated >= retainTokens) break
- }
- if (keepFromIdx === 0) return null
- while (keepFromIdx > 0) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx]!)) break
- keepFromIdx -= 1
- }
- if (keepFromIdx === 0) return null
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const first = surfaceNodes[0]!
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const cutoff = surfaceNodes[keepFromIdx - 1]!
- return { start: first, end: cutoff }
- }
- /**
- * Validate and compact one positional surface span.
- * @param dependencies - conversation meter and dynamically dispatched summarizer hook.
- * @param session - session whose surface is mutated.
- * @param start - inclusive first surface-node seq.
- * @param end - inclusive last surface-node seq.
- * @param agent - agent used by the summarizer.
- * @param signal - optional summarization cancellation signal.
- * @returns the successful durable compaction result.
- */
- export async function compactSurfaceRegion(
- dependencies: RegionDependencies,
- session: Session,
- start: number,
- end: number,
- agent: Agent,
- signal?: AbortSignal,
- ): Promise<CompactionResult> {
- const nodes = session.surface.nodes
- const startIdx = nodes.indexOf(start)
- const endIdx = nodes.indexOf(end)
- if (startIdx === -1) throw new Error(`compactRegion: start seq ${start} not found in surface`)
- if (endIdx === -1) throw new Error(`compactRegion: end seq ${end} not found in surface`)
- if (startIdx > endIdx) {
- throw new Error(
- `compactRegion: start seq ${start} (position ${startIdx}) is after end seq ${end} (position ${endIdx}) on the surface`,
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (!toolPairingBalancedBefore(session, nodes[startIdx]!)) {
- throw new Error(`compactRegion: start seq ${start} is not a balanced boundary (would split a step's tool-call/result pair)`)
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (!toolPairingBalancedAfter(session, nodes[endIdx]!)) {
- throw new Error(`compactRegion: end seq ${end} is not a balanced boundary (would split a step, or the step is still open)`)
- }
- const tail = inspectTurnTail(session.events)
- if (tail.compactionInProgress) throw new Error('compaction already in progress')
- if (tail.turn === null) {
- throw new Error('compactRegion: no open turn — compaction events must be enclosed in a turn')
- }
- const shadowedSeqs = nodes.slice(startIdx, endIdx + 1)
- const startEvent = session.append('compact/start', { turn: tail.turn })
- try {
- // Capture after the lock event so a later surface mutation invalidates the
- // async selection before replacement. Unrelated log-only facts may append.
- const lockedMeasurement = dependencies.meter.measure(session)
- const selected = lockedMeasurement.nodes.slice(startIdx, endIdx + 1)
- if (selected.length !== shadowedSeqs.length
- || selected.some((node, index) => node.seq !== shadowedSeqs[index])) {
- throw new Error('compaction: selected surface changed before summarization began')
- }
- const shadowedTokenCount = selected.reduce((total, node) => total + node.tokens, 0)
- const summarizationInput = buildSummarizationInput(session, shadowedSeqs)
- const { summary, provider, model, maxTokens } = await dependencies.summarize(summarizationInput, agent, signal)
- const currentMeasurement = dependencies.meter.measure(session)
- if (!isDeepStrictEqual(currentMeasurement.nodes, lockedMeasurement.nodes)) {
- throw new Error('compaction: session surface changed during summarization')
- }
- const framedSummary = frameSummary(summary)
- const framedSummaryTokenCount = dependencies.meter.estimateMessage({
- role: 'user',
- content: framedSummary,
- })
- if (framedSummaryTokenCount >= shadowedTokenCount) {
- throw new Error(
- `summary is not smaller than the shadowed content (${framedSummaryTokenCount} estimated framed tokens >= ${shadowedTokenCount})`,
- )
- }
- const summaryEvent = session.append('compact/summary', {
- summary,
- shadowedRange: { start, end },
- shadowedSeqs,
- shadowedTokenCount,
- provider,
- model,
- ...maxTokens === undefined ? {} : { maxTokens },
- })
- session.append('user/message', {
- content: framedSummary,
- source: COMPACT_CHECKPOINT_SOURCE,
- }, {
- surfaceOp: { op: 'replace', start, end },
- sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
- })
- const endEvent = session.append('compact/end', { turn: tail.turn })
- return {
- startSeq: startEvent.seq,
- summarySeq: summaryEvent.seq,
- endSeq: endEvent.seq,
- summary,
- shadowedRange: { start, end },
- shadowedSeqs,
- shadowedTokenCount,
- }
- } catch (error: unknown) {
- const message = error instanceof Error ? error.message : String(error)
- session.append('compact/end', { turn: tail.turn, error: message })
- throw error
- }
- }
- /**
- * Reconstruct the last routed request's cacheable prefix for the shadowed
- * region: its system prompt and tool schemas, then the request-only message
- * prefix followed by the region's own derived messages in surface order. The
- * summarizer appends only the compaction instruction after this, so the call
- * is a genuine prefix of the conversation and reuses the provider's KV cache.
- * @param session - session supplying the request header and per-node projection.
- * @param shadowedSeqs - the surface-node seqs, in order, being compacted.
- * @returns the replayed conversation prefix to condense.
- */
- function buildSummarizationInput(
- session: Session,
- shadowedSeqs: readonly number[],
- ): SummarizationInput {
- const header = session.requestHeader()
- const events = session.events
- const regionMessages = shadowedSeqs
- // shadowedSeqs are current surface seqs, so each is a valid log index.
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- .map(seq => session.deriveEventMessage(events[seq]!))
- .filter((message): message is Message => message !== null)
- return {
- ...header?.system === undefined ? {} : { system: header.system },
- ...header?.tools === undefined ? {} : { tools: header.tools },
- messages: [...header?.messagePrefix ?? [], ...regionMessages],
- }
- }
- /** Inspect the current turn boundary and latest compaction bracket once. */
- function inspectTurnTail(
- events: readonly SessionEvent[],
- ): { turn: number | null; compactionInProgress: boolean } {
- let compactionInProgress = false
- let compactionStateKnown = false
- for (let index = events.length - 1; index >= 0; index -= 1) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const event = events[index]!
- if (!compactionStateKnown) {
- if (event.type === 'compact/start') {
- compactionInProgress = true
- compactionStateKnown = true
- } else if (event.type === 'compact/end') {
- compactionStateKnown = true
- }
- }
- if (event.type === 'turn/start') return { turn: event.data.turn, compactionInProgress }
- if (event.type === 'turn/end') return { turn: null, compactionInProgress }
- }
- return { turn: null, compactionInProgress }
- }
|