Skip to content

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 dispatchstandard jobвиконати tool-зобов'язання в worker Job
Idle timersdelayed jobreaper присипляє/зносить простійну сесію (Destroy)
Scheduled / cronrepeatable jobзапуски agent/cron → той самий шлях orchestrator
Ingestionstandard job (budgeted)індексація документів LightRAG

Двигун: BullMQ на Redis (дає delayed- + repeatable-задачі та pub/sub для events). Відкрита альтернатива — Postgres-only pg-boss + LISTEN/NOTIFY — захована за слайсом runtime/task, щоб двигун був замінним. Див. Рішення по черзі задач.

Режими рантайму → k8s-примітиви

Форма ресурсів береться з RuntimeProfile (каталог пресетів):

РежимТригерk8s-примітивРесурсиIdle
Noneзвичайний чат, без тулівбез пода (лише LLM + пам'ять)n/a
Lightbash, скрипти, файлові операціїJob (restartPolicy: Never, ttlSecondsAfterFinished)~0.5 CPU / 512Mi60–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 (ілюстративно)

yaml
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/workersessions · 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, який живить ця черга.