UNPKG

agentfootprint

Version:

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

212 lines • 9.84 kB
/** * 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' } * ``` */ import { type InstantText } from './instant.js'; import { type TimeRange } from './range.js'; import { type ZoneName } from './zone.js'; import { type TimeReader } from './reader.js'; import { type TimePolicy } from './resolveRecord.js'; import { type TimeAskMessages } from './ask.js'; /** The run's clock, stamped once per turn (time design § 4). */ export interface TimeClock { /** The anchor for this turn — the app's `now`, else the turn's start. */ readonly now: InstantText; /** The app passed `now`, or the library took the turn's start. */ readonly nowSource: 'app' | 'default'; /** * The person's zone for this run — an IANA name. Under `zoneSource: * 'unknown'` it is `'UTC'`: the zone instants are SPELLED in, never the * person's. */ readonly zone: ZoneName; /** * The run's `time.zone`, else the `.time({ zone })` fallback; with neither, * the zone the person answered in an earlier turn of this conversation * (`'answered'`), else `'unknown'` (G15). */ readonly zoneSource: 'run' | 'builder' | 'answered' | 'unknown'; } /** * 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. */ export declare const UNKNOWN_ZONE_SPELLING: string; /** * The run's time input — `agent.run({ message, time })`, or the options bag of * `run`, `followUp`, `resume` and `resumeOnError`. Every key is optional; a * run with no zone here needs the builder's fallback. * * @example * ```ts * await agent.run({ message: 'errors since 8?', time: { now: sentAt, zone: 'America/Los_Angeles' } }); * ``` */ export interface RunTime { /** * The turn's anchor — the MESSAGE's time, so a replay or a late resume * reads "yesterday" the same way. An ISO 8601 instant with a zone (upper * case `T` and `Z`, no leap second), or a `Date`. Omitted: the turn's start, * recorded `nowSource: 'default'`. */ readonly now?: string | Date; /** The person's IANA zone for this run (`'America/Los_Angeles'`). Never an abbreviation. */ readonly zone?: string; /** * A window set in a UI — a brushed chart range, a range picker — as a * half-open `{ from, to }` of instants. Recorded with `source: 'control'`. */ readonly window?: TimeRange; } /** The builder's `.time(options)` — the fallback zone, the reader and its policy (time design § 11). */ export interface TimeOptions { /** * The fallback zone for a run that names none. Omitted with no run zone * either: the zone is unknown — never the server's — and the person is * asked it before any time they wrote is read (G15). */ readonly zone?: string; /** * The strategy that reads the person's words into time parts (`TimeReader`). * No default: without one, the person's words are not read — no * `time-reading` row is filed. */ readonly reader?: TimeReader; /** * How the library picks among a reading's candidates (needs `reader`). * `dateOrder` (default `'ask'`): a numeric date's orders become choices, * or the one order your people write (`'MDY'`, `'DMY'`, `'YMD'`), recorded * as assumed. `year` (default `'ask'`): a date said without a year is * asked, or `'current'` — the clock's year, recorded as assumed. */ readonly policy?: { readonly dateOrder?: TimePolicy['dateOrder']; readonly year?: TimePolicy['year']; }; /** * Your words for the time ask (`defaultTimeAskMessages` keys → sentence): * the reason a refused time answer is re-asked with, the ask's questions, * the label on a reading to confirm. A key you leave out keeps the default. */ readonly messages?: Partial<TimeAskMessages>; } /** {@link RunTime} as read: every value checked and kept as written, a `Date` spelled in UTC. */ export interface ReadRunTime { readonly now?: InstantText; readonly zone?: ZoneName; readonly window?: TimeRange; } /** {@link TimeOptions} as read — the policy's defaults filled in when a reader is armed. */ export interface ReadTimeOptions { readonly zone?: ZoneName; readonly reader?: TimeReader; readonly policy?: TimePolicy; /** The app's catalog overrides — every key one the catalog has. */ readonly messages?: Partial<TimeAskMessages>; } /** What a turn's clock is before seed knows the turn's start. */ export interface ClockDraft { readonly now?: InstantText; readonly zone: ZoneName; /** `'unknown'`: neither the run nor the builder named a zone — `zone` is the UTC spelling. */ readonly zoneSource: 'run' | 'builder' | 'unknown'; readonly window?: TimeRange; } /** A read answer: the value, or a problem named in words. */ export type Read<T> = { readonly value: T; } | { readonly problem: string; }; /** * 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`. */ export declare function readRunTime(value: unknown): Read<ReadRunTime | undefined>; /** * The builder's `.time(options)`, checked. `undefined` reads as `{}` — the * layer armed with no fallback zone, so every run names its own. */ export declare function readTimeOptions(value: unknown): Read<ReadTimeOptions>; /** * 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`). */ export declare function draftClock(run: ReadRunTime | undefined, options: ReadTimeOptions): ClockDraft; /** * 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'`). */ export declare function completeClock(draft: ClockDraft, turnStartMs: number, answeredZone?: ZoneName): TimeClock; /** What a resume passed that differs from the kept clock, and the kept values it would have replaced. */ export interface ClockChange { readonly passed: ReadRunTime; readonly kept: { readonly now: InstantText; readonly zone: ZoneName; readonly window?: TimeRange; }; } /** * 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' } } * ``` */ export declare function clockChange(passed: ReadRunTime, kept: { readonly now: InstantText; readonly zone: ZoneName; readonly window?: TimeRange; }): ClockChange | undefined; //# sourceMappingURL=clock.d.ts.map