Liking cljdoc? Tell your friends :D

Getting Started

This guide uses only the public API and the fake adapter, so it is safe to run offline and does not require provider credentials. The examples assume a normal Clojure REPL with this repository on the classpath.

The public namespace is:

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

The agent-operable CLI is:

clojure -M:cli <command> [options] [args]

Use the API when embedding the engine in Clojure. Use the CLI when an agent or script should drive a durable session from the shell while a human inspects structured artifacts.

flowchart LR
  Goal["I want to use fractal-engine"] --> Embed{"Embedding in Clojure?"}
  Embed -- yes --> API["Use fractal.engine.api"]
  Embed -- no --> CLI["Use clojure -M:cli"]
  API --> Fake["Start with fake adapter"]
  CLI --> Config["Start with config file"]
  Fake --> Durable["Add :store :sqlite when state must survive"]
  Config --> Durable
  Durable --> Live["Add :adapter :sdk only when ready for live calls"]

1. Run a Fake-Adapter Turn

The fake adapter scripts what the model will say. The engine still runs the real session loop: it appends messages and events, extracts fenced Clojure, evaluates it in SCI, and returns only when the code calls FINAL.

(def cfg
  (fe/make-config
   {:adapter :fake
    :model "fake-model"
    :capability :default
    :fake/respond
    (fe/responder
     [[:default "```clojure\n(FINAL {:answer 42})\n```"]])}))

(def session (fe/start-session! cfg))
(def result (fe/run-turn! session "What is 6 times 7?"))

(:status result)
;; => :final

(:turn/final-value result)
;; => {:answer 42}

(fe/stop-session! session)

Important details:

  • make-config validates runtime options but does not construct an adapter.
  • start-session! is the composition root that builds the store, adapter, and SCI context.
  • run-turn! blocks until FINAL or a terminal modeled outcome.
  • The returned :turn/final-value is hydrated through the payload layer.

2. Inspect the Session

Read calls do not make provider requests:

(def session
  (fe/start-session!
   (fe/make-config
    {:adapter :fake
     :model "fake-model"
     :fake/respond
     (fe/responder
      [[:default "```clojure\n(FINAL :ok)\n```"]])})))

(fe/run-turn! session "go")

(select-keys (fe/progress session)
             [:session/id :session/status :turn-count :last-event-id])

(count (fe/event-stream session))

(map :event/type (fe/events-since session 0))

Use view when you need the full folded state, including messages, turns, evals, heads, current-head, lineage edges, counters, and events:

(select-keys (fe/view session) [:current-head :heads :edges])

(fe/stop-session! session)

Large values may be represented internally as payload refs. Use read-payload on values returned by readback surfaces when you need explicit hydration; non-refs pass through unchanged.

3. Drive A Durable Session With The CLI

The CLI defaults to JSON and expects explicit config and session ids for automation. init creates a small fake-adapter config file that uses a durable SQLite store under an ignored .fractal/ directory:

clojure -M:cli init \
  --config fractal.edn \
  --store-dir .fractal/sessions/demo \
  --session demo

Run one turn, then read the compact report:

clojure -M:cli run \
  --config fractal.edn \
  --session demo \
  --message "return a small value" \
  --pretty

clojure -M:cli report \
  --config fractal.edn \
  --session demo \
  --pretty

Continue the same durable session with turn:

clojure -M:cli turn \
  --config fractal.edn \
  --session demo \
  --message "continue from the same REPL vars" \
  --pretty

Use --edn for exact Clojure-shaped payload refs:

clojure -M:cli turns --config fractal.edn --session demo --edn

The command inventory and output contract are documented in Agent Control Plane.

4. Reopen a Durable Session

The default store is in-memory. Use :store :sqlite with a writable relative store directory when the session should survive process restart.

(def store-dir "var/demo-store")

(def durable-cfg
  (fe/make-config
   {:adapter :fake
    :model "fake-model"
    :capability :default
    :store :sqlite
    :store/dir store-dir
    :fake/respond
    (fe/responder
     [["define" "```clojure\n(def remembered 7)\n(FINAL :defined)\n```"]
      ["use" "```clojure\n(FINAL (* remembered 6))\n```"]])}))

(def first-handle (fe/start-session! durable-cfg {:id "demo"}))

(fe/run-turn! first-handle "define the value")
;; => {:status :final, ... :turn/final-value :defined, ...}

(fe/close-session! first-handle)

(def reopened (fe/resume-session! durable-cfg "demo"))

(:turn/final-value
 (fe/run-turn! reopened "use the remembered value"))
;; => 42

(fe/close-session! reopened)

Durable state is not reconstructed by replaying provider calls. The SQLite event store is folded, payloads are read through the BlobStore, and the live REPL vars are restored from the published current head when one exists.

5. Try the Recursive Harness Offline

Set :harness :rlm to inject the recursive host functions into the model's session REPL. The public API call shape is unchanged.

(def recursive-cfg
  (fe/make-config
   {:adapter :fake
    :model "fake-model"
    :harness :rlm
    :capability :default
    :fake/respond
    (fe/responder
     [["Assigned task"
       "```clojure\n(FINAL {:child-answer 41})\n```"]
      ["delegate"
       "```clojure\n(FINAL (:rlm/value (rlm \"compute child answer\")))\n```"]])}))

(def root (fe/start-session! recursive-cfg))

(:turn/final-value (fe/run-turn! root "delegate to a child"))
;; => {:child-answer 41}

(fe/stop-session! root)

Inside the model's REPL:

  • lm makes one bounded leaf model call.
  • map-lm fans out leaf calls up to the configured fan-out cap.
  • rlm spawns a fresh child session and returns an envelope.
  • map-rlm runs independent child sessions and preserves input order.
  • attach-rlm derives a fresh child from a selected immutable source head without advancing the source session.
  • FINAL is the only way to return the turn value to the caller.

Child accounting lives in the child envelope's :rlm/meta; the root turn's usage and cost remain self-only.

The same harness can be driven through the CLI by setting :harness :rlm in the selected config profile and running clojure -M:cli run or turn.

6. Add A Custom SDK Surface

SDK surfaces let an embedder expose a real world/API to the model as namespaced Clojure functions. The functions are ordinary trusted host code. The engine owns validation, SCI injection, prompt truth, capability gating, cache placement, and durable resume stamps.

flowchart LR
  Host["Embedder code"] --> Desc["Surface descriptor"]
  Desc --> Gate[":surface/fns gate"]
  Gate --> Repl["Session SCI namespace"]
  Gate --> Prompt["Generated prompt cards"]
  Repl --> Call["(repo/list-files {})"]
  Prompt --> Model["Root and child model requests"]
  Desc --> Stamp["Public surface stamp"]
  Stamp --> Resume["Resume compatibility check"]

A minimal fake-backed example:

(def repo-surface
  {:surface/id :repo
   :surface/version 1
   :surface/prompt "Use repo/list-files before guessing available files."
   :surface/prompts
   {:request (fn [_ctx]
               "The current task is allowed to inspect only repo/list-files and repo/read-file.")
    :leaf "Leaf calls may classify bounded snippets returned by repo/read-file."}
   :surface/namespaces
   {'repo {'list-files {:doc "Return known file names."
                        :arglists '([opts])
                        :fn (fn [_opts] ["README.md" "src/core.clj"])}
           'read-file  {:doc "Return bounded file text."
                        :arglists '([path opts])
                        :fn (fn [path _opts]
                              (case path
                                "README.md" "# Demo\n"
                                "src/core.clj" "(ns demo.core)\n"))}}}})

(def surface-profile
  {:capability/name :repo-demo
   :cap/fs-read :deny
   :cap/fs-write :deny
   :cap/shell :deny
   :cap/network :deny
   :ns/granted '#{clojure.core clojure.string clojure.edn
                  clojure.set clojure.walk}
   :cap/java-classes {}
   :engine-fns #{:FINAL :inspect}
   :surface/fns '#{repo/list-files repo/read-file}})

(def cfg
  (fe/make-config
   {:adapter :fake
    :model "fake-model"
    :capability surface-profile
    :surfaces [repo-surface]
    :fake/respond
    (fe/responder
     [[:default
       "```clojure\n(FINAL {:files (repo/list-files {})\n        :readme (repo/read-file \"README.md\" {})})\n```"]])}))

(def h (fe/start-session! cfg))

(:turn/final-value (fe/run-turn! h "inspect the repo surface"))
;; => {:files ["README.md" "src/core.clj"], :readme "# Demo\n"}

(fe/stop-session! h)

Surface calls must be qualified, for example (repo/read-file "README.md" {}). Configured functions that are not listed in :surface/fns do not exist in SCI. Children inherit the same configured surfaces, but their capability profile is clamped by set intersection, so a child cannot gain a function the parent lacked.

Because a surface descriptor contains functions, this is an in-process SDK extension point. A plain EDN CLI config file cannot manufacture :fn or :factory values; embedding products should load surfaces in code and call the API, or provide their own CLI wrapper around that configured engine.

Stable surface prompt text is rendered after the harness doctrine and before overlays. Dynamic :request prompt text is rendered as transient request context; it is not stored in the durable transcript, and cache metadata is adjusted so the dynamic tail is not treated as a stable cache anchor. Dynamic :leaf prompt text is appended to leaf system prompts. Durable sessions persist only public surface stamps, never function objects; resume-session! fails with :surface/mismatch when configured stamps differ from the stored session stamps.

7. Move Toward Provider-Backed Runs

Provider-backed runs use the same API with :adapter :sdk, a model id, and provider configuration appropriate for the adapter. Keep fake-adapter tests as the first validation path, then add live-provider checks only when credentials and cost controls are intentionally configured.

The important boundary is architectural: application code should depend on fractal.engine.api, not on the internal adapter, store, kernel, or recursion namespaces.

For CLI-driven live runs, keep provider credentials outside tracked files and use a config profile with the engine's runtime governor configured explicitly: :max-steps, :max-turns, :call-timeout-ms, :max-fanout, :fanout-pool, :leaf-concurrency, and the hard context-window threshold.

Those controls bound execution. Provider usage/cost records, when available, are for observability and reporting.

8. Read Deeper

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