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
TypeScript
/**
* 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;