UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

143 lines • 6.66 kB
/** * prose — the prose door (design § 5.2): negative phrases anchored in a dispatched read's * declared words, sent values or resolved names are resolved and NOTED against the record. * An anchor permits a finding, never binds an entity or establishes a claim. Observe-only: * a note never withholds, whatever the validation mode; a claim may carry no note. * * Pattern: detect (`detect.ts`) → data anchor (`anchor.ts`) → resolve (`resolve.ts`) → containment notes * (`containment.ts`), one row per claim and one per unit the grammar could not judge * (`in-code-block`, `language-unsupported`), in text order. A record that may be missing * a call, or keeps one out by redaction, notes every claim `record-incomplete` and * nothing else: nothing can be said around a hole. More than MAX_NEGATIVE_CLAIMS * returns one `too-many-claims` note for the whole text, before reading the record. * Role: integrity/ layer (L3). The agent's negativeClaims arm observes every original * model string within a whole-answer budget and files these rows; no prose enforcement. * * Rows carry spans into the text, phrase ids, the pack version, note codes and result * identities — never a word of the text or a value of the record. */ import { claimWordsOf, entityNotes } from './containment.js'; import { proseAnchorOf } from './anchor.js'; import { eligibleNoReadClaim } from './noRead.js'; import { isRead } from './readings.js'; import { detectNegativeClaims } from './detect.js'; import { PROSE_NOTES } from './types.js'; import { MAX_NEGATIVE_CLAIMS } from './limits.js'; import { NEGATIVE_CLAIM_PHRASES_VERSION } from './phrases.js'; import { isCurrent, resolveProseClaim } from './resolve.js'; import { createProseWorkBudget, ProseWorkLimit } from './work.js'; const ordered = (notes) => PROSE_NOTES.filter((note) => notes.includes(note)); function claimRow(field, claim, notes, results, extra = {}) { return { kind: 'negative-claim', field, claim: claim.kind, span: claim.span, phraseId: claim.phraseId, packVersion: claim.packVersion, notes: ordered(notes), results: [...new Set(results.map((call) => call.resultId))], ...(extra.candidates === undefined || extra.candidates.length === 0 ? {} : { candidates: extra.candidates }), ...(extra.staleSince === undefined ? {} : { staleSince: extra.staleSince }), }; } /** Judge one claim against the record. */ function judgeClaim(input, claim, peers) { const { field, text, record } = input; if (!record.ledgerComplete || peers.some((call) => call.keptOut === true)) { return claimRow(field, claim, ['record-incomplete'], []); } const resolution = resolveProseClaim(claim, text, record, input.personMessage, input.work); if (resolution.status === 'entity-unresolved') { return claimRow(field, claim, ['entity-unresolved'], [], { candidates: resolution.candidates }); } const words = claimWordsOf(claim, text, record.clock, input.work); const notes = []; const results = []; let staleSince; for (const group of resolution.groups) { results.push(...group.calls); if (group.status === 'stale') { notes.push('stale-turn'); staleSince = group.staleSince; } else if (group.status === 'subject-unmatched') { notes.push('subject-unmatched'); } else { notes.push(...entityNotes(group.calls, words, record, peers)); } } const inOrder = record.calls.filter((call) => results.includes(call)); return claimRow(field, claim, notes, inOrder, staleSince === undefined ? {} : { staleSince }); } /** Note each claim, or one whole-text overflow note without judging any claim. */ export function judgeProseNegatives(input) { const { field, text, record } = input; const detection = detectNegativeClaims(text, input.phrases); if (detection.claims.length > MAX_NEGATIVE_CLAIMS) { return [ { kind: 'negative-claim-note', field, code: 'too-many-claims', span: { start: 0, end: text.length }, packVersion: input.phrases?.version ?? NEGATIVE_CLAIM_PHRASES_VERSION, }, ]; } // Detection and its cap precede every record read. A phrase alone is not // a data claim; anchoring only permits a note, never resolves its entity. const work = input.work ?? createProseWorkBudget(); try { work.charge(0); const anchored = detection.claims.length === 0 ? undefined : proseAnchorOf(record, work); const peers = record.calls.filter((call) => isCurrent(call, record)); // This describes the native record, never an assertion about external work. // A failed or hidden dispatched read is still a read attempt; an earlier // turn, a proposal or a write cannot stand in for one on this turn. const noRead = detection.claims.some(eligibleNoReadClaim) && !peers.some((call) => call.dispatched && isRead(call)); const rows = [ ...(noRead ? [ { kind: 'negative-claim-note', field, code: 'no-read', span: { start: 0, end: text.length }, packVersion: input.phrases?.version ?? NEGATIVE_CLAIM_PHRASES_VERSION, }, ] : []), ...detection.notes.map((note) => ({ kind: 'negative-claim-note', field, code: note.code, span: note.span, packVersion: note.packVersion, ...(note.phraseId === undefined ? {} : { phraseId: note.phraseId }), })), ...detection.claims .filter((claim) => anchored?.(claim, text) === true) .map((claim) => judgeClaim({ ...input, work }, claim, peers)), ]; return rows.sort((a, b) => a.span.start - b.span.start); } catch (error) { if (!(error instanceof ProseWorkLimit)) throw error; return [ { kind: 'negative-claim-note', field, code: 'input-limit', span: { start: 0, end: text.length }, packVersion: input.phrases?.version ?? NEGATIVE_CLAIM_PHRASES_VERSION, }, ]; } } //# sourceMappingURL=prose.js.map