@amd-gaia/gaia 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md ADDED
@@ -0,0 +1,597 @@
1
+ ---
2
+ name: integrate-gaia
3
+ description: Use when integrating the @amd-gaia/gaia npm package — running GAIA's flagship agent, or embedding its local sidecar into a Node, TypeScript, or Electron app. Covers the one-command path, the SHA-256 integrity gate, platform coverage, starting the sidecar, the /v1/gaia/query SSE contract, and the gotchas that will bite you.
4
+ ---
5
+
6
+ # Integrating `@amd-gaia/gaia`
7
+
8
+ `@amd-gaia/gaia` delivers **two binaries** and owns their process lifecycle: the
9
+ frozen **agent sidecar** (`gaia-agent`) and the Go **terminal UI** (`gaia-tui`).
10
+ It ships no agent logic of its own, and it **builds neither binary at install
11
+ time** — both are published artifacts it downloads and verifies. The terminal UI
12
+ is the published `terminal-hub` component, the same binary a full GAIA install
13
+ runs as `gaia tui`, so its behaviour cannot differ from that one. Everything runs on the local machine against
14
+ a local model server — nothing you type or index leaves it.
15
+
16
+ Two ways in:
17
+
18
+ - **`npx @amd-gaia/gaia`** — fetch, verify, launch the terminal UI. What a human runs.
19
+ - **The programmatic exports** — fetch, spawn the sidecar, drive `POST /v1/gaia/query`
20
+ yourself. What you use when embedding GAIA in an app.
21
+
22
+ > **This file is NOT one of the agent's own skills.** It is the integration
23
+ > playbook: how *you* wire this package into an app. The agent separately loads
24
+ > **Agent Skills** into its own prompt at runtime from
25
+ > `gaia_agent/skills/<name>/SKILL.md`. Same filename, different artifact —
26
+ > don't ship this one as an agent skill. See
27
+ > [Skills](#10-skills--one-always-on-the-rest-opt-in).
28
+
29
+ ## 1. Install
30
+
31
+ ```bash
32
+ npx @amd-gaia/gaia # no install step; fetches what it needs on first run
33
+ npm install @amd-gaia/gaia # when you want the programmatic exports
34
+ ```
35
+
36
+ The package is **ESM-only** (`"type": "module"`) and needs **Node 18+** for the
37
+ built-in `fetch`. Use `import`, not `require`; from CommonJS use
38
+ `await import("@amd-gaia/gaia")`.
39
+
40
+ > `@amd-gaia/gaia` **publishes with this release** — it is not on npm yet. Until
41
+ > the release tag lands, `npx @amd-gaia/gaia` will not resolve. Run the agent from
42
+ > a source checkout in the meantime (see
43
+ > [the guide](https://amd-gaia.ai/docs/guides/gaia)).
44
+
45
+ ## 2. What `npx @amd-gaia/gaia` actually does
46
+
47
+ 1. Resolves the host platform key (`` `${process.platform}-${process.arch}` ``).
48
+ 2. Reads `binaries.lock.json`, the checksum manifest published with this exact
49
+ package version. Each component records its own hub lane, version, artifact
50
+ name and hash — they do not share a base URL.
51
+ 3. Downloads **both** binaries, each from its own lane, and **SHA-256 verifies
52
+ each against the lock**.
53
+ 4. Installs the sidecar into `~/.gaia/agents/gaia/` and the TUI into
54
+ `~/.gaia/npm-cache/gaia-<version>/`.
55
+ 5. Writes `~/.gaia/agents/gaia/.installed` — the record the daemon and the TUI
56
+ both read to decide the sidecar is installed. Written on a cache hit too, so
57
+ an install staged by an earlier release repairs itself.
58
+ 6. Execs the TUI, whose exit code becomes ours.
59
+
60
+ `run` deliberately does **not** spawn a sidecar. The TUI reaches agents through
61
+ the GAIA daemon's relay and never holds a sidecar token, and the daemon is what
62
+ spawns and supervises the sidecar — from exactly the directory step 4 wrote to.
63
+ A second sidecar started here would only fight the daemon's own for port 8141. Use
64
+ `gaia serve` when you want to own the process.
65
+
66
+ Other commands: `gaia fetch` (download + verify, print JSON, exit),
67
+ `gaia serve` (sidecar alone), `gaia version` (per-component version, source URL,
68
+ and platform matrix). Anything after a bare `--` goes to the TUI verbatim.
69
+
70
+ ### Where each binary comes from
71
+
72
+ | Component | Hub lane | Artifact names |
73
+ | --------- | ----------------------------------------- | ------------------------- |
74
+ | `sidecar` | `agents/gaia/<agentVersion>/` | `gaia-agent-<platformKey>[.exe]` |
75
+ | `tui` | `agents/terminal-hub/<componentVersion>/` | `gaia-<goPlatform>[.exe]` |
76
+
77
+ Two things follow from that, and both bite if you assume otherwise:
78
+
79
+ - **The two components version independently.** `lock.agentVersion` is this
80
+ package's version; `components.tui.componentVersion` is the terminal-hub release
81
+ it consumes. They will not match.
82
+ - **The TUI's artifact names use `win-x64` / `win-arm64`, not `win32-*`.** Platform
83
+ *keys* stay in Node's namespace (`process.platform` says `win32`); only the
84
+ `filename` crosses over. Never build a TUI URL by interpolating a platform key —
85
+ read `filename` from the lock entry.
86
+
87
+ ## 3. The integrity gate — it will stop you, by design
88
+
89
+ The SHA-256 check is this package's security boundary, and there is no flag,
90
+ env var, or option that relaxes it.
91
+
92
+ - Bytes are hashed **in memory and compared before anything is written** to the
93
+ cache path. A mismatch raises `IntegrityError` naming expected vs actual and
94
+ leaves nothing on disk.
95
+ - A **placeholder hash blocks the fetch before any network call** — between
96
+ releases every `sha256` in the lock is `PENDING-replace-with-real-sha256`, and
97
+ a value that is all zeros or contains `PENDING` (case-insensitive) is treated
98
+ as a placeholder. You get a `PlatformError`, not a download.
99
+ - A cache hit **re-hashes the on-disk file**. A cached binary whose bytes drifted
100
+ is re-downloaded, not reused.
101
+
102
+ If you need to run against a locally built binary, build it and point
103
+ `startSidecar` / `runTui` at it directly. The fetcher will not be talked into it.
104
+
105
+ ## 4. Platform coverage — the sidecar has two gaps
106
+
107
+ `terminal-hub` publishes the TUI for all six targets. The sidecar is a PyInstaller
108
+ freeze built on the platform it targets, and there is **no arm64 Linux and no arm64
109
+ Windows sidecar build**.
110
+
111
+ | Platform key | Sidecar | TUI |
112
+ | -------------- | :-----: | :-: |
113
+ | `win32-x64` | yes | yes |
114
+ | `darwin-arm64` | yes | yes |
115
+ | `darwin-x64` | yes | yes |
116
+ | `linux-x64` | yes | yes |
117
+ | `linux-arm64` | **no** | yes |
118
+ | `win32-arm64` | **no** | yes |
119
+
120
+ Resolving the sidecar on those two keys raises `PlatformError` naming the
121
+ platform and the supported set. It is not silently skipped, and the TUI is never
122
+ launched with no agent behind it. `npx @amd-gaia/gaia version` prints the matrix
123
+ for the version you have.
124
+
125
+ ## 5. Prerequisite — a local Lemonade server
126
+
127
+ The agent thinks with a model hosted by **Lemonade Server**, which this package
128
+ does not install. Required before any query succeeds:
129
+
130
+ 1. Lemonade **10.2.0 or newer**, running (`lemonade-server serve`).
131
+ 2. The default model downloaded (`gaia download Gemma-4-E4B-it-GGUF`, or
132
+ `gaia init`).
133
+
134
+ Do not guess — ask the sidecar. `GET /v1/gaia/init` is a read-only preflight
135
+ (it never pulls or loads) that probes Lemonade, compares its version to the
136
+ floor, and checks the model is present:
137
+
138
+ ```bash
139
+ curl -s http://127.0.0.1:8141/v1/gaia/init
140
+ ```
141
+
142
+ It answers **200** when ready and **503** when not, with the **same body shape
143
+ either way** — so branch on `.ready` and render `.hint`, never on the status code
144
+ alone:
145
+
146
+ ```jsonc
147
+ {
148
+ "ready": false,
149
+ "lemonade": { "reachable": false, "base_url": "…", "version": null,
150
+ "min_version": "10.2.0", "compatible": null },
151
+ "model": { "id": "Gemma-4-E4B-it-GGUF", "present": false,
152
+ "loadable": null, "ctx_size": null },
153
+ "hint": "Local Lemonade Server is not reachable at … — start it with `lemonade-server serve`, or set LEMONADE_BASE_URL to a running server."
154
+ }
155
+ ```
156
+
157
+ `lemonade.compatible: null` is **indeterminate**, not a pass — the version could
158
+ not be parsed. Render it as unknown.
159
+
160
+ `GET /health` is liveness only. A green `/health` means the REST surface is up;
161
+ it says nothing about whether a query will work.
162
+
163
+ ## 6. Start the sidecar
164
+
165
+ ```ts
166
+ import { fetchAll, startSidecar, shutdown } from "@amd-gaia/gaia";
167
+
168
+ // Fetch + SHA-256 verify both binaries. Build step, not per request.
169
+ const { sidecar, tui } = await fetchAll();
170
+
171
+ // Spawn -> poll /health -> check the contract apiVersion, in one call.
172
+ const proc = await startSidecar({ binaryPath: sidecar.binaryPath }); // port 8141
173
+
174
+ // ... drive proc.baseUrl ...
175
+
176
+ await shutdown(proc); // tree-kill; auto-cleanup also reaps on exit
177
+ ```
178
+
179
+ - `fetchAll(opts?)` returns `{ sidecar, tui, lock }`. Each result carries
180
+ `binaryPath`, `platformKey`, `sha256`, `cached`, `url`. For one component use
181
+ `fetchBinary({ component: "sidecar" | "tui", outDir })`.
182
+ - `startSidecar` throws if the binary can't start, never becomes healthy
183
+ (`HealthTimeoutError`, 60 s default — a cold one-file build unpacks first), or
184
+ reports an `apiVersion` whose **major** differs from this package's
185
+ (`VersionMismatchError`). On any failure it shuts the sidecar down before
186
+ rethrowing, so a failed start never leaks a process.
187
+ - **Tree-kill is not optional.** The frozen sidecar spawns a child uvicorn
188
+ process that `child.kill()` on the parent does not reap, which leaves port 8141
189
+ bound. `shutdown` kills the group (POSIX `SIGTERM` to `-pid`, escalating to
190
+ `SIGKILL`; Windows `taskkill /T /F`). `autoCleanup` (default `true`) also reaps
191
+ on `exit`, `SIGINT`/`SIGTERM`/`SIGHUP`, `uncaughtException`, and
192
+ `unhandledRejection`. A `SIGKILL` of *your* process is the one case nothing
193
+ in-process can catch.
194
+ - **Mint a caller token, or you are running the sidecar unauthenticated.** The
195
+ sidecar requires `Authorization: Bearer <token>` on every `/v1/gaia/*`
196
+ request, and skips the check only when neither token env var is set — dev
197
+ mode, which is what `startSidecar` gives you, because this package mints
198
+ nothing. Loopback binding is not the boundary: this agent has shell and file
199
+ tools. Mint your own and pass it through the `env` option (`StartOptions`
200
+ extends `SpawnOptions`, so it merges over `process.env`):
201
+
202
+ ```ts
203
+ import { randomBytes } from "node:crypto";
204
+ const token = randomBytes(32).toString("hex");
205
+ const proc = await startSidecar({
206
+ binaryPath: sidecar.binaryPath,
207
+ env: { GAIA_GAIA_SIDECAR_TOKEN: token }, // or ..._TOKEN_FILE, a 0600 path
208
+ });
209
+ ```
210
+
211
+ Then send `authorization: "Bearer " + token` on every `/v1/gaia/*` call.
212
+ `/health`, `/version`, and `/v1/gaia/version` are exempt, so `startSidecar`'s
213
+ own health-and-version handshake works either way. **If you did not spawn the
214
+ sidecar** — you are talking to one the GAIA daemon started — the token is the
215
+ daemon's, delivered to the sidecar as `GAIA_GAIA_SIDECAR_TOKEN_FILE` (a `0600`
216
+ file path); read it from there, don't invent one. A 401 whose `detail` names
217
+ both env vars means you sent the wrong token or none.
218
+
219
+ Or skip the code entirely and let the CLI own it:
220
+
221
+ ```bash
222
+ npx @amd-gaia/gaia serve --port 8141
223
+ curl http://127.0.0.1:8141/health
224
+ ```
225
+
226
+ ## 7. Call `POST /v1/gaia/query`
227
+
228
+ This is the whole agent surface. There is **no typed query client** in this
229
+ package — call it with plain `fetch`. Contract version **2.12**; the stream is
230
+ `text/event-stream` terminated by **exactly one** `final` or `error`.
231
+
232
+ Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
233
+
234
+ | Field | Required | Notes |
235
+ |---|---|---|
236
+ | `query` | yes | Non-empty. |
237
+ | `run_id` | yes | **You mint it**, and it must be a UUID (non-UUID → 422). It is the cancel handle, valid from the instant the request is sent. |
238
+ | `context` | yes | Transcript slice, pushed in the body — may be `[]`, never absent. Each item `{ role, content }`; `role` ∈ `user` / `assistant` / `system` / `tool`. |
239
+ | `session_id` | no | Contract ≥ 2.12. **Pass it.** The agent persists its indexed-document set per session — without it, it forgets a document between the turn that indexed it and the next question. |
240
+ | `can_answer_questions` | no | Set `false` for one-shot / batch runs so the agent resolves ambiguity itself instead of parking on a question nobody can see. |
241
+ | `model` | no | Overrides the model id — only when the run builds a fresh agent. On a retained `session_id` the agent already exists, so a model that differs from the one it was built with is a **409**, not an override. |
242
+ | `provider` | no | Local inference only — anything but `"lemonade"` is a **400**. |
243
+ | `max_steps` | no | ≥ 1. |
244
+
245
+ ```ts
246
+ import { randomUUID } from "node:crypto";
247
+
248
+ const runId = randomUUID();
249
+ const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
250
+ method: "POST",
251
+ headers: {
252
+ "content-type": "application/json",
253
+ accept: "text/event-stream",
254
+ authorization: `Bearer ${token}`, // required unless the sidecar is in dev mode — §6
255
+ },
256
+ body: JSON.stringify({
257
+ query: "Summarize the PDFs in ~/Documents/reports",
258
+ run_id: runId,
259
+ context: [],
260
+ session_id: "s1",
261
+ can_answer_questions: false,
262
+ }),
263
+ });
264
+
265
+ const reader = res.body!.getReader();
266
+ const dec = new TextDecoder();
267
+ let buf = "";
268
+ outer: for (;;) {
269
+ const { value, done } = await reader.read();
270
+ if (done) break;
271
+ buf += dec.decode(value, { stream: true });
272
+ let i: number;
273
+ while ((i = buf.indexOf("\n\n")) >= 0) {
274
+ const frame = buf.slice(0, i);
275
+ buf = buf.slice(i + 2);
276
+ const line = frame.split("\n").find((l) => l.startsWith("data: "));
277
+ if (!line) continue; // ":" frames are keepalive comments
278
+ const ev = JSON.parse(line.slice(6));
279
+ switch (ev.type) {
280
+ case "status": console.log(ev.message); break;
281
+ case "token": process.stdout.write(ev.delta); break;
282
+ case "tool_call": console.log(`→ ${ev.tool}`, ev.args); break;
283
+ case "tool_result": console.log(`← ${ev.tool}`, ev.data); break;
284
+ case "needs_confirmation": break; // see §8 — a refusal follows
285
+ case "needs_input": /* answer it — see below */ break;
286
+ case "final": console.log(ev.answer); break outer; // terminal
287
+ case "error": console.error(ev.detail); break outer; // terminal
288
+ default: console.warn("unsupported event", ev); // future additive type
289
+ }
290
+ }
291
+ }
292
+ ```
293
+
294
+ The canonical event shapes, as emitted:
295
+
296
+ | Event | Shape |
297
+ |---|---|
298
+ | `status` | `{ type, message }` — progress and reasoning narration |
299
+ | `token` | `{ type, delta }` — answer text to append |
300
+ | `tool_call` | `{ type, tool, args }` |
301
+ | `tool_result` | `{ type, tool, data, render? }` |
302
+ | `needs_confirmation` | `{ type, run_id, action, summary }` — no `confirm_url`; see §8 |
303
+ | `needs_input` | `{ type, run_id, request_id, question, options[], allow_free_text, sensitive, respond_url, timeout_seconds? }` |
304
+ | `final` | `{ type, answer, usage? }` — terminal |
305
+ | `error` | `{ type, detail, status }` — terminal, surface `detail` verbatim |
306
+
307
+ Rules a client must respect:
308
+
309
+ - **An idle run emits `: keepalive` SSE comments every 10 s.** Skip lines that
310
+ aren't `data:` and reset your read-idle timer on them — a long tool call is not
311
+ a dead stream.
312
+ - **Never treat stream close without a terminal event as success.** The server
313
+ guarantees one; a close without one means something broke on your side.
314
+ - **Answer `needs_input`, don't restart.** The run is parked on the *same*
315
+ stream. `POST /v1/gaia/query/{run_id}/respond` with
316
+ `{ request_id, response }` and keep reading the existing stream — a fresh
317
+ `/query` POST abandons the paused run. Unknown run → **404**; a `request_id` that
318
+ is no longer pending → **409** (both loud, never a silent drop). Render each
319
+ option's `description`, and mask the input when `sensitive` is set.
320
+ - **Cancel with `POST /v1/gaia/query/{run_id}/cancel`.** It returns
321
+ `{ run_id, cancelled }` — an unknown id reports `cancelled: false` with a
322
+ **200**, not a 404, because a cancel racing a normal completion is expected.
323
+ Dropping the HTTP connection also cancels the run.
324
+
325
+ ## 8. Over `/v1/gaia/query`, confirmation-gated tools are **refused, not prompted**
326
+
327
+ Read this before you design a workflow around it. This section is about the HTTP
328
+ surface — the agent's other transport can collect an approval; see SPEC §5.5.
329
+
330
+ Seven of the agent's 71 tools mutate the machine and need explicit approval
331
+ before they run. Five sit in the base `TOOLS_REQUIRING_CONFIRMATION` set —
332
+ **`write_file`**, **`edit_file`**, **`run_shell_command`**,
333
+ **`execute_python_file`**, and **`notify_desktop`**, which spawns a PowerShell
334
+ child on Windows to draw the notification — and the flagship adds two of its
335
+ own, **`install_skill`** and **`remove_skill`**, because installing a skill
336
+ writes third-party code under `~/.gaia/skills` and removing one deletes it.
337
+ Everything else — reading, indexing, querying, web fetching, memory — runs
338
+ without asking.
339
+
340
+ Over `/v1/gaia/query` there is **no way to collect an approval**, so the stream
341
+ does not prompt. When the agent reaches one of those tools it emits a
342
+ `needs_confirmation` event, and the server **immediately follows it with a
343
+ terminal `final`** whose `answer` says it stopped before running that action,
344
+ then cancels the run. There is no `confirm_url`, no resume, and no
345
+ `/query/{run_id}/confirm` endpoint — it is a deliberate deny-by-default stub, not
346
+ an oversight.
347
+
348
+ Concretely, your client sees:
349
+
350
+ ```
351
+ data: {"type":"needs_confirmation","run_id":"…","action":"write_file","summary":"Run 'write_file'?"}
352
+ data: {"type":"final","answer":"I stopped before running 'write_file' because it needs your explicit approval, and this streaming surface cannot collect that yet. …"}
353
+ ```
354
+
355
+ So: **`/query` cannot run any of those seven tools.** If your integration needs
356
+ that, drive the agent from a surface that can prompt — its stdio transport is the
357
+ one that can, because its control channel carries an approval back to a turn
358
+ already in flight (SPEC §5.5) — or perform the mutation yourself from your own
359
+ code and let the agent do the reading and reasoning. Treat `needs_confirmation`
360
+ as an early warning that the run is about to end, not as a question you can
361
+ answer.
362
+
363
+ ## 9. File-access scope
364
+
365
+ The agent's file, document, and data tools are confined to a set of allowed
366
+ paths, and **the default is the user's home directory**. That is the honest scope
367
+ for a personal document agent, and it is still a real boundary — system
368
+ directories, program files, and other users' homes are refused, with the check
369
+ run against the *resolved* path so a symlink out of scope doesn't slip through.
370
+
371
+ **Being in scope is not the same as being safe, and two denylists apply inside
372
+ it.** Reads refuse secrets — `.env`, `id_rsa`, `credentials.json`, `.netrc`,
373
+ `.pem`/`.key`, and everything under `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube` —
374
+ even in an allowed directory. Writes additionally refuse anything that executes
375
+ on its own: shell startup files, PowerShell profiles, `~/.config/autostart/*`,
376
+ systemd user units, `LaunchAgents`, and a repo's `.git/` (hooks and config). Both
377
+ come back as a structured error naming the file and the reason, so do not plan an
378
+ integration around reading a credential file or editing a shell rc — perform
379
+ those from your own code.
380
+
381
+ **In 0.1.1 narrowing it is a construction-time setting only.** The packaged
382
+ sidecar exposes no flag or env var for `allowed_paths` (its CLI accepts only
383
+ `--host` and `--port`), so restricting the scope means embedding `GaiaAgent` in
384
+ your own Python process:
385
+
386
+ ```python
387
+ from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
388
+
389
+ agent = GaiaAgent(config=GaiaAgentConfig(allowed_paths=["/home/me/Documents"]))
390
+ ```
391
+
392
+ ## 10. Skills — one always-on, the rest opt-in
393
+
394
+ The agent is built to host **Agent Skills** (short playbooks loaded into its own
395
+ prompt, grouped into named sets, one set active per launch), and its bundled
396
+ skill directory is the highest-precedence discovery root.
397
+
398
+ Loaded skills are **not** all resident every turn: each turn the agent embeds
399
+ the query against every loaded skill's description and renders only the
400
+ matching bodies in full — the rest collapse to a one-line menu entry, and the
401
+ model (or the user) re-activates one by calling `load_skill` on it again.
402
+ `GAIA_DYNAMIC_SKILLS=0` disables the per-turn selection (every loaded body
403
+ renders every turn); `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides the match
404
+ threshold. Manifest `skills:` entries are always-on and never collapse. If the
405
+ embedder is unavailable, selection disables itself for the session and every
406
+ body renders — capability is never silently lost to a failed match.
407
+
408
+ **One skill ships enabled: `gaia-voice`.** It is a manifest `skills:` entry, so
409
+ it is always on, always rendered in full, and paid on every LLM call of every
410
+ turn — budget for it. It is not a task recipe but the agent's honesty floor: do
411
+ not claim work you did not do, do not present empty output as a result, do not
412
+ substitute a near-miss and report success. Those failures corrupt an answer
413
+ whatever the task is, which is why it cannot live in an opt-in bundle. It
414
+ declares no tools, and its body measures 676 tokens (tiktoken `cl100k`).
415
+
416
+ **No skill *set* loads.** `gaia-agent.yaml` ships its `skill_sets:` and
417
+ `default_skill_set:` blocks **commented out** — following the email agent's
418
+ precedent, because loading several skill bodies into every prompt costs tokens
419
+ and no eval has measured that trade for this agent yet. Re-enabling is
420
+ uncommenting two blocks; no code change.
421
+
422
+ So today there is nothing for `GAIA_SKILL_SET` to select — leave it unset. Once
423
+ a release declares sets, `GAIA_SKILL_SET` is the selection channel for the
424
+ packaged sidecar (its CLI accepts only `--host` and `--port`), and an undeclared
425
+ name raises naming the valid sets rather than falling back to a default. Beyond
426
+ `gaia-voice`, do not design around a skill being on by default.
427
+
428
+ ## 11. The project map — two things it costs you
429
+
430
+ When the agent's working directory is a code repository, every task starts with
431
+ a **project map** in the system prompt: the root, the directory shape, the
432
+ likely entry points, which commands are installed, and the three platform
433
+ differences that change command syntax. It exists so the agent stops burning
434
+ round trips on "no such file" and "command not found".
435
+
436
+ Two consequences an integrator needs to plan for:
437
+
438
+ - **Up to 600 prompt tokens, every turn.** That is the enforced ceiling
439
+ (1.8% of the NPU profile's 32K window), not a typical value — budget it
440
+ alongside `gaia-voice`'s 676.
441
+ - **A background embedding pass on first contact with a new repository.** If
442
+ the repo has no [code index](https://amd-gaia.ai/docs/guides/code-index), the
443
+ map starts one in a background thread so semantic search is ready when it is
444
+ needed. On a large monorepo that is minutes of local embedding.
445
+ `GAIA_PROJECT_MAP_AUTO_INDEX=0` turns it off.
446
+
447
+ The sidecar's CLI accepts only `--host` and `--port`, so pointing the map at a
448
+ specific project means `GAIA_PROJECT_ROOT=/path/to/repo` in its environment, or
449
+ `GaiaAgentConfig(project_root=...)` when embedding. A directory that is neither
450
+ a VCS checkout nor holds a recognised manifest gets **no map** — that is the
451
+ designed answer, not a failure.
452
+
453
+ ## 12. Ports
454
+
455
+ | Service | Port |
456
+ |---|---|
457
+ | Agent sidecar | `8141` on `127.0.0.1` |
458
+ | GAIA daemon | assigned at start, recorded in `~/.gaia/host/instance.json` |
459
+
460
+ Port **4001 is reserved repo-wide**: `spawnSidecar` throws a `RangeError` and
461
+ `gaia serve --port 4001` exits 2. Both services bind loopback only — this agent
462
+ speaks for the user's documents and memory and has no business on a LAN
463
+ interface.
464
+
465
+ ## 13. Running in a server or long-lived app
466
+
467
+ - **`fetchAll` / `fetchBinary` are a build step**, not per request — network plus
468
+ a full SHA-256 hash of a large artifact. Run once at install time.
469
+ - **`resolveSidecarPath` / `resolveTuiPath` are startup, not per request.** They
470
+ re-hash the binary against `binaries.lock.json` before handing back a path that
471
+ gets spawned, so they cost a full read of a large file. Resolve once and keep
472
+ the path. `{ verify: false }` skips the check for a binary you built yourself.
473
+ - **Spawn once at boot** and hold the `Sidecar` handle for the process lifetime.
474
+ Never per request.
475
+ - **Low concurrency.** One local Lemonade model slot, so parallel queries
476
+ serialize. Cap inflight runs.
477
+ - **The package does not restart a crashed sidecar.** It reaps one; supervision
478
+ is the daemon's job (or yours).
479
+ - **`DEBUG=gaia`** puts download, spawn, and sidecar output on **stderr**. stdout
480
+ belongs to the TUI once exec'd, and to machine-readable JSON for `fetch` /
481
+ `version` — never write diagnostics there.
482
+
483
+ Every failure throws a typed error extending `GaiaError`, so
484
+ `instanceof GaiaError` catches any of ours: `IntegrityError`, `PlatformError`,
485
+ `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, `HttpError`.
486
+ There is no silent null.
487
+
488
+ ## Gotchas — read before debugging
489
+
490
+ - **`/health` green ≠ ready.** It never touches the model server. Use
491
+ `GET /v1/gaia/init` and branch on `.ready`; it returns 503 with a full body and
492
+ a `hint`, not an empty error.
493
+ - **A terminal `error` whose `detail` starts "Local Lemonade Server is not
494
+ reachable"** means Lemonade isn't running or isn't reachable — not a bug in
495
+ this package. Start it, or set `LEMONADE_BASE_URL`.
496
+ - **`needs_confirmation` is followed by a refusal and the run ends.** See §8.
497
+ The seven gated tools are unreachable **over `/query`** — the agent itself can
498
+ run them on a transport that can prompt (SPEC §5.5).
499
+ - **A placeholder hash in `binaries.lock.json` blocks the fetch before any
500
+ network call.** Between releases that is the *expected* state — it is not a
501
+ broken install, and there is no override.
502
+ - **No `linux-arm64` / `win32-arm64` sidecar.** The TUI has both. A `PlatformError`
503
+ on those hosts is the design, not a missing artifact.
504
+ - **A `401` from `/v1/gaia/*` is the caller-auth token, not a bug.** The sidecar
505
+ requires `Authorization: Bearer <token>`; `/health`, `/version`, and
506
+ `/v1/gaia/version` are exempt, which is why a green health check sits happily
507
+ in front of a 401 on `/query`. It skips the check only in dev mode — neither
508
+ token env var set — which is what `spawnSidecar` and `gaia serve` produce,
509
+ because this package mints nothing. Do not treat loopback binding as the
510
+ boundary; mint a token and pass it (§6).
511
+ - **`run_id` must be a UUID**, and unknown fields in the request body are a
512
+ **422** — the model forbids extras. Typos don't get ignored.
513
+ - **`gaia run` needs the *Python* `gaia` CLI on `PATH`** — the TUI shells out to
514
+ it to start the daemon. So the TUI doesn't re-invoke our own npm shim, the
515
+ child's `PATH` is rewritten: a directory holding nothing but our shim (an npx
516
+ temp dir) is dropped, and a **shared** bin directory is moved to the end
517
+ instead of removed, so the `python3` / `lemonade-server` / real `gaia` beside
518
+ it stay reachable. If the Python CLI isn't installed anywhere, the daemon never
519
+ comes up. It must also be **0.23.1+**: an older core's daemon starts fine but
520
+ has no sidecar entry for this agent, which reads as a UI with a dead agent
521
+ rather than as a version problem.
522
+ - **The TUI is installed as `gaia-tui`, never `gaia`** — the terminal-hub artifact
523
+ *is* called `gaia-<platform>`, and a file named `gaia` in a cache directory would
524
+ shadow the npm bin shim. The lock's `filename` and `executable` differ for that
525
+ reason. Don't rename it back.
526
+ - **The TUI comes from a lane this package doesn't publish.** If a fetch 404s on
527
+ the TUI but not the sidecar, the pinned `terminal-hub` version is the thing to
528
+ check — `gaia version` prints it and its base URL.
529
+ - **ESM-only.** `require("@amd-gaia/gaia")` fails; use `import` or dynamic
530
+ `import()`.
531
+
532
+ ## Verify the integration
533
+
534
+ Green path, in order:
535
+
536
+ ```bash
537
+ npx @amd-gaia/gaia version # per-component version + source URL + matrix
538
+ npx @amd-gaia/gaia fetch # JSON: one entry per binary with its sha256
539
+ npx @amd-gaia/gaia serve --port 8141
540
+ ```
541
+
542
+ Against a lock that still carries `PENDING-…` hashes, `fetch` is *expected* to
543
+ fail with a `PlatformError` before any download — that is the gate working, not a
544
+ broken install. Only a published release has real hashes.
545
+
546
+ Then, in another terminal:
547
+
548
+ ```bash
549
+ curl -s http://127.0.0.1:8141/health # {"status":"ok","service":"gaia-agent-gaia"}
550
+ curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.12","agentVersion":"0.1.1"}
551
+ curl -s http://127.0.0.1:8141/v1/gaia/init # 200 + "ready":true, or 503 + a "hint"
552
+ curl -N -X POST http://127.0.0.1:8141/v1/gaia/query \
553
+ -H 'content-type: application/json' \
554
+ -d '{"query":"What can you do?","run_id":"00000000-0000-4000-8000-000000000001","context":[],"can_answer_questions":false}'
555
+ ```
556
+
557
+ A healthy run streams `status` / `token` events and ends with one `final`. If
558
+ `/v1/gaia/init` is 503, fix what its `hint` names and retry — the rest of your
559
+ integration is fine.
560
+
561
+ That `/query` call carries no `Authorization` header because `gaia serve` starts
562
+ the sidecar in dev mode. Against one started with a token, add
563
+ `-H "authorization: Bearer $TOKEN"` — a **401** here and a green `/health` is
564
+ that and nothing else (§6).
565
+
566
+ **A 503 from `/query` itself is a different condition**: every retained
567
+ session slot is busy and none is idle enough to evict (SPEC §5.2). Do NOT
568
+ loop on `/v1/gaia/init` — it will report ready. Wait for a running turn to
569
+ finish (or close an idle session) and retry the same `/query`.
570
+
571
+ **Three more refusals are yours to avoid**, each naming its fix in `detail`
572
+ (SPEC §5.2 has the reasoning):
573
+
574
+ - **409 — the `run_id` is still in flight.** You mint it, so mint a fresh UUID
575
+ per request; reusing one would leave the earlier run with no way to be
576
+ cancelled.
577
+ - **409 — `model` differs from what this `session_id` was built with.** Only
578
+ construction reads a model, so it cannot be applied to the retained agent.
579
+ Omit `model` to stay on the session's current one, or start a new
580
+ `session_id` to switch.
581
+ - **400 — the `Host` header is absent or empty.** The loopback check fails
582
+ closed, so omitting the header is refused rather than served. Send
583
+ `Host: 127.0.0.1:<port>`; every real HTTP client already does.
584
+
585
+ For the full wire contract, lock schema, exit codes, and timeout table, see
586
+ [`SPEC.md`](./SPEC.md). For the user-facing overview, see [`README.md`](./README.md)
587
+ and <https://amd-gaia.ai/docs/guides/gaia>.
588
+
589
+ ## TUI inference providers
590
+
591
+ The stdio TUI supports `/provider` for Local, Fireworks AI, and AMD LLM Gateway.
592
+ Keys are entered in a masked field and sent directly to local Lemonade's runtime
593
+ auth API, never as agent queries. `/model` lists supported discovered cloud and
594
+ downloaded local models; `/model fireworks.gemma-4-31b-it` selects Gemma 4 31B IT
595
+ when available. Cloud chat sends conversation history to the selected provider;
596
+ embeddings remain on Lemonade. The status event names the actual provider and
597
+ marks remote inference. This is a TUI/stdio capability, not an HTTP query command.