agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
277 lines • 12.4 kB
JavaScript
;
/**
* arguments/rows — the inputs layer's verdicts, as rows on the one ledger.
*
* Pattern: Map. One row type, its builders and its well-formedness check —
* the one owner of the `argument` row kind. `findings/types.ts` ·
* `FindingsRow` imports the type; this folder never imports
* `findings/` (the one-way law).
* Role: core/ layer leaf of the inputs layer (honesty layer 2).
* Emits: N/A (the Record stage emits one `agentfootprint.findings.argument`
* per row through the ledger's emit half).
*
* ## What a row may hold
*
* Names, enums and counts — and, in three fields only (`value`, `proposed`,
* `quote`), a value AS THE TOOL'S OWN ARGUMENT VIEW SHOWS IT
* (`core/toolShownArgs.ts` · `shownArgsOf`): a key the view hides reads
* `'REDACTED'`. The layer's checks run on the raw value in memory; the raw
* value is never stored on a row.
*
* ## One row per ruled argument per call, stamped with the turn
*
* `turn` is `AgentState.turnNumber` when the row was filed — the conversation
* turn, not the window position — because the ledger crosses turns on a
* continued conversation and `iteration` restarts at 1 every run. The
* standing fold reads this turn's rows only.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.argumentRowIsWellFormed = exports.sourcedRowOf = exports.shownQuote = exports.QUOTE_CHARS = exports.TIME_WINDOW_SOURCE = exports.windowRowOf = exports.argumentRowOf = exports.answeredRowOf = exports.askedRowOf = exports.shownValue = exports.HIDDEN_VALUE = exports.ARGUMENT_CHECKS_FAILED = exports.ARGUMENT_CLAIMS = exports.ARGUMENT_ASKED = exports.ARGUMENT_SOURCES = exports.ARGUMENT_MATCHED = void 0;
const argumentLeaves_js_1 = require("../../../integrity/argumentLeaves.js");
/** The closed vocabularies, for the checkpoint door and the event. */
exports.ARGUMENT_MATCHED = Object.freeze([
'quote',
'phrase',
'spelling',
'mention',
]);
exports.ARGUMENT_SOURCES = Object.freeze([
'said',
'answered',
'result',
'app',
'default',
'model',
]);
exports.ARGUMENT_ASKED = Object.freeze([
'missing',
'unverified',
'invalid-answer',
]);
exports.ARGUMENT_CLAIMS = Object.freeze([
'user',
'result',
'turn',
'app',
'assumed',
'none',
]);
exports.ARGUMENT_CHECKS_FAILED = Object.freeze([
'quote-not-found',
'composed-message',
'unknown-result',
'placed-result',
'not-in-result',
'no-earlier-turn',
'not-in-earlier-turns',
'only-in-model-answer',
'not-in-app-text',
'uncheckable',
]);
// ─── Builders ───────────────────────────────────────────────────────────
/** The placeholder a hidden argument reads — the redacted mirror's own. */
exports.HIDDEN_VALUE = 'REDACTED';
/**
* One value as a row may hold it: the shown view's value, printed as a string
* (`String()` for a number or a boolean, JSON for anything else) and clipped.
* A value the view replaced with the placeholder stays the placeholder.
*/
function shownValue(value) {
if (typeof value === 'string')
return (0, argumentLeaves_js_1.clipValue)(value);
if (typeof value === 'number' || typeof value === 'boolean')
return String(value);
let printed;
try {
printed = JSON.stringify(value) ?? String(value);
}
catch {
printed = String(value);
}
return (0, argumentLeaves_js_1.clipValue)(printed);
}
exports.shownValue = shownValue;
/** The identity fields, in the one order every `argument` row is written in. */
function identityOf(who, stamp) {
return {
kind: 'argument',
turn: stamp.turn,
toolCallId: who.toolCallId,
toolName: who.toolName,
iteration: stamp.iteration,
argument: who.argument,
rule: who.rule,
...(who.period === true && { period: true }),
};
}
/**
* The row for an argument the person is ASKED for: no `source` (nobody has
* given the value yet) and no `value` (there is none). `invalid-answer` when
* an answer did not fit the property's own schema and the field is asked
* again — the answer itself is not kept.
*/
function askedRowOf(who, stamp, asked = 'missing') {
return { ...identityOf(who, stamp), asked };
}
exports.askedRowOf = askedRowOf;
/**
* The row for a value the person's ANSWER filled (`source: 'answered'`): the
* value the call runs with, in the tool's own argument view; `proposed` only
* when the call had carried a value the answer replaced (absent = the call
* left it out); `free` when the answer came through a free-text field — a
* name the person typed, which is provenance and never support.
*/
function answeredRowOf(who, stamp) {
return {
...identityOf(who, stamp),
source: 'answered',
value: shownValue(who.shownValue),
...(who.shownProposed !== undefined && { proposed: shownValue(who.shownProposed) }),
...(who.free === true && { free: true }),
};
}
exports.answeredRowOf = answeredRowOf;
/**
* The row for one verdict. `proposed` is written only on a `default` row the
* model sent itself — on a `model` row the value IS the proposal, and a second
* copy would say nothing.
*/
function argumentRowOf(verdict, stamp) {
return {
kind: 'argument',
turn: stamp.turn,
toolCallId: verdict.toolCallId,
toolName: verdict.toolName,
iteration: stamp.iteration,
argument: verdict.argument,
rule: verdict.rule,
...(verdict.period === true && { period: true }),
source: verdict.source,
value: shownValue(verdict.shownValue),
...(verdict.source === 'default' &&
verdict.shownProposed !== undefined && { proposed: shownValue(verdict.shownProposed) }),
};
}
exports.argumentRowOf = argumentRowOf;
/**
* The row for a value the library filled from the turn's ONE window of the
* person's (the time layer, `time/bind.ts`): from the window the person
* settled in the time ask (`answered`, `matched: 'mention'`), from a `model`
* reader's unconfirmed reading of their words (`said`, `matched: 'mention'`
* and `reading` — it can support "not sure", never "known"), or from a window
* set in a UI (`app`, `appSource: 'time.window'`). A `said` window with no
* `reading` is read back only from a record an earlier version filed. The
* call's `call-window` row names the window and the form.
*/
function windowRowOf(who, stamp) {
const value = shownValue(who.shownValue);
if (who.window === 'control') {
return { ...identityOf(who, stamp), source: 'app', value, appSource: exports.TIME_WINDOW_SOURCE };
}
if (who.window === 'answered') {
return { ...identityOf(who, stamp), source: 'answered', value, matched: 'mention' };
}
return {
...identityOf(who, stamp),
source: 'said',
value,
matched: 'mention',
...(who.window === 'derived-from-reading' && { reading: true }),
};
}
exports.windowRowOf = windowRowOf;
/** The `appSource` of a value filled from the run's `time.window` (a UI control). */
exports.TIME_WINDOW_SOURCE = 'time.window';
// ─── The declared sources' rows ─────────────────────────────────────────
/**
* A quote's bound on a row — the bound a basis row's one-line texts carry
* (`findings/types.ts` · `PROPOSITION_CHARS`, which this leaf cannot import),
* the cut STATED in the text (`…[clipped N chars]`).
*/
exports.QUOTE_CHARS = 240;
/** A quote as a row holds it: at most `QUOTE_CHARS`, the cut stated. */
function shownQuote(quote) {
if (quote.length <= exports.QUOTE_CHARS)
return quote;
return `${quote.slice(0, exports.QUOTE_CHARS)} …[clipped ${quote.length - exports.QUOTE_CHARS} chars]`;
}
exports.shownQuote = shownQuote;
/**
* The row for one argument the declared-sources check judged (honesty layer
* 2, declared sources): the identity, the verdict's
* source (or `asked: 'unverified'` — the person is asked, and nothing runs on
* the model's value), the value in the tool's own view, and every field of the
* claim and the check as the verdict carries it. Written in the one field order
* `ArgumentRow` declares.
*/
function sourcedRowOf(verdict, stamp) {
return {
kind: 'argument',
turn: stamp.turn,
toolCallId: verdict.toolCallId,
toolName: verdict.toolName,
iteration: stamp.iteration,
argument: verdict.argument,
...(verdict.rule !== undefined && { rule: verdict.rule }),
...(verdict.period === true && { period: true }),
...(verdict.source !== undefined && { source: verdict.source }),
...(verdict.asked !== undefined && { asked: verdict.asked }),
...(verdict.shownValue !== undefined && { value: shownValue(verdict.shownValue) }),
...(verdict.shownProposed !== undefined && { proposed: shownValue(verdict.shownProposed) }),
claimed: verdict.claimed,
...(verdict.matched !== undefined && { matched: verdict.matched }),
...(verdict.shownQuoteText !== undefined && { quote: shownQuote(verdict.shownQuoteText) }),
...(verdict.reading === true && { reading: true }),
...(verdict.earlier === true && { earlier: true }),
...(verdict.result !== undefined && { result: verdict.result }),
...(verdict.setAside !== undefined && { setAside: verdict.setAside }),
...(verdict.argumentsFrom !== undefined && { argumentsFrom: verdict.argumentsFrom }),
...(verdict.appSource !== undefined && { appSource: (0, argumentLeaves_js_1.clipValue)(verdict.appSource) }),
...(verdict.coincides !== undefined && { coincides: verdict.coincides }),
...(verdict.malformed !== undefined &&
verdict.malformed > 0 && { malformed: verdict.malformed }),
...(verdict.failed !== undefined && { failed: verdict.failed }),
};
}
exports.sourcedRowOf = sourcedRowOf;
// ─── Well-formed ────────────────────────────────────────────────────────
const isIn = (value, vocabulary) => typeof value === 'string' && vocabulary.includes(value);
const optionalIn = (value, vocabulary) => value === undefined || isIn(value, vocabulary);
const optionalString = (value) => value === undefined || typeof value === 'string';
const optionalTrue = (value) => value === undefined || value === true;
/**
* Whether a value read from a stored checkpoint is an `argument` row this
* library could have written: the identity strings, a numeric `iteration` and
* `turn`, a `source` or an `asked` in its vocabulary, and every enum field in
* its own. The checkpoint door's `argument` arm (`core/runCheckpoint.ts`).
*/
function argumentRowIsWellFormed(r) {
return (r.kind === 'argument' &&
typeof r.toolCallId === 'string' &&
typeof r.toolName === 'string' &&
typeof r.argument === 'string' &&
typeof r.iteration === 'number' &&
typeof r.turn === 'number' &&
(isIn(r.source, exports.ARGUMENT_SOURCES) || isIn(r.asked, exports.ARGUMENT_ASKED)) &&
optionalIn(r.source, exports.ARGUMENT_SOURCES) &&
optionalIn(r.asked, exports.ARGUMENT_ASKED) &&
optionalIn(r.rule, ['ask', 'assume']) &&
optionalIn(r.claimed, exports.ARGUMENT_CLAIMS) &&
optionalIn(r.matched, exports.ARGUMENT_MATCHED) &&
optionalIn(r.setAside, ['open', 'noise', 'ruled-out']) &&
optionalIn(r.argumentsFrom, ['listed', 'unlisted']) &&
optionalIn(r.coincides, ['person', 'result', 'app']) &&
optionalIn(r.failed, exports.ARGUMENT_CHECKS_FAILED) &&
optionalString(r.value) &&
optionalString(r.proposed) &&
optionalString(r.quote) &&
optionalString(r.result) &&
optionalString(r.appSource) &&
optionalTrue(r.period) &&
optionalTrue(r.reading) &&
optionalTrue(r.earlier) &&
optionalTrue(r.free) &&
(r.malformed === undefined || typeof r.malformed === 'number'));
}
exports.argumentRowIsWellFormed = argumentRowIsWellFormed;
//# sourceMappingURL=rows.js.map