Deploy
Як worker провіжиниться й стартує. Worker — це руки агента: пісочниця на вимогу (bash · fs · браузер), реалізована на Kubernetes як короткоживучий Job, а не постійний Deployment. Нічого «завжди-онлайн» на кожного агента немає.
Це крок 1 життєвого циклу → далі Use → потім Destroy.
Хто його створює
Застосунок worker не планує себе сам. Менеджер рантайму всередині api (слайс runtime/worker) володіє k8s-клієнтом і життєвим циклом сесії:
orchestrator → tasks (BullMQ) → runtime manager → k8s Job (worker pod)
│ mints a short-lived session token (user/auth)
│ builds the manifest from a RuntimeProfile
▼
worker pod boots → WS to Core → ready → runs → uploads → exits
│ heartbeats reset the idle timer
▼
idle reaper deletes the Job (see Destroy)У чергу кладеться лише «агенту потрібні руки»
Звичайний турн чату без тулів ніколи не чіпає чергу (слайс runtime/task). У чергу кладуться лише worker/tool-задачі:
message → orchestrator (load agent + memory + chat, tool-analysis)
├─ no tools → llm.stream() answers directly → save → done ← NO queue
└─ tools → tasks (queue) → dispatcher → worker Job → stream ← queueЧерга виправдовує себе через backpressure + таймери + надійність — а не заради сирої пропускної здатності:
| Призначення | Що робить | Що ламається без неї |
|---|---|---|
| Backpressure | дозує створення Job під ємність нод | сплеск із N задач → N Pending-подів → тиск на scheduler/etcd |
| Конкурентність / справедливість | per-team-ліміти, глобальні стелі, преміум-пріоритет | один тенант голодоморить усіх |
| Надійність | retry-with-backoff + dead-letter | втрачена задача просто зникає |
| Таймери | delayed-задачі = idle-reaper; repeatable = agent/cron | немає чистого idle-teardown чи запланованих запусків |
| Розчеплення | задача переживає те, яка stateless-репліка api її поклала | rescale HPA роняє роботу в польоті |
Споживач — це dispatcher
Worker'и ефемерні — один k8s Job на задачу — тож споживач черги — це не пул worker'ів. Це dispatcher (менеджер runtime/worker в api): він тягне задачу, застосовує ліміти конкурентності, обирає RuntimeProfile і створює Job. Worker спавниться на кожну задачу й стримить результати назад через events (Redis pub/sub → SSE/WS); сам він ніколи не є споживачем черги.
| Вид Job | Механізм | Для чого |
|---|---|---|
| Task dispatch | standard job | виконати tool-зобов'язання в worker Job |
| Idle timers | delayed job | reaper присипляє/зносить простійну сесію (Destroy) |
| Scheduled / cron | repeatable job | запуски agent/cron → той самий шлях orchestrator |
| Ingestion | standard job (budgeted) | індексація документів LightRAG |
Двигун: BullMQ на Redis (дає delayed- + repeatable-задачі та pub/sub для
events). Відкрита альтернатива — Postgres-onlypg-boss+LISTEN/NOTIFY— захована за слайсомruntime/task, щоб двигун був замінним. Див. Рішення по черзі задач.
Режими рантайму → k8s-примітиви
Форма ресурсів береться з RuntimeProfile (каталог пресетів):
| Режим | Тригер | k8s-примітив | Ресурси | Idle |
|---|---|---|---|---|
| None | звичайний чат, без тулів | без пода (лише LLM + пам'ять) | — | n/a |
| Light | bash, скрипти, файлові операції | Job (restartPolicy: Never, ttlSecondsAfterFinished) | ~0.5 CPU / 512Mi | 60–120с |
| Browser | використано браузер-тул | Job з in-pod headless Chromium (Playwright, per-tenant --user-data-dir) | на задачу | до простою браузера |
| Heavy | скрейпінг, важкі обчислення | Job на нодпулі workers, високі ліміти | 2+ CPU / 2Gi+ | 30–60с |
| Warm | преміум | Light/Heavy Job живе довше за idle | за профілем | 10–30 хв |
Маніфест Job (ілюстративно)
apiVersion: batch/v1
kind: Job
metadata:
name: agent-{sessionId}
namespace: tenant-{teamId} # namespace (or labels) per tenant
labels: { app: agentfy-worker, team: "{teamId}", session: "{sessionId}" }
spec:
backoffLimit: 0 # no retries — a failed obligation fails the task
ttlSecondsAfterFinished: 60 # k8s auto-deletes the finished Job
activeDeadlineSeconds: 1020 # dialSeconds + maxExecSeconds — НЕ лише maxExecSeconds:
# k8s рахує це від старту JOB, тобто включно з холодним
# стартом. Див. /uk/worker/protocol#deadlines
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector: { node-role: workers }
tolerations: [{ key: node-role, value: workers, effect: NoSchedule }]
# `fsGroup` is what makes the 0440 pass file readable by the image's
# `USER 1000:1000`: a Secret volume's files are owned by root.
securityContext: { runAsNonRoot: true, fsGroup: 1000, seccompProfile: { type: RuntimeDefault } }
containers:
- name: worker
image: registry/agentfy-worker:{tag}
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi", ephemeral-storage: "5Gi" }
env:
- { name: SESSION_ID, value: "{sessionId}" }
- { name: CONTROL_URL, value: "wss://core/ws/runtime" }
- { name: TOOL_ALLOWLIST, value: "bash,fs,browser" }
# Where `http` / `web_fetch` may go. Hosts, or `*.host` — never a URL and
# never `*`. ABSENT MEANS EMPTY, and empty reaches nowhere.
- { name: EGRESS_ALLOWLIST, value: "example.com,*.example.com" }
# Де `browser_play` може ВПИСУВАТИ та НАТИСКАТИ (AGNT2-221). ДРУГИЙ
# список, а не прочитання попереднього: дозвіл читати сайт не є
# дозволом на ньому натискати. Лише точні імена хостів — ані `*.`,
# ані `internet:unrestricted`. ВІДСУТНІЙ — ОЗНАЧАЄ ПОРОЖНІЙ, а
# порожній не чіпає нічого.
- { name: BROWSER_PLAY_ALLOWLIST, value: "shop.example.com" }
# The pass arrives as a FILE, never as a value in the environment
# (006 T104). This names the PATH; the Secret behind it is owned by
# this Job, so the cluster reaps it when the Job goes.
- { name: WORKER_TOKEN_FILE, value: /var/run/agentfy/token }
volumeMounts:
- { name: workspace, mountPath: /workspace }
- { name: session-pass, mountPath: /var/run/agentfy, readOnly: true }
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumes:
- name: workspace
emptyDir: { sizeLimit: "5Gi" } # ephemeral; wiped when the pod dies
- name: session-pass
projected:
defaultMode: 0440 # root:1000 — readable by the pod's user
sources:
- secret:
name: worker-{jobName}-pass
items: [{ key: token, path: token }]Boot та реєстрація
Под стартує з SESSION_ID, CONTROL_URL, короткоживучим session-токеном (JWT, scoped на цю одну AgentRuntimeSession, інжектиться як projected/short-TTL Secret — жодних довгоживучих секретів у поді), allowlist'ом тулів і storage-scope. Далі він дзвонить назад до Core через WebSocket і сигналить ready. Повний handshake і протокол повідомлень — у Use; сам контракт дроту — кожен кадр, його відповідь, його таймаут і правило версій — це Канал інструментів.
Масштабування
- Scale-to-zero вбудований: Job існує лише поки йде задача, потім видаляється. KEDA/Knative для MVP не потрібні.
- Конкурентність обмежена нодпулом
workers+ per-tenant ResourceQuota/LimitRange; cluster-autoscaler росте нодпул під навантаженням. - Worker містить Chromium → холодний старт браузера = підняття Job + запуск Chrome. Пре-пуллені образи worker (і пара теплих нод
workers/ Warm-профіль) ховають цю latency.
Чекліст безпеки
- Namespace (або NetworkPolicy + labels) на тенанта/сесію.
- NetworkPolicy egress-allowlist (за замовчуванням deny-all; дозволити Core WS + потрібні домени).
- Ліміти CPU/RAM/storage з профілю; read-only root fs, drop all capabilities, non-root,
automountServiceAccountToken: false. - Лише тимчасовий
emptyDir-workspace; жодних постійних секретів в образі; креди короткоживучі, спливають разом із сесією. activeDeadlineSeconds— жорстка стеля на runaway-рантайми, розміромdialSeconds + maxExecSeconds, бо k8s починає рахувати від Job, а не від процесу (канал інструментів); аудит-лог кожної сесії черезadmin/audit.
Де живе код
- Менеджер (control):
api→ слайсruntime/worker—sessions·k8s(збірка маніфесту + create/delete) ·browser(життєвий цикл in-pod Chromium) ·idle(reaper) ·profiles. - Черга:
api→ слайсruntime/task(BullMQ). - Пісочниця (образ): верхньорівневий застосунок
worker— цикл агента + bash/fs/browser виконавці + WS-клієнт. Тул-виконавців беремо зcleanslice/runtime, патерни k8s/маніфесту — з Ranch. - Кластер: Hetzner k8s, нодпул
workers(tainted). Див. Ресурси.
Дивіться також
- Use — як мозок жене живий worker (сесії, протокол, тули).
- Канал інструментів — контракт дроту: кадри, відповіді, мовчання, версії.
- Destroy — idle-reaper, release, TTL та очистка.
- Implementation — build-prompt для цієї підсистеми.
- Кластер і ноди — backpressure + autoscaler, який живить ця черга.