/** * Validate-then-repair engine for LLM tool-call inputs. * * Design (matching the publicly described behavior of commandcode's repair layer): * * - Strictly valid inputs are never touched: the fast path is a plain * `Value.Check` and returns the original input by reference. * - On failure, the validator's own issue list localizes the damage. Repairs are * attempted only at the exact paths the schema disagreed with, in a fixed * order (JSON-array parsing must run before bare-string wrapping, or * `'["a","b"]'` becomes `['["a","b"]']`). * - Every mutation produces a model-facing note so the model can learn the real * contract on the next turn. Transparency over silent magic. * * The strict check deliberately runs BEFORE TypeBox's `Value.Convert` (which pi * applies during validation), because Convert silently corrupts exactly the * inputs this layer exists to fix: `'["a","b"]'` for an array field becomes * `['["a","b"]']`, `null` for an optional string becomes the string `"null"`, * and `null` for an optional number becomes `0` — all of which then pass * validation and execute with garbage. Repairing at the strict-error sites * first means those inputs are fixed properly (with a note) instead. Benign * coercions ("5" -> 5) are left to Convert: if no repair rule fires and Convert * alone makes the input valid, the input is reported as valid and returned * untouched, so pi's native behavior is preserved. * * The one deliberate exception to validate-then-repair is markdown auto-link * unwrapping on path fields: `[notes.md](http://notes.md)` is a perfectly valid * string, so validation can never flag it. It is unwrapped unconditionally, but * only in the degenerate case where the link text equals the url without its * protocol — real markdown links pass through untouched. */ import type { TSchema } from "typebox"; export interface StructuralRepair { /** Rule name recorded in telemetry when the repair fires. */ name: string; /** Mutate `args` in place. Return a model-facing note when a repair was applied, false otherwise. */ apply(args: Record, toolName: string): string | false; } export interface ToolRepairConfig { /** canonical field name -> wrong names models emit for it, matched at any depth by key. */ fieldAliases?: Record; /** When the whole input is a bare string, wrap it as `{ [field]: value }`. */ rootString?: { field: string; wrapInArray?: boolean; }; /** Top-level string fields holding filesystem paths (markdown auto-link unwrapping). */ pathFields?: readonly string[]; /** Tool-specific shape folds that single-field rules cannot express. */ structural?: readonly StructuralRepair[]; } export interface RepairResult { outcome: "valid" | "repaired" | "unrepairable"; /** What prepareArguments should return: the untouched input, the repaired input, or (unrepairable) the untouched input. */ args: unknown; rulesFired: string[]; notes: string[]; /** One entry per mutation, including repeated use of the same stable rule. */ changes: Array<{ ruleId: string; note: string; }>; /** Compact description of the original validation failure, for telemetry. */ issueSummary: string | undefined; /** Stable hash of (tool, failure shape), for spotting per-model regressions. */ fingerprint: string | undefined; /** * Model-readable error for unrepairable input. Throwing this from * prepareArguments matters: handing the raw input back to pi instead would * let Value.Convert corrupt it (null -> "null") and execute the call anyway. */ retryMessage: string | undefined; } export declare function unwrapMarkdownAutoLinks(value: string): string; export declare function repairSchemaInput(options: { toolName: string; schema: TSchema; input: unknown; config?: ToolRepairConfig; }): RepairResult; /** * Current-major compatibility facade. New consumers should prefer * `runRepairPipeline`, but this shape and its legacy rule names remain stable. */ export declare function repairToolInput(options: { toolName: string; schema: TSchema; input: unknown; config?: ToolRepairConfig; }): RepairResult; //# sourceMappingURL=repair-engine.d.ts.map