← Agent Engine

Модель данных

agent-engine · workzone

Agent Engine хранит определения агентов и журнал прогонов, плюс тонкую связку agent_tools — выбор опциональных инструментов. Личность агента не отдельная сущность, а FK на владельца (agents); недельный бюджет не таблица-счётчик, а сумма по agent_runs. Знания живут в Knowledge Store, потолок недельного бюджета — в platform_settings; из чужого здесь хранятся две ссылки — user_id владельца и model_id выбранной модели, всё прочее читается у соседей на лету.

Определение агента: agents

Что сотрудник завёл: имя, промт (его слой композиции), расписание и два замка — выключатель владельца и липкий замок админа. FK на владельца несёт двойную службу: и каскад удаления, и личность, под которой агент читает базу.

1 Агент Что завёл сотрудник — промт, расписание, стопы.
agents определения агентов
FK → users
id BigInteger PK
user_id BigInteger FK→usersIDX владелец · CASCADE · его ACL = личность агента перед KS
name Text NOT NULL имя агента — для списка «мои агенты»
description Text NULL короткое описание — подзаголовок карточки агента · опционально · не prompt: человеку, не модели
prompt Text NOT NULL слой владельца · композируется под платформенным prompt в рантайме
schedule JSONB NULL простое расписание — размеченное объединение (интервал / календарь) · NULL = только ручной запуск · имя по смыслу, не meta
model_id BigInteger FK→agent_modelsNULLIDX модель прогона · выбор владельца из разрешённого списка · SET NULL: сняли модель из списка → NULL = агент остановлен, владелец выбирает заново
enabled Boolean NOT NULLDEFAULT выключатель владельца · true = агент рабочий (расписание + ручной); false — полный стоп
admin_paused Boolean NOT NULLDEFAULT липкий замок админа · владелец снять не может (два замка)
next_run_at DateTime(tz) NULLIDX вычисленный следующий слот · NULL = только ручной / выключен владельцем / заперт админом · планировщик сканирует по нему
meta JSONB NULL гибкие атрибуты агента
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger · правка промта, расписания, стопов
Личность агента — это FK на владельца, не отдельная модель
Агент и есть его владелец перед базой: user_id ведёт прямо к ACL создателя, и тем же ребром идёт каскад. Отдельной таблицы личностей, сервисных аккаунтов, выбора «чей доступ» — нет; v2, полная модель личности. Связать дёшево: личность сотрудника уже сведена с учёткой мостом identity.user_id в модели личностей.
Два замка — оба должны быть открыты
enabled — выключатель владельца, admin_paused — замок админа; это разные оси, а не одно поле состояния. Агент стартует — по расписанию или вручную — только если включён владельцем и не заперт админом. Замок админа липкий: владелец правит свой флаг, чужой — нет. Деактивация владельца не заводит третьей оси: она пишет тот же enabled=false, и вернуть его вправе только сам владелец (жизненный цикл).
Модель — ссылка на разрешённый список, без неявного дефолта
model_id ведёт на строку agent_models — список chat-моделей, что admin разрешил агентам. Владелец выбирает из него; при создании поле проставляется (преднабор — дефолт списка). Неявного дефолта в рантайме нет: NULL значит не «взять дефолт», а «модели нет — агент остановлен». Сюда ведут два пути: модель ещё не выбрана или admin снял её из списка — ON DELETE SET NULL обнуляет ссылку, и model_id IS NOT NULL становится четвёртым условием старта. Ссылка на строку списка, не на каталог моделей, нарочно: снятие гасит агента сразу, без сверки «ещё ли модель в списке», и повторное добавление не оживляет его молча — владелец подтверждает выбор.
schedule — размеченное объединение по type, не свободный JSON
Форму поля держит backend (Pydantic v2, дискриминатор type) — источник истины, surface-редактор получает её типизированно. Два варианта. Интервал — { "type": "interval", "every_hours": N }, период «каждые N часов». Календарь — { "type": "calendar", "cadence": "daily" | "weekly", "weekday": 0–6, "time": "HH:MM" }: weekday (0 = понедельник) значим лишь для weekly, time — локальное время владельца. NULL — расписания нет, агент чисто ручной. Кодировку дня и формат времени берём те же, что у платформенного curation_* (weekday 0–6, 'HH:MM') — второй диалект расписаний не заводим.
Журнал прогонов: agent_runs

Строка на каждый прогон — и состоявшийся, и отклонённый. Машина состояний (queued → running → succeeded / failed) плюс выход агента и расход токенов. Журнал — дом результата (выход) и источник недельного бюджета: его tokens_used суммируются в потолок.

2 Прогон Один запуск — исход, выход, расход.
agent_runs журнал прогонов
FK → agents state machine один активный на агента
id BigInteger PK
agent_id BigInteger FK→agentsIDX чей прогон · CASCADE · история по нему
trigger Text NOT NULLCHECK как стартовал · manual · scheduled
state Text NOT NULLCHECK queued · running · succeeded · failed · skipped
reason Text NULLCHECK почему skipped / failed · машинный код, не свободный текст · skipped: budget_exceeded · already_running · failed: iteration_cap · error · stale
output Text NULL финальный ответ агента · весь выход, читается на экране агента
tokens_used BigInteger NOT NULLDEFAULT расход прогона · суммируется в недельный бюджет
error Text NULL деталь сбоя при failed
started_at DateTime(tz) NULL когда вошёл в running · NULL у skipped
finished_at DateTime(tz) NULLIDX когда завершился · окно недельного бюджета считается по нему
heartbeat_at DateTime(tz) NULL живость воркера прогона · протухание отпускает замок зависшего running
created_at DateTime(tz) DEFAULT now() · постановка в очередь
Прогон — машина состояний, но журнал несёт только created_at
Строка живёт переходами: queued на постановке, running на старте, терминал на финале — это in-place UPDATE. Но ход прогона несут доменные вехи started_at / finished_at / heartbeat_at (heartbeat ~30 с — точнее служебного updated_at), поэтому отдельный updated_at избыточен: журнал прогона несёт только created_at. Единый паттерн run-journal с sync_runs (Harvester) и curation_runs / backup_snapshots (Knowledge Store).
Один активный прогон на агента — замок в БД, не только проверка в коде
Замок — partial unique index UNIQUE (agent_id) WHERE state IN ('queued','running'): у агента максимум один незавершённый прогон. Гонку (ручной запуск совпал с наступившим слотом, два воркера схватили строку) гасит сама база — второй INSERT незавершённого прогона упирается в замок; движок ловит отказ и пишет терминальный skipped с reason=already_running (это состояние вне предиката замка, записи не мешает). Умер воркер, не закрыв прогон, — без присмотра строка зависла бы в running навсегда: и агента бы заперла, и токены держала вне бюджета. Её разбирает heartbeat_at: протухший прогон жнётся в failed с reason=stale, замок отпускается, агент снова стартует. Тот же замок и тот же heartbeat несут sync_runs Harvester и curation_runs Knowledge Store — единый паттерн журналов прогонов по платформе. Порог протухания — константа модуля (proximity principle), не magic-число в коде жатвы: воркер бьёт heartbeat_at раз в ~30 с, прогон считается зависшим после ~90 с тишины (три пропуска) — тот же порядок, что у журналов-прогонов соседей.
Бюджет производный — сумма по tokens_used, не счётчик
Отдельной таблицы расхода нет: недельный потолок сверяется SUM(tokens_used) по прогонам всех агентов владельца, где finished_at попал в текущую неделю (календарную, воскресенье 00:00 в зоне организации — бюджет). Окно по finished_at значит, что расход ещё идущего прогона входит в сумму лишь на финале: стартующий следом видит его нулём, и в пределах недели возможен небольшой перебег потолка, сходящий на нет по мере завершения прогонов. Для редких личных прогонов перебег узок и приемлем. Такой объём (личные агенты, редкие прогоны) сумма тянет без агрегата; материализованный счётчик — оптимизация на потом. Лимит при этом здесь не хранится — он платформенный, в platform_settings (граница).
skipped — это запись, а не отсутствие прогона
Строка state=skipped пишется, лишь когда движок взялся стартовать прогон, но рантайм-ворота закрылись: недельный бюджет выбран (budget_exceeded) или прошлый прогон агента ещё идёт (already_running). Оба невидимы из стоячей настройки агента — потому и нужна запись: отказ виден в журнале («лимит превышён»), а не теряется молчанием; tokens_used = 0, цикла не было. Замки же гасят агента раньше ворот: при выключателе владельца или замке админа next_run_at обнулён, планировщик агента не сканирует, кнопка ручного запуска недоступна — durable-стоп строк не плодит, он виден статусом агента, не потоком skipped.
Выбор инструментов: agent_tools

Какие опциональные инструменты владелец доцепил агенту поверх ядра — тонкая связка многие-ко-многим между agents и каталогом (tools). Ядро из трёх KS-инструментов locked и в таблице не лежит — оно у каждого агента по умолчанию; строки здесь только про выбор сверх него.

3 Связка Опциональные инструменты, выбранные владельцем.
agent_tools выбор инструментов агента
FK → agents FK → tools UNIQUE (agent, tool)
id BigInteger PK
agent_id BigInteger FK→agentsIDX чей выбор · CASCADE · уходит вместе с агентом
tool_id BigInteger FK→toolsIDX какой инструмент из каталога · CASCADE
created_at DateTime(tz) DEFAULT now() · связь-выбор append-only, без updated_at
Снятие инструмента — мягкая деградация, не стоп
Снять допуск можно двумя путями, и оба не валят агент. Админ убрал инструмент из allowlist (tools.agents_allowed = false) — строка связки остаётся, но рантайм её не подаёт в набор, фильтруя выбор по текущему флагу на старте прогона. Удалили же сам инструмент из каталога (или агента целиком) — связку чистит ON DELETE CASCADE. В обоих случаях агент продолжает на ядре KS, в гейт старта это не входит — в отличие от снятой модели, что гасит прогон. Запись и MCP-источники инструментов — v2.
Граница: что хранит движок, что у соседей

Agent Engine держит агентов и прогоны — и только. Всё остальное, чего касается агент, принадлежит соседям: учётка и личность — Auth и Knowledge Store, реестр моделей и потолок бюджета — Admin, сами знания — Knowledge Store. Из чужого здесь хранятся две ссылкиuser_id владельца и model_id выбранной модели; потолок бюджета не лежит даже ссылкой — движок читает его из platform_settings на старте каждого прогона.

Agent Engine хранит

agents (определения) и agent_runs (журнал с выходом и расходом). Личность не хранит — берёт ACL по user_id. Модель не хранит — ссылается model_id на разрешённый список. Недельный расход не хранит агрегатом — суммирует из прогонов. Потолок не хранит — читает из platform_settings. Знания не хранит — читает из KS под правами.

Соседи владеют

users и личности — Auth и Knowledge Store; разрешённые модели (agent_models) и потолок токенов (platform_settings) — Admin; тело знаний, граф, векторы — Knowledge Store, откуда агент только читает.