agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
369 lines • 18 kB
JavaScript
;
/**
* statement — the library's sentence for an established typed claim, written from the cited
* reads' rows alone (design § 5.1): what was searched, and every gap said out loud.
*
* Pattern: fixed templates; the template is chosen by the ROW's shape (an `among` miss reads
* "did not find … among N names"), then by the claim's kind. One clause per cited read,
* one line each, in ordinal order.
*
* The library's own voice states ONLY what the record shows ("no cap reached"). Every
* other text is QUOTED and ATTRIBUTED to where it came from: "(declared by the tool)",
* "(fixed by the tool)", "(the tool's declared default)", "(as sent by the model)",
* "(as the tool's result says)", "(the caller's identity)". A declared fact is said as
* the tool's ("the tool declares no server cap", "the tool declares one page"). An
* unsent argument is said with calibrated words: the tool may have applied its own
* default, so this may cover only part.
*
* Quoting is the boundary: NFKC, control and format characters (bidi overrides,
* zero-width marks) removed, line breaks flattened, backticks turned into apostrophes.
* A value is capped at 64 characters plus "…"; a gap's text at the 120 its declaration
* may hold. EVERY gap is listed, never cut short in number; other lists are capped and
* counted ("and N more"), so a clause stays within `clauseLimit(gaps)`.
* Role: integrity/ layer (L3), read by `judge.ts`. `complete` states the rows READ — their
* count and identities (a fact's `entity`, or the declared id column) — never a list
* the model wrote.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.composeRecordedStatement = exports.composeStatement = exports.formatSpan = exports.quoteValue = exports.clauseLimit = exports.STATEMENT_LIMITS = exports.STATEMENT_VERSION = void 0;
const capture_js_1 = require("../claim-binding/capture.js");
const stableJson_js_1 = require("../../lib/stableJson.js");
const period_js_1 = require("../../semantics/coverage/period.js");
const span_js_1 = require("../../semantics/coverage/search/span.js");
const instant_js_1 = require("../../time/instant.js");
const range_js_1 = require("../../time/range.js");
const readings_js_1 = require("./readings.js");
const instantMs = (value) => (0, instant_js_1.instantOf)(value, 'lenient')?.ms;
/** Bump with any change to a template or to how a value is rendered. */
exports.STATEMENT_VERSION = '7';
const VALUE_CHARS = 64;
const GAP_CHARS = 120;
const LIST_ITEMS = 12;
const SCOPE_ITEMS = 24;
/** The stated bounds of one value, one gap's text, and the capped lists. */
exports.STATEMENT_LIMITS = Object.freeze({
valueChars: VALUE_CHARS,
gapChars: GAP_CHARS,
listItems: LIST_ITEMS,
scopeItems: SCOPE_ITEMS,
});
/**
* The most UTF-16 units a clause can hold for a row with `gaps` gaps: the head and at most ~60
* capped parts (each quoted value ≤ 2 × 64 + 3 units, with its label and attribution), plus
* each gap (≤ 2 × 120 + 3 units, with its attribution).
*/
function clauseLimit(gaps) {
return 12_000 + gaps * 300;
}
exports.clauseLimit = clauseLimit;
/** One text as quoted data, capped at `cap` code points. */
function quote(value, cap) {
const raw = typeof value === 'string'
? value
: typeof value === 'number' || typeof value === 'boolean'
? String(value)
: (0, stableJson_js_1.stableJson)(value) ?? '';
const clean = raw
.normalize('NFKC')
.replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, ' ')
.replace(/\p{Cf}/gu, '')
.replace(/`/g, "'")
.replace(/\s+/gu, ' ')
.trim();
const points = Array.from(clean);
const capped = points.length > cap ? `${points.slice(0, cap).join('')}…` : clean;
return `\`${capped}\``;
}
/** One value as quoted data (64 code points). */
const quoteValue = (value) => quote(value, VALUE_CHARS);
exports.quoteValue = quoteValue;
/** One gap's text as quoted data (its declared 120). */
const quoteGap = (value) => quote(value, GAP_CHARS);
const capped = (items, cap, more) => items.length > cap ? [...items.slice(0, cap), more(items.length - cap)] : [...items];
const ATTRIBUTION = {
call: 'as sent by the model',
default: "the tool's declared default",
fixed: 'fixed by the tool',
tool: 'declared by the tool',
result: "as the tool's result says",
host: "the caller's identity",
};
const attributed = (text, from) => `${text} (${ATTRIBUTION[from ?? 'tool'] ?? ATTRIBUTION.tool})`;
const pad = (n) => String(n).padStart(2, '0');
function clockOf(ms) {
const d = new Date(ms);
const base = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
// Seconds when they are not zero; never milliseconds — a half-open span's end one
// millisecond past a minute is that minute.
return d.getUTCSeconds() === 0 ? base : `${base}:${pad(d.getUTCSeconds())}`;
}
const dayOf = (ms) => new Date(ms).toISOString().slice(0, 10);
/**
* A span (`time/range.ts`) as the statement writes it — always with its date, so a historical
* window is never read as today's: `2026-10-01 08:00–10:00Z`, or a date on each bound across
* days (`2026-10-15 10:00Z–2026-10-22 10:00Z`); undefined when it is not a pair of instants.
*/
function formatSpan(span) {
const from = instantMs(span.from);
const to = instantMs(span.to);
if (from === undefined || to === undefined)
return undefined;
return dayOf(from) === dayOf(to)
? `${dayOf(from)} ${clockOf(from)}–${clockOf(to)}Z`
: `${dayOf(from)} ${clockOf(from)}Z–${dayOf(to)} ${clockOf(to)}Z`;
}
exports.formatSpan = formatSpan;
/** An absolute window as sent (`<from>..<to>` or `<from>/<to>`), read by the time layer's parser. */
const sentRange = (value) => (0, range_js_1.parseRange)(value, '..') ?? (0, range_js_1.parseRange)(value, '/');
/** An instant, with its date unless it falls on the leg clock's day. */
function formatInstant(ms, clock) {
const now = instantMs(clock);
return now !== undefined && dayOf(now) === dayOf(ms)
? `${clockOf(ms)}Z`
: `${dayOf(ms)} ${clockOf(ms)}Z`;
}
const HELD = {
covered: 'held in full',
'partly-held': 'held in part',
'not-held': 'not held',
unknown: 'held: unknown',
};
const items = (row, facet) => row.checked.filter((item) => item.facet === facet);
/**
* The period part — the span the call RAN with (its period item's dispatch-time span, else the
* result's `queried`) and how much of the read the store held — and what it adds to "not
* checked".
*/
function periodParts(row) {
const declared = items(row, 'period')[0];
const period = row.period;
// The window the call ran with: a relative one's dispatch-time span; an absolute one as sent;
// else the result's own `queried`.
const ran = declared?.span ??
sentRange(declared?.value) ??
(period === undefined ? undefined : (0, span_js_1.resultSpan)(period));
const shown = ran === undefined ? undefined : formatSpan(ran);
const windowText = shown ?? (declared === undefined ? undefined : (0, exports.quoteValue)(declared.value ?? declared.what));
if (windowText === undefined)
return { unchecked: [] };
if (period === undefined)
return { window: `window ${windowText} (held: undeclared)`, unchecked: [] };
let verdict;
try {
verdict = (0, period_js_1.periodVerdict)(period);
}
catch {
verdict = 'unknown';
}
const queried = (0, span_js_1.resultSpan)(period);
const unchecked = verdict === 'unknown'
? queried === undefined
? []
: [`${formatSpan(queried) ?? (0, exports.quoteValue)(period.queried.from)} (held: unknown)`]
: (0, readings_js_1.unheldRanges)(period).flatMap((span) => {
const text = formatSpan(span);
return text === undefined ? [] : [`${text} (not held)`];
});
return { window: `window ${windowText} (${HELD[verdict] ?? 'held: unknown'})`, unchecked };
}
/** One scope item, quoted and attributed. */
function itemPart(item) {
const argument = item.argument === undefined ? undefined : (0, exports.quoteValue)(item.argument);
if (item.facet === 'subject')
return attributed(`subject ${(0, exports.quoteValue)(item.what)}`, item.from);
if (item.facet === 'source') {
const named = item.from === 'call' && item.value !== undefined ? item.value : item.what;
return attributed(`source ${(0, exports.quoteValue)(named)}`, item.from);
}
if (item.facet === 'names')
return attributed(`names ${(0, exports.quoteValue)(item.what)}`, 'result');
if (item.any === true && argument !== undefined)
return attributed(`any ${argument}`, item.from);
if (item.from === 'host' && argument !== undefined)
return attributed(argument, 'host');
if (argument !== undefined && item.value !== undefined) {
return attributed(`${argument} ${(0, exports.quoteValue)(item.value)}`, item.from);
}
return attributed((0, exports.quoteValue)(item.what), item.from);
}
/** The scope the row records, in a fixed facet order. */
function scopeParts(row, among) {
const subject = items(row, 'subject').map(itemPart);
const population = among
? []
: capped(items(row, 'population').map(itemPart), LIST_ITEMS, (n) => `and ${n} more values`);
const filters = [
...items(row, 'filter').filter((i) => i.from === 'fixed'),
...items(row, 'filter').filter((i) => i.from !== 'fixed'),
].map(itemPart);
const names = items(row, 'names').map(itemPart);
const source = items(row, 'source').map(itemPart);
return [
...subject,
...population,
...capped(filters, SCOPE_ITEMS, (n) => `and ${n} more qualifiers`),
...names,
...source,
];
}
/** Every gap, said out loud — none dropped. */
function gapParts(row) {
return row.gaps.flatMap((gap) => {
switch (gap.kind) {
case 'period':
return [];
case 'argument-unsent':
return [
`${(0, exports.quoteValue)(gap.argument ?? gap.what)} was not sent, so the tool may have applied ` +
'its own default and this may cover only part',
];
case 'partial':
return ['what the partial result left out'];
case 'credential-limited':
return ["what the caller's credential may not see"];
case 'match':
return [`${quoteGap(gap.what)} (by the tool's own match rule)`];
case 'ranking':
return ['the rows an approximate ranking may have left out'];
case 'prose':
return [`${quoteGap(gap.what)} (as the tool wrote it)`];
case 'scope':
return [`${quoteGap(gap.what)} (declared by the tool)`];
default:
return [`${quoteGap(gap.what)} (as the tool's result says)`];
}
});
}
/** The identities of the rows read, as quoted data. */
function identities(call) {
if (call.rowsMayDisplay !== true)
return [];
const column = call.idColumn ?? 'entity';
return (call.rowsRead ?? []).flatMap((row) => {
const id = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : undefined;
return typeof id === 'string' || typeof id === 'number' ? [(0, exports.quoteValue)(id)] : [];
});
}
/** Display only unchanged native rows admitted at the final tool-result boundary.
* An arbitrary id/key field has no identity contract, so its row is quoted as
* data instead of guessing a business identity. Both values and row lists cap. */
function returnedValues(call) {
if (call.rowsMayDisplay !== true || !call.rowsRead?.length)
return undefined;
const column = call.idColumn ?? 'entity';
const values = call.rowsRead.slice(0, LIST_ITEMS).map((row) => {
const id = Object.hasOwn(row, column) ? row[column] : undefined;
return (0, exports.quoteValue)(typeof id === 'string' || typeof id === 'number' ? id : row);
});
const more = Math.max(0, (call.rows ?? call.rowsRead.length) - values.length);
return attributed(`returned ${values.join(', ')}${more > 0 ? `, and ${more} more rows` : ''}`, 'result');
}
/** What the tool declares about a server cap — said only when the declaration is in hand. */
function serverCapPart(tool) {
if (tool?.search === undefined)
return [];
const cap = tool.search.limits?.cap;
if (cap === undefined)
return ['the tool declares no server cap'];
return typeof cap === 'number' && Number.isSafeInteger(cap)
? [`the tool declares a server cap of ${cap}`]
: [];
}
/** One clause for one cited read. */
function clauseOf(kind, read, clock, recorded = false) {
const { call, row, tool } = read;
const toolName = (0, exports.quoteValue)(row.toolName);
const names = items(row, 'names')[0];
const among = names !== undefined;
const rowsRead = call.rows ?? call.rowsRead?.length ?? 0;
const count = among && typeof names.count === 'number' && Number.isSafeInteger(names.count)
? String(names.count)
: 'the';
const head = recorded
? call.outcome !== 'ok'
? `${toolName} did not establish a completed read`
: call.emptiness === 'declared-absent'
? `${toolName} returned no matching rows`
: call.rows !== undefined || call.rowsRead !== undefined
? `${toolName} read ${rowsRead} ${rowsRead === 1 ? 'row' : 'rows'}`
: call.emptiness === 'non-empty'
? `${toolName} returned matching rows whose count is undetermined`
: `${toolName} returned a result whose row count is undetermined`
: among
? `${toolName} did not find ${attributed((0, exports.quoteValue)(row.lookedFor ?? ''), 'call')} among ${count} names`
: kind === 'complete'
? `${toolName} read ${rowsRead} rows`
: `${toolName} found none`;
const { window, unchecked } = periodParts(row);
const parts = scopeParts(row, among);
if (recorded && call.emptiness === 'non-empty') {
const values = returnedValues(call);
if (values !== undefined)
parts.push(values);
}
if (window !== undefined)
parts.push(window);
const measured = among ? instantMs(names.measuredAt) : undefined;
if (measured !== undefined)
parts.push(`read ${formatInstant(measured, clock)}`);
if (!recorded && !among && kind === 'complete') {
const ids = identities(call);
if (ids.length > 0) {
const more = Math.max(0, rowsRead - Math.min(ids.length, LIST_ITEMS));
const shown = ids.slice(0, LIST_ITEMS).join(', ');
parts.push(attributed(`rows ${shown}${more > 0 ? `, and ${more} more` : ''}`, 'result'));
}
parts.push('no cap reached', ...serverCapPart(tool), 'the tool declares one page');
}
const notChecked = [...gapParts(row), ...unchecked];
if (notChecked.length > 0)
parts.push(`not checked: ${notChecked.join('; ')}`);
return `${head} — ${parts.join('; ')}.`;
}
const ordinalOf = (call) => {
try {
return (0, capture_js_1.readResultIdentity)(call.resultId).ordinal;
}
catch {
return Number.MAX_SAFE_INTEGER;
}
};
/** The statement for an established claim: one clause per cited read, in ordinal order. */
function composeStatement(kind, reads, clock) {
const ordered = [...reads].sort((a, b) => ordinalOf(a.call) - ordinalOf(b.call));
const clauses = ordered.map((read) => clauseOf(kind, read, clock));
return { text: clauses.join('\n'), clauses, version: exports.STATEMENT_VERSION };
}
exports.composeStatement = composeStatement;
/** State only the admitted read and all its limits, without asserting completeness. */
function composeRecordedStatement(reads, clock, explanation) {
const clauses = explanation === undefined ? [] : [explanation.refusal];
const contrary = explanation?.contraryReads ?? [];
for (const read of contrary.slice(0, LIST_ITEMS)) {
const handle = (0, exports.quoteValue)(read.call.row?.handle ?? `r${ordinalOf(read.call)}`);
if (read.row !== undefined) {
clauses.push(`Contrary recorded evidence (read ${handle}): ${clauseOf('complete', { ...read, row: read.row }, clock, true)}`);
}
else {
const count = read.call.rows ?? read.call.rowsRead?.length;
const parts = [
`${(0, exports.quoteValue)(read.call.toolName)} returned ${count === undefined
? 'matching rows with an undetermined count'
: `${count} ${count === 1 ? 'row' : 'rows'}`}`,
returnedValues(read.call),
'coverage not declared for this result',
].filter((part) => part !== undefined);
clauses.push(`Contrary recorded evidence (read ${handle}): ${parts.join('; ')}.`);
}
}
if (contrary.length > LIST_ITEMS)
clauses.push(`The record contains ${contrary.length - LIST_ITEMS} additional positive reads for this subject.`);
clauses.push(...[...reads]
.sort((a, b) => ordinalOf(a.call) - ordinalOf(b.call))
.map((read) => clauseOf('complete', read, clock, true)));
if (explanation === undefined)
clauses.push('The requested negative claim is not established by these recorded searches.');
return { text: clauses.join('\n'), clauses, version: exports.STATEMENT_VERSION };
}
exports.composeRecordedStatement = composeRecordedStatement;
//# sourceMappingURL=statement.js.map