01The problem: today's agents are stateless
Every LLM call today is a one-shot. You send a prompt, you get a response, the context evaporates. There is no memory of what was tried, no provenance for what was produced, no way to compose the output with anyone else's work, no way to roll back to a known-good state, and no way to share a result with the next agent without re-deriving it from scratch.
It is the worst of both worlds: as fragile as a shell pipeline, and as expensive as a kernel boot. We have all the machinery of distributed systems — content addressing, capability negotiation, deterministic replay — and none of it is in the agent stack.
Stateless LLM calls
- One-shot, no memory between calls
- No composition between agents
- No provenance ("who produced this?")
- No rollback ("yesterday's run worked")
- Re-deriving the same answer costs the same every time
- Tools hand-coded per agent, not reusable
Quilt as agent substrate
- Artifacts pinned by content hash — never drift
- Manifests validated before any code runs
- Run traces are immutable audit logs
{{template}}substitution built into URIs- Any agent can call any artifact — registry is shared
- Rollback is one
engine.serialize()away
The spec we're implementing is small — five primitives — but it is the same shape as the rest of the internet works (URIs, content addressing, signed manifests, immutable logs). We are not inventing a new model. We are catching up.
02The five primitives
The spec defines five operations. Every other feature — caches, retries, replay, lineage, capability routing — is a composition of these. They are deliberately minimal so that any agent (Claude, GPT, a custom LLM, a shell script) can implement them.
① resolveArtifact(uri, ctx)
Given a canonical URI, return the pinned, content-addressed artifact. Templates like quilt://demo/dataset:{{run_id}} are filled from ctx before lookup. The result includes a SHA-256 of the bytes; downstream code can verify the result has not been tampered with.
// spec const a = await resolveArtifact("quilt://demo/dataset:abc123", { run_id: "abc123" }); // → { uri, hash: "sha256:9f86d0...", bytes, kind } // Quilt — every cell IS a content-addressed artifact import { Engine } from '@quilt/core'; const engine = await Engine.load(); const cell = engine.cell(`/demo/dataset:abc123`); // pinned await engine.flush(); // resolve & cache
② validateManifest(manifest)
Check that a manifest is well-formed before any of its cells run. Two layers: structural (JSON Schema: required fields, types, enums) and semantic (preconditions: input URIs resolve, model names are known, conditions are satisfiable). If either fails, the run is rejected with a structured error — not a stack trace.
// spec const r = validateManifest({ name: "classifier-dag", version: "1.0.0", cells: [...], preconditions: ["model_requirements.provider == 'openai'"] }); // → { ok: true } or { ok: false, errors: [{ path, msg }] } // Quilt — ajv for schema, sensor + validate listeners for preconditions import { validate } from '@quilt/sdk'; const r = validate(manifest); if (!r.ok) throw new ManifestError(r.errors);
③ publishArtifact(path, metadata)
Take a file or directory, hash it, attach metadata, write it to the registry, return the canonical URI. The URI is content-addressed: quilt://<namespace>/<name>:<sha256-prefix>. Same bytes, same URI, always. The metadata is signed by the publisher's key — agents can verify provenance without trusting the registry.
// spec const uri = await publishArtifact("./dist/classifier-dag.yaml", { name: "classifier-dag", version: "1.0.0", author: "team@quilt" }); // → "quilt://team/classifier-dag:a3f1b9c" (sha256-prefix) // Quilt — content-addressed storage, signed metadata import { publish } from '@quilt/sdk'; const uri = await publish({ path: './dist/classifier-dag.yaml', registry: 'quilt://team' });
④ publishRunTrace(trace)
Append a run trace to the immutable audit log. A trace is the complete record: which artifact ran, which inputs it used, the order and result of every cell evaluation, wall time, tokens consumed, model versions, hashes of the manifest and output. Once appended, the trace cannot be edited — only new traces can be appended.
// spec const uri = await publishRunTrace({ manifest_uri: "quilt://team/classifier-dag:a3f1b9c", run_id: "01HXY...", started: "2026-01-15T14:22:01Z", steps: [{ cell: "load", ok: true, ms: 128 }, { cell: "embed", ok: true, ms: 2410 }] }); // Quilt — engine.serialize() IS the trace (reactive history) const trace = engine.serialize(); // { nodes, edges, evals, timing, hashes } const uri = await publish({ path: trace, registry: 'quilt://traces' });
⑤ resolveTemplate(uri, ctx)
Substitute {{variables}} in a URI before resolution. This is the part that makes a registry composable rather than static: an artifact can declare quilt://team/classifier-dag:{{tag}} and the caller fills in { tag: 'a3f1b9c' } at run time. Pure function, no cache to invalidate — and the same shape works in cells (their derived_from can be templated too).
// spec resolveTemplate("quilt://demo/dataset:{{run_id}}", { run_id: "01HXY" }); // → "quilt://demo/dataset:01HXY" resolveTemplate("quilt://{{org}}/{{name}}:{{tag}}", { org: "team", name: "classifier", tag: "v1" }); // → "quilt://team/classifier:v1" // Quilt — 4 lines, used by resolveArtifact() before lookup export function resolveTemplate(uri, ctx) { return uri.replace(/\{\{(\w+)\}\}/g, (_, k) => { if (!(k in ctx)) throw new Error(`missing var: ${k}`); return ctx[k]; }); }
03How Quilt already implements this
The mapping is not 1:1 — Quilt has its own vocabulary, and the right way to read this table is "the existing concept is the realization of the spec concept". In every case, the Quilt equivalent was either already the right primitive or was a small, principled generalization of one.
| Spec concept | Quilt equivalent | How it works |
|---|---|---|
| Manifest (declarative primitive) | Quilt sheet (YAML) | A sheet is a list of named cells with kinds, formulas, and edges. The manifest schema is a strict subset of the sheet schema. |
| Container entrypoint | Cell evaluate() | No Docker needed. A cell is a function — call it, get a value. Process isolation is one engine, not one container. |
| Inputs / outputs | derived_from + reactive propagation | A cell declares which other cells it reads. The engine recomputes the closure when an input changes — no explicit "get input" call. |
| Pre / postconditions | kind: 'sensor' + validate listener | A sensor cell checks a precondition; a validate listener runs a function on the value before it propagates. Both fire before any consumer sees the value. |
| Rollback | @quilt/evolve revert step | The engine holds a reactive history. Revert is engine.load(snapshot) — one call, deterministic, no manual checkpointing. |
| model_requirements | kind: 'ai' + ai_kind + provider config | An ai cell declares its model, provider, and parameters. The engine routes the call to the right API key and tracks tokens. |
| Run trace | engine.serialize() + reactive history | Every cell evaluation is recorded. Serialize captures the full graph + history + hashes — the audit log is the engine state, not a separate write path. |
| Capability negotiation | @quilt/ai routes across 4 providers | Quilt already speaks OpenAI, Anthropic, DeepSeek, and z.ai. The router picks based on model + cost + availability — no agent has to encode this. |
| Agent substrate (the whole point) | @quilt/mcp exposes Quilt to ANY agent | MCP (Model Context Protocol) is the standard agent-to-tool bus. @quilt/mcp mounts the registry, validator, and runtime as MCP tools. |
Why this mapping works
Quilt was designed as a reactive graph runtime. The spec was designed as a portable interface to agent runtimes. They were not designed in coordination — but they describe the same thing. The reactive graph IS the substrate; the spec is just the surface area that makes it accessible to agents that don't want to learn Quilt's internals.
04The insight: reactive > imperative
The most-quoted sentence in this design is also the most undersold. If you internalize it, the rest of the spec falls out for free.
Don't fetch inputs, watch them change.
Don't checkpoint state, serialize the engine.
Before: the imperative agent
After: the reactive agent
The shift is small in code, but enormous in what becomes possible:
- Replay becomes free. The engine's reactive history IS the replay log. No separate trace to maintain.
- Lineage becomes free. Each cell knows its parents (declared in
derived_from). Provenance walks the graph. - Rollback becomes free. The engine's state is serializable.
engine.load(snapshot)restores any prior state exactly. - Composition becomes free. An artifact is a URI. A URI can be embedded in another artifact's manifest. No glue code.
- Provenance becomes free. The trace is the engine state. There is no other place to write to.
The one-sentence version
An imperative agent is a program that calls functions. A reactive agent is a graph that the engine evaluates. The graph is the artifact, the evaluation is the run, the trace is the audit. Three things, one source of truth.
05The QuiltAgent class
The whole spec, condensed into four methods. An agent is anything that can plan (decompose a goal into artifacts), execute (run them in the substrate), replay (re-run with new inputs), and inspect (look at what actually happened). The class below is a working reference implementation — about 60 lines on top of @quilt/sdk and @quilt/core.
// QuiltAgent — the 4-method interface every agent implements. // The substrate does the rest. import { Engine } from '@quilt/core'; import { resolveTemplate, resolveArtifact, validateManifest, publishRunTrace, publish, } from '@quilt/sdk'; export class QuiltAgent { constructor(private llm: LLM, private reg = 'quilt://team') {} // ① plan — goal in, manifest out. LLM picks artifacts & wires them. async plan(goal: string) { const catalog = await this.listCatalog(); const manifest = await this.llm.json({ system: `You are a planner. Pick artifacts and return a Quilt manifest.`, user: `Goal: ${goal}\nCatalog:\n${catalog}`, }); return { manifest, uris: manifest.cells.map(c => c.ref) }; } // ② execute — validate, resolve, run, trace. Four steps, all substrate. async execute(plan, inputs) { const v = validateManifest(plan.manifest); if (!v.ok) throw new Error(`manifest invalid: ${v.errors}`); await Promise.all(plan.uris.map(u => resolveArtifact(resolveTemplate(u, inputs)))); const engine = await Engine.fromManifest(plan.manifest); await engine.flush(); const trace_uri = await publishRunTrace(engine.serialize()); return { trace_uri, outputs: engine.outputs() }; } // ③ replay — same plan, different inputs. Reactive engine = free replay. async replay(plan, _prior, inputs) { return this.execute(plan, inputs); } // ④ inspect — what actually happened? Read the trace, walk the graph. async inspect(trace_uri: string) { const t = await resolveArtifact(trace_uri); return { manifest: t.manifest_uri, ms: t.duration_ms, cells: t.steps.map(s => ({ name: s.cell, ms: s.ms, ok: s.ok })), tokens: t.tokens, cost: t.tokens * 0.000002 }; } } // Usage: const agent = new QuiltAgent(gpt4); const plan = await agent.plan("classify these 1000 reviews"); const result = await agent.execute(plan, { dataset: "s3://..." }); const report = await agent.inspect(result.trace_uri);
That is the entire agent surface area. plan → execute → replay → inspect. Everything else — caching, retries, parallelism, rollback, lineage — is provided by the substrate for free, because the engine already implements it.
Why 4 methods, not 40
The spec is deliberately tiny. A 40-method API would be a tax on every agent; a 4-method interface is one any LLM can hold in context. The substrate does the heavy lifting so the agent stays focused on what to do, not how to do it.
06Playground — try the primitives
All three examples run in the browser. No server, no API keys. Paste a manifest (JSON), click a button, see the substrate work.
Live primitives
The point of these three boxes is not to be a full implementation — it is to show that the substrate's contract is small enough to demonstrate end-to-end in 50 lines of JS. When you read the SDK, you will find the same shape, just with the I/O filled in.
07Provenance-first UX
One backend, three surfaces. Every surface shows the same thing: where did this result come from? The TUI is for power users, the GUI is for the rest of us, the voice interface is for the visually impaired and the impatient. All three hit the same QuiltAgent instance — there is exactly one source of truth.
▣ TUI · terminal
◫ GUI · browser
◉ Voice · spoken
The single contract
All three surfaces serialize to the same RunResult. The TUI renders it as monospace, the GUI as a graph, the voice agent as natural language. None of them invent new state. That is the whole point of having a substrate — one source of truth, many ways to read it.
08Roadmap
Where we are, where we are going. The first three rows are shipped; the rest are in flight.
resolveArtifact, validateManifest, publishArtifact, publishRunTrace, resolveTemplate.quilt validate CLInpx quilt validate manifest.yaml — schema check + precondition probe + URI reachability. Returns a structured report.If you build on the substrate, your work goes here. The next row could be yours — the registry is open, the schema is open, the SDK is open.
Where to go from here
Three doors, depending on what brought you here.