Code Mode is a tool-use pattern, not a transport:
The model writes JavaScript that calls typed methods. That code runs against an explicit tool surface (
asgrep.*), can fan out work in parallel, filter intermediates, and return only the shaped value the model needs.
That is the same idea as:
- Cloudflare Code Mode — one
codemodetool, typed connector globals - OpenCode CodeMode — orchestration over host-supplied tools (authority = tools you expose, not an OS jail)
- Anthropic programmatic tool calling — tools callable from code execution via
allowed_callers - OpenAI programmatic tool calling — JS in a V8 runtime coordinates tools
Traditional MCP/tool calling does one model round-trip per operation. Code Mode moves loops, branching, filtering, and parallel fan-out into executable code.
Use either Code Mode or MCP in a given client — not both. They are sibling front ends on the same retrieval core. Stacking them doubles tool catalogs, duplicates index opens, and confuses the model about which surface to call.
| Client | Choose |
|---|---|
| Pi | Code Mode via pi install npm:pi-ast-sgrep (asgrep tool). Do not also register asgrep-mcp. |
| MCP hosts (Cursor Cloud, Claude Desktop, Agent Plugins, …) | asgrep-mcp / packages/agent-plugin. Do not also load the Pi Code Mode package in that same agent. |
ast-sgrep-core / native asgrep binary
/ \
/ \
ast-sgrep-mcp Code Mode
(stdio JSON-RPC transport) (JS program → asgrep.*)
one tool call ↔ one RPC model writes JS once
│
▼
asgrep.search()
asgrep.find() / asgrep.read()
Promise.all([...])
filter / shape
│
▼
final value → model
MCP (asgrep-mcp) |
Code Mode | |
|---|---|---|
| Role | Protocol transport for hosts that speak MCP | Execution model: code orchestrates search |
| Unit of work | One tools/call |
One JS program (many calls inside) |
| Parallelism | Host/model schedules calls | Promise.all / loops in the program |
| Pi | Not used | Primary Pi agent surface |
| Coupling | — | Never imports MCP; MCP never imports Code Mode |
pi-ast-sgrep exposes asgrep as the primary tool:
Model ──► asgrep({ code }) ──► in-process `node:vm` (no Worker)
│
│ asgrep.search / find / read / edit
│ Promise.all → same-tick coalesce
│ in-context asgrep + JSON host bridge
│ │
│ ├─ in-process NAPI Session
│ │ (CodeModeSession → core)
│ └─ degraded: CLI sticky serve
▼
shaped return + stats
The runner is in-process (OpenCode / nicknisi: no Worker sandbox, no OS jail).
node:vm hides process / require and can interrupt synchronous loops.
asgrep / console are constructed inside the context from a JSON host bridge
so host Function cannot leak. Node does not consider vm an adversarial-code
security boundary. Same trust as Pi bash.
Each program is limited to 256 host calls, bounded arguments, logs, and serialized results. Raw memory and WebAssembly globals are unavailable. The native Code Mode boundary also caps each encoded tool value at 1 MiB and complete batch responses at 4 MiB.
One deadline covers freshness work and the Code Mode program. The soft wall
aborts the run's AbortSignal, so queued host calls and later asgrep.*
calls cannot keep using the pooled NAPI Session after timeout. Waiters that have not yet
taken the session mutex return operation cancelled instead of blocking the
pool. Read/search calls that already hold the mutex may finish their current
operation; index_repo polls the abort flag during walk/prepare and returns
operation cancelled without committing. The Pi freshness coordinator also
aborts that shared index when the last waiter cancels, so a timed-out search
cannot leave rayon workers running.
Root jail (host duty): CodeModeSession / NAPI tool root args are jailed
under the configured session workspace the same way MCP jails under
ASGREP_ROOT (canonicalize + containment; message
escapes configured workspace). NAPI has no separate resolver — it inherits
Session. Hosts must set Session root intentionally; this is policy confinement,
not an OS security boundary. ASGREP_INDEX_PATH remains a privileged sink
(see docs/env-trust.md).
Wall time ≈ serial + parallel_work / N.
| Serial cost (cut hard) | Parallel fraction |
|---|---|
SQLite open once per session; in-process vm (no Worker spawn) |
Independent search/find/read inside Promise.all |
Same-tick coalesce turns N serial spawn costs into one batch process. Prefer
session-scoped sticky serve (codemode-serve): one warm Searcher per project
root for the whole Pi session — shared by Code Mode programs, direct tools, and
freshness checks (same idea as pi-codex-conversion's long-lived Code Mode host).
Inside a one-shot batch, Rust Auto is always serial warm. Unique search/find
is ~0.5–1 ms; N parallel SQLite opens are the serial wall. Force Parallel
only for an explicit experiment.
| Surface | Process model |
|---|---|
MCP (asgrep-mcp) |
In-process — links ast-sgrep-core, warm Searcher. |
| Pi / Code Mode | In-process NAPI — ast-sgrep-codemode-napi loads CodeModeSession inside Node. Same retrieval core as MCP; no asgrep child on the hot path. |
Install either MCP or the Pi package (Code Mode) — siblings, not a stack. Both should feel like a native grep: warm index, zero process spawn, microseconds-to-milliseconds per lookup after the first open.
CLI codemode-serve remains only as a degraded fallback when the .node addon is missing (unsupported host). Official npm installs ship ast-sgrep-codemode.node inside each @ast-sgrep/<platform> package next to the CLI binary.
# Dev: build the in-process addon for this host
cargo build -p ast-sgrep-codemode-napi --release
npm run build:native -w pi-ast-sgrepThe in-process path removes per-search process startup and reuses one open searcher. This review did not retain a clean before/after benchmark fixture, so no numeric speedup is claimed.
Direct tools (asgrep_search, asgrep_index, asgrep_status) remain for simple one-shot lookups. Prefer Code Mode whenever the task needs composition, parallel lookups, or filtering before the model sees data.
Example the model writes:
async () => {
const seed = await asgrep.search({ query: "auth refresh", limit: 5 });
const hit = seed.hits?.[0];
if (!hit) return { seed };
const [defs, window] = await Promise.all([
asgrep.find({ query: `defs:${hit.symbol}`, limit: 5 }),
asgrep.read({ refs: [hit.ref] }),
]);
return { symbol: hit.symbol, defs: defs.hits, window };
}Runner capabilities: asgrep.*, Promise, JSON, arrays/objects/math. No
direct require, process, fetch, or filesystem globals. The configured wall
deadline interrupts synchronous vm loops and aborts awaited host calls.
Call arguments, logs, and serialized results are capped. There is no Worker:
a busy microtask loop after await can pin the Pi event loop (same as nicknisi
in-process Code Mode). Do not treat this as an OS jail.
Separate library for:
- Typed tool catalog + JSON Schema
- Warm
CodeModeSessionoverast-sgrep-core(fast in-process dispatch for Rust hosts) - Deterministic JSON plan runner (hosts without a JS sandbox)
- Adapters that emit Anthropic / OpenAI / Cloudflare-shaped tool defs for hosts that already provide a code-execution sandbox
It does not depend on ast-sgrep-mcp, and MCP must not depend on it.
cargo test -p ast-sgrep-codemode| Path | Role |
|---|---|
crates/ast-sgrep-codemode |
Rust catalog + session + plan + host adapters |
packages/pi/extension/src/codemode/ |
JS connector + restricted node:vm runner |
packages/pi/extension tool asgrep |
Pi primary Code Mode entry |
crates/ast-sgrep-mcp |
Unrelated MCP transport |
- Linking MCP ↔ Code Mode
- Embedding Cloudflare Workers / V8 isolates in Rust (Pi uses Node
vm; cloud hosts bring their own executor) - Replacing the native binary with a JS search reimplementation