mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge master into codex/session-title
This commit is contained in:
@@ -1,111 +1,310 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
validateSummarizationPair(config, name)
|
||||
}
|
||||
|
||||
/** Require one scope to omit, clear, or replace the summarization target as a pair. */
|
||||
function validateSummarizationPair(
|
||||
config: CompactPolicyConfig | Record<string, unknown>,
|
||||
name: string,
|
||||
): void {
|
||||
const provider = config.summarizationProvider
|
||||
const model = config.summarizationModel
|
||||
if (provider !== undefined && typeof provider !== 'string') {
|
||||
throw new Error(`${name}.summarizationProvider must be a string`)
|
||||
}
|
||||
if (model !== undefined && typeof model !== 'string') {
|
||||
throw new Error(`${name}.summarizationModel must be a string`)
|
||||
}
|
||||
if (provider === undefined && model === undefined) return
|
||||
if (provider === undefined || model === undefined
|
||||
|| (provider.length === 0) !== (model.length === 0)) {
|
||||
throw new Error(
|
||||
`${name}: summarizationProvider and summarizationModel must be set together `
|
||||
+ 'as an empty or non-empty pair',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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]`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,29 +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.
|
||||
@@ -45,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()
|
||||
}
|
||||
|
||||
@@ -90,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
|
||||
@@ -135,19 +206,25 @@ export class BasicCompactService extends CompactService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a rendered region through a direct one-shot `ctx.llm.stream()`
|
||||
* call. Override this sole hook for a template or remote summarizer.
|
||||
* @param text - plain-text conversation region to condense.
|
||||
* Summarize the replayed conversation region through a direct one-shot
|
||||
* `ctx.llm.stream()` call whose prefix reuses the conversation's own system
|
||||
* prompt, tools, and messages so the provider's KV cache is not invalidated.
|
||||
* Override this sole hook for a template or remote summarizer.
|
||||
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
||||
* @param agent - supplies routed-model history, fallback model, and session id.
|
||||
* @param signal - optional cancellation forwarded to the adapter.
|
||||
* @returns safe text summary blocks and exact auxiliary-call provenance.
|
||||
*/
|
||||
protected async summarize(
|
||||
text: string,
|
||||
input: SummarizationInput,
|
||||
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, input, agent, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,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:
|
||||
@@ -182,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
|
||||
@@ -209,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})`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,7 +330,7 @@ export class BasicCompactService extends CompactService {
|
||||
const session = agent.session
|
||||
return compactSurfaceRegion({
|
||||
meter: this.ctx.tokenMeter,
|
||||
summarize: (text, owner, abort) => this.summarize(text, owner, abort),
|
||||
summarize: (input, owner, abort) => this.summarize(input, owner, abort),
|
||||
}, session, start, end, agent, signal)
|
||||
}
|
||||
}
|
||||
|
||||
30
packages/compact/compact-basic/src/invariant.ts
Normal file
30
packages/compact/compact-basic/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-compact-basic`.
|
||||
* @module @deepseek-ai/dsh-compact-basic/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-compact-basic'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'compact-basic-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
||||
* beyond contracts enforced at its owning seam.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -6,20 +6,20 @@
|
||||
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
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>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,8 +123,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 (!isDeepStrictEqual(currentMeasurement.nodes, lockedMeasurement.nodes)) {
|
||||
@@ -174,6 +174,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[],
|
||||
|
||||
@@ -6,17 +6,28 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { BlockAssembler } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, FinishReason, GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, FinishReason, GenerateOptions, Message, ToolSchema } 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>'
|
||||
const SUMMARY_CLOSE_TAG = '</compacted-summary>'
|
||||
|
||||
/** Fixed structure required from the auxiliary summarization call. */
|
||||
const SUMMARIZE_SYSTEM_PROMPT = [
|
||||
'You are a compaction engine for an AI coding assistant. Condense the conversation transcript into a structured checkpoint that lets another model resume the work with no loss of essential context.',
|
||||
/**
|
||||
* The summarization directive, delivered as the FINAL user message after the
|
||||
* replayed conversation rather than as a distinct summarizer system prompt.
|
||||
* Keeping the conversation's own system prompt, tools, and message prefix in
|
||||
* front of it makes the auxiliary call a genuine prefix of the last routed
|
||||
* request, so the provider's KV cache is reused instead of invalidated.
|
||||
*/
|
||||
const COMPACTION_INSTRUCTION = [
|
||||
'You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.',
|
||||
'',
|
||||
'Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write "(none)" for an empty section — never drop a section.',
|
||||
'',
|
||||
@@ -47,14 +58,30 @@ const SUMMARIZE_SYSTEM_PROMPT = [
|
||||
'Rules:',
|
||||
'- Preserve exact file paths, commands, error strings, identifiers, and function signatures.',
|
||||
'- Capture user feedback and explicit instructions faithfully, especially corrections.',
|
||||
'- Do NOT mention this summarization process or that the context was compacted.',
|
||||
`- If the transcript already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`,
|
||||
'- Do NOT mention this summarization request or that the context was compacted.',
|
||||
'- Output only the checkpoint text: do not call any tool or take any other action.',
|
||||
`- If the conversation already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`,
|
||||
].join('\n')
|
||||
|
||||
/** Framing that makes the replacement user message established context. */
|
||||
const CHECKPOINT_PREAMBLE =
|
||||
'This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'
|
||||
|
||||
/**
|
||||
* The replayed conversation surface the summarizer condenses. Reproducing the
|
||||
* last routed request's system prompt, tools, and leading messages verbatim
|
||||
* lets the auxiliary call reuse the provider's warm prefix cache; the trailing
|
||||
* compaction instruction is then the only novel input.
|
||||
*/
|
||||
export interface SummarizationInput {
|
||||
/** The conversation's own system prompt, reused for prefix-cache alignment; absent for a system-less request. */
|
||||
readonly system?: string
|
||||
/** The conversation's tool schemas, reused for prefix-cache alignment; absent when the request carried none. */
|
||||
readonly tools?: readonly ToolSchema[]
|
||||
/** The request prefix followed by the shadowed region, in surface order, that precedes the compaction instruction. */
|
||||
readonly messages: readonly Message[]
|
||||
}
|
||||
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
||||
export interface SummaryResult {
|
||||
summary: ContentBlock[]
|
||||
@@ -64,18 +91,20 @@ export interface SummaryResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the default direct `ctx.llm.stream()` summarization call.
|
||||
* Run the default cache-reusing `ctx.llm.stream()` summarization call: replay
|
||||
* the conversation prefix, then append the compaction instruction as the final
|
||||
* user message so the provider's warm prefix cache is reused.
|
||||
* @param ctx - context providing the LLM service.
|
||||
* @param config - resolved backend configuration.
|
||||
* @param text - rendered transcript region to summarize.
|
||||
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
||||
* @param agent - supplies routed-model history, fallback model, and session id.
|
||||
* @param signal - optional cancellation forwarded to the adapter.
|
||||
* @returns safe text-only summary blocks and exact call provenance.
|
||||
*/
|
||||
export async function summarizeWithLlm(
|
||||
ctx: Context,
|
||||
config: ResolvedConfig,
|
||||
text: string,
|
||||
config: SummaryConfig,
|
||||
input: SummarizationInput,
|
||||
agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SummaryResult> {
|
||||
@@ -97,14 +126,16 @@ export async function summarizeWithLlm(
|
||||
}
|
||||
|
||||
const assembler = new BlockAssembler()
|
||||
const messages: Message[] = [
|
||||
...input.messages,
|
||||
{ role: 'user', content: [{ type: 'text', text: COMPACTION_INSTRUCTION }] },
|
||||
]
|
||||
const options: GenerateOptions = {
|
||||
provider: target.provider,
|
||||
model: target.model,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: `Summarize this conversation history:\n\n${text}\n\nSummary:` }],
|
||||
}],
|
||||
system: SUMMARIZE_SYSTEM_PROMPT,
|
||||
messages,
|
||||
...input.system === undefined ? {} : { system: input.system },
|
||||
...input.tools === undefined ? {} : { tools: [...input.tools] },
|
||||
maxTokens: config.maxTokens,
|
||||
sessionId: agent.session.id,
|
||||
...signal === undefined ? {} : { signal },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user