@shift-labs/pi-rlm
A pi extension with a single tool: execute, running TypeScript in a persistent Bun evaluator
Package details
Install @shift-labs/pi-rlm from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@shift-labs/pi-rlm- Package
@shift-labs/pi-rlm- Version
0.4.0- Published
- Aug 9, 2026
- Downloads
- 298/mo · 298/wk
- Author
- miclivs
- License
- MIT
- Types
- extension
- Size
- 185.4 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/extension/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-rlm
A pi extension that replaces the usual toolbox with a single
tool: execute, which runs TypeScript in a persistent Bun evaluator.
Everything an agent would normally reach for a separate tool to do — reading files, running shell commands, editing, searching, delegating to subagents — is expressed as code inside that one tool.
✓ rlm · shell · const files = (await Bun.$`ls -1`.text()).split("\n") · ↑ 2 ↓ 7 lines · 41ms
✓ rlm · const tests = files.filter((f) => f.includes("test")) · ↑ 1 ↓ 1 lines · 3ms
The second cell reuses the first cell's variable. Nothing was re-read, and nothing was re-parsed from text — because the evaluator is still there.
Why one tool
A fixed set of tools is a fixed vocabulary. Every new capability means a new tool, a new schema, and a model that has to be taught when to reach for it.
Here the vocabulary is a programming language. Capabilities arrive as functions in the evaluator's namespace rather than as entries in a tool list, so the interface the model sees never changes while what it can do keeps growing. It also changes how an agent works: intermediate results live in variables instead of being re-derived from earlier output, so a long task compounds rather than repeating itself.
What the agent gets
Subagents as a call stack. A cell that calls rlm.run renders its
children as stack frames beneath it — glyph, name, status, age, spawn site —
nested by depth. While anything runs the stack stays visible on the collapsed
cell; once every frame settles it folds into a header chip (3 subagents · 1 failed). Frame records live on disk beside each child's output, so stacks
survive session resume and read truthfully post-mortem.
A namespace that persists. Variables, functions, classes, and imports stay
available across calls, across turns, and — on a best-effort basis — across
session resumes. Whatever cannot be serialised is named in the restore report
rather than silently dropped. Long sessions stay cheap: snapshots re-serialise
only what changed, large long-untouched values revive lazily (they load the
first time they are read), and rlm.forget("name") is the one true delete —
the engine never discards agent state on its own.
Shell as values, not text. await Bun.$git log --oneline.quiet() returns an
object with an exit code and captured output, ready to be assigned and filtered.
No parsing a transcript to recover what a command said.
Subagents as function calls. await rlm.run("task") spawns a real child
agent and returns a handle at admission. Children write their answers to files;
the parent polls the registry and reads them when it wants. Delegation happens
mid-computation instead of as a separate mode.
Cancellation that costs one cell. Interrupting a running cell leaves the namespace intact, and the cancelled cell cannot keep writing to it afterwards.
Install
pi install npm:@shift-labs/pi-rlm
Bun is required. pi itself runs on Node, but the evaluator is a Bun process — without it on your PATH the engine will tell you so and stop.
curl -fsSL https://bun.sh/install | bash
Launch
The extension is dormant until asked for. A plain pi session is untouched —
default prompt, default tools, no evaluator. Activation is one flag:
pi --rlm
That collapses the tool surface to execute and replaces the system prompt;
no other pi flags are needed. To verify the two worlds:
pi -p "what tools do you have?" # stock pi: read, bash, edit, ...
pi -p --rlm "what tools do you have?" # one tool: execute
To run from a clone (development), load the extension explicitly — the flag
works the same, or set PI_RLM_FORCE=1 where flag plumbing is awkward:
git clone https://github.com/shift-labs-ai/pi-rlm && cd pi-rlm
bun install
pi --rlm -e ./src/extension/index.ts
Configuration
| Variable | Default | Meaning |
|---|---|---|
PI_RLM_SUBAGENT_MODEL |
anthropic/haiku |
Model children are spawned with |
PI_RLM_MAX_DEPTH |
2 |
How deep recursive delegation may go |
PI_RLM_DEPTH |
0 |
Depth of the current agent; set on children automatically |
PI_RLM_NPM_CACHE_DIR |
~/.cache/pi-rlm-npm |
Where npm: imports install |
Session state lives in .pi-rlm/<session>/: the namespace snapshot and each
subagent's session file and output.
npm imports
Static top-level imports can name npm packages directly:
import { z } from "npm:zod@4";
import { format } from "npm:date-fns@4/format";
The first import of a name@version installs it into an isolated cache
directory — the working directory's node_modules is never touched — and the
binding persists across cells like any other import. Pin versions when
repeatability matters: an unpinned name means latest, resolved once at first
install and reused from the cache after that. Specifiers are
code-execution input: importing a package runs its code. Dynamic
import("npm:...") is not supported.
How it works
The extension runs a Bun child process that owns the namespace. Cells are
transformed so their top-level declarations become namespace assignments, then
executed inside a with block over a proxy. Host and guest talk over a private
pipe with authenticated framing, which is what stops a cell from being able to
report its own outcome.
ARCHITECTURE.md covers the design and the reasoning behind it.
Development
bun run check # typecheck, lint, and the full suite — the gate
bun test # tests only
bun run typecheck # tsc --noEmit
bun run format # biome
The test suite is the specification. test/engine.contract.test.ts states each
guarantee the evaluator makes and why it exists; read it before changing engine
behaviour, and never weaken a case to make a change pass.
Layout
src/engine/ the evaluator
index.ts EngineManager — host side: lifecycle, queueing, output, snapshots
guest.ts the Bun process that owns the namespace and runs cells
protocol.ts typed, authenticated framing between the two
transform.ts cell source → executable body
npm.ts lazy, isolated installs behind npm: imports
src/extension/ the pi integration
index.ts tool registration, session wiring
prompt.ts the system prompt
subagents.ts spawning, registry, file-based results
render-core.ts cell layout (pure)
render.ts binds pi's theme and width primitives to it