Skip to content

Agent — состав и запуск

Агент — это конфиг + состояние, а не контейнер

Самое главное: агент — это строка в Postgres, а не запущенный процесс. Он «всегда доступен», потому что это данные — определение + память + история. У него нет своего пода. Когда ему пишут, эфемерный мозг (образ api, запускаемый на один турн) «надевает» эти данные, отрабатывает один турн и исчезает.

Agent (строка в БД)  ──грузится на турн──▶  эфемерный мозг (api)  ──руки при нужде──▶  worker
   конфиг + состояние                         orchestrator + LLM-loop                  (k8s Job)

Из чего состоит агент

Группа слайсов agent (слой L5) — это агент и всё, чем он владеет или пользуется:

часть (слайс)что вносит в агента
agentопределение: soul, user, heartbeat, config, runtimeProfile, type, status
memoryсобственная долгосрочная память агента (вектора) — что он помнит между чатами
knowledgeкурируемый RAG-корпус (LightRAG), по которому он ищет — загруженные доки, по базам
chatистория диалога (треды, сообщения) — текущий контекст
appвнешние коннекторы (OAuth / куки), которые агент использует как тулы (Google, X, …)
channelповерхности, на которых он говорит (telegram, slack, веб-виджет)
orchestratorмозг, который на каждый турн собирает всё выше и гоняет цикл

memoryknowledge: память — это собственная эволюционирующая память агента; знания — это курируемый корпус, который заливает пользователь. Оба кормят турн, но это разные источники.

Сущность Agent

prisma
model Agent {
  id               String   @id      // agent-{uuid}; у консьержа выводится из id команды
  teamId           String            // тенант
  externalId       String?           // id происхождения из пакета — ключ повторного импорта
  ownerId          String            // участник-владелец
  name             String
  description      String?
  status           String   @default("active")     // active | disabled | archived
  type             String   @default("standard")   // standard | concierge — назначает только сервер
  soul             String?           // SOUL.md      — персона, дословно
  user             String?           // USER.md      — контекст о пользователе, дословно
  heartbeat        String?           // HEARTBEAT.md — текст автозапуска из пакета, дословно
  config           Json     @default("{}")  // agent.config.json рантайма, дословно
  runtimeProfile   String   @default("none")   // можно ли этому агенту руки и сколько машины
  promptLogEnabled Boolean  @default(false)  // записывать собранные промпты этого агента
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
}

Создать агента = вставить эту строку. Он сразу пригоден — без провижининга, без пода.

Две колонки стоит прочитать дважды, потому что у обеих намеренно узкое значение по умолчанию, и ни один релиз не должен расширять его существующему агенту: runtimeProfile это none (рук нет), а promptLogEnabledfalse (ничего не записывается). См. Руки и Что агент делал.

Три дословных документа — это собственный текст агента, а не его отображение. soul, user и heartbeat — те самые файлы, которые несёт пакет; они уезжают в экспорт байт в байт и так же возвращаются.

Удаление строки теперь уносит всё за собой. Переписка, заметки, навыки, секреты и конфигурация автозапуска уходят каскадом одним оператором, поэтому удаление, оборванное посередине, не удаляет ничего, а не оставляет полупустую оболочку. Единственное, чего каскад не достаёт, — векторы агента, у них по замыслу нет внешнего ключа, и их убирают явно.

Чем агент является и что разрешает его тип

У каждого агента есть тип. Тип — а не россыпь булевых флагов — отвечает на вопрос «что это такое?», и всё остальное следует из него. Сегодня их два:

типчто это
standardвсё, что создаёт владелец; значение по умолчанию для любой строки
conciergeсистемный агент команды. Персона приходит из кода; его нельзя переименовать, выключить, заархивировать, удалить и экспортировать, у него нет секретов, навыков и автозапуска

Тип никогда не приходит снаружи. Его назначает только сервер: его нет ни в теле создания, ни в теле обновления, ни в манифесте пакета — иначе загруженный пакет объявил бы себя консьержем команды и унаследовал всё, что типу разрешено, начиная с «нельзя удалить».

Одна декларация, два потребителя. Что разрешает тип, описано одной таблицей (AGENT_CAPABILITIES): редактируется ли душа, откуда берётся системный промпт, можно ли переименовать / выключить / заархивировать / удалить, есть ли секреты / автозапуск / навыки, можно ли экспортировать пакет и перезаписать агента импортом, какой у него тулсет. Эту таблицу читает каждый путь записи — обновление, смена статуса, удаление, импортёр и экспортёр пакета и тул консьержа create_agent. Интерфейс читает то же самое значение: оно едет в каждом ответе про агента полем capabilities, так что второй копии, которой можно разъехаться, у приложения нет.

Незнакомое значение типа из БД (строка, записанная более новым релизом) читается как standard — это наименьшие права, а не наибольшие.

Архивация — консьержу, удаление — человеку

Консьерж умеет отправить агента в архив разговором и вернуть его оттуда. Удалить он не может — никаким инструментом, ни под каким флагом, и никакой неиспользуемый метод не ждёт своего часа.

Граница проведена по обратимости, а не по опасности вообще: архивация это смена статуса, и её разворачивает одно движение, а удаление уносит переписку, заметки, навыки и секреты агента, и не возвращает их ничто. Ошибка модели в первом случае стоит одного клика, во втором — всего, что в этого агента вложили.

Чтобы консьерж действовал, должны выполняться сразу два условия — агента вообще можно архивировать и консьержу это позволено, — и оба читаются из одной и той же декларации возможностей, а не проверяются заново где-то в теле инструмента. Именно это не даёт двум копиям одного правила разъехаться.

Удаление живёт в кабинете, во вкладке General самого агента, и подтверждение называет агента по имени, а не спрашивает «вы уверены?»: человек должен прочитать, что именно исчезнет.

Как турн «запускает» агента

Шага «запустить агента» нет. Агент просыпается на турн:

1. сообщение приходит в канал → ingress резолвит (agentId, sessionId), проверка доступа
2. orchestrator (api, эфемерный) СОБИРАЕТ агента:
      • грузит определение Agent (soul, config, runtimeProfile, type)
      • поднимает контекст памяти (memory) + читает историю чата (chat)
      • резолвит прикреплённые базы знаний (knowledge) + креды коннекторов (app, secrets)
      • enforcement-проверка (setting) — можно ли команде тратить?
3. собирает промпт → гоняет LLM-loop (system/llm), стримит токены наружу
4. tool-анализ:
      • без тулов → стримит ответ напрямую → сохраняет → ГОТОВО  (без пода)
      • с тулами  → tasks (BullMQ) → worker (k8s Job) делает bash/fs/browser → результаты назад
5. сохраняет: сообщение ассистента (chat), апдейты памяти, usage-события
6. эфемерный мозг ВЫХОДИТ — агент снова просто строка в БД

Следующее сообщение повторяет это; если рантайм-сессия ещё тёплая, worker переиспользуется (без холодного старта).

Тулы и обязательства

Инструменты агента делятся по месту исполнения:

  • В мозге (api): память, поиск по переписке, секреты, текущее время, запрос к знаниям. Под не нужен.
  • В worker: всё, чему нужна среда — оболочка, файлы, браузер или произвольный выход в сеть. Последнее — не удобство: запрос, сделанный мозгом, был бы нашим же сервером, идущим туда, куда ему сказали, а мозг держит базу и ключ шифрования.

Модель видит один слитый список, инструменты воркера в нём под собственными именами, а маршрутизацию делает хост. Ни префикса, ни второго списка — но инструмент воркера, имя которого совпало с инструментом мозга, становится молча недостижимым: коллизию выигрывает базовый набор. Это стоит знать заранее, а не обнаружить.

Руки — профиль исполнения и грант инструментов

Два разных вопроса, и оба отвечают «нет», пока кто-нибудь не скажет иначе.

Разрешены ли этому агенту руки вообще? runtimeProfile, колонка у агента — none · light · browser · heavy · warm, по умолчанию none. Агент с none не трогает ни очередь, ни кластер, и ни один релиз не выдаёт руки тому, у кого их не было: миграция, добавившая колонку, не заполнила ничего. У консьержа та же колонка на его собственной строке — одна на команду, — поэтому включить руки ему стоит команде одного воркера, а не одного на каждого агента.

Какие из инструментов воркера могут достаться его сессиям? Грант, в config.tools агента. Это подмножество тех десяти имён, которые разделение инструментов отдаёт рукам, и по умолчанию он пуст — то есть воркер без инструментов, а не воркер со всеми. Имя вне этих десяти отвергается, а не игнорируется. Имя, которое не умеет образ, — другой случай, и его не отвергают: грант с browser в light-сессии просто не даёт инструмента браузера, а прицепляется тот список, который воркер сообщил в ответ на готовность.

Три следствия, которые надо знать до того, как идти искать поломку:

  • Обычный разговор не поднимает ничего. В начале хода не провижинится ничто; запрос уходит в очередь только тогда, когда модель действительно тянется к инструменту воркера.
  • Одно поручение просит один раз. Сколько бы инструментов модель ни вызвала за ход, воркер запрашивается один, а живая сессия из прошлого хода переиспользуется, а не заменяется.
  • Ход, поднявший воркер, не успевает им воспользоваться. Набор инструментов хода собирается до первого обращения к модели, а под отвечает на tools/list секундами позже — поэтому новые инструменты приезжают в следующий ход. Это форма конвейера, а не дефект.

Всё, что вернулось из инструмента воркера, — недоверенный ввод, и он попадает в тот же контекст: текст загруженной страницы, вывод команды. Важнее всего это для консьержа, потому что именно он умеет ещё и создавать и архивировать агентов.

Промпт собирается слоями

Промпт, под которым работает агент, — это не его душа. Он собирается заново на каждом ходу из трёх слоёв:

[ PLATFORM_BASE ]         что такое агент Agentfy + правила дома   (код, для всех агентов)
[ TOOL_USE_GUIDANCE ]     как вообще вызывать инструменты — без схем конкретных тулов
[ soul || DEFAULT_SOUL ]  персона ЭТОГО агента, дословно

База живёт в коде, поэтому продуктовое правило действует для каждого агента, и его не надо перепечатывать в каждую душу; душа остаётся ровно той персоной, которую написал её автор.

Всё, что меняется от хода к ходу, сюда не попадает. Локальное время пользователя добавляет преамбула хода в слое инструментов, а контракт NO_REPLY у тика автозапуска — само сообщение тика. Факт одного прогона, записанный в общий промпт, протекает во все остальные ходы.

Клиент присылает вместе с ходом свою IANA-таймзону, и преамбула сообщает модели локальное время пользователя. Ход, у которого пригодной таймзоны нет, — тик автозапуска, вызов из API, который её не прислал, — просто работает в UTC.

Автозапуск

Агент может выполнить ход, когда с ним никто не разговаривает. Тик — это полноценный ход (тулы, память, расход), а не дешёвый пинг, и это единственный способ для агента тратить деньги без пользователя за экраном.

  • Интервал 10..1440 минут, по умолчанию 60. Включение автозапуска требует непустого промпта. Пол существует потому, что тик — это полноценный ход модели, а пока нет учёта потребления, интервал и есть единственное, что ограничивает расход. Он объявлен один раз и проверяется на пути записи, а не только на HTTP-границе, поэтому никакой другой писатель под него не пролезет; уже сохранённые строки ниже пола поднимаются, каждая со строкой в логе. Понизить пол можно переменной окружения, предназначенной только для разработки: проверка ценой в десять минут ожидания перестаёт выполняться и начинает додумываться.
  • Один глобальный обходчик, а не таймер на агента. Единственный повторяемый джоб на Redis просыпается раз в 60 с на весь парк api и выбирает те heartbeat'ы, которым пора, — поэтому тик срабатывает ровно один раз при любом числе инстансов, и ничто нельзя запланировать точнее периода обхода.
  • Тик никогда не прерывает ход. Занятый агент пропускается и ловится следующим обходом; за раз у агента идёт один ход (см. Один ход на агента).
  • lastRunAt — это запланированный слот, а не часы обходчика, и heartbeat считается созревшим в пределах половины периода обхода до слота. Только вместе эти два правила не дают сетке дрейфовать и не дают двойного срабатывания.
  • Молчание не пишет ничего. Тик, весь ответ которого — NO_REPLY, не сохраняет вообще ничего. Синтетическое сообщение-триггер тоже не пишется: действующий тик сохраняет только ответ ассистента и его шаги-тулы с пометкой origin=heartbeat.
  • Agent.heartbeat — это не движок. Это дословный HEARTBEAT.md, привезённый импортированным пакетом. Агент может нести этот текст с выключенным автозапуском и иметь включённый автозапуск без всякого текста — индикатор, построенный на поле, врал бы в обе стороны. Состояние движка едет отдельно, полем autostart на агенте.
  • Заархивированного или выключенного агента не будят никогда. Обходчик задаёт тот же вопрос, который мгновением позже задаст мозг: активен ли агент? Тик, выпущенный к любому другому статусу, забирает блокировку хода, двигает часы и получает отказ — и так вечно, не производя ничего. Возврат агента из архива снова делает его видимым для обходчика, и расписание при этом не тронуто.
  • У concierge автозапуска нет вовсе: его тип этого не даёт.

Что агент может менять в своём автозапуске

Агент правит свой автозапуск изнутри хода. Граница проведена не по тому, какое поле названо, а по тому, кто начал ход:

В ХОДЕ, КОТОРЫЙ ОТКРЫЛ ЧЕЛОВЕК   текст  +  интервал  +  включённость
В СОБСТВЕННОМ ТИКЕ АГЕНТА        только текст
НИКТО И НИКОГДА                  ниже пола, выше потолка

Опасна была не сама правка, а петля. Агент, который может перенастроить себе частоту на каждом пробуждении, решает, как часто ему решать, и человека нет ни в одной точке этого. Когда просьба приходит в разговоре, человек присутствует: он читает ответ и видит новое значение на экране Heartbeat в ту же секунду — это и есть подтверждение, без очереди, которую кому-то надо разбирать. Ход без явной пометки читается как тик, а не как человек.

Всё, что пишет агент, идёт через тот же сервис, что и запись человека, поэтому пол в 10 минут действует и здесь, и второго пути записи не существует. Выключить себя можно, но агент обязан сказать об этом вслух в ответе: агент, ушедший в тишину молча, — это состояние, которого никто не заказывал, и снаружи оно видно только снятой галочкой на экране.

Проснуться чаще на время

Агент, которому поручили дело, требующее присмотра, может поднять себе частоту на ограниченный срок и вернуться к обычной, чтобы никому не пришлось об этом помнить.

  • Ускоренная частота хранится рядом с обычной, а не поверх неё, вместе с моментом, когда она перестаёт действовать. Что в силе сейчас, вычисляется по часам на каждом чтении.
  • Поэтому возврату не нужен исполнитель: ничего не планируется и ничего не запоминается, и он переживает перезапуск api по той же причине — процесс, который не работает, не может не сделать того, чего никому делать не надо.
  • Не больше 60 минут на одно ускорение и не больше 3 ускорений за 24 часа. Ускорение — это «пока я с этим разбираюсь», а не новое умолчание; постоянная смена частоты — обычная правка выше.
  • Пол всё равно действует. Ускорение тоже не может уйти ниже 10 минут. «Напомни через минуту» — ровно то, что пол запрещает, и инструмент в обход означал бы отмену решения, а не его исполнение.

Инструкции, которые выполняются один раз

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

Два «жизненных цикла» — не путать

  • Жизненный цикл агента = состояние конфига (status: activedisabledarchived). Постоянный, в БД. Это «существует ли агент / включён ли он».
  • Жизненный цикл рантайм-сессии = исполнение (AgentRuntimeSession: pending → running → idle → stopped). Эфемерный, живёт только пока идёт задача. См. Worker.

Агент может быть active месяцами, не имея рантайм-сессий бóльшую часть времени — в этом весь смысл.

Каналы — как сообщения доходят до агента

channel привязывает агента к поверхности (telegram / slack / веб-виджет). Входящие сообщения попадают в ingress, который резолвит целевого агента + сессию и отдаёт турн orchestrator. Один и тот же агент (одна строка в БД) может быть привязан к нескольким каналам сразу.

Где живёт код

  • Определение и домен: api → группа agent (agent · memory · knowledge · chat · app · channel · orchestrator).
  • Мозг: agent/orchestrator крутится внутри образа api, эфемерно, на каждый турн.
  • Руки: верхнеуровневое приложение worker для тяжёлых тулов.
  • Движок знаний: отдельный сервис LightRAG.