agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
361 lines • 16.4 kB
JavaScript
/**
* scripts/attachment — `.scriptTravelsWithTheAnswer()`: the recorded program
* itself, under the answer, labelled from the execution record.
*
* Pattern: composed by the FRAMEWORK, never requested from the model (the
* `.limitsTravelWithTheAnswer()` precedent, `semantics/coverage/
* answer.ts`). Pure: rows in, data and text out.
* Role: core/ layer. Read by the final branch (`stages/prepareFinal.ts`):
* the data rides `turn_end.scripts` on every answer under the arm;
* the text is appended to a PROSE answer through the one composer
* (`composeAnswerWithCoverage`), after the standing line and the
* limits block. A typed answer gets the data only — its bytes are
* never edited.
* Emits: N/A.
*
* ## Why the library attaches it
*
* An app's rule "always show the script" is a rule the model can break, and
* the cheapest revision that passes a names-and-numbers check is to delete
* the script. The attachment needs no cooperation from the model: the program
* comes from the record (the row's `text` — what was SUBMITTED to the tool),
* so there is nothing to retype and nothing to check. When the model also
* showed a matched copy, the attachment is still appended: the model's copy
* never decides whether the authoritative one is delivered.
*
* ## What is attached (design § 4.1, § 4.4)
*
* - `which: 'last'` (default): the last call of the turn that REACHED the tool
* (its label is not `submitted`); `'all'`: every such call, at most
* {@link MAX_ATTACHED_SCRIPTS}, the rest counted in `omitted`.
* - `maxBytes` (default 4,000 UTF-8 bytes) bounds each program shown, cut at a
* code-point boundary; a cut says so — `[incomplete: showing N of M bytes —
* not the whole program]` and `truncated.shownBytes` — and is never
* advertised as a complete, reproducible program.
* - A program over `MAX_SCRIPT_BYTES` has no text on its row: it is counted in
* `omitted` and the text says "one script of N bytes is over the bound and
* is not shown". Calls past `MAX_SCRIPT_ROWS` have no row: counted too.
*
* Every line is library-authored and past tense, names the CALL (never a
* destination), and uses the label's words (`label.ts` · `labelWords`).
*
* @example
* ```ts
* const data = scriptsOfAnswer(scriptRowsOfTurn(ledger, turn).current, { which: 'last', maxBytes: 4000 });
* scriptsBlock(data);
* // Script submitted to run_code on call c1 — completed, exit 0. Attached by the library from the run record:
* //
* // ```python
* // print(41 + 1)
* // ```
* ```
*/
import { utf8Length, utf8Prefix } from '../../../lib/utf8.js';
import { codeRegionsOf } from '../evidence/codeRegions.js';
import { inertCode } from './inert.js';
import { exitCodeFits, labelWords, SCRIPT_LABELS } from './label.js';
import { MAX_SCRIPT_BYTES } from './rows.js';
import { wrapProgram } from './wrapper.js';
/** Scripts attached under `which: 'all'`; the rest are counted (`omitted`). Design § 4.1. */
export const MAX_ATTACHED_SCRIPTS = 8;
/** Default UTF-8 bytes of each program shown. Design § 4.1. */
export const DEFAULT_ATTACHED_BYTES = 4_000;
const DOOR = 'AgentBuilder.scriptTravelsWithTheAnswer';
/**
* Check `.scriptTravelsWithTheAnswer()`'s options once, at the call site, into
* the arm the chart carries. Refusals name the option and the fix.
*/
export function resolveScriptTravel(opts = {}) {
if (opts === null || typeof opts !== 'object' || Array.isArray(opts)) {
throw new Error(`${DOOR}: options must be an object ({ which?, maxBytes? }).`);
}
const unknown = Object.keys(opts).filter((k) => k !== 'which' && k !== 'maxBytes');
if (unknown.length > 0) {
throw new Error(`${DOOR}: unknown option(s) ${unknown.map((k) => `'${k}'`).join(', ')}. ` +
"The options are which ('last' | 'all') and maxBytes (UTF-8 bytes shown per program).");
}
const which = opts.which ?? 'last';
if (which !== 'last' && which !== 'all') {
throw new Error(`${DOOR}: which '${String(which)}' is not one this version has. Use 'last' (the last ` +
"call of the turn that reached the tool — the default) or 'all' (every such call, at " +
`most ${MAX_ATTACHED_SCRIPTS}).`);
}
const maxBytes = opts.maxBytes ?? DEFAULT_ATTACHED_BYTES;
if (!Number.isInteger(maxBytes) || maxBytes < 1) {
throw new Error(`${DOOR}: maxBytes must be a whole number of at least 1 — got ${String(opts.maxBytes)}. ` +
`It bounds the UTF-8 bytes of each program shown (default ${DEFAULT_ATTACHED_BYTES}); ` +
'a longer program is cut and says so.');
}
return { which, maxBytes };
}
/** A call that reached the tool — the attachment's candidates. */
const reached = (row) => row.outcome !== 'submitted';
/** One row as an attached script, cut at `maxBytes`. */
function shownFrom(row, maxBytes) {
const cut = utf8Prefix(row.text, maxBytes);
const shownBytes = cut === row.text ? row.bytes : utf8Length(cut);
return {
toolCallId: row.toolCallId,
dispatch: row.dispatch,
tool: row.tool,
...(row.language !== undefined && { language: row.language }),
label: row.outcome,
...(row.exitCode !== undefined && { exitCode: row.exitCode }),
...(row.resultWithheld !== undefined && { resultWithheld: row.resultWithheld }),
...(row.rewritten === true && { rewritten: true }),
bytes: row.bytes,
lines: row.lines,
...(cut !== row.text && { truncated: { shownBytes } }),
text: cut,
};
}
/**
* What travels with this turn's answer — the data form (`turn_end.scripts`).
* `rows` is this turn's reading (`rows.ts` · `scriptRowsOfTurn`); fresh plain
* objects, never a reference into the ledger.
*/
export function scriptsOfAnswer(rows, travel, scriptsDropped = 0) {
const candidates = rows.filter(reached);
const chosen = travel.which === 'last' ? candidates.slice(-1) : candidates;
const shown = [];
const overSize = [];
let omitted = scriptsDropped;
for (const row of chosen) {
if (row.text === undefined) {
overSize.push(row);
omitted += 1;
}
else if (shown.length < MAX_ATTACHED_SCRIPTS) {
shown.push(shownFrom(row, travel.maxBytes));
}
else {
omitted += 1;
}
}
return { shown, omitted, overSize };
}
/** The call a line names, and what its record says — every dynamic value inert (`inert.ts`). */
function callSaid(script) {
const withheld = script.resultWithheld === 'over-ceiling'
? "; its result was withheld over the tool's ceiling"
: '';
const rewritten = script.rewritten === true ? '; a before-tool rule changed it before dispatch' : '';
return (`Script submitted to ${inertCode(script.tool)} on call ${inertCode(script.toolCallId)} — ` +
`${labelWords(script.label, script.exitCode)}${withheld}${rewritten}.`);
}
/** The label line of one attached script — library words, past tense, the call named. */
export function scriptLabelLine(script) {
return `${callSaid(script)} Attached by the library from the run record:`;
}
/** The line that stands for a script whose program is kept out of the text (`withheldFromText`). */
export function scriptWithheldLine(script) {
return (`${callSaid(script)} The program is not shown here: the formatting above could display ` +
'part of it as ordinary text. It is kept on the run record.');
}
/** One attached script as text: its label line, then the program in the library's fence. */
export function shownScriptBlock(script) {
const body = `${scriptLabelLine(script)}\n\n${wrapProgram(script.text, script.language)}`;
if (script.truncated === undefined)
return body;
return (`${body}\n[incomplete: showing ${script.truncated.shownBytes} of ${script.bytes} bytes — ` +
'not the whole program]');
}
const CLEAN = { kind: 'clean' };
/** The boundary after `before` (see {@link AttachmentBoundary}). */
export function attachmentBoundary(before) {
if (before.length === 0)
return CLEAN;
const read = codeRegionsOf(`${before}\n`);
if (read.stoppedAt !== undefined)
return { kind: 'unsettled' };
if (read.openFence !== undefined)
return { kind: 'closed', fence: read.openFence };
return CLEAN;
}
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
const isCount = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0;
/**
* The boundary a `turn_end.scripts` record states — `undefined` when the
* record holds a value no writer of this library files: a `closedFence` that
* is not `{ char: 'backtick' | 'tilde', length }` with a whole length of at
* least 3 and at most `within` (the answer the closing fence was written into
* — a fence longer than the text cannot be in it), a `withheldFromText` that
* is not `true`, or both at once.
*/
function recordedBoundary(scripts, within) {
const withheld = scripts.withheldFromText;
const closed = scripts.closedFence;
if (withheld !== undefined && withheld !== true)
return undefined;
if (closed === undefined)
return withheld === true ? { kind: 'unsettled' } : { kind: 'clean' };
if (withheld === true || !isObject(closed))
return undefined;
const char = closed.char;
if ((char !== 'backtick' && char !== 'tilde') || !isCount(closed.length))
return undefined;
if (closed.length < 3 || closed.length > within)
return undefined;
return { kind: 'closed', char, length: closed.length };
}
/**
* The boundary to rebuild a recorded text behind — `undefined` when its fence
* is longer than `within`, the text it would have to be found in: the one
* repetition a reader makes is bounded by what it reads.
*/
export function rebuildBoundary(recorded, within) {
if (recorded.kind !== 'closed')
return recorded;
if (recorded.length > within)
return undefined;
return { kind: 'closed', fence: (recorded.char === 'tilde' ? '~' : '`').repeat(recorded.length) };
}
/**
* One `turn_end.scripts.shown` entry, checked against what `shownFrom` writes
* — a fresh copy, or `undefined` when any field is one no writer of this
* library files: a call that never reached the tool (`submitted` is never
* attached), an exit code its label does not carry (`label.ts` ·
* `exitCodeFits`), a program over `MAX_SCRIPT_BYTES`, or a text whose UTF-8
* length is not the bytes it says it shows (`truncated.shownBytes` when cut,
* and then fewer than `bytes`; else `bytes`).
*/
function shownScriptOf(value) {
if (!isObject(value))
return undefined;
const v = value;
const { toolCallId, tool, text, bytes, lines, dispatch, label, exitCode } = v;
if (typeof toolCallId !== 'string' || typeof tool !== 'string')
return undefined;
if (typeof text !== 'string' || !isCount(bytes) || !isCount(lines))
return undefined;
if (!isCount(dispatch) || dispatch < 1)
return undefined;
if (label === 'submitted' || !SCRIPT_LABELS.includes(label)) {
return undefined;
}
if (!exitCodeFits(label, exitCode))
return undefined;
if (v.language !== undefined && typeof v.language !== 'string')
return undefined;
if (v.resultWithheld !== undefined && v.resultWithheld !== 'over-ceiling')
return undefined;
if (v.rewritten !== undefined && v.rewritten !== true)
return undefined;
if (bytes > MAX_SCRIPT_BYTES)
return undefined;
const cut = v.truncated;
let shownBytes = bytes;
if (cut !== undefined) {
if (!isObject(cut) || !isCount(cut.shownBytes) || cut.shownBytes >= bytes)
return undefined;
shownBytes = cut.shownBytes;
}
// A UTF-8 encoding is never shorter than the string's length in code
// units: a longer string is refused before it is measured.
if (text.length > shownBytes || utf8Length(text) !== shownBytes)
return undefined;
return {
toolCallId,
dispatch,
tool,
...(v.language !== undefined && { language: v.language }),
label: label,
...(exitCode !== undefined && { exitCode: exitCode }),
...(v.resultWithheld !== undefined && { resultWithheld: 'over-ceiling' }),
...(v.rewritten !== undefined && { rewritten: true }),
bytes,
lines,
...(cut !== undefined && { truncated: { shownBytes } }),
text,
};
}
/**
* A saved `turn_end.scripts` — THE reader of the shape for a reader of a
* record. `undefined` when the event carries no scripts arm. `within` is the
* length of the answer the block was appended to (a fence longer than it is
* refused). Bounded by the writer's cap: no reader iterates more entries
* than a writer attaches.
*/
export function recordedScripts(value, within = Number.MAX_SAFE_INTEGER) {
if (value === undefined)
return undefined;
if (!isObject(value) || !Array.isArray(value.shown))
return { shown: [], unread: 1 };
const all = value.shown;
const shown = [];
let unread = Math.max(0, all.length - MAX_ATTACHED_SCRIPTS);
for (let index = 0; index < Math.min(all.length, MAX_ATTACHED_SCRIPTS); index++) {
const script = shownScriptOf(all[index]);
if (script === undefined)
unread += 1;
else
shown.push({ index, script });
}
const omitted = isCount(value.omitted) ? value.omitted : undefined;
if (omitted === undefined)
unread += 1;
const boundary = recordedBoundary(value, within);
if (boundary === undefined)
unread += 1;
return {
shown,
unread,
...(omitted !== undefined && { omitted }),
...(boundary !== undefined && { boundary }),
};
}
/**
* The text form: one block per attached script, then one line per omission —
* `''` when nothing travels (the composer then appends nothing). Every cut and
* every omission is stated. Behind a `closed` boundary the block opens with
* the closing fence; behind an `unsettled` one each script is its label line
* alone (`scriptWithheldLine`) — the programs ride the data only.
*/
export function scriptsBlock(scripts, boundary = CLEAN) {
const parts = boundary.kind === 'unsettled'
? scripts.shown.map(scriptWithheldLine)
: scripts.shown.map(shownScriptBlock);
const overSize = scripts.overSize ?? [];
for (const row of overSize) {
parts.push(`One script of ${row.bytes} bytes (${inertCode(row.tool)}, call ${inertCode(row.toolCallId)}) is over the bound and is not shown.`);
}
const others = scripts.omitted - overSize.length;
if (others > 0) {
parts.push(`${others} more script${others === 1 ? ' was' : 's were'} submitted this turn and ` +
`${others === 1 ? 'is' : 'are'} not shown here.`);
}
if (parts.length === 0)
return '';
if (boundary.kind === 'closed')
parts.unshift(boundary.fence);
return parts.join('\n\n');
}
/**
* The text form behind the boundary the text before it leaves — `''` when
* nothing travels (no boundary is then taken). The second value is the
* boundary the text was written behind: what `turn_end.scripts` records.
*/
export function scriptsBlockAfter(scripts, before) {
if (scriptsBlock(scripts) === '')
return { text: '', boundary: CLEAN };
const boundary = attachmentBoundary(before);
return { text: scriptsBlock(scripts, boundary), boundary };
}
/** The data form as detached plain data — what `turn_end.scripts` carries. */
export function turnEndScriptsOf(scripts, boundary = CLEAN) {
return {
shown: scripts.shown.map((s) => ({
...s,
...(s.truncated !== undefined && { truncated: { shownBytes: s.truncated.shownBytes } }),
})),
omitted: scripts.omitted,
...(boundary.kind === 'closed' && {
closedFence: {
char: boundary.fence.charCodeAt(0) === 0x7e ? 'tilde' : 'backtick',
length: boundary.fence.length,
},
}),
...(boundary.kind === 'unsettled' && { withheldFromText: true }),
};
}
//# sourceMappingURL=attachment.js.map