UNPKG

agentfootprint

Version:

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

184 lines • 9 kB
"use strict"; /** * resolve — which reads a PROSE claim names (design § 5.2): by the entity each call was SENT * for (its `resolved` id, name and aliases — store data never binds another entity's name), or, * for a search with no population, by its `source.said` words. * * Pattern: mentions are token runs of the unit that hold a name by the library's one name rule * (`lib/normalize.ts` · `sameName`: `Payments` is `payments`, `ARRAY-X9` is not * `array-x9`); a mention joined to another word by "or", * "and", "nor" or a comma is a second entity, and an entity no read resolves makes the * whole claim `entity-unresolved` — "payments or checkout" with one searched cannot pass * as checked. The reads a claim RESTS ON are this turn's reads (any leg) of a resolved * entity whose tool's `subject.said` word the unit names; a call on the same entity and * another subject is never pulled in. A claim naming no entity is unresolved; the * person's message and the single call of the turn only name a CANDIDATE. * Role: integrity/ layer (L3), read by `prose.ts`. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveProseClaim = exports.isCurrent = void 0; const capture_js_1 = require("../claim-binding/capture.js"); const matcher_js_1 = require("./matcher.js"); const readings_js_1 = require("./readings.js"); /** The calls of the answer's turn, in this recording. */ function isCurrent(call, record) { try { const identity = (0, capture_js_1.readResultIdentity)(call.resultId); // Namespace and turn, never the run id: a pause between legs of one turn keeps a read current. return identity.namespace === record.leg.namespace && call.turn === record.turn; } catch { return false; } } exports.isCurrent = isCurrent; /** The names a read was sent for: each population value's resolved id, name and aliases. */ function namesOf(call, work) { const out = []; for (const item of (0, readings_js_1.scopeOf)(call) ?? []) { if (item.facet !== 'population' || item.resolved === undefined) continue; const entity = (0, readings_js_1.spellValue)(item.resolved.id); for (const name of [item.resolved.id, item.resolved.name, ...(item.resolved.aliases ?? [])]) { if (typeof name !== 'string') continue; const words = (0, matcher_js_1.spell)(name, work); if (words.length > 0) out.push({ entity, name, words }); } } return out; } const hasPopulation = (call) => ((0, readings_js_1.scopeOf)(call) ?? []).some((item) => item.facet === 'population'); /** Words that join or frame entities, never name one. */ const GRAMMAR = new Set(('a an the any no none not nor or and but so then also only just all each every either ' + 'neither both some many most more less few other others its it their they them this that ' + 'these those there here is are was were be been being has have had do does did will would ' + 'can could may might must should in on at of to for from with by into over under about ' + 'across within without between during since until than as if when while where which who ' + 'what why how i we you he she me us my our your his her yet still ' + 'found exist exists').split(' ')); /** Every word a declaration uses for a subject, source, qualifier or filter value. */ function declaredWords(record, work) { const out = new Set(); const add = (phrases) => { for (const phrase of phrases ?? []) for (const word of (0, matcher_js_1.spell)(phrase, work)) out.add(word); }; for (const tool of Object.values(record.tools)) { const search = tool.search; if (search === undefined) continue; add(search.subject.said); add(search.source.said); for (const fixed of search.fixed ?? []) add(fixed.said); for (const argument of search.arguments) for (const said of Object.values(argument.said ?? {})) add(said); } return out; } const JOINERS = new Set(['or', 'and', 'nor']); /** Whether a mention is joined to a word that names an entity no read resolved. */ function joinedToUnknown(text, tokens, mention, covered, declared) { const named = (i) => { const token = tokens[i]; return (token !== undefined && !covered.has(i) && /^[\p{L}\p{N}]/u.test(token.text) && !GRAMMAR.has(token.text) && !declared.has(token.text)); }; const gap = (a, b) => { const left = tokens[a]; const right = tokens[b]; return left === undefined || right === undefined ? '' : text.slice(left.end, right.start); }; const after = mention.last + 1; if (JOINERS.has(tokens[after]?.text ?? '') && named(after + 1)) return true; if (gap(mention.last, after).includes(',') && named(after)) return true; const before = mention.first - 1; if (JOINERS.has(tokens[before]?.text ?? '') && named(before - 1)) return true; return gap(before, mention.first).includes(',') && named(before); } /** The reads a prose claim resolves to, grouped by entity. */ function resolveProseClaim(claim, text, record, personMessage, work) { const tokens = claim.tokens; const reads = record.calls.filter((call) => (0, readings_js_1.isRead)(call)); const mentions = []; for (const call of reads) { // A name is mentioned only where the text holds it by the one name rule (`sameName`). for (const { entity, name, words } of namesOf(call, work)) { for (let at = (0, matcher_js_1.nameAt)(text, tokens, name, 0, work); at !== -1; at = (0, matcher_js_1.nameAt)(text, tokens, name, at + 1, work)) { work?.charge(words.length + 1); mentions.push({ entity, first: at, last: at + words.length - 1 }); } } } const covered = new Set(mentions.flatMap((m) => Array.from({ length: m.last - m.first + 1 }, (_, i) => m.first + i))); const declared = declaredWords(record, work); if (mentions.some((m) => joinedToUnknown(text, tokens, m, covered, declared))) { return { status: 'entity-unresolved', candidates: [] }; } const subjectFits = (call) => (0, matcher_js_1.holdsAnySaid)(tokens, record.tools[call.toolName]?.search?.subject.said, work); if (mentions.length === 0) { const bySource = reads.filter((call) => isCurrent(call, record) && !hasPopulation(call) && (0, matcher_js_1.holdsAnySaid)(tokens, record.tools[call.toolName]?.search?.source.said, work)); if (bySource.length > 0) { const restsOn = bySource.filter(subjectFits); return { status: 'groups', groups: [ restsOn.length > 0 ? { status: 'resolved', calls: restsOn } : { status: 'subject-unmatched', calls: bySource }, ], }; } return { status: 'entity-unresolved', candidates: candidatesOf(record, reads, personMessage, work), }; } const entities = [...new Set(mentions.map((m) => m.entity))]; const groups = entities.map((entity) => { const named = reads.filter((call) => namesOf(call, work).some((n) => n.entity === entity)); const current = named.filter((call) => isCurrent(call, record)); if (current.length === 0) { const last = named[named.length - 1]; return { status: 'stale', calls: named, staleSince: { turn: last.turn, ...(last.readAt === undefined ? {} : { readAt: last.readAt }), }, }; } const restsOn = current.filter(subjectFits); return restsOn.length > 0 ? { status: 'resolved', calls: restsOn } : { status: 'subject-unmatched', calls: current }; }); return { status: 'groups', groups }; } exports.resolveProseClaim = resolveProseClaim; /** A fallback's candidates: the reads the person's message names, else the turn's single call. */ function candidatesOf(record, reads, personMessage, work) { const current = reads.filter((call) => isCurrent(call, record)); if (personMessage !== undefined) { work?.charge(personMessage.length + 1); const said = (0, matcher_js_1.tokensOf)(personMessage); const named = current.filter((call) => namesOf(call, work).some((n) => (0, matcher_js_1.nameAt)(personMessage, said, n.name, 0, work) !== -1)); if (named.length > 0) return named.map((call) => call.resultId); } return current.length === 1 ? [current[0].resultId] : []; } //# sourceMappingURL=resolve.js.map