Scalable Frontends — one shared backend, multiple thin instances¶
The standard. A frontend is a thin client over a shared agent-utilities backend. The heavy parts — the Rust epistemic-graph engine, the embedding model, the consolidated background daemon — live in one backend (or a sharded set), never co-located per frontend instance. This is what lets the operator run multiple frontend instances cheaply (the agent-terminal-ui pattern): one node hosts the engine, dozens of light frontends fan out against it.
The anti-pattern is the opposite: a frontend that imports the engine in-process and forces itself to be the KG host. Then every instance carries a full engine + daemon, RSS balloons (hundreds of MB to >1 GB each), and the operator cannot scale horizontally.
The five-point checklist¶
A frontend is "scale-multiple-instances ready" when:
- Talks to a shared backend over the wire — HTTP to the gateway (
AGENT_URL) and/or the engine socket (GRAPH_SERVICE_ENDPOINTS), never an in-process engine it owns exclusively. - Never forces itself to be the KG host — runs as
KG_DAEMON_ROLE=clientso exactly one shared host runs the consolidated daemon (queue drain, graph writer, workers, scheduler, file-watch); clients reach it over the socket. - Lazy heavy imports — any
agent_utilities.knowledge_graph/ engine import is inside the function that uses it, never at module top, so importing the frontend does not eagerly load the engine. - Slim runtime image — multi-stage build, runtime deps only, no test/dev
extras, non-root, no caches. (See the
agent-terminal-ui/Dockerfiletemplate.) - A lightweight/headless mode where the surface supports it (CLI/server), for multiple concurrent non-interactive instances against one backend.
Shared env knobs (the same on every thin instance)¶
| Variable | Thin-instance value | Effect |
|---|---|---|
KG_DAEMON_ROLE |
client |
Do not run the in-process host daemon; reach the shared host. |
GRAPH_SERVICE_ENDPOINTS |
unix:///run/eg-0.sock,… or tls://engine.example.test:9100 |
Connect-only contacts for the existing shared engine (or shards, AU-KG.sharding.tenant-partitioned-sharding-hrw). |
AGENT_URL / gateway URL |
http://agent-utilities:8000 |
The shared gateway the UI calls over HTTP. |
STATE_DB_URI |
shared Postgres DSN | So client instances share sessions, fleet metadata, and queue delivery; WorkItem checkpoints remain engine-native (AU-OS.state.unified-durable-state-externalization–18). |
One node runs the host (KG_DAEMON_ROLE=host or unset + the engine), everything
else runs client. Pair with the Enterprise Enablement Runbook
when turning the shared backend's scale-out flags on.
Per-surface posture¶
agent-terminal-ui — the reference (fully decoupled)¶
The gold standard: no agent_utilities dependency at all (deps are
textual/httpx/rich/agent-client-protocol/pyyaml). It speaks only HTTP to AGENT_URL;
a dependency-free vendored GoalSpec parser (agent_terminal_ui/goal.py) exists
specifically to avoid importing the backend. A --headless mode (headless.py,
~30 MB, no Textual widget tree) runs multiple non-interactive instances per node, and a
runtime-only Dockerfile ships the frontend without the backend. ~30–50 MB/instance.
agent-webui — thin via client role¶
The React SPA scales infinitely in the browser; the Python API server is what the operator
scale horizontally. It needs agent-utilities[agent-runtime,graph] for model runtime and
canonical gateway helpers; the full engine is already a hard base dependency. It is not
as small as terminal-ui, but it is thin when
run as KG_DAEMON_ROLE=client: the server skips the in-process host daemon
(server.py _start_kg_host_daemon) and reaches a shared host over the engine socket.
Run one host instance (or a sharded backend) and multiple client-role API instances
behind a load balancer. The heavy KG imports in api_extensions.py are used only inside
their handlers (no module-level engine instantiation), so importing the server is light.
geniusbot — desktop cockpit (not horizontally scaled)¶
A PySide6 desktop app is one-per-user-desktop, not an instance the operator fan out in
containers (it needs a display). Its scaling concern is therefore startup weight, and
it already follows the standard: the BackendAdapter
(geniusbot/services/backend_adapter.py) is gateway-first — it routes through the
HTTP GatewayClient and only lazy-imports the agent_utilities engine inside the one
method that needs a local fallback, so the UI never eagerly loads the backend. The
gateway_client SDK it consumes (AU-ECO.interop.gateway-client-sdk) imports only httpx + agent_utilities.httpsupport,
not the engine.
Why webui can't be as small as terminal-ui (and that's fine)¶
terminal-ui is a pure protocol client — it never needs backend code, so it drops the
agent_utilities dependency entirely. webui mounts the canonical gateway routes and
uses the engine client, so it must ship agent_utilities. The win there is not a
tiny image; it is runtime memory + host topology: a client-role webui does not load
the embedding model, does not host the engine, and does not run the daemon — so N of
them share one host instead of standing up N engines. Same principle (one shared
backend, multiple thin frontends), different floor.