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.
| Piece | Plain version | Role |
|---|---|---|
api | The brain + the control center | Core 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. |
app | The customer's website | Customer cabinet (Nuxt). |
admin | The control panel | Admin panel (Nuxt). |
worker | The hands | An 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.

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.

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 (theruntimeslice group: queue, session lifecycle, k8s, events), and persisting outputs viasystem/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 fromapi(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.
appandadminread it from Nuxt'sruntimeConfig.public.apiBase, fed byNUXT_PUBLIC_API_BASE(defaulthttp://localhost:3333). Nothing else may set it:import.meta.envnever reaches the browser bundle, so a value put there reads asundefinedand a hardcoded fallback wins — silently, and only on the machines where the address differs from the default. apiwill not boot on a placeholder secret. Outsidedevelopment, aJWT_SECRETthat 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
401used 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
/loginwith the reason — a user who is bounced to a sign-in screen with no explanation assumes the product is broken.
Next
- How we build it — how the code inside
apiis organized. - Container images — how each piece is built and shipped.
- Layered slices — the eight slice groups in depth.