@ian-pascoe/pi-codemode

Process-isolated persistent TypeScript tool composition for Pi

Packages

Package details

extensionskill

Install @ian-pascoe/pi-codemode from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@ian-pascoe/pi-codemode
Package
@ian-pascoe/pi-codemode
Version
0.7.4
Published
Sep 14, 2026
Downloads
2,374/mo · 910/wk
Author
ianpascoe_
License
MIT
Types
extension, skill
Size
356.1 KB
Dependencies
4 dependencies · 6 peers
Pi manifest JSON
{
  "skills": [
    "./skills"
  ],
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@ian-pascoe/pi-codemode

Run persistent TypeScript notebook Cells that compose Pi's registered tools. CodeMode calls the exact handlers Pi registered; it does not contain substitute implementations of built-in tools.

Tested with Pi 0.84.2 and Node 22.19.0. The package installs its pinned deno@2.9.5 runtime, which itself transpiles and executes Cells. Its official npm binaries cover macOS, glibc Linux, and Windows on x64 and arm64.

Install

pi install npm:@ian-pascoe/pi-codemode

From this repository:

pi install git:github.com/ian-pascoe/pi-extensions

Tools

codemode_execute

codemode_execute({
  script: string;
  timeoutMs?: number;
  wait?: boolean;
  sessionId?: string;
});

wait defaults to true. timeoutMs has no default. Omitting sessionId creates a Session with a generated ID; supplying an unknown ID creates a Session under that exact ID.

const result = await tools.read({ path: "README.md" });
return result.content[0];

Set wait: false to return immediately with pending, then poll the returned ID. An accepted asynchronous execution always returns pending, even if its Cell finishes before the outer call returns.

codemode_result

codemode_result({ sessionId: string });

Returns the active or latest terminal result without consuming it.

codemode_cancel

codemode_cancel({ sessionId: string });

Stops the session process and frees its capacity. The cancel call succeeds; subsequent polling returns the retained cancellation failure.

codemode_sessions

codemode_sessions({});

Lists every live Session, with idle Sessions first in least-recently-used order, then running Sessions in least-recently-used order. Listing does not refresh Session recency.

type CodeModeSessionsResult = {
  result: "success";
  sessions: Array<{
    sessionId: string;
    state: "idle" | "running";
    cellCount: number;
    lastActivityAtMs: number;
  }>;
};

The execute, result, and cancel tools return:

type CodeModeConsoleEntry = {
  method: "log" | "info" | "warn" | "error" | "debug";
  text: string;
};

type CodeModeResult =
  | {
      result: "success";
      sessionId: string;
      data?: JsonValue;
      reclaimedSessionId?: string;
      console?: CodeModeConsoleEntry[];
    }
  | { result: "pending"; sessionId: string }
  | {
      result: "failed";
      sessionId: string;
      error: {
        code:
          | "unknown"
          | "busy"
          | "capacity"
          | "eviction"
          | "script"
          | "serialization"
          | "timeout"
          | "cancellation"
          | "termination"
          | "runtime";
        message: string;
      };
      console?: CodeModeConsoleEntry[];
    };

AgentToolResult.content remains exactly this JSON and is the only CodeMode result text returned to the model. Pi retains additional bounded Presentation Snapshots in tool-result details for Transcript replay and the TUI.

Cells may call console.log, console.info, console.warn, console.error, and console.debug. One call creates one ordered entry without a trailing newline, while embedded newlines remain intact:

console.log("answer:", 42);
return 42;
{
  "result": "success",
  "sessionId": "...",
  "data": 42,
  "console": [{ "method": "log", "text": "answer: 42" }]
}

Formatting matches the pinned Deno Console for format tokens, primitives, spacing, and multiline inspection. Getters, coercion hooks, and custom inspectors do not run; CodeMode uses safe inspection instead when that differs from Deno. Console output arrives only with terminal results. Ordinary script, serialization, and worker-reported runtime failures retain prior calls. A timeout, cancellation, termination, or process death may omit them because the parent kills the worker before it can reply.

Transcript and Observer UI

The CodeMode Transcript gives all five tools semantic collapsed and expanded rendering. Session rows prioritize Cell lifecycle, a short Session ID, Cell Ordinal, returned-value shape, Console-call count, nested-tool count, and elapsed time. Expanded rows show the full Session ID, explicit call arguments, TypeScript source, bounded Console output before structured returned data or the error, and bounded nested-tool names, outcomes, and durations. These compact Presentation Snapshots remain separate from the native nested-tool rows below.

When a Cell finishes, its invoked tools appear as a tree of native tool rows in invocation order, including built-in, extension, and MCP tools. They use Pi's normal collapsed/expanded rendering (including saved edit diffs), without an extra CodeMode expansion. Both awaited and background Cells place their tree inside the original codemode_execute entry, labelled by Session and Cell Ordinal. Background completion refreshes that entry without polling or changing the model-facing pending result. Polling never duplicates the tree. History without the owning entry retains a standalone Transcript fallback.

The rows are durable human-only custom entries, excluded from model context. Successful, failed, and cancelled Cells retain their calls; an unfinished call has outcome unknown, not an assertion that its side effects were cancelled. Late results cannot rewrite a settled display or attach it to a new branch or Pi session. Reopening history renders saved data without rerunning any tool. Arguments and results are bounded to Pi's 2,000-line/50-KB display budget per call. Unsafe, oversized, or unavailable native data uses an explicit bounded fallback. Oversized safe results have a private live-session Result Spill when available; those files are not guaranteed after reopening history.

Search rows show the query, result range, and next offset without exposing raw JSON. Expanding a discovery search shows each exact tool name, display group, and description. An exact-name search instead shows the highlighted TypeScript declaration or its declaration-size failure. Search declarations are bounded only in the Transcript; the model-facing result remains unchanged.

Status always uses a symbol and text together:

◉ running   ○ idle   ✓ completed
× failed    ■ cancelled   ■ reclaimed   ! timed out

Awaited Cells publish a presentation update immediately and once per second. Collapsed calls show one highlighted line inline or the first eight highlighted lines of a multi-line Cell, truncating long lines to the viewport width. Expanded calls wrap long lines and show the complete TypeScript source. Returned-data display uses Pi's 2,000-line/50-KB limit; complete oversized data is written to a private Result Spill while the model-facing result remains unchanged. Result Spill files last for the live Pi session. Replayed history falls back to its retained bounded data when a prior spill is no longer available.

In TUI mode, the read-only CodeMode Observer UI appears above the editor during Cell activity. It shows up to eight running, idle, or recently terminal Sessions, uses the shortest unique Session prefix of at least eight characters, and adds … +N more when bounded. The widget disappears ten seconds after every Session becomes idle or terminal and remounts on later activity. It has no controls and issues no hidden CodeMode or Pi tool calls.

Notebook Bindings

Top-level let, const, var, function, class, and destructuring declarations become Notebook Bindings. Later Cells in the same session use them without globalThis:

// Cell 1
let count = 1;
function current() {
  return count;
}

// Cell 2
count += 1;
return current(); // 2

A later declaration may replace an existing binding, including a const. Ordinary assignment to the current const still fails. Existing functions see later assignments and successful redefinitions. A failed declaration initializer preserves the previous value; earlier completed mutations and declarations in the same failing Cell remain committed.

Cells accept TypeScript syntax, which Deno transpiles without type checking. Type annotations therefore do not validate tool inputs or results. Cells support top-level await, explicit return, and automatic return of the final expression. Static and dynamic imports, eval, and dynamic function constructors are unavailable. Annex-B block functions, nested lexical scopes, and declarations beneath a source with statement remain Cell-local.

Notebook Bindings use protected non-configurable Deno global properties internally. Normal unqualified and globalThis assignment both observe const protection. Protected runtime names are rejected.

One Cell may run at a time in each session. Ordinary script and catchable Pi tool failures leave the session reusable. Timeout, cancellation, Pi termination, or process failure destroys that session's heap.

Registered tools

The codemode_execute description contains constant execution and discovery guidance, independent of the live Tool Catalogue. Discover current declarations with direct codemode_search before a Cell or tools.codemode_search inside one:

const page = await tools.codemode_search({
  query: "intent or exact registered name",
  group: "optional display group",
  limit: 10,
  offset: 0,
});

Search returns stable pages with items, total, hasMore, and nextOffset. Empty or fuzzy queries return compact items containing the exact flat name, display-only group, and bounded description. Search that exact name to get its complete declaration. A pathological declaration above the search response bound instead has an explicit declarationError. Call a discovered tool with tools[item.name](input). Unfamiliar tools may require a discovery call in exchange for an execute definition that stays reusable across catalogue changes.

Direct search reads the current Tool Catalogue; in-Cell search reads the Cell's frozen exposure snapshot. Both expose only CodeMode-callable tools and use the same search implementation. Dynamic registry changes update execution policy immediately, but catalogue rendering waits until the next model turn or CodeMode access needs a synchronized snapshot. This keeps bulk tool registration from rebuilding the complete catalogue after every tool.

Ordinary guest tool calls resolve to:

type PiToolResult<Output = unknown> = {
  content: Array<
    { type: "text"; text: string } | { type: "image"; data: string; mimeType: string }
  >;
  details?: Output;
};

Each declaration derives its input from parameters and its details type from the registered definition's optional outputSchema. Source-gated fallback schemas cover Pi's built-in tools and @howaboua/pi-codex-conversion 3.0.25; tool-provided schemas take precedence. Other missing or unsupported output schemas remain unknown.

Ordinary tool failures reject with a catchable CodeModeToolError. A Pi result that requests termination stops the complete CodeMode Session and cannot be caught by guest code.

Exposure settings

Configure Exposure Modes under codemode in ~/.pi/agent/settings.json or a trusted project's .pi/settings.json:

{
  "codemode": {
    "maxSessions": 8,
    "tools": [
      { "pattern": "*", "exposure": "codemode-only" },
      { "pattern": "bash", "exposure": "direct-and-codemode" },
      { "pattern": "browser_*", "exposure": "direct-only" }
    ]
  }
}

Patterns are case-sensitive minimatch globs over exact registered names; the last match wins. Project tools replaces the global array, while project maxSessions overrides only that field. /reload rereads settings.

An unmatched active tool defaults to direct-and-codemode; an inactive tool remains unavailable even when it matches a rule. Exposure rules redistribute Pi's active tools but never reactivate tools disabled by Pi or another extension. The five registered codemode_* tools are always direct-only. Pi's global allowed/excluded registry remains authoritative. Invalid fields or patterns disable CodeMode for that session without changing Pi's active tools.

maxSessions defaults to 8 and counts only live Deno processes. When capacity is full, a new Session gracefully stops the least-recently-used idle Session before starting; its Notebook Bindings are discarded, the new success reports reclaimedSessionId, and polling the old ID returns eviction. Running Sessions are never reclaimed, so admission still returns capacity when every process is busy. Executing or polling refreshes recency; listing and Observer rendering do not. Up to 64 recent worker-free terminal or failed-admission records remain pollable.

Isolation and limits

Each live CodeMode Session owns a pinned Deno subprocess. Deno itself executes unique Blob modules with the application/typescript media type, keeping generated helper source out of ordinary source locations. Every operating-system permission class is denied: filesystem read/write, network, environment, system information, subprocesses, FFI, and remote imports.

Guest code receives ECMAScript built-ins, a read-only tools object, a frozen five-method Console facade, and only a frozen Deno.version identity. Raw process and standard-stream access, Worker, timers, filesystem/network APIs, and module loading are withheld. Console calls never write to worker streams and do not include output from registered Pi tool handlers. The parent watchdog terminates the subprocess for timeout or an infinite loop. Deno/V8 bounds each Session to a 128 MiB old-space heap and a 1 MiB stack. Protocol inputs, tool results, returned data, and Console entries share one 8 MiB UTF-8 worker-message limit. An oversized response becomes the bounded serialization failure and may omit its Console entries.

Registered Pi tools still execute in Pi's parent process with their normal permissions and lifecycle hooks. Cancellation aborts them through Pi's AbortSignal; a handler that ignores that signal cannot be forcibly killed, so its late result is discarded after the CodeMode process stops.

CodeMode uses a capability-gated private Pi AgentSession seam, tested against Pi 0.84.2, to reach wrapped registered handlers and enforce direct exposure. An incompatible Pi version fails closed and leaves active tools unchanged.

Sessions are memory-only in this release and end on Pi reload, session switch, fork, resume, or shutdown. A future persistence format may checkpoint complete JSON-safe Notebook Bindings; V1 neither serializes heaps nor replays Cells.