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. Двойное списание получает высокий приоритет и направляется в биллинг. Задержки доставки — обычный приоритет и запрос в логистику. Агент не должен обещать возврат денег или точную дату доставки без подтверждения.
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, сохраняя остальные обращения.
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 завершает песочницу и освобождает ресурсы.