Merge branch 'master' into fix/compaction-kv-cache-reuse

# Conflicts:
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/src/summarizer.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-21 19:45:43 +08:00
623 files changed
+21230 -3010

No files matched your search

+128 -37
View File
@@ -10,30 +10,79 @@ import { CompactService } from '@deepseek-ai/dsh-compact'
import type { CompactionResult, CompactionTrigger } from '@deepseek-ai/dsh-compact'
import type { Session } from '@deepseek-ai/dsh-session'
import { CONTEXT_WINDOW_EXCEEDED_CODE, assertNever } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, LlmCallConfig } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
// Type-only: makes the optional sibling service available to `ctx.get()`.
import type {} from '@deepseek-ai/dsh-compact-tool-result-prune'
import { resolveConfig } from './config.ts'
import {
resolveCompactSpec,
resolveConfig,
resolveTargetPolicy,
TargetPressureConfigError,
} from './config.ts'
import { compactSurfaceRegion, selectCompactableRange } from './region.ts'
import { summarizeWithLlm } from './summarizer.ts'
import type { SummarizationInput } from './summarizer.ts'
import type {
BasicCompactConfig,
ModelCompactPolicyConfig,
ResolvedConfig,
} from './types.ts'
export type {
BasicCompactConfig,
CompactPolicyConfig,
ModelCompactPolicyConfig,
ResolvedCompactSpec,
ResolvedConfig,
ResolvedRetention,
ResolvedTargetPolicy,
} from './types.ts'
/** Resolve the exact model durably routed for the latest provider request. */
function routedModel(session: Session): string | undefined {
const model = session.requestHeader()?.config.model
return model === undefined || model.length === 0 ? undefined : model
/** Resolve the exact provider/model durably routed for the latest request. */
function routedTarget(
session: Session,
): Pick<LlmCallConfig, 'provider' | 'model'> | undefined {
const config = session.requestHeader()?.config
if (config === undefined || config.provider.length === 0 || config.model.length === 0) {
return undefined
}
return { provider: config.provider, model: config.model }
}
/** Resolve the conversation target used to select an optional policy override. */
function conversationTarget(
agent: Agent,
): Pick<LlmCallConfig, 'provider' | 'model'> | undefined {
const routed = routedTarget(agent.session)
if (routed !== undefined) return routed
if (agent.options.provider === undefined || agent.options.provider.length === 0
|| agent.options.model === undefined || agent.options.model.length === 0) return undefined
return { provider: agent.options.provider, model: agent.options.model }
}
const thresholdRatioSchema = z.number()
const retainRatioSchema = z.number()
const retainTokensSchema = z.number().step(1).min(0)
const summarizationProviderSchema = z.string()
const summarizationModelSchema = z.string()
const maxTokensSchema = z.number().step(1).min(1)
const compactionRetriesSchema = z.number().step(1).min(0)
const maxOverflowRetriesSchema = z.number().step(1).min(0)
const modelPolicy: z<ModelCompactPolicyConfig> = z.object({
provider: z.string().required(),
model: z.string().required(),
thresholdRatio: thresholdRatioSchema,
retainRatio: retainRatioSchema,
retainTokens: retainTokensSchema,
summarizationProvider: summarizationProviderSchema,
summarizationModel: summarizationModelSchema,
maxTokens: maxTokensSchema,
compactionRetries: compactionRetriesSchema,
maxOverflowRetries: maxOverflowRetriesSchema,
})
/**
* Dependency-light compaction backend using `ctx.tokenMeter` for pressure,
* retention, provenance, and summary-convergence pricing.
@@ -46,22 +95,26 @@ export class BasicCompactService extends CompactService {
static inject = ['llm', 'tokenMeter']
static Config: z<BasicCompactConfig> = z.object({
thresholdRatio: z.number().default(0.8),
retainTokens: z.number().step(1),
summarizationProvider: z.string().default(''),
summarizationModel: z.string().default(''),
maxTokens: z.number().step(1).min(1).default(8192),
compactionRetries: z.number().step(1).min(0).default(1),
maxOverflowRetries: z.number().step(1).min(0).default(1),
auto: z.boolean().default(true),
thresholdRatio: thresholdRatioSchema,
retainRatio: retainRatioSchema,
retainTokens: retainTokensSchema,
summarizationProvider: summarizationProviderSchema,
summarizationModel: summarizationModelSchema,
maxTokens: maxTokensSchema,
compactionRetries: compactionRetriesSchema,
maxOverflowRetries: maxOverflowRetriesSchema,
modelPolicies: z.array(modelPolicy),
auto: z.boolean(),
})
/** Resolved and validated compaction configuration. */
readonly config: ResolvedConfig
private readonly warnedPressureConfigTargets = new Set<string>()
constructor(ctx: Context, config: BasicCompactConfig = {}) {
super(ctx)
this.config = resolveConfig(config, ctx.tokenMeter)
this.config = resolveConfig(config)
if (this.config.auto) this._registerAutomaticCompaction()
}
@@ -91,16 +144,33 @@ export class BasicCompactService extends CompactService {
const result = await this.compactIfNeeded(agent, 'pressure', signal)
if (result !== null) logResult(result, 'post-step pressure')
} catch (error: unknown) {
if (error instanceof TargetPressureConfigError) {
if (this.warnedPressureConfigTargets.has(error.targetKey)) return
this.warnedPressureConfigTargets.add(error.targetKey)
}
const message = error instanceof Error ? error.message : String(error)
ctx.logger.warn(`post-step compaction failed: ${message}; continuing the turn`)
}
})
ctx.on('agent/request-error', async (agent, _turn, _step, _error, failure, priorFailures, signal, next) => {
const priorOverflowFailures = priorFailures.filter(item => item.code === CONTEXT_WINDOW_EXCEEDED_CODE).length
if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE
|| priorOverflowFailures >= this.config.maxOverflowRetries
|| signal.aborted) return next()
ctx.on('agent/request-error', async (
agent,
_turn,
_step,
_error,
failure,
priorFailures,
signal,
next,
) => {
const priorOverflowFailures = priorFailures.filter(
item => item.code === CONTEXT_WINDOW_EXCEEDED_CODE,
).length
if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next()
const target = routedTarget(agent.session)
if (target === undefined) return next()
const policy = resolveTargetPolicy(this.config, target)
if (priorOverflowFailures >= policy.maxOverflowRetries) return next()
const generation = agent.session.surface.replaceGeneration
let result: CompactionResult | null
@@ -150,7 +220,11 @@ export class BasicCompactService extends CompactService {
agent: Agent,
signal?: AbortSignal,
): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> {
return summarizeWithLlm(this.ctx, this.config, input, agent, signal)
const target = conversationTarget(agent)
const config = target === undefined
? this.config
: resolveTargetPolicy(this.config, target)
return summarizeWithLlm(this.ctx, config, input, agent, signal)
}
/**
@@ -168,16 +242,15 @@ export class BasicCompactService extends CompactService {
trigger: CompactionTrigger,
signal: AbortSignal,
): Promise<CompactionResult | null> {
const model = routedModel(agent.session)
if (model === undefined) return null
const target = routedTarget(agent.session)
if (target === undefined) return null
const policy = resolveTargetPolicy(this.config, target)
const meter = this.ctx.tokenMeter
const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio)
let measurement = meter.measure(agent.session)
switch (trigger) {
case 'context-overflow':
break
case 'pressure':
if (measurement.totalTokens < threshold) return null
break
/* v8 ignore next -- closed-union exhaustiveness guard */
default:
@@ -185,25 +258,43 @@ export class BasicCompactService extends CompactService {
}
// Pruning is optional so compact-basic remains independently composable.
// Once either trigger qualifies, land the model-free pass before choosing
// a summary range, then remeasure through the singleton replay fold.
// Overflow always qualifies; pressure first resolves the routed model's
// capacity and checks its target-specific threshold.
const prune = this.ctx.get('toolResultPrune')
if (prune !== undefined) {
prune.pruneSession(agent.session)
measurement = meter.measure(agent.session)
}
if (trigger === 'context-overflow') {
if (prune !== undefined) {
prune.pruneSession(agent.session)
measurement = meter.measure(agent.session)
}
const range = selectCompactableRange(agent.session, measurement, 0)
if (range === null) return null
return this.compactRegion(range.start, range.end, agent, signal)
}
if (measurement.totalTokens < threshold) return null
const context = await this.ctx.llm.resolveModelContext(target.provider, target.model)
const targetKey = `${target.provider}/${target.model}`
if (context === undefined) {
throw new TargetPressureConfigError(
targetKey,
`compact-basic: no context capacity for ${targetKey}; `
+ 'configure contextWindow on that adapter model',
)
}
const spec = resolveCompactSpec(policy, context.contextWindow)
if (measurement.totalTokens < spec.thresholdTokens) return null
// Once pressure qualifies, land the model-free pass before choosing a
// summary range, then remeasure through the singleton replay fold.
if (prune !== undefined) {
prune.pruneSession(agent.session)
measurement = meter.measure(agent.session)
}
if (measurement.totalTokens < spec.thresholdTokens) return null
let result: CompactionResult | null = null
for (let attempt = 0; attempt <= this.config.compactionRetries; attempt += 1) {
const range = selectCompactableRange(agent.session, measurement, this.config.retainTokens)
for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) {
const range = selectCompactableRange(agent.session, measurement, spec.retainTokens)
if (range === null) {
/* v8 ignore else -- concrete replacement preserves a compactable checkpoint; subclass hooks cannot mutate it. */
if (result === null) return null
@@ -212,12 +303,12 @@ export class BasicCompactService extends CompactService {
}
result = await this.compactRegion(range.start, range.end, agent, signal)
measurement = meter.measure(agent.session)
if (measurement.totalTokens < threshold) return result
if (measurement.totalTokens < spec.thresholdTokens) return result
}
throw new Error(
`compaction still above threshold after ${this.config.compactionRetries + 1} compaction attempts `
+ `(${measurement.totalTokens} estimated tokens >= threshold ${threshold})`,
`compaction still above threshold after ${spec.compactionRetries + 1} compaction attempts `
+ `(${measurement.totalTokens} estimated tokens >= threshold ${spec.thresholdTokens})`,
)
}