455 lines
14 KiB
TypeScript
455 lines
14 KiB
TypeScript
import { applyLiteCompression } from "../lite.ts";
|
|
import { cavemanCompress } from "../caveman.ts";
|
|
import { compressAggressive } from "../aggressive.ts";
|
|
import { ultraCompressHeuristic } from "../ultra.ts";
|
|
import { createCompressionStats } from "../stats.ts";
|
|
import { adaptBodyForCompression } from "../bodyAdapter.ts";
|
|
import {
|
|
DEFAULT_AGGRESSIVE_CONFIG,
|
|
DEFAULT_ULTRA_CONFIG,
|
|
type CavemanIntensity,
|
|
} from "../types.ts";
|
|
import type { CompressionEngine, EngineConfigField, EngineValidationResult } from "./types.ts";
|
|
|
|
const CAVEMAN_INTENSITIES: CavemanIntensity[] = ["lite", "full", "ultra"];
|
|
|
|
const CAVEMAN_SCHEMA: EngineConfigField[] = [
|
|
{
|
|
key: "intensity",
|
|
type: "select",
|
|
label: "Intensity",
|
|
defaultValue: "full",
|
|
options: CAVEMAN_INTENSITIES.map((value) => ({ value, label: value })),
|
|
},
|
|
{
|
|
key: "minMessageLength",
|
|
type: "number",
|
|
label: "Minimum message length",
|
|
defaultValue: 50,
|
|
min: 0,
|
|
max: 10000,
|
|
},
|
|
{
|
|
key: "enabled",
|
|
type: "boolean",
|
|
label: "Enabled",
|
|
defaultValue: true,
|
|
},
|
|
];
|
|
|
|
const AGGRESSIVE_SCHEMA: EngineConfigField[] = [
|
|
{
|
|
key: "summarizerEnabled",
|
|
type: "boolean",
|
|
label: "Summarizer enabled",
|
|
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.summarizerEnabled,
|
|
},
|
|
{
|
|
key: "maxTokensPerMessage",
|
|
type: "number",
|
|
label: "Max tokens per message",
|
|
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.maxTokensPerMessage,
|
|
min: 256,
|
|
max: 32768,
|
|
},
|
|
{
|
|
key: "minSavingsThreshold",
|
|
type: "number",
|
|
label: "Minimum savings threshold",
|
|
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.minSavingsThreshold,
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
{
|
|
key: "preserveSystemPrompt",
|
|
type: "boolean",
|
|
label: "Preserve system prompt",
|
|
defaultValue: true,
|
|
},
|
|
];
|
|
|
|
const ULTRA_SCHEMA: EngineConfigField[] = [
|
|
{
|
|
key: "enabled",
|
|
type: "boolean",
|
|
label: "Enabled",
|
|
defaultValue: DEFAULT_ULTRA_CONFIG.enabled,
|
|
},
|
|
{
|
|
key: "compressionRate",
|
|
type: "number",
|
|
label: "Compression rate",
|
|
defaultValue: DEFAULT_ULTRA_CONFIG.compressionRate,
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
{
|
|
key: "minScoreThreshold",
|
|
type: "number",
|
|
label: "Minimum score threshold",
|
|
defaultValue: DEFAULT_ULTRA_CONFIG.minScoreThreshold,
|
|
min: 0,
|
|
max: 1,
|
|
},
|
|
{
|
|
key: "slmFallbackToAggressive",
|
|
type: "boolean",
|
|
label: "Fallback to aggressive",
|
|
defaultValue: DEFAULT_ULTRA_CONFIG.slmFallbackToAggressive,
|
|
},
|
|
{
|
|
key: "modelPath",
|
|
type: "string",
|
|
label: "Model path",
|
|
defaultValue: "",
|
|
},
|
|
{
|
|
key: "maxTokensPerMessage",
|
|
type: "number",
|
|
label: "Max tokens per message",
|
|
defaultValue: DEFAULT_ULTRA_CONFIG.maxTokensPerMessage,
|
|
min: 0,
|
|
max: 32768,
|
|
},
|
|
{
|
|
key: "preserveSystemPrompt",
|
|
type: "boolean",
|
|
label: "Preserve system prompt",
|
|
defaultValue: true,
|
|
},
|
|
];
|
|
|
|
function ok(): EngineValidationResult {
|
|
return { valid: true, errors: [] };
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function validateBoolean(config: Record<string, unknown>, key: string, errors: string[]): void {
|
|
if (config[key] !== undefined && typeof config[key] !== "boolean") {
|
|
errors.push(`${key} must be a boolean`);
|
|
}
|
|
}
|
|
|
|
function validateNumberRange(
|
|
config: Record<string, unknown>,
|
|
key: string,
|
|
min: number,
|
|
max: number,
|
|
errors: string[]
|
|
): void {
|
|
const value = config[key];
|
|
if (value === undefined) return;
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
errors.push(`${key} must be a number between ${min} and ${max}`);
|
|
}
|
|
}
|
|
|
|
function validateCavemanLikeConfig(config: Record<string, unknown>): EngineValidationResult {
|
|
const errors: string[] = [];
|
|
if (
|
|
config.intensity !== undefined &&
|
|
!CAVEMAN_INTENSITIES.includes(config.intensity as CavemanIntensity)
|
|
) {
|
|
errors.push("intensity must be lite, full, or ultra");
|
|
}
|
|
if (
|
|
config.minMessageLength !== undefined &&
|
|
(typeof config.minMessageLength !== "number" || config.minMessageLength < 0)
|
|
) {
|
|
errors.push("minMessageLength must be a non-negative number");
|
|
}
|
|
if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
|
|
errors.push("enabled must be a boolean");
|
|
}
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
function validateAggressiveConfig(config: Record<string, unknown>): EngineValidationResult {
|
|
const errors: string[] = [];
|
|
validateBoolean(config, "summarizerEnabled", errors);
|
|
validateBoolean(config, "preserveSystemPrompt", errors);
|
|
validateNumberRange(config, "maxTokensPerMessage", 256, 32768, errors);
|
|
validateNumberRange(config, "minSavingsThreshold", 0, 1, errors);
|
|
|
|
if (config.thresholds !== undefined) {
|
|
if (!isRecord(config.thresholds)) {
|
|
errors.push("thresholds must be an object");
|
|
} else {
|
|
for (const key of ["fullSummary", "moderate", "light", "verbatim"]) {
|
|
validateNumberRange(config.thresholds, key, 1, 100, errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (config.toolStrategies !== undefined) {
|
|
if (!isRecord(config.toolStrategies)) {
|
|
errors.push("toolStrategies must be an object");
|
|
} else {
|
|
for (const key of ["fileContent", "grepSearch", "shellOutput", "json", "errorMessage"]) {
|
|
validateBoolean(config.toolStrategies, key, errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
function validateUltraConfig(config: Record<string, unknown>): EngineValidationResult {
|
|
const errors: string[] = [];
|
|
validateBoolean(config, "enabled", errors);
|
|
validateBoolean(config, "slmFallbackToAggressive", errors);
|
|
validateBoolean(config, "preserveSystemPrompt", errors);
|
|
validateNumberRange(config, "compressionRate", 0, 1, errors);
|
|
validateNumberRange(config, "minScoreThreshold", 0, 1, errors);
|
|
validateNumberRange(config, "maxTokensPerMessage", 0, 32768, errors);
|
|
if (config.modelPath !== undefined && typeof config.modelPath !== "string") {
|
|
errors.push("modelPath must be a string");
|
|
}
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
// Lite only honors `preserveSystemPrompt` (model/vision are runtime, not user config).
|
|
// Previously this engine wrongly exposed AGGRESSIVE_SCHEMA, surfacing irrelevant
|
|
// summarizer/threshold fields in the per-engine config UI.
|
|
const LITE_SCHEMA: EngineConfigField[] = [
|
|
{
|
|
key: "preserveSystemPrompt",
|
|
type: "boolean",
|
|
label: "Preserve system prompt",
|
|
defaultValue: true,
|
|
},
|
|
];
|
|
|
|
function validateLiteConfig(config: Record<string, unknown>): EngineValidationResult {
|
|
const errors: string[] = [];
|
|
if (
|
|
config.preserveSystemPrompt !== undefined &&
|
|
typeof config.preserveSystemPrompt !== "boolean"
|
|
) {
|
|
errors.push("preserveSystemPrompt must be a boolean");
|
|
}
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
export const liteEngine: CompressionEngine = {
|
|
id: "lite",
|
|
name: "Lite",
|
|
description: "Fast whitespace, tool-result and image URL reduction.",
|
|
icon: "compress",
|
|
targets: ["messages", "tool_results"],
|
|
stackable: true,
|
|
stackPriority: 5,
|
|
metadata: {
|
|
id: "lite",
|
|
name: "Lite",
|
|
description: "Fast whitespace, tool-result and image URL reduction.",
|
|
inputScope: "messages",
|
|
targetLatencyMs: 1,
|
|
supportsPreview: true,
|
|
stable: true,
|
|
},
|
|
apply(body, options) {
|
|
const adapter = adaptBodyForCompression(body);
|
|
const result = applyLiteCompression(adapter.body, {
|
|
...options,
|
|
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
|
});
|
|
return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result;
|
|
},
|
|
compress(body, config) {
|
|
return this.apply(body, { stepConfig: config });
|
|
},
|
|
getConfigSchema() {
|
|
return LITE_SCHEMA;
|
|
},
|
|
validateConfig(config) {
|
|
return validateLiteConfig(config);
|
|
},
|
|
};
|
|
|
|
export const cavemanEngine: CompressionEngine = {
|
|
id: "caveman",
|
|
name: "Caveman",
|
|
description: "Rule-based message compression with preservation and validation.",
|
|
icon: "compress",
|
|
targets: ["messages"],
|
|
stackable: true,
|
|
stackPriority: 20,
|
|
metadata: {
|
|
id: "caveman",
|
|
name: "Caveman",
|
|
description: "Rule-based message compression with preservation and validation.",
|
|
inputScope: "messages",
|
|
targetLatencyMs: 1,
|
|
supportsPreview: true,
|
|
stable: true,
|
|
},
|
|
apply(body, options) {
|
|
const adapter = adaptBodyForCompression(body);
|
|
// Mirror rtkAdapter's default-enabled behavior (see rtk/index.ts:530-535). When this engine
|
|
// is invoked as a stacked step without explicit `enabled` on either the cavemanConfig or the
|
|
// stepConfig, default `enabled: true` so the rules actually run. Without this,
|
|
// DEFAULT_CAVEMAN_CONFIG.enabled=false made cavemanCompress() a silent no-op, and the
|
|
// preview route's default [rtk, caveman] pipeline reported 0% savings even on trigger prose.
|
|
// (Issue #6425.)
|
|
const explicitCavemanConfig = options?.config?.cavemanConfig;
|
|
const explicitStepConfig = options?.stepConfig;
|
|
const explicitEnabled =
|
|
(explicitCavemanConfig && "enabled" in explicitCavemanConfig) ||
|
|
(explicitStepConfig && "enabled" in explicitStepConfig);
|
|
const enabledDefault = explicitEnabled ? {} : { enabled: true };
|
|
const cavemanConfig = {
|
|
...enabledDefault,
|
|
...(explicitCavemanConfig ?? {}),
|
|
...(explicitStepConfig ?? {}),
|
|
...(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"
|
|
),
|
|
}
|
|
: {}),
|
|
};
|
|
const result = cavemanCompress(
|
|
adapter.body as Parameters<typeof cavemanCompress>[0],
|
|
cavemanConfig
|
|
);
|
|
return adapter.adapted ? { ...result, body: adapter.restore(result.body) } : result;
|
|
},
|
|
compress(body, config) {
|
|
return this.apply(body, { stepConfig: config });
|
|
},
|
|
getConfigSchema() {
|
|
return CAVEMAN_SCHEMA;
|
|
},
|
|
validateConfig(config) {
|
|
return validateCavemanLikeConfig(config);
|
|
},
|
|
};
|
|
|
|
export const aggressiveEngine: CompressionEngine = {
|
|
id: "aggressive",
|
|
name: "Aggressive",
|
|
description: "Summarization, tool result compression and progressive aging.",
|
|
icon: "speed",
|
|
targets: ["messages", "tool_results"],
|
|
stackable: true,
|
|
stackPriority: 30,
|
|
metadata: {
|
|
id: "aggressive",
|
|
name: "Aggressive",
|
|
description: "Summarization, tool result compression and progressive aging.",
|
|
inputScope: "messages",
|
|
targetLatencyMs: 5,
|
|
supportsPreview: true,
|
|
stable: true,
|
|
},
|
|
apply(body, options) {
|
|
const adapter = adaptBodyForCompression(body);
|
|
const messages = (adapter.body.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 ?? {}),
|
|
...(options?.stepConfig ?? {}),
|
|
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
|
};
|
|
const result = compressAggressive(messages, aggressiveConfig);
|
|
const compressedBody = { ...adapter.body, messages: result.messages };
|
|
return {
|
|
body: adapter.restore(compressedBody),
|
|
compressed: result.stats.savingsPercent > 0,
|
|
stats: createCompressionStats(
|
|
adapter.body,
|
|
compressedBody,
|
|
"aggressive",
|
|
["aggressive"],
|
|
result.stats.rulesApplied,
|
|
result.stats.durationMs
|
|
),
|
|
};
|
|
},
|
|
compress(body, config) {
|
|
return this.apply(body, { stepConfig: config });
|
|
},
|
|
getConfigSchema() {
|
|
return AGGRESSIVE_SCHEMA;
|
|
},
|
|
validateConfig(config) {
|
|
return validateAggressiveConfig(config);
|
|
},
|
|
};
|
|
|
|
export const ultraEngine: CompressionEngine = {
|
|
id: "ultra",
|
|
name: "Ultra",
|
|
description: "Heuristic token pruning with optional local SLM fallback.",
|
|
icon: "bolt",
|
|
targets: ["messages"],
|
|
stackable: true,
|
|
stackPriority: 40,
|
|
metadata: {
|
|
id: "ultra",
|
|
name: "Ultra",
|
|
description: "Heuristic token pruning with optional local SLM fallback.",
|
|
inputScope: "messages",
|
|
targetLatencyMs: 5,
|
|
supportsPreview: true,
|
|
stable: true,
|
|
},
|
|
apply(body, options) {
|
|
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 ?? {}),
|
|
...(options?.stepConfig ?? {}),
|
|
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
|
};
|
|
const result = ultraCompressHeuristic(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",
|
|
["ultra"],
|
|
result.stats.rulesApplied,
|
|
result.stats.durationMs
|
|
),
|
|
};
|
|
},
|
|
compress(body, config) {
|
|
return this.apply(body, { stepConfig: config });
|
|
},
|
|
getConfigSchema() {
|
|
return ULTRA_SCHEMA;
|
|
},
|
|
validateConfig(config) {
|
|
return validateUltraConfig(config);
|
|
},
|
|
};
|