Глава 10. Идемпотентность, повторы, лимиты запросов и границы отката¶
1. Почему самые дорогие сбои часто выглядят как “мы просто повторили вызов”¶
Когда агентная система начинает выполнять реальные действия, один из самых неприятных источников инцидентов оказывается очень прозаичным: повторный вызов.
Снаружи это часто выглядит безобидно:
- запрос завис;
- рантайм решил повторить;
- интеграция ответила нестабильно;
- агент попытался “помочь” и отправил действие еще раз.
Но в реальном мире это означает:
- два одинаковых тикета;
- два письма одному клиенту;
- повторную оплату;
- повторную запись в CRM;
- несколько изменений одного и того же объекта.
То есть проблема не в рассуждении модели как таковом, а в том, что слой выполнения не умеет безопасно жить в мире частичных сбоев.
2. Идемпотентность это не приятное дополнение, а базовая страховка¶
Идемпотентность нужна для простого вопроса: “Если система по ошибке повторит этот вызов, что произойдет?”
Для операций записи хороший ответ должен быть одним из двух:
- повторный вызов не меняет результат;
- система может надежно распознать дубль и не создать побочный эффект повторно.
Если такого свойства нет, любая нестабильность сети, тайм-аут или гонка между несколькими запусками начинает стоить слишком дорого.
3. Повтор без классификации ошибок только множит хаос¶
Очень плохая стратегия выглядит так: “если запрос не удался, повторить еще три раза”.
В эксплуатации это опасно, потому что не все ошибки одинаковы:
validation_failureпочти никогда не нужно повторять;permission_deniedнельзя чинить количеством повторов;retryable_failureкак раз может требовать backoff;side_effect_unknownтребует осторожности, а не слепого повтора.
То есть политика повторов должна зависеть не от эмоции “ну вдруг получится”, а от класса исхода.
4. Самый неприятный статус это side_effect_unknown¶
Есть особенно неприятная категория сбоев: ты уже не знаешь, произошел побочный эффект или нет.
Примеры:
- тайм-аут после отправки запроса во внешний сервис;
- соединение оборвалось после commit;
- адаптер упал, не сохранив подтверждение;
- внешний API ответил так, что итоговое состояние неясно.
Это именно тот случай, где наивный повтор особенно опасен. Иногда правильное поведение тут не “повторить”, а:
- проверить текущее состояние во внешней системе;
- выполнить сверку;
- запросить человека;
- остановить рабочий процесс и зафиксировать неопределенность.
Сквозной кейс: неизвестный итог тикета
В кейсе сортировки обращений поддержки самый опасный момент — не ошибка в рассуждении, а тайм-аут после вызова create_support_ticket. Если helpdesk мог уже создать тикет, повторный вызов без ключа идемпотентности превратит один клиентский запрос в два инцидента. Правильная ветка сначала ищет тикет по correlation ID, затем либо привязывает найденный результат к трассе, либо останавливает запуск и просит оператора подтвердить состояние.
Заметка о сквозных сценариях надежности: повторы, ограничения частоты и границы отката нужно проверять на всех трех канонических сценариях. Разбор обращений поддержки требует ключей идемпотентности, обнаружения дублей тикетов и сверки перед повтором. Внутренний ассистент знаний требует ограничений частоты для веерного поиска, пауз при потере свежести и запрета повторять устаревшие записи памяти. Координация инцидентов требует устранения дублей эскалации, ограничения частоты уведомлений, ясной границы отката для изменений состояния инцидента и человеческого подтверждения при side_effect_unknown.
4.1. Ветка восстановления тоже должна проектироваться явно¶
Свежие работы по сценариям отказов инструментов усиливают еще одну мысль: путь восстановления почти никогда не стоит оставлять как импровизацию слоя выполнения.
Полезно заранее определить:
- какие классы исходов требуют сверки, а не повтора;
- где частичный успех должен создавать следующую задачу;
- когда восстановление само требует подтверждения;
- какие ветки должны явно попадать в eval schema;
- где опасный путь восстановления лучше остановить, чем “помочь пользователю еще раз”.
Многие тяжелые инциденты происходят не в счастливом пути, а именно в плохо спроектированной ветке восстановления.
5. Лимиты запросов это тоже часть безопасного дизайна¶
Когда о лимитах запросов думают только как о проблеме производительности, слой выполнения недооценивает их архитектурную роль.
На деле лимиты запросов нужны еще и для того, чтобы:
- один сорвавшийся агент не заддосил внешнюю систему;
- циклическое планирование не превратилось в лавину вызовов инструментов;
- дорогая возможность не съедала весь бюджет;
- шторм повторов не убивал интеграцию.
Именно поэтому лимит полезно задавать не только на уровне всего сервиса, но и:
- на инструмент;
- на арендатора;
- на рабочий процесс;
- на класс риска.
Для multi-agent режима нужен отдельный fanout budget. Один пользовательский запрос может превратиться не в один run, а в дерево subagent runs, retrieval calls и tool calls. Поэтому лимит должен включать не только max_attempts, но и subagent_count, token_budget, context_handoff_size, максимальное число параллельных веток и stop condition для manager. Иначе команда будет видеть “один запрос” в продуктовой метрике и лавину работы в инфраструктуре.
Хороший runtime поэтому блокирует fanout до запуска, если:
- задача write-heavy и ветки будут менять один объект;
- нет явного
delegation_reason; - budget не покрывает worst-case merge/retry;
- high-risk capability может быть вызвана несколькими subagents без единого approval boundary.
6. Граница отката должна быть определена заранее¶
Очень опасно обнаруживать только в инциденте, что операция “вообще-то неоткатываемая”.
У каждой возможности записи полезно заранее понимать:
- можно ли отменить действие;
- можно ли безопасно повторить действие;
- где проходит точка невозврата;
- какое компенсирующее действие допустимо;
- когда нужна ручная сверка.
То есть граница отката — это не “потом посмотрим”. Это часть контракта инструмента и рабочего процесса.
После побочного эффекта слой выполнения должен различать безопасный повтор, сверку и остановку
flowchart TD
A["Запрос инструмента"] --> B["Выполнить действие записи"]
B --> C{"Исход известен?"}
C -->|Да, успех| D["Сохранить результат и продолжить"]
C -->|Ошибка, которую можно повторить| E["Повторить по политике с backoff"]
C -->|Неизвестный побочный эффект| F["Сверить или запросить проверку человеком"]
C -->|Ошибка валидации или доступа| G["Остановиться и показать ошибку"] 7. Хороший контракт выполнения хранит операционную семантику явно¶
Недостаточно знать только схему входа и форму ответа. Для опасных операций контракт должен хранить операционную семантику:
- идемпотентно ли действие;
- нужен ли ключ идемпотентности;
- какие ошибки допустимы для повтора;
- где предел повторов;
- какие лимиты на частоту вызова;
- что делать при неизвестном исходе;
- возможен ли откат или компенсирующее действие.
Ниже очень практичный шаблон:
tools:
create_ticket:
mode: write
idempotent: true
idempotency_key_required: true
retry:
max_attempts: 3
backoff: exponential
retry_on: ["retryable_failure"]
rate_limit:
per_tenant_per_minute: 20
rollback:
strategy: "none"
reconcile_on_unknown: true
send_email:
mode: write
idempotent: false
idempotency_key_required: false
retry:
max_attempts: 1
retry_on: []
rate_limit:
per_user_per_hour: 10
rollback:
strategy: "manual_only"
reconcile_on_unknown: true
Такой YAML помогает команде заранее признать неприятную правду: не все действия одинаково безопасны для автоматизации.
7.1. Долговечный шаг не равен сообщению о прогрессе¶
Cloudflare Agents SDK полезно разделяет работу, которая остается внутри агента, и работу, которую лучше вынести в Workflows: агент отвечает за живую коммуникацию и состояние границы взаимодействия, а рабочий процесс — за долговечное многошаговое выполнение, автоматические повторы, ожидание внешних событий и восстановление.2
Для этой главы это важная граница. Долговечный шаг должен иметь устойчивый идентификатор, ключ идемпотентности, политику воспроизведения, политику повторов, тайм-аут и связь с аудитом. Сообщение о прогрессе — WebSocket-сообщение, потоковое обновление или состояние интерфейса — не должно само становиться источником истины. Если прогресс был отправлен, но долговечный шаг не был зафиксирован, рантайм обязан вернуться к последней долговечной контрольной точке, а не “доверять” последнему видимому сообщению пользователю.
Для durable named agent instance это правило становится еще конкретнее: agent_instance_id и durable_state_version должны переживать retry, wakeup и reconnect, а scheduled_wakeup_id и resumable_stream_id должны связываться с тем же idempotency/replay contract. Иначе система легко реконструирует “похожую” сессию из stateless handler и незаметно повторяет write step или теряет уже принятое approval decision.
То же правило относится к webhook, email, Slack command и другим programmatic turns. Внешний submit не должен быть просто “новым сообщением в чат”. У него нужен submission_id или correlation key, actor identity, accepted-at timestamp, дедупликация, replay policy и trace link к тому же agent_instance_id. Иначе повтор доставки, retry провайдера или восстановление после deploy создаст второй run, который выглядит легитимно, но на самом деле повторяет уже принятый вход.
7.2. Production runtime это контракт, а не только цикл вызовов¶
Свежая практика LangChain вокруг production deep agents формулирует ту же границу с другой стороны: длинный агентный горизонт требует runtime, который держит долговечное выполнение, память, human-in-the-loop, observability и изоляцию рабочих сред как инфраструктурные свойства, а не как набор локальных helper-функций.3
Для этой главы полезно записать это как runtime contract. Production runtime должен отвечать на вопросы, которые простой agent loop обычно оставляет неявными:
- какой
run_id,step_idиagent_instance_idпереживают crash, deploy и reconnect; - где хранится checkpoint перед внешним side effect;
- какие шаги можно replay, а какие требуют reconciliation;
- как approval pause/resume связывается с той же сессией и capability session;
- какие traces доказывают, что восстановление не повторило побочный эффект;
- где tenant boundary, sandbox boundary и budget boundary проверяются до запуска, а не после инцидента.
Microsoft Foundry похожим образом описывает проблему governance: written policy сама по себе не превращается в runtime control, если контрольные точки не стоят там, где агент реально может ошибиться.4 Поэтому policy для повторов, approvals, budget и tool access должна компилироваться в hooks около конкретных переходов: before_tool_call, before_side_effect_commit, on_approval_pause, on_resume, on_retry, on_budget_exhausted и on_trace_export.
Практический тест простой: если после рестарта процесса нельзя доказать, какой шаг был committed, какой только показал progress, какое approval решение было живым и почему повтор не создаст новый side effect, это еще не production runtime. Это только цикл выполнения, который пока надеется на удачный день.
8. Простой кодовый пример логики решений для повторов¶
Ниже не движок политик эксплуатационного уровня, а понятный каркас. Его задача показать, что повтор должен зависеть от класса исхода, а не быть общим рефлексом.
from dataclasses import dataclass
@dataclass
class ExecutionOutcome:
status: str
attempts: int
max_attempts: int
def next_step(outcome: ExecutionOutcome) -> str:
if outcome.status in {"validation_failure", "permission_denied"}:
return "stop"
if outcome.status == "retryable_failure" and outcome.attempts < outcome.max_attempts:
return "retry_with_backoff"
if outcome.status == "side_effect_unknown":
return "reconcile"
if outcome.status == "success":
return "continue"
return "escalate"
Важен не код сам по себе, а то, что у системы появляется явная операционная таблица решений.
9. У цикла агента должны быть явные условия остановки¶
Еще один практический совет из гайда OpenAI, который очень полезно формализовать: у цикла запуска должны быть понятные условия остановки.1
Хороший рантайм завершает запуск не “когда модель вроде бы успокоилась”, а по явным причинам:
- получен финальный структурированный результат;
- больше не требуются вызовы инструментов;
- пришла неустранимая ошибка;
- достигнут лимит по шагам или бюджету;
- сработала граница подтверждения и нужен человек.
Это звучит скучно, но именно такие правила не дают агенту уйти в бесконечное планирование, бессмысленные повторы или декоративные переходы между инструментами.
10. Ключ идемпотентности должен быть частью протокола, а не опциональной договоренностью¶
Если инструмент записи формально “поддерживает идемпотентность”, но ключ:
- не обязателен;
- по-разному генерируется в разных местах;
- не доживает до пути повтора;
- не логируется,
то это почти не настоящая идемпотентность.
Хорошая практика:
- генерировать ключ на границе рабочего процесса или действия;
- передавать его через весь путь выполнения;
- логировать его в журнале аудита;
- использовать для сверки и расследований.
11. Частые ошибки¶
Проблемы здесь довольно типовые:
- повторы идут на ошибки, которые нельзя повторять;
- неизвестный исход трактуется как обычная ошибка;
- лимиты запросов ставят только на входе, но не на инструментах;
- откат обещан, но фактически не существует;
- ключ идемпотентности забывается между планировщиком и адаптером;
- агент получает слишком много свободы в повторных попытках.
Все это означает одно: слой выполнения еще не дорос до модели отказов эксплуатационного уровня.
12. Практикум: разбор side_effect_unknown, idempotency key и rollback boundary перед production-вызовом¶
Этот практикум закрывает главный эксплуатационный разрыв главы: команда уже понимает, что повторять все ошибки подряд опасно, но ей нужен проверяемый способ решить, что делать с конкретным write-вызовом до того, как он попадет в production.
Разбираем тот же сквозной пример: агент поддержки хочет создать тикет во внешней helpdesk-системе. Модель сформулировала намерение, политика решила, что действие требует подтверждения, человек одобрил запись, а затем внешний API завис после отправки запроса. С этого момента команда не имеет права выбирать между “повторить” и “сдаться” на интуиции. Нужен контракт восстановления.
Цель практикума — собрать один review packet для write-capability: ключ идемпотентности, классы исходов, retry policy, rate limit, граница отката, правила сверки, подтверждение восстановления, trace evidence и eval gate. Такой пакет должен быть понятен владельцу продукта, инженеру интеграции, SRE и человеку, который будет расследовать дубль тикета через месяц.
Шаг 1. Описать write intent до вызова инструмента¶
До вызова внешнего инструмента у системы должно появиться намерение записи. Не текстовое “надо создать тикет”, а структурированная запись, которую можно передать в подтверждение, трассу, идемпотентность и сверку.
write_intent:
intent_id: write-intent-2026-04-09-001
trace_id: trace-support-001
session_id: session-support-001
capability: create_ticket
target_system: jira
destination: project://OPS
operation: create
risk_tier: high
principal_id: user-42
tool_principal: svc-ticket-writer
idempotency_key: ticket-req-2026-04-09-001
requested_fields:
summary: "Open a Sev-2 onboarding incident"
customer_id: customer-acme
severity: sev2
Эта запись нужна, чтобы у всех последующих слоев был один и тот же объект обсуждения. Approval подтверждает именно requested_fields. Инструмент получает тот же idempotency_key. Trace сохраняет тот же intent_id. Recovery ищет во внешней системе не “что-то похожее”, а объект с известной корреляцией.
Если write intent отсутствует, повтор будет спорить с человеком и трассой. Человек мог подтвердить одно, адаптер отправил другое, а recovery потом ищет третье.
Шаг 2. Сделать idempotency key частью протокола, а не удобной переменной¶
Ключ идемпотентности должен жить дольше одного HTTP-запроса. Он создается на границе workflow или durable step, проходит через approval, tool call, trace, audit record и reconciliation.
Минимальный контракт:
idempotency_contract:
key_source: workflow_boundary
key_scope: tenant_and_capability
required_for:
- create_ticket
- notify_team
- create_incident_thread
persisted_in:
- write_intent
- approval_request
- tool_execution
- recovery_attempt
- audit_record
reuse_policy: same_intent_only
replay_policy: new_run_gets_new_key_unless_reconciling_same_intent
external_lookup_key: correlation_id
Самая частая ошибка — генерировать ключ глубоко внутри адаптера. Тогда слой политики и подтверждения не видит, что именно будет защищать запись от дубля. Вторая ошибка — генерировать новый ключ на каждый retry. Это превращает идемпотентность в декорацию: внешний сервис честно видит новые операции и создает новые побочные эффекты.
Хорошая проверка простая: если запуск поставлен на паузу, возобновлен, повторен после тайм-аута или передан в recovery job, ключ должен оставаться связанным с исходным намерением записи.
Шаг 3. Разделить retryable_failure и side_effect_unknown в decision matrix¶
Глава уже показала, что retryable_failure и side_effect_unknown нельзя смешивать. На ревью это должно быть таблицей решений, а не устной договоренностью.
execution_outcome_matrix:
success:
next_step: continue
audit: record_result
validation_failure:
next_step: stop
retry_allowed: false
permission_denied:
next_step: stop_or_reapprove
retry_allowed: false
retryable_failure:
next_step: retry_with_backoff
retry_allowed: true
requires_same_idempotency_key: true
side_effect_unknown:
next_step: reconcile_before_retry
retry_allowed: false
requires_human_review_if_reconcile_fails: true
partial_side_effect:
next_step: compensate_or_manual_recovery
retry_allowed: false
Эта матрица должна быть ближе к runtime, чем к документации. Если общий helper просто видит exception и повторяет вызов три раза, матрица не работает. Runtime должен знать не только факт ошибки, но и класс исхода.
Для create_ticket особенно важно правило: side_effect_unknown не делает автоматический повтор. Сначала сверка по idempotency_key или correlation_id, затем решение.
Шаг 4. Описать retry policy как часть capability contract¶
Retry policy должна отвечать на четыре вопроса: что можно повторять, сколько раз, с каким backoff и какой ключ сохраняется между попытками.
capability_retry_contract:
capability: create_ticket
mode: write
idempotent: true
idempotency_key_required: true
retry:
max_attempts: 3
backoff: exponential_with_jitter
retry_on:
- retryable_failure
- rate_limited_retry_after
never_retry_on:
- validation_failure
- permission_denied
- side_effect_unknown
- partial_side_effect
rate_limit:
per_tenant_per_minute: 20
per_principal_per_minute: 5
burst: 3
stop_condition:
on_retry_budget_exhausted: escalate_with_trace
Здесь важно не количество попыток само по себе, а то, что retry budget становится частью политики. Агент не может решить “попробую еще разок”, если бюджет исчерпан. Адаптер не может обойти лимит, если модель попросила “очень срочно”.
Для rate limit полезно считать не только входящие пользовательские запросы, но и внутренние tool calls. Иначе один агентный запуск с циклом повторов может сжечь лимит внешней системы быстрее, чем обычный пользовательский трафик.
Шаг 5. Заранее определить rollback boundary и compensating action¶
Не каждую операцию можно откатить. Важно сказать это до production, а не во время инцидента.
rollback_boundary:
capability: create_ticket
point_of_no_return: external_ticket_created
automatic_rollback: false
compensating_action:
type: link_or_close_duplicate_ticket
requires_human_review: true
reconcile_on_unknown: true
manual_recovery_owner: support-ops
incident_if_duplicate_created: true
У create_ticket хороший rollback часто невозможен в строгом смысле. Можно закрыть дубль, связать тикеты, добавить комментарий, отменить уведомление, но нельзя сделать вид, что побочного эффекта не было. Поэтому честный контракт говорит automatic_rollback: false и требует recovery-путь.
Для других возможностей граница может быть другой. Запись черновика можно удалить автоматически. Отправленное письмо — нет. Эскалацию в incident channel можно дополнить коррекцией, но нельзя стереть факт уведомления. Поэтому rollback boundary должен быть специфичным для capability.
Шаг 6. Спроектировать reconciliation до первого тайм-аута¶
Reconciliation — это не “потом руками посмотрим”. Это отдельная ветка workflow.
reconciliation_plan:
trigger: side_effect_unknown
lookup_order:
- idempotency_key
- external_correlation_id
- target_system_recent_events
possible_results:
object_found:
next_step: attach_result_and_continue
object_not_found:
next_step: ask_human_or_retry_same_intent
multiple_candidates_found:
next_step: manual_reconciliation_required
lookup_failed:
next_step: stop_and_escalate
evidence_required:
- lookup_query
- lookup_result_count
- selected_external_object_id
- reviewer_if_manual
Ключевое слово здесь — same_intent. Если объект не найден и команда решает повторить, это не новая произвольная запись, а продолжение того же намерения с тем же idempotency key и теми же requested_fields. Если полезная нагрузка изменилась, это уже новый write intent и новое подтверждение.
В хорошей реализации сверка сама не должна иметь скрытых побочных эффектов. Она читает состояние, фиксирует доказательства и возвращает ограниченный набор решений.
Шаг 7. Вынести опасное восстановление за approval gate¶
Человек нужен не только до исходного write-действия. Иногда он нужен перед recovery.
recovery_approval_request:
kind: approval_request
reason: side_effect_unknown_recovery
capability: create_ticket
original_approval_id: apr-2026-04-07-001
idempotency_key: ticket-req-2026-04-09-001
requested_fields_unchanged: true
recovery_options:
- attach_found_ticket
- retry_same_intent
- stop_without_retry
- create_manual_followup
required_role: oncall_manager
Такой запрос должен показывать не только бизнес-данные, но и состояние восстановления: был ли найден внешний объект, сколько кандидатов найдено, не истекло ли исходное подтверждение, не изменились ли поля действия.
Если recovery approval не связан с original approval, audit trail разваливается. Потом будет видно, что кто-то что-то подтвердил, но не будет понятно, это продолжение исходного действия или новое действие после сбоя.
Шаг 8. Сделать durable step источником истины, а progress — только сигналом¶
Если действие может пережить retry, pause, timeout или human review, оно должно быть durable step, а не просто строчка в UI.
durable_step:
workflow_instance_id: wf-support-001
durable_step_id: step-create-ticket-001
trace_id: trace-support-001
status: waiting_for_reconciliation
idempotency_key: ticket-req-2026-04-09-001
retry_policy_ref: capability_retry_contract:create_ticket
timeout_policy: 30s_then_reconcile
waiting_for:
- external_state_lookup
- approval_if_lookup_ambiguous
evidence_refs:
- trace:tool_execution:step-create-ticket-001
- approval:apr-2026-04-07-001
Progress update может сказать пользователю “проверяю состояние тикета”, но он не должен быть источником истины. Истина — durable step, trace, approval record и external lookup evidence. Иначе после сбоя UI может помнить одно, workflow — другое, а внешний сервис — третье.
Шаг 9. Зафиксировать trace payload для сбоя и восстановления¶
Трасса должна объяснять не только счастливый путь, но и то, почему система остановилась или пошла в сверку.
trace_events:
- event_type: tool_policy_decision
payload:
capability_name: create_ticket
decision: approval_required
risk_tier: high
idempotency_key: ticket-req-2026-04-09-001
- event_type: approval_requested
payload:
approval_id: apr-2026-04-07-001
requested_fields_hash: sha256:...
- event_type: tool_execution
payload:
capability_name: create_ticket
tool_status: side_effect_unknown
attempts: 1
idempotency_key: ticket-req-2026-04-09-001
- event_type: reconciliation_started
payload:
lookup_key: ticket-req-2026-04-09-001
reason: timeout_after_external_write
- event_type: verification_result
payload:
stop_condition: no_duplicate_ticket_side_effect
verification_result: pass
Даже если текущий минимальный каталог событий пока не содержит отдельного reconciliation_started, полезно резервировать это поле в payload или governance event. Без такого следа инцидент будет выглядеть как “инструмент упал”, хотя важная часть истории — то, что система правильно отказалась делать слепой повтор.
Шаг 10. Добавить eval gate для дубля тикета¶
Если команда не проверяет ветку side_effect_unknown, она почти наверняка сломает ее при следующем изменении prompt, adapter или retry helper.
Минимальный сценарий оценки:
scenario_id: create_ticket_timeout_after_write
labels:
- write_path
- side_effect_unknown
- duplicate_ticket_guard
risk_class: high
expected_outcomes:
max_ticket_side_effects: 1
latest_status: failed_or_waiting_for_reconciliation
idempotency_key_present: true
approval_record_present: true
reconciliation_attempted: true
grading_rules:
- type: status_in
expected: [failed, waiting_for_reconciliation, success_after_reconcile]
blocking: true
- type: max_tool_calls
tool: create_ticket
expected: 1
blocking: true
- type: contains_trace_field
expected: idempotency_key
blocking: true
- type: duplicate_ticket_guard
expected: true
blocking: true
Эта оценка не должна проверять только текст ответа. Она должна смотреть на trace, tool calls, approval record и итоговое количество побочных эффектов. Если новая версия системы отвечает пользователю красиво, но создает два тикета, eval должен блокировать выпуск.
Минимальный checklist перед production-вызовом write-capability¶
Перед выпуском опасной capability пройди по этому списку:
- write intent создается до вызова инструмента;
idempotency_keyобязателен и проходит через approval, tool call, trace и recovery;retryable_failure,side_effect_unknownиpartial_side_effectразличаются в runtime;- retry policy запрещает автоматический повтор при неизвестном побочном эффекте;
- rate limit задан на capability, tenant и principal, а не только на внешний endpoint;
- rollback boundary описывает точку невозврата и допустимые compensating actions;
- reconciliation plan существует до первого production-инцидента;
- recovery approval связан с исходным approval и исходным write intent;
- durable step хранит статус, retry policy, timeout policy и evidence refs;
- trace показывает idempotency key, outcome class, attempts, approval и recovery decision;
- eval gate воспроизводит тайм-аут после записи и блокирует дубль.
Что должно измениться в реализации после такого ревью¶
После такого ревью обычно приходится поправить не один файл, а границу между несколькими слоями.
В capability contract появляется поле idempotency_key_required и явная retry секция. В policy bundle create_ticket остается approval_required, но approval начинает видеть тот же idempotency key, который попадет в инструмент. В runtime появляется outcome matrix, где side_effect_unknown ведет в reconciliation, а не в retry. В telemetry появляются поля idempotency_key, attempts, tool_status, failure_reason, recovery_decision и ссылки на approval. В eval dataset появляется сценарий, который доказывает, что тайм-аут после записи не создает второй тикет.
Самый полезный итог практикума — команда перестает говорить “у нас есть retries” и начинает говорить точнее: “у нас есть политика повторов, которая знает границу побочного эффекта, сохраняет ключ идемпотентности, умеет сверяться перед повтором и оставляет доказательства для оценки и инцидента”.
13. Что сделать сразу¶
Сначала пройди по короткому списку и отдельно отметь все ответы «нет»:
- Есть ли у инструментов записи явная стратегия идемпотентности?
- Различает ли система
retryable_failureиside_effect_unknown? - Привязаны ли повторы к политике, а не к общему вспомогательному обработчику?
- Есть ли лимиты запросов на инструмент или на арендатора?
- Понимаешь ли ты границу отката для каждого опасного действия?
- Может ли рантайм делать сверку вместо слепого повтора?
- Видно ли ключ идемпотентности в трассах и журналах аудита?
Если на несколько вопросов подряд ответ “нет”, то следующая нестабильность интеграции почти наверняка превратится в дубль, шум или ручной разбор инцидента.
Шаблон завершения главы
Что запомнить: надежность агентного выполнения начинается с явной семантики побочного эффекта, повтора, лимита и отката.
Типичные ошибки: повторять вызов без ключа идемпотентности; не различать неизвестный побочный эффект и чистую ошибку; не задавать границу отката заранее.
Что проверить в своей системе: какие операции можно повторять; где хранится ключ идемпотентности; что происходит при неизвестном результате; кто видит исчерпание лимитов.
Сопутствующие материалы: проверь схемы трассировки и подтверждений, практический кейс дубля тикета и проверочный список поэтапного выпуска.
Что читать дальше: переходи к Части V, где надежность превращается в трассы, цели уровня сервиса и оценочные шлюзы.
14. Что делать дальше¶
Часть IV уже закрывает базовый слой выполнения: контракты, песочницу, транспорт возможностей и дисциплину вокруг побочных эффектов. Дальше стоит переходить к наблюдаемости и надежности на уровне всей агентной системы.