agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
109 lines • 4.67 kB
JavaScript
/**
* arguments/sources — where the MODEL says each argument value came from
* (`_findings.from`), and the one reader of it.
*
* Pattern: Map. The vocabulary of a declared source and one pure reader that
* judges the model's `from` array against the call it rides on. The
* reader is called by the ONE reader of `_findings`
* (`findings/reserved.ts` · `readDeclaration`, under declared
* sources — `.inputsLayer({ argumentSources: true })` or
* `.findings({ argumentSources: true })`), so the inputs layer and
* ToolCalls' peel read one `_findings` the same way.
* Role: core/ layer leaf of the inputs layer (honesty layer 2). Imports
* nothing from `findings/` — the folder's one-way law. Small and
* synchronous on purpose: the one reader is on every armed agent's
* graph; the CHECKS (`checks.ts`) load with the layer.
* Emits: N/A.
*
* ## What the model may say
*
* ```json
* "_findings": { "basis": "direct",
* "from": [ { "argument": "window", "source": "user", "quote": "over the last week" },
* { "argument": "host", "source": "result", "id": "toolu_01AbC" },
* { "argument": "limit", "source": "assumed" } ] }
* ```
*
* `user` (+ `quote`, the person's words copied exactly), `result` (+ `id`, a
* tool_result id), `turn` (the person answered it, when asked, earlier),
* `app` (the app's own text carries it), `assumed` (the model chose it). A
* declaration is a CLAIM: the library checks it (`checks.ts` · `checkSource`)
* and files its verdict beside it; an unchecked claim is never trusted.
*
* ## Never repaired
*
* An entry is dropped and COUNTED (never defaulted) when it is not an object;
* its `source` is outside the five; a `user` entry has no string `quote` or a
* `result` entry no string `id`; its `argument` is not a top-level argument
* this call carries a value for (a key the call left out, a missing value, or
* a nested / non-primitive value — not ruled in this version); or an earlier
* entry already named the same argument (the first wins).
*/
import { isMissing } from '../../../tools/declare.js';
/** The five, in the order the served schema enumerates them. */
export const DECLARED_SOURCE_KINDS = Object.freeze([
'user',
'result',
'turn',
'app',
'assumed',
]);
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
const isPrimitive = (value) => typeof value === 'string' ||
typeof value === 'boolean' ||
(typeof value === 'number' && Number.isFinite(value));
/** One entry, judged against the call's own arguments — `undefined` when malformed. */
function readEntry(raw, args) {
if (!isPlainObject(raw))
return undefined;
const { argument, source } = raw;
if (typeof argument !== 'string')
return undefined;
if (typeof source !== 'string' ||
!DECLARED_SOURCE_KINDS.includes(source)) {
return undefined;
}
if (isMissing(args, argument) || !isPrimitive(args[argument]))
return undefined;
const kind = source;
if (kind === 'user') {
return typeof raw.quote === 'string' ? { argument, source: kind, quote: raw.quote } : undefined;
}
if (kind === 'result') {
return typeof raw.id === 'string' ? { argument, source: kind, id: raw.id } : undefined;
}
return { argument, source: kind };
}
/**
* The model's `from` array for ONE call, judged against the arguments that
* call carries (`args` — the call's own arguments, `_findings` taken off).
* Each entry that cannot be read is dropped and counted; nothing is defaulted
* and nothing is inferred. A `from` that is not an array reads as nothing,
* one malformed.
*
* @example
* ```ts
* readSources([{ argument: 'window', source: 'user', quote: 'the last week' }], { window: '7d' });
* // { from: [{ argument: 'window', source: 'user', quote: 'the last week' }], malformed: 0 }
* readSources([{ argument: 'window', source: 'user' }], { window: '7d' }); // no quote
* // { from: [], malformed: 1 }
* ```
*/
export function readSources(raw, args) {
if (!Array.isArray(raw))
return { from: [], malformed: 1 };
const from = [];
const named = new Set();
let malformed = 0;
for (const item of raw) {
const entry = readEntry(item, args);
if (entry === undefined || named.has(entry.argument)) {
malformed += 1;
continue;
}
named.add(entry.argument);
from.push(entry);
}
return { from, malformed };
}
//# sourceMappingURL=sources.js.map