Перейти к содержимому

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. Для задачи без согласованного срока агент указывает, что срок не определён. Обсуждение без решения он выносит отдельно. Входной файл можно заменить расшифровкой вашей встречи.

mjs
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. Сначала агент составляет список действий по встрече, затем получает уточнённый срок и обновляет тот же файл, сохраняя контекст разговора.

mjs
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_updateassistantMessageEventtext_delta. Обработчик ждёт agent_settled и проверяет итоговое сообщение: после agent_end Pi ещё может автоматически повторить запрос.

Второй prompt отправляется тому же процессу после завершения первого задания. Песочница удаляется только после чтения обновлённого actions.md. Другие команды, включая остановку задания и выбор сохранённой сессии, описаны в протоколе Pi.