by deepfates
the extensible, customizable, self-documenting, real-time multi-agent computing environment
# Add to your Claude Code skills
git clone https://github.com/deepfates/cantripLast scanned: 6/10/2026
{
"issues": [],
"status": "PASSED",
"scannedAt": "2026-06-10T08:08:35.156Z",
"npmAuditRan": true,
"pipAuditRan": true
}No comments yet. Be the first to share your thoughts!
30 days in the Featured rail · terms & refunds
A spellbook for summoning entities from language. Disguised as an Elixir agent runtime.
Putting language in a loop can make it come alive. You say words, the words change the room, the room changes you, you say different words. We call it chanting, and it is one of the oldest tools of magic.
An agent is the same shape. The model predicts a token; put it in a loop with an environment, and something emerges that wasn't in the instructions. Cantrip names the parts:
A cantrip is the reusable value that binds an LLM, an identity, and a
circle. When you cast or summon it, an entity appears in the loop. The
action space is the formula:
A = M ∪ G − W
mix deps.get
cp .env.example .env
mix cantrip.cast "explain what a cantrip is"
That's a bare conversation cantrip with a done gate. For the full
code-medium coordinator that lives in your codebase:
mix cantrip.familiar
mix cantrip.familiar "summarize the loom storage modules"
mix cantrip.familiar --acp
The same package primitives cover several distinct shapes:
A code-medium cantrip that inspects a workspace through scoped filesystem
gates and leaves a JSONL loom behind. The entity thinks in Elixir, uses
list_dir, search, and read_file as host functions, and records every
turn:
{:ok, llm} = Cantrip.LLM.from_env()
root = File.cwd!()
{:ok, cantrip} =
Cantrip.new(
llm: llm,
identity: %{
system_prompt: """
You are a careful codebase analyst. Inspect the workspace through the
available gates and call done with a concise findings list.
"""
},
circle: %{
type: :code,
gates: [
:done,
%{name: "list_dir", dependencies: %{root: root}},
%{name: "search", dependencies: %{root: root}},
%{name: "read_file", dependencies: %{root: root}}
],
wards: [%{max_turns: 8}, %{sandbox: :port}, %{code_eval_timeout_ms: 5_000}]
},
loom_storage: {:jsonl, "tmp/cantrip-analysis.jsonl"}
)
{:ok, result, _next, loom, meta} =
Cantrip.cast(cantrip, """
Find the modules responsible for loom storage and summarize their
persistence choices, including any operational risks a deployer should know.
""")
Provider configuration is routed through ReqLLM:
CANTRIP_LLM_PROVIDER=openai_compatible
CANTRIP_MODEL=gpt-5-mini
CANTRIP_API_KEY=sk-...
CANTRIP_BASE_URL=https://api.openai.com/v1
Cantrip.FakeLLM scripts deterministic responses for tests.
Use summon when an entity should keep process-owned state across multiple
intents:
{:ok, pid} = Cantrip.summon(cantrip)
{:ok, _first, _next, _loom, _meta} = Cantrip.send(pid, "Map the storage modules.")
{:ok, second, _next, loom, _meta} =
Cantrip.send(pid, "Continue from there: compare JSONL and Mnesia.")
Use ordinary cantrips as children. Results return in request order; each child also produces a loom.
{:ok, jsonl_reader} =
Cantrip.new(
llm: llm,
identity: %{system_prompt: "Summarize the JSONL storage implementation."},
circle: %{type: :conversation, gates: [:done], wards: [%{max_turns: 5}]}
)
{:ok, mnesia_reader} =
Cantrip.new(
llm: llm,
identity: %{system_prompt: "Summarize the Mnesia storage implementation."},
circle: %{type: :conversation, gates: [:done], wards: [%{max_turns: 5}]}
)
{:ok, summaries, _children, _looms, _meta} =
Cantrip.cast_batch([
%{cantrip: jsonl_reader, intent: "Focus on lib/cantrip/loom/storage/jsonl.ex"},
%{cantrip: mnesia_reader, intent: "Focus on lib/cantrip/loom/storage/mnesia.ex"}
])
The Familiar is the batteries-included coordinator for codebase work. It observes the workspace, reasons in Elixir, delegates to child cantrips, and persists its loom.
{:ok, familiar} = Cantrip.Familiar.new(llm: llm, root: File.cwd!())
{:ok, report, _next, _loom, _meta} =
Cantrip.cast(familiar, "Inspect this repo and report the package shape.")
Hot-loading is opt-in. Pass evolve: true to include compile_and_load
and an exact allowlist for Elixir.Cantrip.Hot.Tally. Be careful what you
wish for; the Familiar is minimally warded.
Cantrip.new/1 builds a reusable cantrip value from an LLM tuple, identity,
circle, loom storage, retry policy, and folding options.
Cantrip.cast/3 summons a one-shot entity for one intent:
{:ok, result, cantrip, loom, meta} =
Cantrip.cast(cantrip, "Analyze this data", stream_to: self())
Cantrip.cast_batch/2 runs child cantrips concurrently and returns results
in request order:
{:ok, results, children, looms, meta} =
Cantrip.cast_batch([
%{cantrip: analyst, intent: "Read chapter one."},
%{cantrip: analyst, intent: "Read chapter two."}
])
Cantrip.cast_stream/2 returns {stream, task} for event consumers.
Cantrip.summon/1 and Cantrip.send/3 keep a supervised entity process
alive across multiple intents.
Cantrip.Loom.fork/4 replays a loom prefix and branches from a prior turn.
See docs/public-api.md for a task-oriented API guide.
The medium is the inside of the circle — what the entity thinks in.
Conversation. The LLM receives gates as tool definitions and responds with structured calls. Right when the work IS speech: interpretation, judgment, naming.
Code. The entity writes Elixir. Bindings persist across turns. Gates
are injected as functions; loom is available as data. Right when the work
is composition: gathering pieces, transforming them, aggregating, fanning
out. Children are constructed through the public package API:
data = read_file.(path: "metrics.txt")
done.("Read #{byte_size(data)} bytes")
Plain code-medium cantrips use the safe port boundary by default: LLM-written
Elixir is evaluated by Dune inside a child BEAM process, while gates, child
cantrip API calls, stdio, and hot-loading are resolved through explicit
parent/child protocol messages. Use %{sandbox: :port} when you want that
default boundary to be explicit in a circle. The Familiar defaults to
sandbox: :unrestricted for trusted operator-local coding work so native
Elixir affordances such as binding/0 and Code.fetch_docs/1 match what its
prompt teaches. Use sandbox: :port_unrestricted only when you explicitly
want raw Elixir in the child process, sandbox: :dune when you want
in-process language restriction with a deliberately smaller binding surface
(see docs/port-isolated-runtime.md for the
divergence — entity prompts need to match the variant in use), or sandbox: :unrestricted for trusted local development in the host BEAM.
Child-origin atoms outside Cantrip's wire vocabulary cross the port boundary
as strings, which keeps hot-loaded child code from forcing new atoms into the
parent BEAM.
Bash. The entity writes shell commands. Each command runs in a fresh
OS-sandboxed subprocess from the configured cwd. Shell state does not persist.
Filesystem writes are denied except under %{bash_writable_paths: [...]}, and
network is off unless %{bash_network: :on} is declared. Declared gates are
projected as commands at the front of PATH: read_file README.md,
list_dir ., search pattern lib, mix test, and cantrip_done "answer"
for the done gate. SUBMIT: output still works for shell-only answers. The
Bash sandbox is release-tested against representative local shell workloads
(git, make, jq, redirects through /dev/null, and common
find/sed/grep pipelines); that workload suite is the support contract
for expanding the adapter configuration over time. The workload tests opt into
%{bash_network: :on} so GitHub-hosted runners can execute bubblewrap even
when they cannot create a network namespace; separate tests pin the default
network-deny command shape.
Built-in gates close over construction-time dependencies and produce observations the entity reads as data:
done(answer) — terminate with the final answerecho(text) — visible observationread_file(%{path}) — read a file under :rootlist_dir(%{path}) — list a directory under :rootsearch(%{pattern, path}) — regex search returning %{path, line, text}
matchesmix(%{task, args}) — run an allowlisted Mix task under :root