FASCICLE
Why Primitives Providers Architecture Docs GITHUB
Composable agentic toolkit

Compose agents like plain values.

An agent is just a program that calls a model. Fascicle hands you the pieces to write that program yourself: every LLM call, tool, and plain function is a Step<i, o>, and steps snap together into whatever shape your problem actually has. No magic. Just logic.

$ pnpm add fascicle ⎘
{{ fileName }}
{{ tk.t }}
ONE GENERATE SURFACE · EIGHT PROVIDERS
{{ c.name }}
ONE STEP TYPE · THREE LAYERS
{{ g.label }}
{{ p.name }}
{{ selectionNote }}
01 · WHY FASCICLE

Not a framework. A toolkit.

Most agent libraries are frameworks — they own your control flow, hide it behind decorators, and tie you to one vendor. Fascicle is the opposite: a small set of plain, typed functions you compose yourself. You keep the architecture; it just makes the hard parts composable. Bigger frameworks will always ship more integrations; fascicle competes on a different axis: controllability, auditability, and portability.

No vendor lock-in

Develop against a free local model on your laptop; ship the same flow on Bedrock, Anthropic, or OpenAI by changing a string, not your code. Eight transports sit behind one generate, and the local-to-hosted path is proven in production.

No magic

No decorators, no dependency injection, no framework lifecycle, no ambient scheduler that decides when your code runs. A flow is plain functions you can read top to bottom, set a breakpoint in, and unit-test.

Sovereign by default

No hosted control plane, no vendor runtime, no dashboard-first product. Adapters are injected per run, every peer is optional, and a Step drops into the code you already have. No rewrite, no buy-in.

Composition, all the way down

Every primitive takes a Step<i,o> and returns a Step<i,o>. Wrap one in retry, fallback, or ensemble_step and it's still just a step — behavior layers on, the type never changes.

02 · THE HARNESS

You hold the reins.

The harness is the product. The model is a service it calls. Your outer program is ordinary, deterministic code that you can read, test, and reason about — it reaches for a model only where real judgment is needed, and runs token-free everywhere else.

That's the opposite of handing the reins to a model and hoping it orchestrates the others. Real engineering lives in the harness — Fascicle just gives you the primitives to compose it. Even the frontier labs now state the equation plainly: an agent is a model plus a harness, and the harness is the durable, controllable part.

✕ A MODEL IN THE DRIVER'S SEAT
Reins in the model's hands
model
↻
model
↻
model
tokens spent on every hop
—A model decides every next move at runtime.
—Every hop spends tokens, so latency and cost stay unpredictable.
—Nondeterministic. Hard to test, replay, or debug.
✓ AN ENGINEER IN THE DRIVER'S SEAT
Reins in your hands
fn
→
model_step
→
branch
→
fn
one model call, surrounded by token-free code
+Your script holds the control flow — plain, readable, testable.
+model_step only where real judgment is needed.
+Everything else is token-free: fast, cheap, deterministic.
This shape is the future: boring, predictable code that calls models sparingly — not models that burn tokens to orchestrate other models.
03 · ADD A MODEL STEP

A model is just another step.

model_step turns a model role into a Step<i, o> that returns the answer itself: a string, or the schema-validated value. Every primitive you've already seen applies to it. Wrap it in retry, fan it out with map, race it with fallback. Fascicle doesn't offer a second API for “calling a model carefully.” When the caller wants the envelope (usage, cost, tool calls), model_call takes the same config and returns the full GenerateResult.

It also threads the tedious parts for you (cancellation through ctx.abort, tracing through ctx.trajectory, and streaming chunks), so your flow stays pure composition and never imports a provider SDK.

Read the concepts →
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.' }),
])

try {
console.log(await run(flow, 'Rust ownership'))
} finally {
await engine.dispose()
}
04 · COMPOSITION

Add capability by wrapping — never by rewriting.

Because every composer takes a Step and returns a Step, a composition is just another Step. No layer is privileged as a “top level,” and no interface widens as a flow grows — a thousand nested layers still type as Step<i, o>.

So you harden a flow the way you'd wrap a function. Need retries, a fallback, or three-model voting? Wrap the step you already have. What's inside doesn't change; what calls it doesn't change. No refactor, no bespoke glue for every feature — just one more layer.

Step<i, o> → Step<i, o>
retry + survives flaky providers
adversarial + critiques & revises
ensemble_step + runs 3, a judge keeps the best
step
step
step
your logic
Three capabilities added. The step in the middle was never touched.
05 · TWO STYLES

Declarative or direct. Same trajectory.

Write a flow as a visible chain when you want the topology as data, or as a plain step body when the control flow is genuinely dynamic. ctx.call(step, input) is the one bridge between them, and the audit story is identical under each: everything the model saw is logged and replayable, because the trajectory is enforced below both styles, at the model boundary.

DECLARATIVE · THE TOPOLOGY IS DATA
release-notes/main.ts
const flow = chain<Repo, 'repo'>('repo')
.step('commits', ({ repo }) => list_commits(repo))
.step('draft', ({ commits }, ctx) =>
ctx.call(writer, format_prompt(commits)), { arm: writer })
.step('checked', ({ draft }, ctx) =>
ctx.call(fact_gate, draft), { arm: fact_gate })
.output(({ checked, draft }) => (checked ? draft : 'needs review'))
Bindings are typed, arms are declared, and describe prints the whole tree (spine, arms, leaves) before anything runs.
DIRECT · A PLAIN BODY, WHEN WIRING IS DYNAMIC
release-notes-direct/main.ts
const flow = step('release_notes', async (repo: Repo, ctx) => {
const commits = list_commits(repo)
const draft = await ctx.call(writer, format_prompt(commits))
if (!needs_check(commits)) return draft
const checked = await ctx.call(fact_gate, draft)
return checked ? draft : 'needs review'
})
Ordinary const / if / await. ctx.call keeps spans, abort, and error paths intact, so nothing is lost but static describability.
Choose per flow; the two styles compose freely in both directions. The same agent is written once in each style in release-notes/main.ts and release-notes-direct/main.ts.
06 · PROVIDERS

One generate surface, eight providers.

Your logic shouldn't care who serves the tokens. Write a flow once and point it at Anthropic, Bedrock, or a model that runs on your laptop — by changing a string. Develop against free local models, ship on a compliant cloud, fall back to another vendor mid-outage; the code never moves.

Aliases resolve, a provider-neutral effort knob translates per vendor, and supports(capability) lets you degrade gracefully instead of failing at runtime. Every SDK is an optional peer — install only the ones you call.

PROVIDER text tools schema streaming image reasoning
{{ row.name }} {{ row.text }} {{ row.tools }} {{ row.schema }} {{ row.streaming }} {{ row.image }} {{ row.reasoning }}
✅ supported · — not in v1 (ollama, lmstudio, claude_cli have no image input; ollama and lmstudio also drop reasoning effort)

Build your first flow.

Install, wire a sequence, run it as a value. The docs walk you from a one-liner to a full agentic harness.

$ pnpm add fascicle ⎘
Open the docs →
FASCICLE

Composable TypeScript toolkit for agentic workflows. Compose LLM calls, tools, and plain functions into a Step<i, o>.

Apache-2.0 · © 2026
MODULES
core engine composites adapters mcp viewer