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

ADR 0098 — Согласованные пределы транспорта и эффективное окно сжатия

  • Статус: Принят
  • Дата: 2026-08-19
  • Контекст: исправление 413 request entity too large в PR (найдено в реальном использовании), RN-412

Контекст​

Gate QA/SecOps умирал с 413 request entity too large на легитимных PR. Комментарии, разбросанные по коду (apps/engine/config/runtime.exs, apps/engine/lib/engine/actions/terminal_executor.ex, docker/docker-compose.yml), приписывали ошибку LLM-PROVIDER. Расследование подтвердило, что это приписывание было ошибочным: Engine.Agents.FalhaDeTurno.origem({413, _}) уже классифицировал сбой как "codigo", а не "modelo", — и классификатор был прав, просто никто не прошёл по следу до конца.

У настоящей причины два независимых конца, и оба должны закрыться вместе — исправление лишь одного откладывает переполнение, а не решает его:

  1. api никогда не настраивала предел тела Express. NestFactory.create в apps/api/src/main.ts не трогал JSON-парсер, поэтому действовало значение по умолчанию Express: 100 KB. Phoenix (engine) принимает тела до 8 MB. В самом тяжёлом направлении транспорта — engine → api, конкретно POST /internal/sessions/:sessionId/llm-turn, который получает ВСЮ ИСТОРИЮ разговора на каждой итерации ToolLoop, — api была самым узким местом в 80 раз, и никто этого не решал: это было отсутствие конфигурации, а не выбор.
  2. Сжатие контекста в engine было структурно недостижимо до переполнения. Engine.Harness.ContextManager.Default решает сжимать, когда оценка токенов превышает threshold * context_window. Два дефекта отодвигали этот триггер далеко за предел транспорта:
    • estimate/1 суммировал лишь поле content сообщений. Сообщения assistant с toolCalls (формат вызова инструмента) имеют пустой content — аргументы tool call, которые ЯВЛЯЮТСЯ реальными байтами в HTTP-теле, считались как ~ноль токенов.
    • Окно сжатия использовало лишь context_window — 128.000 токенов у пяти агентов gate/dev (qa_automacao_agent.ex, qa_performance_seguranca_agent.ex, qa_estrategia_agent.ex, appsec_agent.ex, dev_agent_server.ex). При threshold: 0.7 это даёт сжатие примерно на ~350 KB оценочного payload — далеко за любым разумным пределом транспорта, и ещё хуже, когда сама оценка уже занижала.

При gate в 60 итераций (предел агентов выполнения/gate) и трёх- четырёх tool results по 32 KiB (ИНДИВИДУАЛЬНЫЙ предел, уже закрытый RN-150) накопленное тело превышало 100 KB задолго до того, как сжатие хотя бы рассматривало возможность действовать.

Решение​

Оба конца закрываются одним исправлением, намеренно вместе:

  1. apps/api/src/main.ts начинает явно настраивать предел JSON-парсера (app.useBodyParser('json', { limit })), читаемый из API_JSON_BODY_LIMIT со значением по умолчанию 10mb — запас поверх 8 MB, которые Phoenix уже принимает, без необходимости нового deploy при изменении предела engine.
  2. Engine.Harness.ContextManager.Default получает:
    • estimate/1 считает content КАЖДОГО сообщения ПЛЮС JSON- сериализацию toolCalls сообщений assistant — эвристика байтов-на-токен ТА ЖЕ, что у приближённого токенизатора (Engine.Harness.Tokenizer.bytes_per_token/0, новая публичная функция, чтобы не дублировать константу).
    • ЭФФЕКТИВНОЕ окно сжатия становится min(context_window, teto_de_transporte), где предел транспорта — ОДНА новая конфигурация (transport_max_body_bytes, по умолчанию 8 MiB — предел СОБСТВЕННОГО транспорта engine, не повторяемый в пяти файлах агентов, которые продолжают объявлять context_window: 128_000, потому что это описывает МОДЕЛЬ, а не транспорт).
    • Разрез между старыми (суммаризируемыми) и недавними (сохраняемыми) сообщениями начинает соблюдать ГРАНИЦУ ИТЕРАЦИИ ToolLoop (group_by_iteration/1) — сообщение assistant с toolCalls и отвечающие на него role: "tool" всегда идут вместе на одну сторону разреза. Разрез посередине сломал бы протокол tool-use provider (результат инструмента без соответствующего вызова в истории).

Общее правило, которое фиксирует этот ADR: сжатие должно срабатывать ДО того, как тело переполнит реальный HTTP-предел, а не до того, как модель "забудет" своё окно. Предел транспорта, существующий лишь неявно (значение по умолчанию библиотеки), — не предел, а сбой, ждущий подходящего payload.

Последствия​

  • Значение transport_max_body_bytes (8 MiB) — объявленная конфигурация, не откалиброванная по реальному трафику, — то же мерило честности, которому уже следуют веса гибридного поиска (RAG) и пределы search_workspace (RN-150): регулируемая отправная точка, а не окончательное число.
  • Поднять лишь предел api, не исправив оценку engine (или наоборот), — значит вновь внести дефект в иной форме: либо тело снова растёт без тормоза до большего предела, либо сжатие начинает действовать слишком рано и суммаризировать контекст, который ещё поместился бы в транспорт. Оба конца родились в одном коммите.
  • dev.awaiting_gate как состояние dev agent на практике должно стать реже (gate больше не умирает от 413 на легитимном payload), но RN-412 также расширяет DEV_PENDING_TYPES, чтобы удерживать сессию в этом состоянии, — эшелонированная защита, а не расчёт на то, что этот ADR закроет 100% случаев медленного/зависшего gate по иной причине.