agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
261 lines • 11.7 kB
JavaScript
/**
* honesty/sourceCorpus — the calls' declared sources (`declaredSourcesOf`) and
* the corpora the inputs layer's checks read (`sourceCorpusOf`), built from
* the batch and the pieces the mount handed in.
*
* Pattern: one pure builder. Every corpus is read the way its one owner reads
* it: the person's messages through `lib/saidByPerson.ts` · `isSaidByPerson`,
* a result through `findings/offer.ts` · `isResultMessage` and its TOOL bytes
* through `lib/toolBytes.ts` · `toolBytesOf`, the model's current standing
* through `findings/ledger.ts` · `foldLedger`, this batch's own standings
* through `findings/reserved.ts` · `readDeclaration`.
* Role: core/agent/honesty — the one module on the layer's side that may
* import BOTH `findings/` and `arguments/` (the one-way law keeps
* `arguments/` a leaf). Loaded through `import()` by the mount's
* closures on first use (`mounts.ts` · `buildInputsSubflow`), only
* under declared sources (`.findings({ argumentSources: true })` or
* `.inputsLayer({ argumentSources: true })`) — never on a plain
* agent's graph.
* Emits: N/A.
*
* ## What counts, and what never does
*
* - **The person's words**: every served message a person wrote, the current
* request included, within the window. In a COMPOSED run
* (`AgentInput.messageFrom: 'composed'`) the run's own message is marked:
* it is another runner's output, never the person's.
* - **A result**: its tool's own bytes, cut at the tool-bytes boundary, so a
* library note never speaks for the tool; an id known only through the
* previous batch has no cut to read through and carries no text.
* - **The app's text**: `role: 'system'` history messages, the composed
* system prompt's records (raw content, or the summary a redacted record
* carries — the evidence gate's own reach, which includes memory recall and
* retrieval passages, adopted Q15), and the app's `externalGrounds` with
* their labels. The library's own always-on instructions (the findings
* ledger's, the ontology's) are left out: library text is never evidence.
* - **The person's answers**: the ledger's `answered` argument rows — the
* ask writes no message, so they live on the ledger, and compaction never
* evicts them.
* - **Assistant text** only to say a value was found nowhere else.
*/
import { isSaidByPerson } from '../../../lib/saidByPerson.js';
import { toolBytesOf } from '../../../lib/toolBytes.js';
import { ONTOLOGY_INSTRUCTION_ID } from '../../../ontology/instruction.js';
import { isRefused, rulesOf } from '../../../tools/declare.js';
import { foldLedger } from '../findings/ledger.js';
import { isResultMessage } from '../findings/offer.js';
import { FINDINGS_INSTRUCTION_ID, ownsReservedArgument, readDeclaration, } from '../findings/reserved.js';
import { RESERVED_ARGUMENT, } from '../findings/types.js';
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
const isMessage = (value) => isRecord(value) && typeof value.role === 'string' && typeof value.content === 'string';
/** The library's own always-on instructions — never the app's text. */
const LIBRARY_INSTRUCTIONS = new Set([
FINDINGS_INSTRUCTION_ID,
ONTOLOGY_INSTRUCTION_ID,
]);
/** The index of the current request — the last message a person said; `-1` when none is. */
function currentRequestOf(history) {
for (let i = history.length - 1; i >= 0; i -= 1) {
if (isSaidByPerson(history[i]))
return i;
}
return -1;
}
function personWordsOf(history, current, composed) {
const out = [];
history.forEach((m, i) => {
if (!isSaidByPerson(m))
return;
out.push({
text: m.content,
...(i < current && { earlier: true }),
...(composed && i === current && { composed: true }),
});
});
return out;
}
/**
* The model's CURRENT standing on each result — the ledger's fold plus the
* standings this batch's own `_findings.previous[]` declares, in batch order
* (the contingent law at dispatch counts a standing declared on the same
* batch; ToolCalls files them a moment later). Computed in memory, never
* filed.
*/
function standingsOf(ledger, calls, toolOf) {
const rows = (ledger ?? []).filter((row) => isRecord(row) && row.kind === 'standing');
const standings = new Map();
for (const [id, row] of foldLedger(rows).standingOf) {
standings.set(id, row.standing);
}
for (const call of calls) {
if (ownsReservedArgument(toolOf(call.name)?.schema))
continue;
const raw = call.args[RESERVED_ARGUMENT];
if (raw === undefined)
continue;
for (const entry of readDeclaration(raw).declaration?.previous ?? []) {
standings.set(entry.toolCallId, entry.standing);
}
}
return standings;
}
function servedResultsOf(history, current, standings, previousBatch) {
const out = [];
const seen = new Set();
history.forEach((m, i) => {
const id = m.toolCallId;
if (!isResultMessage(m) || id === undefined || id.length === 0 || seen.has(id))
return;
seen.add(id);
const standing = standings.get(id);
out.push({
toolCallId: id,
...(m.toolName !== undefined && { toolName: m.toolName }),
text: toolBytesOf(m),
...(i < current && { earlier: true }),
...(standing !== undefined && { standing }),
});
});
// An id the previous batch landed but the served window no longer holds: known, with
// no cut to read its bytes through — a claim on it is `uncheckable`, never "found".
for (const r of previousBatch ?? []) {
if (seen.has(r.toolCallId))
continue;
seen.add(r.toolCallId);
const standing = standings.get(r.toolCallId);
out.push({
toolCallId: r.toolCallId,
...(r.toolName !== undefined && { toolName: r.toolName }),
...(standing !== undefined && { standing }),
});
}
return out;
}
/** The app's `externalGrounds`, read as the choice seam reads them: a failing provider adds nothing. */
function groundsOf(provider) {
if (provider === undefined)
return [];
let entries;
try {
entries = provider();
}
catch {
return [];
}
if (!Array.isArray(entries))
return [];
const out = [];
for (const e of entries) {
if (!isRecord(e) || typeof e.value !== 'string' || typeof e.source !== 'string')
continue;
if (e.value.trim() === '' || e.source.trim() === '')
continue;
out.push({ text: e.value, label: e.source });
}
return out;
}
function appWordsOf(history, injections, externalGrounds) {
const out = [];
for (const m of history)
if (m.role === 'system')
out.push({ text: m.content });
for (const rec of injections ?? []) {
if (!isRecord(rec))
continue;
const id = typeof rec.sourceId === 'string' ? rec.sourceId : undefined;
if (rec.source === 'instructions' && id !== undefined && LIBRARY_INSTRUCTIONS.has(id))
continue;
const text = typeof rec.rawContent === 'string'
? rec.rawContent
: typeof rec.contentSummary === 'string'
? rec.contentSummary
: undefined;
if (text !== undefined && text !== '')
out.push({ text });
}
out.push(...groundsOf(externalGrounds));
return out;
}
/** The person's earlier answers to the library's ask — the ledger's `answered` argument rows. */
function answersOf(ledger, toolOf) {
const out = [];
for (const row of ledger ?? []) {
if (!isRecord(row) || row.kind !== 'argument' || row.source !== 'answered')
continue;
const { toolName, argument, value, turn } = row;
if (typeof toolName !== 'string' || typeof argument !== 'string')
continue;
if (typeof value !== 'string' || typeof turn !== 'number')
continue;
const period = row.period === true;
const rules = period ? rulesOf(toolOf(toolName)) : undefined;
const spelling = rules !== undefined && !isRefused(rules) ? rules.period?.spelling : undefined;
out.push({
toolName,
argument,
value,
turn,
...(period && { period: true }),
...(spelling !== undefined && { spelling }),
});
}
return out;
}
/**
* Each call's declared sources, through the ONE reader of `_findings`
* (`findings/reserved.ts` · `readDeclaration`, under the sources arm), judged
* against the call's own arguments. A call whose tool OWNS the reserved
* argument declares nothing and gets no entry; a call with no `_findings`
* declares an empty `from`. The dropped entries' count rides only when the
* call files no basis row (that row carries it otherwise) — and a call files
* one only beside the findings ledger (`options.basisRows`: `.findings()` is
* armed and the call declared a basis). Declared sources without the ledger
* file no basis row at all, so the count always rides the first argument row.
*/
export function declaredSourcesOf(calls, toolOf, options) {
const out = [];
for (const call of calls) {
const tool = toolOf(call.name);
if (ownsReservedArgument(tool?.schema))
continue;
const args = call.args;
if (!Object.prototype.hasOwnProperty.call(args, RESERVED_ARGUMENT)) {
out.push({ toolCallId: call.id, from: [] });
continue;
}
const { [RESERVED_ARGUMENT]: raw, ...rest } = args;
const read = readDeclaration(raw, { argumentSources: true }, rest);
const filesBasisRow = options?.basisRows === true && read.declaration?.basis !== undefined;
const malformed = !filesBasisRow && (read.sourcesMalformed ?? 0) > 0 ? read.sourcesMalformed : undefined;
out.push({
toolCallId: call.id,
from: read.declaration?.from ?? [],
...(malformed !== undefined && { malformed }),
});
}
return out;
}
// FOLD · the one owner of the corpora the declared-sources checks read
// consumers read this and never re-derive it: arguments/checks.ts · checkSource, handed the corpus by the
// layer's VERIFY stage (arguments/subflow.ts · verifyArgumentsStage) through the mount's closure.
// detached: yes — built per batch from plain copies of committed state; never stored.
/**
* The corpora the declared-sources checks read, for one batch — built from
* the raw pieces the mount handed in (`SourceInputs`), the batch itself (its
* own `previous[]` standings) and the app's `externalGrounds`.
*/
export function sourceCorpusOf(inputs, calls, turn, deps) {
const history = inputs.history.filter(isMessage);
const current = currentRequestOf(history);
return {
turn,
person: personWordsOf(history, current, inputs.composed === true),
results: servedResultsOf(history, current, standingsOf(inputs.ledger, calls, deps.toolOf), inputs.previousBatch),
assistant: history
.filter((m) => m.role === 'assistant' && m.content.trim() !== '')
.map((m) => m.content),
app: appWordsOf(history, inputs.systemPromptInjections, deps.externalGrounds),
answers: answersOf(inputs.ledger, deps.toolOf),
};
}
//# sourceMappingURL=sourceCorpus.js.map