Install fascicle, wire a sequence of plain functions, and run it as a value. Then add a model call when you need one.
Provider SDKs are optional peers — each is loaded lazily on first generate against that provider. Install only the ones you call.
A flow is a tree of plain Step values. sequence threads each output into the next input.
model_step is the default model boundary: it returns the answer itself (a string, or the schema-validated value when schema is set) and threads abort, trajectory, and streaming for you. model_call is the envelope variant, for when the caller wants usage, cost, or tool calls.
The mental model behind fascicle. Read this once — the rest of the docs assume it.
fascicle ships two independently useful layers from a single src/ tree, and exactly one value glues them: model_call.
Every composable unit is a Step<i, o>. Every composer takes one and returns one — the type never widens.
run(flow, input) executes to completion. run.stream(...) returns { events, result } — purely observational over the same graph. run.until_suspended(...) drives flows with a suspend gate: suspension comes back as a typed outcome with a resume closure instead of a thrown error.
The moment a step needs a value that isn't its immediate predecessor's output, reach for chain: each .step merges its result into a growing typed record, later bindings destructure whatever earlier names they need (checked at compile time), and .output projects the result into an ordinary Step.
Underneath sits the raw state tier (scope / stash / use, string keys over ctx.state) for the shapes that bindings can't express. See advanced composition before reaching for it.
Every well-factored fascicle app converges on the same three-layer shape, whatever its domain. The layering is a convention, not a mechanism: it keeps a flow readable top to bottom, renders fully under describe, and leaves every piece swappable behind a Step type.
One boundary each: a model role, a tool, a pure function. model_step is the default model boundary; reach for model_call only when the caller wants the envelope (usage, cost, tool calls). Give each model leaf a stable role id as the first line of its system prompt: stub engines route canned responses on it, and trajectory readers orient by it.
A named composition around leaves: hardening, selection, verification, fan-out, straight pipes. The judgment composites return result envelopes; their project option unwraps at the source, so an arm presents a domain value instead of leaking its machinery downstream.
One chain per flow: the typed record everything threads through. A binding invokes its arm with ctx.call(arm, input) and declares it as arm metadata, so describe renders the whole topology without running anything.
Every composer takes a Step<i, o> and returns one. The tags name each primitive's layer (leaf, arm, spine); the advanced tier is supported but not the default — each of those entries names the primary primitive to try first. Click any primitive to expand its signature and an example.
Configure the engine with create_engine(config). Only providers is required; everything else has a default.
defaults pre-fills per-call options so your generate sites stay terse. Per-call options win via nullish coalesce; provider_options shallow-merges per provider key.
Retries apply only to provider-side failures — 429s, 5xx, and network errors. Backoff is exponential with jitter; Retry-After always wins.
Construct once per process. dispose() is idempotent; after it, every generate throws engine_disposed_error. Subprocess providers abort in-flight children on dispose.
Eight adapters ship with the engine layer. Seven wrap Vercel's AI SDK by default, and five of those (anthropic, openai, openrouter, lmstudio, ollama) can instead run transport: 'native' — raw HTTP, no AI SDK in the path, no peer to install. The eighth, claude_cli, spawns the claude binary. Each SDK is an optional peer.
{{ active.note }}
effort: 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' is a provider-neutral knob, and each provider translates it.
ollama · lmstudio drop effort and record effort_ignored on the trajectory. claude_cli forwards it verbatim as CLAUDE_CODE_EFFORT_LEVEL in the subprocess env.
Typed errors live in fascicle and bubble out of run(...) as normal promise rejections. Composition errors carry a .path of step ids.