agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
133 lines (132 loc) • 7.75 kB
TypeScript
/**
* toolArgsValidation — validate LLM-produced tool args against the tool's
* declared `inputSchema` BEFORE dispatch (backlog #9).
*
* Pattern: pure function module — no events, and no state beyond one compiled-
* regex cache (which doubles as the warn-once ledger for `pattern`s
* that do not compile); each dispatch boundary owns when to call it
* and what to do with the verdict.
* Role: The model writes tool args as free-form JSON; nothing guaranteed
* they match the schema the tool advertised. Dispatching garbage
* surfaced as deep tool stack traces (or worse, silent misbehavior).
* Validating at the boundary turns a malformed call into a
* MODEL-VISIBLE structured tool result, so the LLM corrects its
* args and retries on the next ReAct iteration.
*
* ── Honest-subset contract ────────────────────────────────────────────────
* This is NOT a full JSON Schema implementation. It enforces the core that
* tool schemas in the wild actually use, and IGNORES everything else
* (permissive on unknown keywords — a schema using `oneOf`/`$ref` still
* validates the supported core, never false-rejects on the rest):
*
* ENFORCED: `type` (object/array/string/number/integer/boolean/null,
* union arrays), `required`, `properties` (recursive),
* `items` (single-schema, recursive), `enum` (primitives),
* `additionalProperties: false` when declared names are knowable,
* and the STRING SHAPE keywords `pattern` / `minLength` /
* `maxLength`. Object membership means OWN properties, not
* prototype lookup. An omitted `properties` map declares no names.
* IGNORED: format, numeric min/max, oneOf/anyOf/allOf/not, $ref,
* const, dependencies, patternProperties, …
* A malformed `properties` map or nonempty/malformed `patternProperties`
* defers extra-key rejection: unknown name coverage cannot prove a key extra.
* Independent required and declared-property checks still apply.
*
* ── Why string SHAPE joined the subset ────────────────────────────────────
* A tool result ended with an offer ("I can also map these ids to volume
* names"); the person answered "yes please"; the model bound that sentence as
* the IDENTIFIER argument and dispatched. The tool's schema declared the
* identifier's shape and this boundary did not read it, so a call that could
* never succeed cost a round trip — and the consumer hand-rolled an
* affirmative blocklist to do what the declaration already said. A declared
* shape a boundary ignores is worse than no declaration: the author believes
* it is enforced.
*
* `pattern` is JSON Schema's own semantics — UNANCHORED, ECMA-262, applied to
* strings only. A regex the schema author got wrong must never take dispatch
* with it: it is compiled once, and a compile failure degrades to no-pattern
* plus one developer warning.
*
* ── Security: what an issue may echo ──────────────────────────────────────
* `type` / `enum` / `required` / `additionalProperties` issues name the PATH,
* the EXPECTED shape, and the TYPE of what arrived — never the supplied value.
* Enum expectations echo SCHEMA values only (already LLM-visible in the tools
* block).
*
* String-SHAPE issues are the one exception, and it is a narrow one: the
* complaint IS the value's shape, so `expected string, got string` teaches
* nothing and the correction cannot converge. They carry `value` — a capped
* excerpt ({@link MAX_VALUE_CHARS}) of the offending string — plus `hint`, the
* parameter's own `description`, which is where an author writes what the
* identifier looks like. Neither adds exposure: the value is the model's own
* tool-call argument, already verbatim in the history this message is appended
* to. The OTel adapter renders `path`/`expected`/`got` only, so third-party
* telemetry stays value-free either way.
*/
/** When to enforce: 'enforce' rejects before dispatch (default), 'warn'
* emits the event but executes anyway, 'off' skips validation entirely. */
export type ToolArgValidationMode = 'enforce' | 'warn' | 'off';
/** Finite, value-free transport failure through the existing argument refusal owner. */
export declare function streamedToolArgIssues(failure: NonNullable<import('../provider-contract/types.js').LLMResponse['toolCalls'][number]['argumentsFailure']>): readonly ToolArgIssue[];
/** One schema violation. `got` is a TYPE NAME (optionally with a measured
* length), never a value. */
export interface ToolArgIssue {
/** Dot/bracket path from the args root, '' for the root itself. */
readonly path: string;
readonly expected: string;
readonly got: string;
/**
* A capped excerpt of the offending STRING. Present on string-shape issues
* (`pattern` / `minLength` / `maxLength`) and on nothing else — see the
* security note in this file's header for why those alone may echo.
*/
readonly value?: string;
/**
* The parameter's own `description` from the schema, when it declares one.
* Carried on string-shape issues: the description is where an author writes
* what the identifier looks like, and that sentence is the correction.
*/
readonly hint?: string;
}
export interface ToolArgValidationResult {
readonly ok: boolean;
readonly issues: readonly ToolArgIssue[];
}
/**
* Validate tool-call args against the tool's `inputSchema`.
*
* Total function: a malformed/exotic SCHEMA never throws — anything outside
* the honest subset is ignored, so the worst a bad schema can do is
* under-validate (never block a legitimate call).
*/
export declare function validateToolArgs(args: unknown, inputSchema: Readonly<Record<string, unknown>> | undefined): ToolArgValidationResult;
/**
* Validate ONE value against ONE property's own schema — the node rule
* `validateToolArgs` applies below the root, and nothing above it.
*
* Exists for the inputs layer (`tools/declare.ts` ·
* `assertAskOrAssume`): a declared `assume` value or `ask` choice is judged
* against `inputSchema.properties[argument]` alone. Wrapping it as
* `validateToolArgs({ [argument]: value }, inputSchema)` would enforce the
* ROOT `required` too, and refuse a rule on `window` because the
* declaration does not also carry `service`. Same honest subset, same total
* function: an exotic property schema under-validates, never throws.
*
* @example
* ```ts
* validatePropertyValue('2h', { type: 'string', enum: ['1h', '2h'] }).ok; // true
* validatePropertyValue('9h', { type: 'string', enum: ['1h', '2h'] }).ok; // false
* ```
*/
export declare function validatePropertyValue(value: unknown, propertySchema: Readonly<Record<string, unknown>> | undefined): ToolArgValidationResult;
/**
* Render the MODEL-VISIBLE tool result for a rejected call. Names paths and
* expectations; reports received TYPES for structural issues and the capped
* offending VALUE for string-shape ones, because a shape complaint that will
* not say which string it is about cannot be acted on. `JSON.stringify` quotes
* and escapes the excerpt, so a multi-line argument stays one line.
*
* The parameter's own `description` follows on its own line when the schema
* declares one: that sentence is usually the whole correction.
*/
export declare function formatToolArgIssues(toolName: string, issues: readonly ToolArgIssue[]): string;