ADR 0038 — Иерархия агентов: области, leads и делегирование
Контекст
Начиная с Фазы 3b handoff адресуется любому агенту из реестра напрямую:
CreateHandoffUseCase получает toAgent и создаёт запись, не проверяя, кто может
быть целью, — Handoff { fromAgent, toAgent, artifactId, status }, со
свободным toAgent (apps/api/src/domain/sessions/handoff.entity.ts). Единственное правило
активации сегодня — canActivateAgent (domain/sessions/agent-activation.ts):
агент выходит на сцену только с адресованным ему handoff в состоянии accepted, кроме
Criativo, который запускается по команде пользователя.
Это работало до Фазы 4, потому что каждый агент был единым целым: один handoff,
один агент, одно заключение. Фаза 8 вводит подспециализацию внутри области —
первый экземпляр — QA (8b), у которого появляются QA de Automação и QA de
Performance/Segurança, — и в этой схеме handoff, адресованный напрямую
подспециализации, сломал бы предпосылку, на которую опирается вся остальная система: один
handoff — один ответ. Если бы Arquiteto мог адресовать qa-performance
напрямую, кто сводит это заключение с заключением qa-automacao? Ответ не
может быть «никто» — gate ожидает ОДИН вердикт на область
(pr-gate-state-machine.ts:nextGateStatus, который получает только один GateVerdict).
Этот ADR фиксирует обобщённую модель — область, lead, делегирование, консолидация, бюджет, сбой — до того, как появится какая-либо подспециализация. QA (8b) и субагент Workflows в Infra (8c) — экземпляры одной модели, а не собственные схемы.
Почему ИСТОЧНИК сбоя нужно типизировать сейчас
CLAUDE.md начиная с ADR 0020 требует, чтобы каждый исход сбоя фиксировал источник —
infra | modelo | código | política — и запрещает диагностику методом исключения. На
практике источник сегодня — свободный текст внутри reason/diagnosis: в
qa_agent_server.ex:176-183 falha_do_qa({:limit_reached, _}) возвращает фразу
«limite de iterações atingido sem emit_qa_verdict» — верную, но это не
значение, по которому другой код может ветвиться.
Пункт 4 этой фазы требует, чтобы lead решал на основе источника
(перераспределить | консолидировать частично | заблокировать), а автоматическое решение не может
ветвиться по прозе. Поэтому этот ADR типизирует failure_origin и дооснащает
места, которые уже неформально классифицируют сбой, — task_blocked, dev.error,
заключения QA и SecOps, — вместо того чтобы вводить тип только для делегирований и
оставлять остальную систему с двумя сосуществующими словарями сбоев.
Решение
1. Область принадлежит проекту, и у неё ровно один lead
agent_areas (одна строка на область, на проект) и agent_area_members (кто
входит, с булевым is_lead). Два инварианта гарантированы в базе,
а не только в приложении, — этот репозиторий трактует constraint как бизнес-правило
(docs/.docmap.yml, правило schema-e-migrations):
unique(project_id, agent)вagent_area_members— агент принадлежит не более чем одной области, и lead подчиняется тому же правилу: он не может быть ещё и обычным участником другой области.unique(area_id) where is_lead— не более одного lead на область. Существование lead (хотя бы одного) — инвариант создания, проверяемый в домене при сборке области, а не constraint базы — область, собираемая участник за участником, прошла бы через переходное состояние без lead, которое база не может отличить от ошибки.
Агенты без области — Criativo, PO, Arquiteto, Psicólogo, Anamnese — не входят в
agent_area_members и остаются ровно такими, как сегодня: адресуемыми прямым
handoff. Этот ADR не меняет их поток.
Dev остаётся вне в этой фазе. dev-<modulo> создаётся динамически
по модулю (ADR Фазы 4) и не становится областью — «Dev Lead» пока нет. Зафиксировано
как будущее расширение, не реализовано (см. Последствия).
2. Внешний handoff адресуется только lead или агенту без области
CreateHandoffUseCase — единственное место в системе, которое сегодня записывает toAgent без
проверки, — начинает вызывать assertHandoffTargetAllowed(toAgent, membrosDaÁrea)
перед созданием записи. Цель, являющаяся субагентом области (участник, не lead),
отклоняется типизированной ошибкой (HandoffToSubagentError, с агентом и областью
в ошибке), а не фильтруется молча и не продвигается к lead по ошибке.
OfferInfraHandoffUseCase (подтверждение готовности, сигнализирующее engine
предложить handoff InfraAgent) не записывает toAgent — он лишь запускает
engine, который затем вызывает CreateHandoffUseCase. Проверка живёт в одном
месте — в той точке, которая фактически решает цель.
Это расширенный прецедент agent-activation.ts: тот файл решает
«кто может АКТИВИРОВАТЬ»; этот решает «кто может быть ЦЕЛЬЮ внешнего handoff» —
второго вопроса не существовало, потому что до сих пор каждый агент был внешним по
определению.
3. Делегирование — внутренний механизм, и он приватен для области
delegations: lead делегирует задачу субагенту ТОЙ ЖЕ области.
assertDelegationAllowed(lead, subagent, membros) отклоняет того, кто не является lead
области, и того, кто делегирует за её пределы. Делегирование никогда не появляется как handoff —
это разные таблицы и жизненные циклы, и это различие сохраняет
свойство «lead — единственный внешний контакт»: ничто вне области не наблюдает
отдельное делегирование, только консолидированный результат.
4. Консолидация: один артефакт, внешний контракт не тронут
Lead закрывает полученный handoff одним артефактом
consolidated_verdict — новым типом в ArtifactSchemas (engine),
server-emitted, как уже task_blocked и *_verdict: ни один из трёх
не является тем, что модель решает выдать через tool call, — это запись исхода, который
определяет сервер. Payload: área, veredito, resumo и
delegações — список внутренних заключений, на которые ссылаются по id, а не
копируют (трассируемость без дублирования содержимого).
ArtifactSchemas проверяет ФОРМУ (у каждого делегирования completed есть
parecerArtifactId; у каждого failed есть failureOrigin) — это тот же вопрос, на который
check_extra/2 уже отвечает для qa_verdict/secops_verdict сегодня. Домен
api проверяет ПРАВИЛО — консолидировать можно только при всех разрешённых
делегированиях (assertConsolidatable, отклоняющий со списком недостающего), —
потому что только у api есть полный список делегирований области; engine в
момент выдачи артефакта знает лишь то, что уже получил.
Внешний контракт gates не меняется. nextGateStatus по-прежнему получает
один GateVerdict (approved | changes_requested) на gate — consolidated_verdict
— это то, что QA Lead использует, чтобы решить этот единственный вердикт; вызывающий gate
никогда не видит делегирования. Это центральная гарантия этого ADR: иерархия
невидима извне области.
5. Каскадный бюджет, сбой с обязательным источником
Потолок на области (agent_areas.budget_micros), подпотолок на делегирование
(delegations.budget_micros, необязательный — не каждому делегированию он нужен).
Превышение подпотолка становится failed с failure_origin = politica — это та же
классификация, которую бюджет сессии/проекта уже использует в
budget-threshold.ts, расширенная на делегирование.
budgets Фазы 1 не затрагивается. У него budgets_scope_check, закрытый
ровно на две области (project XOR session); размещение третьей
области там изменило бы центральную таблицу ради локальной потребности области.
Потолок области и подпотолок делегирования живут в новых таблицах.
Любой сбой субагента — превышение, эквивалент task_blocked, что угодно —
приходит к lead с заполненным failure_origin. Lead решает один из трёх
исходов, и решение является событием (area.decision), никогда не молчаливым
побочным эффектом:
- перераспределить: рождается новое делегирование, покрывающее то, что сбой оставил незавершённым;
- консолидировать частично:
consolidated_verdictзакрывается тем, что есть, цитируя делегированиеfailedи его источник — внешний потребитель видит полный вердикт, даже если внутри часть упала; - заблокировать: вся область блокируется с распространённым реальным источником —
никогда не обобщённый
changes_requested, как Фаза 4a уже исправила для случая QA без заключения (см. комментарий вqa_agent_server.ex:148-153, который этот ADR обобщает).
6. Источник сбоя: новый тип, дооснащение всей системы, уже классифицирующей сбой
failure_origin: infra | modelo | codigo | politica (enum Postgres, без
диакритики, как и остальные enum в schema.ts). Дооснащение — явное решение, а не
компромисс модели только с делегированием:
tasks.blocked_originиstories.blocked_origin(новые колонки рядом с существующимиblocked/blocked_reason— новое поле, не замена);Engine.Dev.AgentIo.block_task/3получает 4-й аргумент; ~18 мест вызова вdev_agent_server.ex/noop_dev_agent_server.exклассифицируются по одному (падающий worktree —infra; некорректный контекст —codigo; лимит итераций —modelo; бюджет —politica);falha_do_qa/1(qa_agent_server.ex) и аналог SecOps начинают возвращать источник вместе сreason/diagnosis;task_blockedвArtifactSchemasполучаетoriginв обязательных ключах.
Ни одно существующее сообщение не удаляется и не переписывается — источник — НОВОЕ поле рядом со свободным текстом, который продолжает существовать для чтения человеком.
Последствия
Что становится доступным
- Handoff субагенту — ошибка во время создания, а не латентный баг, который проявился бы только при попытке.
- Gate QA (8b) и субагент Workflows в Infra (8c) получают готовый механизм — ни одному не нужно заново изобретать делегирование или консолидацию.
- Каждое решение о частичном сбое становится проверяемым событием (
area.decision), закрывая пробел, который CLAUDE.md уже запрещал в теории и не мог предотвратить на практике — источник был текстом, а текст не мешаетifугадывать.
Что зафиксировано как будущее расширение, не реализованное
- Dev Lead.
dev-<modulo>по-прежнему создаётся динамически по модулю, без lead. Если однажды понадобится область, модель уже подходит — область не предполагает фиксированного числа участников или фиксированной инструкции на субагента. - Области, предлагаемые Arquiteto через
module_map. Сегодня область создаётся неявно объёмом каждой фазы (QA в 8b, Infra в 8c); идея о том, что Arquiteto динамически предлагает новую область на основе карты модулей, которую он уже производит, вне объёма — потребовался бы собственный поток одобрения.
Принятые риски
- Дооснащение источника затрагивает код Фазы 4, проверенный реальным выполнением (ADR 0020). Каждое из ~18 мест классифицируется отдельно, и ни одно существующее сообщение не меняется — риск в том, чтобы забыть место, а не в том, чтобы сломать уже работающее; тесты покрывают места, а не выборку.
delegation_statusрождается с неиспользуемым значением (dispensed, зарезервированным для 8b — «нет RNF производительности, делегирование освобождено»). Изменение enum Postgres после записи — миграция с блокировкой таблицы; оно рождается сейчас, чтобы не платить эту цену дважды.- Без просмотра осиротевших делегирований. Если engine умрёт между началом
делегирования и сообщением результата, оно остаётся
pendingнавсегда в этой фазе — lead не консолидирует (правильное поведение: не закрывать неполное заключение молча), но и автоматической тревоги нет. Оставлено до появления реального, а не гипотетического случая.
Закрытие (Фаза 8d)
Оба экземпляра, предусмотренные этим ADR, построены — QA (8b) и субагент Workflows в Infra (8c), — плюс сторона представления (8d: панель команды, сгруппированная по области, временная шкала PR, раскрывающая внутренние заключения, Insights, сгруппированные по области, лента, повествующая о делегировании). Закрываем то, что решено, против того, что осталось незакрытым:
consolidated_verdict не реализован — решение подтверждено на практике
Решение № 4 выше спроектировало обобщённый артефакт consolidated_verdict. Ни
QA, ни Infra его не используют: у обоих уже был СОБСТВЕННЫЙ и ПРЕДШЕСТВУЮЩИЙ
ADR контракт — qa_verdict (QA) и open_infra_pr (Infra), — и изменение этого контракта
сломало бы RecordGateVerdictUseCase/ExecuteInfraPrUseCase и демо, которые
уже доказывали счастливый путь без иерархии. Engine.Gates.QaLead. consolidar/1 производит ровно форму qa_verdict;
Engine.Infra.InfraLead.consolidar/2 производит объединение файлов, которое
open_infra_pr уже ожидал. Потребитель никогда не знал, что за этим стоит больше
одного агента, — центральная гарантия ADR (строка 118 выше) доказала себя
без необходимости в обобщённом артефакте.
consolidated_verdict по-прежнему доступен в ArtifactSchemas как
ЗАМЫСЕЛ, а не как код — будущая область без собственного артефакта для
переиспользования (в отличие от QA/Infra, у которых он уже был до превращения в
область) — естественный кандидат на его настоящую реализацию. До тех пор
установленный шаблон таков: предпочитайте переиспользовать артефакт, который область уже выдавала
до превращения в область; реализуйте обобщённый, только если его нет.
Dev Lead и динамические области — всё ещё не реализованы, и зафиксировано зерно
Два пункта, которые раздел «будущее расширение» уже фиксировал, остаются вне, подтверждённые после трёх реальных экземпляров модели (QA, Infra и UI, который их показывает). Фиксируем зерно второго, как было запрошено:
module_map уже диктует, СКОЛЬКО dev agents существует — по одному на модуль
(devAgentId/Engine.Dev.* сегодня выводятся из этого, без собственной таблицы:
существование dev — это ФУНКЦИЯ module_map, а не параллельная запись).
Естественный шаг — не реализованный здесь — чтобы Arquiteto предлагал не только
модули, но и их ГРУППИРОВКУ в область: например, payments-api и
payments-worker под областью «payments», с одним из двух (или выделенным
lead) в качестве внешнего контакта. Это стало бы той же динамикой «область
существует, потому что module_map говорит, что существует», которая сегодня действует только для отдельного
dev, — область перестала бы быть фиксированным каталогом (QA, Infra) и стала бы
ПРЕДЛОЖЕНИЕМ Arquiteto с одобрением пользователя, каким module_map уже
является.
Предпосылки для превращения этого в код, а не только идею: настоящие agent_areas/
agent_area_members (аппарат, который 8b/8c намеренно вырезали из объёма —
сегодня area/subagent/leadAgent в delegations — это
TEXT, без таблицы связи, потому что существовали только две фиксированные области,
известные заранее); поток одобрения для создания области (тот же
шаблон handoff/module_map, решает пользователь); и ответ на вопрос «кто
становится lead», когда область рождается из предложения, а не из фиксированного каталога, — Dev
Lead и динамическая область — ОДНА и та же открытая проблема, а не две.