UNPKG

agentfootprint

Version:

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

321 lines (320 loc) • 16.7 kB
/** * tools/declare — what a tool author declares about its arguments, and the * ONE rule set that judges the declaration. * * Pattern: Map. A declaration is plain data on the `Tool` (`askOrAssume`, * `period`), judged by one assert at three doors: definition * (`tools/tools.ts` · `defineTool`), dispatch (`rulesOf`, because a * Tool built by hand or served by a ToolProvider never passes through * `defineTool`) and MCP ingest (`mcp/toolExtras.ts` · * `readToolExtras`). A malformed declaration is refused and never * repaired. * Role: tools/ layer leaf of the inputs layer (honesty layer 2). Imports * nothing from `findings/` — the folder's one-way law. * Emits: N/A. * * ## The two forms of a rule * * - `{ assume: value }` — a value the call leaves out is FILLED BY THE * LIBRARY (never by the tool) and recorded as `default`; the answer's * standing then says the value was assumed. * - `{ ask: question, choices? }` — a value the call leaves out is asked of * the person, ONCE per batch, before anything in the batch runs, through * the typed ask (`tools/inputRequest.ts`); the answer is filled and * recorded as `answered` (`arguments/ask.ts`). A PRESENT value runs, * recorded as the model's own — unless declared sources are armed * (`.inputsLayer({ argumentSources: true })`, or `.findings({ argumentSources: * true })` beside the ledger): then the model says where it * came from, the library checks it (`arguments/checks.ts` · `checkSource`), and a * value the check does not trace is asked about too. A choice may carry * phrases the author vouches for (`{ value, said }`), matched only inside * a quote the model declared. * * ## The period — `ToolPeriod` * * Which argument bounds the period the answer covers, and how its values are * SPELLED: `lookback` (`30m`, `24h`, `7d`, `2w`), `signed-lookback` (`-24h`), * `iso-range` (two ISO 8601 instants joined by `..`) or `wall-range` (two wall * times joined by `..`, with the zone in `zoneArgument`). Declared formats, * never phrases. A period on an argument with no rule is refused (adopted * Q13): a period argument is exactly what a rule is for. * * Since the time layer's step T5a the single-argument form is SUGAR over * `forms` (`time/convert.ts` · `sugarForms`): a tool declares every shape * its period takes — two arguments (`bounds`), one joined string, an object, a * `day`, a look-back with its `units` — and the facts about its source * (`direction`, `retention`, `maxRange`, `granularity`, `filtersToAsked`). * Under `.time()` the library fills a period the model left out from the * turn's one window, binds a sent one to the person's window, and hands the * tool `ctx.time` (`time/bind.ts`). Without `.time()` a period is judged * and read exactly as before. */ import { type InputValue } from './inputRequest.js'; import { PERIOD_SPELLINGS, type PeriodDirection, type PeriodFacts, type PeriodForm, type PeriodSpelling } from '../time/periodForm.js'; import { type DurationText } from '../time/duration.js'; /** * One allowed answer to an `ask` rule: a bare value, or a value with phrases * the author vouches for as meaning it (`{ value: '7d', said: ['last week'] }`). * The phrases are matched as whole tokens, only inside a quote the model * declared — never scanned for in the person's words. */ export type AskChoice = InputValue | { readonly value: InputValue; readonly said?: readonly string[]; }; /** * One argument's rule — either ask the person, or assume a declared default. * * `assume`: a value the call leaves out is filled by the library and recorded * as `default`. `ask`: a value the call leaves out is asked of the person once * per batch, before anything in the batch runs, and recorded as `answered`. */ export type ArgumentRule = { readonly ask: string; readonly choices?: readonly AskChoice[]; } | { readonly assume: InputValue; }; /** Per argument name, its rule. Arguments with no rule run free. */ export type AskOrAssume = Readonly<Record<string, ArgumentRule>>; export type { PeriodSpelling, PeriodForm, PeriodFacts } from '../time/periodForm.js'; export { PERIOD_SPELLINGS }; /** * Which arguments set the period a tool's answer covers, how their values are * spelled, and the facts about the source. The one period shape's TOOL half; * the result half (`DeclaredPeriod`: what a read queried and what the store * holds) belongs to the result doors. * * Two ways to name the shape, never both: today's single argument * (`argument` + `spelling`, or `accepts` for several spellings, and * `zoneArgument` beside a `wall-range`), which is sugar over the general * `forms` — every shape the tool accepts, in preference order. * * @example * ```ts * period: { argument: 'window', spelling: 'lookback' } // '2h', '24h', '7d' * period: { * forms: [{ kind: 'bounds', * from: { argument: 'start_time', as: 'epoch-ms' }, * to: { argument: 'end_time', as: 'epoch-ms', edge: 'exclusive' } }], * direction: 'past', retention: '30d', * } * ``` */ export interface ToolPeriod { readonly argument?: string; readonly spelling?: PeriodSpelling; /** Every single-argument spelling the argument takes, in preference order. */ readonly accepts?: readonly PeriodSpelling[]; /** The argument that carries the zone of a `wall-range`. */ readonly zoneArgument?: string; readonly forms?: readonly PeriodForm[]; /** A `wall` / `date` / `day` form with no zone argument reads in the app's `.time({ zone })` — explicit, never implied. */ readonly wallZone?: 'app'; /** Which side of now the source can hold; absent: not checked. */ readonly direction?: PeriodDirection; /** The oldest data the source keeps (`30d`). */ readonly retention?: DurationText; /** The widest window the tool accepts at once (`24h`). */ readonly maxRange?: DurationText; /** The source's smallest step (`1m`). */ readonly granularity?: DurationText; /** The tool reads `ctx.time.asked` and drops rows outside it. */ readonly filtersToAsked?: boolean; } /** The JSON Schema types a ruled argument may have — flat primitives only (v1). */ export declare const RULED_TYPES: readonly string[]; /** An `ask` question's bound — the typed ask's own (`tools/inputRequest.ts`). */ export declare const ASK_QUESTION_CHARS = 4096; /** Choices per `ask` rule. */ export declare const MAX_CHOICES = 100; /** One `said` phrase's length. */ export declare const SAID_PHRASE_CHARS = 120; /** `said` phrases per choice. */ export declare const MAX_SAID_PHRASES = 16; /** Arguments that may carry `ask` on one tool — the typed ask's field limit. */ export declare const MAX_ASK_ARGUMENTS = 32; /** * The ONE owner of "this argument is missing from this call": no own key, or * a value that is `undefined`, `null`, or a string that is empty after * trimming. Deterministic; never a reading of what the value means. * * @example * ```ts * isMissing({ service: 'checkout' }, 'window'); // true — no key * isMissing({ window: ' ' }, 'window'); // true — blank string * isMissing({ window: '2h' }, 'window'); // false * ``` */ export declare function isMissing(args: Readonly<Record<string, unknown>>, argument: string): boolean; /** * Whether two argument values are the same value: same JavaScript type, and * the same token sequence under the evidence module's normaliser * (`lib/normalize.ts` · `tokenize`, `canonicalForm` — one owner of "the * same value", so `41,200` ≡ `41200` here exactly as at the evidence gate). A * value with no token after normalisation equals nothing, not even itself: it * cannot be checked, and "cannot be checked" is never "equal". */ export declare function sameArgumentValue(a: unknown, b: unknown): boolean; /** * Whether `value` is spelled the way `spelling` declares. `lookback` is a * positive integer and a unit (`m`, `h`, `d`, `w`); `signed-lookback` the * same with a leading minus; `iso-range` two ISO 8601 instants WITH a zone, * joined by `..`, each a day that exists (`2026-02-30` and hour `24` are * refused — `Date.parse` used to roll them forward). A format check, never a * conversion: nothing is ever turned into a duration, and no instant is * compared with "now". */ export declare function parsesUnderSpelling(value: unknown, spelling: PeriodSpelling): boolean; /** * Convert a period value between the two spellings the library converts — * `lookback` ↔ `signed-lookback`, by adding or removing the leading minus. * Nothing is ever turned into a duration; an `iso-range` is never converted. * `undefined` when the value does not parse under `from`. The one owner: the * batch ask's shared period field (`arguments/ask.ts` · `periodsShareField`) and the * declared-sources check's earlier answer in another spelling (`arguments/checks.ts` · * `checkSource`, `matched: 'spelling'`) both convert through it. */ export declare function convertSpelling(value: InputValue, from: PeriodSpelling, to: PeriodSpelling): InputValue | undefined; declare class DeclarationRefused extends Error { } /** Forms per period — a tool spells its period in a handful of shapes, never dozens. */ export declare const MAX_PERIOD_FORMS = 8; /** * Judge a tool's `askOrAssume` and `period`, at definition — and again at * dispatch (`rulesOf`) and at MCP ingest, by this same function. Throws an * `Error` naming the tool and the argument; returns nothing on a declaration * this version can apply. Both absent → a no-op. * * Refused: a rule on an argument the schema does not offer, whose type is not * exactly one of `string`, `number`, `integer`, `boolean`, or that is a `wants` * argument; a rule with both forms, neither, or an unknown key; a blank or * over-long question; empty, over-long or repeating choices; a choice or an * `assume` value the property's OWN schema rejects (never the root `required`); * a malformed `said` phrase; more than 32 `ask` arguments; a period on an * argument with no rule, an unknown spelling, or a declared value not spelled * that way. * * @example * ```ts * assertAskOrAssume('search_logs', { window: { assume: '2h' } }, undefined, schema); // ok * assertAskOrAssume('search_logs', { window: { ask: 'Which period?', choices: ['1h', '24h'] } }, * undefined, schema); // ok * assertAskOrAssume('search_logs', { window: { assume: '9h' } }, undefined, schema); // throws * ``` */ export declare function assertAskOrAssume(toolName: string, askOrAssume: unknown, period: unknown, inputSchema: Readonly<Record<string, unknown>> | undefined, wants?: Readonly<Record<string, string>>): void; /** One declared choice's phrases — words the author vouches for as meaning that value. */ export interface ChoicePhrases { readonly value: InputValue; readonly said: readonly string[]; } /** What an `ask` rule declares, as the layer reads it. */ export interface RuledAsk { /** The author's question — the ask field's `description`. */ readonly question: string; /** The author's choices' values, in declared order — the field's `enum`. Absent: a free field. */ readonly choices?: readonly InputValue[]; /** * The choices that declare `said` phrases, in declared order — matched as * whole tokens only inside a quote the model declared (`arguments/checks.ts` · * `checkSource`, `matched: 'phrase'`), never scanned for in the person's * words. Absent when no choice declares any. */ readonly phrases?: readonly ChoicePhrases[]; } /** One ruled argument of a tool, as the layer reads it. */ export interface RuledArgument { readonly argument: string; readonly rule: 'ask' | 'assume'; /** The declared default — present on an `assume` rule. */ readonly assume?: InputValue; /** The question and the choices — present on an `ask` rule. */ readonly ask?: RuledAsk; /** The property's JSON Schema type — one of `RULED_TYPES`, judged at definition. */ readonly type: 'string' | 'number' | 'integer' | 'boolean'; /** Set on the argument `Tool.period` names. */ readonly period?: true; } /** A tool's rules, read and judged. */ export interface ToolRules { readonly ruled: readonly RuledArgument[]; readonly period?: ToolPeriod; } /** * Every form a tool's period takes, the sugar read (`time/convert.ts` · * `sugarForms`) — empty when it declares none, or names an argument and no * spelling. */ export declare function periodFormsOf(period: ToolPeriod | undefined): readonly PeriodForm[]; /** * The form a call's window is in, read off what the call GIVES — the first form given whole * (every argument but the zone), else the first form given in part; `undefined` when every * form is left out. A period's forms are alternatives, so this is the one reading of "which * form did the call take", asked by the inputs layer (`arguments/resolve.ts` · `windowUntakenOf`) and * inner dispatch (`toolDispatch.ts` · `refuseUnaccountedRuledArguments`). */ export declare function sentFormOf(forms: readonly PeriodForm[], args: Readonly<Record<string, unknown>>): number | undefined; /** * The arguments of every form but `forms[taken]` that it does not also name — an * alternative the call did not take. Empty for a single form: one form is one window. */ export declare function untakenBesides(forms: readonly PeriodForm[], taken: number): ReadonlySet<string>; /** * The form a call that leaves EVERY form out owes, by the rules: a form this turn's kept * answers fill whole (`answered`), else the first form the rules ASSUME whole — its default IS * the window — else the first form, asked by its own asks. Never every form at once: that * would ask the person for two windows, which the tool refuses. */ export declare function owedFormOf(forms: readonly PeriodForm[], rules: ToolRules, answered: (argument: string) => boolean): number; /** * Every argument a period names — the single argument, or each form's bound, * object and zone arguments. What a row's `period: true` marks. */ export declare function periodArgumentsOf(period: ToolPeriod | undefined): ReadonlySet<string>; /** * The ONE argument a period is known by — the single `argument`, else the * first form's first bound. The results layer's join key (a `period` row names * it, and joins the call's `argument` row by it). */ export declare function periodArgumentOf(period: ToolPeriod | undefined): string | undefined; /** The facts a period declares about its source, only those it declares. */ export declare function periodFactsOf(period: ToolPeriod | undefined): PeriodFacts; /** Why a tool's rules could not be read — the assert's own sentence. */ export interface RulesRefused { readonly refused: string; } /** The shape `rulesOf` reads — any object carrying the three declarations. */ export interface RuledToolLike { readonly schema: { readonly name: string; readonly inputSchema?: Readonly<Record<string, unknown>>; }; readonly askOrAssume?: unknown; readonly period?: unknown; readonly wants?: Readonly<Record<string, string>>; } /** * A tool's rules as the inputs layer applies them, or why they cannot be read, * or `undefined` for a tool that declares none. Runs the SAME assert * `defineTool` runs, because a Tool built by hand or delivered by a * ToolProvider never passed through `defineTool` — and a rule that fails here * refuses the call; it is never repaired. Cached per tool object. */ export declare function rulesOf(tool: RuledToolLike | undefined): ToolRules | RulesRefused | undefined; /** True when `rulesOf` could not read the rules. */ export declare function isRefused(value: ToolRules | RulesRefused | undefined): value is RulesRefused; /** * Whether a tool's argument rules can be read and rule at least one argument * — the one predicate for "this tool is RULED" wherever the declared sources' * `_findings.from` is planted (`core/slots/buildToolsSlot.ts`, `stages/seed.ts`): * only such a tool's calls are checked and filed. */ export declare function carriesRules(tool: RuledToolLike | undefined): boolean; /** * Forget every definition-time warning issued so far. * @internal test seam — the ledger is process-wide and warn-once. */ export declare function _resetDeclareWarnings(): void; /** @internal — `DeclarationRefused` is the assert's own error class, for tests. */ export { DeclarationRefused };