{"id":46293662,"url":"https://github.com/deepfates/cantrip","last_synced_at":"2026-03-04T09:04:28.138Z","repository":{"id":336151429,"uuid":"1140376602","full_name":"deepfates/cantrip","owner":"deepfates","description":"agent grimoire starter pack","archived":false,"fork":false,"pushed_at":"2026-03-04T01:05:18.000Z","size":2814,"stargazers_count":8,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-04T01:38:51.166Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deepfates.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":"AUDIT-bibliography-spec.md","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-23T07:35:42.000Z","updated_at":"2026-02-25T02:53:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/deepfates/cantrip","commit_stats":null,"previous_names":["deepfates/cantrip"],"tags_count":0,"template":true,"template_full_name":null,"purl":"pkg:github/deepfates/cantrip","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deepfates%2Fcantrip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deepfates%2Fcantrip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deepfates%2Fcantrip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deepfates%2Fcantrip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deepfates","download_url":"https://codeload.github.com/deepfates/cantrip/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deepfates%2Fcantrip/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30076935,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T08:01:56.766Z","status":"ssl_error","status_checked_at":"2026-03-04T08:00:42.919Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-04T09:04:25.496Z","updated_at":"2026-03-04T09:04:28.131Z","avatar_url":"https://github.com/deepfates.png","language":"TypeScript","readme":"# cantrip\n\n\u003e \"The cantrips have been spoken. The patterns of force are aligned. Now it is up to your machine.\"\n\u003e — Gargoyles: Reawakening (1995)\n\n\nA framework for building autonomous LLM entities. You draw a circle, speak an intent into it, and an entity arises — it reasons, acts in an environment, observes the results, and loops until the work is done or a limit is reached.\n\nEleven terms describe everything in the system. Three are fundamental: the **crystal** (the model), the **call** (the invocation that shapes it), and the **circle** (the environment it acts in). Everything else is what happens when you put those together and let the loop run.\n\n---\n\n## Quick Start: Launch an Entity\n\nThe fastest way to experience cantrip is to launch the **Familiar** — a long-running entity that can explore a codebase, run shell commands, browse the web, and reason about what it finds. It works in a JS medium and delegates to child cantrips with different mediums (bash, browser, etc.).\n\n```bash\n# Clone and install\ngit clone https://github.com/deepfates/cantrip.git\ncd cantrip\nbun install\n\n# Set your API key\nexport ANTHROPIC_API_KEY=\"sk-...\"\n\n# Launch the Familiar as an interactive REPL\nbun run examples/16_familiar.ts\n```\n\nYou'll get an interactive session where the entity can observe the repo (`repo_files`, `repo_read`), spawn child cantrips to run shell commands or browse the web, and reason about everything in code. Ask it to explore the codebase, run tests, analyze files — it figures out how to decompose the task and coordinate the work.\n\n```bash\n# Or give it a single task\nbun run examples/16_familiar.ts \"Explain the gate system and find all builtin gates\"\n```\n\nThe `examples/` directory has simpler starting points too — see the [examples table](#examples) below to walk through the concepts one at a time.\n\n---\n\n## Minimal Example\n\nTo build a cantrip from scratch: a crystal, a circle with gates and wards, and a call.\n\n```typescript\nimport { cantrip, Circle, ChatAnthropic, done, max_turns, gate } from \"cantrip\";\n\n// Crystal — an LLM\nconst crystal = new ChatAnthropic({ model: \"claude-sonnet-4-5\" });\n\n// A gate — a function the entity can call\nconst add = gate(\"Add two numbers\", async ({ a, b }: { a: number; b: number }) =\u003e a + b, {\n  name: \"add\",\n  params: { a: \"number\", b: \"number\" },\n});\n\n// Circle — gates + wards (constraints)\nconst circle = Circle({\n  gates: [add, done],\n  wards: [max_turns(10)],\n});\n\n// Cantrip — crystal + call + circle\nconst spell = cantrip({\n  crystal,\n  call: \"You are a calculator. Use the add tool, then call done with the result.\",\n  circle,\n});\n\n// Cast it on an intent\nconst result = await spell.cast(\"What is 2 + 3?\");\nconsole.log(result); // \"5\"\n```\n\nEach `cast` creates a fresh entity — the cantrip is a reusable recipe. No medium specified here: the circle uses **conversation** by default, where gates appear as tool calls in natural language. Add a medium to upgrade the entity's action space — see [Mediums](#mediums) below.\n\n---\n\n## Core Concepts\n\n### Crystal (Cognition)\n\nA crystal wraps an LLM. It takes messages and tools, returns a response. Stateless — each query is independent.\n\n```typescript\nimport { ChatAnthropic } from \"cantrip\";\n\nconst crystal = new ChatAnthropic({ model: \"claude-sonnet-4-5\" });\nconst result = await crystal.query([\n  { role: \"user\", content: \"What is 2 + 2? Reply with just the number.\" },\n]);\nconsole.log(result.content); // \"4\"\n```\n\nMultiple providers: `ChatAnthropic`, `ChatOpenAI`, `ChatGoogle`, `ChatOpenRouter`, `ChatLMStudio`.\n\n### Call (Invocation)\n\nThe call shapes the entity's behavior — a system prompt plus any hyperparameters. It can be a string or an object:\n\n```typescript\n// String shorthand\ncantrip({ crystal, call: \"You analyze code for bugs.\", circle });\n\n// Object form\ncantrip({\n  crystal,\n  call: { system_prompt: \"You analyze code for bugs.\" },\n  circle,\n});\n```\n\nGate definitions are automatically derived from the circle — you don't wire them manually.\n\n### Circle (Control)\n\nA circle is the entity's capability envelope: **medium + gates + wards**.\n\n```typescript\nimport { Circle, done, max_turns, require_done } from \"cantrip\";\n\nconst circle = Circle({\n  gates: [done],\n  wards: [max_turns(10)],\n});\n```\n\nEvery circle must have a `done` gate (how the entity signals completion) and at least one ward (how the host prevents infinite loops). This is enforced at construction time.\n\n**Gates** are functions bound into the circle from outside. The entity calls them as tools:\n- `done` — signals task completion via `submit_answer(result)`\n- Custom gates — any function you define with `gate()`\n- Builtin sets — `safeFsGates` (filesystem), `repoGates` (repo observation), `cantripGates` (child cantrip construction)\n\n**Wards** are constraints on the loop:\n- `max_turns(n)` — limit loop iterations\n- `require_done()` — only explicit `done` terminates (text-only responses don't stop the loop)\n\n### Entity (Emergence)\n\nAn entity is what arises when you cast a cantrip on an intent. You don't build it — it emerges from the loop. It accumulates context, develops strategies, and adapts turn by turn.\n\nTwo ways to create one:\n\n```typescript\n// Cast — one-shot. Entity runs, returns result, disposes.\nconst result = await spell.cast(\"Analyze this data\");\n\n// Invoke — persistent. Entity survives, accepts more intents.\nconst entity = spell.invoke();\nconst r1 = await entity.cast(\"First task\");\nconst r2 = await entity.cast(\"Follow-up task\"); // remembers r1\n```\n\nFor interactive sessions, use `invoke()` with the built-in REPL:\n\n```typescript\nimport { runRepl } from \"cantrip\";\n\nconst entity = spell.invoke();\nawait runRepl({\n  entity,\n  greeting: \"Agent ready. Ctrl+C to exit.\",\n});\n```\n\n---\n\n## Mediums\n\nA **medium** is the substrate the entity works in. When no medium is specified, the circle uses **conversation** — the baseline where gates appear as tool calls in natural language. Add a medium to upgrade the entity's action space.\n\nOne medium per circle. The medium replaces conversation — it doesn't sit alongside it. The entity works *in* the medium, not through it.\n\n### Conversation (default)\n\nNo medium specified. The entity communicates in natural language and uses gates as tool calls. This is how most chat-based agents work.\n\n```typescript\nconst circle = Circle({\n  gates: [...safeFsGates, done],\n  wards: [max_turns(100)],\n});\n```\n\n### VM (node:vm sandbox)\n\nThe entity writes and runs JavaScript in a node:vm context. Full ES2024 — arrow functions, async/await, destructuring. Zero external dependencies. Gates are projected as async functions the entity calls with `await`.\n\n```typescript\nimport { vm } from \"cantrip\";\n\nconst circle = Circle({\n  medium: vm({ state: { context: { items: [1, 2, 3] } } }),\n  wards: [max_turns(20), require_done()],\n});\n```\n\nThe entity sees a `context` variable in its sandbox and explores it with code. `var` and `globalThis` persist across turns. Weak isolation (V8 context, not a security boundary).\n\n### JavaScript (QuickJS sandbox)\n\nThe entity works in a QuickJS WASM sandbox. Strong isolation but limited ES version and a serialization boundary — gate results are strings, not native objects.\n\n```typescript\nimport { js } from \"cantrip\";\n\nconst circle = Circle({\n  medium: js({ state: { context: { items: [1, 2, 3] } } }),\n  wards: [max_turns(20), require_done()],\n});\n```\n\n### Bash\n\nThe entity writes shell commands. Full access to CLI tools — git, curl, ffmpeg, jq, whatever's installed.\n\n```typescript\nimport { bash } from \"cantrip\";\n\nconst circle = Circle({\n  medium: bash({ cwd: \"/project\" }),\n  wards: [max_turns(10)],\n});\n```\n\n### Browser (Taiko)\n\nThe entity controls a headless browser by writing Taiko code — navigation, clicking, data extraction.\n\n```typescript\nimport { browser } from \"cantrip\";\n\nconst circle = Circle({\n  medium: browser({ headless: true, profile: \"full\" }),\n  wards: [max_turns(50), require_done()],\n});\n```\n\n### jsBrowser\n\nJS sandbox with browser automation combined — the entity writes JavaScript that can also control a browser.\n\n```typescript\nimport { jsBrowser, BrowserContext } from \"cantrip\";\n\nconst browserCtx = await BrowserContext.create({ headless: true, profile: \"full\" });\nconst circle = Circle({\n  medium: jsBrowser({ browserContext: browserCtx }),\n  wards: [max_turns(200), require_done()],\n});\n```\n\n### Other mediums\n\nAny interactive environment can become a medium — Python, SQL, Frida, GDB, Redis, or a custom DSL. The interface is the same: the entity writes, the medium executes, the result feeds back.\n\n---\n\n## Patterns\n\n### One-shot cast\n\nThe simplest pattern. Create a cantrip, cast it, get a result.\n\n```typescript\nimport { cantrip, Circle, ChatAnthropic, js, max_turns, require_done } from \"cantrip\";\n\nconst spell = cantrip({\n  crystal: new ChatAnthropic({ model: \"claude-sonnet-4-5\" }),\n  call: \"Explore the context variable. Use submit_answer() when you have a final answer.\",\n  circle: Circle({\n    medium: js({ state: { context: { items: [\"alpha\", \"beta\", \"gamma\"] } } }),\n    wards: [max_turns(20), require_done()],\n  }),\n});\n\nconst answer = await spell.cast(\"Which item comes first alphabetically?\");\n```\n\n### Persistent REPL\n\nFor interactive sessions — the entity remembers across intents.\n\n```typescript\nimport { runRepl, safeFsGates, getSandboxContext, SandboxContext } from \"cantrip\";\n\nconst fsCtx = await SandboxContext.create();\n\nconst entity = cantrip({\n  crystal,\n  call: `Coding assistant. Working dir: ${fsCtx.working_dir}`,\n  circle: Circle({\n    gates: [...safeFsGates, done],\n    wards: [max_turns(100)],\n  }),\n  dependency_overrides: new Map([[getSandboxContext, () =\u003e fsCtx]]),\n}).invoke();\n\nawait runRepl({ entity, greeting: \"Agent ready.\" });\n```\n\n### Recursive delegation\n\nA parent entity in a JS medium delegates subtasks to children via `call_entity`.\n\n```typescript\nimport { call_entity_gate, Loom, MemoryStorage, js } from \"cantrip\";\n\nconst entityGate = call_entity_gate({ max_depth: 2, depth: 0, parent_context: data });\n\nconst circle = Circle({\n  medium: js({ state: { context: data } }),\n  gates: entityGate ? [entityGate] : [],\n  wards: [max_turns(20), require_done()],\n});\n\nconst loom = new Loom(new MemoryStorage());\nconst spell = cantrip({ crystal, call: \"Delegate analysis to child entities.\", circle, loom });\nconst answer = await spell.cast(\"Analyze each category and summarize the trend.\");\n```\n\nChildren get independent circles. The shared loom captures parent + child turns as a tree.\n\n### The Familiar\n\nThe capstone pattern: a long-running entity in a `vm()` medium that creates and casts child cantrips from code. It observes the repo, delegates to specialized children (bash, browser, JS), and synthesizes results.\n\n```typescript\nimport {\n  cantripGates, repoGates, RepoContext, Loom, JsonlStorage, done,\n  vm, js, bash, browser, getRepoContextDepends,\n} from \"cantrip\";\n\nconst loom = new Loom(new JsonlStorage(\".cantrip/loom.jsonl\"));\nawait loom.load();\n\nconst cantripConfig = {\n  mediums: {\n    bash: (opts) =\u003e bash({ cwd: opts?.cwd ?? repoRoot }),\n    js: (opts) =\u003e js({ state: opts?.state }),\n    vm: (opts) =\u003e vm({ state: opts?.state }),\n    browser: () =\u003e browser({ headless: true, profile: \"full\" }),\n  },\n  gates: { done: [done] },\n  default_wards: [{ max_turns: 15 }],\n  loom,\n};\n\nconst { gates: cGates, overrides: cOverrides } = cantripGates(cantripConfig);\nconst repoCtx = new RepoContext(repoRoot);\n\nconst circle = Circle({\n  medium: vm(),\n  gates: [...repoGates, ...cGates],\n  wards: [max_turns(50), require_done()],\n});\n\nconst spell = cantrip({\n  crystal,\n  call: SYSTEM_PROMPT,\n  circle,\n  dependency_overrides: new Map([\n    [getRepoContextDepends, () =\u003e repoCtx],\n    ...cOverrides,\n  ]),\n  loom,\n  folding_enabled: true,\n});\n```\n\nInside the Familiar's vm medium, the entity writes modern JS to coordinate:\n\n```javascript\n// Shell work — child runs in bash\nconst worker = cantrip({\n  crystal: \"anthropic/claude-haiku-4.5\",\n  call: \"Execute the command and report output.\",\n  circle: { medium: \"bash\", gates: [\"done\"], wards: [{ max_turns: 5 }] }\n});\nconst output = await cast(worker, \"Run the test suite\");\n\n// Thinking — leaf cantrip, single LLM call\nconst thinker = cantrip({ crystal: \"anthropic/claude-haiku-4.5\", call: \"Analyze code.\" });\nconst analysis = await cast(thinker, \"What bugs do you see?\\n\" + code);\n\n// Compose in code — loops, conditionals, pipelines\nconst files = JSON.parse(await repo_files(\"src/**/*.ts\"));\nfor (const file of files) {\n  const src = await repo_read(file);\n  if (src.includes(\"TODO\")) {\n    const review = await cast(\n      cantrip({ crystal: \"anthropic/claude-haiku-4.5\", call: \"Find TODOs.\" }),\n      src\n    );\n    console.log(file + \": \" + review);\n  }\n}\n```\n\nSee `examples/16_familiar.ts` for the full implementation.\n\n---\n\n## The Loom\n\nEvery turn is recorded in a **loom** — a structured log that captures the entity's full execution history as a tree of turns.\n\n```typescript\nimport { Loom, JsonlStorage, MemoryStorage } from \"cantrip\";\n\n// In-memory (ephemeral)\nconst loom = new Loom(new MemoryStorage());\n\n// Persistent to disk\nconst loom = new Loom(new JsonlStorage(\".cantrip/loom.jsonl\"));\nawait loom.load();\n```\n\nThe loom records whether each thread **terminated** (entity called `done`) or was **truncated** (ward triggered). This distinction matters: terminated threads are complete episodes, truncated threads are interrupted ones.\n\n**Folding** compresses old turns to keep the entity's context window manageable while preserving key information. It reads from the loom and writes compressed summaries back into the entity's working state.\n\n---\n\n## The Spec and the Ghost Library\n\n[SPEC.md](./SPEC.md) is the formal specification — eleven terms, behavioral rules, and the design rationale. It describes behavior, not technology: \"the circle must provide sandboxed code execution\" — not \"use QuickJS.\"\n\nThis TypeScript package is the reference implementation. The spec is designed so that implementations in other languages can be generated from it — the **ghost library** pattern, where the spec and its test suite are the durable artifacts and code regenerates from them. A Python cantrip and a TypeScript cantrip are both cantrips. The concepts compose across language boundaries because each language implements its own native mediums while sharing the same circle/gate/ward semantics.\n\nThe [bibliography](./BIBLIOGRAPHY.md) traces each idea from first appearance through academic formalization to independent confirmation.\n\n---\n\n## Examples\n\nThe `examples/` directory walks through the concepts in order:\n\n| # | Example | What it teaches |\n|---|---------|----------------|\n| 01 | `crystal` | LLM as stateless query |\n| 02 | `gate` | Defining callable functions |\n| 03 | `circle` | Gates + wards + validation |\n| 04 | `cantrip` | Crystal + call + circle = script |\n| 05 | `ward` | Constraints and safety limits |\n| 06 | `providers` | Multi-provider crystals |\n| 07 | `conversation` | Conversation medium (default) |\n| 08 | `js_medium` | QuickJS sandbox |\n| 09 | `browser_medium` | Taiko browser automation |\n| 10 | `composition` | Parallel delegation via call_entity_batch |\n| 11 | `folding` | Context compression |\n| 12 | `full_agent` | JS medium + filesystem gates |\n| 13 | `acp` | Agent Client Protocol adapter |\n| 14 | `recursive` | Depth-limited recursive entities |\n| 15 | `research_entity` | jsBrowser + recursion + ACP |\n| 16 | `familiar` | Cantrip construction as medium physics |\n| 17 | `leaf_cantrip` | Simplest delegation — crystal + call, one LLM call |\n| 18 | `vm_medium` | node:vm sandbox — full ES2024, async/await |\n| 19 | `bash_medium` | Entity works IN bash as primary medium |\n| 20 | `data_exploration` | RLM pattern — data in sandbox, explore with code |\n| 21 | `independent_axes` | M, G, W as orthogonal knobs |\n\nRun any example:\n```bash\nbun run examples/04_cantrip.ts\n```\n\n---\n\n## Installation\n\n```bash\nbun install cantrip\n```\n\nSet your API key for your crystal provider:\n```bash\nexport ANTHROPIC_API_KEY=\"sk-...\"\n# or\nexport OPENROUTER_API_KEY=\"sk-...\"\n# or\nexport OPENAI_API_KEY=\"sk-...\"\n```\n\n---\n\n## Why \"Cantrip\"?\n\nIn tabletop RPGs, a cantrip is the simplest spell — it costs nothing to cast and you can cast it repeatedly. The etymology traces to Gaelic *canntaireachd*, a piper's mnemonic chant. It's a loop of language.\n\nHere, a cantrip is a reusable script for creating LLM entities. Configure once, cast many times, compose into larger spells. The loop is the mechanism. The repetition is the point.\n\n---\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeepfates%2Fcantrip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeepfates%2Fcantrip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeepfates%2Fcantrip/lists"}