AmigaОбучение ИИ
Модуль 5 · Управляемые агенты · урок 12 из 14

Первый управляемый агент

11 мин чтения▶ есть видеоверсия

В этом уроке соберём ревьюера PR на Managed Agents от начала до конца: создадим среду и агента, запустим сессию с репозиторием, отправим задание, прочитаем поток событий и ответим на вызов собственного инструмента. Код разбит на «настройку один раз» и «каждый запуск», потому что именно это разделение чаще всего нарушают. Напоминаем: Managed Agents в бете, SDK сам добавляет нужный бета-заголовок к вызовам client.beta.agents, environments и sessions.

Настройка: среда и агент

Этот код запускается один раз, например скриптом setup.ts, а результат — идентификаторы — сохраняется в конфигурации.

typescript
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), которое создаёт новую версию, а не через повторное создание.

Каждый запуск: сессия

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

Отправить задание и читать события

Поток событий отдаёт только то, что произошло после подключения. Поэтому подключаемся к потоку и отправляем сообщение одновременно, а не по очереди.

typescript
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 означает, что сессия завершена окончательно (успешно или с ошибкой — это видно на странице сессии).

Свой инструмент с подтверждением

typescript
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 мин на рабочем месте
  1. Запустите setup.ts и создайте среду и агента. Найдите созданного агента в консоли и посмотрите, как выглядит его конфигурация.
  2. Запустите сессию на любом своём открытом репозитории с тестами. Откройте трассу в консоли и понаблюдайте за вызовами инструментов вживую. Отклоните публикацию комментария и посмотрите, что ответит агент.
  3. Обновите системный промпт агента (например, попросите оценивать ещё и покрытие тестами). Убедитесь, что версия выросла, а сессия с закреплённой старой версией ведёт себя по-старому.

Коротко

  • Настройка один раз: 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, который сам читает документацию и пишет интеграцию.

Отметка хранится только в вашем браузере