agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
238 lines • 11.2 kB
JavaScript
;
/**
* What every row reader shares: the view, the declarations, the per-sentence
* catch, and the small pointer / var builders.
*
* `say` is THE way a reader writes a line. It fills the template and, when the
* fill throws (a programming error — a var the reader forgot), writes
* `unreadable.line@1` instead and counts it in `unread`. One bad line never
* crashes the account.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isPersonEntry = exports.historyOf = exports.countedFields = exports.emptinessSource = exports.rowsAtOf = exports.readEmptiness = exports.parseMaybeJson = exports.skillVars = exports.toolVar = exports.declarationAt = exports.historyAt = exports.stateAt = exports.derived = exports.at = exports.boundedPointer = exports.jsonPointer = exports.makeSay = exports.FACT_TEXT_CHARS = exports.SHORT_TEXT_CHARS = exports.LONG_TEXT_BUDGET = exports.takeItem = exports.ITEM_BUDGET = void 0;
const emptiness_js_1 = require("../../core/agent/emptiness.js");
Object.defineProperty(exports, "parseMaybeJson", { enumerable: true, get: function () { return emptiness_js_1.parseMaybeJson; } });
Object.defineProperty(exports, "readEmptiness", { enumerable: true, get: function () { return emptiness_js_1.readEmptiness; } });
const boundedJson_js_1 = require("../../lib/boundedJson.js");
const saidByPerson_js_1 = require("../../lib/saidByPerson.js");
const size_js_1 = require("../size.js");
const render_js_1 = require("../render.js");
const view_js_1 = require("../view.js");
/**
* The bulleted items the account prints IN TOTAL — declared coverage items and
* flagged values. Each item rides a sentence (its text three times: `text`,
* `parts`, `vars`, plus pointers, about 1 KB of structure), so both the count
* and the characters are bounded; past either, items fold into `items.more@1`
* for readability. `size.ts` separately enforces the serialized-byte bound.
*/
exports.ITEM_BUDGET = { items: 12, chars: 3_000 };
/** Take one item of `chars` characters from the budget; `false` = fold it. */
function takeItem(ctx, chars) {
if (ctx.budget.items + 1 > exports.ITEM_BUDGET.items || ctx.budget.itemChars + chars > exports.ITEM_BUDGET.chars)
return false;
ctx.budget.items += 1;
ctx.budget.itemChars += chars;
return true;
}
exports.takeItem = takeItem;
/**
* Long text the account prints in TOTAL, past the first `SHORT_TEXT_CHARS` of
* each value: once spent, every later value is cut at `SHORT_TEXT_CHARS` (marked
* `clipped`, followed by `part.clipped@1`). This keeps ordinary accounts
* readable; `size.ts` separately enforces the serialized-byte bound.
*/
exports.LONG_TEXT_BUDGET = 3_000;
exports.SHORT_TEXT_CHARS = 300;
/**
* A text value a call's FACT carries is cut here (the sentence that prints it
* carries up to 2,000). A cut that keeps an ordinary account readable; the
* account's size bound itself is measured in bytes (`../size.ts`).
*/
exports.FACT_TEXT_CHARS = 200;
function spendText(vars, budget) {
const out = {};
for (const [name, variable] of Object.entries(vars)) {
const text = variable.value;
if (typeof text !== 'string' || text.length <= exports.SHORT_TEXT_CHARS) {
out[name] = variable;
continue;
}
const allowed = Math.min(text.length, exports.SHORT_TEXT_CHARS + Math.max(0, exports.LONG_TEXT_BUDGET - budget.longChars));
budget.longChars += allowed - exports.SHORT_TEXT_CHARS;
out[name] =
allowed >= text.length
? variable
: { ...variable, value: text.slice(0, allowed), clipped: true };
}
return out;
}
function makeSay(internals, onUnread, budget = { longChars: 0 }) {
return (id, spec = {}) => {
try {
if (internals?.failTemplate === id)
throw new Error(`forced failure of ${id}`);
return (0, render_js_1.sentence)(id, spec.vars === undefined ? spec : { ...spec, vars: spendText(spec.vars, budget) });
}
catch {
onUnread();
return (0, render_js_1.sentence)('unreadable.line', {
status: 'not-recorded',
missing: 'unreadable',
pointers: spec.pointers ?? [],
vars: {},
});
}
};
}
exports.makeSay = makeSay;
// ── pointers ─────────────────────────────────────────────────────────────
/** RFC 6901: escape `~` and `/` in one segment. */
const segment = (key) => String(key).replace(/~/g, '~0').replace(/\//g, '~1');
const jsonPointer = (...keys) => keys.map((k) => `/${segment(k)}`).join('');
exports.jsonPointer = jsonPointer;
/**
* An identity a pointer copies from the record — an event's `runtimeStageId`,
* a history message's `toolCallId` — cut at `POINTER_ID_BYTES` SERIALIZED
* bytes (`../size.ts`): short in every record this library writes, but a
* saved record can hold anything, and an id rides EVERY pointer into its
* event. Measured in the contract's unit, so a character JSON writes six
* bytes wide costs six. The pointer still lands: "show me" finds a leaf by its
* index and path (`pointerKey.ts`), never by this id.
*/
const identity = (id) => (0, boundedJson_js_1.cutToJsonBytes)(id, size_js_1.POINTER_ID_BYTES);
/**
* A pointer the account did not build itself — the standing fold's witnesses
* (`core/agent/assessment/assess.ts`) — with its identity cut as the
* builders below cut theirs.
*/
function boundedPointer(p) {
if (p.kind === 'history' && p.toolCallId !== undefined) {
return { ...p, toolCallId: identity(p.toolCallId) };
}
if (p.kind === 'event' && p.runtimeStageId !== undefined) {
return { ...p, runtimeStageId: identity(p.runtimeStageId) };
}
return p;
}
exports.boundedPointer = boundedPointer;
function at(event, ...keys) {
const runtimeStageId = (0, view_js_1.str)(event.meta.runtimeStageId);
return {
kind: 'event',
index: event.index,
type: event.type,
path: (0, exports.jsonPointer)(...keys),
...(runtimeStageId !== undefined && { runtimeStageId: identity(runtimeStageId) }),
};
}
exports.at = at;
/** A derived leaf on an event (`#emptiness`, `#meta/runId`). */
function derived(event, path) {
const runtimeStageId = (0, view_js_1.str)(event.meta.runtimeStageId);
return {
kind: 'event',
index: event.index,
type: event.type,
path,
...(runtimeStageId !== undefined && { runtimeStageId: identity(runtimeStageId) }),
};
}
exports.derived = derived;
const stateAt = (key, ...keys) => ({
kind: 'state',
key,
path: (0, exports.jsonPointer)(...keys),
});
exports.stateAt = stateAt;
const historyAt = (index, path, toolCallId) => ({
kind: 'history',
index,
path,
...(toolCallId !== undefined && { toolCallId: identity(toolCallId) }),
});
exports.historyAt = historyAt;
function declarationAt(declarations, field) {
return {
kind: 'declaration',
owner: 'app',
field,
...(declarations.id !== undefined && { id: declarations.id }),
...(declarations.version !== undefined && { version: declarations.version }),
};
}
exports.declarationAt = declarationAt;
// ── vars ─────────────────────────────────────────────────────────────────
/** A tool name the LIBRARY recorded (its claims are vouched by the template). */
const toolVar = (name, from) => (0, render_js_1.v)(name, 'library', from);
exports.toolVar = toolVar;
/**
* The `{{name:skill}}` pair: the id (library) and, when there is one, its plain
* label — the recorded `title` (library) or the app's declared label (app).
*/
function skillVars(ctx, name, id, from) {
const out = { [name]: (0, render_js_1.v)(id, 'library', from) };
const node = ctx.view.ofType('skill.graph_declared').flatMap((e) => {
const nodes = Array.isArray(e.payload.nodes) ? e.payload.nodes : [];
return nodes.flatMap((node, i) => (0, view_js_1.isRecord)(node) && node.id === id && typeof node.title === 'string' && node.title.length > 0
? [{ title: node.title, pointer: at(e, 'nodes', i, 'title') }]
: []);
})[0];
if (node !== undefined) {
out[`${name}Label`] = (0, render_js_1.v)(node.title, 'library', node.pointer);
return out;
}
const label = ctx.declarations.skills?.[id]?.label;
if (label !== undefined) {
out[`${name}Label`] = (0, render_js_1.v)(label, 'app', declarationAt(ctx.declarations, `skills.${id}.label`));
}
return out;
}
exports.skillVars = skillVars;
/** The app's declared rows key for one tool, when it declared one. */
const rowsAtOf = (declarations, toolName) => declarations.tools?.[toolName]?.rowsAt;
exports.rowsAtOf = rowsAtOf;
/** The source a sentence's emptiness claim rests on. */
const emptinessSource = (reading) => reading.source === 'app' ? 'app' : 'library';
exports.emptinessSource = emptinessSource;
/**
* The count a fact carries from one reading — ONE owner for the three fact
* kinds (this run's calls, the calls before a pause, the earlier answers in
* view): how many rows, who counted them, and where in the value read the
* count was taken (a JSON pointer; "show me" points there — `shown.ts`).
*/
const countedFields = (reading) => ({
...(reading.rows !== undefined && { rows: reading.rows }),
...(reading.source !== undefined && { emptinessSource: reading.source }),
...(reading.countedAt !== undefined && { countedAt: (0, exports.jsonPointer)(...reading.countedAt) }),
});
exports.countedFields = countedFields;
/**
* `sharedState.history`, when it is an array — and EMPTY when the record keeps
* it out (a redaction policy's placeholder). It asks the view first, so the
* view's guard counts the kept-out history as handled; a reader that would
* otherwise say something about what the history lacks asks
* `view.isStateKeptOut('history')` itself (`inView.ts`, `checked.ts`).
*/
function historyOf(view) {
if (view.isStateKeptOut('history'))
return [];
const history = view.state?.history;
return Array.isArray(history) ? history : [];
}
exports.historyOf = historyOf;
/**
* True when this recorded history entry is a message a PERSON wrote — the
* one rule (`lib/saidByPerson.ts` · `isSaidByPerson`), read off a record. A
* `role: 'user'` entry is not enough: the library writes frames in that role
* (an evidence correction, a budget wrap-up, a drop notice), and one that
* lands after a turn's results would move "the current request" past them.
*/
function isPersonEntry(entry) {
// The cheap test first: this runs once per history entry per witness (`inView.ts`).
return ((0, view_js_1.isRecord)(entry) &&
entry.role === 'user' &&
typeof entry.content === 'string' &&
(0, saidByPerson_js_1.isSaidByPerson)(entry));
}
exports.isPersonEntry = isPersonEntry;
//# sourceMappingURL=common.js.map