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

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);
},
};