UNPKG

agentfootprint

Version:

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

127 lines • 8.04 kB
/** * time/ask — the one time ask (time design § 6): what a time field's * answer must be, why one is refused, and the choices a reading offers. * * Pattern: one judge per boundary (the `range.ts` precedent). A typed ask's * `InputField.format` (`tools/inputRequest.ts`) names what the answer * is; {@link checkTimeAnswer} is the ONE judge of it — at definition * (a choice, a supplied value) and at the resume door (the person's * answer). A refusal is a CODE with its facts; the sentence is a * catalog entry (`src/locales/timeAsk.ts` · `defaultTimeAskMessages`) * the app may override (`.time({ messages })`), filled here * ({@link refusalReason}) — the library writes a reason only for a * check the app armed by declaring the field (TQ7). * Role: L0 leaf (the time layer). Imports `instant.ts`, `zone.ts`, * `range.ts`, `present.ts` and the row / candidate TYPES only. * Emits: N/A. * * ## What an answer must be (§ 6.1, § 6.2) * * | `format` | The answer | Refused as | * |----------|------------|------------| * | `instant` | an ISO 8601 date-time WITH its offset, strict profile (`2026-10-09T08:00-07:00`) | `not-an-instant`; `no-offset` (a date-time with no zone); `dst-gap` | * | `time-range` | an ISO 8601 interval `from/to` of two such instants, `from` before `to` | `not-a-range`; either end's refusal; `out-of-order` | * | `zone` | an IANA zone name (`America/Los_Angeles`) — never an abbreviation or a bare offset | `not-a-zone` | * | `time-range`, for a tool's period | the same, inside the tool's declared facts | `time-future`; `time-past`; `beyond-retention`; `over-max-range` | * * A `time-range` answer given for a tool's period is also judged against the * tool's declared FACTS (§ 6.2, step T5a — `convert.ts` · * `periodFactProblem`), when the caller hands them with the turn's clock: * `time-future` (a `past` source asked for a window after now), `time-past` * (a `future` source asked for one that ended), `beyond-retention` (the WHOLE * window older than the source keeps — a partial overlap is taken) and * `over-max-range` (wider than the source reads at once). * * `dst-gap` is asked only when the ask knows the person's zone (the run * clock's, under `.time()`): a wall time that zone's clocks skip, written with * one of the two offsets around the change (`2026-03-08T02:30-08:00` in Los * Angeles) names an instant nobody's clock showed. An instant written in some * other offset is that instant, and is taken. * * ## The choices a reading offers (§ 6.1, § 6.3) * * `readingAsk.ts` · `timeAskOf` (split out: it renders labels through * `present.ts`, which only an armed agent loads) turns a `time-reading` row whose choice is `open` into one * field: a `format: 'zone'` field when the person named a zone the layer * cannot read (`PST`), else a `format: 'time-range'` field whose `enum` is * the candidates left (each an ISO interval) and whose `labels` render each in * the reader's locale, the zone named, with the end the person said. A * `kind: 'model'` reader's window is offered as the LIBRARY'S reading to * confirm ("I read “yesterday” as … — is that right?"), never as the person's * words (§ 5.5). So is EVERY `rule` reading (the owner's decision "Always * confirm", time design TQ29 — `rowsBuild.ts` · `timeReadingRows`): the choice is * the reading pre-filled WITH ITS ZONE ("I read “yesterday” as Thu, Oct 8, * 2026, PDT in America/Los_Angeles — is that right?"), so a person who meant * another zone's day corrects it in one answer. Free entry stays open: a * field is `strict` only when the app says so. * * @example * ```ts * checkTimeAnswer('time-range', '2026-10-09T08:40-07:00/2026-10-09T08:00-07:00'); * // { problem: 'out-of-order', facts: { from: '2026-10-09T08:40-07:00', to: '2026-10-09T08:00-07:00' } } * checkTimeAnswer('instant', '2026-10-09T08:00'); // { problem: 'no-offset', facts: { value: '2026-10-09T08:00' } } * checkTimeAnswer('instant', '2026-03-08T02:30-08:00', 'America/Los_Angeles'); // { problem: 'dst-gap', … } * checkTimeAnswer('zone', 'PST'); // { problem: 'not-a-zone', facts: { value: 'PST' } } * refusalReason([checkTimeAnswer('zone', 'PST')!], defaultTimeAskMessages); * // '“PST” is not a time zone name. Name one such as America/Los_Angeles.' * ``` */ import { type PeriodFacts } from './periodForm.js'; import { type InstantText } from './instant.js'; import { type ZoneName } from './zone.js'; /** What a time field's answer is (time design § 6.1). The value stays a string on the wire. */ export type TimeFormat = 'instant' | 'time-range' | 'zone'; export declare const TIME_FORMATS: readonly TimeFormat[]; /** Whether `value` names a {@link TimeFormat}. */ export declare function isTimeFormat(value: unknown): value is TimeFormat; /** Why a time answer was refused — a code; the sentence is the catalog's. */ export type TimeAnswerProblem = 'not-an-instant' | 'no-offset' | 'not-a-range' | 'out-of-order' | 'dst-gap' | 'not-a-zone' | 'time-future' | 'time-past' | 'beyond-retention' | 'over-max-range'; /** One refused answer: the code and the facts its sentence names (the value as given, capped). */ export interface TimeAnswerRefusal { readonly problem: TimeAnswerProblem; readonly facts: Readonly<Record<string, string>>; } export declare const quoted: (value: string) => string; /** A tool's declared facts and the turn's clock — what a `time-range` answer for that tool's period is judged against. */ export interface ToolTimeFacts { readonly facts: PeriodFacts; readonly now: InstantText; } /** * Why `value` is not a well-formed answer of `format`, or `undefined` when it * is one. `zone` — the person's zone, when the ask knows it (the run clock's) * — arms the DST-gap check; without it the answer is judged for shape, order * and offset only. `tool` — a tool's declared facts and the clock — judges a * well-formed `time-range` against them too. */ export declare function checkTimeAnswer(format: TimeFormat, value: string, zone?: ZoneName, tool?: ToolTimeFacts): TimeAnswerRefusal | undefined; /** * Every sentence the time ask can put before a person — the keys of the * catalog (`src/locales/timeAsk.ts`). `answer.*` is a refusal's reason, one * per {@link TimeAnswerProblem}; `ask.*` a question; `choice.confirm` the * label on a reading to confirm — every reading the armed reader made. * Placeholders are `{{name}}`; the label also takes `zone`. */ export declare const TIME_ASK_MESSAGE_KEYS: readonly ["answer.not-an-instant", "answer.no-offset", "answer.not-a-range", "answer.out-of-order", "answer.dst-gap", "answer.not-a-zone", "answer.time-future", "answer.time-past", "answer.beyond-retention", "answer.over-max-range", "ask.which", "ask.confirm", "ask.zone", "ask.zone-unknown", "choice.confirm"]; export type TimeAskMessageKey = (typeof TIME_ASK_MESSAGE_KEYS)[number]; /** A whole catalog: one sentence per key. */ export type TimeAskMessages = Readonly<Record<TimeAskMessageKey, string>>; /** * The app's overrides (`.time({ messages })`) read — every key one the * catalog has, every value a non-blank sentence of at most 4096 characters — * or a problem in words. */ export declare function readTimeAskMessages(value: unknown): { readonly value: Partial<TimeAskMessages>; } | { readonly problem: string; }; /** A catalog sentence with its `{{name}}` placeholders filled; a placeholder with no fact is left as written. */ export declare function fillMessage(template: string, facts: Readonly<Record<string, string>>): string; /** * The reason a re-ask carries (`InputRefusal.reason`): one catalog sentence * per refused field, in field order, joined by a space — at most 4096 * characters, the refusal's own bound. */ export declare function refusalReason(refusals: readonly TimeAnswerRefusal[], messages: TimeAskMessages): string; //# sourceMappingURL=ask.d.ts.map