UNPKG

agentfootprint

Version:

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

276 lines • 12.2 kB
/** * time/windows — the turn's windows, read from the record (time design * § 5.6): the window the person settled in the time ask (`answered`), a * `model` reader's one reading waiting for confirmation * (`derived-from-reading`) and the clock's `control` window; the quotes still * waiting on the person; and what the served sentence names * (`readerWindowsOf`). * * Pattern: Walker over recorded rows — never over words, never the resolver. * Role: the record half of `bind.ts`, split out BY FILE for the synchronous * doors: the Tools mount's `inputMapper` (`agent/buildAgentChart.ts` · * `timeWindowsArg`) and the evidence gate's lineage read it before * any `import()` can run, and a bundler places a whole file on the * synchronous graph — so this file imports only the leaves and the * row readers, never `convert.ts` or `resolve.ts` (the optional-family * law, `test/trace-toolpack/browserGraph.test.ts`). `bind.ts` * re-exports every public name. * Emits: N/A (pure). * * @example * ```ts * readerWindowsOf(ledger); * // { now: '2026-10-09T15:40:00Z', windows: [{ source: 'answered', quote: 'yesterday', … }] } * ``` */ import { sameRange } from './range.js'; import { answersOf, clockOf, readingsOf, } from './rows.js'; /** * The one window a `model` reading offers, when it waits only for the * person's confirmation — a reading, filled as `derived-from-reading`. A * `rule` reading settles on nothing: it is a PROPOSAL the time ask offers, * and only the person's answer settles it (the owner's decision "Always * confirm", time design TQ29). */ function readingCandidate(row) { const choice = row.choice; if (row.reader.kind !== 'model' || choice?.by !== 'open') return undefined; if (choice.open.length !== 1 || choice.open[0] !== 'confirm') return undefined; return row.candidates?.[choice.remaining[0]]; } /** * The look-back the library OFFERED for a reading, when the person's answer is * exactly it (`last 2 hours` confirmed as offered) — the window then stays a * look-back from now. `undefined` for any other answer (an edit, a range). */ export function offeredLookback(row, range) { const offered = row?.candidates?.find((c) => c.window.kind === 'lookback' && sameRange(c.range, range))?.window; return offered?.kind === 'lookback' ? offered.duration : undefined; } /** * What a turn window hands the conversion — its range and, when it was said * (and answered) as a look-back, that look-back. THE one input every door * that writes a window into a tool's arguments builds (the fill, the time * ask's answer, the served line), so the value served to the model and the * value the tool is handed are one spelling (time follow-ups, packet "gaps": * the served line spelled `15:59:59Z` where the ask's fill handed `16:00:00Z`). */ export function windowToConvert(window) { return { range: window.range, ...(window.lookback !== undefined && { lookback: window.lookback }), }; } /** * Whether a mention the person has not answered still waits on THEM — the * one owner of "a time the person wrote that the library holds no settled * window for" (the lazy ask, the served line's pending half and the window * plan all ask it): a reading still `open` (readings to confirm, a zone to * name), and words the library holds NO reading of (`by: 'none'` — words the * reader could not read, or no candidate). The person said a time either way, * so a tool's default never stands in for it (time follow-ups, packet "gaps": * an unreadable phrase to a tool whose rule assumed its period ran that * default silently). A `model` reading's one window is not open: it fills as * a reading. A refused mention (its quote was not in the text) is not the * person's words. */ export function isOpenForPerson(row) { if (row.mentions === 0 || row.refused !== undefined || row.quote === undefined) return false; const by = row.choice?.by; if (by !== 'open' && by !== 'none') return false; return readingCandidate(row) === undefined; } /** * This turn's windows, read from the record: per mention, the window the * person settled in the time ask (`answered`, its latest `time-answer` row), * else a `model` reader's one window waiting for confirmation * (`derived-from-reading`); and the clock's `control` window. A `rule` * reading the person has not answered, and words the library could not read, * are counted and are OPEN ({@link isOpenForPerson}). */ export function turnWindowsOf(readings, clock, answers = []) { const windows = []; const open = []; let mentions = 0; const answered = new Map(answers.map((a) => [a.mention, a])); for (const row of readings) { if (row.mentions === 0 || row.refused !== undefined || row.quote === undefined) continue; mentions++; const answer = row.mention === undefined ? undefined : answered.get(row.mention); if (answer !== undefined) { const range = { from: answer.from, to: answer.to }; // The person picked a look-back the library offered: it stays a look-back from now. const lookback = offeredLookback(row, range); windows.push({ source: 'answered', mention: answer.mention, quote: row.quote, range, ...(lookback !== undefined && { lookback }), zone: answer.zone, answer: answer.how, }); continue; } const candidate = readingCandidate(row); if (candidate === undefined) { const choice = row.choice; const left = choice?.by === 'open' ? choice.remaining.flatMap((i) => { const c = row.candidates?.[i]; return c === undefined ? [] : [c.range]; }) : []; // Open with no reading yet (a zone to name, words not read) is still open: nothing to rule // out, and the person is asked — never left to the tool's own rule. if (left.length > 0 || isOpenForPerson(row)) open.push(left); continue; } windows.push({ source: 'derived-from-reading', ...(row.mention !== undefined && { mention: row.mention }), quote: row.quote, range: candidate.range, ...(candidate.window.kind === 'lookback' && { lookback: candidate.window.duration }), zone: candidate.zone, }); } if (clock?.window !== undefined) { mentions++; windows.push({ source: 'control', range: { from: clock.window.from, to: clock.window.to }, zone: clock.zone, }); } return { windows, mentions, ...(open.length > 0 && { open }) }; } /** Whether a call of `turn` ran on a window the model wrote (`call-window` `how: 'model'`). */ function ranOnSentWindow(ledger, turn) { return (ledger ?? []).some((row) => { const r = row; return (r !== null && typeof r === 'object' && r.kind === 'call-window' && r.turn === turn && r.how === 'model'); }); } /** * The person's windows the tools refused this turn (`ReaderWindows.refused`): * each `call-window` row `how: 'refused'` whose window was the person's — its * `person.mention`'s quote — or, with no range at all, the turn's one open * reading (`openQuote`), refused in every reading. Once per (quote, tool). */ function refusedWindowsOf(ledger, turn, readings, openQuote) { const quoteOf = new Map(readings.flatMap((r) => r.quote !== undefined && r.refused === undefined ? [[r.mention ?? 0, r.quote]] : [])); const out = []; for (const row of ledger ?? []) { const r = row; if (r === null || typeof r !== 'object' || r.kind !== 'call-window' || r.turn !== turn) continue; if (r.how !== 'refused' || r.refused === undefined || typeof r.toolName !== 'string') continue; const quote = r.person !== undefined ? r.person.mention === undefined ? undefined : quoteOf.get(r.person.mention) : r.asked === undefined && r.form === undefined ? openQuote : undefined; if (quote === undefined) continue; if (out.some((w) => w.quote === quote && w.toolName === r.toolName)) continue; out.push({ quote, toolName: r.toolName, refused: r.refused, ...(r.argument !== undefined && { argument: r.argument }), }); } return out; } /** * The quotes of this turn's mentions still waiting on the person * ({@link isOpenForPerson}), not answered in the time ask: a reading to * confirm, a zone to name, or words the library could not read (asked with * nothing pre-filled). A `model` reading's one window is served as a reading; * a refused mention is not the person's words. */ export function pendingQuotesOf(readings, answers = []) { const answered = new Set(answers.map((a) => a.mention)); const quotes = []; for (const row of readings) { if (!isOpenForPerson(row)) continue; if (row.mention !== undefined && answered.has(row.mention)) continue; quotes.push(row.quote); } return quotes; } /** The quotes among `pending` the library could not read at all (`ReaderWindows.pendingUnread`). */ function pendingUnreadOf(readings, pending) { return pending.filter((quote) => readings.some((r) => r.quote === quote && r.choice?.by === 'none')); } /** * The quotes among `pending` whose reading offers nothing to confirm yet — * no candidate, the choice open on the zone the words name * (`ReaderWindows.pendingZones`). */ function pendingZonesOf(readings, pending) { return pending.filter((quote) => readings.some((r) => r.quote === quote && (r.candidates ?? []).length === 0 && r.choice?.by === 'open' && r.choice.open.includes('zone'))); } /** * The latest turn's windows of the person's words, read off the ledger (its * last `clock` row and that turn's `time-reading` and `time-answer` rows) — * `undefined` when the turn has no clock, no settled mention, none pending * and no `control` window. A `rule` reading is named as a window only once * the person answered it in the time ask; before that its quote is * `pending`. The `control` window is no reading: it rides `control`, never * `windows`. */ export function readerWindowsOf(ledger) { const clock = clockOf(ledger); if (clock === undefined) return undefined; const turn = clock.turn; const readings = readingsOf(ledger, turn); const answers = answersOf(ledger, turn); const { windows } = turnWindowsOf(readings, undefined, answers); const pending = pendingQuotesOf(readings, answers); const control = clock.window === undefined ? undefined : { source: 'control', range: { from: clock.window.from, to: clock.window.to }, zone: clock.zone, }; if (windows.length === 0 && pending.length === 0 && control === undefined) return undefined; const zones = pendingZonesOf(readings, pending); const unread = pendingUnreadOf(readings, pending); const refused = refusedWindowsOf(ledger, turn, readings, pending.length === 1 ? pending[0] : undefined); return { now: clock.now, windows, ...(control !== undefined && { control }), ...(pending.length > 0 && { pending }), ...(zones.length > 0 && { pendingZones: zones }), ...(unread.length > 0 && { pendingUnread: unread }), ...(pending.length > 0 && ranOnSentWindow(ledger, turn) && { ranUnconfirmed: true }), ...(refused.length > 0 && { refused }), ...(clock.zoneSource === 'unknown' && { zoneUnknown: true }), }; } //# sourceMappingURL=windows.js.map