refactor(token-meter): simplify singleton service (round 1)
This commit is contained in:
@@ -7,10 +7,6 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
TOKEN_METER_MODEL_UNCONFIGURED,
|
||||
TokenMeterError,
|
||||
} from '@deepseek-ai/dsh-token-meter'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
interface AutomaticCompactor {
|
||||
@@ -49,10 +45,6 @@ export function registerAutomaticCompaction(
|
||||
)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// A named routed model without a meter profile is configuration failure,
|
||||
// not an optional operational compaction miss.
|
||||
if (error instanceof TokenMeterError
|
||||
&& error.code === TOKEN_METER_MODEL_UNCONFIGURED) throw error
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
ctx.logger.warn(`compaction failed: ${message}; proceeding with full history`)
|
||||
}
|
||||
|
||||
@@ -1,63 +1,49 @@
|
||||
/**
|
||||
* Runtime defaulting and per-model policy validation for compact-basic.
|
||||
* Runtime defaulting and policy validation for compact-basic.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-compact-basic/config
|
||||
*/
|
||||
|
||||
import { deepFreeze } from '@deepseek-ai/dsh-llm'
|
||||
import type { ModelTokenMeter, TokenMeterService } from '@deepseek-ai/dsh-token-meter'
|
||||
import type {
|
||||
BasicCompactConfig,
|
||||
ModelCompactConfig,
|
||||
ResolvedConfig,
|
||||
ResolvedModelCompactConfig,
|
||||
} from './types.ts'
|
||||
import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter'
|
||||
import type { BasicCompactConfig, ResolvedConfig } from './types.ts'
|
||||
|
||||
/** Default request-pressure fraction for every metered model. */
|
||||
/** Default request-pressure fraction of the token meter's context window. */
|
||||
const DEFAULT_THRESHOLD_RATIO = 0.8
|
||||
|
||||
/** Default verbatim-tail fraction of a model's context window. */
|
||||
/** Default verbatim-tail fraction of the token meter's context window. */
|
||||
const DEFAULT_RETAIN_RATIO = 0.16
|
||||
|
||||
/**
|
||||
* Resolve common defaults and validate every named model override.
|
||||
* Resolve defaults and validate the service-wide compaction policy.
|
||||
* @param config - raw compact-basic configuration.
|
||||
* @param tokenMeter - owning meter service used to reject unknown override names.
|
||||
* @returns a detached deeply immutable top-level configuration.
|
||||
* @param tokenMeter - token meter supplying the context capacity.
|
||||
* @returns a detached deeply immutable configuration.
|
||||
*/
|
||||
export function resolveConfig(
|
||||
config: BasicCompactConfig = {},
|
||||
tokenMeter: TokenMeterService,
|
||||
): ResolvedConfig {
|
||||
const configuredModels: unknown = config.models
|
||||
const models = configuredModels === undefined ? {} : configuredModels
|
||||
if (typeof models !== 'object' || models === null || Array.isArray(models)) {
|
||||
throw new Error('BasicCompactConfig: models must be an object')
|
||||
}
|
||||
|
||||
const detachedModels: Record<string, ModelCompactConfig> = {}
|
||||
for (const [model, override] of Object.entries(models as Record<string, unknown>)) {
|
||||
if (typeof override !== 'object' || override === null || Array.isArray(override)) {
|
||||
throw new Error(`BasicCompactConfig: models.${model} must be an object`)
|
||||
}
|
||||
const meter = tokenMeter.resolve(model)
|
||||
detachedModels[model] = { ...override as ModelCompactConfig }
|
||||
resolveModelConfig({
|
||||
models: detachedModels,
|
||||
summarizationModel: '',
|
||||
maxTokens: 8192,
|
||||
compactionRetries: 1,
|
||||
auto: true,
|
||||
}, meter)
|
||||
}
|
||||
|
||||
const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO
|
||||
const retainTokens = config.retainTokens
|
||||
?? Math.floor(tokenMeter.contextWindow * DEFAULT_RETAIN_RATIO)
|
||||
const resolved: ResolvedConfig = {
|
||||
models: detachedModels,
|
||||
thresholdRatio,
|
||||
retainTokens,
|
||||
summarizationModel: config.summarizationModel ?? '',
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
compactionRetries: config.compactionRetries ?? 1,
|
||||
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}`,
|
||||
)
|
||||
}
|
||||
assertPositiveInteger('maxTokens', resolved.maxTokens)
|
||||
assertNonNegativeInteger('compactionRetries', resolved.compactionRetries)
|
||||
if (typeof resolved.summarizationModel !== 'string') {
|
||||
@@ -66,36 +52,7 @@ export function resolveConfig(
|
||||
if (typeof resolved.auto !== 'boolean') {
|
||||
throw new Error('BasicCompactConfig: auto must be a boolean')
|
||||
}
|
||||
return deepFreeze(structuredClone(resolved))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one effective model's default policy plus optional field overrides.
|
||||
* @param config - validated compact-basic configuration.
|
||||
* @param meter - effective model's token-meter handle and context capacity.
|
||||
* @returns a detached immutable model policy.
|
||||
*/
|
||||
export function resolveModelConfig(
|
||||
config: ResolvedConfig,
|
||||
meter: ModelTokenMeter,
|
||||
): ResolvedModelCompactConfig {
|
||||
const override = config.models[meter.model]
|
||||
const thresholdRatio = override?.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO
|
||||
const retainTokens = override?.retainTokens ?? Math.floor(meter.contextWindow * DEFAULT_RETAIN_RATIO)
|
||||
assertRatio(`models.${meter.model}.thresholdRatio`, thresholdRatio)
|
||||
assertNonNegativeInteger(`models.${meter.model}.retainTokens`, retainTokens)
|
||||
const thresholdTokens = Math.floor(meter.contextWindow * thresholdRatio)
|
||||
if (retainTokens >= thresholdTokens) {
|
||||
throw new Error(
|
||||
`BasicCompactConfig: models.${meter.model}.retainTokens (${retainTokens}) must be less than threshold tokens ${thresholdTokens}`,
|
||||
)
|
||||
}
|
||||
return deepFreeze({
|
||||
model: meter.model,
|
||||
contextWindow: meter.contextWindow,
|
||||
thresholdRatio,
|
||||
retainTokens,
|
||||
})
|
||||
return deepFreeze(resolved)
|
||||
}
|
||||
|
||||
function assertPositiveInteger(name: string, value: number): void {
|
||||
|
||||
@@ -11,24 +11,20 @@ import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import { canonicalHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { EpochHeader, Session } from '@deepseek-ai/dsh-session'
|
||||
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { ModelTokenMeter } from '@deepseek-ai/dsh-token-meter'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { registerAutomaticCompaction } from './automatic.ts'
|
||||
import { resolveConfig, resolveModelConfig } from './config.ts'
|
||||
import { resolveConfig } from './config.ts'
|
||||
import { compactSurfaceRegion, selectCompactableRange } from './region.ts'
|
||||
import { summarizeWithLlm } from './summarizer.ts'
|
||||
import type {
|
||||
BasicCompactConfig,
|
||||
ResolvedConfig,
|
||||
ResolvedModelCompactConfig,
|
||||
} from './types.ts'
|
||||
|
||||
export { resolveConfig, resolveModelConfig } from './config.ts'
|
||||
export { resolveConfig } from './config.ts'
|
||||
export type {
|
||||
BasicCompactConfig,
|
||||
ModelCompactConfig,
|
||||
ResolvedConfig,
|
||||
ResolvedModelCompactConfig,
|
||||
} from './types.ts'
|
||||
|
||||
/** Resolve the latest actual routed model, then the agent's configured fallback. */
|
||||
@@ -61,28 +57,24 @@ function provisionalHeader(
|
||||
* retention, provenance, and summary-convergence pricing.
|
||||
*
|
||||
* `summarize()` is the sole subclass customization hook; the replay and durable
|
||||
* mutation strategy stays fixed so every pricing decision uses one effective
|
||||
* conversation-model meter.
|
||||
* mutation strategy stays fixed so every pricing decision uses the singleton
|
||||
* token meter.
|
||||
*/
|
||||
export class BasicCompactService extends CompactService {
|
||||
static inject = ['llm', 'tokenMeter']
|
||||
|
||||
static Config: z<BasicCompactConfig> = z.object({
|
||||
models: z.dict(z.object({
|
||||
thresholdRatio: z.number(),
|
||||
retainTokens: z.number().step(1),
|
||||
})),
|
||||
thresholdRatio: z.number().default(0.8),
|
||||
retainTokens: z.number().step(1),
|
||||
summarizationModel: z.string().default(''),
|
||||
maxTokens: z.number().step(1).min(1).default(8192),
|
||||
compactionRetries: z.number().step(1).min(0).default(1),
|
||||
auto: z.boolean().default(true),
|
||||
})
|
||||
|
||||
/** Resolved and validated common configuration plus named partial overrides. */
|
||||
/** Resolved and validated compaction configuration. */
|
||||
readonly config: ResolvedConfig
|
||||
|
||||
private readonly modelConfigs = new Map<string, ResolvedModelCompactConfig>()
|
||||
|
||||
constructor(ctx: Context, config: BasicCompactConfig = {}) {
|
||||
super(ctx)
|
||||
this.config = resolveConfig(config, ctx.tokenMeter)
|
||||
@@ -107,9 +99,8 @@ export class BasicCompactService extends CompactService {
|
||||
|
||||
/**
|
||||
* Check replayed pressure for the provisional pre-step envelope and compact
|
||||
* a tool-balanced head until it falls below the effective model threshold.
|
||||
* A genuinely model-less router-first step skips this provisional check;
|
||||
* naming an unconfigured model throws the token meter's typed error.
|
||||
* a tool-balanced head until it falls below the service-wide threshold.
|
||||
* A genuinely model-less router-first step skips this provisional check.
|
||||
* @param agent - agent whose session and provisional model are measured.
|
||||
* @param fullSystemPrompt - current assembled system prompt override.
|
||||
* @param sessionPrefix - current request-only prefix override.
|
||||
@@ -124,10 +115,9 @@ export class BasicCompactService extends CompactService {
|
||||
): Promise<CompactionResult | null> {
|
||||
const model = effectiveModel(agent)
|
||||
if (model === undefined || model.length === 0) return null
|
||||
const meter = this.ctx.tokenMeter.resolve(model)
|
||||
const policy = this._modelConfig(meter)
|
||||
const meter = this.ctx.tokenMeter
|
||||
const requestHeader = provisionalHeader(model, agent.session, fullSystemPrompt, sessionPrefix)
|
||||
const threshold = Math.floor(policy.contextWindow * policy.thresholdRatio)
|
||||
const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio)
|
||||
let measurement = meter.measure(agent.session, requestHeader)
|
||||
if (measurement.totalTokens < threshold) return null
|
||||
|
||||
@@ -139,7 +129,7 @@ export class BasicCompactService extends CompactService {
|
||||
`compaction: pressure revision ${measurement.logRevision} does not match surface revision ${surface.logRevision}`,
|
||||
)
|
||||
}
|
||||
const range = selectCompactableRange(agent.session, surface, policy.retainTokens)
|
||||
const range = selectCompactableRange(agent.session, surface, this.config.retainTokens)
|
||||
if (range === null) {
|
||||
/* v8 ignore else -- concrete replacement preserves a compactable checkpoint; subclass hooks cannot mutate it. */
|
||||
if (result === null) return null
|
||||
@@ -159,12 +149,12 @@ export class BasicCompactService extends CompactService {
|
||||
|
||||
/**
|
||||
* Compact one inclusive positional surface range using the effective
|
||||
* conversation model for all retention and shrink pricing. Reject an agent
|
||||
* that does not own the exact target before any resolution or mutation.
|
||||
* token meter for all retention and shrink pricing. Reject an agent that does
|
||||
* not own the exact target before any mutation.
|
||||
* @param session - session whose surface is mutated; must equal `agent.session`.
|
||||
* @param start - inclusive first surface-node seq.
|
||||
* @param end - inclusive last surface-node seq.
|
||||
* @param agent - owner of the target session, used by the summarizer and model resolver.
|
||||
* @param agent - owner of the target session, used by the summarizer.
|
||||
* @param signal - optional summarization cancellation signal.
|
||||
* @returns the successful durable compaction result.
|
||||
*/
|
||||
@@ -178,27 +168,11 @@ export class BasicCompactService extends CompactService {
|
||||
if (session !== agent.session) {
|
||||
throw new Error('compactRegion: agent.session must be the exact target session')
|
||||
}
|
||||
const model = effectiveModel(agent)
|
||||
if (model === undefined || model.length === 0) {
|
||||
throw new Error('compactRegion: no routed or configured conversation model is available for token pricing')
|
||||
}
|
||||
const meter = this.ctx.tokenMeter.resolve(model)
|
||||
this._modelConfig(meter)
|
||||
return compactSurfaceRegion({
|
||||
meter,
|
||||
meter: this.ctx.tokenMeter,
|
||||
summarize: (text, owner, abort) => this.summarize(text, owner, abort),
|
||||
}, session, start, end, agent, signal)
|
||||
}
|
||||
|
||||
/** Resolve and memoize one lazy default/override model policy. */
|
||||
private _modelConfig(meter: ModelTokenMeter): ResolvedModelCompactConfig {
|
||||
let modelConfig = this.modelConfigs.get(meter.model)
|
||||
if (modelConfig === undefined) {
|
||||
modelConfig = resolveModelConfig(this.config, meter)
|
||||
this.modelConfigs.set(meter.model, modelConfig)
|
||||
}
|
||||
return modelConfig
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicCompactService
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
toolPairingBalancedBefore,
|
||||
} from '@deepseek-ai/dsh-compact'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import type { ModelTokenMeter, TokenSurfaceMeasurement } from '@deepseek-ai/dsh-token-meter'
|
||||
import type { TokenMeterService, TokenSurfaceMeasurement } 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'
|
||||
|
||||
interface RegionDependencies {
|
||||
readonly meter: ModelTokenMeter
|
||||
readonly meter: TokenMeterService
|
||||
summarize(text: string, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,12 @@
|
||||
* @module @deepseek-ai/dsh-compact-basic/types
|
||||
*/
|
||||
|
||||
/** Optional pressure and retention policy for one metered model. */
|
||||
export interface ModelCompactConfig {
|
||||
/** Compact at this fraction of the model's configured context window. Defaults to `0.8`. */
|
||||
/** 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`. */
|
||||
thresholdRatio?: number
|
||||
/** Recent surface tokens retained verbatim. Defaults to `floor(contextWindow * 0.16)`. */
|
||||
retainTokens?: number
|
||||
}
|
||||
|
||||
/** Basic compaction configuration; every common field has a deployment default. */
|
||||
export interface BasicCompactConfig {
|
||||
/** Field-wise pressure/retention overrides keyed by configured token-meter model name. */
|
||||
models?: Record<string, ModelCompactConfig>
|
||||
/** Summary model; `''` resolves the latest routed model, then `AgentOptions.model`. Defaults to `''`. */
|
||||
summarizationModel?: string
|
||||
/** Provider generation cap for summarization. Defaults to `8192`. */
|
||||
@@ -26,19 +20,12 @@ export interface BasicCompactConfig {
|
||||
auto?: boolean
|
||||
}
|
||||
|
||||
/** Validated top-level defaults plus detached per-model partial overrides. */
|
||||
/** Validated and detached compaction configuration. */
|
||||
export interface ResolvedConfig {
|
||||
readonly models: Readonly<Record<string, Readonly<ModelCompactConfig>>>
|
||||
readonly thresholdRatio: number
|
||||
readonly retainTokens: number
|
||||
readonly summarizationModel: string
|
||||
readonly maxTokens: number
|
||||
readonly compactionRetries: number
|
||||
readonly auto: boolean
|
||||
}
|
||||
|
||||
/** Fully resolved pressure/retention policy for one effective model. */
|
||||
export interface ResolvedModelCompactConfig {
|
||||
readonly model: string
|
||||
readonly contextWindow: number
|
||||
readonly thresholdRatio: number
|
||||
readonly retainTokens: number
|
||||
}
|
||||
Reference in New Issue
Block a user