UNPKG

agentfootprint

Version:

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

274 lines • 11.9 kB
"use strict"; /** * time/clock — the run clock: a declared, recorded input, never a hidden read. * * Pattern: normalize-or-refuse at a boundary (`core/runInput.ts`'s shape): the * app's `.time({ zone })` and the run's `time: { now, zone, window }` * are read here into one {@link TimeClock}, or refused by name. * Role: L0 leaf (the time layer's one owner). Imports `instant.ts`, * `zone.ts`, `range.ts`, `reader.ts` (the reader's check) and * `resolve.ts` (the policy's) only. The Agent reads the two inputs * (`Agent.run`, `Agent.resume`); seed stamps the clock once per turn * (`rows.ts` · `ClockRow`); the ToolCalls resume door compares a * resume's `time` with the kept clock ({@link clockChange}). * Emits: N/A. * * ## The laws (time design § 4) * * - **The zone is per run; the builder's is a fallback.** The run's * `time.zone` wins, `.time({ zone })` is used when the run names none, and * with neither the zone is UNKNOWN (G15) — never the server's zone, never a * guess: the clock records `zoneSource: 'unknown'` and spells instants in * {@link UNKNOWN_ZONE_SPELLING} (UTC), every reading of the person's words * waits on their zone (it is asked first), and the zone they answer holds * for the turns after it (`zoneSource: 'answered'`). * - **`now` is the app's, else the turn's start** — admitted as * `nowSource: 'default'`, like any default nobody chose. * - **Frozen across a pause.** The clock is written once per turn. A resume * that passes a different `time` keeps the frozen clock and records what was * passed ({@link clockChange}); it is not refused, because an app that * passes the current time on every call is doing nothing wrong. * - **A window set in a UI is a run input** (`time.window`), recorded with * `source: 'control'` — the person's own typed value, never filed as an * answer to an ask. * * @example * ```ts * const run = readRunTime({ now: '2026-10-09T15:40:00Z', zone: 'America/Los_Angeles' }); * const draft = draftClock(run.value, readTimeOptions({}).value!); * // { now: '2026-10-09T15:40:00Z', zone: 'America/Los_Angeles', zoneSource: 'run' } * completeClock(draft as ClockDraft, Date.parse('2026-10-09T15:40:02Z')); * // { now: '2026-10-09T15:40:00Z', nowSource: 'app', zone: 'America/Los_Angeles', zoneSource: 'run' } * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.clockChange = exports.completeClock = exports.draftClock = exports.readTimeOptions = exports.readRunTime = exports.UNKNOWN_ZONE_SPELLING = void 0; const instant_js_1 = require("./instant.js"); const range_js_1 = require("./range.js"); const zone_js_1 = require("./zone.js"); const reader_js_1 = require("./reader.js"); const resolveRecord_js_1 = require("./resolveRecord.js"); const ask_js_1 = require("./ask.js"); /** * The zone a clock with no known zone spells its instants in (G15) — named * as unknown wherever it is shown, never read as the person's. */ exports.UNKNOWN_ZONE_SPELLING = 'UTC'; // ─── Reading the two inputs ────────────────────────────────────────────── const RUN_KEYS = ['now', 'zone', 'window']; const OPTION_KEYS = ['zone', 'reader', 'policy', 'messages']; function isPlainObject(value) { if (value === null || typeof value !== 'object' || Array.isArray(value)) return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function unknownKeys(value, allowed) { return Object.keys(value).filter((k) => !allowed.includes(k)); } /** * A zone as the app WROTE it, once `zone.ts` · `isZoneName` accepts it — never * `Intl`'s canonical form, which can be an older link the person did not name * (`Asia/Kolkata` resolves to `Asia/Calcutta` on some runtimes). The record * keeps spellings, as it keeps a passed `now`. */ function readZone(value) { if ((0, zone_js_1.isZoneName)(value)) return { value }; return { problem: `zone must be an IANA zone name such as 'America/Los_Angeles' or 'UTC' — ` + `an abbreviation (PST) or a bare offset (+05:30) names no zone`, }; } function readNow(value) { if (value instanceof Date) { const ms = value.getTime(); // A Date's year can leave 0000–9999, which no instant spelling carries. if (!Number.isFinite(ms) || (0, instant_js_1.instantOf)(new Date(ms).toISOString(), 'strict') === undefined) { return { problem: 'now is an invalid Date' }; } return { value: new Date(ms).toISOString() }; } if ((0, instant_js_1.instantOf)(value, 'strict') !== undefined) return { value: value }; return { problem: `now must be an ISO 8601 instant with a zone (e.g. '2026-10-09T15:40:00Z' or ` + `'2026-10-09T08:40:00-07:00') or a Date`, }; } /** * The run's `time` input, checked. `undefined` reads as "nothing passed". * Refused, by name: a non-object, an unknown key, a zone that is not an IANA * name, a `now` that is not an instant with a zone, a `window` that is not two * strict instants with `from` before `to`. */ function readRunTime(value) { if (value === undefined) return { value: undefined }; if (!isPlainObject(value)) return { problem: 'time must be an object — { now?, zone?, window? }' }; const extra = unknownKeys(value, RUN_KEYS); if (extra.length > 0) { return { problem: `time takes { now?, zone?, window? } only — unknown key ${extra .map((k) => `'${k}'`) .join(', ')}`, }; } let now; let zone; if (value.now !== undefined) { const read = readNow(value.now); if ('problem' in read) return read; now = read.value; } if (value.zone !== undefined) { const read = readZone(value.zone); if ('problem' in read) return { problem: `time.${read.problem}` }; zone = read.value; } if (value.window !== undefined && !(0, range_js_1.isTimeRange)(value.window)) { return { problem: 'time.window must be { from, to } — two ISO 8601 instants with a zone, from before to', }; } const window = value.window; return { value: { ...(now !== undefined && { now }), ...(zone !== undefined && { zone }), ...(window !== undefined && { window: { from: window.from, to: window.to } }), }, }; } exports.readRunTime = readRunTime; /** * The builder's `.time(options)`, checked. `undefined` reads as `{}` — the * layer armed with no fallback zone, so every run names its own. */ function readTimeOptions(value) { if (value === undefined) return { value: {} }; if (!isPlainObject(value)) { return { problem: 'options must be an object — { zone?, reader?, policy?, messages? }' }; } const extra = unknownKeys(value, OPTION_KEYS); if (extra.length > 0) { return { problem: `options take { zone?, reader?, policy?, messages? } — unknown key ${extra .map((k) => `'${k}'`) .join(', ')}`, }; } let zone; if (value.zone !== undefined) { const read = readZone(value.zone); if ('problem' in read) return read; zone = read.value; } let messages; if (value.messages !== undefined) { const read = (0, ask_js_1.readTimeAskMessages)(value.messages); if ('problem' in read) return read; messages = read.value; } if (value.reader === undefined) { // A policy with no reader has nothing to choose among — it would look // configured and do nothing. if (value.policy !== undefined) { return { problem: "policy chooses among a reader's readings — arm a reader with it" }; } return { value: { ...(zone !== undefined && { zone }), ...(messages !== undefined && { messages }) }, }; } const issue = (0, reader_js_1.readerIssue)(value.reader); if (issue !== undefined) return { problem: issue }; const policy = (0, resolveRecord_js_1.readPolicy)(value.policy); if (typeof policy === 'string') return { problem: policy }; return { value: { ...(zone !== undefined && { zone }), reader: value.reader, policy, ...(messages !== undefined && { messages }), }, }; } exports.readTimeOptions = readTimeOptions; // ─── The clock ─────────────────────────────────────────────────────────── /** * The turn's clock as far as the run's inputs decide it. With no zone from * the run or the builder the zone is UNKNOWN (G15): `zoneSource: 'unknown'`, * instants spelled in {@link UNKNOWN_ZONE_SPELLING} — never the server's * zone, never a guess (seed reads a zone the person answered in an earlier * turn, `stages/timeLayer.ts` · `stampClock`). */ function draftClock(run, options) { const zone = run?.zone ?? options.zone; return { ...(run?.now !== undefined && { now: run.now }), zone: zone ?? exports.UNKNOWN_ZONE_SPELLING, zoneSource: run?.zone !== undefined ? 'run' : zone !== undefined ? 'builder' : 'unknown', ...(run?.window !== undefined && { window: run.window }), }; } exports.draftClock = draftClock; /** * The clock, once the turn's start is known: `now` is the app's, else that * start. `answeredZone` — the zone the person answered in an earlier turn — * is read only when the draft's zone is unknown, and is then the turn's zone * (`zoneSource: 'answered'`). */ function completeClock(draft, turnStartMs, answeredZone) { // Fixed width (`toISOString`, milliseconds always written) — the spelling // `rowsBuild.ts` · `callRow` gives `dispatchedAt`, so the two wall-clock reads // compare as text too. const now = draft.now ?? new Date(turnStartMs).toISOString(); const answered = draft.zoneSource === 'unknown' ? answeredZone : undefined; return { now, nowSource: draft.now !== undefined ? 'app' : 'default', zone: answered ?? draft.zone, zoneSource: answered !== undefined ? 'answered' : draft.zoneSource, }; } exports.completeClock = completeClock; /** * The difference between a resume's `time` and the turn's frozen clock, or * `undefined` when every value passed equals the kept one (a key not passed * is no difference). `now` compares as text: a resume passing the same * instant in another spelling is recorded, because the record keeps spellings. * * @example * ```ts * clockChange({ now: '2026-10-09T16:10:00Z' }, { now: '2026-10-09T15:40:00Z', zone: 'UTC' }); * // { passed: { now: '2026-10-09T16:10:00Z' }, kept: { now: '2026-10-09T15:40:00Z', zone: 'UTC' } } * ``` */ function clockChange(passed, kept) { const nowDiffers = passed.now !== undefined && passed.now !== kept.now; const zoneDiffers = passed.zone !== undefined && passed.zone !== kept.zone; const windowDiffers = passed.window !== undefined && (kept.window === undefined || passed.window.from !== kept.window.from || passed.window.to !== kept.window.to); if (!nowDiffers && !zoneDiffers && !windowDiffers) return undefined; return { passed, kept: { now: kept.now, zone: kept.zone, ...(kept.window !== undefined && { window: kept.window }), }, }; } exports.clockChange = clockChange; //# sourceMappingURL=clock.js.map