FASCICLE
DOCS
Home 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 zod

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 call

model_call is the only sanctioned bridge between composition and the engine. It threads abort, trajectory, and streaming for you.

brief-flow.ts
import { create_engine, model_call, 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_call({ engine, model: 'claude-sonnet-4-6', system: 'No preamble.' }),
step('extract', (r) => r.content),
])

await run(flow, 'Rust ownership')
Next: Concepts →
Configure a provider →
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, glued by exactly one value — model_call.

core
Composition
18 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 sharing 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.

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

Scope, stash & use

When two non-adjacent steps must agree on a value, bind it by name instead of rewiring the whole chain.

scope.ts
const flow = scope([
stash('user_id', step('lookup', (email) => find_user(email))),
use(['user_id'], ({ user_id }) => publish_event(user_id)),
])
CORE · API REFERENCE

The 18 primitives

Every composer takes a Step<i, o> and returns one. 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
pricing?: PricingTable
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'],
}

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; 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, translated per provider.

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

ollama · lmstudio · claude_cli drop effort and record effort_ignored on the trajectory.

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 }}