Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 10 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof NutritionFacts>

const Judgement = z.object({ ok: z.boolean(), reason: z.string() })
type Judgement = z.infer<typeof Judgement>
import { check, judge, ThinkingBlock } from "thinking-blocks"

// The machine. Define it once; the catalog it serves is infinite.
const nutrition = new ThinkingBlock<FoodInput, NutritionFacts>({
// 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<FoodInput, NutritionFacts>("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<FoodInput, NutritionFacts, Judgement>("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 +
Expand Down
Loading