agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
116 lines • 5.73 kB
JavaScript
/**
* Earlier answers' tool results that were IN FRONT OF THE MODEL when it
* answered — proven by the record's witness, never by end-of-run state.
*
* The witness rule, in typed terms:
* N = the `iteration` of the LAST `stream.llm_start` before `turn_end` (the
* answering iteration). Context assembly for an iteration PRECEDES its
* `iteration_start`, and `context.injected` carries no iteration of its
* own, so the rule keys on the compose STAGE:
* the witnesses are the `context.injected` rows with `slot: 'messages'`,
* `source: 'tool-result'`, `sourceId: <toolCallId>` whose
* `meta.runtimeStageId` equals that of the `context.slot_composed` with
* `slot: 'messages'` and `iteration: N`.
* The content is parsed from the matching `history` message (by `toolCallId`).
*
* Only EARLIER answers' results: `distance` (messages a PERSON wrote after the
* result, up to and including the current one — `common.ts` · `isPersonEntry`,
* never a frame the library wrote in the user role) must be at least 1, and a call of this run
* is never "in view" — on the flagship recording the answering compose also
* witnesses this run's own `get_array_inventory` result (`events[68]`), which
* must never become an in-view line.
*/
import { toolBytesOf } from '../../lib/toolBytes.js';
import { v } from '../render.js';
import { isRecord, keptOut as isFieldKeptOut, num, str, } from '../view.js';
import { at, countedFields, FACT_TEXT_CHARS, historyAt, historyOf, isPersonEntry, readEmptiness, rowsAtOf, } from './common.js';
/** At most this many in-view results are listed, newest first. */
export const MAX_IN_VIEW = 3;
/** N — the answering iteration. `undefined` when no `llm_start` is recorded. */
export function answeringIteration(view) {
const end = view.last('agent.turn_end');
const starts = view
.ofType('stream.llm_start')
.filter((e) => end === undefined || e.index < end.index);
return num(starts[starts.length - 1]?.payload.iteration);
}
/** The witness rows of the answering iteration's messages compose. */
export function witnessesOf(view, iteration) {
if (iteration === undefined)
return [];
const composed = view
.ofType('context.slot_composed')
.filter((e) => e.payload.slot === 'messages' && e.payload.iteration === iteration);
const stage = str(composed[composed.length - 1]?.meta.runtimeStageId);
if (stage === undefined)
return [];
return view.ofType('context.injected').filter((e) => e.meta.runtimeStageId === stage &&
e.payload.slot === 'messages' &&
e.payload.source === 'tool-result' &&
// A witness whose call id the record keeps out is still a witness: a
// result was in view, whose call the record will not name.
(isFieldKeptOut(e, 'sourceId') || typeof e.payload.sourceId === 'string'));
}
export function readInView(ctx, callIds) {
const history = historyOf(ctx.view);
const historyKeptOut = ctx.view.isStateKeptOut('history');
const windowed = typeof ctx.view.first('agent.run_configured')?.payload.window === 'string';
const reads = [];
let keptOut = 0;
for (const witness of witnessesOf(ctx.view, ctx.answeringIteration)) {
// A call id the record keeps out is not one of this run's calls (those are
// ids the run recorded, and kept): an earlier result whose call is unnamed.
if (isFieldKeptOut(witness, 'sourceId')) {
keptOut += 1;
continue;
}
const id = witness.payload.sourceId;
if (callIds.has(id))
continue;
if (historyKeptOut) {
keptOut += 1;
continue;
}
const historyIndex = history.findIndex((m) => isRecord(m) && m.role === 'tool' && m.toolCallId === id);
if (historyIndex < 0)
continue;
const message = history[historyIndex];
const toolName = str(message.toolName);
if (toolName === undefined)
continue;
const distance = history.slice(historyIndex + 1).filter((m) => isPersonEntry(m)).length;
if (distance < 1)
continue;
// An EARLIER answer's result: its run's door is not in this record, so the one reader's strict
// recognizers read the history text itself — the only evidence of the door that is left. The
// TOOL's own bytes (`lib/toolBytes.ts` · `toolBytesOf`): a note the inputs layer appended after
// a filled call's result is the library's, and read whole it breaks the parse.
const rowsAt = rowsAtOf(ctx.declarations, toolName);
const reading = readEmptiness(toolBytesOf({ content: message.content, toolChars: message.toolChars }), rowsAt !== undefined ? { rowsAt } : {});
reads.push({
fact: {
toolName: toolName.slice(0, FACT_TEXT_CHARS),
toolCallId: id.slice(0, FACT_TEXT_CHARS),
distance,
windowed,
emptiness: reading.emptiness,
...countedFields(reading),
pointers: [at(witness, 'sourceId'), historyAt(historyIndex, '/toolName', id)],
},
witness,
historyIndex,
reading,
toolName,
tool: v(toolName, 'library', historyAt(historyIndex, '/toolName', id), FACT_TEXT_CHARS),
});
}
// Newest first: the result nearest the current question.
reads.sort((a, b) => b.historyIndex - a.historyIndex);
return {
all: reads,
listed: reads.slice(0, MAX_IN_VIEW),
more: Math.max(0, reads.length - MAX_IN_VIEW),
keptOut,
};
}
//# sourceMappingURL=inView.js.map