diff --git a/README.md b/README.md index 3b8746d..8456c52 100644 --- a/README.md +++ b/README.md @@ -11,70 +11,22 @@ through QC (your validators), reworked until it's in-spec, stamped with a serial model call. **The catalog is infinite.** ```ts -import { Output, ToolLoopAgent } from "ai" -import { check, InMemoryThinkingBlockStore, judge, ThinkingBlock } from "thinking-blocks" -import { z } from "zod" - -type FoodInput = { food: string } - -// The spec — the shape every finished part must match. -const NutritionFacts = z.object({ - serving: z.string(), - calories: z.number().nonnegative(), - protein: z.number().nonnegative(), - carbs: z.number().nonnegative(), - fat: z.number().nonnegative(), -}) -type NutritionFacts = z.infer - -const Judgement = z.object({ ok: z.boolean(), reason: z.string() }) -type Judgement = z.infer +import { check, judge, ThinkingBlock } from "thinking-blocks" -// The machine. Define it once; the catalog it serves is infinite. -const nutrition = new ThinkingBlock({ +// A machine: an agent makes the part, validators are its QC gates. +const nutrition = new ThinkingBlock({ name: "nutrition", - store: new InMemoryThinkingBlockStore(), - agent: new ToolLoopAgent({ - model: "openai/gpt-5", - output: Output.object({ schema: NutritionFacts }), - }), - // The serial number: same food -> same part, forever. - identity: ({ food }) => food.trim().toLowerCase(), - prepareCall: ({ input }) => ({ - prompt: `Nutrition facts for one typical serving of "${input.food}". Give "serving" as a short human description and protein/carbs/fat in grams.`, - }), - attempts: { max: 3 }, + store, // where finished parts are kept + agent, // a ToolLoopAgent — its output schema is the spec + prepareCall: ({ input }) => ({ prompt: `Nutrition facts for ${input.food}` }), validators: [ - // QC, no model — a caliper: the stated calories must reconcile with the - // macros (4·protein + 4·carbs + 9·fat), or the part is reworked. - check("macros-reconcile", { - validate: ({ output }) => { - const implied = 4 * output.protein + 4 * output.carbs + 9 * output.fat - return Math.abs(implied - output.calories) <= Math.max(40, 0.25 * output.calories) - ? { success: true } - : { success: false, feedback: `${output.calories} kcal doesn't match the macros (~${Math.round(implied)} kcal) — fix the numbers.` } - }, - }), - // QC, with a model — an inspector: is this a realistic serving for the food? - judge("serving-is-realistic", { - agent: new ToolLoopAgent({ - model: "openai/gpt-5", - output: Output.object({ schema: Judgement }), - }), - schema: Judgement, - prepareCall: ({ input, output }) => ({ - prompt: `Is "${output.serving}" a realistic serving of "${input.food}", with plausible macros?`, - }), - validate: ({ judgement }) => - judgement.ok ? { success: true } : { success: false, feedback: judgement.reason }, - }), + check("macros-reconcile", { validate }), // QC in code + judge("serving-realistic", { agent, schema, prepareCall, validate }), // QC by a model ], }) -// Order a finished part: the machine makes it, runs it through QC, keeps it. -const facts = await nutrition.get({ food: "dragon fruit" }) -// facts.output is { serving, calories, protein, carbs, fat }, validated and kept; -// the same food -> the same part, instantly, forever, with no model call. +const part = await nutrition.get({ food: "dragon fruit" }) +if (part.ok) part.output.calories // 60 — typed, validated, kept ``` Under the hood a Thinking Block is `function + AI agent + validation + memory +