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 | мозг, который на каждый турн собирает всё выше и гоняет цикл |
memory≠knowledge: память — это собственная эволюционирующая память агента; знания — это курируемый корпус, который заливает пользователь. Оба кормят турн, но это разные источники.
Сущность Agent
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 (рук нет), а promptLogEnabled — false (ничего не записывается). См. Руки и Что агент делал.
Три дословных документа — это собственный текст агента, а не его отображение. 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:active→disabled→archived). Постоянный, в БД. Это «существует ли агент / включён ли он». - Жизненный цикл рантайм-сессии = исполнение (
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.