Liking cljdoc? Tell your friends :D

fractal-engine

A small recursive language-model compute engine. A model drives a persistent Clojure REPL: it writes fenced Clojure, the host evaluates it and returns a compact observation, and the loop repeats until the model calls (FINAL value). Some of the functions the model can call are themselves language models — so a problem can be decomposed into sub-problems, each solved by a fresh recursion of the same loop.

;; the model writes this; the host evaluates it and feeds back the result
(def files (->> (file-seq (io/file "src")) (filter #(.endsWith (str %) ".clj")) (mapv str)))
(def summaries (map-lm files "Summarize this file's responsibility in one line." :string))
(FINAL {:n (count files) :summaries summaries})

Why this shape? Large contexts are expensive and lossy. Instead of stuffing everything into one window, fractal-engine gives the model a programming environment and lets it decide how to read its own input — slicing with ordinary code, judging bounded pieces with cheap model calls, and handing whole sub-problems to fresh recursions. It is built in the spirit of Recursive Language Models (Alex Zhang et al.).


Table of contents

Get it running: Requirements · Install · Quickstart (no API keys) · Where runs live · Use as a Clojure dependency · The fractal CLI · codebrain · Going live: providers · Troubleshooting

Understand it: How the loop works · The six functions · Artifacts & the journal · The trust layer · Resume & fork · Sandboxing · Architecture · Evaluations · Anti-goals · Relevant reading

Deep docs: docs/CONCEPTS.md · docs/API.md · docs/CLI.md · docs/ARCHITECTURE.md · docs/CODEBRAIN.md · docs/EVALS.md


Requirements

You need two things, both free and cross-platform (macOS, Linux, Windows/WSL):

WhatCheck itGet it
JDK 21+A Java runtime (the engine evaluates Clojure on the JVM)java -versionTemurin, Homebrew brew install openjdk@21, or your package manager
Clojure CLIThe clojure / clj build+run toolclojure --versionclojure.org/guides/install_clojure

There is no native binary on purpose — the engine compiles and runs model-generated code at runtime, so it ships as a JVM uberjar. Tested on OpenJDK 21 and Clojure CLI 1.12.

Install

The engine runs on any platform with a JDK 21+ — it ships as a single self-contained uberjar (a native binary is out: the core loop evals model-emitted Clojure at run time, which needs the JVM and the Clojure compiler present).

Download the prebuilt jar (no build)

Grab fractal.jar from the latest release:

mkdir -p ~/.local/bin
curl -fsSL -o ~/.local/bin/fractal.jar \
  https://github.com/DeadMeme5441/fractal-engine/releases/latest/download/fractal.jar

# run it directly…
java -jar ~/.local/bin/fractal.jar help

# …or drop a tiny wrapper on your PATH so `fractal` just works:
printf '#!/usr/bin/env bash\nexec java -jar "$HOME/.local/bin/fractal.jar" "$@"\n' > ~/.local/bin/fractal
chmod +x ~/.local/bin/fractal
fractal help

Build from source

Clone, build the uberjar, and put fractal on your PATH:

git clone https://github.com/DeadMeme5441/fractal-engine.git
cd fractal-engine

clojure -T:build uber            # -> target/fractal.jar   (prints: built target/fractal.jar)

# put `fractal` on your PATH (any dir on your PATH works; ~/.local/bin is common)
mkdir -p ~/.local/bin
ln -sf "$PWD/bin/fractal" ~/.local/bin/fractal

fractal help                     # should print the verb list

bin/fractal prefers the built jar (fast startup; runs in whatever directory you invoke it from) and resolves symlinks, so a fractal symlink anywhere on your PATH still finds the repo. If the jar isn't built it transparently falls back to running from source.

Prefer not to build? Skip the uberjar and run the engine straight from source (slower startup, and it runs from the repo directory):

clojure -M -m fractal-engine <verb> ...      # e.g. clojure -M -m fractal-engine help

Quickstart (no API keys)

The default provider is an offline scripted fake — no keys, no network, no spend. Run a scripted task end to end:

fractal run "Define x and return it." --fake-script simple

You should see something like:

run session-3269ffb5-65ba-41c8-921b-c4a1d8beebd8
● {:answer 42}   $? · 2 calls
  next: fractal show session-3269ffb5-65ba-41c8-921b-c4a1d8beebd8   ·   fractal verify session-3269ffb5-65ba-41c8-921b-c4a1d8beebd8

That is a full run: the green means the model reached (FINAL …), {:answer 42} is the final value, and $? · 2 calls is the spend summary — $? because the scripted provider has no real pricing (a live run shows a dollar amount here).

Did it work? Two checks:

ls .fractal/                # a run directory appeared here (see "Where runs live")
fractal ls                  # ○ session-…  s2 c0 final

Now look inside what it did:

fractal show <run>          # node detail: steps, leaves, children, final
fractal tree <run>          # the whole addressable run tree

<run> is the session-… name printed above (or copy it from fractal ls). fractal show prints the run's steps — the exact Clojure the model wrote and the observation the host fed back at each step — ending in the FINAL value.

Run the test suite to confirm a healthy checkout (all offline, no keys):

clojure -M:test                # Ran 49 tests containing 476 assertions. 0 failures, 0 errors.

Other offline scenarios are available via --fake-script: simple, lm, map-lm, rlm, map-rlm, multi-turn-chat, and more (see src/fractal_engine/scripts.clj).

Where runs live: .fractal/

Every session writes a directory. Like git and bd, fractal keeps its data in a .fractal/ directory in the directory you invoke it from, and finds it the same way git finds .git:

  • If a .fractal/ already exists in the current directory or any ancestor, that one is reused — so running from a subdirectory still lands in the project's runs.
  • Otherwise a fresh .fractal/ is created in the current directory on first write.

So cd ~/some-project && fractal run "…" just works, and keeps that project's runs with that project. Point it somewhere else for a single command with --runs-dir:

fractal run "…" --runs-dir /tmp/scratch-runs       # write here instead of ./.fractal
fractal ls       --runs-dir /tmp/scratch-runs       # read from there too

.fractal/ is git-ignored by default. A <run> argument is either a path (.fractal/foo) or a bare name resolved under the runs dir (foo.fractal/foo).

Use as a Clojure dependency

The engine is published to Clojars. Add it to deps.edn:

{:deps {net.clojars.deadmeme5441/fractal-engine {:mvn/version "0.1.1"}}}

The CLI is one consumer of the engine. Clojure applications should prefer the stable facade namespace, fractal-engine.api, instead of reaching into runtime namespaces such as process, session, projection, or provenance.

(require '[fractal-engine.api :as fe])

(def cfg
  (fe/config {:runs-dir ".fractal"
              :models {:root  {:provider :scripted :model "scripted"}
                       :leaf  {:provider :scripted :model "scripted"}
                       :child {:provider :scripted :model "scripted"}}}))

(def s
  (fe/start-session! cfg {:id "demo"
                          :dir ".fractal/demo"
                          :overlay "Additional application role instructions can go here."}))

(def result
  (fe/run-turn! s "Define x and FINAL {:answer 42}."))

(fe/stop-session! s)
(fe/load-node (:dir result))

The overlay is session-level specialization: it is appended once to the base system message and carried in the transcript. It does not add functions to the model-facing surface or change engine behavior. That surface remains exactly FINAL, lm, map-lm, rlm, map-rlm, and attach-rlm.

Applications may put their own Clojure namespaces on the classpath and ask the model, through the overlay or task prompt, to require and use them. Run artifacts stay engine-shaped and can be read with fe/load-node, fe/load-at, fe/tree, fe/journal-events, and the claim/provenance helpers. Full reference: docs/API.md.

The fractal CLI

fractal is the engine's use surface — modeled on tools like bd: short verbs, a positional address instead of flag ceremony, run-name resolution, and output that prints the next command to run. One grammar covers both halves of the loop — driving the engine and reading what it did. Every verb takes --json; exit codes mean something. Full reference: docs/CLI.md.

Drive (do work)

fractal chat [run]            # talk to a persistent, resumable session (the "brain")
fractal run    "<task>"       # one-shot; prints a run handle to chain into a read verb
fractal resume <run> "<task>" # continue a saved session from its snapshot
fractal fork   <run> "<task>" # branch a session at a turn

fractal chat is the headline. It holds one live session and runs each message as a turn — REPL vars persist in memory, the journal grows, and a live ◐ thinking… line shows children/steps/leaves as the engine works. Leave with /quit; come back with fractal chat <run>.

Read (look inside its head)

fractal show   <run> [node]   # node detail — the hub; node defaults to root
fractal tree   <run>          # the full addressable run tree
fractal prime  <run>          # compact "what is this run"
fractal ls                    # list runs
fractal verify <run> [node]   # claim-vs-evidence (the confabulation backstop)
fractal trace  <run> [node]   # claim provenance
fractal cost   <run>          # spend breakdown
fractal leaves <run> [node]   # leaf inputs/outputs
fractal step   <run> [node] N # one step, in full
fractal stream <run>          # journal events as JSONL

A node address is root, child-0001, or child-0001/child-0004 — the leading root/ is implied. Drilling is just following the addresses a node view prints for its children.

Exit codes: 0 final · 1 error · 2 no-final · 3 timeout · 5 confabulation suspected. So you can gate on them: fractal verify <run> --deep --root . && deploy.

codebrain — a code-discovery brain

fractal codebrain is a thin product surface that points the engine at a codebase: it builds itself a repo map (by fanning children over the subsystems, not by a regex dump), keeps it on disk like bd keeps its db, and then answers a coding agent's questions about the code with small, cited EDN — so the agent spends its context on the change, not on reading the tree.

fractal codebrain init --path ./src --provider vertex-gemini --model gemini-3.1-pro-preview
fractal codebrain ask  "Where are CLI verbs registered and what's the handler contract?"
fractal codebrain map        # show the persisted map  ·  status  # freshness + HEAD

Born once (the build is the amortized cost); each ask resumes the warm brain and runs cheap. Full setup, auth for every provider, and the answer shape: docs/CODEBRAIN.md.

Going live: providers

Provider calls go through clojure-llm-sdk. Select provider and model per role — root / leaf / child can differ (a common, cheap split is a strong root with cheaper children and leaves):

fractal run "Map this repo's subsystems with evidence." \
  --provider vertex-gemini       --model gemini-3.1-pro-preview \
  --leaf-provider vertex-gemini  --leaf-model gemini-3.1-flash-lite-preview \
  --child-provider vertex-gemini --child-model gemini-3.5-flash \
  --max-turns 15 --call-timeout-ms 120000

Credentials come from environment variables (an ignored .env is read for local dev — never commit secrets). Two non-obvious provider facts that will cost you time if you miss them:

  • Codex OAuth is the provider keyword codex-backend (plain codex is the API-key path). It reads ~/.codex/auth.json.
  • Vertex Gemini (vertex-gemini) needs GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION exported into the JVM environment (the .env loader does not push them to System/getenv), plus Application Default Credentials (gcloud auth application-default login).

Root-model strength is decisive. A weak root will confabulate past its own correct observations. Don't put a mini model at the root when you care about the answer.

Live runs cost money and can hang. There is no engine-level budget/timeout governor yet. Always leash live runs: --call-timeout-ms, --max-turns, --max-fanout, and run them in the background with monitoring.

Evaluations

The repository includes a small eval harness under evals/. It is an external consumer of the engine behind the :evals deps alias, not part of the shipped runtime or model-facing surface.

The current public v17 run covers OOLONG-synth and FanOutQA long-context examples with a strong-root / cheap-leaf model split. Headline results:

benchmarknheadlinespend
OOLONG-synth15exact accuracy 0.867$3.3989
FanOutQA15loose accuracy 0.695; semantic audit 11/15$4.5360

See docs/EVALS.md for exact commands, aggregate outputs, scorer caveats, and what the results do and do not establish.

Troubleshooting

SymptomCause / fix
fractal: command not foundThe symlink isn't on your PATH. Confirm the target dir is on PATH (echo $PATH), or just run ./bin/fractal from the repo.
java: command not found / UnsupportedClassVersionErrorNo JDK, or older than 21. bin/fractal runs the jar with java. Install JDK 21+ and ensure java -version reports 21 or newer.
Could not find or load main class / jar missingThe uberjar isn't built. Run clojure -T:build uber (creates target/fractal.jar). Without it, bin/fractal falls back to source — that needs clojure on your PATH.
no such run: <name>Runs resolve under .fractal/ of the directory you're in (or an ancestor). Run fractal ls to see what's there, cd to the project, or pass --runs-dir <dir>.
Live provider: unauthorized / auth errorscodex-backend needs ~/.codex/auth.json; vertex-gemini needs GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION exported into the JVM env and valid ADC. See Going live.
Vertex first-call hang / EOFA known cold-start transport hiccup. Retry is on by default and the SDK retries transient EOF/timeout with backoff; if a call truly hangs, your --call-timeout-ms bounds the whole retry loop (total wall-clock).
A live run hangs for minutesThere's no governor yet — kill it and re-run leashed: --call-timeout-ms, --max-turns, --max-fanout, in the background.

How the loop works

  1. Conversation messages are the model's working transcript.
  2. The model replies with one or more fenced ```clojure blocks.
  3. The host evaluates them, in order, in a persistent namespace (vars survive across steps and turns).
  4. The host appends a compact observation message (values are projected, not dumped).
  5. Steps repeat until the model calls (FINAL value), which ends the turn and returns value.

A session is long-lived. (FINAL value) completes the current turn; it does not end the session — the same history, REPL vars, and cache scope are available to the next turn. That is what makes a session a persistent brain you can keep talking to.

The six functions

The entire model-facing surface is six functions, plus ordinary Clojure:

functionkindwhat it does
(FINAL value)end the current turn and return value to the caller
(lm input query [mode])probabilisticone bounded input → one model judgment (:string/:edn)
(map-lm inputs query [mode])probabilisticlm mapped over up to 50 inputs, in parallel
(rlm task)recursiverun this whole loop on a sub-problem; returns its FINAL
(map-rlm tasks [shared])recursiverlm over up to 50 independent sub-problems, in parallel
(attach-rlm path task [opts])recursiveresume a prior session as a child, then run task

Everything is input -> processing -> output; only the kind of processing varies — deterministic (plain Clojure), probabilistic (a leaf, lm/map-lm, whose body is a model), or recursive (a child, rlm/map-rlm, a full recursion of the loop). A leaf is the non-recursive base case. There is no magic context variable — working state lives in REPL vars the model defines with def. The root, every child, and every leaf run the same loop; there is no separate "planner" or "executor." See docs/CONCEPTS.md for the model in depth.

Artifacts & the journal

Every session writes a directory under .fractal/. The source of truth is events.ednl, an append-only event log (one EDN form per line); everything else is a projection of it, materialized at turn boundaries for convenient reading.

.fractal/<session-id>/
  events.ednl        # append-only journal — the source of truth
  session.edn  messages.edn  turns.edn  evals.edn  calls.edn  snapshots.edn
  final.edn  usage.edn  tree.edn        # derived views
  blobs/                               # large values, referenced by SHA-256
  children/child-0001/ …               # child sessions, same shape, recursively

The load-bearing invariant is results, not recipes: an event carries the outcome of work (inline, or a blob ref for large values), so folding the journal reconstructs the full state without ever re-invoking a model. Reading, resuming, and inspecting are all pure folds that cost zero provider calls. Mid-turn, the journal is authoritative (the .edn projections are only rebuilt at boundaries) — which is why fractal's read verbs fold events.ednl rather than trusting the projections.

The trust layer

A model's FINAL value is a claim, not a fact. fractal verify makes claims auditable in two layers:

  1. Grep floor (free, default). Extract the code symbols a claim cites as evidence and check they actually occur in the cited file. Catches a fabricated citation instantly. Cited paths are resolved against --root <repo>.
  2. Engine judge (--deep). Hand the claims back to the engine as a fresh task — "read the cited code and decide whether it genuinely supports the claim; spawn a child or use leaves, your call; be adversarial." The engine reads the real source and returns :supported / :refuted per claim.

The floor and the judge are complementary: grep answers "are the cited symbols real?"; the judge answers "does the code actually mean what's claimed?" In practice the judge catches confabulations the grep waves through. Details in docs/CONCEPTS.md.

fractal verify <run> child-0001 --root /path/to/repo          # grep floor
fractal verify <run> child-0001 --root /path/to/repo --deep \
  --provider vertex-gemini --model gemini-3.1-pro-preview     # + engine judge

Resume & fork

Snapshots are written at graceful turn boundaries: message history plus EDN-safe REPL vars (non-EDN values are recorded as unresumable, never silently dropped). resume restores that state into a fresh namespace, reinstalls the runtime functions, and runs a new turn. fork does the same into a new directory, branching the lineage.

fractal resume <run> "Use the var you defined and FINAL the result."
fractal fork   <run> "Try a different approach." --turn 2

Sandboxing

The engine evaluates model-generated Clojure in a live JVM — it can read files, spawn subprocesses, and use the network. That power is the point (it is how a node inspects a codebase and reaches its provider), but it means you must treat the model's code as code you are running locally.

There is no true in-process sandbox on a modern JVM: the SecurityManager was disabled in JDK 24 (JEP 486), and an interpreter sandbox (e.g. SCI) can't preserve the engine's real-var / snapshot model. Isolation is therefore a process/OS-level concern. bin/fractal-sandboxed runs the engine under a best-effort OS sandbox:

  • macOSsandbox-exec with sandbox/macos.sb (Seatbelt). Tested.
  • Linuxbwrap (bubblewrap). Written but untested; review before relying on it.

It is a safety net, not a prison: reads/subprocesses/compute/network stay open; only writes are confined to the run workspace and temp. It does not filter the network — a node that reads a file can send it to your provider (its purpose) or elsewhere. For untrusted inputs, use a hardened tier: a network-filtering sandbox such as Anthropic's sandbox-runtime (allowlist the provider domain) or a container with a locked-down network namespace.

Architecture

The codebase is decomplected by concern; only the compute engine lives in core namespaces. Full map in docs/ARCHITECTURE.md.

layerresponsibilitynamespaces
compute enginethe agent + persistent REPL loop, fanout, child/attachprocess runtime prompt concurrent call
journal & projectionsappend-only events + pure folds into viewsjournal event artifacts
persistencesnapshot / restore / resume / fork / lineagesnapshot resume session
providerthe LLM adapter boundaryprovider
read surfacejournal-folding projection + the trust layerprojection provenance render
public APIstable Clojure facade for library consumersapi
productthe fractal CLIagentcli cli

Anti-goals

The core runtime does not include persistent-memory databases, vector search, workflow templates, task schemas, repository analyzers, MCP-server concepts, a web UI, deterministic planner layers, or hidden convenience functions. Those belong in layers around the kernel. The model-facing surface is exactly the six functions — kept small on purpose.

Relevant reading

  • Recursive Language Models — Alex Zhang et al.: the idea this engine is built in the spirit of (REPL-as-context, recursive self-calls). Repo.
  • AGENTS.md — the design boundary and invariants for contributors and agents.
  • docs/CONCEPTS.md, docs/API.md, docs/CLI.md, docs/ARCHITECTURE.md — deep dives.

License

Apache-2.0.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close