Pi
Image updates
Ready-made agent images are updated every morning to include the current CLI version. To use a fixed image version, pin a template version.
Pi is an extensible AI agent with a choice of models and providers. Its core tools read, write, and edit files and run commands. Skills and extensions add other capabilities. Use the ready-made pi template in AgentBox.
Connect
Install the AgentBox SDK and set AGENTBOX_API_KEY. This example uses OpenRouter: it reads OPENROUTER_API_KEY, selects the provider with --provider openrouter, and the model with --model qwen/qwen3-coder-next. Unlike the OpenCode command, this command passes the provider in a separate flag.
Pi supports other providers too. When switching, match --provider, --model, and the key variable: for example, ANTHROPIC_API_KEY for Anthropic or OPENAI_API_KEY for OpenAI. See the Pi provider documentation for the full list.
To access the model, you can use the AgentBox HTTPS proxy at https://sandbox-proxy.agentbox.ru:65181.
The examples pass HTTPS_PROXY, HTTP_PROXY, and NO_PROXY only to the agent command and its child processes. These are process-wide network settings: other agent requests may also use the proxy. NO_PROXY keeps connections to localhost and 127.0.0.1 direct.
Extract meeting actions
The example uploads meeting notes and asks Pi to extract agreed actions, owners, and deadlines into actions.md. An action without an agreed deadline should be marked accordingly. Discussion without a decision goes in a separate section. Replace the input with your own meeting transcript.
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error("OPENROUTER_API_KEY is required");
const proxy =
process.env.SANDBOX_PROXY_URL ?? "https://sandbox-proxy.agentbox.ru:65181";
const sandbox = await Sandbox.create("pi", { timeoutMs: 600_000 });
try {
await sandbox.files.makeDir("/home/user/work");
await sandbox.files.write(
"/home/user/work/meeting.md",
"Meeting: 2026-09-18\nOlga will send a revised quote to Acme by 2026-09-21.\nPavel will check stock for the next delivery; no deadline was agreed.\nThe marketing budget was discussed but no decision was made.\n",
);
const handle = await sandbox.commands.run(
"pi --provider openrouter --model qwen/qwen3-coder-next --no-session -p",
{
cwd: "/home/user/work",
timeoutMs: 300_000,
background: true,
stdin: true,
envs: {
OPENROUTER_API_KEY: apiKey,
HTTPS_PROXY: proxy,
HTTP_PROXY: proxy,
NO_PROXY: "localhost,127.0.0.1",
},
onStderr: (chunk) => process.stderr.write(chunk),
},
);
await handle.sendStdin(
"Read meeting.md and write actions.md: agreed actions, owners, and deadlines. Mark missing deadlines as not agreed. Keep discussion without a decision separate.",
);
await handle.closeStdin();
const result = await handle.wait();
console.log(result.stdout);
console.log(
"actions.md\n" + (await sandbox.files.read("/home/user/work/actions.md")),
);
} finally {
await sandbox.kill();
}-p prints the final response and exits. The task is sent through stdin. Closing the input stream tells the agent that the complete text has arrived. Pi uses its default tools for reading, writing, editing files, and running commands through bash. The example adds no tool restrictions. The AgentBox sandbox provides isolation. --no-session disables history persistence without preventing the agent from writing its output file.
The application reads actions.md before stopping the sandbox. Command or file errors propagate to the caller, and finally deletes the sandbox.
Events and follow-up questions
This example starts Pi with --mode rpc: the application sends JSON commands to stdin and reads events from stdout. The agent first prepares the meeting actions, then receives an updated deadline and edits the same file while keeping the conversation context.
import { Readable } from "node:stream";
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error("OPENROUTER_API_KEY is required");
const proxy =
process.env.SANDBOX_PROXY_URL ?? "https://sandbox-proxy.agentbox.ru:65181";
const sandbox = await Sandbox.create("pi", { timeoutMs: 600_000 });
try {
await sandbox.files.write(
"/home/user/meeting.md",
"Olga will send a revised quote to Acme by 2026-09-21. Pavel will check stock; no deadline was agreed.\n",
);
const messages = new Readable({ objectMode: true, read() {} });
let buffer = "";
const agent = await sandbox.commands.run(
"pi --mode rpc --provider openrouter --model qwen/qwen3-coder-next",
{
cwd: "/home/user",
background: true,
stdin: true,
timeoutMs: 300_000,
envs: {
OPENROUTER_API_KEY: apiKey,
HTTPS_PROXY: proxy,
HTTP_PROXY: proxy,
NO_PROXY: "localhost,127.0.0.1",
},
onStdout(chunk) {
buffer += chunk;
let end;
while ((end = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, end);
buffer = buffer.slice(end + 1);
try {
if (line.trim()) messages.push(JSON.parse(line));
} catch (error) {
messages.destroy(error);
}
}
},
onStderr: (chunk) => process.stderr.write(chunk),
},
);
agent.wait().then(
() => messages.push(null),
(error) => messages.destroy(error),
);
const incoming = messages[Symbol.asyncIterator]();
let nextId = 0;
async function prompt(text) {
const id = String(++nextId);
await agent.sendStdin(
JSON.stringify({ id, type: "prompt", message: text }) + "\n",
);
let accepted = false,
settled = false,
lastAssistant;
while (!accepted || !settled) {
const { value: event, done } = await incoming.next();
if (done) throw new Error("Pi closed before completing the task");
if (event.type === "response" && event.id === id) {
if (!event.success) throw new Error(event.error);
accepted = true;
}
if (
event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta"
) {
process.stdout.write(event.assistantMessageEvent.delta);
}
if (event.type === "message_end" && event.message.role === "assistant")
lastAssistant = event.message;
if (event.type === "agent_settled") settled = true;
}
if (
!lastAssistant ||
["error", "aborted"].includes(lastAssistant.stopReason)
) {
throw new Error(
lastAssistant?.errorMessage ?? "Pi did not complete the response",
);
}
console.log();
}
await prompt(
"Read meeting.md and write actions.md with actions, owners and deadlines. Mark unagreed deadlines explicitly.",
);
await prompt(
"Pavel has now confirmed a deadline of 2026-09-22. Update his action in actions.md; keep the other action unchanged.",
);
console.log(
"actions.md\n" + (await sandbox.files.read("/home/user/actions.md")),
);
} finally {
await sandbox.kill();
}A response with success: true acknowledges the prompt. UI text arrives in message_update → assistantMessageEvent → text_delta. The handler waits for agent_settled and checks the final assistant message: Pi may still retry automatically after agent_end.
The second prompt goes to the same process after the first task finishes. The sandbox is deleted only after reading the updated actions.md. See the Pi protocol for other commands, including aborting a task and selecting a saved session.