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

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 и динамическая область — ОДНА и та же открытая проблема, а не две.