@shift-labs/pi-rlm

A pi extension with a single tool: execute, running TypeScript in a persistent Bun evaluator

Packages

Package details

extension

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