Resolve compaction policy per routed model

This commit is contained in:
Yichen Jiang
2026-07-20 15:34:00 +08:00
parent b877ede82d
commit cfa180c127
54 changed files with 1210 additions and 319 deletions

View File

@@ -1,111 +1,298 @@
/**
* Runtime defaulting and policy validation for compact-basic.
* Load-time validation and routed-model policy resolution for compact-basic.
*
* @module @deepseek-ai/dsh-compact-basic/config
*/
import { deepFreeze } from '@deepseek-ai/dsh-llm'
import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter'
import type { BasicCompactConfig, ResolvedConfig } from './types.ts'
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
import type {
BasicCompactConfig,
CompactPolicyConfig,
ModelCompactPolicyConfig,
ResolvedCompactSpec,
ResolvedConfig,
ResolvedRetention,
ResolvedTargetPolicy,
} from './types.ts'
/** Default request-pressure fraction of the token meter's context window. */
/** Default request-pressure fraction for every routed model. */
const DEFAULT_THRESHOLD_RATIO = 0.8
/** Default verbatim-tail fraction of the token meter's context window. */
/** Default verbatim-tail fraction for every routed model. */
const DEFAULT_RETAIN_RATIO = 0.16
/** Complete public configuration key set. */
const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([
/** Fields shared by top-level defaults and exact-target overrides. */
const POLICY_CONFIG_KEYS = [
'thresholdRatio',
'retainRatio',
'retainTokens',
'summarizationProvider',
'summarizationModel',
'maxTokens',
'compactionRetries',
'maxOverflowRetries',
] as const
/** Complete public top-level configuration key set. */
const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([
...POLICY_CONFIG_KEYS,
'modelPolicies',
'auto',
])
/** Reject stale or misspelled keys before defaults can hide them. */
function validateConfigKeys(config: BasicCompactConfig): void {
for (const key of Object.keys(config)) {
if (!BASIC_COMPACT_CONFIG_KEYS.has(key)) {
throw new Error(
`BasicCompactConfig: unknown key "${key}" `
+ '(allowed: thresholdRatio, retainTokens, summarizationProvider, summarizationModel, '
+ 'maxTokens, compactionRetries, maxOverflowRetries, auto)',
)
}
/** Complete exact-target override key set. */
const MODEL_POLICY_KEYS: ReadonlySet<string> = new Set([
'provider',
'model',
...POLICY_CONFIG_KEYS,
])
/** Target-specific pressure configuration failure eligible for warning suppression. */
export class TargetPressureConfigError extends Error {
/**
* @param targetKey - exact provider/model route used as the warning key.
* @param message - actionable configuration failure detail.
*/
constructor(readonly targetKey: string, message: string) {
super(message)
}
}
/**
* Resolve defaults and validate the service-wide compaction policy.
* @param config - raw compact-basic configuration.
* @param tokenMeter - token meter supplying the context capacity.
* @returns a detached deeply immutable configuration.
* Resolve and validate service defaults plus exact-target partial overrides.
* @param config - untrusted plugin configuration after Loader normalization.
* @returns detached immutable defaults and validated exact-target overrides.
*/
export function resolveConfig(
config: BasicCompactConfig = {},
tokenMeter: TokenMeterService,
): ResolvedConfig {
validateConfigKeys(config)
export function resolveConfig(config: BasicCompactConfig = {}): ResolvedConfig {
validateKeys(config, BASIC_COMPACT_CONFIG_KEYS, 'BasicCompactConfig')
validatePolicy(config, 'BasicCompactConfig')
if (config.auto !== undefined && typeof config.auto !== 'boolean') {
throw new Error('BasicCompactConfig: auto must be a boolean')
}
const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO
const retainTokens = config.retainTokens
?? Math.floor(tokenMeter.contextWindow * DEFAULT_RETAIN_RATIO)
const resolved: ResolvedConfig = {
const retention = resolveRetention(config, { retainRatio: DEFAULT_RETAIN_RATIO })
validateRatioRetention(thresholdRatio, retention, 'BasicCompactConfig')
const modelPolicies = resolveModelPolicies(config.modelPolicies)
for (const [index, policy] of modelPolicies.entries()) {
validateRatioRetention(
policy.thresholdRatio ?? thresholdRatio,
resolveRetention(policy, retention),
`BasicCompactConfig: modelPolicies[${index}]`,
)
}
return deepFreeze({
thresholdRatio,
retainTokens,
...retention,
summarizationProvider: config.summarizationProvider ?? '',
summarizationModel: config.summarizationModel ?? '',
maxTokens: config.maxTokens ?? 8192,
compactionRetries: config.compactionRetries ?? 1,
maxOverflowRetries: config.maxOverflowRetries ?? 1,
modelPolicies,
auto: config.auto ?? true,
}
})
}
assertRatio('thresholdRatio', resolved.thresholdRatio)
assertNonNegativeInteger('retainTokens', resolved.retainTokens)
const thresholdTokens = Math.floor(tokenMeter.contextWindow * resolved.thresholdRatio)
if (resolved.retainTokens >= thresholdTokens) {
throw new Error(
`BasicCompactConfig: retainTokens (${resolved.retainTokens}) must be less than threshold tokens ${thresholdTokens}`,
/**
* Merge the exact provider/model override over the validated default policy.
* @param config - validated service defaults and override table.
* @param target - exact durable provider/model route to match.
* @returns detached immutable policy before model-capacity scaling.
*/
export function resolveTargetPolicy(
config: ResolvedConfig,
target: Pick<LlmCallConfig, 'provider' | 'model'>,
): ResolvedTargetPolicy {
const override = config.modelPolicies.find(policy => (
policy.provider === target.provider && policy.model === target.model
))
const inheritedRetention: ResolvedRetention = config.retainTokens === undefined
? { retainRatio: config.retainRatio }
: { retainTokens: config.retainTokens }
return deepFreeze({
target: { provider: target.provider, model: target.model },
thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,
...resolveRetention(override ?? {}, inheritedRetention),
summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,
summarizationModel: override?.summarizationModel ?? config.summarizationModel,
maxTokens: override?.maxTokens ?? config.maxTokens,
compactionRetries: override?.compactionRetries ?? config.compactionRetries,
maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries,
})
}
/**
* Scale one routed policy into concrete token budgets for its model capacity.
* @param policy - merged policy for the exact routed target.
* @param contextWindow - positive adapter-owned capacity for that target.
* @returns detached immutable pressure and retention budgets.
*/
export function resolveCompactSpec(
policy: ResolvedTargetPolicy,
contextWindow: number,
): ResolvedCompactSpec {
const targetKey = `${policy.target.provider}/${policy.target.model}`
if (!Number.isInteger(contextWindow) || contextWindow <= 0) {
throw new TargetPressureConfigError(
targetKey,
`BasicCompactConfig: contextWindow (${contextWindow}) must be a positive integer`,
)
}
assertPositiveInteger('maxTokens', resolved.maxTokens)
assertNonNegativeInteger('compactionRetries', resolved.compactionRetries)
assertNonNegativeInteger('maxOverflowRetries', resolved.maxOverflowRetries)
if (typeof resolved.summarizationProvider !== 'string') {
throw new Error('BasicCompactConfig: summarizationProvider must be a string')
}
if (typeof resolved.summarizationModel !== 'string') {
throw new Error('BasicCompactConfig: summarizationModel must be a string')
}
if ((resolved.summarizationProvider.length === 0) !== (resolved.summarizationModel.length === 0)) {
throw new Error(
'BasicCompactConfig: summarizationProvider and summarizationModel must both be set or both be empty',
const thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio)
const retainTokens = policy.retainTokens === undefined
? Math.floor(contextWindow * policy.retainRatio)
: policy.retainTokens
if (retainTokens >= thresholdTokens) {
throw new TargetPressureConfigError(
targetKey,
`BasicCompactConfig: ${policy.target.provider}/${policy.target.model} retainTokens `
+ `(${retainTokens}) must be less than threshold tokens ${thresholdTokens}`,
)
}
if (typeof resolved.auto !== 'boolean') {
throw new Error('BasicCompactConfig: auto must be a boolean')
}
return deepFreeze(resolved)
return deepFreeze({
target: { ...policy.target },
contextWindow,
thresholdRatio: policy.thresholdRatio,
thresholdTokens,
retainTokens,
summarizationProvider: policy.summarizationProvider,
summarizationModel: policy.summarizationModel,
maxTokens: policy.maxTokens,
compactionRetries: policy.compactionRetries,
maxOverflowRetries: policy.maxOverflowRetries,
})
}
function assertPositiveInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`BasicCompactConfig: ${name} (${value}) must be a positive integer`)
/** Choose an explicit retention form or inherit the already-resolved fallback. */
function resolveRetention(
config: CompactPolicyConfig,
fallback: ResolvedRetention,
): ResolvedRetention {
if (config.retainTokens !== undefined) return { retainTokens: config.retainTokens }
if (config.retainRatio !== undefined) return { retainRatio: config.retainRatio }
return fallback
}
/** Reject a capacity-independent retention conflict at plugin load. */
function validateRatioRetention(
thresholdRatio: number,
retention: ResolvedRetention,
name: string,
): void {
if (retention.retainRatio !== undefined && retention.retainRatio >= thresholdRatio) {
throw new Error(
`${name}: retainRatio (${retention.retainRatio}) must be less than `
+ `the resolved thresholdRatio (${thresholdRatio})`,
)
}
}
function assertNonNegativeInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new Error(`BasicCompactConfig: ${name} (${value}) must be a non-negative integer`)
/** Validate, detach, and reject duplicate exact-target policies. */
function resolveModelPolicies(configured: unknown): ModelCompactPolicyConfig[] {
if (configured === undefined) return []
if (!Array.isArray(configured)) {
throw new Error('BasicCompactConfig: modelPolicies must be an array')
}
const seen = new Set<string>()
return configured.map((source: unknown, index) => {
const name = `BasicCompactConfig: modelPolicies[${index}]`
assertModelPolicy(source, name)
const key = `${source.provider}\u0000${source.model}`
if (seen.has(key)) {
throw new Error(
`BasicCompactConfig: duplicate model policy for ${source.provider}/${source.model}`,
)
}
seen.add(key)
return { ...source }
})
}
/** Validate one untrusted exact-target override and narrow its public type. */
function assertModelPolicy(
source: unknown,
name: string,
): asserts source is ModelCompactPolicyConfig {
if (!isUnknownRecord(source)) throw new Error(`${name} must be an object`)
validateKeys(source, MODEL_POLICY_KEYS, name)
assertNonEmptyString(`${name}.provider`, source.provider)
assertNonEmptyString(`${name}.model`, source.model)
validatePolicy(source, name)
}
/** Validate the fields common to defaults and exact-target partial overrides. */
function validatePolicy(
config: CompactPolicyConfig | Record<string, unknown>,
name: string,
): void {
const thresholdRatio = config.thresholdRatio
const retainRatio = config.retainRatio
const retainTokens = config.retainTokens
const maxTokens = config.maxTokens
const compactionRetries = config.compactionRetries
const maxOverflowRetries = config.maxOverflowRetries
if (thresholdRatio !== undefined) assertRatio(`${name}.thresholdRatio`, thresholdRatio)
if (retainRatio !== undefined) assertRatio(`${name}.retainRatio`, retainRatio)
if (retainTokens !== undefined) assertNonNegativeInteger(`${name}.retainTokens`, retainTokens)
if (retainRatio !== undefined && retainTokens !== undefined) {
throw new Error(`${name}: retainRatio and retainTokens are mutually exclusive`)
}
if (maxTokens !== undefined) assertPositiveInteger(`${name}.maxTokens`, maxTokens)
if (compactionRetries !== undefined) {
assertNonNegativeInteger(`${name}.compactionRetries`, compactionRetries)
}
if (maxOverflowRetries !== undefined) {
assertNonNegativeInteger(`${name}.maxOverflowRetries`, maxOverflowRetries)
}
const summarizationProvider = config.summarizationProvider
const summarizationModel = config.summarizationModel
if (summarizationProvider !== undefined && typeof summarizationProvider !== 'string') {
throw new Error(`${name}.summarizationProvider must be a string`)
}
if (summarizationModel !== undefined && typeof summarizationModel !== 'string') {
throw new Error(`${name}.summarizationModel must be a string`)
}
if (((summarizationProvider ?? '').length === 0)
!== ((summarizationModel ?? '').length === 0)) {
throw new Error(`${name}: summarizationProvider and summarizationModel must both be empty or both be non-empty`)
}
}
function assertRatio(name: string, value: number): void {
/** Reject stale or misspelled keys before defaults can hide them. */
function validateKeys(config: object, keys: ReadonlySet<string>, name: string): void {
for (const key of Object.keys(config)) {
if (!keys.has(key)) throw new Error(`${name}: unknown key "${key}"`)
}
}
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function assertNonEmptyString(name: string, value: unknown): asserts value is string {
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`${name} must be a non-empty string`)
}
}
function assertPositiveInteger(name: string, value: unknown): asserts value is number {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
throw new Error(`${name} (${String(value)}) must be a positive integer`)
}
}
function assertNonNegativeInteger(name: string, value: unknown): asserts value is number {
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
throw new Error(`${name} (${String(value)}) must be a non-negative integer`)
}
}
function assertRatio(name: string, value: unknown): asserts value is number {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {
throw new Error(`BasicCompactConfig: ${name} (${value}) must be a number in (0, 1]`)
throw new Error(`${name} (${String(value)}) must be a number in (0, 1]`)
}
}

View File

@@ -10,27 +10,76 @@ 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'
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 {
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.
@@ -43,22 +92,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()
}
@@ -88,15 +141,21 @@ 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, retryAttempt, signal, next) => {
if (error.code !== CONTEXT_WINDOW_EXCEEDED_CODE
|| retryAttempt >= this.config.maxOverflowRetries
|| signal.aborted) return next()
if (error.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 (retryAttempt >= policy.maxOverflowRetries) return next()
let generation: number
let result: CompactionResult | null
@@ -131,7 +190,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, text, agent, signal)
const target = conversationTarget(agent)
const config = target === undefined
? this.config
: resolveTargetPolicy(this.config, target)
return summarizeWithLlm(this.ctx, config, text, agent, signal)
}
/**
@@ -149,8 +212,9 @@ 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
switch (trigger) {
case 'context-overflow': {
@@ -166,13 +230,22 @@ export class BasicCompactService extends CompactService {
assertNever(trigger, 'compaction trigger')
}
const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio)
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)
let measurement = meter.measure(agent.session)
if (measurement.totalTokens < threshold) return null
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
@@ -181,12 +254,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})`,
)
}

View File

@@ -8,7 +8,12 @@ import type { Context } from 'cordis'
import { BlockAssembler } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, FinishReason, GenerateOptions } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { ResolvedConfig } from './types.ts'
interface SummaryConfig {
readonly summarizationProvider: string
readonly summarizationModel: string
readonly maxTokens: number
}
/** Tags wrapping the structured summary inside the landed checkpoint node. */
const SUMMARY_OPEN_TAG = '<compacted-summary>'
@@ -74,7 +79,7 @@ export interface SummaryResult {
*/
export async function summarizeWithLlm(
ctx: Context,
config: ResolvedConfig,
config: SummaryConfig,
text: string,
agent: Agent,
signal?: AbortSignal,

View File

@@ -4,15 +4,19 @@
* @module @deepseek-ai/dsh-compact-basic/types
*/
/** Basic compaction configuration; every common field has a deployment default. */
export interface BasicCompactConfig {
/** Compact at this fraction of the token meter's context window. Defaults to `0.8`. */
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
/** Policy fields shared by the default policy and exact model overrides. */
export interface CompactPolicyConfig {
/** Compact at this fraction of the model's context window. Defaults to `0.8`. */
thresholdRatio?: number
/** Recent surface tokens retained verbatim. Defaults to `floor(contextWindow * 0.16)`. */
/** Recent context retained as a fraction of the model's window. Defaults to `0.16`. */
retainRatio?: number
/** Absolute recent-context budget; mutually exclusive with `retainRatio`. */
retainTokens?: number
/** Summary provider; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
/** Summary provider; set together with `summarizationModel`, or inherit the conversation target. */
summarizationProvider?: string
/** Summary model; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
/** Summary model; set together with `summarizationProvider`, or inherit the conversation target. */
summarizationModel?: string
/** Provider generation cap for summarization. Defaults to `8192`. */
maxTokens?: number
@@ -20,18 +24,53 @@ export interface BasicCompactConfig {
compactionRetries?: number
/** Maximum retries after canonical context overflow; `0` disables recovery. Defaults to `1`. */
maxOverflowRetries?: number
}
/** Exact provider/model override merged over the default compaction policy. */
export interface ModelCompactPolicyConfig extends CompactPolicyConfig {
/** Registered provider route to match. */
provider: string
/** Exact routed model id to match within `provider`. */
model: string
}
/** Basic compaction configuration with an optional exact-target policy table. */
export interface BasicCompactConfig extends CompactPolicyConfig {
/** Exact provider/model overrides; duplicate targets fail plugin load. */
modelPolicies?: ModelCompactPolicyConfig[]
/** Enable automatic post-step pressure and overflow-recovery listeners. Defaults to `true`. */
auto?: boolean
}
/** Validated and detached compaction configuration. */
export interface ResolvedConfig {
/** Exactly one validated retention form. */
export type ResolvedRetention =
| { readonly retainRatio: number; readonly retainTokens?: never }
| { readonly retainRatio?: never; readonly retainTokens: number }
/** Validated policy fields shared before and after exact-target matching. */
interface ResolvedPolicyFields {
readonly thresholdRatio: number
readonly retainTokens: number
readonly summarizationProvider: string
readonly summarizationModel: string
readonly maxTokens: number
readonly compactionRetries: number
readonly maxOverflowRetries: number
}
/** Validated immutable config whose target-specific defaults remain unresolved. */
export type ResolvedConfig = ResolvedPolicyFields & ResolvedRetention & {
readonly modelPolicies: readonly Readonly<ModelCompactPolicyConfig>[]
readonly auto: boolean
}
/** Fully merged policy for one routed conversation target, before capacity scaling. */
export type ResolvedTargetPolicy = ResolvedPolicyFields & ResolvedRetention & {
readonly target: Pick<LlmCallConfig, 'provider' | 'model'>
}
/** One routed model's concrete pressure and retention budget. */
export type ResolvedCompactSpec = Omit<ResolvedTargetPolicy, 'retainRatio' | 'retainTokens'> & {
readonly contextWindow: number
readonly thresholdTokens: number
readonly retainTokens: number
}