# Бюджеты внешних вызовов, которые останавливают циклы агентов

Автономным агентам не нужен безграничный доступ к внешним сервисам, чтобы быть полезными. Им нужен четко определенный объем полномочий: сколько раз они могут попробовать, подождать, повторить попытку и потратить средства, прежде чем вернуть задачу человеку. Без такой границы небольшая неоднозначность, например пустой результат поиска или задержка задачи, может превратиться в сотни запросов, повторные побочные эффекты и счет, которого никто не ожидал.

Бюджет внешних вызовов - это договор на время выполнения одного запуска агента. Он ограничивает число попыток, прошедшее время и денежные расходы, а затем явно останавливает работу, когда исчерпан любой из лимитов. Воспринимайте его как рабочее ограничение, а не как рекомендацию в промпте агента. Промпт легко трактовать по-разному, когда модель пытается завершить задачу. Контроль должен находиться за пределами модели.

## Бюджет должен отдельно учитывать попытки, время и деньги

Одного ограничения недостаточно, чтобы охватить все способы расходования ресурсов агентом. Число запросов выявляет циклы. Лимит времени выявляет медленный опрос и паузы между повторами. Лимит расходов выявляет небольшое число дорогих действий. Каждый показатель отражает свой тип сбоя и может остановить запуск, даже если два других ресурса еще доступны.

Считайте **попытки**, а не успешные ответы. Запрос, завершившийся тайм-аутом, все равно мог дойти до провайдера. Он занял сокет, использовал ресурсы провайдера и часто появился в тарифицируемом API. Если считать только успешные ответы, агент сможет бесконечно отправлять неудачные запросы в поисках подходящего результата.

Для каждого запуска отслеживайте как минимум такие значения:

- `attempts_used` и `attempts_remaining`, включая повторы и перенаправления, которые создают новый внешний запрос
- `deadline_at`, используя монотонные часы для измерения длительности, а не системное время
- `reserved_spend` и `settled_spend` в наименьшей практически применимой единице тарификации провайдера
- `side_effect_attempts`, отдельный счетчик действий, которые записывают, отправляют, создают или покупают
- `budget_stop_reason`, где фиксируется первый лимит, из-за которого дальнейшая работа была запрещена

Не объединяйте число запросов с расходами, назначая каждому запросу выдуманную цену. Запрос метаданных и endpoint генерации модели могут стоить по одному запросу, но их счета будут сильно различаться. И наоборот, API может не сообщать цену запроса, который все равно создает облачный ресурс с оплатой. Используйте потолок числа запросов, даже если у вас есть оценка стоимости.

Для начала агенту, который собирает данные из известного набора API, можно задать 40 попыток, 10 минут работы и небольшой фиксированный резерв. Агенту развертывания может требоваться меньше запросов, но более строгий лимит побочных эффектов. Исследовательскому агенту, который переходит по найденным URL, нужен гораздо меньший охват, чем он предложит вначале. Это стартовые гипотезы, а не универсальные настройки. Измеряйте обычные запуски и задавайте ограничения достаточно близко к норме, чтобы прервать необычное поведение до того, как оно превратится в длительную уборку последствий.

Бюджет относится к запуску, а не к процессу модели на весь день. Получив новую задачу, агент должен получить новый идентификатор запуска и новое явно выделенное ограничение. Общий дневной счетчик скрывает дорогой запуск среди обычной работы. Кроме того, одна задача может израсходовать ресурсы, предназначенные для другой.

## Резерв нужно выделить до первого дорогого вызова

Лимиты расходов не работают, если система узнает цену только после покупки. До вызова зарезервируйте максимально возможную плату, сразу уменьшите оставшийся бюджет, а затем сверяйте его с фактическим использованием после ответа провайдера. Если резерв не помещается в бюджет, запретите вызов до того, как он покинет вашу границу контроля.

Представьте агента, который может создать размещенную сборку. Запрос провайдера принимает класс машины, регион, тайм-аут и размер артефакта, но точная сумма появляется после завершения. Шлюзу агента нужна таблица цен или консервативная оценка. Идеального учета не требуется, чтобы принять безопасное решение.

Запись резерва может выглядеть так:

```json
{
  "run_id": "run_7c1f",
  "budget": {
    "attempts_remaining": 18,
    "deadline_at": "2025-04-21T14:42:00Z",
    "spend_remaining_cents": 1200
  },
  "reservation": {
    "action": "create_build",
    "maximum_cents": 850,
    "provider_reference": "build-request-41"
  },
  "decision": "allow"
}
```

После такого резерва у запуска остается 350 центов на все остальные действия. Если фактический счет составит 620 центов, верните 230 центов в доступный резерв. Если счет составит 910 центов, зафиксируйте перерасход, запретите последующие траты и выясните, почему оценка оказалась неверной. Не позволяйте агенту свободно занимать средства из будущего бюджета только потому, что итоговая сумма оказалась неудобной.

Некоторые провайдеры указывают цену в запросе или возвращают оценку использования до начала работы. Пользуйтесь этими данными. Другие этого не делают. Для таких сервисов классифицируйте действия по риску. Разрешайте дешевое чтение в рамках обычного бюджета запросов. Для действия, которое может создать ресурс без верхнего лимита, отправить платные сообщения, разместить заказ или запустить работу с неограничиваемой стоимостью, требуйте явного подтверждения.

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

Держите кредиты провайдера и общие квоты организации за пределами бюджета запуска. Это запасные барьеры, а не замена бюджету. Месячная квота обычно позволяет одному запуску потратить намного больше, чем заслуживает его задача.

## Для повторов нужен меньший лимит, чем для первых попыток

Повторы должны быть редкими, ограниченными и классифицированными с учетом того, может ли повтор запроса повторить побочный эффект. Общее указание повторять запросы при ошибках превращает временный сбой в дорогой поток трафика.

RFC 9110 определяет идемпотентные методы, такие как GET, PUT и DELETE, через предполагаемый эффект на сервере. Там также сказано, что клиенту не следует автоматически повторять неидемпотентный запрос, если он не знает, что повтор безопасен. Для агентов эта оговорка важнее, чем для обычного прикладного кода: агент может изменить тело запроса между попытками, решить, что предыдущий ответ был неполным, и отправить то, что выглядит как новый запрос.

RFC 6585 определяет HTTP 429, Too Many Requests, и указывает, что ответ может содержать `Retry-After`. Если этот заголовок присутствует, соблюдайте его. Но не воспринимайте его как разрешение спать до истечения указанного времени, а потом продолжать бесконечно. Повтор все равно расходует бюджет времени запуска, а первоначальная задача может уже не оправдывать ожидание.

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

```yaml
request_classes:
  read:
    max_attempts: 3
    retry_on: [408, 429, 502, 503, 504]
    backoff_seconds: [2, 8]
  idempotent_write:
    max_attempts: 2
    require_idempotency_token: true
    retry_on: [408, 429, 503]
  non_idempotent_write:
    max_attempts: 1
    retry_on: []
```

Такая политика предотвращает типичный сбой. Агент отправляет `POST /invoices` и теряет соединение до получения ответа. Неосторожный повтор может создать второй счет. Ключ идемпотентности позволяет провайдеру, который его поддерживает, распознать дубликат, но только если агент повторно использует тот же ключ и ту же логическую операцию. Если во второй попытке агент меняет сумму, клиента или ключ, защита больше не действует.

Для неидемпотентного действия лучше запросить статус по идентификатору операции, созданному клиентом. Если провайдер не поддерживает идемпотентность или получение статуса, считайте неоднозначный тайм-аут поводом для проверки человеком. Это кажется медленнее автоматического повтора, пока кому-то не приходится отменять дублирующиеся переводы, заказы или сообщения.

Разброс задержки важен, когда один и тот же сбой затрагивает много агентов. Используйте случайную задержку, чтобы они не повторяли запросы одновременно. Сохраняйте расписание достаточно коротким, чтобы срок запуска оставался осмысленным. Экспоненциальная задержка на шесть часов может защитить провайдера, но оставить сессию агента открытой надолго после того, как задача перестала быть важной.

## Для циклов опроса нужен отдельный потолок

Агенту, ожидающему асинхронную работу, нужен отдельный лимит опросов. Обычные лимиты запросов скрывают такую закономерность, пока она уже не станет дорогой. Опрос может быть полезен, но у него должны быть интервал, максимальное число проверок и срок, относящийся к конкретной операции.

Неудачную версию легко узнать по журналам:

```text
14:00:03 POST /exports                 202 accepted
14:00:04 GET  /exports/ea91            202 running
14:00:05 GET  /exports/ea91            202 running
14:00:06 GET  /exports/ea91            202 running
...
14:11:58 GET  /exports/ea91            202 running
```

Агент воспринял «все еще выполняется» как инструкцию спросить еще раз. Это не настойчивость, а отсутствие политики.

Начните с рекомендаций провайдера. Если endpoint возвращает `Retry-After`, соблюдайте его в пределах оставшегося срока. Если он сообщает расчетное время завершения, не опрашивайте его раньше. Если доступен webhook или callback, используйте его вместо того, чтобы удерживать запуск агента открытым. Внешний callback может позднее продолжить контролируемый workflow, но он не должен оживлять истекший запуск с прежними полномочиями.

Бюджет опроса также должен различать состояние задачи и сбой транспорта. `202 running` сообщает, что задача существует. Тайм-аут ничего не говорит о том, дошел ли запрос статуса. Не расходуйте один и тот же лимит повторов на оба случая. В первом случае можно подождать до следующей запланированной проверки. Во втором может быть оправдан один повтор при соблюдении обычного лимита.

Для истекшего лимита опроса задайте конечное действие: сохраните последнее известное состояние задачи, идентификатор удаленной операции и инструкцию для продолжения. Не отменяйте задачу автоматически, если исходная задача не указала, что отмена безопасна. Некоторые задачи оставляют пригодный результат после того, как агент перестал ждать, а некоторые запросы отмены сами имеют побочные эффекты.

Такой дизайн делает задержанную работу видимой человеку. Сообщение «Экспорт все еще выполняется после шести проверок; операцию ea91 можно проверить позже» помогает передать задачу. Сообщение «Агент завершил работу» после скрытого фонового цикла не помогает.

## Срок должен охватывать ожидание, инструменты и очередь

Срок запуска должен измерять весь период, когда агент способен вызвать внешнюю работу. В него нужно включить размышления модели между вызовами, паузы перед повторами, выполнение локальных инструментов, задержку в очереди, зависания DNS и ожидание подтверждения. Таймер только вокруг HTTP-клиента оставляет большие промежутки, в которых цикл может продолжаться.

Для контроля этого ограничения используйте монотонный таймер длительности. Системные часы могут скакнуть, когда ноутбук засыпает, просыпается или получает корректировку времени. Сохраняйте отметки системного времени для аудита, но вычисляйте истечение срока по длительности, которая не может пойти назад.

Передавайте оставшееся время каждой операции. Если у запуска осталось 40 секунд, не начинайте HTTP-запрос с тайм-аутом клиента 90 секунд и не запускайте SSH-команду вообще без тайм-аута. Дочерняя операция получает меньшее из двух значений: своего локального лимита и оставшейся длительности запуска.

```text
remaining = run_deadline_monotonic - now_monotonic
if remaining \u003c= 0:
    deny("run_deadline_exhausted")
else:
    operation_timeout = min(remaining, endpoint_timeout)
    execute(operation_timeout)
```

Ожидание подтверждения требует особого внимания. Человек может ответить через пять минут, но у запуска агента может оставаться всего 30 секунд. Если подтверждение приходит после истечения срока, запретите действие и объясните причину. Разрешение на возобновление истекшего запуска создает лазейку: агент может поставить в очередь сколько угодно дорогих действий до истечения срока, а затем выполнять их, когда человек нажимает карточки.

Для долгих задач нужна другая структура. Разделите их на контрольные точки с новыми бюджетами и зафиксированными переходами состояния. Например, агент может отправить экспорт в одном запуске, а следующий запуск проверит готовый экспорт и решит, что с ним делать. Каждый запуск получает цель, небольшой набор полномочий и четкое время завершения. Это менее магично, чем одна бессмертная сессия агента, и именно поэтому такую схему проще проверять.

## Ограничения области не дают агенту тратить бюджет не по назначению

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

Определяйте область конкретно: разрешенные хосты, идентификаторы учетных данных, HTTP-методы, SSH-адресаты, допустимые пути или семейства команд и максимальный размер тела запроса. Определение должно быть достаточно узким, чтобы его мог понять проверяющий. Не пытайтесь писать миниатюрный язык программирования для каждого возможного условия. Сложные системы политик обрастают исключениями, и в напряженной ситуации никто не может предсказать их результат.

Разница между границей учетных данных и границей бюджета важна. Граница учетных данных определяет, может ли агент использовать секрет для вызова сервиса. Граница бюджета определяет, может ли этот запуск выполнить еще один вызов после получения такого разрешения. Команды часто устанавливают первую границу и считают, что она обеспечивает вторую. Это не так. Идеально защищенный токен API все равно может оплатить тысячу ненужных запросов.

Для HTTP отклоняйте перенаправления за пределы набора разрешенных хостов, если человек явно не разрешил конечный адрес. Перенаправления легко упустить из виду, потому что многие клиенты обрабатывают их автоматически. Запрос, начавшийся с разрешенного URL, может завершиться на другом хосте, передав заголовки или потратив бюджет на сервис, которого не было в задаче.

Для SSH не выдавайте агенту обычную оболочку только потому, что его первая задача включает одну команду. По возможности ограничьте адресат и интерфейс команд. Установите тайм-аут команды и считайте каждую попытку подключения. Цикл неудачных подключений все равно остается внешней активностью, даже если аутентификация ни разу не состоялась.

Разделяйте области чтения и записи. Задаче сбора данных может требоваться широкий набор endpoint для чтения и полный запрет на создание записей. Задаче изменения может понадобиться один endpoint записи и узкий предварительный запрос на чтение. Такое разделение делает бюджет полезнее, поскольку число запросов само по себе не отличает безобидное повторение от повторяющихся побочных эффектов.

## Отказы должны оставлять достаточно данных для восстановления цикла

Остановка по бюджету полезна только тогда, когда ее можно объяснить задним числом. Записывайте каждое решение до начала внешнего вызова, результат после его завершения и отказы с той же тщательностью, что и успешные вызовы. Иначе отсутствие события может выглядеть так же, как заблокированный запрос.

Используйте один неизменяемый идентификатор запуска для сообщений модели, локальных инструментов, HTTP-запросов, SSH-подключений, подтверждений и записей аудита. Каждая внешняя попытка должна иметь порядковый номер. Если происходит повтор, свяжите его с исходной логической операцией и укажите, было ли действие идемпотентным, неоднозначным или подтвержденно не выполненным.

Компактная запись события содержит достаточно данных для расследования без хранения чувствительных тел запросов:

```json
{
  "run_id": "run_7c1f",
  "attempt": 17,
  "logical_operation": "fetch_export_status:ea91",
  "channel": "http",
  "destination_class": "approved-export-api",
  "outcome": "denied",
  "reason": "poll_allowance_exhausted",
  "attempts_remaining": 0,
  "elapsed_ms": 598244,
  "reserved_spend_cents": 0
}
```

По умолчанию не записывайте bearer-токены, пароли, приватные ключи и полные тела запросов. Для аудита бюджета нужны идентификаторы, классы, при необходимости хеши и контекст решения. Не нужен второй склад секретов с теми же учетными данными, которые вы пытались защитить.

Сделайте последовательность событий защищенной от незаметного изменения. Хеш-цепочка связывает каждую запись с предыдущей, поэтому удаление или изменение нарушает проверку. По возможности храните материалы для проверки отдельно от работающего агента. Агент, который может записывать собственную историю, не должен иметь возможности изменить запись, доказывающую превышение бюджета.

Sallyport сохраняет сессии агентов и отдельные вызовы в зашифрованном журнале аудита с хеш-цепочкой, недоступном для записи, а команда `sp audit verify` проверяет цепочку офлайн по шифротексту. Такой подход полезен для контроля бюджета: заблокированное действие остается частью доказательств, а не исчезает как внутреннее решение.

## Подтверждение человека нужно для исключений, а не для каждого чтения

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

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

Не представляйте подтверждение как лекарство от слабого бюджета. Рассеянный человек может разрешить плохое действие, а у запуска без наблюдения может не быть ответа. Бюджет все равно должен истечь во время ожидания. Если задача по-прежнему важна, человек может выделить новый бюджет для нового запуска.

Не просите людей подтверждать безликое «продолжить». Продолжить что? С каким сервисом? Какова расчетная стоимость? Сколько вызовов запуск уже выполнил? Хороший запрос на подтверждение делает эти факты видимыми. Расплывчатый запрос заставляет проверяющего доверять резюме агента, то есть именно той стороне, которая заинтересована в продолжении работы.

Полезно разделить обязанности. Шлюз последовательно применяет фиксированные ограничения. Человек решает, достаточно ли ценности у исключительного действия, чтобы оправдать расширение полномочий. Разделите эти обязанности, и ни одной стороне не придется притворяться, что она решает задачу другой.

## Исчерпание бюджета должно приводить к передаче, которую можно продолжить

Когда лимит останавливает запуск, агент должен вернуть короткую запись состояния, которая позволит человеку или следующему запуску безопасно продолжить работу. Расплывчатое сообщение об ошибке провоцирует повторить всю задачу, а это дублирует завершенные вызовы и может повторить побочные эффекты.

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

Например:

```text
Остановлено: исчерпан бюджет времени.
Завершено: задача экспорта ea91 отправлена; файлы не скачаны.
Последнее наблюдаемое состояние: выполняется в 14:09:58 UTC.
Внешние попытки: 14 из 14; зарезервировано: 0 центов.
Безопасное продолжение: один раз проверить статус ea91 после 14:20 UTC.
Опасное действие: не отправлять еще один запрос на экспорт.
```

Такое сообщение предотвращает самый дорогой способ восстановления: начать все сначала, потому что никто не знает, что произошло. Оно также помогает настраивать бюджеты. Если многие обычные запуски останавливаются после одной проверки статуса, разрешенная длительность слишком мала. Если запуски часто тратят весь лимит на поиски без продвижения, неверно устроены декомпозиция задачи или область доступа.

Не пополняйте бюджет автоматически после его исчерпания. Правило пополнения - это просто неограниченный бюджет, записанный небольшими порциями, если отдельный уполномоченный процесс не решает, когда оно применяется. Требуйте новый запуск или явное действие человека, сохраняйте старую запись и пусть следующая попытка объяснит, почему ей нужно больше ресурсов.

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