UNPKG

agentfootprint

Version:

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

147 lines (146 loc) • 8.65 kB
/** * time/readers/english — the library's careful English reader (time * design § 5.3): a tokenizer over a small, closed set of phrases, and * "unreadable" for every other time phrase it recognises. * * Pattern: Strategy behind the `TimeReader` port (`../reader.ts`). It * TOKENIZES only: it returns the parts it sees and the verbatim * quote. It never resolves an instant, never applies a date order, * never maps a zone token (an abbreviation, a place) to a zone and * never refuses a future date — * all of that is `resolve.ts`'s, the policy's or the ask's. * Role: L0 leaf (the time layer). Imports only the port's types. Armed * by the app: `.time({ reader: englishTimeReader() })` — there is no * default reader. * Emits: N/A. * * ## What it reads (the v1 rows of § 5.3) * * | Phrase | Example | Parts | * |--------|---------|-------| * | an ISO date or instant | `2026-10-09`, `2026-10-09T08:00-07:00` | `date: fixed`, `wall` (marked `clock: '24h'` — never also pm), `zoneToken` | * | a numeric date | `10/09/26`, `10/9` | `date: numeric` — the ORDER is not decided here | * | a clock time | `8 AM`, `8:40`, `20:40`, `8:40 p.m.` | `wall` (a bare `8` is a time only as a range's first side: `8 to 9 AM`) | * | a range between two of the above | `8 AM to 8:40 AM`, `08:00–08:40`, `between 8 and 9 AM`, `today between 1 pm and 2 pm` | `rangeOf` (a day or zone said once for both sides is the whole mention's) | * | a zone after a time, a date or a day word | `America/Los_Angeles`, `UTC-07:00`, `London time`, `PST` | `zoneToken`, as written — `resolve.ts` maps it (a place the tz database names once; an abbreviation only through the app's map; else ASKED) | * | a named-month date | `11 September`, `Sept 29`, `September 29, 2026`, `the 29th of Sep` | `date: fixed` — no `year` unless written (the policy's or the person's, as for `10/9`) | * | a day word | today, yesterday, tomorrow | `relative: { unit: 'day', offset }` | * | a relative span from now | last 40 minutes, past 2 hours, last 24h, past 6h, the last hour, the past week | `relative: { unit, count }` — a look-back in minutes, hours, days or weeks, spelled out or compact (`m`, `h`, `d`, `w`) | * * ## What it says "unreadable" for — never a partial reading * * Parts of the day (`yesterday morning`, `tonight`, `noon` — a greeting, `Good morning`, is * no time and reads no mention), calendar spans * (`last week`, `this month`), week days and a month named alone (`Friday`, * `in September`), `N hours ago`, spans in words (`last two hours`), in a unit it * does not read (`last 30 seconds`, `last 30s`, `last 3 months`) or said with `previous` * (`previous 7 days` — often relative to another window), a look-ahead (`next 2 hours`), an * ordinal day (`the 9th`), `8 o'clock`, and any v1 phrase a modifier changes * (`since 8 AM`, `before yesterday`, `around 8:40`, `earlier today`, * `8 AM to now`, `past 8 PM`, `8 AM-ish`, `from 3 PM yesterday` with no `to`), a * bare first side whose far side says no meridiem (`8 to 9:30` — `9:30` alone * would drop the start) and a meridiem the number contradicts (`13:00 PM`). Such a phrase is ONE mention * with `problem: 'unreadable'`, quoting the whole phrase: reading `yesterday` * out of `yesterday morning` would silently widen what the person said. * * ## It only PROPOSES — the person confirms (the owner's decision "Always confirm") * * Nothing this reader reads is ever filed as the person's words. Seven review * rounds each found the next English spelling a word list, an allow-list or a * position rule missed (a zone named in words, a look-back tied to an event, * an open end, a half-read range), so the owner decided (2026-09-30, time * design TQ29): every reading of a chat message is a PROPOSAL, offered * through the time ask pre-filled and editable, with its window AND its zone * ("I read “last 2 hours” as … in America/Los_Angeles — is that right?"). * Only what the person picks or types in that form is theirs. The library * owns the law (`../rowsBuild.ts` · `timeReadingRows`), so this reader returns * parts and quotes only — no confirm flag, no leftover list, no allow-list. * A form may later skip the click only when a registered benchmark shows it * is always read right (TQ29's growth rule). * * A numeric date the tokens could split two ways stays ONE parse — `10/09/26` * is three numbers; which is the month is the policy's or the person's. * * @example * ```ts * const reader = englishTimeReader(); * reader.read('errors on 10/09/26 8 AM to 8:40 AM PST', { locale: 'en-US' }); * // { mentions: [{ quote: '10/09/26 8 AM to 8:40 AM PST', parses: [{ * // date: { kind: 'numeric', fields: [10, 9, 26], yearDigits: 2 }, zoneToken: 'PST', * // rangeOf: [{ wall: { h: 8, meridiem: 'am' } }, { wall: { h: 8, m: 40, meridiem: 'am' } }] }] }] } * reader.read('errors in the last 2 hours', { locale: 'en-US' }); * // { mentions: [{ quote: 'last 2 hours', parses: [{ relative: { unit: 'hour', count: 2 } }] }] } * reader.read('what failed yesterday morning?', { locale: 'en-US' }); * // { mentions: [{ quote: 'yesterday morning', parses: [], problem: 'unreadable' }] } * ``` */ import { type TimeReader } from '../reader.js'; /** The reader's recorded identity — on every `time-reading` row with its version. */ export declare const ENGLISH_TIME_READER_ID = "agentfootprint/english"; /** Raised whenever what the reader returns for some text changes. */ export declare const ENGLISH_TIME_READER_VERSION = "1.2.0"; /** A word right before a phrase that changes what it means: the phrase is not read. */ export declare const MODIFIER_BEFORE: RegExp; /** `from` with no `to`: a time after it is where a window STARTS (`from 3 PM yesterday`), not an hour. */ export declare const FROM_BEFORE: RegExp; /** * A bare number joined to a clock time as a range's first side the grammar * does not read (`8 to 9:30`, `from 8 to 9:30`, `between 8 and 9:30` — the * far side says no meridiem, so `8` is no time): the whole phrase is not * read. Reading `9:30` alone would drop the start the person said. */ export declare const BARE_SIDE_BEFORE: RegExp; /** * Whether some span of `covering` holds `span` whole — `covering.some((a) => * a.start <= span.start && span.end <= a.end)` — answered by binary search: * among the spans starting at or before `span.start`, the farthest end. Asking * `some` once per match cost matches × phrases, and a long message has many of * both. */ export declare function coverage(covering: readonly { readonly start: number; readonly end: number; }[]): (span: { readonly start: number; readonly end: number; }) => boolean; /** * `/\bbetween\s+$/i.test(text.slice(0, end))` — `between`, then blanks right * up to `end` — walked back from `end`. The regex read the whole text before * each phrase it was asked about, so a message of many short phrases cost its * length times their number. */ export declare function saysBetweenBefore(text: string, end: number): boolean; /** * Where the text before `end` that those three patterns can reach starts: back * over the blanks before `end`, then over `WORDS_BEFORE` words and the blanks * before each. Each pattern ends at `end` (`\s*$`), spans fewer words, and * takes its leading boundary from the character just before its first word — * all inside this window, which starts at a blank or at 0, so the window's * `^` adds no match. The leftmost match in the window is therefore the leftmost * match in all the text before `end`; matching that text instead re-read the * whole message once per group. */ export declare function windowBefore(text: string, end: number): number; /** Options for {@link englishTimeReader}. */ export interface EnglishTimeReaderOptions { /** * The language tag the time ask's labels are rendered in (`present.ts`), * e.g. `'en-GB'`. Default `'en-US'`. It never decides a date order — that is * the policy's (`dateOrder`) or the person's. */ readonly locale?: string; } /** * The library's careful English reader: a `kind: 'rule'` tokenizer over the * v1 phrases of the time design's § 5.3, "unreadable" for every other time * phrase it recognises. No dependency; deterministic over the text. * * @example * ```ts * Agent.create({ provider, model }) * .time({ zone: 'America/Los_Angeles', reader: englishTimeReader() }) * .build(); * ``` */ export declare function englishTimeReader(options?: EnglishTimeReaderOptions): TimeReader;