UNPKG

agentfootprint

Version:

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

230 lines • 11.3 kB
/** * time/bind — which of the person's windows a tool call carries (time * design § 5.6, § 7.3): fill from one mention, bind by quote, and record a * differing model window and run it. * * Pattern: Walker over recorded rows. The turn's WINDOWS are read from what * the record already holds — this turn's `time-answer` rows (a * mention the person settled in the time ask), a `model` reader's * `time-reading` rows (a reading, never the person's) and the * `clock` row's `control` window — never from words. A `rule` * reading settles nothing: it is a proposal the person confirms * (the owner's decision "Always confirm", time design TQ29). One call's decision * ({@link callWindowOf}) is a pure function of its arguments, the * tool's forms and those windows. * Role: L0 leaf (the time layer). Imports `convert.ts`, `range.ts`, * `rows.ts`, `resolve.ts`' types and `zone.ts` only. Asked by the * inputs layer (`arguments/resolve.ts`), which files the argument * rows and the `call-window` row the decision implies. * Emits: N/A. * * ## The laws * * - **Fill only when the turn has exactly one window.** A message holding two * mentions ("this morning vs yesterday morning") fills nothing: which one a * call means is the model's to say, through the quote. A mention whose * reading is still open (the person has not chosen) is counted and fills * nothing; a refused mention (its quote was not in the message) is not the * person's words and is not counted. A `control` window (`time.window`, set * in a UI) counts as one more mention with no quote. * - **Fill only what the model left out — exactly, else wider, never * narrower.** Every argument of every form must be missing; the window goes * into the first form that holds it exactly (`convert.ts` · `convertExact`), * else the first that holds MORE (`convertWidened`: a whole day, the * covering look-back — recorded with what it adds). No such form → the call * is REFUSED, `no-form-holds` (`convert.ts` · `convertForTool`, the one * answer): the tool's own rule never stands in for the person's window, so * an assumed `1h` never silently reads a time the person did not ask about. * - **Refuse before dispatch what the tool cannot honestly read (step * T5b).** A window — the person's to fill, or the model's sent — outside * the tool's declared `direction`, wholly beyond its `retention` or wider * than its `maxRange`; a range across days for a tool whose every form is a * `day`; a window of the person's no form can read (`no-form-holds`); a * sent wall time the zone skips. A window only partly beyond `retention` * runs, marked. An open reading the tool can read in NO reading is refused * too, and never asked. * - **A present window is never written over.** Bound by quote (the model's * declared `user` quote names a recorded mention) or by value (the sent * value, read back, IS a window of this turn); otherwise it runs as sent and * is recorded `model-chosen` beside the person's window — a drill-down, a * comparison and a baseline call are normal work. */ import { convertForTool, formArguments, formFacts, lookbackAsBounds, partlyBeyondRetention, periodFactProblem, readBack, sameRange, wallGapArgument, } from './convert.js'; // ─── The turn's windows (`windows.ts`) ─────────────────────────────────── // The record half lives in `windows.ts` (split by FILE so the synchronous doors // never load this module's conversions); every public name is re-exported here. export { isOpenForPerson, offeredLookback, pendingQuotesOf, readerWindowsOf, turnWindowsOf, windowToConvert, } from './windows.js'; import { windowToConvert } from './windows.js'; const normalQuote = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim(); /** Whether a declared quote names a recorded mention: one holds the other, whitespace and case aside. */ function quoteNames(declared, mention) { const a = normalQuote(declared); const b = normalQuote(mention); return a.length > 0 && b.length > 0 && (a.includes(b) || b.includes(a)); } /** Whether a sent range is a window: the same instants, or a look-back sent as `[now − L, now)`. */ function isWindow(sent, window, now) { if (sameRange(sent, window.range)) return true; if (window.lookback === undefined) return false; const bounds = lookbackAsBounds(now, window.lookback); return bounds !== undefined && sameRange(sent, bounds); } /** * Which window one call carries (the module laws). Pure: the same call, forms * and windows always give the same decision. */ export function callWindowOf(call, turn, ctx) { const periodArguments = [ ...new Set(call.forms.flatMap((f) => formArguments(f).map((a) => a.argument))), ]; const anyPresent = periodArguments.some((a) => !call.isMissing(a)); return anyPresent ? presentWindow(call, turn, ctx) : fillWindow(call, turn, ctx); } /** * A present window's facts: refused when it breaks one, else marked when partly beyond retention. * The window was SENT in one form, so it is held to that form's own `maxRange` (`formFacts`). */ function judged(decision, call, now) { const sentIn = call.forms[decision.form]; const facts = call.facts === undefined || sentIn === undefined ? call.facts : formFacts(sentIn, call.facts); if (facts === undefined) return decision; const problem = periodFactProblem(decision.asked, facts, now); if (problem !== undefined) { const person = decision.how === 'bound' ? decision.window : decision.how === 'model-chosen' ? decision.person : undefined; return { how: 'refused', refused: problem, form: decision.form, asked: decision.asked, ...(person !== undefined && { person }), }; } return partlyBeyondRetention(decision.asked, facts, now) ? { ...decision, partlyBeyondRetention: true } : decision; } /** The model left every period argument out: fill from the turn's one window, exactly, else widened (§ 7.2). */ function fillWindow(call, turn, ctx) { if (turn.mentions === 0) return { how: 'not-filled', why: 'no-window' }; if (turn.mentions > 1) return { how: 'not-filled', why: 'several-mentions' }; const window = turn.windows[0]; if (window === undefined) return openReadingWindow(call, turn, ctx); const read = convertForTool(windowToConvert(window), call.forms, call.facts, { now: ctx.now, zone: window.zone, appZone: ctx.appZone, granularityMs: ctx.granularityMs, }); // No form reads it (or a fact rules it out): refused with the reason — the tool's own rule // never stands in for the person's window, which its default would silently not read. if ('refused' in read) { return { how: 'refused', refused: read.refused, asked: window.range, person: window }; } return { how: 'filled', window, conversion: read.conversion, ...('sent' in read.conversion && call.facts?.filtersToAsked === true && { trimmedByTool: true }), ...(read.partlyBeyondRetention === true && { partlyBeyondRetention: true }), }; } /** * The turn's one mention is still an open reading (the person has not chosen): * nothing is filled. When the tool can read NONE of the readings left — each * breaks a fact, or no form holds it — nothing is asked (§ 6.3): the call is * refused with the first reading's reason. Otherwise the reading is asked. */ function openReadingWindow(call, turn, ctx) { const left = turn.open?.[0] ?? []; const reasons = left.map((range) => { const read = convertForTool({ range }, call.forms, call.facts, ctx); return 'refused' in read ? read.refused : undefined; }); const first = reasons[0]; if (left.length > 0 && first !== undefined && reasons.every((r) => r !== undefined)) { return { how: 'refused', refused: first }; } return { how: 'not-filled', why: 'open-reading' }; } /** A present window: bound, the model's own, unread — or refused on a skipped wall time or a fact. */ function presentWindow(call, turn, ctx) { // The first form whose arguments read back as a range. let form = -1; let asked; for (let i = 0; i < call.forms.length && asked === undefined; i++) { const f = call.forms[i]; if (formArguments(f).some((a) => a.role !== 'zone' && call.isMissing(a.argument))) continue; asked = readBack(call.args, f, { now: ctx.now, ...(ctx.appZone !== undefined && { appZone: ctx.appZone }), }); if (asked !== undefined) form = i; } if (asked === undefined) return unreadWindow(call, ctx); if (turn.windows.length === 0) return judged({ how: 'model', form, asked }, call, ctx.now); // The windows the declared quote names; when it names several, the one the value matches wins. const named = call.quotes === undefined ? [] : turn.windows.filter((w) => w.quote !== undefined && call.quotes?.some((q) => quoteNames(q, w.quote))); const quoted = named.find((w) => isWindow(asked, w, ctx.now)) ?? named[0]; if (quoted !== undefined) { return judged(isWindow(asked, quoted, ctx.now) ? { how: 'bound', form, asked, window: quoted, by: 'quote' } : { how: 'model-chosen', form, asked, person: quoted }, call, ctx.now); } const equal = turn.windows.find((w) => isWindow(asked, w, ctx.now)); if (equal !== undefined) { return judged({ how: 'bound', form, asked, window: equal, by: 'value' }, call, ctx.now); } return judged({ how: 'model-chosen', form, asked, ...(turn.windows.length === 1 && { person: turn.windows[0] }), }, call, ctx.now); } /** No form reads the call back: a sent wall time the zone skips is refused (§ 7.2); anything else runs `unread`. */ function unreadWindow(call, ctx) { for (let i = 0; i < call.forms.length; i++) { const f = call.forms[i]; if (formArguments(f).some((a) => a.role !== 'zone' && call.isMissing(a.argument))) continue; const argument = wallGapArgument(call.args, f, { ...(ctx.appZone !== undefined && { appZone: ctx.appZone }), }); if (argument !== undefined) return { how: 'refused', refused: 'dst-gap', form: i, argument }; } return { how: 'unread' }; } /** The range a decision says the call asks for — the person's on a fill, the sent one otherwise. */ export function askedRangeOf(decision) { switch (decision.how) { case 'filled': return decision.window.range; case 'bound': case 'model-chosen': case 'model': return decision.asked; case 'refused': return decision.asked; default: return undefined; } } //# sourceMappingURL=bind.js.map