agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
120 lines • 4.98 kB
JavaScript
import { sameName } from '../../lib/normalize.js';
import { spell } from './matcher.js';
import { isRead, scopeOf } from './readings.js';
/** Precompute each native spelling once. Prefix fallback prevents a long sent
* name and a repeated-token answer from multiplying every matching prefix. */
function anchorPattern(raw, name, work) {
const words = spell(raw, work);
work?.charge(words.length);
const fallback = new Array(words.length).fill(0);
for (let i = 1, matched = 0; i < words.length; i += 1) {
work?.charge(1);
while (matched > 0 && words[i] !== words[matched]) {
work?.charge(1);
matched = fallback[matched - 1] ?? 0;
}
if (words[i] === words[matched])
matched += 1;
fallback[i] = matched;
}
return (text, tokens) => {
if (words.length === 0)
return false;
let matched = 0;
for (let i = 0; i < tokens.length; i += 1) {
work?.charge(1);
const token = tokens[i];
const word = token.text;
while (matched > 0 && word !== words[matched]) {
work?.charge(1);
matched = fallback[matched - 1] ?? 0;
}
if (word === words[matched])
matched += 1;
if (matched === words.length) {
const first = tokens[i + 1 - words.length];
// Token folding only locates a candidate: identifiers, mixed case,
// punctuation and whitespace still use the library's one name rule.
if (!name)
return true;
work?.charge(token.end - first.start + raw.length + 1);
if (sameName(text.slice(first.start, token.end), raw))
return true;
matched = fallback[matched - 1] ?? 0;
}
}
return false;
};
}
/** Build once per checked text. The runtime bounds the record before this reader. */
export function proseAnchorOf(record, work) {
const names = new Set();
const phrases = new Set();
const declared = new Set();
const addNames = (value) => {
for (const item of Array.isArray(value) ? value : [value]) {
if (typeof item === 'string' || (typeof item === 'number' && Number.isFinite(item)))
names.add(String(item));
}
};
const addSaid = (said) => {
for (const phrase of said ?? [])
phrases.add(phrase);
};
for (const call of record.calls) {
if (!call.dispatched || !isRead(call))
continue;
const scope = call.row?.composed === 'remote' ? call.scope ?? [] : scopeOf(call) ?? [];
const host = new Set(scope.filter((item) => item.from === 'host').map((item) => item.argument));
if (!declared.has(call.toolName)) {
declared.add(call.toolName);
const search = record.tools[call.toolName]?.search;
if (search !== undefined) {
addSaid(search.subject.said);
addSaid(search.source.said);
for (const item of search.fixed ?? [])
if (!host.has(item.name))
addSaid(item.said);
for (const argument of search.arguments) {
if (host.has(argument.name))
continue;
for (const said of Object.values(argument.said ?? {}))
addSaid(said);
}
}
}
// A kept-out payload contributes no names, even when a historical carrier
// still holds its bytes. The public declaration can still anchor a note
// that the record is incomplete.
if (call.keptOut === true)
continue;
for (const [argument, value] of Object.entries(call.arguments)) {
if (!host.has(argument))
addNames(value);
}
for (const item of scope) {
if (item.from === 'host' || item.from === 'result')
continue;
addSaid(item.said);
if (item.from === 'call')
addNames(item.value);
if (item.resolved !== undefined) {
addNames(item.resolved.id);
addNames(item.resolved.name);
addNames(item.resolved.aliases);
}
}
}
const patterns = [
...[...phrases].map((phrase) => anchorPattern(phrase, false, work)),
...[...names].map((name) => anchorPattern(name, true, work)),
];
return (claim, text) => {
// A bare protocol-like "none" is not an absence about a named thing,
// even when a searched value happens to have that spelling.
if (claim.phraseId === 'none.none' && claim.tokens.length === 1)
return false;
return patterns.some((matches) => matches(text, claim.tokens));
};
}
//# sourceMappingURL=anchor.js.map