FASCICLE
DOCS
Home Architecture GITHUB
{{ grp.title }}
{{ it.label }}
GET STARTED

Getting started

Install fascicle, wire a sequence of plain functions, and run it as a value. Then add a model call when you need one.

Install

shell
$ pnpm add fascicle

Provider SDKs are optional peers — each is loaded lazily on first generate against that provider. Install only the ones you call.

Your first flow

A flow is a tree of plain Step values. sequence threads each output into the next input.

first-flow.ts
import { run, sequence, step } from 'fascicle'

const flow = sequence([
step('add', (n: number) => n + 1),
step('double', (n: number) => n * 2),
])

await run(flow, 1) // 4

Add a model step

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.

brief-flow.ts
import { create_engine, model_step, run, sequence, step } from 'fascicle'

const engine = create_engine({
providers: { anthropic: { api_key: process.env.ANTHROPIC_API_KEY! } },
})

const flow = sequence([
step('brief', (topic) => `Write a 2-sentence brief on: ${topic}`),
model_step({ engine, model: 'claude-sonnet-4-6', system: 'No preamble.' }),
])

await run(flow, 'Rust ownership')
Next: Concepts →
Configure a provider →
BUILDING A WHOLE APP?
Once your flow outgrows one file, follow the agent blueprint — the standard architecture for fascicle apps: one composition layer, markdown prompts, module contracts, and ast-grep rules to enforce the boundaries.
CORE

Concepts

The mental model behind fascicle. Read this once — the rest of the docs assume it.

Two layers

fascicle ships two independently useful layers from a single src/ tree, and exactly one value glues them: model_call.

core
Composition
22 primitives for composing work out of plain values. No network, no LLM calls, no ambient state.
engine
Engine
create_engine(config) returns one generate surface over eight providers. No step plumbing.

Step-as-value

Every composable unit is a Step<i, o>. Every composer takes one and returns one — the type never widens.

types.ts
type Step<i, o> = {
readonly id: string
readonly kind: string
run(input: i, ctx: RunContext): Promise<o> | o
readonly config?: Readonly<Record<string, unknown>>
readonly children?: ReadonlyArray<Step<unknown, unknown>>
}
Substitutability
Replace any step with any composition that shares its I/O.
Introspectability
A flow is a tree of plain objects. Walk it with describe().
No hidden state
Steps are values, not instances. Flows share nothing.

Running a flow

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.

stream.ts
const handle = run.stream(flow, input)
for await (const event of handle.events) {
if (event.kind === 'emit') console.log(event)
}
const output = await handle.result

Named state: chain

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.

chain.ts
const flow = chain('email')
.step('user', ({ email }) => find_user(email))
.step('sent', ({ email, user }) => publish_event(email, user))
.output(({ sent }) => sent)

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.

CORE · THE DECISION GUIDE

Leaves, arms, spine

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.

Leaves
model_step · model_call · step · define_agent

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.

RULE → if it's one call, keep it a leaf. A single leaf wrapped just for a span name is composition theater.
Arms
retry · timeout · fallback · checkpoint · map · parallel · sequence · ensemble_step · adversarial · consensus

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.

RULE → an arm exists when a second primitive genuinely composes around a leaf.
Spine
chain · ctx.call · branch · loop

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.

RULE → fan-in decides. A step that needs only its predecessor stays in sequence; the first step that needs anything older moves the flow to chain.
The topology is the file: the spine reads top to bottom as the agent diagram, arms are named, leaves are role ids.
THE ADVANCED TIER
scope/stash/use, plain ensemble, tournament, and improve/learn remain fully supported, but each has a primary counterpart to try first. Advanced composition covers when each earns its keep.
CORE · API REFERENCE

The primitives

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.

{{ p.name }}
{{ p.desc }}
{{ p.tag }}
{{ p.icon }}
{{ p.example }}
ENGINE

Configuration

Configure the engine with create_engine(config). Only providers is required; everything else has a default.

The config shape

EngineConfig
type EngineConfig = {
providers: ProviderConfigMap // required
custom_providers?: Record<string, ProviderFactory>
pricing?: PricingTable
default_retry?: RetryPolicy
default_effort?: EffortLevel
default_max_steps?: number
defaults?: EngineDefaults
}

Engine defaults

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.

defaults.ts
const engine = create_engine({
providers: { claude_cli: { auth_mode: 'oauth' } },
defaults: {
provider: 'claude_cli',
model: 'sonnet',
system: 'Reply in one short sentence.',
max_steps: 8,
},
})

const result = await engine.generate({ prompt: 'hello' })

Retry policy

Retries apply only to provider-side failures — 429s, 5xx, and network errors. Backoff is exponential with jitter; Retry-After always wins.

DEFAULT_RETRY
const DEFAULT_RETRY: RetryPolicy = {
max_attempts: 3,
initial_delay_ms: 500,
max_delay_ms: 30_000,
retry_on: ['rate_limit', 'provider_5xx', 'network', 'timeout'],
}

Lifecycle

Construct once per process. dispose() is idempotent; after it, every generate throws engine_disposed_error. Subprocess providers abort in-flight children on dispose.

ENGINE

Providers

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.

Capability matrix

PROVIDER text tools schema stream image reason
{{ row.name }} {{ row.text }} {{ row.tools }} {{ row.schema }} {{ row.streaming }} {{ row.image }} {{ row.reasoning }}

Configure a provider

{{ t.name }}
{{ active.peer }}
{{ active.code }}

{{ active.note }}

Effort translation

effort: 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' is a provider-neutral knob, and each provider translates it.

PROVIDER low medium high
{{ row.name }} {{ row.low }} {{ row.medium }} {{ row.high }}

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.

ENGINE

Errors

Typed errors live in fascicle and bubble out of run(...) as normal promise rejections. Composition errors carry a .path of step ids.

CLASS THROWN BY
{{ e.name }} {{ e.by }}