Skip to content

Commit 238fe9a

Browse files
authored
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
1 parent ce73087 commit 238fe9a

50 files changed

Lines changed: 12188 additions & 268 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/reference/cli.mdx

Lines changed: 118 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,85 +2371,133 @@ under [Agent Command](#agent-command).
23712371

23722372
---
23732373

2374-
### TUI Control API
2374+
### Terminal hub (`gaia tui`)
23752375

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`).
23812379

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>
23842385

23852386
```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
23892395
```
23902396

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+
23912496
<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.
23952499
</Note>
23962500

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-
24532501
---
24542502

24552503
### TUI Control API
@@ -2531,8 +2579,6 @@ was asked to drive, so the state says which.
25312579
Most callers should not speak this API directly — the
25322580
[TUI MCP server](/guides/mcp/tui) wraps it as MCP tools.
25332581

2534-
---
2535-
25362582
### Cache Command
25372583

25382584
Inspect or clear GAIA's on-disk caches (document-Q7 metadata, chat history,

docs/spec/agent-hub-restructure.mdx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ src/gaia/agents/ src/gaia/agents/ ← framework only
6666
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.
6767
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.
6868
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).
6970

7071
## Pre-requisite: Promote shared tool mixins
7172

@@ -130,6 +131,69 @@ chat = "gaia_agent_chat.agent:ChatAgent"
130131

131132
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"]`.
132133

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
184+
verdict**`` success, `` failure, `` succeeded-but-unverified. Consumers
185+
must read it rather than the status code; the TUI's preflight gate
186+
(`tui/internal/ui/preflight`) already does.
187+
188+
Two distinctions the contract preserves deliberately, because each has a
189+
different remedy:
190+
191+
- **"Could not tell" is not "missing."** If the backend answers `/health` but
192+
its model list cannot be read, `present:false` means unknown — reporting it as
193+
missing would send the user to re-download a model they may already have.
194+
- **Indeterminate is not a pass.** A backend that advertises no version yields
195+
`compatible: null`, never `true`.
196+
133197
## Implementation Steps
134198

135199
<Steps>
@@ -155,7 +219,7 @@ Remove agent packages from `setup.py`. Remove `gaia-emr`/`gaia-code` console scr
155219
Replace ~16 direct agent imports in `cli.py`, `ui/_chat_helpers.py`, `ui/agent_loop.py`, and apps with `registry.create_agent(id)`.
156220
</Step>
157221
<Step title="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.
159223
</Step>
160224
<Step title="Add CI/CD workflow">
161225
`.github/workflows/build_agents.yml` matrix-builds the C++ agent binaries;

0 commit comments

Comments
 (0)