agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
163 lines • 7.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.findingsContext = exports.findingsContextBytes = exports.MAX_FINDINGS_CONTEXT_BYTES = exports.DEFAULT_FINDINGS_CONTEXT_BYTES = exports.FINDINGS_LAYOUT = void 0;
const boundedJson_js_1 = require("../../../lib/boundedJson.js");
const saidByPerson_js_1 = require("../../../lib/saidByPerson.js");
const ledger_js_1 = require("./ledger.js");
const offer_js_1 = require("./offer.js");
const reserved_js_1 = require("./reserved.js");
const serve_js_1 = require("./serve.js");
exports.FINDINGS_LAYOUT = 'late-v1';
exports.DEFAULT_FINDINGS_CONTEXT_BYTES = 16_384;
exports.MAX_FINDINGS_CONTEXT_BYTES = 262_144;
/** The host owns this bound; a model declaration never changes it. */
function findingsContextBytes(value) {
const bytes = value ?? exports.DEFAULT_FINDINGS_CONTEXT_BYTES;
if (!Number.isSafeInteger(bytes) || bytes < 1_024 || bytes > exports.MAX_FINDINGS_CONTEXT_BYTES)
throw new RangeError('findings.maxContextBytes must be a whole number from 1024 to 262144.');
return bytes;
}
exports.findingsContextBytes = findingsContextBytes;
/** Explicit projections preserve model declarations, not raw ledger internals.
* In particular, judge results, argument values and service payloads are not served.
*/
function declaration(row) {
return {
toolCallId: row.toolCallId,
...(row.toolName !== undefined && { toolName: row.toolName }),
...(row.ref !== undefined && { ref: row.ref }),
...(row.unknownId === true && { unknownId: true }),
standing: row.standing,
assertions: row.assertions.map((assertion) => ({
subject: { kind: assertion.subject.kind, id: assertion.subject.id },
predicate: assertion.predicate,
value: assertion.value,
stratum: assertion.stratum,
})),
...(row.line !== undefined && { line: row.line }),
...(row.settles !== undefined && { settles: row.settles }),
};
}
/** Projection may fail on an unreadable row; the caller withholds the WHOLE
* reading, including advisory IDs, instead of salvaging superseded assertions.
* Other structured row kinds (judge, arguments, future metadata) are not served.
*/
function bucketsOf(messages, ledger) {
const fold = (0, ledger_js_1.foldLedger)(ledger);
const standings = [...fold.standingOf.values()];
const noise = standings.filter((row) => row.standing === 'noise').map((row) => row.toolCallId);
const propositions = new Map();
for (const row of ledger)
if (row.kind === 'basis' && row.proposition !== undefined)
propositions.set(row.toolCallId, row.proposition);
const proposed = (row) => ({
...declaration(row),
...(propositions.has(row.toolCallId) && { proposition: propositions.get(row.toolCallId) }),
});
return {
advisoryResultIds: (0, offer_js_1.offeredResultIds)(messages, ledger),
facts: standings.filter((row) => row.standing === 'fact').map(proposed),
evidenceRefs: standings.filter((row) => row.standing === 'open').map(proposed),
limitations: standings.filter((row) => row.standing === 'ruled-out').map(proposed),
conflicts: fold.conflicts,
contingent: ledger.flatMap((row) => row.kind === 'contingent'
? [
{
declaredOn: row.declaredOn,
value: row.value,
carriers: row.carriers.map((carrier) => ({
toolCallId: carrier.toolCallId,
standing: carrier.standing,
})),
},
]
: []),
unsettledByAbsence: [...fold.unsettled.values()].map((row) => ({
toolCallId: row.toolCallId,
...(row.notChecked !== undefined && {
notChecked: row.notChecked.map((item) => ({
what: item.what,
...(item.why !== undefined && { why: item.why }),
})),
}),
...(row.cannotCover !== undefined && {
cannotCover: row.cannotCover.map((item) => ({
what: item.what,
...(item.why !== undefined && { why: item.why }),
})),
}),
...(row.tryInstead !== undefined && { tryInstead: row.tryInstead }),
})),
noise,
undeclaredResultIds: (0, offer_js_1.undeclaredIds)((0, offer_js_1.servedToolCallIds)(messages), fold.standingOf),
};
}
/** Stable JSON field meanings; all mutable values occur after the cached prefix. */
function findingsContext(messages, rows, maxBytes = exports.DEFAULT_FINDINGS_CONTEXT_BYTES) {
const limit = findingsContextBytes(maxBytes);
// The record may come from a checkpoint. Admit plain bounded data before
// folding, so a value's getter/toJSON cannot run during conflict comparison.
// If the source cannot be admitted, withhold its reading as a whole: using
// an older surviving row would revive a superseded assertion.
let sourceUnknown = false;
let buckets;
try {
const admitted = (0, boundedJson_js_1.copyJson)(rows === undefined ? [] : rows, 8_388_608, false);
if (!Array.isArray(admitted) ||
admitted.some((row) => row === null ||
typeof row !== 'object' ||
Array.isArray(row) ||
typeof row.kind !== 'string'))
throw new Error('Expected an array of structured findings rows.');
// Build the projection before admitting any of it. Malformed declarations
// or unreadable rows must not leave an older, apparently current fact.
buckets = bucketsOf(messages, admitted);
}
catch {
sourceUnknown = true;
buckets = bucketsOf([], []);
}
const data = {};
const omitted = {};
for (const [key, entries] of Object.entries(buckets)) {
data[key] = [];
omitted[key] = entries.length;
}
const encode = () => `${saidByPerson_js_1.FINDINGS_DATA_FRAME_PREFIX}\n${JSON.stringify({
layout: exports.FINDINGS_LAYOUT,
ledgerStatus: sourceUnknown ? 'unknown' : 'captured',
// Rejected sources have no trusted row count. Never inspect them again.
sourceRowsOmitted: sourceUnknown ? null : 0,
meaning: 'Model declarations are not verified facts; recorded observations are not a diagnosis. IDs are advisory, not authority. Omitted entries remain unknown.',
data,
omitted,
})}`;
const encoder = new TextEncoder();
// Counts start at their maximum, reserving their serialized space before
// entries are admitted. They only decrease, so overflow cannot hide omissions.
if (encoder.encode(encode()).byteLength > limit)
throw new Error('Findings context metadata exceeds its byte budget.');
for (const [key, entries] of Object.entries(buckets)) {
const cap = key.endsWith('Ids') || key === 'noise'
? reserved_js_1.FINDINGS_OFFER_CAP
: serve_js_1.FINDINGS_PIECE_LIMITS.bucketLines;
for (const entry of entries.slice(0, cap)) {
let value;
try {
value = (0, boundedJson_js_1.copyJson)(entry, limit, false);
}
catch {
continue;
} // Unrepresentable or oversized data is counted, never coerced.
data[key].push(value);
omitted[key]--;
if (encoder.encode(encode()).byteLength > limit) {
data[key].pop();
omitted[key]++;
}
}
}
return encode();
}
exports.findingsContext = findingsContext;
//# sourceMappingURL=context.js.map