UNPKG

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
"use strict"; /** * 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