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.