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