UNPKG

agentfootprint

Version:

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

170 lines • 7.88 kB
/** * time/reader — the port a strategy reads a person's words through, and * the checks every reading passes before anything resolves it. * * Pattern: port + adapter (Duckling's value/resolution split). A strategy * TOKENIZES: it returns the parts it sees and the verbatim quote, * never an instant, never a date order, never a zone. Everything * that turns parts into instants is `resolve.ts`'s, the same way for * every strategy (time design § 5.1). * Role: L0 leaf (the time layer). Imports nothing. The builder's * `.time({ reader })` checks a reader through {@link readerIssue}; * seed checks what it returned through {@link checkReading}. * Emits: N/A. * * ## What is checked — shape, never meaning * * | Check | A failure | * |-------|-----------| * | the reader: `id`, `version`, `locale` non-empty strings, `kind` `'rule'` or `'model'`, `read` a function | refused at build | * | the reading: `{ mentions: [] }`, at most {@link MAX_MENTIONS} | the reader broke its port — the run fails, naming it | * | each mention's `quote`: a non-empty VERBATIM substring of the text read | the mention is refused (`quote-not-in-text`) and keeps no text | * | each mention's parts: the {@link TimeParts} shape, every field in range, no unknown key, at most {@link MAX_PARSES} | the mention is refused (`malformed`) | * * None of these checks what a word MEANS: a model that reads "yesterday" as * the wrong day passes every one of them. That is why NO reading is ever the * person's words — a `rule` reading as much as a `model` one (the owner's * decision "Always confirm", time design TQ29): a reading only PROPOSES a * window, offered through the time ask with its zone, and only what the * person picks or types in that form is theirs (`rowsBuild.ts` · * `timeReadingRows`). * * @example * ```ts * const fixture: TimeReader = { * id: 'fixture', version: '1.0.0', locale: 'en-US', kind: 'rule', * read: () => ({ mentions: [{ quote: '10/09/26', parses: [{ date: { kind: 'numeric', fields: [10, 9, 26] } }] }] }), * }; * readerIssue(fixture); // undefined — a well-formed reader * checkReading('errors on 10/09/26', fixture.read('', { locale: 'en-US' }), 'fixture'); * // [{ quote: '10/09/26', parses: [{ date: { kind: 'numeric', fields: [10, 9, 26] } }] }] * ``` */ /** What a reader is told besides the text: a hint for its tokenizer — no clock, no zone, no map. */ export interface TimeReadContext { readonly locale: string; } /** * A strategy that reads a person's words into zone-less PARTS (time design * § 5.1). Armed with `.time({ reader })`; there is no default. It runs once * per turn, at the seed, on the message a person wrote, and never again for * that message — a resume and a retry read the recorded reading. * * @example * ```ts * const reader: TimeReader = { * id: 'my-app/english', version: '1.2.0', locale: 'en-US', kind: 'rule', * read: (text) => ({ mentions: tokenize(text) }), * }; * Agent.create({ provider, model }).time({ zone: 'UTC', reader }).build(); * ``` */ export interface TimeReader { /** Recorded on every reading with the version, e.g. `'agentfootprint/english'`. */ readonly id: string; readonly version: string; /** The language it reads, e.g. `'en-US'`. */ readonly locale: string; /** * `'rule'`: deterministic over the text. `'model'`: an LLM or other learned * reader. Neither kind's reading is ever the person's words: a `rule` * reading is offered through the time ask to confirm; a `model` reading * fills as a reading (`derived-from-reading`) until the person confirms it. */ readonly kind: 'rule' | 'model'; read(text: string, context: TimeReadContext): TimeReading | Promise<TimeReading>; } /** What a reader returns: every time mention it found, in the order the text wrote them. */ export interface TimeReading { readonly mentions: readonly TimeMention[]; } /** One mention of a time in the text. */ export interface TimeMention { /** A VERBATIM substring of the text — the library checks it. */ readonly quote: string; /** Usually one; more only when the TOKENS split two ways. Empty when `problem` is set. */ readonly parses: readonly TimeParts[]; /** The reader saw a time here and could not read it. */ readonly problem?: 'unreadable'; } /** A date as the text wrote it. */ export type TimeDate = /** `'10/09/26'` → `fields: [10, 9, 26]` — the ORDER is not decided by the reader. Two or three fields. */ { readonly kind: 'numeric'; readonly fields: readonly number[]; readonly yearDigits?: 2 | 4; } /** ISO or a named month: the text fixes the order. */ | { readonly kind: 'fixed'; readonly year?: number; readonly month: number; readonly day: number; }; /** A wall-clock time as the text wrote it — no zone. */ export interface TimeWall { readonly h: number; readonly m?: number; readonly s?: number; readonly meridiem?: 'am' | 'pm'; /** * `'24h'`: the text's FORM is a 24-hour clock (an ISO instant's * `T08:00`), so the hour is read as written — never also as pm. A form * fact the reader sees and `resolve.ts` cannot; absent, an hour 1–12 with * no meridiem is read both ways. Never with `meridiem`. */ readonly clock?: '24h'; } /** A time said relative to the message's moment. */ export type TimeRelative = /** `'yesterday'` = `{ unit: 'day', offset: -1 }`. */ { readonly unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; readonly offset: number; } /** `'last 40 minutes'` = `{ unit: 'minute', count: 40 }` — a look-back. */ | { readonly unit: 'second' | 'minute' | 'hour' | 'day' | 'week'; readonly count: number; }; /** Zone-less parts, in the order the text wrote them. Every field present was said. */ export interface TimeParts { readonly date?: TimeDate; readonly wall?: TimeWall; /** As written: `'PST'`, `'-07:00'`, `'America/Los_Angeles'` — `resolve.ts` maps it. */ readonly zoneToken?: string; readonly relative?: TimeRelative; /** `'morning'` — a KEY into a table, never hours. */ readonly partOfDay?: string; /** `'the hour before that'` — resolved from the recorded previous window only. */ readonly anchor?: 'previous'; /** `'8 AM to 8:40 AM'`. */ readonly rangeOf?: readonly [TimeParts, TimeParts]; } /** The most mentions one reading may hold — a bound on the record, not a guess at language. */ export declare const MAX_MENTIONS = 16; /** The most parses one mention may hold. */ export declare const MAX_PARSES = 4; /** Why `value` is not a {@link TimeReader}, or `undefined` when it is one. */ export declare function readerIssue(value: unknown): string | undefined; /** Why one mention was refused. A refused mention keeps no text: its quote is not the person's words. */ export type MentionRefusal = 'quote-not-in-text' | 'malformed'; /** One mention after the checks. */ export type CheckedMention = { readonly quote: string; readonly parses: readonly TimeParts[]; readonly problem?: 'unreadable'; } | { readonly refused: MentionRefusal; }; /** Whether `value` is a well-formed {@link TimeParts} — `nested` refuses a range inside a range side. */ export declare function isTimeParts(value: unknown, nested?: boolean): value is TimeParts; /** * A reader's answer for `text`, checked: one {@link CheckedMention} per * mention, in order. Throws a `TypeError` naming the reader when the answer * is not `{ mentions: [] }` (at most {@link MAX_MENTIONS}) — the reader * broke its port, and a run is never read by a guess at what it meant. */ export declare function checkReading(text: string, value: unknown, readerId: string): CheckedMention[]; //# sourceMappingURL=reader.d.ts.map