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