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

OpenCode

Обновление образов

Готовые образы агентов обновляются каждое утро и содержат актуальную версию CLI. Чтобы использовать фиксированную версию образа, закрепите версию шаблона.

OpenCode — AI-агент с поддержкой разных моделей и провайдеров. Он работает с файлами и выполняет команды внутри песочницы AgentBox. Готовый шаблон opencode уже содержит CLI агента. Приложение передаёт задание, данные и ключ выбранного провайдера.

Подключение

Установите SDK AgentBox и задайте AGENTBOX_API_KEY. В этом примере модель вызывается через OpenRouter: ключ берётся из OPENROUTER_API_KEY и передаётся в песочницу. Выбранная модель — openrouter/qwen/qwen3-coder-next. Для другого провайдера измените модель и его настройку авторизации вместе.

Для доступа к модели можно использовать HTTPS-прокси AgentBox https://sandbox-proxy.agentbox.ru:65181.

Переменные HTTPS_PROXY, HTTP_PROXY и NO_PROXY передаются только команде агента и наследуются её дочерними процессами. Это общие настройки сети процесса: другие запросы агента тоже могут идти через прокси. NO_PROXY оставляет соединения с localhost и 127.0.0.1 прямыми.

Разберите очередь поддержки

Входные данные — три обращения в tickets.csv и правила приоритизации в policy.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("opencode", {
  timeoutMs: 600_000,
  envs: {
    OPENROUTER_API_KEY: apiKey,
    OPENCODE_CONFIG_CONTENT: '{"autoupdate":false,"permission":"allow"}',
  },
});
try {
  await sandbox.files.makeDir("/home/user/work");
  await sandbox.files.write(
    "/home/user/work/tickets.csv",
    "id,topic,message\n101,delivery,Order arrived two days late\n102,payment,Card charged twice for one order\n103,delivery,Tracking has not changed for three days\n",
  );
  await sandbox.files.write(
    "/home/user/work/policy.md",
    "Payment duplicates: high priority, route to billing. Delivery delays: normal priority, ask logistics. Never promise a refund or a delivery date without confirmation.\n",
  );
  await sandbox.commands.run(
    "opencode run --auto --model openrouter/qwen/qwen3-coder-next 'Read tickets.csv. Write triage.md with one row per ticket: id, priority, reason, and a draft reply. Apply policy.md.'",
    {
      envs: {
        HTTPS_PROXY: proxy,
        HTTP_PROXY: proxy,
        NO_PROXY: "localhost,127.0.0.1",
      },
      cwd: "/home/user/work",
      timeoutMs: 300_000,
      onStderr: (chunk) => process.stderr.write(chunk),
    },
  );
  console.log(
    "triage.md\n" + (await sandbox.files.read("/home/user/work/triage.md")),
  );
} finally {
  await sandbox.kill();
}

opencode run выполняет задание без терминального интерфейса. permission: "allow" в OPENCODE_CONFIG_CONTENT разрешает все инструменты, а --auto включает автоматическое подтверждение запросов. Изоляцию обеспечивает AgentBox. Агент может читать и менять файлы песочницы. Здесь он сохраняет triage.md. Приложение читает файл до завершения песочницы.

OPENCODE_CONFIG_CONTENT отключает автоматическое обновление CLI во время запуска. Для воспроизводимой версии агента также закрепите версию шаблона.

Вывод и продолжение работы

Пример разбирает обращения и показывает события по ходу работы. Затем приложение передаёт уточнение из биллинга: двойное списание по обращению 102 подтверждено, создана заявка BILL-204. Агент продолжает ту же сессию и обновляет ответ в triage.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("opencode", {
  timeoutMs: 600_000,
  envs: {
    OPENROUTER_API_KEY: apiKey,
    OPENCODE_CONFIG_CONTENT: '{"autoupdate":false,"permission":"allow"}',
  },
});
try {
  await sandbox.files.makeDir("/home/user/work");
  await sandbox.files.write(
    "/home/user/work/tickets.csv",
    "id,topic,message\n101,delivery,Order arrived two days late\n102,payment,Card charged twice for one order\n103,delivery,Tracking has not changed for three days\n",
  );
  await sandbox.files.write(
    "/home/user/work/policy.md",
    "Payment duplicates: high priority, route to billing. Delivery delays: normal priority, ask logistics. Never promise a refund or a delivery date without confirmation.\n",
  );
  async function execute(prompt, sessionId) {
    let pending = "";
    let result;
    let failure;
    let nextSessionId;
    function event(message) {
      if (message.sessionID) nextSessionId = message.sessionID;
      if (message.type === "text") console.log(message.part.text);
      if (message.type === "tool_use")
        console.log("Tool:", message.part.tool, message.part.state.status);
      if (message.type === "step_finish") result = message.part;
      if (message.type === "error")
        failure = new Error(
          message.error?.data?.message ?? JSON.stringify(message.error),
        );
    }
    function onStdout(chunk) {
      pending += chunk;
      let newline;
      while ((newline = pending.indexOf("\n")) !== -1) {
        const line = pending.slice(0, newline);
        pending = pending.slice(newline + 1);
        if (line.trim()) event(JSON.parse(line));
      }
    }
    const handle = await sandbox.commands.run(
      "opencode run --auto --model openrouter/qwen/qwen3-coder-next --format json" +
        (sessionId ? ' --session "$AGENT_SESSION_ID"' : ""),
      {
        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",
          AGENT_SESSION_ID: sessionId ?? "",
        },
        onStdout,
        onStderr: (chunk) => process.stderr.write(chunk),
      },
    );
    await handle.sendStdin(prompt);
    await handle.closeStdin();
    await handle.wait();
    if (pending.trim()) event(JSON.parse(pending));
    if (failure) throw failure;
    if (result?.reason !== "stop")
      throw new Error("OpenCode did not finish normally: " + result?.reason);
    if (!nextSessionId) throw new Error("OpenCode did not return a session ID");
    console.log("\nSession:", nextSessionId);
    return nextSessionId;
  }
  const sessionId = await execute(
    "Read tickets.csv. Write triage.md with one row per ticket: id, priority, reason, and a draft reply. Apply policy.md.",
  );
  await execute(
    "Billing confirmed a duplicate charge for ticket 102 and opened case BILL-204. Update its draft reply in triage.md with this case number. Keep the other tickets and do not promise a refund date.",
    sessionId,
  );
  console.log(
    "triage.md\n" + (await sandbox.files.read("/home/user/work/triage.md")),
  );
} finally {
  await sandbox.kill();
}

--format json выводит по одному событию на строку. text содержит готовый текстовый блок в part.text, а tool_use сообщает о завершении инструмента и его статусе. Этот режим CLI не выводит текст по отдельным токенам.

Обработчик сохраняет sessionID из событий и передаёт его в --session для второго запроса. Он также обрабатывает error и проверяет причину последнего step_finish: stop означает, что модель закончила ответ. Промежуточные шаги могут завершаться вызовом инструментов. Файл читается после успешного завершения команды, затем песочница удаляется вместе с локальной историей.

Другие параметры — в справочнике OpenCode.

Ошибки и завершение

При ошибке ключа, модели или сетевого доступа не показывайте пустой результат как готовую работу. Обработайте ошибку команды и сохраните диагностику без ключей. Отсутствие triage.md также означает, что ожидаемый результат не получен.

Время команды ограничено пятью минутами, жизнь песочницы — десятью. После чтения файла finally завершает песочницу и освобождает ресурсы.