ADR 0008 — Разговорный агент Criativo в harness + фундамент handoffs
- Статус: принят
- Дата: 2026-07-24
- Фаза: 3b (сессия 1)
Контекст
Фаза 3a поставила детерминированный harness + одноразовый ToolLoop
(автономный, работает до остановки), а человеческий чат — это stateless api-only echo
(SendChatMessageUseCase отправляет provider лишь текущее сообщение — без
истории, без harness, без tools; engine даже не участвует). Фазе 3b нужен
разговорный агент Criativo, ведущий идеацию ВМЕСТЕ с пользователем,
выдающий business_rule по ходу разговора и — только после явного подтверждения
пользователя — выдающий product_brief и предлагающий handoff к PO. Также нужен
фундамент handoffs с правилом, которое охраняет активацию агентов.
Жёсткое правило CLAUDE.md: "Агенты работают ВСЕГДА внутри Harness" и "engine НИКОГДА не говорит с LLM-provider напрямую". Следовательно, Criativo живёт в engine (harness), и каждый ход проходит через api (metering + budget).
Решения
1. Criativo как GenServer с состоянием (не stateless)
Один Engine.Agents.CriativoServer (GenServer, restart: :temporary) на сессию,
под надзором CriativoSupervisor (DynamicSupervisor), зарегистрированный в
Engine.Sessions.Registry с ключом "criativo:<session_id>" — повторяет
SessionServer. История разговора живёт в памяти и регидратируется из
session_events (chat.message/agent.response) в init — event log является долговечным
источником истины. Запускается командой пользователя (исключение из правила
активации), через api → ApiToEngineClient.startAgent → engine
POST /internal/sessions/:id/agent/start.
Отброшенная альтернатива: восстанавливать историю из event log на каждом ходе (без
живого процесса). Мы выбрали GenServer, чтобы иметь естественное место для состояния и
жизненного цикла на сессию (в соответствии с SessionServer).
2. Потоковая передача по токенам через SSE engine→api + ретрансляция Phoenix
Каждое сообщение пользователя выполняет ОДИН потоковый ход: StreamLlmTurnUseCase
(SSE-брат RunLlmTurnUseCase из 3a, который остаётся turn-result для
ToolLoop) передаёт дельты; CriativoServer потребляет SSE
(EngineApiClient.llm_turn_stream) и ретранслирует дельты в web через
канал Phoenix session:<id> (agent.delta/agent.done) — который web уже
подключал лишь для heartbeat. Итоговое сохранение (agent.response + артефакты)
приходит через опрос session-events (3 с), который SessionPage уже делает. Metering
(token_usage) обязателен и выполняется в api; engine никогда не говорит с provider
напрямую. Частично отменяет решение turn-result из 3a только для интерактивных
агентов.
3. Handoffs: таблица с изменяемым статусом + неизменяемые события
Новая таблица handoffs {from_agent, to_agent, artifact_id, status: offered|accepted|completed|rejected}. В отличие от таблиц событий,
status ИЗМЕНЯЕМ (это текущее состояние); каждый переход также становится
неизменяемым session_event handoff.*. Владелец таблицы — api: engine создаёт
handoff через POST /internal/sessions/:id/handoffs (никогда не пишет в таблицу api
напрямую).
4. Правило активации агента (чистый домен)
domain/sessions/agent-activation.ts: агент активируется в сессии только при
handoff accepted, адресованном ему; единственное исключение: Criativo (запускается
командой пользователя — USER_STARTED_AGENTS). Чисто и протестировано изолированно
(повторяет decide.ts/session-state-machine.ts); ActivateAgentUseCase
загружает handoffs и применяет правило перед подъёмом процесса в engine.
5. Готовность — действие пользователя; product_brief выдаётся сервером (защита в домене)
"Подтверждение готовности — действие пользователя (кнопка), а не вывод модели."
business_rule выдаётся моделью через инструмент emit_artifact (с
origin — ссылками на разговор — проверяемым на НЕпустоту в ArtifactSchemas).
product_brief НЕ выдаётся инструментом (known/0 его исключает; EmitArtifact блокирует
типы, выдаваемые системой) — он выходит только когда пользователь кликает "Estou pronto para
produzir" (ConfirmReadinessUseCase → engine confirm_readiness): тогда
CriativoServer выполняет ход консолидации, выдаёт product_brief напрямую
(append_event_returning, захватывая id) и создаёт handoff offered к PO.
Это делает "brief только после подтверждения" гарантией домена, а не prompt.
6. Артефакты остаются типизированными session_events
Без таблицы артефактов: business_rule/product_brief — это session_events
artifact.<tipo> с payload, проверяемым в engine (ArtifactSchemas). Web
читает их тем же опросом событий (карточки правил в боковой панели; разделитель
handoff в чате, когда появляется handoff.offered).
Область
Только фундамент handoffs (таблица + правило + handoff offered) и Criativo. PO (принять handoff, активировать PO, backlog) — более поздняя сессия 3b. Без Bitbucket/GenericGitProvider; очереди остаются в Postgres (Oban), без Redis.
Последствия
- Web получает второй путь чата (агент vs. человек), решаемый по
agent.activatedв event log; сессии без Criativo остаются в человеческом чате. CriativoServerрегидратируется из event log — переживает перезапуск, не теряя нить разговора.- Детерминированные тесты: правило активации (unit),
ArtifactSchemas(обязательный источник) иCriativoServerчерез прямыеinit/1+handle_call/3со скриптованным фейком LLM (streaming, защита product_brief, готовность→brief+handoff, broadcast, регидратация).