Первый управляемый агент
В этом уроке соберём ревьюера PR на Managed Agents от начала до конца: создадим среду и агента, запустим сессию с репозиторием, отправим задание, прочитаем поток событий и ответим на вызов собственного инструмента. Код разбит на «настройку один раз» и «каждый запуск», потому что именно это разделение чаще всего нарушают. Напоминаем: Managed Agents в бете, SDK сам добавляет нужный бета-заголовок к вызовам client.beta.agents, environments и sessions.
Настройка: среда и агент
Этот код запускается один раз, например скриптом setup.ts, а результат — идентификаторы — сохраняется в конфигурации.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Среда: облачный контейнер с доступом в сеть (нужен для клонирования репозитория)
const environment = await client.beta.environments.create({
name: "pr-review-env",
config: { type: "cloud", networking: { type: "unrestricted" } },
});
// Агент: модель, правила, инструменты. Создаётся ОДИН раз.
const agent = await client.beta.agents.create({
name: "PR Reviewer",
model: "claude-opus-5",
system: `Ты ревьюер кода в продуктовой студии. Репозиторий примонтирован в /workspace/repo.
Проверь изменения ветки относительно main: прочитай diff, посмотри связанный код,
запусти тесты. Замечания формулируй конкретно, со ссылкой на файл и строку.
Финальный список замечаний опубликуй через инструмент post_review_comment.`,
tools: [
{
type: "agent_toolset_20260401",
default_config: { enabled: true, permission_policy: { type: "always_allow" } },
configs: [
{ name: "write", enabled: false },
{ name: "edit", enabled: false },
],
},
{
type: "custom",
name: "post_review_comment",
description: "Публикует итоговый комментарий ревью в pull request. Вызывай один раз, в конце.",
input_schema: {
type: "object",
properties: {
pr: { type: "integer", description: "Номер pull request" },
body: { type: "string", description: "Текст комментария в Markdown" },
},
required: ["pr", "body"],
},
},
],
});
console.log("ENVIRONMENT_ID =", environment.id);
console.log("AGENT_ID =", agent.id, "version", agent.version);Разберём решения. Ревьюер не должен править код, поэтому write и edit выключены; bash, read, grep и остальное — включены и выполняются без подтверждения. Инструмент post_review_comment объявлен как custom: выполнять его будет наш код, и это единственный способ вставить в цикл подтверждение человеком. Обратите внимание, что модель, промпт и инструменты — на агенте. У сессии этих полей нет.
Сохраните agent.id и agent.version в переменные окружения или конфигурацию. Менять поведение агента дальше нужно через обновление (client.beta.agents.update), которое создаёт новую версию, а не через повторное создание.
Каждый запуск: сессия
const AGENT_ID = process.env.AGENT_ID!;
const AGENT_VERSION = Number(process.env.AGENT_VERSION);
const ENVIRONMENT_ID = process.env.ENVIRONMENT_ID!;
async function startReview(prNumber: number, branch: string) {
const session = await client.beta.sessions.create({
agent: { type: "agent", id: AGENT_ID, version: AGENT_VERSION },
environment_id: ENVIRONMENT_ID,
title: `Review PR #${prNumber}`,
resources: [
{
type: "github_repository",
url: "https://github.com/amiga-example/portal",
mount_path: "/workspace/repo",
authorization_token: process.env.GITHUB_TOKEN,
branch,
},
],
});
console.log(`Трасса: https://platform.claude.com/workspaces/default/sessions/${session.id}`);
return session.id;
}Версию мы закрепляем явно: так прогон воспроизводим, и обновление агента не изменит поведение ревьюера, пока вы сами не поднимете AGENT_VERSION. Репозиторий монтируется как ресурс в момент создания сессии. Кроме репозитория ресурсом может быть файл: загрузите его через client.files.upload с purpose: "agent" и укажите { type: "file", file_id, mount_path } — так ревьюеру можно подложить, например, стандарт кодирования команды.
Если нужен жёсткий потолок расхода на один прогон, при создании сессии передайте budget — сумму в минорных единицах валюты строкой ({ type: "limit", max_list_cost: { amount: "500", currency: "USD" } } — пять долларов). Достигнув потолка, сессия не падает, а останавливается в idle с stop_reason: budget_reached; продолжить можно, подняв или сняв бюджет. Для ночного ревью десятков PR это надёжнее любых проверок в вашем коде. Ссылка на трассу печатается сразу: пока агент работает, её удобно держать открытой в консоли (подставьте идентификатор своего рабочего пространства, если ключ не из рабочего пространства по умолчанию).
Отправить задание и читать события
Поток событий отдаёт только то, что произошло после подключения. Поэтому подключаемся к потоку и отправляем сообщение одновременно, а не по очереди.
async function runReview(sessionId: string, prNumber: number) {
const send = (events: any[]) =>
client.beta.sessions.events.send(sessionId, { events });
await Promise.all([
consumeEvents(sessionId, send),
send([
{
type: "user.message",
content: [{ type: "text", text: `Проверь pull request #${prNumber}.` }],
},
]),
]);
}
async function consumeEvents(
sessionId: string,
send: (events: any[]) => Promise<unknown>,
) {
while (true) {
const stream = await client.beta.sessions.events.stream(sessionId);
const pending: Anthropic.Beta.Sessions.BetaManagedAgentsAgentCustomToolUseEvent[] = [];
for await (const event of stream) {
switch (event.type) {
case "agent.message":
for (const block of event.content) {
if (block.type === "text") process.stdout.write(block.text);
}
break;
case "agent.tool_use":
console.log(`\n[инструмент] ${event.name}`);
break;
case "agent.custom_tool_use":
pending.push(event);
break;
case "session.status_idle":
break;
case "session.status_terminated":
return;
}
if (event.type === "session.status_idle") break;
}
if (pending.length === 0) return;
const results = [];
for (const call of pending) {
results.push({
type: "user.custom_tool_result" as const,
custom_tool_use_id: call.id,
content: [{ type: "text" as const, text: await runCustomTool(call) }],
});
}
await send(results);
}
}Логика такая: читаем поток до session.status_idle. Если агент по дороге запросил наш инструмент, сессия остановилась и ждёт; выполняем, отправляем user.custom_tool_result и снова открываем поток. Если запросов не было, агент закончил. session.status_terminated означает, что сессия завершена окончательно (успешно или с ошибкой — это видно на странице сессии).
Свой инструмент с подтверждением
async function runCustomTool(
call: Anthropic.Beta.Sessions.BetaManagedAgentsAgentCustomToolUseEvent,
): Promise<string> {
if (call.name !== "post_review_comment") return `Неизвестный инструмент ${call.name}`;
const { pr, body } = call.input as { pr: number; body: string };
const approved = await askHuman(`Опубликовать ревью в PR #${pr}?\n\n${body}`);
if (!approved) return "Пользователь отклонил публикацию. Ревью не опубликовано.";
await publishComment(pr, body);
return "Комментарий опубликован.";
}Тот же принцип, что в уроке про вызов инструментов: отказ человека — это результат, который агент прочитает и учтёт. Пока человек думает, сессия просто ждёт в idle; никаких таймаутов со стороны Anthropic на это не накладывается, и вы можете вернуться к сессии позже.
Что ещё пригодится
Если ревьюер должен не только читать, но и запускать bash с подтверждением, поменяйте политику для bash на always_ask: сессия будет останавливаться с stop_reason: requires_action, а вы — отвечать событием user.tool_confirmation с result: "allow" или "deny" и необязательным пояснением для агента.
Файлы, которые агент положил в /mnt/session/outputs/ (например, полный отчёт), после завершения можно перечислить через client.files.list({ scope_id: session.id, betas: ["managed-agents-2026-04-01"] }) и скачать.
Если поток оборвался, не рассчитывайте на повтор пропущенных событий: при переподключении сразу запросите историю через client.beta.sessions.events.list(sessionId) и объедините её с потоком, отбрасывая дубликаты по event.id.
Для остановки агента посреди работы есть событие user.interrupt. Для смены поведения — обновление агента и новая версия. Для повторяющихся запусков (ночное ревью всех открытых PR) — механизм развёртываний по расписанию, о котором подробнее в документации.
Попробуйте сами
10–15 мин на рабочем месте- Запустите
setup.tsи создайте среду и агента. Найдите созданного агента в консоли и посмотрите, как выглядит его конфигурация. - Запустите сессию на любом своём открытом репозитории с тестами. Откройте трассу в консоли и понаблюдайте за вызовами инструментов вживую. Отклоните публикацию комментария и посмотрите, что ответит агент.
- Обновите системный промпт агента (например, попросите оценивать ещё и покрытие тестами). Убедитесь, что версия выросла, а сессия с закреплённой старой версией ведёт себя по-старому.
Коротко
- Настройка один раз:
environments.createиagents.create; идентификаторы — в конфигурацию. - Каждый запуск:
sessions.createсо ссылкой на агента (лучше с закреплённой версией) и ресурсами (репозиторий, файлы). - Подключайтесь к потоку событий одновременно с отправкой
user.message: поток не воспроизводит прошлое. - Свой инструмент:
agent.custom_tool_use→ ваш код →user.custom_tool_result; подтверждение человеком живёт здесь. session.status_idle— агент ждёт вас;session.status_terminated— сессия закончена.- Опасные встроенные инструменты — на
always_ask; правки кода ревьюеру выключены. - Менять поведение — обновлением агента (новая версия), не созданием нового.
Видеоверсия
Сценарий озвучки · 480 слов, ≈ 4 мин
В этом уроке соберём ревьюера пул-реквестов как управляемого агента от начала до конца. Код делится на две части — настройка один раз и каждый запуск, — и это разделение важнее любой отдельной строчки.
Настройка. Сначала создаём среду: облачный контейнер с доступом в сеть, он нужен, чтобы клонировать репозиторий. Потом создаём агента. У него имя, модель, системный промпт с правилами ревью и список инструментов. Инструменты двух видов. Первый — встроенный набор: bash, чтение файлов, поиск. Мы включаем его целиком без подтверждений, но выключаем запись и правку файлов: ревьюер не должен править код. Второй — наш собственный инструмент «опубликовать комментарий ревью». Он объявлен как пользовательский: выполнять его будет наш код, и это единственный способ вставить в цикл подтверждение человеком. Скрипт печатает идентификаторы среды и агента, и мы сохраняем их в конфигурацию. Больше этот скрипт не запускается. Менять поведение агента дальше — через обновление, которое создаёт новую версию.
Каждый запуск — это сессия. Создаём её со ссылкой на агента, причём версию закрепляем явно: так прогон воспроизводим, и обновление агента ничего не изменит, пока мы сами не поднимем номер версии. Указываем среду и ресурсы: репозиторий по адресу, ветку и путь, куда его примонтировать. Ресурсом может быть и файл, загруженный заранее, — например, стандарт кодирования команды. Если нужен потолок расхода на прогон, при создании сессии задаём бюджет: сумму в центах строкой. Достигнув потолка, сессия не падает, а останавливается и ждёт, пока вы поднимете или снимете бюджет. Сразу печатаем ссылку на трассу сессии в консоли — пока агент работает, её удобно держать открытой.
Теперь отправляем задание и читаем события. Есть тонкость: поток отдаёт только то, что произошло после подключения. Поэтому мы подключаемся к потоку и отправляем сообщение одновременно, а не по очереди. Дальше цикл. Открываем поток и читаем события. Текст агента печатаем. Вызовы встроенных инструментов логируем. Если пришёл вызов нашего инструмента — запоминаем. Когда пришёл статус «жду», выходим из потока. Если запомненных вызовов нет — агент закончил. Если есть — выполняем каждый, отправляем результаты событием «результат пользовательского инструмента» и снова открываем поток. Статус «завершено» означает, что сессия закончена окончательно.
Наш инструмент — публикация комментария. Внутри: спросить человека, если отказал — вернуть агенту текст «пользователь отклонил публикацию». Отказ — это результат, который агент прочитает и учтёт. Пока человек думает, сессия просто ждёт. Можно вернуться к ней позже.
Ещё несколько вещей, которые пригодятся. Если нужно подтверждать и bash, поменяйте его политику на «всегда спрашивать»: сессия будет останавливаться, а вы — отвечать событием подтверждения с разрешением или отказом. Файлы, которые агент положил в папку результатов, после завершения можно перечислить и скачать. Если поток оборвался, пропущенные события не повторятся: при переподключении запросите историю и объедините её с потоком, отбрасывая дубликаты по идентификатору. Остановить агента посреди работы — событие «прервать». Запускать по расписанию — отдельный механизм развёртываний, подробности в документации.
Итого у нас есть ревьюер, которого не нужно хостить: репозиторий и тесты живут в контейнере у Anthropic, история — в сессии, а единственное, что делает наш код, — подтверждает публикацию. В следующем уроке посмотрим, как всё это разрабатывать быстрее — с помощью Claude Code, который сам читает документацию и пишет интеграцию.
