Primitives

Six independent functions that handle the hard parts of working with LLM output. Use them inside enforce, or drop any one into an existing pipeline.

clean

(raw: string) => unknown

Normalizes raw LLM output into clean JSON. Strips markdown fences, extracts JSON from prose wrapping, and coerces string-typed values to their correct types.

Markdown fences

clean('```json\n{"score": 85}\n```');
// { score: 85 }

Prose wrapping

clean('Here you go: {"score": "85"}');
// { score: 85 }

Type coercion

clean('{"active": "true", "count": "3"}');
// { active: true, count: 3 }

Language variations

clean('```JSON\n{"score": 85}\n```');
// { score: 85 }

This runs automatically inside enforce. Use it standalone when you already have raw output and want clean JSON.

check

(data, schema, invariants?) => Result<T>

Validates data against a Zod schema and optional invariants. Deterministic — no LLM involved. Returns typed data or a structured error with every issue listed.

Pass

check({ name: "Alice", age: 30 }, Schema);
// { ok: true, data: { name: "Alice", age: 30 } }

Fail — schema violation

check({ name: "Alice", age: -5 }, Schema);
// { ok: false, error: { issues: ["age: must be >= 0 (received -5)"] } }

Fail — missing field

check({ name: 123 }, Schema);
// { ok: false, error: { issues: ["name: expected string, got number", "age: required"] } }

classify

(raw, cleaned) => FailureCategory

Categorizes a failed LLM response into one of 8 failure types. Used internally by enforce to generate the right repair message for each situation.

Category What happened Default repair
EMPTY_RESPONSEModel returned nothingAsk for JSON matching the schema
REFUSALModel declined ("I'm sorry", "as an AI")Redirect to structured data task
NO_JSONResponse contained no JSON at allAsk for JSON only, no prose
TRUNCATEDJSON cut off (unbalanced braces)Ask for a shorter, complete response
PARSE_ERRORJSON present but malformedAsk for strictly valid JSON
VALIDATION_ERRORValid JSON but failed schemaList the specific Zod errors
INVARIANT_ERRORCorrect types but failed constraintsList the specific violations
RUN_ERRORYour function threw an exceptionAsk to try again

fix

(detail, repairs?) => Message[] | false

Turns validation failures into targeted repair messages for the next attempt. The output is an array of messages you append to the conversation — the model gets specific feedback about what went wrong.

fix(checkResult.error);
// [{
//   role: "user",
//   content: "Your previous response had validation errors:\n"
//     + "- age: must be >= 0 (received -5)\n"
//     + "- email: expected string, got undefined\n"
//     + "Please correct these issues and respond with valid JSON."
// }]

You can override or disable repair for any category. Pass false to stop retrying a specific failure type.

select

(state, schema) => Record<string, unknown>

Strips a state object down to only the fields the schema defines. Prevents sending PII, secrets, or unrelated data to the LLM.

const fullUser = {
  name: "Alice Chen",
  email: "alice@company.com",
  ssn: "123-45-6789",
  passwordHash: "$2b$10$abc...",
  loginHistory: [{ ts: "2024-01-01", ip: "10.0.0.1" }],
};

select(fullUser, z.object({ name: z.string(), email: z.string() }));
// { name: "Alice Chen", email: "alice@company.com" }
//
// ssn, passwordHash, loginHistory — gone.
// The LLM never sees them.

prompt

(schema: ZodType) => string

Generates format instructions from a Zod schema. Used internally by enforce to populate attempt.prompt. Use it directly when building prompts manually.

import { prompt } from "llm-contract";
import { z } from "zod";

const instructions = prompt(
  z.object({
    sentiment: z.enum(["positive", "negative", "neutral"]),
    confidence: z.number().min(0).max(1),
  })
);
// Generates format instructions describing the expected JSON shape,
// field types, enums, and constraints — ready to use as a system message.

Composability

enforce runs the full loop. But every primitive is exported independently — use whichever pieces fit your pipeline.

import { clean, check, fix, classify } from "llm-contract";

const raw = await yourLLMCall();
const cleaned = clean(raw);
const result = check(cleaned, schema, invariants);

if (!result.ok) {
  const category = classify(raw, cleaned);
  const repairs = fix(result.error);
  // feed repairs back into your own retry logic
}