Pi
Обновление образов
Готовые образы агентов обновляются каждое утро и содержат актуальную версию CLI. Чтобы использовать фиксированную версию образа, закрепите версию шаблона.
Pi — расширяемый AI-агент с выбором модели и провайдера. Его базовые инструменты читают, записывают и редактируют файлы, а также выполняют команды. Навыки и расширения добавляют другие возможности. В AgentBox используйте готовый шаблон pi.
Подключение
Установите SDK AgentBox и задайте AGENTBOX_API_KEY. В примере выбран OpenRouter: ключ берётся из OPENROUTER_API_KEY, провайдер задаётся флагом --provider openrouter, модель — --model qwen/qwen3-coder-next. В отличие от команды OpenCode, здесь имя провайдера передаётся отдельным флагом.
Pi поддерживает и других провайдеров. При замене провайдера согласуйте --provider, --model и переменную ключа: например, ANTHROPIC_API_KEY для Anthropic или OPENAI_API_KEY для OpenAI. Полный перечень находится в документации Pi.
Для доступа к модели можно использовать HTTPS-прокси AgentBox https://sandbox-proxy.agentbox.ru:65181.
Переменные HTTPS_PROXY, HTTP_PROXY и NO_PROXY передаются только команде агента и наследуются её дочерними процессами. Это общие настройки сети процесса: другие запросы агента тоже могут идти через прокси. NO_PROXY оставляет соединения с localhost и 127.0.0.1 прямыми.
Разберите итоги встречи
Пример загружает заметки и просит Pi выделить согласованные действия, ответственных и сроки в actions.md. Для задачи без согласованного срока агент указывает, что срок не определён. Обсуждение без решения он выносит отдельно. Входной файл можно заменить расшифровкой вашей встречи.
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 печатает финальный ответ и завершает процесс. Задание передаётся через stdin. Закрытие ввода сообщает агенту, что текст получен целиком. Pi использует стандартный набор инструментов, включая чтение, запись, редактирование файлов и выполнение команд через bash. Дополнительные ограничения инструментов не задаются. Изоляцию обеспечивает песочница AgentBox. --no-session отключает сохранение истории, но не мешает записать итоговый файл.
Приложение читает actions.md до завершения песочницы. Ошибки запуска или чтения файла передаются вызывающему коду, а finally удаляет песочницу.
События и следующий вопрос
Пример запускает Pi с --mode rpc: приложение отправляет JSON-команды в stdin и получает события из stdout. Сначала агент составляет список действий по встрече, затем получает уточнённый срок и обновляет тот же файл, сохраняя контекст разговора.
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();
}response с success: true подтверждает приём задания. Текст для интерфейса приходит в message_update → assistantMessageEvent → text_delta. Обработчик ждёт agent_settled и проверяет итоговое сообщение: после agent_end Pi ещё может автоматически повторить запрос.
Второй prompt отправляется тому же процессу после завершения первого задания. Песочница удаляется только после чтения обновлённого actions.md. Другие команды, включая остановку задания и выбор сохранённой сессии, описаны в протоколе Pi.