temporality
Изменить страницу↗
Инженерные документы

Саги: явная регистрация долгой работы (Sagas)

#1. Цель

Сделать сагу — продолжительное усилие, которое живёт дольше одного рана и может переходить между агентами, — явной сущностью с явной регистрацией, а не скрытой группировкой по task_id.

Первоначальная реализация бюджета (kernel/cost) называла сагой каждую задачу: события группировались по ev.Context.Task, и любые раны одной задачи складывались в «сагу» автоматически. Это давало сумму затрат, но не отвечало на главный вопрос: какая работа считается одним целым, когда её ведут разные люди и агенты. Продолжение беседы в чате, делегирование, перехват задачи лидом у аналитика — всё это одна работа с общей стоимостью, и решение о том, что она едина, должен принимать агент, а не эвристика группировки.

Ключевые принципы:


#2. Модель

Saga (явная регистрация)
 └── Task 1 ── раны (продолжения чата, retry)
 │            └── делегированные дети (delegate/plan, наследуют TaskID)
 └── Task 2 ── раны (задача лида, присоединилась через saga_attach)

Задачи без саги никуда не исчезают: они сворачиваются в отдельный список «задачи без саги» на странице обзора проекта. Сага не обязана быть у каждой задачи — только у работы, которую агент считает продолжительной или сквозной.


#3. Инструменты агента

Инструмент Что делает Идемпотентность
saga_register(title, goal?) Регистрирует сагу и привязывает текущую задачу Повторный вызов для уже привязанной задачи возвращает существующую сагу без новой эмиссии
saga_attach(saga_id, note?) Привязывает текущую задачу к существующей саге Повторная привязка к той же саге возвращает already_attached; привязка к другой саге переносит задачу (последний выигрывает)
saga_list() Список саг проекта: id, title, goal, задачи, время Чтение

Требования к вызову: у рана должны быть Project и TaskID — иначе понятная ошибка («этот ран без задачи, привязывать нечего»).

saga_attach проверяет, что сага зарегистрирована в этом проекте (по journal), иначе отказ с подсказкой вызвать saga_list — защита от фантомных привязок по опечатке в id.

Классификация инструментов:


#4. События

Тип Data Context
saga.registered saga_id, title, goal?, origin (agent-tool / human-ui), task_id? Project, Run?, Task?, Actor
saga.attached saga_id, task_id, note?, origin? Project, Run?, Task?, Actor
saga.renamed saga_id, title?, goal?, origin=human-ui Project, Actor
saga.merged saga_id (цель), source_saga_ids[], origin=human-ui Project, Actor
saga.detached task_id, origin=human-ui Project, Task, Actor

EventID включает UnixNano (<run>/<type>/<nano> или saga-lifecycle/<saga>/<type>/<nano> для ручных) — повторные эмиссии после ретрая активности не коллайдят. Дубликаты безопасны по смыслу: проекция сворачивает их по saga_id/task_id.

Эмиссия идёт через durable outbox из activity (как skill.proposed), а не из workflow-потока: тул выполняется в ActivityRunTool.


#5. Распространение саги

Ничего из этого не требует новых механизмов распространения: сага ездит на задаче, задача — на ранах и делегациях.


#6. Агрегация (kernel/cost)

ProjectBudget теперь содержит три уровня:

Алгоритм агрегации двухпроходный и устойчив к порядку событий:

  1. Проход по saga.* событиям строит мету саг и карту task→saga (последняя по времени привязка выигрывает; привязка к неизвестной саге игнорируется).
  2. Проход по остальным событиям накапливает задачу в её собственный аккумулятор и, если задача привязана, — параллельно в аккумулятор саги.

SagaCount сменил смысл с «число задач» на «число явных саг»; TaskCount — новое поле. Пер-рановый /v1/agent/cost/task не изменился.

Детерминизм: сортировки саг и задач — по времени начала (новые сверху), tie-break по идентификатору; внутри саги задачи упорядочены по времени старта.


#7. UI

Вкладка Саги дашборда (/dashboard, рядом с «Запусками»):

Из вкладки Runs секция саг убрана: раны — исполнение, саги — учёт. Пер-рановый budget-вью (Task budget по выбранному рану) остался в Runs без изменений.


#8. Ручное управление (UI, writer)

Саги остаются event-sourced: ручные операции — это те же журнальные события, что и у тулов агента, только с origin=human-ui и человеческим актором. Никаких таблиц и переписываний истории — обе проекции (kernel/cost и saga_list) перекладывают события заново.

API (cmd/agent-kernel/sagalifecycle.go, все writer-only, ?project= обязателен):

Эндпоинт Тело Событие
POST /v1/workspace/sagas title, goal?, task_id? saga.registered
POST /v1/workspace/sagas/{id}/rename title?, goal? (хотя бы одно) saga.renamed
POST /v1/workspace/sagas/{id}/merge source_saga_ids[] saga.merged
POST /v1/workspace/sagas/{id}/delete — saga.deleted
POST /v1/workspace/tasks/{taskID}/saga saga_id (пустая — отвязать) saga.attached / saga.detached

Правила проекции:

Существование саги проверяется свёрткой жизненного цикла по журналу: saga.registered создаёт, saga.deleted отзывает (позднейшая повторная регистрация с тем же id воскрешает) — ровно та же свёртка, что строит rollup, поэтому валидация роутов и список в UI не могут разойтись. Плюс окно read-your-writes: только что созданная через роуты сага валидируется ~30 секунд, пока её регистрация стекает через outbox в журнал (bulk-create привязывает задачи сразу после создания); удалённая сага окном не воскрешается.

#Что сознательно не сделано


#9. Открытые вопросы