Files
2026-07-13 13:39:12 +08:00

105 lines
4.2 KiB
TypeScript

import type { DerivedPlan } from "../deriveDefaultPlan.ts";
import type { AdaptiveTelemetry, ContextBudgetConfig, LadderStage } from "./types.ts";
import { DEFAULT_LADDER, aggressivenessOf, expectedReductionFactor } from "./ladder.ts";
import { computeTarget } from "./computeTarget.ts";
export interface ResolveAdaptiveInput {
basePlan: DerivedPlan;
estimatedTokens: number;
modelContextLimit: number | null;
requestMaxTokens: number | null;
config: ContextBudgetConfig;
/**
* Injected per-stage estimator (design §4.1/§9). Returns the NEW token estimate after a
* stage is applied to a prompt of `priorTokens`. Default models it with the cheap
* per-engine expected-reduction factor (no dry-run, no real tokenizer).
*/
estimate?: (priorTokens: number, stage: LadderStage) => number;
}
export interface ResolveAdaptiveResult {
plan: DerivedPlan;
telemetry: AdaptiveTelemetry | null;
}
const defaultEstimate = (prior: number, stage: LadderStage): number =>
Math.round(prior * expectedReductionFactor(stage.engine));
/**
* Pure adaptive resolver (design §4.2). Floors/escalates a base plan so the (estimated)
* compressed prompt fits the context budget. Never drops content: if the ladder is
* exhausted while still over target, returns the best-effort plan with fit=false.
*
* Returns telemetry=null only when there is no usable target (unknown model limit) — the
* caller then skips adaptive entirely (design §6: skip, never throw).
*/
export function resolveAdaptivePlan(input: ResolveAdaptiveInput): ResolveAdaptiveResult {
const { basePlan, estimatedTokens, modelContextLimit, requestMaxTokens, config } = input;
const estimate = input.estimate ?? defaultEstimate;
if (config.mode === "off") return { plan: basePlan, telemetry: null };
if (!modelContextLimit || modelContextLimit <= 0) {
return { plan: basePlan, telemetry: null }; // unknown limit → skip (D-C / §6)
}
const target = computeTarget(config.policy, modelContextLimit, requestMaxTokens, config);
const headroomBefore = target - estimatedTokens;
// replace-autotrigger only acts on a bare Default/off base plan; an explicit choice wins.
const baseRank = aggressivenessOf(basePlan.mode);
if (config.mode === "replace-autotrigger" && baseRank > aggressivenessOf("off")) {
return {
plan: basePlan,
telemetry: { policy: config.policy, target, headroomBefore, stagesApplied: [], headroomAfter: headroomBefore, fit: headroomBefore >= 0 },
};
}
// Already fits → never over-compress (D-C2).
if (headroomBefore >= 0) {
return {
plan: basePlan,
telemetry: { policy: config.policy, target, headroomBefore, stagesApplied: [], headroomAfter: headroomBefore, fit: true },
};
}
// Escalation: start just ABOVE the base plan's aggressiveness (floor escalates beyond it).
const ladder = config.ladderOverride && config.ladderOverride.length > 0 ? config.ladderOverride : DEFAULT_LADDER;
const startTier = config.mode === "floor" ? baseRank : aggressivenessOf("off");
const stages = ladder.filter((s) => aggressivenessOf(s.engine) > startTier);
let current = estimatedTokens;
const applied: LadderStage[] = [];
for (const stage of stages) {
current = estimate(current, stage);
applied.push(stage);
if (current <= target) break;
}
const headroomAfter = target - current;
const fit = headroomAfter >= 0;
return {
plan: planFromStages(basePlan, applied),
telemetry: {
policy: config.policy,
target,
headroomBefore,
stagesApplied: applied.map((s) => s.engine),
headroomAfter,
fit,
},
};
}
/**
* Turn the applied ladder stages into a DerivedPlan. A single applied stage that is a
* single-mode engine collapses to that engine's mode; otherwise a stacked pipeline.
* The base plan's own pipeline is preserved as the prefix (floor escalates AFTER the base).
*/
function planFromStages(basePlan: DerivedPlan, applied: LadderStage[]): DerivedPlan {
if (applied.length === 0) return basePlan;
const basePipeline =
basePlan.mode === "stacked" ? basePlan.stackedPipeline : [];
const stackedPipeline = [...basePipeline, ...applied];
return { mode: "stacked", stackedPipeline };
}