import type { CompressionConfig, CompressionMode, CompressionPipelineStep, CompressionResult, } from "./types.ts"; import { applyHardBudget } from "./hardBudget.ts"; import { type FidelityGateConfig } from "./fidelityGate.ts"; import { gateAdvance } from "./fidelityGateStep.ts"; import type { CompressionEngineApplyOptions } from "./engines/types.ts"; import { applyLiteCompression } from "./lite.ts"; import { cavemanCompress } from "./caveman.ts"; import { compressAggressive } from "./aggressive.ts"; import { ultraCompress, ultraCompressHeuristic } from "./ultra.ts"; import { createCompressionStats } from "./stats.ts"; import { guardPipelineInflation } from "./pipelineGuards.ts"; import { resolvePipelineBreakerConfig, canRunEngine, recordEngineFailure, recordEngineSuccess, type PipelineCircuitBreakerConfig, } from "./pipelineEngineBreaker.ts"; import { type BailoutConfig, type StackAccumulator, createStackAccumulator, decideStep, mergeStackStep, } from "./stackedStepCore.ts"; import { registerBuiltinCompressionEngines } from "./engines/index.ts"; import { getCompressionEngine, getEngineEntry } from "./engines/registry.ts"; import { applyRtkCompression } from "./engines/rtk/index.ts"; import { adaptBodyForCompression } from "./bodyAdapter.ts"; import { detectCachingContext, getCacheAwareStrategy, type CachingDetectionContext, } from "./cachingAware.ts"; import { resolveCompressionPlan } from "./resolveCompressionPlan.ts"; import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts"; import { withSource, planFromHeader, formatCompressionMeta, formatCompressionAnnotation, deriveDefaultPlanFromConfig, buildNamedComboLookup, } from "./planResolution.ts"; import { resolveAdaptivePlan } from "./adaptiveCompression/resolveAdaptivePlan.ts"; import type { AdaptiveTelemetry } from "./adaptiveCompression/types.ts"; import type { RiskGateConfig } from "./riskGate/riskGate.ts"; import { resolveRiskGate, withRiskGate, withRiskGateAsync } from "./riskGate/strategyWrap.ts"; import { withCompressionEntrypointGuards, withCompressionEntrypointGuardsAsync, } from "./entrypointWrap.ts"; import { makeMemoKey, memoLookup, memoStore, isDeterministicMode } from "./resultMemo.ts"; export { resolveCacheAwareConfig } from "./cacheAwareConfig.ts"; // Re-export so existing importers (resolver test + chatCore dynamic import) keep resolving. export { planFromHeader, formatCompressionMeta, formatCompressionAnnotation, buildNamedComboLookup, }; /** Named-combo map: combo id → its stacked pipeline (operator-defined profiles). */ type NamedCombos = Record; export function checkComboOverride( config: CompressionConfig, comboId: string | null ): CompressionMode | null { if (!comboId || !config.comboOverrides) return null; return config.comboOverrides[comboId] ?? null; } export function shouldAutoTrigger(config: CompressionConfig, estimatedTokens: number): boolean { return config.autoTriggerTokens > 0 && estimatedTokens >= config.autoTriggerTokens; } /** * Resolves the effective compression plan (mode + derived stacked pipeline) WITHOUT * the caching-aware mode adjustment (that is layered on by {@link selectCompressionPlan}). * * Precedence — preserved from the historical {@link getEffectiveMode} ordering: * 1. master off → off * 2. routing-combo override (comboId)→ that mode (resolver honors it via ctx.comboId) * 3. active named profile (Phase 2) → that combo's stacked pipeline (manual operator choice) * 4. auto-trigger (large prompt) → autoTriggerMode, BEFORE the plain derived default * 5. derived default → resolveCompressionPlan (engines map → mode/pipeline) * * Step 3 is an EXPLICIT operator selection (`config.activeComboId` resolved against the * `combos` map): it beats auto-trigger (a manual choice outranks automatic escalation) but * stays below a routing-combo override (route-scoped is more specific). Step 4 mirrors the * historical behaviour: auto-trigger precedes the plain derived default but never a routing * override. * * `combos` defaults to `{}` so Phase-1 callers are unchanged; when supplied, chatCore passes * its DB-loaded named-combo map so the active profile can resolve here purely (no DB import). */ /** True when the adaptive resolver owns automatic-by-size escalation (D-C4). */ function adaptiveEnabled(config: CompressionConfig): boolean { const mode = config.contextBudget?.mode; return mode === "floor" || mode === "replace-autotrigger"; } function resolveBasePlan( config: CompressionConfig, comboId: string | null, estimatedTokens: number, combos: NamedCombos = {}, header: string | null = null ): DerivedPlan { if (!config.enabled) return withSource({ mode: "off", stackedPipeline: [] }, "off"); // Phase 3: an explicit, recognized header wins over every operator layer (Decision B). // The master switch above is the hard kill: a header cannot turn compression on. if (header) { const fromHeader = planFromHeader(config, header, combos); if (fromHeader) return fromHeader; // already tagged "request-header" } const comboMode = checkComboOverride(config, comboId); if (comboMode) { // A routing-combo "stacked" override still wants the configured stacked pipeline, // so route it through the resolver (which reads config.stackedPipeline for stacked). return withSource(resolveCompressionPlan(config, { comboId, combos }), "routing-override"); } // Active profile: an EXPLICIT operator choice. Resolves regardless of enginesExplicit and // above auto-trigger (manual choice beats automatic escalation), but below a routing-combo // override (route-scoped is more specific). if (config.activeComboId && combos[config.activeComboId]) { return withSource( { mode: "stacked", stackedPipeline: combos[config.activeComboId] }, "active-profile" ); } if (!adaptiveEnabled(config) && shouldAutoTrigger(config, estimatedTokens)) { const mode = config.autoTriggerMode ?? "lite"; return withSource( mode === "stacked" ? { mode, stackedPipeline: config.stackedPipeline ?? [] } : { mode, stackedPipeline: [] }, "auto-trigger" ); } const plan = deriveDefaultPlanFromConfig(config, comboId, combos); return withSource(plan, plan.mode === "off" ? "off" : "default"); } /** * True when the EXPLICITLY-configured engines map (panel-saved) derives a multi-engine * stacked pipeline. chatCore uses this to know the panel's derived pipeline is authoritative * and the legacy default-combo fallback must NOT override it. Returns false for legacy * (non-explicit) installs so their historical default-combo path is preserved untouched. */ export function enginesMapDerivesStackedPipeline(config: CompressionConfig): boolean { if (!config.enginesExplicit) return false; const plan = deriveDefaultPlan(config.engines ?? {}, config.enabled !== false); return plan.mode === "stacked" && plan.stackedPipeline.length > 0; } /** * True when the config has an active named-combo selection that exists in the supplied combos * map. chatCore uses this to keep the legacy default-combo fallback from shadowing the * operator's active profile. */ export function activeComboResolves(config: CompressionConfig, combos: NamedCombos = {}): boolean { return Boolean(config.activeComboId && combos[config.activeComboId]); } export function getEffectiveMode( config: CompressionConfig, comboId: string | null, estimatedTokens: number, combos: NamedCombos = {}, header: string | null = null ): CompressionMode { return resolveBasePlan(config, comboId, estimatedTokens, combos, header).mode as CompressionMode; } /** * Like {@link selectCompressionStrategy} but returns the full derived plan * (effective `mode` + `stackedPipeline`). When the resolver derives a `stacked` * plan from the per-engine toggle map, the pipeline is exposed here so the caller * can feed it to {@link applyCompressionAsync} (which reads config.stackedPipeline). * The caching-aware mode adjustment is applied to `mode` exactly as in * {@link selectCompressionStrategy}. */ /** Adaptive (Sub-project C) inputs + telemetry sink for selectCompressionPlan. */ export interface AdaptiveSelectOptions { modelContextLimit?: number | null; requestMaxTokens?: number | null; onAdaptive?: (telemetry: AdaptiveTelemetry) => void; } export function selectCompressionPlan( config: CompressionConfig, comboId: string | null, estimatedTokens: number, body?: Record, context?: CachingDetectionContext, combos: NamedCombos = {}, header: string | null = null, adaptiveOptions?: AdaptiveSelectOptions ): DerivedPlan { let plan = resolveBasePlan(config, comboId, estimatedTokens, combos, header); // Adaptive context-budget floor/escalation (D-C4): after the base plan, replacing the // (now-bypassed) auto-trigger branch. Pure resolver; chatCore supplies the model limit. if (adaptiveEnabled(config) && config.contextBudget) { const { plan: adaptivePlan, telemetry } = resolveAdaptivePlan({ basePlan: plan, estimatedTokens, modelContextLimit: adaptiveOptions?.modelContextLimit ?? null, requestMaxTokens: adaptiveOptions?.requestMaxTokens ?? null, config: config.contextBudget, }); plan = adaptivePlan; if (telemetry && adaptiveOptions?.onAdaptive) adaptiveOptions.onAdaptive(telemetry); } // Apply caching-aware adjustments to the mode if body is provided if (body) { const ctx = detectCachingContext(body, context); const cacheAware = getCacheAwareStrategy(plan.mode as CompressionMode, ctx); return { ...plan, mode: cacheAware.strategy as CompressionMode }; // ...plan preserves source } return plan; } export function selectCompressionStrategy( config: CompressionConfig, comboId: string | null, estimatedTokens: number, body?: Record, context?: CachingDetectionContext, combos: NamedCombos = {}, header: string | null = null ): CompressionMode { return selectCompressionPlan(config, comboId, estimatedTokens, body, context, combos, header) .mode as CompressionMode; } export function applyCompression( body: Record, mode: CompressionMode, options?: { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; principalId?: string; /** * Opt into the TV1 stacked bail-out (skip-on-throw + min-gain). Default off keeps the * legacy behavior. The combo proactive-fallback path enables it so a throwing engine is * skipped instead of silently dropping the target. Flows through to applyStackedCompression. */ bailout?: BailoutConfig; /** Risk-gate mask/restore wrapper (opt-in, default off). Read via resolveRiskGate. */ riskGate?: RiskGateConfig; /** Force/override the caching gate (studio dry-run, or chatCore's resolved context). */ cachingContext?: CachingDetectionContext; } ): CompressionResult { return withCompressionEntrypointGuards(body, options, (b) => runCompression(b, mode, options)); } function runCompression( body: Record, mode: CompressionMode, options?: { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; principalId?: string; bailout?: BailoutConfig; riskGate?: RiskGateConfig; cachingContext?: CachingDetectionContext; } ): CompressionResult { if (mode === "off") { return { body, compressed: false, stats: null }; } if ( options?.config?.memoizeCompressionResults === true && // Only memoize for an explicit principal — a missing principalId would collapse // authenticated callers into the shared anonymous (null) key space and let one // principal receive another's cached body. No principal ⇒ skip the cache. typeof options?.principalId === "string" && options.principalId.length > 0 && isDeterministicMode(mode, options.config) ) { const key = makeMemoKey( body, mode, options.config, options.principalId, options.model, options.supportsVision ); const hit = memoLookup(key); if (hit) return hit; const result = runCompression({ ...body }, mode, { ...options, config: { ...options.config, memoizeCompressionResults: false }, }); memoStore(key, result); return memoLookup(key)!; } if (mode === "rtk") { return applyRtkCompression(body, { // Selecting the "rtk" mode IS the enable signal — run it even if the per-engine // rtkConfig.enabled flag is off (that flag gates stacked steps). (B-MODE-ENGINE-DECOUPLE) config: { ...(options?.config?.rtkConfig ?? {}), enabled: true }, }); } const adapter = adaptBodyForCompression(body); const compressionBody = adapter.body; if (mode === "lite") { const result = applyLiteCompression(compressionBody, { ...options, preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, }); return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result; } if (mode === "stacked") { const result = applyStackedCompression( compressionBody, options?.config?.stackedPipeline, options ); return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result; } if (mode === "standard") { const cavemanConfig = { ...(options?.config?.cavemanConfig ?? {}), ...(options?.config?.languageConfig?.enabled ? { language: options.config.languageConfig.defaultLanguage, autoDetectLanguage: options.config.languageConfig.autoDetect, enabledLanguagePacks: options.config.languageConfig.enabledPacks, } : {}), ...(options?.config?.preserveSystemPrompt !== false ? { compressRoles: (options?.config?.cavemanConfig?.compressRoles ?? ["user"]).filter( (role) => role !== "system" ), } : {}), // Selecting the "standard" mode runs caveman regardless of the per-engine // cavemanConfig.enabled flag (that flag gates stacked steps). (B-MODE-ENGINE-DECOUPLE) enabled: true, }; const result = cavemanCompress( compressionBody as Parameters[0], cavemanConfig ); return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result; } if (mode === "aggressive") { const messages = (compressionBody.messages ?? []) as Array<{ role: string; content?: string | Array<{ type: string; text?: string }>; [key: string]: unknown; }>; if (!Array.isArray(messages) || messages.length === 0) { return { body, compressed: false, stats: null }; } const aggressiveConfig = { ...(options?.config?.aggressive ?? {}), preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, }; const result = compressAggressive(messages, aggressiveConfig); const compressedBody = { ...compressionBody, messages: result.messages }; return { body: adapter.restore(compressedBody), compressed: result.stats.savingsPercent > 0, stats: createCompressionStats( compressionBody, compressedBody, mode, ["aggressive"], result.stats.rulesApplied, result.stats.durationMs ), }; } if (mode === "ultra") { const messages = (compressionBody.messages ?? []) as Array<{ role: string; content?: string | unknown[]; [key: string]: unknown; }>; if (!Array.isArray(messages) || messages.length === 0) { return { body, compressed: false, stats: null }; } const ultraConfig = { ...(options?.config?.ultra ?? {}), preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, }; const result = ultraCompressHeuristic(messages, ultraConfig); const compressedBody = { ...compressionBody, messages: result.messages }; return { body: adapter.restore(compressedBody), compressed: result.stats.savingsPercent > 0, stats: { ...createCompressionStats( compressionBody, compressedBody, mode, ["ultra"], result.stats.rulesApplied, result.stats.durationMs ), ultraTier: result.stats.ultraTier, }, }; } return { body, compressed: false, stats: null }; } /** * Async entry point mirroring {@link applyCompression}. Only the stacked mode * can host async engines, so it routes through {@link applyStackedCompressionAsync}; * every other mode delegates to the synchronous path unchanged. Call sites that * already run in an async context (e.g. chatCore) await this so a future * worker-thread engine can await without changing the surrounding code. */ export async function applyCompressionAsync( body: Record, mode: CompressionMode, options?: { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; principalId?: string; onEngineStep?: (step: StackedCompressionStep) => void; cachingContext?: CachingDetectionContext; } ): Promise { return withCompressionEntrypointGuardsAsync(body, options, (b) => runCompressionAsync(b, mode, options) ); } async function runCompressionAsync( body: Record, mode: CompressionMode, options?: { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; principalId?: string; onEngineStep?: (step: StackedCompressionStep) => void; cachingContext?: CachingDetectionContext; } ): Promise { if ( options?.config?.memoizeCompressionResults === true && // Only memoize for an explicit principal — a missing principalId would collapse // authenticated callers into the shared anonymous (null) key space and let one // principal receive another's cached body. No principal ⇒ skip the cache. typeof options?.principalId === "string" && options.principalId.length > 0 && isDeterministicMode(mode, options.config) ) { const key = makeMemoKey( body, mode, options.config, options.principalId, options.model, options.supportsVision ); const hit = memoLookup(key); if (hit) return hit; const result = await runCompressionAsync({ ...body }, mode, { ...options, config: { ...options.config, memoizeCompressionResults: false }, }); memoStore(key, result); return memoLookup(key)!; } if (mode === "stacked") { const adapter = adaptBodyForCompression(body); const result = await applyStackedCompressionAsync( adapter.body, options?.config?.stackedPipeline, options ); return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result; } // Ultra's optional SLM (model) tier is async — route it here when a model is configured. if (mode === "ultra") { return applyUltraAsync(body, options); } return applyCompression(body, mode, options); } /** * Ultra mode with the optional local SLM (model) tier. * * When `config.ultra.modelPath` is set, the prose is routed through the llmlingua engine * (the real local-model compressor). The llmlingua backend fail-opens when the model is * absent (e.g. the ONNX model is not provisioned), so this degrades gracefully: * - model present and it compresses → return the SLM result (tagged "ultra-slm"); * - model absent / no gain / failure → fall back to `aggressive` when * `slmFallbackToAggressive` is set, otherwise the heuristic ultra (`pruneByScore`). * * Without `modelPath` the behavior is byte-identical to the synchronous heuristic ultra. */ async function applyUltraAsync( body: Record, options?: { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; principalId?: string; onEngineStep?: (step: StackedCompressionStep) => void; } ): Promise { const ultraConfig = options?.config?.ultra; const modelPath = typeof ultraConfig?.modelPath === "string" ? ultraConfig.modelPath.trim() : ""; // No explicit modelPath → run the two-tier ultra resolver (heuristic, or SLM when // config.ultraEngine === "slm" and the worker backend is available). This is the // Phase-4 (B) path; it fail-opens to the heuristic and records the resolved tier. if (!modelPath) { const adapter = adaptBodyForCompression(body); const messages = (adapter.body.messages ?? []) as Array<{ role: string; content?: string | unknown[]; [key: string]: unknown; }>; if (!Array.isArray(messages) || messages.length === 0) { return { body, compressed: false, stats: null }; } const ultraConfig = { ...(options?.config?.ultra ?? {}), preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, ultraEngine: options?.config?.ultraEngine, }; const result = await ultraCompress(messages, ultraConfig); const compressedBody = { ...adapter.body, messages: result.messages }; return { body: adapter.restore(compressedBody), compressed: result.stats.savingsPercent > 0, stats: { ...createCompressionStats( adapter.body, compressedBody, "ultra", result.stats.techniquesUsed, result.stats.rulesApplied, result.stats.durationMs ), ultraTier: result.stats.ultraTier, }, }; } registerBuiltinCompressionEngines(); const slmEngine = getCompressionEngine("llmlingua"); if (slmEngine?.applyAsync) { const engineOptions: CompressionEngineApplyOptions = { model: options?.model, supportsVision: options?.supportsVision, config: options?.config, principalId: options?.principalId, stepConfig: { modelPath, ...(typeof ultraConfig?.compressionRate === "number" ? { compressionRate: ultraConfig.compressionRate } : {}), }, }; try { const slm = await slmEngine.applyAsync(body, engineOptions); if (slm.compressed && slm.stats) { // Attribute the result to ultra (the selected mode) while marking the SLM tier. return { ...slm, stats: { ...slm.stats, mode: "ultra", techniquesUsed: Array.from(new Set([...(slm.stats.techniquesUsed ?? []), "ultra-slm"])), }, }; } } catch { // llmlingua fail-opens internally, but guard anyway and use the configured fallback. } } // SLM tier unavailable or produced no gain → fall back per slmFallbackToAggressive. return applyCompression( body, ultraConfig?.slmFallbackToAggressive ? "aggressive" : "ultra", options ); } function normalizePipelineStep(step: CompressionPipelineStep | string): CompressionPipelineStep { if (typeof step !== "string") return step; if (step === "standard") return { engine: "caveman" }; if (step === "rtk") return { engine: "rtk" }; if (step === "lite" || step === "aggressive" || step === "ultra") return { engine: step }; return { engine: "caveman" }; } /** Per-engine progress emitted mid-pipeline by the stacked loops (F3.3 live streaming). */ export interface StackedCompressionStep { stepIndex: number; totalSteps: number; engine: string; state: "done" | "skipped"; originalTokens: number; compressedTokens: number; savingsPercent: number; durationMs?: number; } interface StackOptions { model?: string; supportsVision?: boolean | null; config?: CompressionConfig; compressionComboId?: string | null; /** TV1 bail-out discipline (opt-in, default disabled). */ bailout?: BailoutConfig; /** T02 per-engine circuit-breaker (opt-in, default disabled). Falls back to config + env. */ circuitBreaker?: Partial; /** Opt-in per-step fidelity gate (default disabled). */ fidelityGate?: FidelityGateConfig; /** Risk-gate mask/restore wrapper (opt-in, default off). Read via resolveRiskGate. */ riskGate?: RiskGateConfig; /** Authenticated principal id — threaded through to CCR engine for store scoping. */ principalId?: string; /** F3.3: called once per engine as it completes (live per-engine streaming). */ onEngineStep?: (step: StackedCompressionStep) => void; } /** Emit a per-engine step to the live streaming callback (best-effort, no-op when unset). */ function reportEngineStep( onStep: ((step: StackedCompressionStep) => void) | undefined, stepIndex: number, totalSteps: number, engine: string, result: CompressionResult ): void { if (!onStep) return; const s = result.stats; onStep({ stepIndex, totalSteps, engine, state: result.compressed ? "done" : "skipped", originalTokens: s?.originalTokens ?? 0, compressedTokens: s?.compressedTokens ?? s?.originalTokens ?? 0, savingsPercent: s?.savingsPercent ?? 0, ...(s?.durationMs !== undefined ? { durationMs: s.durationMs } : {}), }); } function resolveStackSteps( pipeline?: Array ): CompressionPipelineStep[] { return pipeline && pipeline.length > 0 ? pipeline.map(normalizePipelineStep) : [ { engine: "rtk", intensity: "standard" }, { engine: "caveman", intensity: "full" }, ]; } function buildStepOptions( step: CompressionPipelineStep, options?: StackOptions ): CompressionEngineApplyOptions { return { ...options, compressionComboId: options?.compressionComboId ?? options?.config?.compressionComboId, principalId: options?.principalId, stepConfig: { ...(step.config ?? {}), ...(step.intensity ? { intensity: step.intensity } : {}), }, }; } function finalizeStackedResult( originalBody: Record, currentBody: Record, compressed: boolean, acc: StackAccumulator, start: number, compressionComboId: string | null | undefined ): CompressionResult { const stats = createCompressionStats( originalBody, currentBody, "stacked", Array.from(acc.techniques), acc.rules.size > 0 ? Array.from(acc.rules) : undefined, Math.round((performance.now() - start) * 100) / 100 ); stats.engine = "stacked"; stats.compressionComboId = compressionComboId ?? null; stats.engineBreakdown = acc.breakdown; if (acc.validationWarnings.size > 0) { stats.validationWarnings = Array.from(acc.validationWarnings); } if (acc.validationErrors.size > 0) { stats.validationErrors = Array.from(acc.validationErrors); } if (acc.fallbackApplied) { stats.fallbackApplied = true; } if (acc.rtkRawOutputPointers.length > 0) { const seenPointers = new Set(); stats.rtkRawOutputPointers = acc.rtkRawOutputPointers.filter((pointer) => { if (seenPointers.has(pointer.id)) return false; seenPointers.add(pointer.id); return true; }); } // T02 / H1: honest aggregate inflation guard. If the fully-stacked body did not actually shrink // (its token count is >= the original), discard it and return the verbatim original — safe by // construction, since the original request body is always a valid payload. const inflation = guardPipelineInflation({ originalBody, compressedBody: currentBody, originalTokens: stats.originalTokens, compressedTokens: stats.compressedTokens, }); if (inflation.inflated) { const inflatedTokens = stats.compressedTokens; const warnings = new Set(stats.validationWarnings ?? []); warnings.add( `pipeline-inflation-guard: stacked output (${inflatedTokens} tok) did not shrink input ` + `(${stats.originalTokens} tok); reverted to original` ); stats.validationWarnings = Array.from(warnings); stats.fallbackApplied = true; stats.compressedTokens = stats.originalTokens; stats.savingsPercent = 0; return { body: inflation.body, compressed: false, stats }; } return { body: currentBody, compressed, stats }; } // ── Shared per-step helpers (used by the sync + async stacked loops; keep them in lockstep) ── interface StepCommitCtx { bailout?: BailoutConfig; breakerOn: boolean; breaker: PipelineCircuitBreakerConfig; fidelityGate?: FidelityGateConfig; } /** Failure path: record the breaker failure (when on) + keep the verbatim body, surfacing it in telemetry. */ function recordStepFailure( acc: StackAccumulator, engineId: string, err: unknown, ctx: StepCommitCtx ): void { if (ctx.breakerOn) recordEngineFailure(engineId, ctx.breaker); acc.validationErrors.add( `${engineId}: bailed out — ${err instanceof Error ? err.message : String(err)}` ); acc.fallbackApplied = true; } /** * Success path: record the breaker success (when on), merge telemetry, and decide whether to * advance `currentBody`. Advance rule: TV1 bail-out uses min-gain (`decideStep`); otherwise the * legacy `result.compressed`. Returns the (possibly unchanged) body + whether it advanced. */ function commitStepResult( acc: StackAccumulator, step: CompressionPipelineStep, result: CompressionResult, currentBody: Record, ctx: StepCommitCtx ): { body: Record; advanced: boolean } { if (ctx.breakerOn) recordEngineSuccess(step.engine, ctx.breaker); mergeStackStep(acc, step.engine, result); const advance = ctx.bailout?.enabled ? decideStep(result, ctx.bailout).advance : result.compressed; if (advance && gateAdvance(result, currentBody, ctx.fidelityGate, acc, step.engine)) { return { body: result.body, advanced: true }; } return { body: currentBody, advanced: false }; } export function applyStackedCompression( body: Record, pipeline?: Array, options?: StackOptions ): CompressionResult { return withRiskGate(body, resolveRiskGate(options), (b) => runStackedCompression(b, pipeline, options) ); } function runStackedCompression( body: Record, pipeline?: Array, options?: StackOptions ): CompressionResult { const steps = resolveStackSteps(pipeline); registerBuiltinCompressionEngines(); let currentBody = body; let compressed = false; const acc = createStackAccumulator(); const start = performance.now(); const bailout = options?.bailout; const breaker = resolvePipelineBreakerConfig( options?.circuitBreaker ?? options?.config?.pipelineCircuitBreaker ); const breakerOn = breaker.enabled; const fidelityGate = options?.fidelityGate ?? options?.config?.fidelityGate; const onStep = options?.onEngineStep; const totalSteps = steps.length; let stepIdx = 0; for (const step of steps) { const engine = getCompressionEngine(step.engine); if (!engine) { acc.validationErrors.add(`Unknown compression engine: "${step.engine}"`); continue; } // Respect the registry enabled flag: a step naming a disabled engine is skipped, so an // operator can turn an engine off (setEngineEnabled) without editing every pipeline. if (getEngineEntry(step.engine)?.enabled === false) continue; // T02: when the per-engine breaker is OPEN, skip this step (verbatim body kept — fail-open). if (breakerOn && !canRunEngine(step.engine, breaker)) { acc.validationWarnings.add(`${step.engine}: skipped (pipeline circuit-breaker open)`); continue; } // TV1 bail-out (per-request) OR T02 breaker (cross-request) wrap the call so a throwing engine // is caught + recorded; when neither is on, a throw propagates (byte-identical to legacy). const ctx = { bailout, breakerOn, breaker, fidelityGate }; let result: CompressionResult; if (bailout?.enabled || breakerOn) { try { result = engine.apply(currentBody, buildStepOptions(step, options)); } catch (err) { recordStepFailure(acc, step.engine, err, ctx); continue; } } else { result = engine.apply(currentBody, buildStepOptions(step, options)); } const committed = commitStepResult(acc, step, result, currentBody, ctx); currentBody = committed.body; if (committed.advanced) compressed = true; // The pre-existing bail-out path did not stream per-step; everything else does. if (!bailout?.enabled) reportEngineStep(onStep, stepIdx++, totalSteps, step.engine, result); } // Hard-budget post-pass (#17): runs after all engines, before finalize. if (options?.config?.targetTokens != null || options?.config?.targetRatio != null) { const hbResult = applyHardBudget(currentBody, { targetTokens: options.config.targetTokens, targetRatio: options.config.targetRatio, }); if (hbResult.compressed) { mergeStackStep(acc, "hard-budget", hbResult); currentBody = hbResult.body; compressed = true; } else { // No unit could be dropped (e.g. every unit is preserve-guarded): surface the // unreachable-budget validationWarnings instead of dropping them silently (#17 fix #3). // mergeStackStep is gated on `compressed`, so propagate the warnings here directly. hbResult.stats?.validationWarnings?.forEach((w) => acc.validationWarnings.add(w)); } } return finalizeStackedResult( body, currentBody, compressed, acc, start, options?.compressionComboId ?? options?.config?.compressionComboId ); } /** * Async sibling of {@link applyStackedCompression} (H10). Awaits engines that * expose `applyAsync` (e.g. worker-thread models) and runs synchronous engines * inline. Behaviour is otherwise identical: same step order, same accumulated * telemetry, same final stats — so sync-only pipelines yield the same result. */ export async function applyStackedCompressionAsync( body: Record, pipeline?: Array, options?: StackOptions ): Promise { return withRiskGateAsync(body, resolveRiskGate(options), (b) => runStackedCompressionAsync(b, pipeline, options) ); } async function runStackedCompressionAsync( body: Record, pipeline?: Array, options?: StackOptions ): Promise { const steps = resolveStackSteps(pipeline); registerBuiltinCompressionEngines(); let currentBody = body; let compressed = false; const acc = createStackAccumulator(); const start = performance.now(); const bailout = options?.bailout; const breaker = resolvePipelineBreakerConfig( options?.circuitBreaker ?? options?.config?.pipelineCircuitBreaker ); const breakerOn = breaker.enabled; const fidelityGate = options?.fidelityGate ?? options?.config?.fidelityGate; const onStep = options?.onEngineStep; const totalSteps = steps.length; let stepIdx = 0; for (const step of steps) { const engine = getCompressionEngine(step.engine); if (!engine) { acc.validationErrors.add(`Unknown compression engine: "${step.engine}"`); continue; } // Respect the registry enabled flag (same as the sync loop) — keep both in lockstep. if (getEngineEntry(step.engine)?.enabled === false) continue; // T02: skip an engine whose breaker is OPEN (verbatim body kept — fail-open). Lockstep w/ sync. if (breakerOn && !canRunEngine(step.engine, breaker)) { acc.validationWarnings.add(`${step.engine}: skipped (pipeline circuit-breaker open)`); continue; } const stepOptions = buildStepOptions(step, options); // TV1 bail-out (per-request) OR T02 breaker (cross-request) wrap the call (lockstep w/ sync). const ctx = { bailout, breakerOn, breaker, fidelityGate }; let result: CompressionResult; if (bailout?.enabled || breakerOn) { try { result = engine.applyAsync ? await engine.applyAsync(currentBody, stepOptions) : engine.apply(currentBody, stepOptions); } catch (err) { recordStepFailure(acc, step.engine, err, ctx); continue; } } else { result = engine.applyAsync ? await engine.applyAsync(currentBody, stepOptions) : engine.apply(currentBody, stepOptions); } const committed = commitStepResult(acc, step, result, currentBody, ctx); currentBody = committed.body; if (committed.advanced) compressed = true; if (!bailout?.enabled) reportEngineStep(onStep, stepIdx++, totalSteps, step.engine, result); } // Hard-budget post-pass (#17): runs after all engines, before finalize. if (options?.config?.targetTokens != null || options?.config?.targetRatio != null) { const hbResult = applyHardBudget(currentBody, { targetTokens: options.config.targetTokens, targetRatio: options.config.targetRatio, }); if (hbResult.compressed) { mergeStackStep(acc, "hard-budget", hbResult); currentBody = hbResult.body; compressed = true; } else { // No unit could be dropped (e.g. every unit is preserve-guarded): surface the // unreachable-budget validationWarnings instead of dropping them silently (#17 fix #3). // mergeStackStep is gated on `compressed`, so propagate the warnings here directly. hbResult.stats?.validationWarnings?.forEach((w) => acc.validationWarnings.add(w)); } } return finalizeStackedResult( body, currentBody, compressed, acc, start, options?.compressionComboId ?? options?.config?.compressionComboId ); }