项目文件夹

文件
2026-07-13 13:39:12 +08:00

197 行
6.6 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* Hard-budget post-pass (#17): compress to ≤ N cl100k tokens.
*
* Deterministic, independent of the relevance engine. Splits prose into
* sentences/lines, ranks by average scoreToken ascending, drops the lowest-
* saliency units until the body fits the target, then reconstructs original order.
* Units containing FORCE_PRESERVE_RE anchors (errors, numbers, URLs, code) are
* never dropped.
*/
import type { CompressionResult } from "./types.ts";
import { scoreToken } from "./ultraHeuristic.ts";
import { countTextTokens } from "../../../src/shared/utils/tiktokenCounter.ts";
import { createCompressionStats } from "./stats.ts";
interface HardBudgetOptions {
targetTokens?: number;
targetRatio?: number;
}
/**
* Units containing these patterns must never be dropped.
* Anchored to meaningful signals only — never matches a bare end-of-sentence
* period (which would make every prose line "preserve" → permanent no-op):
* - `\d` digits (numbers, line refs, ports)
* - `https?:\/\/` URLs
* - `(?:Error|…|Traceback):` error/exception headers
* - "```" code fences
* - `^\s*at\s` stack-trace frames (digit-less)
* - `\/[\w.-]+\/` real multi-segment paths (but NOT "and/or")
* - `[A-Za-z_]\w*=\S` key=value (credential/config lines, digit-less)
*/
const UNIT_PRESERVE_RE =
/\d|https?:\/\/|(?:Error|Exception|TypeError|RangeError|SyntaxError|ReferenceError|Traceback):|```|^\s*at\s|\/[\w.-]+\/|[A-Za-z_]\w*=\S/i;
/** Average scoreToken for each word in the unit (sentence/line). */
function scoreUnit(unit: string): number {
const words = unit.split(/\s+/).filter(Boolean);
if (words.length === 0) return 0.5;
const total = words.reduce((sum, w) => sum + scoreToken(w), 0);
return total / words.length;
}
/** Returns true when the unit must never be dropped. */
function mustPreserve(unit: string): boolean {
return UNIT_PRESERVE_RE.test(unit);
}
/**
* Split text into droppable units (lines, optionally sentence-split for long prose lines).
*/
function splitUnits(text: string): string[] {
return text.split(/\n/).flatMap((line) => {
if (line.trim() === "") return [line];
// Only sentence-split pure prose lines (no numbers, URLs, error patterns, code)
if (!UNIT_PRESERVE_RE.test(line) && line.length > 60) {
const sentences = line.split(/(?<=[.!?])\s+/);
return sentences.length > 1 ? sentences : [line];
}
return [line];
});
}
interface TaggedUnit {
i: number;
u: string;
tokens: number;
score: number;
preserve: boolean;
}
function tagUnits(units: string[]): TaggedUnit[] {
return units.map((u, i) => ({
i,
u,
tokens: countTextTokens(u),
score: scoreUnit(u),
preserve: mustPreserve(u),
}));
}
function dropToTarget(tagged: TaggedUnit[], targetTokens: number): Set<number> {
const dropped = new Set<number>();
let tokCount = tagged.reduce((s, x) => s + x.tokens, 0);
// Sort droppable candidates by score ascending (lowest first = drop first)
const candidates = tagged
.filter((x) => !x.preserve)
.sort((a, b) => a.score - b.score);
for (const candidate of candidates) {
if (tokCount <= targetTokens) break;
dropped.add(candidate.i);
tokCount -= candidate.tokens;
}
return dropped;
}
function rebuildText(tagged: TaggedUnit[], dropped: Set<number>): string {
return tagged
.filter((x) => !dropped.has(x.i))
.map((x) => x.u)
.join("\n");
}
function compressText(text: string, targetTokens: number): string {
const currentTokens = countTextTokens(text);
if (currentTokens <= targetTokens) return text;
const units = splitUnits(text);
if (units.length <= 1) return text;
const tagged = tagUnits(units);
const dropped = dropToTarget(tagged, targetTokens);
if (dropped.size === 0) return text;
return rebuildText(tagged, dropped);
}
function extractMessages(body: Record<string, unknown>): Array<{ role: string; content: unknown }> {
const msgs = body.messages;
if (!Array.isArray(msgs)) return [];
return msgs as Array<{ role: string; content: unknown }>;
}
export function applyHardBudget(
body: Record<string, unknown>,
opts: HardBudgetOptions
): CompressionResult {
const { targetTokens, targetRatio } = opts;
if (targetTokens == null && targetRatio == null) {
return { body, compressed: false, stats: null };
}
const messages = extractMessages(body);
if (messages.length === 0) return { body, compressed: false, stats: null };
// Measure total tokens across all messages
const totalText = messages
.map((m) => (typeof m.content === "string" ? m.content : JSON.stringify(m.content)))
.join(" ");
const totalTokens = countTextTokens(totalText);
// targetTokens wins when both are set
const effectiveTarget =
targetTokens != null
? targetTokens
: Math.floor(totalTokens * (targetRatio as number));
if (totalTokens <= effectiveTarget) {
return { body, compressed: false, stats: null };
}
// Distribute the aggregate budget proportionally per message so the SUM stays
// ≤ target (passing the full target to each message would let an N-message body
// come back N× over budget).
const newMessages = messages.map((m) => {
if (typeof m.content !== "string") return m;
const msgTokens = countTextTokens(m.content);
const perMsgTarget =
totalTokens > 0 ? Math.floor(effectiveTarget * (msgTokens / totalTokens)) : effectiveTarget;
const out = compressText(m.content, perMsgTarget);
return out === m.content ? m : { ...m, content: out };
});
const changed = newMessages.some(
(m, i) => JSON.stringify(m) !== JSON.stringify(messages[i])
);
// Measure the result to detect when preserve-guarded content makes the target
// unreachable, so callers are not silently left over budget.
const usedMessages = changed ? newMessages : messages;
const resultTokens = countTextTokens(
usedMessages
.map((m) => (typeof m.content === "string" ? m.content : JSON.stringify(m.content)))
.join(" ")
);
const overBudget = resultTokens > effectiveTarget;
// Nothing changed and we are within budget → genuine no-op.
if (!changed && !overBudget) {
return { body, compressed: false, stats: null };
}
const newBody = changed ? { ...body, messages: newMessages } : body;
const stats = createCompressionStats(body, newBody, "stacked", ["hard-budget"]);
if (overBudget) {
const warning = `hard-budget: could not reach target (${resultTokens} > ${effectiveTarget}; preserved content exceeds budget)`;
stats.validationWarnings = [...(stats.validationWarnings ?? []), warning];
}
return { body: newBody, compressed: changed, stats };
}