agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
149 lines • 7.87 kB
JavaScript
;
/**
* window/options — validate a window strategy's options ONCE, at build time.
*
* Pattern: Guard functions returning a resolved config.
* Role: core/ layer. Every message here takes a `label`, because the same
* option bag arrives through two doors — `.compaction({...})` and
* `summarizeOldest({...})` — and an error should name the door the
* caller actually used. One implementation, so the two doors can
* never drift into validating differently.
* Emits: N/A.
*
* Everything fails at `.build()`, never mid-run. A window policy that turns
* out to be nonsense on iteration 40 of a paid run is a policy that cost you
* money to discover.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveTokenBudgetOptions = exports.resolveSlidingWindowOptions = exports.resolveCompactionOptions = exports.DEFAULT_RETENTION = exports.requireKeepLastToolResults = exports.DEFAULT_KEEP_LAST_TOOL_RESULTS = exports.DEFAULT_KEEP_RECENT_TURNS = void 0;
/** Default depth of the "never touch this" recent window. */
exports.DEFAULT_KEEP_RECENT_TURNS = 6;
/**
* How many tools' most recent results the window holds beyond
* `keepRecentTurns` when nobody said (9.57.0). ON by default, at 2.
*
* On by default for the same reason 9.55.0's anchor is: the counterfactual to
* a pinned 5,800-character tool result is not 5,800 bytes saved, it is a
* fabricated id, a refusal, a wasted action out of a small budget, and the
* same 5,800 bytes fetched again. That is a correctness failure the framework
* caused, and those are fixed on by default.
*
* TWO rather than one because the ceiling is spent newest-first among STALE
* candidates: a one-word acknowledgement from an actuator the model has
* stopped using can occupy a slot ahead of an older load-bearing observation.
* Two slots absorb that. `false` or `0` switches the pin off entirely.
*/
exports.DEFAULT_KEEP_LAST_TOOL_RESULTS = 2;
/**
* Validate the `keepLastToolResults` dial. Refused at construction, never
* mid-run — the house rule for every window option.
*/
function requireKeepLastToolResults(value, label) {
if (value === false)
return;
if (!Number.isInteger(value) || value < 0) {
throw new Error(`${label}: keepLastToolResults must be a whole number >= 0, or false, got ` +
`${String(value)}. It is how many tools' most recent results stay in the window ` +
`beyond keepRecentTurns; 0 and false both switch the pin off.`);
}
}
exports.requireKeepLastToolResults = requireKeepLastToolResults;
/**
* What happens to folded messages when nobody said. The originals ride with
* the conversation: losing them has to be a choice somebody typed, not a
* default they inherited.
*/
exports.DEFAULT_RETENTION = 'conversation';
const RETENTIONS = ['conversation', 'discard'];
function requireObject(options, label, shape) {
if (options === null || typeof options !== 'object') {
throw new Error(`${label}: expected an options object (${shape}), got ${typeof options}.`);
}
}
function requireThreshold(thresholdTokens, label) {
if (typeof thresholdTokens !== 'number' ||
!Number.isFinite(thresholdTokens) ||
thresholdTokens <= 0) {
throw new Error(`${label}: thresholdTokens must be a positive number of tokens, got ` +
`${String(thresholdTokens)}. There is no default: the right budget depends on your ` +
`model and your bill, and a number this library invented would be inherited silently ` +
`by every run.`);
}
}
function requireKeepRecentTurns(keepRecentTurns, label) {
if (!Number.isInteger(keepRecentTurns) || keepRecentTurns < 1) {
throw new Error(`${label}: keepRecentTurns must be an integer >= 1, got ${String(keepRecentTurns)}. ` +
`Keeping zero recent turns would remove the turn the model is reasoning over right now.`);
}
}
/**
* Validate `.compaction()` / `summarizeOldest()` options.
*
* @param label the door being used, so the error names it
*/
function resolveCompactionOptions(options, label) {
requireObject(options, label, '{ thresholdTokens, summarizer, ... }');
const { thresholdTokens, summarizer, keepRecentTurns, model, retain } = options;
requireThreshold(thresholdTokens, label);
if (summarizer === null ||
typeof summarizer !== 'object' ||
typeof summarizer.complete !== 'function') {
throw new Error(`${label}: summarizer must be an LLMProvider (an object with a ` +
'complete() method). It is explicit on purpose — the library will not quietly bill ' +
'your main model for compaction. Pass a cheap provider/model here.');
}
if (keepRecentTurns !== undefined)
requireKeepRecentTurns(keepRecentTurns, label);
if (model === undefined) {
throw new Error(`${label}: model is required whenever you pass a summarizer. It used to default to the ` +
`agent's own model, and that default had no correct case:\n` +
` • same provider family — it quietly billed your MAIN model for every fold, which is ` +
`the one thing the summarizer option exists to prevent;\n` +
` • a different provider — it sent your agent's model id to a vendor that has never ` +
`heard of it, and the fold died mid-run, on a paid run.\n` +
`Name the model: \`model: 'claude-haiku-4-5'\` (or whatever your summarizer's cheap ` +
`model is called). Two words, and the invoice stops being a surprise.`);
}
if (typeof model !== 'string' || model.length === 0) {
throw new Error(`${label}: model must be a non-empty model id, got ${String(model)}.`);
}
if (retain !== undefined && !RETENTIONS.includes(retain)) {
throw new Error(`${label}: retain must be ${RETENTIONS.map((r) => `'${r}'`).join(' or ')}, got ` +
`${String(retain)}. It decides what happens to the messages a fold removes: ` +
`'conversation' (the default) carries them on the conversation checkpoint so a ` +
`restart can still produce them, and 'discard' does not. A value this library does ` +
`not recognise is refused rather than treated as one of them — guessing here would ` +
`mean guessing whether to keep somebody's transcript.`);
}
return {
thresholdTokens,
keepRecentTurns: keepRecentTurns ?? exports.DEFAULT_KEEP_RECENT_TURNS,
summarizer,
model,
retain: retain ?? exports.DEFAULT_RETENTION,
};
}
exports.resolveCompactionOptions = resolveCompactionOptions;
/** Validate `slidingWindow()` options. */
function resolveSlidingWindowOptions(options, label) {
requireObject(options, label, '{ keepRecentTurns }');
const { keepRecentTurns } = options;
if (keepRecentTurns === undefined) {
throw new Error(`${label}: keepRecentTurns is required and has no default. It IS the policy — how much ` +
`past your agent needs is a fact about your agent, not about this library.`);
}
requireKeepRecentTurns(keepRecentTurns, label);
return { keepRecentTurns };
}
exports.resolveSlidingWindowOptions = resolveSlidingWindowOptions;
/** Validate `tokenBudget()` options. */
function resolveTokenBudgetOptions(options, label) {
requireObject(options, label, '{ thresholdTokens, keepRecentTurns? }');
const { thresholdTokens, keepRecentTurns } = options;
requireThreshold(thresholdTokens, label);
if (keepRecentTurns !== undefined)
requireKeepRecentTurns(keepRecentTurns, label);
return { thresholdTokens, keepRecentTurns: keepRecentTurns ?? exports.DEFAULT_KEEP_RECENT_TURNS };
}
exports.resolveTokenBudgetOptions = resolveTokenBudgetOptions;
//# sourceMappingURL=options.js.map