agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
156 lines (155 loc) • 6.72 kB
TypeScript
/**
* tool-lint types — the tool-catalog confusability lint contract
* (RFC-002 tier 1, blocks C1–C3).
*
* Pattern: Strategy seam (the plug-and-play meta-pattern) — the frame
* and rule engine are the library's; the embedder, thresholds,
* and structural rule pack are all consumer-injected, with our
* defaults. Exactly like NarrativeFormatter / reliability /
* permission / commentary strategies.
* Role: `src/tool-lint/` leaf module. ZERO stack buy-in: input is a plain
* `{ name, description?, inputSchema? }[]` — any OpenAI /
* Anthropic / LangChain / MCP tool list normalizes to it
* (see `coerceCatalog`). The library's own `Tool[]` adapts via
* `catalogFromTools`.
*
* ## Honest claim (RFC-002 §2)
*
* Confusability here is embedding geometry over what the model READS
* (tool name + description) — a deterministic heuristic for "could the
* model mix these up", never a measurement of any model's actual
* selection function. Tier 3 (choice-entropy sampling) validates the
* proxy; until then treat verdicts as review prompts, not ground truth.
*/
import type { Embedder, SimilarityPair } from '../influence-core/index.js';
/**
* One tool as the lint sees it — the minimal, framework-agnostic shape.
* `inputSchema` is a JSON Schema object (the same one the LLM sees);
* structural rules read `properties` / `required` / `enum` from it.
*/
export interface CatalogTool {
readonly name: string;
readonly description?: string;
/** JSON Schema for the tool's arguments (`type: 'object'` shape). */
readonly inputSchema?: Readonly<Record<string, unknown>>;
}
/** Verdict for one description pair from the similarity analysis. */
export type PairVerdict = 'confusable' | 'watch';
/** One flagged pair: two tools the model could plausibly mix up. */
export interface ConfusablePairFinding {
readonly kind: PairVerdict;
/** Tool names (input order: `a` has the lower catalog index). */
readonly a: string;
readonly b: string;
/** cosine(embed(a), embed(b)) over `confusabilityText` of each tool. */
readonly similarity: number;
/**
* Heuristic suggestion for the DIFFERENTIATING AXIS — what to make
* explicit in the descriptions so the model can tell them apart
* (e.g. "names differ only by 'influx' — say WHEN to use each").
*/
readonly hint: string;
}
/** Severity of a structural finding. `error` fails the gate by default. */
export type LintSeverity = 'error' | 'warn';
/** One structural finding from a `LintRule`. */
export interface StructuralFinding {
/** Id of the rule that produced this finding. */
readonly rule: string;
/** Name of the offending tool. */
readonly tool: string;
readonly severity: LintSeverity;
readonly message: string;
/** Offending parameter name, when the finding is param-scoped. */
readonly param?: string;
/** Concrete fix suggestion (e.g. the JSON-Schema `enum` to add). */
readonly suggestion?: string;
}
/**
* One pluggable structural rule. Rules are plain objects — add your own
* to `rules` in `analyzeToolCatalog` options, or filter the exported
* `defaultStructuralRules` to remove ours.
*/
export interface LintRule {
/** Stable id, kebab-case (shows on findings + CLI output). */
readonly id: string;
/** Inspect ONE tool (with the whole catalog for context) and return
* zero or more findings. Must not throw on weird-but-valid input. */
check(tool: CatalogTool, catalog: readonly CatalogTool[]): readonly StructuralFinding[];
}
export interface AnalyzeToolCatalogOptions {
/**
* Injected embedder for the confusability analysis. OMITTED → the
* similarity analysis is skipped (structural rules still run) and
* `report.similarity.analyzed` is false.
*
* Wrap in an `EmbeddingCache` (from `agentfootprint/observe`) so
* catalog descriptions embed once across lint runs — keyed by content
* hash, so unchanged descriptions cost nothing on re-lint.
*/
readonly embedder?: Embedder;
/**
* Pairs with similarity ≥ this are `confusable`. Default 0.85 — a
* STARTING point calibrated for real sentence embedders (where
* unrelated tool descriptions typically land 0.3–0.7).
*
* ⚠ Per-embedder calibration is REQUIRED (RFC-002 §3): absolute
* cosine ranges differ by embedder. The test/demo `mockEmbedder`
* (character-frequency) compresses unrelated prose into ~0.85–0.97,
* so with the mock use `MOCK_EMBEDDER_CALIBRATION` and trust only
* RELATIVE ordering of pairs, never absolute verdicts.
*/
readonly confusabilityThreshold?: number;
/**
* Pairs within this band BELOW the threshold are `watch` (advisory —
* never fail the gate). Default 0.05.
*/
readonly watchBand?: number;
/**
* The structural rule pack. Default `defaultStructuralRules`.
* Add/remove freely — rules are plain `{ id, check }` objects.
*/
readonly rules?: readonly LintRule[];
/**
* Which structural severity fails the gate (`report.ok`).
* Default 'error' — warnings are advisory. 'warn' = strict mode.
*/
readonly failOn?: LintSeverity;
/** Abort signal threaded to the embedder (network backends). */
readonly signal?: AbortSignal;
}
/** The similarity section of a report. */
export interface SimilarityReport {
/** False when no embedder was supplied — similarity was skipped. */
readonly analyzed: boolean;
readonly confusable: readonly ConfusablePairFinding[];
readonly watch: readonly ConfusablePairFinding[];
/**
* EVERY pair ranked by similarity, descending — the relative-ordering
* view that stays meaningful under ANY embedder (including the mock).
* Empty when `analyzed` is false.
*/
readonly ranked: readonly SimilarityPair[];
readonly thresholds: {
readonly confusabilityThreshold: number;
readonly watchBand: number;
};
}
/** The lint report — `ok` is the CI-gateable verdict. */
export interface ToolCatalogReport {
/**
* True when there are no confusable pairs AND no structural findings
* at/above `failOn` severity. The CI gate: exit non-zero on `!ok`.
*/
readonly ok: boolean;
readonly toolCount: number;
readonly similarity: SimilarityReport;
readonly structural: readonly StructuralFinding[];
readonly summary: {
readonly confusable: number;
readonly watch: number;
readonly errors: number;
readonly warnings: number;
};
}
export type { Embedder, SimilarityPair };