Agent Engine хранит определения агентов и
журнал прогонов, плюс тонкую связку
agent_tools — выбор
опциональных инструментов. Личность агента не
отдельная сущность, а FK на владельца (agents); недельный бюджет не таблица-счётчик, а
сумма по agent_runs. Знания живут в Knowledge Store, потолок недельного
бюджета — в platform_settings; из чужого здесь хранятся две ссылки —
user_id владельца и model_id выбранной модели,
всё прочее
читается у соседей на лету.
agentsЧто сотрудник завёл: имя, промт (его слой композиции), расписание и два замка — выключатель владельца и липкий замок админа. FK на владельца несёт двойную службу: и каскад удаления, и личность, под которой агент читает базу.
| 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 · правка промта, расписания, стопов |
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
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 суммируются
в потолок.
| 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() · постановка в очередь |
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).
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 и в таблице не лежит — оно у каждого агента по умолчанию; строки здесь только про выбор сверх него.
| 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 |
tools.agents_allowed =
false) — строка связки остаётся, но рантайм её
не подаёт в набор, фильтруя выбор по текущему
флагу на старте прогона. Удалили же сам инструмент из каталога
(или агента целиком) — связку чистит ON DELETE
CASCADE. В обоих случаях агент
продолжает на ядре KS, в
гейт старта
это не входит — в отличие от снятой модели, что гасит прогон.
Запись и MCP-источники инструментов — v2.
Agent Engine держит агентов и прогоны — и только. Всё
остальное, чего касается агент, принадлежит соседям: учётка и личность —
Auth и Knowledge Store, реестр моделей и потолок бюджета — Admin, сами
знания — Knowledge Store. Из чужого здесь хранятся
две ссылки — user_id владельца и
model_id выбранной модели; потолок бюджета не лежит даже
ссылкой — движок читает его из platform_settings на старте каждого
прогона.
agents (определения) и agent_runs (журнал
с выходом и расходом). Личность не хранит — берёт ACL по
user_id. Модель не хранит — ссылается
model_id на разрешённый список. Недельный расход не
хранит агрегатом — суммирует из прогонов. Потолок не хранит — читает
из platform_settings. Знания не хранит — читает из KS под правами.
users и личности — Auth и
Knowledge Store; разрешённые модели
(agent_models) и потолок токенов
(platform_settings) — Admin; тело знаний, граф, векторы
— Knowledge Store, откуда агент только читает.