You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(tui): install, run and uninstall agents from the TUI (#2484)
The daemon could install agents but the interface had no way to ask: the
hub listed email as Available with no key that did anything, and `gaia
tui` had only `chat` and `hub`. The hub can now install and uninstall,
and the CLI gained `list`, `install`, `uninstall`, `run` and `status`
with a genuine non-interactive one-shot.
Installing a non-verified agent runs third-party code, so the daemon's
403 opens a gate that **names what is being trusted** — agent, version,
publisher, security tier, download size, scopes — and only retries after
an explicit keypress. Trust is never assumed or auto-retried; a test
asserts exactly one install call with `trusted=false` first.
Also fixes four things found by driving the live TUI: switching tabs
could leave nothing selected (#2481), the hub rendered 34 lines into an
80x24 terminal, `backspace` triggered uninstall alongside `d`, and a
fresh machine opened on an empty Installed tab.
### Test plan
- [ ] `cd tui && make lint && go test ./... -count=1 -race`
- [ ] `gaia tui install email` → refused, exit 1, names `--trust` and
what it is trusting; `--trust` then installs
- [ ] `tab`, `down`×2, `shift+tab` → an agent is still selected
- [ ] Resize to 80x24 → the hub fits, footer does not wrap
@@ -2371,85 +2371,133 @@ under [Agent Command](#agent-command).
2371
2371
2372
2372
---
2373
2373
2374
-
### TUI Control API
2374
+
### Terminal hub (`gaia tui`)
2375
2375
2376
-
The terminal hub (`gaia tui`, the Go/Bubble Tea binary) can expose a loopback
2377
-
HTTP API that drives **the session you are looking at** — not a headless copy.
2378
-
An assistant navigates, types, and launches agents while the frames render in
2379
-
your terminal in real time. It is how the TUI gets tested without a human at
2380
-
the keyboard.
2376
+
`gaia tui` is the Go/Bubble Tea terminal hub: browse agents, install them, and
2377
+
chat with them without leaving the terminal. Building from source the binary is
2378
+
`tui/bin/gaia` (`cd tui && make build`).
2381
2379
2382
-
Off by default. Bound to `127.0.0.1` only, and every request needs a bearer
2383
-
token.
2380
+
<Note>
2381
+
The binary accepts a leading `tui` word and drops it, so `gaia tui install
2382
+
email` and `gaia install email` are the same command. Running the source build
2383
+
directly, both `./tui/bin/gaia tui list` and `./tui/bin/gaia list` work.
2384
+
</Note>
2384
2385
2385
2386
```bash
2386
-
gaia tui --control # auto-assign a port
2387
-
gaia tui --control-port 8770 # explicit port (implies --control)
2388
-
gaia tui --control --debug # log every injected key, state change, and wait
2387
+
gaia tui # open the hub
2388
+
gaia tui list # what the Agent Hub offers + what you have
2389
+
gaia tui list --installed # local only, no network call
2390
+
gaia tui install email --trust # download and install
2391
+
gaia tui uninstall email # stop the agent and remove it
2392
+
gaia tui run email # interactive chat
2393
+
gaia tui run email --query "triage my inbox"# one-shot: answer on stdout, exit 0/1
2394
+
gaia tui status # background service + what is installed
2389
2395
```
2390
2396
2397
+
| Command | Description |
2398
+
|---------|-------------|
2399
+
|`gaia tui`| Open the Agent Hub. |
2400
+
|`gaia tui list [--installed]`| List agents with version, install state, download size, and security tier. `--installed` reads the local `~/.gaia/agents/*/.installed` sentinels and never touches the network. |
2401
+
|`gaia tui install <id> [--version X] [--trust]`| Install an agent and wait for it to finish. |
2402
+
|`gaia tui uninstall <id>`| Stop the agent's sidecar, verify it is gone, remove its install directory. |
2403
+
|`gaia tui run <id> [--query "…"] [--model M] [--timeout D]`| Run an agent. `--query` is a genuine one-shot, checked and bounded (`--timeout`, default 15m). |
2404
+
|`gaia tui status`| Whether the background service is running, which sidecars it knows, and what is installed. |
2405
+
|`gaia tui chat --agent <id>`\|`--subprocess <cmd>`| Chat with an agent by catalog id, or with a binary you spawn directly. With `--agent --query` it is the same one-shot `gaia tui run --query` is, `--timeout` included. |
2406
+
|`gaia tui hub`| The hub screen explicitly (same as bare `gaia tui`). |
2407
+
2408
+
Install and uninstall are served by the GAIA daemon
2409
+
(`POST /daemon/v1/agents/{id}/install`, `DELETE /daemon/v1/agents/{id}`), so the
2410
+
TUI, `gaia hub`, and the Agent UI share one integrity check and one install
2411
+
lock. The daemon is started automatically for these commands; `gaia tui status`
2412
+
only ever attaches, so it reports what *is* running rather than starting
2413
+
something.
2414
+
2415
+
#### `--trust`: installing a non-verified agent
2416
+
2417
+
An agent outside the `verified` security tier runs third-party code on your
2418
+
machine. The daemon refuses to install one with **HTTP 403** until the caller
2419
+
passes an explicit opt-in — there is no bypass, and nothing retries on your
2420
+
behalf.
2421
+
2422
+
From the CLI, that opt-in is `--trust`. Without it the command prints exactly
2423
+
what you would be agreeing to and exits non-zero:
2424
+
2425
+
```console
2426
+
$ gaia tui install email
2427
+
2428
+
Refusing to install "email" without --trust.
2429
+
2430
+
Agent email
2431
+
Version 0.5.0
2432
+
Publisher AMD
2433
+
Security experimental (not verified by AMD)
2434
+
Download 31.1 MB
2435
+
Access none declared
2436
+
2437
+
If you trust the publisher, re-run:
2438
+
gaia tui install email --trust
2439
+
```
2440
+
2441
+
In the hub screen the same refusal opens a **Trust & Install** confirmation
2442
+
showing the same facts. It defaults to *Cancel*, and only an explicit yes
2443
+
re-sends the request with the opt-in.
2444
+
2445
+
#### `run --query` is a real one-shot
2446
+
2447
+
`--query` does not open the alt screen. The answer goes to stdout, progress to
2448
+
stderr, and the exit code is `0` on a terminal answer / `1` on an error — so
2449
+
`gaia tui run email --query "…" > answer.txt` captures exactly the answer:
2450
+
2451
+
```bash
2452
+
gaia tui run email --query "how many unread?"> answer.txt 2> progress.log
2453
+
echo$?# 0
2454
+
```
2455
+
2456
+
It never waits on something that is not there. Before the query is sent, a
2457
+
one-shot over the daemon transport (the agents the background service supervises)
2458
+
runs the same readiness check the hub's launch gate runs — background service,
2459
+
sidecar, local model server, model, and for email the mailbox — and if one of
2460
+
them is not ready it prints that row and its remedy to stderr and exits non-zero.
2461
+
Against an unreachable dependency that is about a second; the ceiling is the time
2462
+
it takes to start the sidecar. Nothing is sent to the agent, and the gate screen
2463
+
is never rendered: a script has nobody to press a key. An agent that runs as a
2464
+
local subprocess has no readiness route to probe, so it is launched directly.
2465
+
2466
+
```bash
2467
+
$ gaia tui run email --query "triage my inbox"# with the model server down
2468
+
❌ Email is not ready — Local AI: not running at http://localhost:8000/api/v1
2469
+
GAIA needs a local model server. It runs on your machine; no message text ever leaves it.
2470
+
…
2471
+
run: lemonade-server serve
2472
+
$ echo$?# 1, after about a second
2473
+
```
2474
+
2475
+
The turn itself is bounded — 15 minutes by default, `--timeout 90s` / `--timeout
2476
+
2h` to change it — so an agent that accepts the query and then stops answering is
2477
+
reported with what to read next instead of hanging a CI job until the job's own
2478
+
timeout kills it. There is no "wait forever": `--timeout 0` is refused. Interactive
2479
+
`gaia tui run <id>` is unaffected — a person can read what arrives and press
2480
+
ctrl+c.
2481
+
2482
+
#### Hub keys
2483
+
2484
+
| Key | Action |
2485
+
|-----|--------|
2486
+
|`enter`| Run the selected agent |
2487
+
|`i`| Install it (or update it, when a newer version exists) |
2488
+
|`d`| Uninstall it (asks first) |
2489
+
|`r`| Refresh the agent list from the Agent Hub |
2490
+
|`/`| Search |
2491
+
|`tab` / `shift+tab`| Next / previous category |
2492
+
|`v`| Vote for a coming-soon agent |
2493
+
|`?`| Help |
2494
+
|`q`, `ctrl+c`| Quit |
2495
+
2391
2496
<Note>
2392
-
These are persistent flags on the TUI binary's root command, so they apply to
2393
-
the hub, `hub`, and `chat` alike. Building from source, that binary is
2394
-
`tui/bin/gaia` (`cd tui && make build`).
2497
+
`backspace` is deliberately unbound. It used to share the uninstall binding
2498
+
with `d` and `delete`, which made "go back" a destructive action.
2395
2499
</Note>
2396
2500
2397
-
| Flag | Type | Default | Description |
2398
-
|------|------|---------|-------------|
2399
-
|`--control`| flag | off | Start the control API on an auto-assigned loopback port. |
2400
-
|`--control-port`| integer |`0`| Bind a specific port. Implies `--control`. `0` auto-assigns. Port `4001` is reserved and rejected. |
2401
-
|`--debug`| flag | off | Log every injected key, state transition, and wait resolution/timeout to stderr. |
2402
-
2403
-
On start the TUI writes `~/.gaia/tui/control.json` (mode `0600`) holding the
2404
-
pid, port, and token, and removes it on exit. Clients find the TUI by reading
2405
-
that file — the token is never printed to the terminal. A client must verify
2406
-
**both** that the recorded pid is alive and that `/control/v1/status` answers
2407
-
with a matching pid before trusting the port: after a crash the file can point
2408
-
at a port some unrelated process now owns.
2409
-
2410
-
<Warning>
2411
-
Anything that can read `~/.gaia/tui/control.json` can drive your TUI — the
2412
-
same trust boundary as `~/.gaia/host/instance.json` for the daemon. Leave
2413
-
`--control` off unless you want a session driven.
2414
-
</Warning>
2415
-
2416
-
Endpoints (all under `/control/v1`, all requiring `Authorization: Bearer <token>`):
2417
-
2418
-
| Endpoint | Purpose |
2419
-
|----------|---------|
2420
-
|`GET /status`|`running` plus the active view, active agent, streaming, hub tab, selection, terminal size, and `can_return_to_hub`. |
2421
-
|`GET /screen`| The current rendered frame. `?format=plain` (default, ANSI stripped) or `?format=ansi`. |
2422
-
|`POST /keys`| Inject named keys — `{"keys": ["tab","down","enter"]}`. |
2423
-
|`POST /text`| Type a string as runes — `{"text": "triage my inbox"}`. |
2424
-
|`POST /wait`| Block until the screen or state matches, or time out. A timeout reports what the screen actually contained. |
2425
-
|`GET /frames`| Recent rendered frames (`?since=N`), for debugging what happened. |
2426
-
|`POST /resize`| Re-lay-out at a given size — `{"cols":120,"rows":40}`. |
2427
-
2428
-
Errors come back as `{"error": {"code", "message", "hint"}}`; a `/wait` timeout is
2429
-
HTTP 408 and carries the screen it saw.
2430
-
2431
-
Two response details matter when driving this:
2432
-
2433
-
-**`settled`** on `/keys`, `/text`, and `/resize`. The call returns only once every
2434
-
injected key has been *consumed and redrawn*, so reading `/screen` straight after
2435
-
is race-free. `settled: false` means the model was still busy — the input is
2436
-
queued, so re-read rather than assume it was ignored. It does **not** promise the
2437
-
consequences are done: pressing `enter` to launch an agent is consumed
2438
-
immediately, while the view switch it kicks off lands later. Use `/wait` for
2439
-
anything asynchronous.
2440
-
-**HTTP 503 `not_running`** when the TUI's event loop is not consuming input —
2441
-
it is still starting, or the user has quit. Bubble Tea silently drops messages
2442
-
in that window, so the API refuses instead of reporting a success for keys that
2443
-
went nowhere. Reads (`/status`, `/screen`, `/frames`) keep working.
2444
-
2445
-
`can_return_to_hub` exists because `esc` is not universally "go back": in a chat
2446
-
opened from the hub it returns there, but in a standalone `gaia chat --subprocess`
2447
-
session it **quits the program**. A driver that guesses wrong kills the session it
2448
-
was asked to drive, so the state says which.
2449
-
2450
-
Most callers should not speak this API directly — the
2451
-
[TUI MCP server](/guides/mcp/tui) wraps it as MCP tools.
2452
-
2453
2501
---
2454
2502
2455
2503
### TUI Control API
@@ -2531,8 +2579,6 @@ was asked to drive, so the state says which.
2531
2579
Most callers should not speak this API directly — the
2532
2580
[TUI MCP server](/guides/mcp/tui) wraps it as MCP tools.
2533
2581
2534
-
---
2535
-
2536
2582
### Cache Command
2537
2583
2538
2584
Inspect or clear GAIA's on-disk caches (document-Q7 metadata, chat history,
Copy file name to clipboardExpand all lines: docs/spec/agent-hub-restructure.mdx
+65-1Lines changed: 65 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,6 +66,7 @@ src/gaia/agents/ src/gaia/agents/ ← framework only
66
66
3.**Shared tool mixins promoted to framework.** RAG, FileIO, Shell, CodeIndex mixins move into `src/gaia/agents/tools/` so agents don't depend on each other for tools.
67
67
4.**Entry-point discovery.** Agents register via the `gaia.agent` entry point group. The registry discovers installed agent wheels at startup — no hardcoded agent list.
68
68
5.**Framework-provided generic server.**`src/gaia/agents/base/server.py` wraps any Agent into an OpenAI-compatible REST API + MCP stdio server. Agents don't implement server logic.
69
+
6.**Inherited init.** An agent *declares* what it needs — a model id, a minimum backend version — and the framework serves `GET`/`POST /v1/<id>/init` for it. Agents don't hand-write readiness or provisioning endpoints. See [Inherited init](#inherited-init).
Agents with cross-agent dependencies (e.g. RoutingAgent uses CodeAgent) declare them as package dependencies: `dependencies = ["amd-gaia>=0.18.0", "gaia-agent-code>=0.1.0"]`.
132
133
134
+
## Inherited init
135
+
136
+
An agent that needs a model before it can answer anything should not have to
137
+
hand-write a readiness endpoint to say so. It **declares** the requirement and
138
+
`AgentServer` serves both verbs at `/v1/<id>/init`:
139
+
140
+
| Verb | Effect | Status |
141
+
|------|--------|--------|
142
+
| `GET` | Read-only readiness probe. Reports backend reachability, version compatibility, and whether the declared model is downloaded — with an actionable `hint` per failure. | `200` ready · `503` not ready, **same body** either way |
143
+
| `POST` | Provisioning. Tells an already-running backend to download the declared model, streaming newline-terminated progress. | `503` before streaming if the backend is down · otherwise a committed `200` |
144
+
145
+
Declare requirements in `gaia-agent.yaml` (`models:` and
146
+
`requirements.min_lemonade_version`), or pass them explicitly:
147
+
148
+
```python
149
+
from gaia.agents.base.readiness import AgentRequirements
150
+
from gaia.agents.base.server import AgentServer
151
+
152
+
server = AgentServer(
153
+
MyAgent(),
154
+
agent_id="my-agent",
155
+
requirements=AgentRequirements(
156
+
model_id="Gemma-4-E4B-it-GGUF",
157
+
min_backend_version="10.2.0",
158
+
),
159
+
)
160
+
```
161
+
162
+
An agent that declares nothing gets **no** init routes — a readiness endpoint
163
+
that reports "ready" without having checked anything is worse than a 404,
164
+
because a consumer would trust it.
165
+
166
+
### Where the boundary sits
167
+
168
+
Inherited init makes *this agent's* requirements ready: its model, against a
169
+
backend that is already running. **Installing the backend stays with `gaia
170
+
init`.** An agent process cannot bootstrap the server it depends on, so when
171
+
Lemonade is unreachable both verbs fail loudly and name whose job the fix is
172
+
rather than hanging or half-succeeding:
173
+
174
+
```
175
+
✗ The local Lemonade Server is not reachable at http://127.0.0.1:9099/api/v1.
176
+
✗ Start it with `lemonade-server serve` (or run `gaia init`), then POST to this path again.
177
+
✗ My Agent can't install the backend itself — that's a host prerequisite.
178
+
```
179
+
180
+
### Reading a provisioning result
181
+
182
+
Once a streamed `200` is committed the HTTP status can no longer change, so a
183
+
pull that fails half-way still arrives as `200 OK`. **The final line is the
Replace ~16 direct agent imports in `cli.py`, `ui/_chat_helpers.py`, `ui/agent_loop.py`, and apps with `registry.create_agent(id)`.
156
220
</Step>
157
221
<Steptitle="Add framework generic server">
158
-
`src/gaia/agents/base/server.py`wraps any Agent into REST API + MCP server. Enables `gaia agent run <id> --api` and `--mcp`.
222
+
`src/gaia/agents/base/server.py` wraps any Agent into REST API + MCP server. Enables `gaia agent run <id> --api` and `--mcp`, and serves the agent's [inherited init](#inherited-init) routes.
159
223
</Step>
160
224
<Steptitle="Add CI/CD workflow">
161
225
`.github/workflows/build_agents.yml` matrix-builds the C++ agent binaries;
0 commit comments