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

Жизненный цикл запуска агента

Запуск — это один прогон одного агента в одном разговоре на одном адаптере. Всё, что с ним связано, происходит на cc_server: клиент отправляет сообщение, а затем наблюдает за потоком событий, идущим обратно. Ни в одном клиенте нет собственного пути запуска.

Агента пробуждают по шести причинам, и запуск несёт их в своём контексте пробуждения, чтобы агент знал, почему он пробуждён:

Причина пробуждения Триггер
userMessage Кто-то отправил сообщение в пространстве или упомянул агента
assignment Агенту назначили тикет
pipelineStep Его запустил шаг конвейера
childCompleted Дочерний тикет завершился
followUp Запланированный heartbeat повторного обращения
recovery Путь восстановления выполнил повторный запуск после сбоя

Контекст пробуждения несёт идентификаторы, связанные с причиной, — тикета, пространства, сообщения, запуска конвейера, — и они внедряются в окружение и промпт агента.

Промпт строится слоями, каждый из которых — раздел с меткой:

  • Идентичность — собственный id агента, имя и id рабочего пространства, с явным указанием передавать эти точные UUID в инструменты с областью видимости рабочего пространства. Рабочее пространство неотъемлемо присуще агенту, поэтому присутствует всегда.
  • Каталог инструментов и протоколы ресурсов, затем структура рабочего пространства.
  • Переопределение системного промпта, если оно у агента есть, затем персона (параметризованная ролью) и брифинг о команде.
  • Навыки — имена, привязанные к агенту.
  • Контракт и процедура выполнения для текущего режима.
  • Контекст пробуждения и контекст упоминания: кто вызвал агента и состав вокруг него.
  • Блок режима — генерируется из профиля возможностей режима и его материализованного списка инструментов, поэтому промпт никогда не может назвать инструмент, которого у запуска на самом деле нет.
  • Контекст памяти — факты, извлечённые для задачи.
  • Контекст разговора — недавняя история пространства, ограниченная окном дословного текста в 50 000 символов независимо от настроенного у агента размера контекста. Более старая история не отбрасывается: агент извлекает её по запросу через get_messages, и семантически релевантные старые сообщения всё равно всплывают.

Команды разделены между двумя местами. /todo и /compact перехватываются в поле ввода и намеренно никогда не сохраняются как сообщения и не отправляются агенту — сохранение /compact сдвинуло бы границу сжатия на ход назад и загрязнило бы транскрипт. /plan обрабатывается в обоих местах: поле ввода переключает режим пространства, и именно поэтому набор инструментов, пресет ограничений и промпт согласуются на этот ход и на каждый следующий. /goal, /loop и /skill:<name> обрабатываются внутри самого запуска и только во встроенном раннере — /goal и /loop начинают автономную работу с бюджетом стоимости, а имя с префиксом skill: ищется как навык, тело SKILL.md которого становится директивой хода. Именно пространство имён разделяет два словаря: сопоставление голого имени с навыками означало, что навык с именем plan или compact был бы навсегда затенён встроенной командой. Голое имя, не совпадающее со встроенной командой, всё ещё сопоставляется — для сообщений, написанных до появления пространства имён. Нераспознанное имя просто проходит дальше как обычный текст.

Прежде чем что-либо запустится, разговор получает собственный рабочий корень, а под ним — изолированный worktree с copy-on-write для каждого репозитория. По умолчанию это все репозитории, привязанные к рабочему пространству; пространство PR — или пространство, созданное с явным выбором, — подготавливает только те, что в списке разрешённых, а разговор для проверки PR привязывает свой единственный репозиторий к head ref pull request, тогда как остальные остаются на ветке по умолчанию. Исходная рабочая копия никогда не изменяется, а весь каталог разговора освобождается при удалении пространства.

Copy-on-write — единственная реализация везде, где поставляется rift, и опирается она на встроенный нативный модуль. Файловая система, которая не умеет reflink, — или каталог данных на томе, отличном от репозитория, — приводят к отказу подготовки, а не к откату. git worktree — это бэкенд на Windows, где нет бэкенда copy-on-write, который можно было бы собрать, и на других хостах он не является тихой деградацией. Отсутствие rift на платформе, где он поставляется, — это сломанная установка: сервер отказывается загружаться, а не тихо откатывается, потому что тихий откат навсегда прячет поломку за более медленным путём.

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

Авторство в Git. Коммиты, которые делает агент, авторятся от имени агента — <Name> (agent) <agentId@agents.control-center.local> и в GIT_AUTHOR_*, и в GIT_COMMITTER_* — никогда не выдавая себя за человека. Локальная часть — это UUID агента, а не slug его имени. Запросивший человек указывается в трейлере Co-Authored-By:, который передаётся в окружении и предписан в системном промпте. Явно заданная вызывающим идентичность всегда побеждает и то и другое.

Идентичность на хосте кода. Учётные данные GitHub для запуска всегда принадлежат только брокеру — токен установки App с областью видимости репозитория или его резервный PAT из окружения — но никогда не собственный сохранённый токен запросившего участника. actingUserId ограничивает резервный PAT, чтобы участник, не являющийся владельцем сервера, не наследовал охват сервера; при этом личный токен не накладывается на окружение агента.

Учётные данные провайдера моделей разрешаются в фиксированном порядке, и порядок имеет значение:

  1. переопределение окружения для конкретного адаптера,
  2. окружение вызывающего,
  3. хранилище учётных данных сервера (с обновлением токена OAuth, если срок его действия близок к истечению),
  4. окружение процесса.

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

Токен GitHub запуска поступает от брокера учётных данных: это токен только для чтения с областью действия на репозиторий worktree. Push проходит через шлюз запусков агентов, который перед передачей спрашивает правило Отправить в удалённый репозиторий, а ключ тикетов не внедряется. См. Песочница и безопасность.

Запуск порождается с его worktree в качестве рабочего каталога и — для Claude Code — со сгенерированным конфигом MCP, чтобы агент мог дотянуться до набора инструментов Control Center. ACP и bash раннера получают очищенное окружение; Claude Code через NoSandboxAdapter наследует окружение хоста.

Запуск передаёт обратно типизированную последовательность событий, которую обработчик потока сохраняет как дельты сообщений, складывает в сегменты транскрипта и превращает в события обмена сообщениями:

Событие Что оно несёт
thinking Вывод рассуждений, отрисованный прямо в строке
text Видимый вывод
toolCall Вызов инструмента с его id и структурированными аргументами
toolResult Соответствующий результат, помеченный, если он завершился ошибкой или представляет собой частичный поток
usage Счётчики токенов и стоимость за окно измерения
error Ошибка; если адаптер сообщил структурированный код, он приводится
sandboxViolation ОС отклонила системный вызов. Выдаётся только там, где песочница действительно оборачивает процесс, — см. заметку выше
debug Диагностика времени выполнения (маркеры запуска, решения ограничений, напоминания о контракте)
done Запуск завершился

Намеренно нигде нет потолка ходов. Жёсткое число итераций убивает законную долгую работу (миграции, уборку после веерного запуска) и не ловит ничего из того, что перечисленные ниже защитные механизмы ловят раньше. Запуск завершается, когда:

  • модель останавливается сама;
  • человек останавливает её или перехватывает разговор (это приостанавливает встроенный раннер на следующей чистой границе хода — запуск Claude Code, у которого нет безопасной границы, вместо этого останавливается);
  • срабатывает сторож тишины. Нет вывода в течение порога — и сессия завершается сбоем silentRun. По умолчанию это 15 минут, в режимах с преобладанием чтения (review и plan) — 10; агент может переопределить порог в диапазоне от 1 до 240 минут. Сторож взводится, когда дочерний процесс сообщает свой PID, поэтому он покрывает CLI Claude Code — и не встроенный раннер, который ничего не порождает;
  • срабатывает потолок стоимости — но только для автономных команд.

Защита от зацикливания — не терминатор: после трёх одинаковых вызовов инструментов подряд цикл направляет агента сменить подход.

Если сложить всё вместе, у обычного интерактивного хода во встроенном раннере нет ни потолка ходов, ни потолка стоимости, ни сторожа тишины. Он завершается, когда останавливается модель или когда останавливаете его вы. Это осознанный компромисс — все альтернативы убивают законную долгую работу, — но он действительно означает, что интерактивный запуск без присмотра не ограничен ничем, кроме собственного суждения модели.

Во время выполнения сообщение, отправленное работающему агенту, ставится в очередь как направление и выгружается на следующей границе хода, так что вы можете подтолкнуть запуск, не убивая его. Направление, пауза, субагенты и контракт завершения — это возможности встроенного раннера; у Claude Code нет ни одной из них.

Каждый запуск записывает AgentRunLog: время начала и окончания, сводку, счётчики токенов и оценку стоимости (включая стоимость, свёрнутую из субагентов), адаптер и модель, идентификатор процесса и путь к журналу, разговор и любой тикет или запуск конвейера, к которому он относится, а также два классифицирующих поля, которые несут большую часть правды.

  • Состояние — грубое: pending, running, completed, error.
  • Живость — более тонкое чтение: alive, productive, completed, blocked, empty, looping, failed, stalled, dead.
  • Семейство ошибок полезно группирует сбои: transientUpstream, sandboxInfrastructure, budgetExceeded, processLost, silentRun, unknown.

Закрытие запуска публикует AgentRunCompleted, и от него зависит многое: свод затрат конвейера, возобновление шага конвейера, ожидавшего этот запуск, завершение связанного тикета, перевод оркестрации в терминальное состояние, решение супервизора цели продолжать или остановиться, роль проверяющего и оценку бюджета после запуска. Состав присутствия обновляется отдельно — по наблюдению за последними журналами запусков, а не по событию. Уведомления «Агент завершил работу» в этом списке нет: событие срабатывает, когда заканчивается поток процесса, ещё до записи ответа, поэтому уведомлением служит MessageReceived этого хода, публикуемое после сохранения сообщения.

Контроль бюджета — это реакция, а не шлюз. Когда запуск завершается, оценщик сравнивает расход агента с начала месяца с его действующим потолком (связанная политика бюджета, если она есть, иначе собственная месячная цифра агента; ноль означает неограниченно). Пересечение мягкого порога фиксирует инцидент-предупреждение; исчерпание бюджета фиксирует жёсткий инцидент и переводит жизненный цикл агента в paused на оргсхеме. Запуск по-прежнему принимает работу от этого агента. Триггер конвейера BudgetThresholdCrossed может запустить последующую работу по тому же событию.

Каждый журнал запуска может нести историю повторных попыток — id родительского запуска, счётчик попыток и сводку продолжения. Дерево запусков разговора показывает номер попытки, когда он не равен нулю. «Повторить» из чата — это новый запуск с новым журналом.