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