Идемпотентность агента: хэш аргументов, replay и stable intent

Повторный запуск агента после сбоя не равен «тому же запросу»: модель перепишет письмо, хэш аргументов сменится, побочный эффект уйдёт второй раз. Для соло-разработчика с агентом в цикле tools и ретраев это контракт над недетерминированным выводом LLM; в разборе Anis Meziani про runtime ApowerB сформулировано жёстко: ключ идемпотентности должен отражать устойчивое намерение, а не сгенерированный текст.
Три развилки и классы аргументов tool-call
В ApowerB при падении run фиксируется, какие tools вызывались. Если вызывался любой tool, даже read, автоматический replay отклоняется с HTTP 409 и списком вызовов; форсировать повтор можно только вручную (run_main.py в репозитории). Это default-deny: агент не получает «бесплатный» второй заход после side effect.
Классический обход, sha256(json(args)), ломается на примере send_email к одному получателю: при replay модель меняет subject и body, хэш другой, система считает запрос новым, и дубликат письма проходит как «успешная» идемпотентность.
Третий путь, ключ из stable intent, задаёт вопрос не «уже отправил эти байты?», а «уже сделал это действие по этой причине к этой цели?» Для кампании единицей эффекта служит одно письмо, не весь run; предлагаемый ключ: (triggering_event, tool, recipient) без subject/body в ключе.
Meziani делит входы tool на три класса. Это рамка для любой обёртки вокруг LLM-вызовов, не только для ApowerB.
| Класс | Примеры | В ключе? |
|---|---|---|
| Внешняя identity | event id, message id, thread id, record id, нормализованный to |
Да |
| Детерминированно от входов | хэш байтов файла | Да |
| Сгенерировано моделью | body, subject, summary, любой free text | Никогда |
build the key from stable intent, never from generated content.
В sketch-декораторе (пометка design sketch, not shipped) ключ собирается из trigger_event_id, имени эффекта и нормализованного получателя, не из полей, которые модель перефразирует при каждом replay.
Replay на границе эффекта, не всего run
Сценарий email-кампании на 100 писем: run падает после 40 отправок. При default-deny автоматический replay не стартует, оставшиеся 60 не уйдут, пока человек не вмешается. Если оператор форсирует replay, 40 адресатов получат второе письмо.
Здесь ломается привычка мыслить идемпотентностью на уровне run, а не эффекта. Для агентных пайплайнов с human gate до send (skill email-campaign у Meziani) replay и ключ должны жить на границе «одно действие к одной цели», иначе оркестратор выбирает между голодной очередью и дубликатами.
Shipped-код ApowerB, read-only tools и MCP
Meziani опирается на поведение в проде, не только на RFC.
- Microsoft Graph webhooks: уникальный индекс
(subscription_id, resource_id)отсекает повторную доставку (outlook.py). - BI datasets:
dataset_idкак UUIDv5 от owner, organization_id, project_id, folder, name, sheet и SHA-256 содержимого; повтор того же файла даётreused: true; при soft delete и коллизии fallback на случайный ID послеIntegrityError(bi_datasets.py).
Read-only tools Meziani считает кандидатами на «safe to replay» только если ограничение встроено в конструкцию, а не в флаг: Oracle (SET TRANSACTION READ ONLY и rollback), MongoDB (блок write-стадий $out/$merge), Redshift (проверка read-only SQL до выполнения). Иначе «чистый» GET перед send_email не спасает от ложной уверенности при replay.
Сторонний tool через MCP, если в тексте не объявлен как безопасный, трактуется как unsafe; replay остаётся под default-deny (псевдокод third_party_mcp_action в sketch). Для практики с агентом в IDE вывод прямой: нельзя полагаться на декларацию «read-only» у внешнего MCP без контракта уровня кода.
Если провайдер API поддерживает идемпотентность нативно, ключ должен доходить до API (пример заголовка Stripe Idempotency-Key в тексте Meziani). Sketch также описывает effects ledger, проброс ключа провайдеру и отказ для undeclared tools.
В комментариях на Dev.to (не shipped-фича ApowerB) читатель сравнивает подход с duplicate detection в Azure Service Bus на business ID; Meziani соглашается с аналогией «окно vs навсегда». Идеи из треда (action slug из workflow/tool definition, не из модели; outbox pending→confirmed) остаются на уровне дискуссии.
Четыре шага в своём агентном слое
Meziani не закрывает поля ключа per tool (два письма одному получателю vs один summary в Slack), ветвление replay при другом наборе получателей, TTL ledger и разрыв между side effect и записью в ledger, если у провайдера нет native idempotency.
Для разработчика с агентом в IDE практический вывод такой:
- Не хэшируйте полный JSON аргументов tool, если в нём есть LLM-generated text.
- Привязывайте ключ к внешней identity и событию-триггеру, а не к формулировке модели.
- Считайте единицей идемпотентности эффект (письмо, запись, webhook), не весь agent run.
- Любой необъявленный tool трактуйте как риск при автоматическом replay; MCP third-party без контракта в той же корзине.
В CTA Meziani указывает RFC на GitHub, документацию и Helm-чарт хостинга ApowerB; номер issue в тексте не зафиксирован.
Источники
- Anis Meziani — Idempotency keys for AI agents: why hashing the arguments doesn't work (Dev.to, доступ 2026-10-21)
- Репозиторий ApowerB: github.com/apowerb/apowerb (доступ 2026-10-21)