Модель рантайма
Эта страница — поток: как сообщение превращается в работу, один турн от начала до конца. Та же история простым языком — Жизнь одного запроса; протокол на уровне провода — на страницах Worker. Здесь — последовательность, режимы и то, что переживает турн.
Турн одним взглядом
сообщение
→ orchestrator сохранить сообщение · загрузить агента · регидрировать контекст
→ enforcement гейт квот/лимитов — до любых трат
→ LLM-цикл один объединённый MCP-тулсет:
├─ без тул-коллов → стримим ответ → сохраняем → готово (без пода — большинство турнов)
├─ brain-тул → в процессе (memory · secret · cron · knowledge …)
└─ worker-тул → поднять worker-сессию (однажды) → tools/call по каналу тулов
→ wrap-up сохранить ответ + summary · заэмитить usage · отпуститьШаг за шагом
- Интейк.
orchestratorсохраняет сообщение пользователя (agent/chat), загружает конфиг агента (agent) и регидрирует рабочий контекст из Postgres — хвост окна + бегущий summary (контекст-движок, см. Короткую память). - Enforcement.
setting/enforcementпроверяет квоты/лимиты команды до любой работы. - Цикл.
llm.stream()крутится в мозге с одним объединённым тулсетом: brain-MCP (in-process тулы), worker-MCP (подключён только пока жива worker-сессия) и connector-MCP (API магазина). У worker'а нет LLM — цикл никогда не покидает мозг. - Тулы не нужны → стримим ответ, сохраняем, готово. Под не создаётся; это дешёвый путь, и большинство турнов идёт по нему.
- Нужны руки — первый 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).
- переиспользовать живую
- Wrap-up. Результаты сохраняются через
system/file; записываются ответ ассистента и обновлённый summary; эмитятсяusageBillingEvents; мозг отпускает. 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 |
| Light | bash, скрипты, файлы | k8s Job, restartPolicy: Never, ttlSecondsAfterFinished | 60–120s |
| Browser | используется браузер-тул | worker Job с headless Chromium в поде (Playwright, --user-data-dir на тенанта) | пока браузер не простаивает |
| Heavy | скрейпинг, большой compute | k8s Job на нодпуле workers, высокие лимиты | 30–60s |
| Warm | premium | Light/Heavy живёт дольше idle | 10–30 мин |
Когда сессия падает, запись говорит каким именно способом
Сессия воркера падает шестью разными способами, и каждый указывает на своё место, куда идти смотреть, — поэтому строка несёт причину, а не одно слово failed:
| причина | что подозревать |
|---|---|
dial_timeout | провижининг — под не дозвонился вовсе |
handshake_timeout | образ или пропуск, который ему выдали |
ready_timeout | под поднялся, но так и не достартовал |
session_mismatch | звонящий не та сессия, за которую себя выдаёт |
channel_version | выкатка — стороны говорят на разных версиях |
heartbeat_lost | зомби: был жив и перестал отвечать |
Расхождение версий в этом списке не случайно. Стороны выкатываются раздельно, поэтому согласны они будут не всегда, — а рукопожатие, которое не умеет сказать «я говорю на другой версии», отказывает зависанием вместо отказа, и это та поломка, которую разбирают часами, а не секундами.
Что переживает турн
Мозг и worker исчезают; это — нет:
| Состояние | Живёт в |
|---|---|
| сообщения + бегущий summary | agent/chat (Postgres) — см. Короткую память |
| долговременная память и факты | agent/memory — см. Память |
строки Task · AgentRuntimeSession | runtime (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); долгоживущие секреты в под не попадают (безопасность).
Смотрите также
- Worker — Deploy / Use / Destroy — жизненный цикл сессии и MCP-канал тулов на уровне провода.
- Короткая память — как контекст-движок собирает промпт каждый турн.
- Схема БД — формы
TaskиAgentRuntimeSession. - Как это работает — та же история простым языком.