@molecule/api-agent-runtime-pi

Provider bond · agent-run · API (Node) · v1.0.2 · Apache-2.0

Pi coding-agent runtime — unattended runs on any Pi-supported model provider in ephemeral cloud sandboxes; artifacts-only return, per-run credentials, deny-by-default egress

npm install @molecule/api-agent-runtime-pi

npm · Source on GitHub · Implements @molecule/api-agent-run

How it works

@molecule/api-agent-runtime-pi is a provider bond on the API (Node) side: it implements the agent-run core interface (@molecule/api-agent-run) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider, requireProvider } from '@molecule/api-agent-run'
import { provider as pi } from '@molecule/api-agent-runtime-pi'

setProvider(pi)

const artifact = await requireProvider().run(
  {
    repoUrl: 'https://github.com/acme/widgets',
    instructions: 'Fix the failing test in src/math.test.ts.',
    model: 'deepseek-v4-pro', // catalog id → deepseek/deepseek-v4-pro
    timeoutMs: 600_000,
  },
  {
    env: {
      GITHUB_TOKEN: fineGrainedToken, // ONE repo, contents:read/write, TTL ≈ timeout
      DEEPSEEK_API_KEY: platformKey, // the key of the model's provider
    },
    onLog: (line) => process.stdout.write(line + '\n'),
  },
)
if (artifact.patch) {
  // HOST-side apply: scan, review, then git apply — never trust the sandbox.
}

Works with: @molecule/api-agent-run, @molecule/api-ai-tools, @molecule/api-code-sandbox

Reference

Pi coding-agent runtime for molecule.dev.

Runs Pi (@earendil-works/pi-coding-agent, MIT) unattended inside an EPHEMERAL cloud sandbox that is created for the run and destroyed before the artifact returns. Pi is provider-neutral, so one runtime runs any catalog model whose provider Pi supports — Anthropic, OpenAI, Google, xAI, DeepSeek, Moonshot, MiniMax, OpenRouter — on that provider's own key and price.

Quick Start

import { setProvider, requireProvider } from '@molecule/api-agent-run'
import { provider as pi } from '@molecule/api-agent-runtime-pi'

setProvider(pi)

const artifact = await requireProvider().run(
  {
    repoUrl: 'https://github.com/acme/widgets',
    instructions: 'Fix the failing test in src/math.test.ts.',
    model: 'deepseek-v4-pro', // catalog id → deepseek/deepseek-v4-pro
    timeoutMs: 600_000,
  },
  {
    env: {
      GITHUB_TOKEN: fineGrainedToken, // ONE repo, contents:read/write, TTL ≈ timeout
      DEEPSEEK_API_KEY: platformKey, // the key of the model's provider
    },
    onLog: (line) => process.stdout.write(line + '\n'),
  },
)
if (artifact.patch) {
  // HOST-side apply: scan, review, then git apply — never trust the sandbox.
}

Type

provider

Installation

npm install @molecule/api-agent-runtime-pi @molecule/api-agent-run @molecule/api-ai-tools @molecule/api-code-sandbox

API

Interfaces

PiProviderAccess

How the sandbox reaches one Pi model provider: the environment variable Pi reads its key from, and the API host the egress allowlist must open.

interface PiProviderAccess {
  /** Environment variable Pi reads the provider's key from, e.g. `ANTHROPIC_API_KEY`. */
  keyEnv: string
  /** API hostname Pi calls for this provider, e.g. `api.anthropic.com`. */
  host: string
}

PiRunSummary

What one pi --mode json invocation reported, condensed.

interface PiRunSummary {
  /** Whether the stream opened with Pi's session header. */
  sawSessionHeader: boolean
  /** Whether `agent_settled` arrived — Pi's "no more automatic work" signal. */
  settled: boolean
  /** `stopReason` of the last completed assistant message (`stop`, `toolUse`, `length`, `error`, `aborted`, …). */
  lastStopReason?: string
  /** `errorMessage` of the last completed assistant message, when it failed. */
  lastErrorMessage?: string
  /** Prose of the last completed assistant message. */
  lastText: string
  /** Every distinct `provider/model` that answered, in order of first appearance. */
  answeredBy: string[]
  /** The final error of an automatic retry sequence that gave up. */
  retryFailure?: string
  /** Token usage summed over every assistant `message_end` and every successful compaction. */
  usage?: AgentRunUsage
  /** One short line per tool call and notable event, for the run's logs. */
  lines: string[]
  /** Records that were not JSON (Pi reserves stdout for JSONL, so these are anomalies). */
  unparsed: number
}

PiRuntimeConfig

Options for createProvider.

interface PiRuntimeConfig extends AgentRunConfig {
  /**
   * npm package spec for the Pi CLI, installed at run start from the
   * egress-allowed npm registry. Always an EXACT version. Default
   * `@earendil-works/pi-coding-agent@1.0.0`.
   */
  cliPackage?: string
  /**
   * Model used when a spec names none. A catalog id (`claude-sonnet-5-5`) found
   * in the model map, or a fully qualified Pi `provider/id`. Default
   * `claude-sonnet-5-5`.
   */
  defaultModel?: string
  /**
   * Extra entries for the catalog-id → Pi `provider/id` map, merged over
   * `DEFAULT_MODEL_MAP`.
   */
  modelMap?: Record<string, string>
  /**
   * Extra Pi providers (or overrides), merged over `PI_PROVIDERS`. A run
   * on a provider found in neither is refused before any sandbox is created.
   */
  providers?: Record<string, PiProviderAccess>
  /**
   * Trust the repository's project-local Pi files (`.pi/settings.json`,
   * `.pi/mcp.json`, `.pi/extensions`, `.pi/SYSTEM.md`, project skills) for the
   * run — `--approve` instead of the default `--no-approve`. Repo-supplied
   * extensions are executable code; enable only for repositories you trust.
   * Default false.
   */
  approveProjectFiles?: boolean
  /**
   * Tool selection passed as `--tools`. Default: Pi's own default
   * (`read`, `bash`, `edit`, `write`).
   */
  tools?: string[]
  /**
   * Extra seconds granted to the ENVIRONMENT beyond the task budget — the
   * clone + CLI install happen before the agent starts, and the artifact
   * collection happens after. Default 240000 (4 minutes).
   */
  setupSlackMs?: number
}

ResolvedPiModel

A model resolved to what the Pi CLI needs.

interface ResolvedPiModel {
  /** Pi provider name, e.g. `anthropic`. */
  provider: string
  /** Pi model id within the provider, e.g. `claude-sonnet-5-5`. */
  model: string
  /** The fully qualified `provider/id` passed to `--model`. */
  qualified: string
  /** How the sandbox reaches the provider. */
  access: PiProviderAccess
}

Classes

PiAgentRuntime

Pi coding-agent runtime backed by an ephemeral cloud sandbox.

Functions

createProvider(config)

Create a Pi agent runtime.

function createProvider(config?: PiRuntimeConfig): AgentRuntimeProvider
  • config — CLI package, default model, model map, providers, budgets.

Returns: An AgentRuntimeProvider running the Pi CLI in ephemeral sandboxes.

resolvePiModel(id, modelMap, providers)

Resolve a catalog id or a fully qualified Pi id to the provider, model and credential the run needs.

function resolvePiModel(
  id: string,
  modelMap: Readonly<Record<string, string>>,
  providers: Readonly<Record<string, PiProviderAccess>>,
): ResolvedPiModel
  • id — Catalog id (claude-sonnet-5-5) or Pi id (openrouter/qwen/qwen3-coder).
  • modelMap — Catalog-id map (defaults merged with config).
  • providers — Provider access table (defaults merged with config).

Returns: The resolved model.

splitJsonlRecords(text)

Split a JSONL stream into records exactly as Pi's framing requires: on LF only, stripping one optional preceding CR. Blank records are dropped.

function splitJsonlRecords(text: string): string[]
  • text — Raw stdout.

Returns: The record strings.

summarizePiEvents(stdout)

Condense a pi --mode json stdout into what the runtime reports.

function summarizePiEvents(stdout: string): PiRunSummary
  • stdout — The raw JSONL stream.

Returns: The run summary.

Constants

DEFAULT_MODEL_MAP

molecule catalog model id → Pi provider/id, for every catalog model Pi 1.0.0 serves on the provider's own pay-as-you-go API. Catalog models absent here (e.g. Qwen, GLM — Pi reaches those only through coding/token plans) must be passed as a fully qualified Pi id or added via modelMap.

const DEFAULT_MODEL_MAP: Readonly<Record<string, string>>

DEFAULT_PI_CLI_PACKAGE

The exact Pi CLI version the runtime installs by default.

const DEFAULT_PI_CLI_PACKAGE: '@earendil-works/pi-coding-agent@1.0.0'

PI_PROVIDERS

Pi providers the runtime knows how to credential and allowlist. Keys are Pi's provider names (the part before / in a model id).

const PI_PROVIDERS: Readonly<Record<string, PiProviderAccess>>

provider

The provider implementation (wire with the agent-run core's setProvider).

const provider: AgentRuntimeProvider

RUNTIME_ALLOWED_HOSTS

Hosts the runtime's own tooling needs, always allowed on top of the spec's and the model provider's API host. pi.dev is deliberately absent: the CLI runs with --offline, PI_SKIP_VERSION_CHECK=1 and PI_TELEMETRY=0.

const RUNTIME_ALLOWED_HOSTS: readonly [
  'github.com',
  'api.github.com',
  'codeload.github.com',
  'objects.githubusercontent.com',
  'registry.npmjs.org',
]

Core Interface

Implements @molecule/api-agent-run interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-agent-run'
import { provider } from '@molecule/api-agent-runtime-pi'

export function setupAgentRuntimePi(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-agent-run >=1.0.0
  • @molecule/api-ai-tools >=1.0.0
  • @molecule/api-code-sandbox >=1.0.0

Runtime Dependencies

  • @molecule/api-agent-run

  • @molecule/api-ai-tools

  • @molecule/api-code-sandbox

  • Isolation contract (same as the Claude Code runtime): the sandbox is created through the code-sandbox bond's MINIMAL path; credentials ride ONLY the exec calls that need them — the clone authenticates through an in-sandbox GIT_ASKPASS helper staged at an unguessable path and removed after (the GitHub token never appears in a clone URL or git argv), the model provider's key the agent; egress is deny-by-default where the sandbox supports network policy and is PROBED before any credential is injected; the sandbox is destroyed on every exit path. Never run Pi on the API host: it has no permission prompts and no sandbox of its own.

  • A sandbox provider without per-run egress enforcement is refused (M-3): if the bonded provider has no applyNetwork, the run fails before any credential is injected — an unimplemented capability is inconclusive, never safe.

  • Caller input is validated before any exec (M-1): repoUrl must match https://github.com/<owner>/<repo> (same gate the REST route applies), a non-empty GITHUB_TOKEN must have a GitHub token shape (ghp_/gho_/ghu_/ghs_/github_pat_ + ≥20 more characters), and every allowedHosts entry must be a bare hostname. Anything else refuses the run before a sandbox exists.

  • Pass the model provider's key under Pi's env var name — ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY, MOONSHOT_API_KEY, MINIMAX_API_KEY or OPENROUTER_API_KEY. A missing key fails the run before any sandbox is created. Only that one key enters the agent's environment.

  • Models are resolved to a fully qualified Pi provider/id. Pi's --model fuzzy-matches, so a bare id could silently pick another model. Catalog ids in DEFAULT_MODEL_MAP are mapped; anything else must be passed as provider/id (e.g. openrouter/qwen/qwen3-coder) or added via createProvider({ modelMap }) — an unmapped bare id is refused. Pi's thinking suffix works too (anthropic/claude-opus-5-5:high). The run is marked failed if Pi reports that a different model answered.

  • Pi's exit code does not mean success. In --mode json a failed or aborted model call still exits 0, so exitStatus comes from the event stream: the last assistant stopReason (error → failed, aborted → cancelled), a retry sequence that gave up (failed), and the stream ending before agent_settled (failed, or timeout at the deadline).

  • Project-local Pi files are ignored by default (--no-approve): a repo's .pi/settings.json, .pi/mcp.json, .pi/extensions and .pi/SYSTEM.md do not load. createProvider({ approveProjectFiles: true }) loads them — repo-supplied extensions are executable code, so enable it only for repositories you trust. AGENTS.md / CLAUDE.md always load.

  • No background network calls: the CLI runs with --offline, PI_SKIP_VERSION_CHECK=1 and PI_TELEMETRY=0, so the egress allowlist is just the model provider's API host, GitHub and the npm registry — never add pi.dev.

  • The CLI is installed at run start, pinned EXACTLY (@earendil-works/pi-coding-agent@1.0.0, with --ignore-scripts); its transitive dependencies are not pinned by npm. The sandbox image needs Node.js ≥ 22.19 — the run fails with that message otherwise.

  • Default tools are read, bash, edit, write — no grep/find/ls (the model searches with bash). Pass createProvider({ tools: ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'] }) to add them. Pi has no sub-agents and no plan mode.

  • usage sums every assistant response plus context-compaction summaries (Pi compacts long runs automatically, and those calls are billed too).

  • Cancellation is cooperative: a signalled abort destroys the sandbox at the next phase boundary and the artifact reports exitStatus: 'cancelled'.