fix(compact-basic): replay conversation prefix so summarization reuses KV cache
Automatic compaction fires mid-conversation, right after the loop warmed the provider's KV cache with the last routed request. The default summarizer then issued a separate request whose prefix shared nothing with that warm request — a bespoke summarizer system prompt followed by the older history flattened to one rendered transcript string — so a differing first token invalidated the entire cached prefix and every compaction re-processed the whole replayed history twice. Move the compaction directive from the FRONT (a fresh system prompt) to the END (a trailing user message), and replay the last routed request's own system prompt, tools, message prefix, and shadowed-region messages verbatim via session.requestHeader() + deriveEventMessage. The auxiliary call is now a genuine prefix-extension of the warm request, so the provider reuses the cached tokens up to the trailing instruction. SummarizationInput carries the replayed prefix instead of a flat string; the now-unused renderTranscript/renderContentBlocks path is removed with its spec. Cache reuse is best-effort (head compaction guarantees a hit; a mid-range compaction or a differently-routed summarizer forgoes it), correctness is not.
This commit is contained in:
@@ -5,20 +5,20 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
renderTranscript,
|
||||
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 { SummaryResult } from './summarizer.ts'
|
||||
import type { SummarizationInput, SummaryResult } from './summarizer.ts'
|
||||
|
||||
interface RegionDependencies {
|
||||
readonly meter: TokenMeterService
|
||||
summarize(text: string, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>
|
||||
summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,8 +122,8 @@ export async function compactSurfaceRegion(
|
||||
throw new Error('compaction: selected surface changed before summarization began')
|
||||
}
|
||||
const shadowedTokenCount = selected.reduce((total, node) => total + node.tokens, 0)
|
||||
const text = renderTranscript(session.events, shadowedSeqs)
|
||||
const { summary, provider, model, maxTokens } = await dependencies.summarize(text, agent, signal)
|
||||
const summarizationInput = buildSummarizationInput(session, shadowedSeqs)
|
||||
const { summary, provider, model, maxTokens } = await dependencies.summarize(summarizationInput, agent, signal)
|
||||
|
||||
const currentMeasurement = dependencies.meter.measure(session)
|
||||
if (currentMeasurement.logRevision !== lockedMeasurement.logRevision) {
|
||||
@@ -173,6 +173,34 @@ export async function compactSurfaceRegion(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[],
|
||||
|
||||
Reference in New Issue
Block a user