Skip to content

The pieces

Agentfy.ai 2.0 ships as four things you can deploy. That's the whole system — there is no runner and no separate runtime app, because the ephemeral agent is the api.

PiecePlain versionRole
apiThe brain + the control centerCore and the ephemeral agent brain — one codebase/image. Holds all 8 slice layers, including the brain (agent/orchestrator) and the worker-management plane (runtime slice group). "Runtime" = the api image run ephemerally.
appThe customer's websiteCustomer cabinet (Nuxt).
adminThe control panelAdmin panel (Nuxt).
workerThe handsAn on-demand worker that runs the agent's obligations (bash, files, browser, heavy/long tasks) the brain can't do itself. Stateful per task; auto-stops on idle.

Three of these (api, app, admin) are normal always-on web services. The worker is the odd one out — it appears only when there's a task, then exits.

The cabinet on a fresh account: the built-in Concierge agent, ready to set the first agent up

A fresh account has exactly one agent in it — the built-in Concierge, which the product creates rather than the person. It is the one agent with no Soul, Heartbeat, Secrets, Skills, Website, MCPs or Connections of its own, because its job is to set the others up.

The agent list of a team in the cabinet

Why there's no separate "runner"

The "ephemeral runtime" everyone pictures is just the api image, run on demand. Running the same codebase per turn gives us the brain — so we don't maintain a second runner image with its own agent loop. The worker is the only genuinely separate executable, because it needs a different, heavy image: a real OS with bash + Chromium.

The api ↔ worker boundary

This is the line that matters most:

  • api = brain + manager. The LLM loop, orchestration, and direct (no-tool) answers, plus the plane that manages workers (the runtime slice group: queue, session lifecycle, k8s, events), and persisting outputs via system/file.
  • worker = hands. A container with bash / files / Chromium (Playwright). Stateful per task (a live browser session, a working directory). It holds no long-lived secrets — it gets a short-lived session token from api (user/auth) that expires with the session.

One name for one thing

The hands once had a second name, and half the code used it. They do not any more: it is worker everywhere — the directory, the image, the pass, these pages, the slices, and the objects you see in kubectl. Two names for one concept is how a reader ends up unsure whether they are looking at one thing or two.

Two slices share the word, and that is deliberate rather than unresolved: runtime/worker is the half that raises and tears down workers, and agent/worker is the half that decides a turn should have hands. Nothing on the wire ever carried the retired name, so none of this changed the protocol.

The retired name came back once, three weeks later, in code written by people who had never read this box — so it is the gate's business now rather than a convention. make naming refuses it anywhere in the tree, in a filename as readily as in a sentence, and scripts/naming-check.mjs is the register of retired names and the one place a retired spelling is still written out.

Configuration that refuses rather than drifts

  • The API address has exactly one source. app and admin read it from Nuxt's runtimeConfig.public.apiBase, fed by NUXT_PUBLIC_API_BASE (default http://localhost:3333). Nothing else may set it: import.meta.env never reaches the browser bundle, so a value put there reads as undefined and a hardcoded fallback wins — silently, and only on the machines where the address differs from the default.
  • api will not boot on a placeholder secret. Outside development, a JWT_SECRET that is missing, still the committed placeholder, or shorter than 32 characters stops startup and names what is wrong. A placeholder that boots is a placeholder that ships — and anyone who can read the repository could then mint a token for any user.

One transport in the browser, and what rides on it

app and admin talk to the api through one client, built on the platform fetch. There is no second path: the generated SDK, the streams and the package upload/download all go the same way, so session renewal and error handling apply to every request rather than to half of them. When they were two transports, the refresh loop was fixed on the intercepted half and did not exist on the streams at all.

Three behaviours ride on that single client, and each is there because its absence cost something measurable:

  • One retry per request, never a chain. An unrecoverable 401 used to produce hundreds of request pairs until the rate limiter noticed.
  • One renewal per burst. A handful of parallel 401s renews the session exactly once; the latch is the session itself, not a flag in a module.
  • The retry carries the fresh token, and a terminal failure clears the session and leaves for /login with the reason — a user who is bounced to a sign-in screen with no explanation assumes the product is broken.

Next