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

ADR 0052 — Dev agent ждёт одобрения посреди цикла

  • Статус: Принят — реализован и доказан тестом в фазе A
  • Дата: 2026-08-05
  • Контекст: FASE 13b — первое реальное выполнение dev agent с моделью через API

Контекст​

terminal dev agent — инструмент :pipeline: hook Engine.Harness.Hooks.ActionPipeline создаёт proposed_action в api, которая решает через permissions.json. Когда решение — auto_approve, api выполняет сразу, и hook возвращает реальный вывод команды. Когда require_approval, hook возвращает строку proposed_action <id> status pending как результат инструмента — и ToolLoop переходит к следующей итерации.

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

В реальном выполнении (проект saudacao-local, task cd652d85) итог был таким:

toolloop.limit_reached {"iteration": 8, "max_iterations": 8}
backlog.task_blocked {"reason": "limite de iterações atingido"}

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

Почему allowlist не решает​

DEV_TERMINAL_ALLOW_PATTERNS существует именно чтобы избежать этого, и комментарий в activate-execution.use-case.ts:139-144 точно описывает сбой. Но это список предусмотренных команд, а модель изобретает команды. После добавления команд чтения (ls, find, pwd, cat…) тот же агент продвинулся довольно далеко — worktree создан, команда с exit 0, файлы прочитаны — и снова застрял на git branch -a и git rev-parse.

Каждая новая модель, каждый новый стек — новая дыра. Allowlist — это смягчение, и он не может быть ответом.

Что уже существует и доказывает путь​

:awaiting_approval уже является состоянием dev agent, созданным в Фазе 12e для конца task: когда pr_open остаётся ожидающим, агент удерживает worktree и освобождается через task.pr_settled, выдаваемый api и маршрутизируемый DevAgentWakeWorker. Весь механизм — сохранённое состояние, удержание worktree, событие outbox, worker, Wake.deliver — готов и задействован.

Нужно применить его к середине цикла, а не изобретать.

Решение​

Когда инструмент :pipeline возвращает pending, ToolLoop останавливается, и агент переходит в :awaiting_approval, удерживая всё. Решение пользователя выдаёт событие, которое его будит, и цикл возобновляется с той точки, где остановился, с реальным результатом команды на месте строки "pending".

Почему остановка, а не блокирующее ожидание​

Очевидная альтернатива — hook ждёт решения опросом — проще и неверна: ToolLoop работает внутри callback GenServer агента (dev_agent_server.ex:365). Блокировка там застопорила бы сам почтовый ящик, который принёс бы решение, а также корректное завершение и circuit breaker. Остановка и возобновление по сообщению — это то, что уже делает pr_settled, и по той же причине.

Сообщения остаются в памяти​

ctx цикла (с историей) остаётся в состоянии GenServer, пока агент ждёт. Не сохраняется: агент продолжает жить, лишь простаивает. Это сохраняет свойство, которое восстановление после перезапуска уже объявляет, — ход и сообщения существуют только в памяти, а перезапуск во время task блокирует с диагнозом (dev_agent_server.ex:153-159). Ожидающее одобрение, переживающее перезапуск, попадает на этот же путь, который честен и уже существует.

Пять частей​

#ГдеЧто
1apps/api/.../approve-action.use-case.ts, deny-action.use-case.tsвыдавать task.action_settled, когда актор действия — dev agent, — с actionId, status и результатом выполнения
2apps/engine/.../hooks/action_pipeline.exпри получении pending сигнализировать остановку вместо возврата строки
3apps/engine/.../harness/tool_loop.exподдерживать {:awaiting_approval, action_id, ctx} как исход, рядом с уже существующими halted
4apps/engine/.../dev/dev_agent_server.exхранить ctx, сохранять :awaiting_approval + action_id и возобновлять ToolLoop.run/1 при получении {:action_settled, …}
5apps/engine/.../workers/dev_agent_wake_worker.exмаршрутизировать task.action_settled агенту, как уже делается с task.pr_settled

Отказ — тоже ответ​

Отклонённое действие возобновляет цикл с причиной отказа на месте результата — агент узнаёт, что этот путь закрыт, и пробует другой. Отклонение не блокирует task: это решение пользователя, и здесь действует тот же принцип, что у pr_settled с opened: false (не учитывается circuit breaker).

Предел ожидания​

Бесконечное ожидание — это бессмертная сессия под другим именем. Агент ждёт до предела бездействия task; при превышении блокирует с origin: política — task остановилась, потому что никто не решил, а это отличается от дефекта кода или модели.

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

За

  • Агент перестаёт сжигать итерации на то, что не может выполнить, а одобрения пользователя начинают действовать независимо от того, когда приходят.
  • Allowlist перестаёт быть единственной защитой и возвращается к роли, для которой был задуман: избавлять от одобрения рутину, а не делать агента работоспособным.
  • Пользователь видит dev.awaiting_approval в ленте и знает, что агент ждёт его, вместо того чтобы видеть task, заблокированную "limite de iterações".

Против

  • Ожидающий агент занимает worktree и не берёт следующую task. Это та же уже принятая цена, что в awaiting_gate, и по той же причине: worktree — на АГЕНТА, а не на task.
  • ctx в памяти означает, что перезапуск во время ожидания теряет ход. Это остаётся на уже существующем пути блокировки с диагнозом; сохранение истории сообщений — отдельная проблема, не этого ADR.

Рассмотренные альтернативы​

Увеличить max_iterations. Лишь откладывает: при ожидающем одобрении каждая итерация тратится впустую, поэтому любой предел достигается.

Автоодобрять terminal через agent_autonomy. Разрешило бы ЛЮБУЮ команду внутри контейнера engine, и именно это dev-terminal-patterns.ts:11-15 отвергает — с файлом deny продолжает побеждать allow, а BUILTIN_DENY_PATTERNS остаются активными.

Опрос в hook. Застопоривает GenServer, как объяснено выше.

Ссылки​

  • ADR 0045 — машина состояний и перепланирование по событию, которые расширяет этот ADR
  • RN-047 — circuit breaker dev agent
  • Реальное выполнение: проект saudacao-local, task cd652d85, 2026-08-05