Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Invalid or negative numeric CLI flags now fail clearly instead of producing
`NaN` costs or silently skipping token-budget compaction.
- `compact()` now rejects negative or non-finite token budgets and invalid
protected-turn counts before transforming the payload.

## [0.1.1] - 2026-08-06

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const { payload, report: cut } = compact(messages, {
// cut -> { beforeTokens, afterTokens, savedTokens, savedPct, actions }
```

Token budgets must be finite, non-negative numbers. `keepLastTurns` must also
be an integer; invalid values throw a `RangeError` before the payload changes.

`payload` is an array of messages, or `{ system, messages }`. Message `content` may be a string or an array of Anthropic-style blocks.

## Accuracy
Expand Down
8 changes: 8 additions & 0 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export function analyzePayload(payload, { pricePerMTok = 3, counter = estimateTo

function clone(x) { return JSON.parse(JSON.stringify(x)); }
function tokensToChars(t) { return Math.max(0, Math.round(t * 4)); }
function requireNonNegativeNumber(name, value, { integer = false } = {}) {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (integer && !Number.isInteger(value))) {
throw new RangeError(`${name} must be a non-negative${integer ? " integer" : " finite number"}`);
}
}

// Cut a payload's token cost deterministically. Returns { payload, report }.
// Options:
Expand All @@ -66,6 +71,9 @@ function tokensToChars(t) { return Math.max(0, Math.round(t * 4)); }
// keepLastTurns never drop the last N messages when trimming to budget (default 4)
export function compact(payload, opts = {}) {
const { maxToolResultTokens = 500, dropDuplicates = true, maxTokens = null, keepLastTurns = 4, counter = estimateTokens } = opts;
requireNonNegativeNumber("maxToolResultTokens", maxToolResultTokens);
if (maxTokens != null) requireNonNegativeNumber("maxTokens", maxTokens);
requireNonNegativeNumber("keepLastTurns", keepLastTurns, { integer: true });
const before = analyzePayload(payload, { counter }).totalTokens;
const out = clone(payload);
const actions = [];
Expand Down
18 changes: 18 additions & 0 deletions test/basic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,21 @@ test("compact trims a large payload with linear token counting", () => {
assert.equal(report.actions.length, 496);
assert.ok(counterCalls <= messages.length * 3, `counter called ${counterCalls} times`);
});

test("compact rejects invalid budgets before transforming the payload", () => {
const payload = { messages: [{ role: "user", content: "keep me" }] };
const invalidOptions = [
["maxToolResultTokens", -1],
["maxToolResultTokens", Number.POSITIVE_INFINITY],
["maxTokens", Number.NaN],
["keepLastTurns", -1],
["keepLastTurns", 1.5],
];

for (const [name, value] of invalidOptions) {
assert.throws(
() => compact(payload, { [name]: value }),
{ name: "RangeError", message: new RegExp(`${name} must be`) },
);
}
});
Loading