Skip to content

Модель рантайма

Эта страница — поток: как сообщение превращается в работу, один турн от начала до конца. Та же история простым языком — Жизнь одного запроса; протокол на уровне провода — на страницах Worker. Здесь — последовательность, режимы и то, что переживает турн.

Турн одним взглядом

сообщение
  → orchestrator   сохранить сообщение · загрузить агента · регидрировать контекст
  → enforcement    гейт квот/лимитов — до любых трат
  → LLM-цикл       один объединённый MCP-тулсет:
       ├─ без тул-коллов  → стримим ответ → сохраняем → готово  (без пода — большинство турнов)
       ├─ brain-тул       → в процессе    (memory · secret · cron · knowledge …)
       └─ worker-тул      → поднять worker-сессию (однажды) → tools/call по каналу тулов
  → wrap-up        сохранить ответ + summary · заэмитить usage · отпустить

Шаг за шагом

  1. Интейк. orchestrator сохраняет сообщение пользователя (agent/chat), загружает конфиг агента (agent) и регидрирует рабочий контекст из Postgres — хвост окна + бегущий summary (контекст-движок, см. Короткую память).
  2. Enforcement. setting/enforcement проверяет квоты/лимиты команды до любой работы.
  3. Цикл. llm.stream() крутится в мозге с одним объединённым тулсетом: brain-MCP (in-process тулы), worker-MCP (подключён только пока жива worker-сессия) и connector-MCP (API магазина). У worker'а нет LLM — цикл никогда не покидает мозг.
  4. Тулы не нужны → стримим ответ, сохраняем, готово. Под не создаётся; это дешёвый путь, и большинство турнов идёт по нему.
  5. Нужны руки — первый worker-тул-колл запускает провижининг:
    • переиспользовать живую AgentRuntimeSession для (agent, chat), если она running/idle — очередь трогается максимум раз на сессию, никогда на тул-колл;
    • иначе — provision-задача в BullMQ; менеджер рантайма (runtime/worker) выбирает RuntimeProfile, собирает k8s Job, инжектит короткоживущие креды;
    • worker загружается, звонит обратно по WebSocket, MCP initialize + tools/list — его тулы входят в тулсет сессии (Deploy);
    • дальше мозг гоняет tools/call по каналу тулов; прогресс и логи стримятся во фронтенд через Redis pub/sub (runtime/event).
  6. Wrap-up. Результаты сохраняются через system/file; записываются ответ ассистента и обновлённый summary; эмитятся usage BillingEvents; мозг отпускает. Worker-сессия живёт свой idle-интервал — следующий турн её переиспользует — пока reaper её не снесёт (Destroy).

Что завершает ход

Ход отменяет закрытие сокета. Закрытая вкладка, перезагрузка, уход из приложения, пропавшая сеть и собственная кнопка «стоп» в чате (она обрывает браузерный fetch, а значит закрывает сокет) заканчиваются одинаково: отмена идёт вниз по циклу — в исполняющийся инструмент и в сам вызов модели, — поэтому ход прекращается за миллисекунды, а не жжёт токены для читателя, который ушёл.

SPA-навигация ход НЕ отменяет, и это сделано намеренно. Переключение вкладки карточки, открытие другой страницы, размонтирование компонента чата — браузер держит запрос живым, ход досчитывается и у агента персистится. Пользователь, который переключил экран, никуда не ушёл; ответ по-прежнему для него, и убить ход значило бы выбросить работу, которую он вот-вот прочтёт. «Отменять на размонтировании» — не недостающая половина отмены, а регресс, который только и ждёт, чтобы его написали.

Остановленный ход агента сохраняет уже доставленный текст с пометкой «остановлено», поэтому тред после перезагрузки говорит то же, что говорил экран. Ход, завершившийся ошибкой, не сохраняет ничего.

Один ход на агента

У агента одновременно идёт один ход — это держит блокировка в Redis:

  • блокировка истекает через 5 минут и, пока ход жив, продлевается до полных пяти примерно каждую треть этого срока, но не дольше потолка в 60 минут — так длинный ход не теряет блокировку, а потерянный не может держать агента вечно;
  • блокировка best-effort: недоступный Redis не имеет права ни задержать ход, ни уронить его;
  • автозапуск её читает — занятый агент пропускается, а не прерывается.

Потолки одного хода

  • 8 итераций цикла инструментов. Достигнутый потолок сохраняет частичный результат, а не выбрасывает его.
  • 3 изменяющих данные вызова инструмента за ход, в любом ходе — агентском не меньше, чем консьержском. Бюджет хода тратит каждый пишущий инструмент: создание и архивация агента, переписывание промпта автозапуска, смена частоты, запись заметки в память. Четвёртая попытка получает отказ фразой, которую модель может прочитать, а не молчание. Без этого одна неудачная формулировка могла бы сделать восемь изменений в одном ответе, потому что цикл считает обращения к модели и понятия не имеет, какие инструменты что-то пишут. Один вызов — одно изменение, даже если он двигает сразу три поля: бюджет считает изменения рабочего пространства, а не поля.
  • 429 от провайдера модели выходит наружу как 429, а его недоступность — как 503, и никогда как невнятная пятисотка: клиент должен различать «повтори чуть позже» и «у нас поломка».

Режимы рантайма → примитивы k8s

Подслайс orchestrator/mode решает, сколько машины получает турн:

Режим берётся из колонки runtimeProfile самого агента, и по умолчанию это none — агент, которому никто не выдавал рук, до этой таблицы не доходит вовсе. Внутри режима ничего не поднимается, пока модель действительно не вызовет инструмент воркера; см. Руки.

РежимДля чегоПримитивIdle
Noneрук нет — значение по умолчаниюбез пода — только LLM + памятьn/a
Lightbash, скрипты, файлыk8s Job, restartPolicy: Never, ttlSecondsAfterFinished60–120s
Browserиспользуется браузер-тулworker Job с headless Chromium в поде (Playwright, --user-data-dir на тенанта)пока браузер не простаивает
Heavyскрейпинг, большой computek8s Job на нодпуле workers, высокие лимиты30–60s
WarmpremiumLight/Heavy живёт дольше idle10–30 мин

Когда сессия падает, запись говорит каким именно способом

Сессия воркера падает шестью разными способами, и каждый указывает на своё место, куда идти смотреть, — поэтому строка несёт причину, а не одно слово failed:

причиначто подозревать
dial_timeoutпровижининг — под не дозвонился вовсе
handshake_timeoutобраз или пропуск, который ему выдали
ready_timeoutпод поднялся, но так и не достартовал
session_mismatchзвонящий не та сессия, за которую себя выдаёт
channel_versionвыкатка — стороны говорят на разных версиях
heartbeat_lostзомби: был жив и перестал отвечать

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

Что переживает турн

Мозг и worker исчезают; это — нет:

СостояниеЖивёт в
сообщения + бегущий summaryagent/chat (Postgres) — см. Короткую память
долговременная память и фактыagent/memory — см. Память
строки Task · AgentRuntimeSessionruntime (Postgres) — см. Схему БД
файлы и артефактыsystem/file → object storage
тратыusage BillingEvents → billing

Очередь и масштабирование

  • BullMQ на Redis — провижининг + scheduled-задачи; delayed-jobs заодно служат idle-таймерами; pub/sub несёт поток событий.
  • Нативные k8s Jobs — образ worker'а несёт Playwright + Chromium (браузер в поде, без отдельного пула); ttlSecondsAfterFinished даёт scale-to-zero. Без KEDA/Knative для MVP.
  • Короткоживущие токены сессий — Core чеканит JWT со скоупом одной AgentRuntimeSession (user/auth); долгоживущие секреты в под не попадают (безопасность).

Смотрите также