Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ off. Restoring a session needs Herdr 0.9.2 or later.
To start other agents in Herdr panes from Pi-Bolt, install [pi-herdr](https://www.npmjs.com/package/@andrewjacop/pi-herdr):
`pi-bolt install npm:@andrewjacop/pi-herdr`.

## Editors

`pi-bolt --acp` speaks the [Agent Client Protocol](https://agentclientprotocol.com), so Pi-Bolt runs in the agent panel of Zed,
JetBrains IDEs, VS Code (through an ACP extension), Neovim and other ACP editors, with your Pi settings, extensions, skills and
sessions. In Zed's `settings.json`:

```json
"agent_servers": {
"Pi-Bolt": { "type": "custom", "command": "/full/path/to/pi-bolt", "args": ["--acp"], "env": {} }
}
```

Other editors, and what the editor gets: [docs/ACP.md](docs/ACP.md).

## How it works

<picture>
Expand All @@ -247,6 +261,7 @@ in [`sources.json`](sources.json) with Pi-Bolt's changes on top; the engine chan
| [Building](docs/BUILDING.md) | Building the runtime and Pi from source, training profiles, tests |
| [Benchmarks](docs/BENCHMARKS.md) | Method, results and raw data |
| [Plugins](docs/PLUGINS.md) | Porting Pi extensions and writing them for AOT |
| [Editors](docs/ACP.md) | Pi-Bolt in Zed, JetBrains IDEs, VS Code and other ACP editors |
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Diagnostics, environment variables, known limitations |
| [Releasing](docs/RELEASING.md) | CI, releases, signing |

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.6
0.8.7
70 changes: 70 additions & 0 deletions docs/ACP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Editors (Agent Client Protocol)

`pi-bolt --acp` runs Pi-Bolt as an [Agent Client Protocol](https://agentclientprotocol.com) (ACP) agent: editors that speak ACP
start it and show it in their agent panel. That covers Zed, JetBrains IDEs, VS Code (through an ACP extension), Neovim, Emacs and
[the other ACP clients](https://agentclientprotocol.com/get-started/clients). It is built in, so there is nothing to install. The
ACP code loads only when `--acp` is given; the terminal UI, `-p`, and RPC mode do not load it.

Editors launched from the desktop may not see your shell's `PATH`: give them the full path of the executable (`which pi-bolt`).

## Setting it up

**Zed**: in `settings.json`:

```json
"agent_servers": {
"Pi-Bolt": { "type": "custom", "command": "/full/path/to/pi-bolt", "args": ["--acp"], "env": {} }
}
```

**JetBrains IDEs** (IntelliJ IDEA, PyCharm, WebStorm, ...): in the AI Chat tool window, choose *Add Custom Agent*, which opens
`~/.jetbrains/acp.json`, and add:

```json
{
"agent_servers": {
"Pi-Bolt": { "command": "/full/path/to/pi-bolt", "args": ["--acp"], "env": {} }
}
}
```

With `"use_idea_mcp": true` in its `default_mcp_settings`, the IDE also passes its own MCP server, and Pi-Bolt gets the IDE's
tools.

**VS Code, Neovim, Emacs and others**: add a custom agent in the ACP client of your choice (for example the
[ACP Client](https://github.com/formulahendry/vscode-acp) extension for VS Code, or CodeCompanion for Neovim) with the command
`pi-bolt` and the argument `--acp`.

Other flags work as on the command line, for example `["--acp", "--model", "anthropic/claude-sonnet-4-5"]` or
`["--acp", "--no-mcp"]`.

## What the editor gets

- **Your Pi setup.** Each editor session is a Pi session made as the terminal makes one: settings, models and logins
(`/login` in `pi-bolt`), extensions, skills, prompt templates, context files and `mcp.json` all apply. There is no separate
sign-in in the editor; when no model is set up, a prompt says how to set one up.
- **Streaming replies and thinking**, as the model writes them.
- **Tool calls** with a title, a kind (read, edit, run, search) and the files they touch, so the editor can follow along. Edits and
writes show as diffs, and commands show their output while they run (in a terminal view, with colours, in editors that
offer one, such as Zed).
- **Model and thinking level** as session settings the editor shows next to the prompt. Changing them affects that session only.
- **Slash commands**: extension commands, prompt templates, skills (`/skill:name`), and `/compact` and `/name`.
- **Context usage and cost** after each prompt.
- **Sessions**: Pi's own session files. The editor lists them (`session/list`), opens one with its history (`session/load`), or
continues one without it (`session/resume`). Sessions started in the editor open in the terminal with
`pi-bolt --session <id>`, and the other way round.
- **The editor's MCP servers**, passed when a session starts, connect next to those of `mcp.json` (stdio and HTTP; legacy SSE is not
supported). `/mcp` lists them. A server of the same name in `mcp.json` takes precedence.
- **Images and files** in prompts: images are attached, `@` mentions read as the file's path, and embedded file contents are added to
the prompt as `<file>` blocks.
- **Cancel** stops the model and any running tool.

Pi runs its own tools on the files and runs commands in its own shell. Extensions that ask the user something reach the editor:
`confirm` and `select` as a permission request, `input` and `editor` as a form when the editor supports forms (otherwise they get
no answer), and `notify` as a notice when the editor shows notices.

## Project trust

A project with its own extensions, settings or packages in `.pi/` is trusted as in the other headless modes: by a decision saved
earlier, or by the `defaultProjectTrust` setting. ACP sessions cannot ask, so to trust a project, open it once with `pi-bolt` in a
terminal and trust it there. (`--approve` in the agent's arguments trusts every project the editor opens.)
64 changes: 64 additions & 0 deletions docs/releases/0.8.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Pi-Bolt 0.8.7 runs in editors: `pi-bolt --acp` serves the Agent Client Protocol. Everything else is [0.8.6](0.8.6.md).

### Editors

[Agent Client Protocol](https://agentclientprotocol.com) (ACP) editors start Pi-Bolt and show it in their agent panel: Zed,
JetBrains IDEs, VS Code (through an ACP extension), Neovim, Emacs and the other ACP clients. It is built in; point the editor at
`pi-bolt` with the argument `--acp`. In Zed's `settings.json`:

```json
"agent_servers": {
"Pi-Bolt": { "type": "custom", "command": "/full/path/to/pi-bolt", "args": ["--acp"], "env": {} }
}
```

- **Your Pi setup:** each editor session is a Pi session with your settings, models and logins, extensions, skills, prompt
templates, context files and `mcp.json`.
- **Streaming replies and thinking**; **tool calls** with their kind and the files they touch, edits and writes as diffs, and
command output while it runs (in Zed, in a terminal view with colours).
- **Model and thinking level** as session settings; extension commands, prompt templates, skills, `/compact` and `/name` as slash
commands; context usage and cost after each prompt.
- **Sessions** are Pi's own session files: the editor lists them, opens one with its history or continues it, and a session
started in the editor opens in the terminal with `pi-bolt --session <id>`.
- **The editor's MCP servers** connect next to those of `mcp.json`.
- **Extension dialogs** reach the editor: confirm and select as permission requests, input and editor as forms, notify as notices.

The ACP code loads only with `--acp`: the terminal UI, `-p` and RPC mode start and run as in 0.8.6. Setting up other editors:
[docs/ACP.md](https://github.com/opensec-git/Pi-Bolt/blob/pi-bolt/docs/ACP.md).

### Windows

The Windows builds' prebuilt heap is laid out in the order Pi 1.2.0 uses it, recorded on GitHub's Windows runner.

### Engine

Unchanged since 0.8.5: this release's runtimes are [0.8.5's](0.8.5.md#engine).

### Install

```bash
curl -fsSL https://pi-bolt.opensec.in/install.sh | sh
```

```powershell
irm https://pi-bolt.opensec.in/install.ps1 | iex
```

```bat
powershell -c "irm https://pi-bolt.opensec.in/install.ps1 | iex"
```

or `pi-bolt update`, or `npm install -g --allow-scripts=pi-bolt pi-bolt`, or `bun add -g --trust pi-bolt`.

| Asset | For |
|---|---|
| `pi-bolt-linux-x64.tar.xz` / `.tar.gz` | **Recommended on Linux.** CPUs with AVX2 (Intel Haswell 2013+, AMD Zen+). |
| `pi-bolt-linux-x64-baseline.tar.xz` / `.tar.gz` | Any x86-64 CPU with SSE4.2. |
| `pi-bolt-linux-x64-jit.tar.xz` / `.tar.gz` | JIT also on, for plugins loaded at run time. |
| `pi-bolt-darwin-arm64.tar.xz` / `.tar.gz` | **macOS.** Macs with Apple silicon, macOS 13 or later. |
| `pi-bolt-darwin-arm64-jit.tar.xz` / `.tar.gz` | macOS, JIT also on. |
| `pi-bolt-win32-x64.zip` | **Windows.** Windows 10 (1809) or 11 on x64; compiled code on AVX2 CPUs, bytecode on others. |
| `pi-bolt-win32-x64-jit.zip` | Windows, JIT also on. |
| `pi-bolt-runtime-linux-x64.tar.gz`, `pi-bolt-runtime-darwin-arm64.tar.gz`, `pi-bolt-runtime-win32-x64.zip` | The Pi-Bolt Bun runtime, for compiling plugins in. |

Linux x86-64 with glibc 2.17 or later; macOS 13 or later on Apple silicon; Windows 10 1809 or later, or Windows 11, on x64.
2 changes: 1 addition & 1 deletion npm/bin/pi-bolt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# replaces this script with the native executable (exec), so the launcher costs no startup time.
set -eu

VERSION=0.8.6
VERSION=0.8.7
HOME_DIR="${PIBOLT_HOME:-$HOME/.pi-bolt}"

if [ "$(uname -s)" = Darwin ]; then PLATFORM=darwin; else PLATFORM=linux; fi
Expand Down
2 changes: 1 addition & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pi-bolt",
"version": "0.8.6",
"version": "0.8.7",
"description": "Pi, the coding agent, compiled ahead of time to native code: one executable for Linux x86-64, macOS on Apple silicon and Windows x64, starts 2-3x sooner than Pi on Bun and uses about a third of its CPU over a session",
"keywords": [
"pi",
Expand Down
13 changes: 8 additions & 5 deletions packages/coding-agent/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { COMMAND_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR, PRODUCT_
import type { ExtensionFlag } from "../core/extensions/types.ts";
import { getToolListError, type TuiMode } from "../core/settings-manager.ts";

export type Mode = "text" | "json" | "rpc";
export type Mode = "text" | "json" | "rpc" | "acp";

export interface Args {
provider?: string;
Expand Down Expand Up @@ -96,18 +96,20 @@ export function parseArgs(args: string[]): Args {
} else if (arg === "--mode") {
const mode = args[i + 1];
if (mode === undefined || mode.startsWith("-")) {
result.diagnostics.push({ type: "error", message: "--mode requires text, json, or rpc" });
result.diagnostics.push({ type: "error", message: "--mode requires text, json, rpc, or acp" });
continue;
}
i++;
if (mode !== "text" && mode !== "json" && mode !== "rpc") {
if (mode !== "text" && mode !== "json" && mode !== "rpc" && mode !== "acp") {
result.diagnostics.push({
type: "error",
message: `Invalid mode "${mode}". Valid values: text, json, rpc`,
message: `Invalid mode "${mode}". Valid values: text, json, rpc, acp`,
});
continue;
}
result.mode = mode;
} else if (arg === "--acp") {
result.mode = "acp";
} else if (arg === "--continue" || arg === "-c") {
result.continue = true;
} else if (arg === "--resume" || arg === "-r") {
Expand Down Expand Up @@ -303,7 +305,8 @@ ${chalk.bold("Options:")}
--api-key <key> API key (defaults to env vars)
--system-prompt <text> System prompt (default: coding assistant prompt)
--append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)
--mode <mode> Output mode: text (default), json, or rpc
--mode <mode> Output mode: text (default), json, rpc, or acp
--acp Agent Client Protocol server on stdin/stdout, for editors (same as --mode acp)
--print, -p Non-interactive mode: process prompt and exit
--continue, -c Continue previous session
--resume, -r Select a session to resume
Expand Down
150 changes: 150 additions & 0 deletions packages/coding-agent/src/extensions/acp/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* JSON-RPC 2.0 over newline-delimited JSON, both ways: ACP clients call the agent's methods, and the agent calls the
* client's (permission requests, elicitation). Requests are served concurrently, so `session/cancel` and the other
* methods are answered while a `session/prompt` is still running.
*/

import { AcpErrorCode } from "./types.ts";

type JsonRpcId = string | number;

/** Thrown by method handlers to answer with a specific JSON-RPC error. */
export class AcpError extends Error {
readonly code: number;
readonly data?: unknown;

constructor(code: number, message: string, data?: unknown) {
super(message);
this.name = "AcpError";
this.code = code;
this.data = data;
}
}

export type AcpRequestHandler = (method: string, params: unknown) => Promise<unknown>;
export type AcpNotificationHandler = (method: string, params: unknown) => void;

interface PendingRequest {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}

export class AcpConnection {
private readonly write: (line: string) => void;
private readonly onRequest: AcpRequestHandler;
private readonly onNotification: AcpNotificationHandler;
private readonly pending = new Map<JsonRpcId, PendingRequest>();
private nextId = 0;
private closed = false;

constructor(options: {
write: (line: string) => void;
onRequest: AcpRequestHandler;
onNotification: AcpNotificationHandler;
}) {
this.write = options.write;
this.onRequest = options.onRequest;
this.onNotification = options.onNotification;
}

/** Handles one line received from the client. */
receive(line: string): void {
if (line.trim() === "") return;
let message: unknown;
try {
message = JSON.parse(line);
} catch (error) {
this.send({ jsonrpc: "2.0", id: null, error: { code: AcpErrorCode.ParseError, message: errorText(error) } });
return;
}
if (!isRecord(message)) {
this.send({
jsonrpc: "2.0",
id: null,
error: { code: AcpErrorCode.InvalidRequest, message: "Not a JSON-RPC message" },
});
return;
}
const id = message.id;
const hasId = typeof id === "string" || typeof id === "number";
if (typeof message.method === "string") {
if (hasId) void this.serve(id, message.method, message.params);
else this.notification(message.method, message.params);
return;
}
if (hasId && ("result" in message || "error" in message)) {
const request = this.pending.get(id);
if (!request) return;
this.pending.delete(id);
if (isRecord(message.error)) {
request.reject(
new AcpError(
typeof message.error.code === "number" ? message.error.code : AcpErrorCode.InternalError,
typeof message.error.message === "string" ? message.error.message : "Client error",
message.error.data,
),
);
} else {
request.resolve(message.result);
}
}
}

/** Calls a client method and waits for its result. */
request<T>(method: string, params: unknown): Promise<T> {
if (this.closed) return Promise.reject(new Error("The ACP connection is closed"));
const id = this.nextId++;
return new Promise<T>((resolve, reject) => {
this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject });
this.send({ jsonrpc: "2.0", id, method, params });
});
}

notify(method: string, params: unknown): void {
if (this.closed) return;
this.send({ jsonrpc: "2.0", method, params });
}

/** Fails the requests still waiting for the client, which has gone. */
close(): void {
this.closed = true;
for (const request of this.pending.values()) request.reject(new Error("The ACP connection is closed"));
this.pending.clear();
}

private async serve(id: JsonRpcId, method: string, params: unknown): Promise<void> {
try {
const result = await this.onRequest(method, params);
this.send({ jsonrpc: "2.0", id, result: result ?? null });
} catch (error) {
const code = error instanceof AcpError ? error.code : AcpErrorCode.InternalError;
const data = error instanceof AcpError ? error.data : undefined;
this.send({
jsonrpc: "2.0",
id,
error: data === undefined ? { code, message: errorText(error) } : { code, message: errorText(error), data },
});
}
}

private notification(method: string, params: unknown): void {
try {
this.onNotification(method, params);
} catch {
// Notifications have no response to carry an error.
}
}

private send(message: object): void {
if (this.closed) return;
this.write(`${JSON.stringify(message)}\n`);
}
}

export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading