agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
154 lines • 6.88 kB
JavaScript
/**
* resultCarries — does ONE tool result carry ONE value? The evidence index's
* own reading of a result, asked per value.
*
* Pattern: a pure reader over the index's one reading of a result
* (`evidenceIndex.ts` · `readResult`: parsed JSON with an absence's
* `looked_for` projected away, walked leaf by leaf, keys included —
* the leading JSON of a result a framework note follows, and the
* note as text; the text when it is not JSON) and the one normaliser
* (`lib/normalize.ts`) — never a second haystack.
* Role: core/ layer, `evidence/`. Asked by the inputs layer's declared-
* sources check (`core/agent/arguments/checks.ts` · `checkSource`),
* which loads with the layer — so this module is never on a plain
* agent's graph.
* Emits: N/A.
*/
import { MAX_INDEX_TOKENS, readResult } from './evidenceIndex.js';
import { canonicalForm, normalizeToken, tokenize } from '../../../lib/normalize.js';
/** A value's canonical token sequence — the evidence module's "same value". */
export function canonicalTokens(text) {
return tokenize(text).map(canonicalForm);
}
/** Whether `needle` occurs contiguously in `hay` — whole tokens, never substrings. */
export function occursIn(needle, hay) {
if (needle.length === 0 || needle.length > hay.length)
return false;
for (let i = 0; i + needle.length <= hay.length; i += 1) {
let all = true;
for (let k = 0; k < needle.length; k += 1) {
if (hay[i + k] !== needle[k]) {
all = false;
break;
}
}
if (all)
return true;
}
return false;
}
function leafOf(raw) {
const whole = normalizeToken(raw);
return { whole: whole === '' ? '' : canonicalForm(whole), tokens: canonicalTokens(raw) };
}
function leavesOf(content) {
const leaves = [];
let budget = MAX_INDEX_TOKENS;
const push = (raw) => {
if (budget <= 0)
return;
const leaf = leafOf(raw);
budget -= leaf.tokens.length;
leaves.push(leaf);
};
const visit = (node) => {
if (budget <= 0 || node === null || node === undefined)
return;
if (typeof node === 'string')
return push(node);
if (typeof node === 'number' || typeof node === 'boolean' || typeof node === 'bigint') {
return push(String(node));
}
if (Array.isArray(node)) {
for (const el of node)
visit(el);
return;
}
if (typeof node === 'object') {
for (const [key, value] of Object.entries(node)) {
push(key);
visit(value);
}
}
};
const read = readResult(content);
if ('parsed' in read) {
visit(read.parsed);
// The text after the JSON (a framework note) is one leaf, read to what is left of the ceiling.
if (read.tail !== undefined && budget > 0) {
const tokens = canonicalTokens(read.tail);
leaves.push({ whole: '', tokens: tokens.slice(0, budget) });
budget -= tokens.length;
}
return { leaves, truncated: budget <= 0 };
}
// Text is one leaf — a longer value may span its tokens, never a substring.
const tokens = canonicalTokens(read.text);
const within = tokens.length > MAX_INDEX_TOKENS ? tokens.slice(0, MAX_INDEX_TOKENS) : tokens;
return {
leaves: [{ whole: '', tokens: within }],
truncated: tokens.length > MAX_INDEX_TOKENS,
};
}
/** Whether `needle` is carried by one leaf, the way the index files it. */
function leafCarries(leaf, needle) {
// One token: the leaf whole (a leaf may carry spaces and still be one value)
// or one of its tokens — exactly the forms the index files.
if (needle.length === 1)
return leaf.whole === needle[0] || leaf.tokens.includes(needle[0]);
// A longer value must sit inside ONE leaf, contiguously — never stitched across two.
return occursIn(needle, leaf.tokens);
}
/**
* A result read once, asked many times — `(value) => ResultCarriage`, the
* same answers `resultCarries` gives, without re-reading the result for every
* value. The declared-sources checks ask one result about several values per
* batch (`core/agent/arguments/checks.ts`).
*/
export function resultReader(content) {
let read;
return (value) => {
const needle = canonicalTokens(String(value));
if (needle.length === 0)
return 'uncheckable';
read ??= leavesOf(content);
if (read.leaves.some((leaf) => leafCarries(leaf, needle)))
return 'found';
return read.truncated ? 'uncheckable' : 'not-found';
};
}
// FOLD · the one per-result reading of "does this result carry this value?"
// consumers read this and never re-derive it: core/agent/arguments/checks.ts · checkSource (the
// declared-sources check — a value the model says came from one named result, or a hint looked up).
// detached: yes — a pure function of the result's text and the value; nothing is stored.
/**
* Whether ONE tool result carries a value — read the way the evidence index
* reads a result (`readResult`: parsed JSON with an absence's `looked_for`
* projected away, walked leaf by leaf, keys included; the text when it is not
* JSON), so a number or a boolean in compact JSON is found (`{"limit":50,
* "ok":true}` carries `50` and `true`) where a tokenizer over the raw string
* would see `:50` and `:true` — also when a framework note follows the JSON
* (a step banner, the repeated-call note), when it is several JSON blocks (an
* MCP text result) and in a capped result's `head`. A one-token value is found when it equals a
* leaf or one of a leaf's tokens; a longer value must occur contiguously
* inside one string leaf (or in the text). Whole tokens only, never
* substrings; one normaliser (`lib/normalize.ts`), so `41,200` is `41200` here as
* at the gate.
*
* `content` is the TOOL's own bytes — the caller cuts a library note off first
* (`lib/toolBytes.ts` · `toolBytesOf`). `uncheckable` when the value has no
* token after normalisation, or the result reached the index's token ceiling
* (`MAX_INDEX_TOKENS`) before a match: never a pass, never "not found".
* `resultReader` answers the same question for many values over one read.
*
* @example
* ```ts
* resultCarries('{"host":"srv-4417","limit":50,"ok":true}', 50); // 'found'
* resultCarries('{"host":"srv-4417"}', 'srv-44'); // 'not-found'
* resultCarries('{"host":"srv-4417"}', '24h'); // 'uncheckable'
* ```
*/
export function resultCarries(content, value) {
return resultReader(content)(value);
}
//# sourceMappingURL=resultCarries.js.map