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
JavaScript
/**
* 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