Skip to content

Recipes API (Recipe Handles)

Recipes are the front door to llm-core. Everything is a recipe handle: some recipes are leaf steps, some are composite, but the public surface is the same.

> **Recipes are the public story.** Packs/flows are internal composition tools; most users never need them.

Related:

The Recipe Catalog

llm-core exports a catalog of recipe factories. You access them by name.

ts
import { recipes } from "@geekist/llm-core/recipes";

const agent = recipes.agent();
const rag = recipes.rag();

Available recipes:

  • agent: ReAct agent with tools, planning, and memory.
  • rag: Retrieval-augmented generation.
  • chat.simple: Basic chat with system prompt.
  • chat.rag: RAG specialized for chat history.
  • hitl: Human-in-the-loop gate.
  • ingest: Document ingestion pipeline.
  • eval: Generation evaluation.
  • loop: Iteration control.

The agent recipe aligns with the agent loop contract in @geekist/llm-core/interaction, which defines the event semantics (interaction.item.*, interaction.subagent.*) and deterministic state snapshots used by UI integrations. The contract is adapter-agnostic, and apps do not need to wire packs manually to use it. The agent recipe is the reference implementation of this contract; you can still build your own recipes on the same types if you need a custom loop.

Recipe handles (the public surface)

Every recipe exposes the same handle methods:

  • configure(config) — recipe-specific behavior (prompts, retrieval knobs, tool options).
  • defaults(defaults) — wiring and infra (adapters, plugins, retry defaults).
  • use(otherRecipe) — composition (pluggable sub-recipes).
  • explain() — returns the step graph (no side effects).
  • build() — returns a reusable runnable.
  • run(input, runtime?) — one-shot execution.

Use defaults({ retryDefaults }) when you want per-recipe retry policy defaults; per-run overrides live in runtime.retry.

ts
import { recipes } from "@geekist/llm-core/recipes";
import type { AgentRecipeConfig } from "@geekist/llm-core/recipes";

// Configure once, reuse across requests.
const agent = recipes["agent"]()
  .configure({
    planning: { modelInstructions: "Plan before acting." },
    tools: { toolChoice: "auto" },
  } satisfies AgentRecipeConfig)
  .defaults({ adapters: { model, tools, memory } }); // Wire adapters once.

const result = await agent.run({ input: "Help me debug this RAG flow." });

Recipe-specific config (no global junk drawer)

Each recipe exports its own config type. configure() only accepts that type. There is no global RecipeConfig bag.

ts
import type { RagRecipeConfig } from "@geekist/llm-core/recipes";

const rag = recipes["rag"]().configure({
  retrieval: { topK: 8 },
  synthesis: { system: "Cite your sources." },
} satisfies RagRecipeConfig);

explain(): see the graph

explain() gives you a stable, inspectable view of the recipe DAG.

ts
type PlanView = { steps: Array<{ id: string }> };

const plan: PlanView = recipes["rag"]().explain();
// Inspect the resolved step graph.
console.log(plan.steps.map((step) => step.id));

build(): reusable runnables

build() is optional, but useful when you want to cache a configured recipe.

ts
const runnable = recipes["rag"]()
  .configure({ retrieval: { topK: 5 } })
  .defaults({ adapters: { model, retriever } }) // Keep wiring separate.
  .build();

type RagRunnable = typeof runnable;
const outcome = await(runnable as RagRunnable)({ input: "Explain DSP." });