https://github.com/elixir-vibe/pi-elixir
BEAM runtime tools for pi — connects to the running Elixir app via Tidewave
https://github.com/elixir-vibe/pi-elixir
beam coding-agent elixir live-introspection mcp pi-agent tidewave
Last synced: 4 days ago
JSON representation
BEAM runtime tools for pi — connects to the running Elixir app via Tidewave
- Host: GitHub
- URL: https://github.com/elixir-vibe/pi-elixir
- Owner: elixir-vibe
- Created: 2026-02-20T06:52:46.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2026-06-09T22:40:28.000Z (7 days ago)
- Last Synced: 2026-06-09T23:18:55.824Z (7 days ago)
- Topics: beam, coding-agent, elixir, live-introspection, mcp, pi-agent, tidewave
- Language: Elixir
- Size: 646 KB
- Stars: 75
- Watchers: 1
- Forks: 5
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# pi-elixir
`pi-elixir` is the pi bridge for BEAM-native, verifiable Elixir development.
It gives pi a live connection to the running Elixir system, structural Elixir AST tools, supervised BEAM sessions, and resumable eval state. The emphasis is on callable capabilities and verifiers, not only instructions: the agent can inspect runtime state, make syntax-aware changes, and validate them from formatter/compile/test checks up through duplication, static analysis, and architecture/smell checks.
This follows the broader [Vibe](https://github.com/elixir-vibe/vibe) direction: few model-facing tools outside, rich composable Elixir APIs inside, structured BEAM payloads rendered by pi, and verification through runtime state plus structural analysis.
Real pi TUI output looks like this — compact tool calls, real BEAM status, and session trees rendered in the transcript/widget:
```text
iex case Pi.Agent.parallel(["Reply only: child A ok", "Reply only: child B ok"], name: :review_smoke, timeout: 60000) d…
(70000ms)
%{status: :ok, kind: :parallel, results: ["child A ok", "child B ok"]}
Took 6.8s
✓ review_smoke
2 done
├─ ✓ review_smoke child A ok
└─ ✓ review_smoke child B ok
(ctrl+o to expand)
~/my_app
↑37k ↓156 $0.190 (sub) 6.9%/272k (auto) (openai-codex) gpt-5.5 • medium
⬡ BEAM (embedded)
```
## Why this is different
Instructions are included, but they are not the foundation. The foundation is executable capability: `iex` into the live app, ExAST-backed structural tools, OTP sessions, project-local plugins/skills, and strict verification gates.
`pi-elixir` gives the agent concrete operations and checks:
- **Live runtime inspection** — evaluate trusted Elixir inside the loaded app with project modules, config, deps, application env, processes, ETS, logs, and IEx helpers available.
- **Stateful IEx-like eval** — bindings, aliases, imports, and requires persist across `elixir_eval` calls. The state is stored as sidecar snapshots next to the pi session, so resume and branch navigation keep the right context.
- **Structural code intelligence** — `elixir_ast_search` and `elixir_ast_replace` use [ExAST](https://hex.pm/packages/ex_ast) patterns, so the agent searches and edits Elixir syntax instead of playing regex roulette.
- **OTP-native subagents** — `Pi.Session` and `Pi.Agent` run logical child sessions inside the embedded BEAM. Active work renders as a pi widget; completed trees land in transcript once.
- **Active-model LLM from BEAM** — `Pi.LLM` and optional `Pi.ReqLLM` route BEAM calls through pi's current model. pi owns provider/model selection, credentials, streaming, cancellation, usage, and transcript UI; the BEAM side sends structured completion/stream requests over the active bridge.
- **Project-local skills and plugins** — trusted Elixir code can teach the agent your app's workflows, guardrails, slash commands, UI widgets, and tool hooks.
- **Hard quality gates** — the repo itself is checked with JS lint/typecheck/tests, BEAM compile/test/Credo/Dialyzer, [ExDNA](https://hex.pm/packages/ex_dna) clone detection, and [Reach](https://hex.pm/packages/reach) architecture/smell checks.
The philosophy is the same as Vibe: compact agent APIs, structured BEAM payloads, runtime state, and Elixir/OTP idioms first. The implementation is pi-native: TypeScript owns tool registration/TUI rendering, while BEAM owns Elixir semantics.
## What you can do every day
### Debug a Phoenix/Ecto issue in the running app
The agent uses `iex` (`elixir_eval`) to inspect the live BEAM. Calls render as compact pi tool rows, not giant JSON blobs:
```text
iex alias MyApp.Repo; alias MyApp.Billing.Invoice; stale = Repo.all(...); length(stale)
14
Took 0.1s
```
The next eval continues from the same IEx-like state:
```text
iex stale |> Enum.group_by(& &1.customer_id) |> Enum.map(fn {id, xs} -> {id, length(xs)} end)
[{"cust_123", 5}, {"cust_456", 9}]
Took 0.1s
```
That continuity is real state, not prompt memory. On resume/branch navigation, `pi-elixir` restores the newest matching sidecar eval snapshot.
### Inspect OTP instead of guessing
The agent can ask the live system about supervisors, queues, process state, ETS, logs, and application config:
```text
iex Supervisor.which_children(MyApp.Supervisor)
[
{MyApp.Repo, #PID<0.421.0>, :worker, [MyApp.Repo]},
{MyAppWeb.Endpoint, #PID<0.422.0>, :supervisor, [MyAppWeb.Endpoint]}
]
Took 0.1s
```
For Elixir bugs, this is the daily win: pi does not have to infer runtime truth from files alone.
### Search and edit by Elixir syntax shape
ExAST-backed tools show pi-style compact calls and semantic results. The agent can search for code shape instead of text:
Real captured `ast grep` output:
```text
ast grep defmodule _ do _ end lib/pi/ast.ex · limit 2 · allow broad
1 match defmodule _ do _ end
lib/pi/ast.ex:1 defmodule Pi.AST do @moduledoc "Structured ExAST helpers for bridge tools." ali…
(ctrl+o to expand)
```
Real captured `ast edit` dry-run/no-match output:
```text
ast edit Logger.debug(_) → Logger.info(_) lib/pi/eval/snapshot.ex · limit 2 ·…
No matches found.
```
The structure is Elixir AST. Captures, partial structs/maps, nested expressions, and broad-pattern guards are handled by ExAST, not a regex pretending to know Elixir. When a replacement matches, the same tool row renders semantic replacement counts and diff blocks in the expandable details.
### Run OTP-backed child agents without spawning more pi processes
BEAM sessions render as real pi session trees. This is captured from tmux; names/strings are sanitized only:
```text
iex {:ok, root} = Pi.Session.start(name: :showcase); ...; :ok
:ok
Took 0.1s
○ showcase
3 done
└─ ✓ tests done · done 70 passed
└─ ✓ review done · done LGTM
└─ ✓ research done · done notes ready
(ctrl+o to expand)
~/my_app
↑17k ↓223 R16k CH96.4% $0.102 (sub) 6.3%/272k (auto) (openai-codex) gpt-5.5 • medium
⬡ BEAM (embedded)
```
For real model-backed BEAM agents, the transcript shape is the same:
```text
iex case Pi.Agent.parallel(["Review API", "Review tests"], name: :review_smoke, timeout: 60000) d…
(70000ms)
%{status: :ok, kind: :parallel, results: ["API ok", "tests ok"]}
Took 6.8s
✓ review_smoke
2 done
├─ ✓ review_smoke API ok
└─ ✓ review_smoke tests ok
(ctrl+o to expand)
```
Active/running BEAM snapshots are widget-only. Completed root trees are sent once as transcript messages, so you do not get repeated live snapshot artifacts.
### Add project-specific Elixir knowledge
The startup screen shows `elixir-dev` / `elixir-new-project` as normal pi skills:
```text
[Skills]
... context-management, elixir-dev, elixir-new-project, ...
[Extensions]
... src, webfetch, websearch, ...
```
Your project can add executable Elixir skills and plugins. The main UX effect is that pi gets your release checklist, Oban conventions, Ecto rules, UI widgets, and slash commands as local trusted project behavior — not as generic prompt text.
## The model-facing tool surface
`pi-elixir` deliberately exposes only three model tools:
| Tool | Label | Purpose |
|---|---:|---|
| `elixir_eval` | `iex` | Trusted eval inside the running app. Stateful by default for pi session branches; sandbox mode available for untrusted snippets. |
| `elixir_ast_search` | `ast grep` | ExAST structural search over Elixir code. |
| `elixir_ast_replace` | `ast edit` | ExAST structural rewrite with dry-run diffs. |
Everything else is regular Elixir API reachable through eval:
```elixir
Pi.project()
Pi.logs(tail: 50)
Pi.Bridge.Info.runtime_apis()
Pi.Eval.bindings()
Pi.Eval.forget(:huge_result)
Pi.Eval.reset()
Pi.LLM.complete("Summarize this module")
Pi.LLM.stream("Draft a migration plan")
Pi.ReqLLM.install()
ReqLLM.generate_text(Pi.ReqLLM.current_model(), "Summarize this module")
Pi.Session.start(name: :reviewer)
Pi.Agent.parallel(["Review API", "Review tests", "Review OTP risks"])
{:ok, job} = Pi.Agent.start("Review this module", role: :reviewer)
{:ok, done} = Pi.Agent.await(job, 60_000)
{:ok, text} = Pi.Agent.result(done)
```
Eval also preloads token-efficient aliases for QuackDB session analytics:
```elixir
# preloaded: import Ecto.Query; use QuackDB.Ecto
# preloaded: alias Pi.Self, as: Self
# preloaded: alias Pi.CodeMap, as: CodeMap
# preloaded: alias Pi.Quack, as: Q; require Q
# preloaded: alias Pi.Quack.Event, as: E; alias Pi.Quack.SessionFile, as: SF
Self.status()
Self.context("why did sync crash?", limit: 5)
# After non-trivial Elixir edits, reflect before finalizing.
CodeMap.reflect(changed: true)
q = "function_clause"
from(e in Q.errors(),
where: Q.matches(e.id, ^q),
order_by: [desc: Q.score(e.id, ^q)],
limit: 20,
select: %{s: Q.score(e.id, ^q), tool: e.tool_name, content: Q.json_text(e.payload_json, "$.content")}
)
|> Q.table()
```
This keeps the transcript understandable: the model writes Elixir to control Elixir.
## Stateful eval and session-tree resume
`elixir_eval` behaves like an IEx/Livebook cell runtime scoped to the current pi execution path:
- variables persist across calls;
- `alias`, `import`, and `require` persist through `Macro.Env`;
- errors do not replace the previous good state;
- `Pi.Eval.bindings/0`, `forget/1`, and `reset/0` manage state from inside eval;
- snapshots are stored as sidecar blobs, **not** in the JSONL transcript.
Physical storage:
```text
.pi-elixir/
eval-state/
.term
.term.meta.json
```
When you navigate or resume a pi branch, the extension walks the session branch, finds the newest ancestor eval snapshot, and starts the next eval from that state. New evals write a new immutable checkpoint keyed by the tool call id, so old branch state is not overwritten.
Large or unsafe bindings are handled defensively:
- PIDs, ports, refs, functions, and containers containing them are not persisted.
- Live evaluator memory can hold runtime values while the bridge is active.
- Sidecar snapshots have a size budget and drop largest serializable bindings first.
- Metadata JSON contains only names/types/bytes, never the full state.
## Architecture
```text
pi Node/TUI
├─ TypeScript extension
│ ├─ registers tools and skills
│ ├─ starts embedded stdio by default, with explicit/discovered HTTP MCP escape hatches
│ ├─ owns TUI rendering and sidecar eval-state paths
│ └─ forwards lifecycle/tool events
│
└─ embedded or external BEAM
├─ Pi.Transport.Stdio / MCP endpoint
├─ Pi.Eval.Supervisor
├─ Pi.LLM.Broker
├─ Pi.Session.Supervisor
├─ Pi.Plugin.Manager
├─ Pi.Skill.Loader
└─ project modules, deps, processes, Repo, endpoints
```
The BEAM side emits structured protocol payloads. The TS side renders them in pi style. For example, eval can return an ordered, typed table while staying plain Elixir until the final output helper:
```elixir
Path.wildcard("lib/pi/**/*.ex")
|> Enum.map(&%{path: &1, bytes: File.stat!(&1).size})
|> Enum.sort_by(& &1.bytes, :desc)
|> Enum.take(8)
|> Pi.table(columns: [:path, :bytes])
```
Final eval values auto-render when their shape is known (tables for lists of maps/keywords, trees for maps, text for strings). Use `Pi.output(value, opts)` only when you want to force rendering options such as column order:
```elixir
Path.wildcard("lib/pi/**/*.ex")
|> Enum.map(&%{path: &1, bytes: File.stat!(&1).size})
|> Enum.sort_by(& &1.bytes, :desc)
|> Enum.take(8)
|> Pi.output(columns: [:path, :bytes])
```
Use `Pi.table(rows, columns: [...])` when you explicitly want to construct table output; otherwise columns are inferred from row keys.
Docs/source discovery is also pipeline-first and auto-renders through the same output protocol:
```elixir
Pi.Docs.module(Pi.Output)
|> Pi.Docs.functions()
|> Pi.Docs.search("table")
```
```elixir
Pi.Docs.module(Pi.Output)
|> Pi.Docs.function(:table, 2)
|> Pi.Docs.source(context: 25)
```
Bounded web fetches return structured values and do not expose raw `Req`:
```elixir
Pi.Web.fetch!("https://example.com", format: :text)
```
## Install
```sh
pi install npm:pi-elixir
```
Verify the environment from inside pi:
```text
/elixir:doctor
```
In each Mix project that should use BEAM tools, install the dev-only bridge dependency:
```text
/elixir:install
```
That adds an exact-versioned dependency such as:
```elixir
{:pi_bridge, "== ", only: :dev}
```
The exact version matters: npm `pi-elixir` and Hex `pi_bridge` are released together and must speak the same protocol. If you skip `/elixir:install`, the first Elixir tool call can still prompt to add the dependency.
## Recommended project stack
For new web applications, use Phoenix with Igniter and VibeKit, then add pi-elixir in the project:
```sh
mix archive.install hex phx_new
mix archive.install hex igniter_new
mix phx.new my_app
cd my_app
mix igniter.install vibe_kit --agents-md
pi install npm:pi-elixir
```
For non-web Elixir projects and packages, use Igniter with VibeKit as the baseline:
```sh
mix archive.install hex igniter_new
mix igniter.new my_lib --install vibe_kit --agents-md
cd my_lib
pi install npm:pi-elixir
```
VibeKit provides the project quality baseline (`mix ci`, Credo strict with ExSlop, Dialyzer, ExDNA, and Reach). pi-elixir provides the live BEAM tools used by agents while they work inside that project. In each generated project, run `/elixir:install` once to add the exact matching dev-only `:pi_bridge` dependency.
For local development:
```sh
git clone https://github.com/dannote/pi-elixir
cd pi-elixir
pnpm install
cd packages/bridge && mix deps.get && cd ../..
pi install "$PWD"
```
If you also have `npm:pi-elixir` installed globally, remove it before dogfooding a checkout to avoid duplicate tool registration:
```sh
pi remove npm:pi-elixir
pi install "$PWD"
```
From an already-running local checkout, `/elixir:dogfood` performs that switch for you.
### Troubleshooting setup
Use `/elixir:doctor` first. It reports the resolved Mix project, Elixir/Mix availability, `pi_bridge` dependency status, connection state, embedded startup failures, and a suggested next step.
Common cases:
| Symptom | What to do |
|---|---|
| `Mix cwd: not found` | Start pi from a Mix project directory, or from a supported repo root with a known nested Mix project. |
| `Elixir is not installed or not available on PATH` | Start pi from a shell where Elixir/Mix are available. If you just changed `mise`/`asdf` versions, restart pi. |
| Stale `mise` PATH warning | Restart the shell/pi process so removed tool install paths disappear from `PATH`. |
| `pi_bridge dependency: missing` | Run `/elixir:install` in the Mix project. |
| Embedded BEAM exited before ready | Fix the Mix/Elixir error shown in doctor, then run `/elixir:restart`. Wrong Elixir versions surface here as the real Mix error. |
| `pi_bridge version mismatch` | Update the Mix dependency to the exact version expected by the installed `pi-elixir`, then run `mix deps.get`. |
| Tool registration conflicts with another `pi-elixir` path | Remove the duplicate install, usually `pi remove npm:pi-elixir`, then install only the checkout or only the npm package. |
For setup-flow regression testing in this repository:
```sh
scripts/manual-setup-flow.sh
```
It runs tmux/asciinema playground scenarios for non-Mix directories, missing bridge dependency, explicit install, wrong Elixir startup failure, happy path tools, and duplicate package conflicts.
## Connection model
The normal connection path is an embedded stdio bridge started inside the Mix project with `Pi.Transport.Stdio.start()`. HTTP MCP endpoints are escape hatches for advanced/debug setups.
Resolution order:
1. `PI_MCP_URL`, only when explicitly configured for a manually exposed HTTP MCP endpoint.
2. Discovered local HTTP MCP endpoint matching the Mix app name.
3. Embedded stdio transport inside the project.
```sh
# Advanced/debug only: bypass embedded stdio and use your own HTTP MCP endpoint.
export PI_MCP_URL=http://localhost:4001/mcp
export PI_DISABLE_EMBEDDED=1
```
Status is actionable: external/embedded/starting/missing/incompatible/offline plus integration-specific status such as Phoenix endpoints.
Feature flags are escape hatches for noisy, sensitive, or experimental environments:
| Capability | Default | Escape hatch |
|---|---:|---|
| Stateful `elixir_eval` | on | `PI_ELIXIR_STATEFUL_EVAL=0` |
| Eval sidecar snapshots | on | `PI_ELIXIR_EVAL_SIDECAR=0` |
| BEAM LLM / ReqLLM | on | `PI_ELIXIR_LLM=0` |
| BEAM sessions/widgets/control | on | `PI_ELIXIR_SESSIONS=0` |
| Project plugins/hooks/UI/commands | on | `PI_ELIXIR_PLUGINS=0` |
| Executable Elixir skills | on | `PI_ELIXIR_SKILLS=0` |
| Extra-short eval previews | off | `PI_ELIXIR_COMPACT_EVAL_PREVIEW=1` |
## Included Elixir development skill
The package ships pi skills for Elixir work:
- `elixir-dev` — use BEAM eval for runtime introspection, ExAST tools for structural search/edit, LSP for editor semantics, and Mix only for build/test/format gates.
- `elixir-new-project` — bootstrap new Elixir packages/projects with strict VibeKit/Igniter-style quality setup.
The skill tells the agent how to work idiomatically: prefer runtime truth, inspect installed docs with `Code.fetch_docs/1`/`h/1`, use ExAST patterns for Elixir search/refactors before grep/regex, keep changes verified, and avoid inventing framework behavior.
## Quality stack
The release gate is intentionally strict. `pnpm run check` runs:
- TypeScript lint/typecheck/format/tests/duplication checks.
- BEAM compile with warnings as errors.
- ExUnit.
- Credo strict.
- Dialyzer.
- ExDNA clone detection with zero clone budget.
- Reach architecture and smell checks in strict mode.
- Hex package build validation.
- npm pack validation.
Reach and ExAST are not decorative dependencies. They are the direction: agentic Elixir coding should be semantic, structural, and architecture-aware.
## Debugging
Hidden pi command:
```text
/elixir:debug
```
Writes extension diagnostics to `~/.pi/agent/pi-elixir-debug.log` by default.
For event-loop/embedded bridge investigations:
```sh
export PI_ELIXIR_DEBUG=1
export PI_ELIXIR_DEBUG_LOG=/tmp/pi-elixir-debug.json
```
## Repository shape
```text
packages/
extension/ # npm/pi package: TS extension, tools, skills, embedded stdio launcher
bridge/ # Hex/Mix package: Pi runtime facade, protocol, eval, plugins, sessions
```
The npm package is the user-facing pi package. The Hex package is installed into target Mix projects as a dev-only bridge.
## Relationship to Vibe
[Vibe](https://github.com/elixir-vibe/vibe) is a BEAM-native coding-agent runtime. `pi-elixir` ports the most useful ideas into pi:
- minimal model-facing Elixir tools;
- Livebook-style eval state;
- structured BEAM payloads rendered by pi;
- executable Elixir skills;
- project-local plugins;
- OTP-backed child sessions;
- BEAM-first runtime inspection.
`pi-elixir` keeps pi's UI and tool model, but moves Elixir-specific work into the running BEAM: eval state, AST operations, sessions, skills, plugins, and runtime checks.
## Development
Prerequisites:
- pnpm
- Elixir `~> 1.20` with OTP 28+
- pi installed globally
Common commands:
```sh
pnpm run fmt
pnpm run check
pnpm run check:js
pnpm run check:beam
pnpm run test:integration
pnpm run pack:check
```
`pnpm run check` is the release-readiness gate.
## Part of Elixir Vibe
pi-elixir gives the pi coding agent a live door into the BEAM: stateful eval, AST tools, and composable runtime APIs.
It is one building block of a larger stack — tools that make AI-generated
software checkable: structural search, dependence analysis, duplication and
slop detection, session replay, and ecosystem-wide code search. See the
[Elixir Vibe](https://github.com/elixir-vibe) organization for the rest, and
[Building Blocks for the Future Web](https://github.com/elixir-vibe/building-blocks)
for the thesis, architecture, and roadmap that tie them together.