Идея: автономные агенты — серверные раны, отвязанные от окна браузера (event-sourced) #184

Open
opened 2026-06-25 04:44:31 +03:00 by Ghost · 0 comments

Статус: идея / proposal для обсуждения. Не подтверждённый объём работ. Заметка фиксирует архитектурное направление, наработанное в дипдайве по персистентности истории чата, чтобы будущие шаги ему не противоречили. Прямой предшественник по фундаменту — #183.

Мотивация

Хочу автономных агентов, которые продолжают работать без открытого окна браузера после запуска: поставил задачу — агент крутится на сервере, наработки персистятся, к ним можно вернуться, экспортировать, и любой следующий запуск видит сделанное. Сегодня это невозможно в принципе: цикл агента живёт ровно столько, сколько открыт HTTP-запрос (res.hijack() в ai-chat.controller.ts), фонового исполнения нет.

Ключевой сдвиг

Ран агента — серверный персистентный объект, а не функция внутри HTTP-запроса. БД — единственный источник истины. Все остальные (UI-лайв, экспорт, респаун, второй таб, автономный воркер) — её читатели/подписчики.

   trigger                ┌────────────────────────────────────────┐
 (user msg /              │  BullMQ worker (агентский цикл,         │
  autostart /             │  отвязан от сокета)                     │
  cron / api / continue)─►│   streamText → события:                 │
                          │     • text delta (коалесцируем)         │
                          │     • tool call / tool RESULT (сразу)   │
                          │     • step finish / run finish          │
                          └───────────────┬─────────────────────────┘
                                          │ append / upsert
                                          ▼
                             ┌──────────────────────────────┐
                             │  POSTGRES = SOURCE OF TRUTH   │
                             │  ai_chat_runs (lifecycle)     │
                             │  ai_chat_messages (проекция)  │
                             │  [ai_chat_events опц.]        │
                             └──────┬───────────────┬────────┘
                                    │               │
                одна проекция читается одинаково всеми:
              UI live tail │ Export(MD) │ Respawn-контекст │ Второй таб/девайс

Составляющие

1. ai_chat_runs — ран как первоклассный объект

  • Поля: status (pending / running / succeeded / failed / aborted), chat_id, trigger (user / autostart / schedule / api / continue), created_by, токен/шаг-бюджет, error, тайминги.
  • Даёт: «один активный ран на чат» (advisory-lock), audit, бюджеты и kill-switch, и точку, к которой подписывается клиент (runId).

2. Запись по событию, проекция — сообщение

  • Ассистентское сообщение создаётся в начале хода (status='streaming') и инкрементально материализуется: текст — коалесцированно, tool result — немедленно (граница сайд-эффекта). Это уже делает #183.
  • Снизу опционально append-only ai_chat_events как полный аудит, а ai_chat_messages — его проекция.

3. Воркер на BullMQ

  • BullMQ + ioredis уже в стеке (крутят эмбеддинги — embedding.processor.ts). Переиспользуем как исполнитель агентского цикла, отвязанного от сокета. HTTP только ставит ран и подписывается.
  • Чистая flushAssistant(capturedSteps, status) из #183 переиспользуется воркером как есть.

4. Триггеры рана без открытого окна

  • Autostart как серверный ран: сейчас auto_start / launch_message (ai-agent-roles) — чисто клиентское поведение (клиент авто-отправляет сообщение). Перенести в серверный триггер рана.
  • Schedule / cron: периодический запуск агента по роли/задаче.
  • API / webhook: внешний триггер.
  • «Продолжай» как новый ран поверх персистентного контекста (контекст уже из БД).

5. Клиент — просто подписчик

  • Когда браузер есть — он один из подписчиков рана: resumable stream по runId (AI SDK v6 умеет нативно, Redis уже есть). На reload / втором табе — реплей из персистентного стрима, никакого in-memory-only состояния.
  • Когда браузера нет — ран всё равно идёт; результат увидим при следующем открытии чата.

Что нужно решить отдельно (риски автономии)

  • Один активный ран на чат — advisory-lock / уникальность, чтобы автономный и пользовательский запуски не топтались.
  • Бюджеты и kill-switch — лимит шагов/токенов/стоимости на ран, ручная остановка, защита от runaway-циклов и расходов.
  • Идемпотентность tool-сайд-эффектов при ретраях воркера — повтор джобы не должен дублировать правки страниц.
  • Компакция длинной истории — саммари вместо нынешней молчаливой обрезки окна findRecent(50).
  • Provenance уже есть и не спуфитсяusers.is_agent + agent-provenance (pages.last_updated_ai_chat_id, comments.ai_chat_id); переиспользовать для атрибуции автономных правок.
  • Транспорт лайв-хвоста (resumable streams AI SDK + Redis vs подписка на проекцию из БД) — решается на этом этапе, не раньше.

Чем #183 готовит почву (не противоречит этой идее)

  • flushAssistant как чистая функция от накопленных шагов → готова к вызову из воркера.
  • Инкрементальная строка + status — это и есть проекция, которую позже поддержит слой ai_chat_runs / событий.
  • Серверный экспорт из БД (без браузера) — autonomy-friendly по определению.
  • Персистентность уже идёт независимо от сокета (consumeStream) — не усиливаем привязку к HTTP.

Ориентир по этапам (когда дойдут руки)

  1. Event-sourced + воркер: вынести цикл в BullMQ, ввести ai_chat_runs, resumable stream по runId, проекция событий, single active run per chat.
  2. Автономия: триггеры ранов без окна (autostart-as-server-run, schedule, api, continue), бюджеты / kill-switch, идемпотентность, компакция истории.
> **Статус: идея / proposal для обсуждения.** Не подтверждённый объём работ. Заметка фиксирует архитектурное направление, наработанное в дипдайве по персистентности истории чата, чтобы будущие шаги ему не противоречили. Прямой предшественник по фундаменту — #183. ## Мотивация Хочу автономных агентов, которые продолжают работать **без открытого окна браузера** после запуска: поставил задачу — агент крутится на сервере, наработки персистятся, к ним можно вернуться, экспортировать, и любой следующий запуск видит сделанное. Сегодня это невозможно в принципе: цикл агента живёт ровно столько, сколько открыт HTTP-запрос (`res.hijack()` в `ai-chat.controller.ts`), фонового исполнения нет. ## Ключевой сдвиг **Ран агента — серверный персистентный объект, а не функция внутри HTTP-запроса. БД — единственный источник истины. Все остальные (UI-лайв, экспорт, респаун, второй таб, автономный воркер) — её читатели/подписчики.** ``` trigger ┌────────────────────────────────────────┐ (user msg / │ BullMQ worker (агентский цикл, │ autostart / │ отвязан от сокета) │ cron / api / continue)─►│ streamText → события: │ │ • text delta (коалесцируем) │ │ • tool call / tool RESULT (сразу) │ │ • step finish / run finish │ └───────────────┬─────────────────────────┘ │ append / upsert ▼ ┌──────────────────────────────┐ │ POSTGRES = SOURCE OF TRUTH │ │ ai_chat_runs (lifecycle) │ │ ai_chat_messages (проекция) │ │ [ai_chat_events опц.] │ └──────┬───────────────┬────────┘ │ │ одна проекция читается одинаково всеми: UI live tail │ Export(MD) │ Respawn-контекст │ Второй таб/девайс ``` ## Составляющие ### 1. `ai_chat_runs` — ран как первоклассный объект - Поля: `status` (pending / running / succeeded / failed / aborted), `chat_id`, `trigger` (user / autostart / schedule / api / continue), `created_by`, токен/шаг-бюджет, `error`, тайминги. - Даёт: «один активный ран на чат» (advisory-lock), audit, бюджеты и kill-switch, и точку, к которой подписывается клиент (`runId`). ### 2. Запись по событию, проекция — сообщение - Ассистентское сообщение создаётся в начале хода (`status='streaming'`) и инкрементально материализуется: текст — коалесцированно, **tool result — немедленно** (граница сайд-эффекта). Это уже делает #183. - Снизу опционально append-only `ai_chat_events` как полный аудит, а `ai_chat_messages` — его проекция. ### 3. Воркер на BullMQ - BullMQ + ioredis **уже в стеке** (крутят эмбеддинги — `embedding.processor.ts`). Переиспользуем как исполнитель агентского цикла, отвязанного от сокета. HTTP только ставит ран и подписывается. - Чистая `flushAssistant(capturedSteps, status)` из #183 переиспользуется воркером как есть. ### 4. Триггеры рана без открытого окна - **Autostart как серверный ран:** сейчас `auto_start` / `launch_message` (`ai-agent-roles`) — чисто клиентское поведение (клиент авто-отправляет сообщение). Перенести в серверный триггер рана. - **Schedule / cron:** периодический запуск агента по роли/задаче. - **API / webhook:** внешний триггер. - **«Продолжай»** как новый ран поверх персистентного контекста (контекст уже из БД). ### 5. Клиент — просто подписчик - Когда браузер есть — он один из подписчиков рана: resumable stream по `runId` (AI SDK v6 умеет нативно, Redis уже есть). На reload / втором табе — реплей из персистентного стрима, никакого in-memory-only состояния. - Когда браузера нет — ран всё равно идёт; результат увидим при следующем открытии чата. ## Что нужно решить отдельно (риски автономии) - **Один активный ран на чат** — advisory-lock / уникальность, чтобы автономный и пользовательский запуски не топтались. - **Бюджеты и kill-switch** — лимит шагов/токенов/стоимости на ран, ручная остановка, защита от runaway-циклов и расходов. - **Идемпотентность tool-сайд-эффектов при ретраях воркера** — повтор джобы не должен дублировать правки страниц. - **Компакция длинной истории** — саммари вместо нынешней молчаливой обрезки окна `findRecent(50)`. - **Provenance уже есть и не спуфится** — `users.is_agent` + `agent-provenance` (`pages.last_updated_ai_chat_id`, `comments.ai_chat_id`); переиспользовать для атрибуции автономных правок. - **Транспорт лайв-хвоста** (resumable streams AI SDK + Redis vs подписка на проекцию из БД) — решается на этом этапе, не раньше. ## Чем #183 готовит почву (не противоречит этой идее) - `flushAssistant` как чистая функция от накопленных шагов → готова к вызову из воркера. - Инкрементальная строка + `status` — это и есть проекция, которую позже поддержит слой `ai_chat_runs` / событий. - Серверный экспорт из БД (без браузера) — autonomy-friendly по определению. - Персистентность уже идёт независимо от сокета (`consumeStream`) — не усиливаем привязку к HTTP. ## Ориентир по этапам (когда дойдут руки) 1. **Event-sourced + воркер:** вынести цикл в BullMQ, ввести `ai_chat_runs`, resumable stream по `runId`, проекция событий, single active run per chat. 2. **Автономия:** триггеры ранов без окна (autostart-as-server-run, schedule, api, continue), бюджеты / kill-switch, идемпотентность, компакция истории.
Ghost added the idea label 2026-06-25 04:44:31 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#184