@amd-gaia/gaia 0.1.0 → 0.1.1

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,473 @@
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 [Skills](#10-skills--opt-in-and-empty-in-010).
27
+
28
+ ## 1. Install
29
+
30
+ ```bash
31
+ npx @amd-gaia/gaia # no install step; fetches what it needs on first run
32
+ npm install @amd-gaia/gaia # when you want the programmatic exports
33
+ ```
34
+
35
+ The package is **ESM-only** (`"type": "module"`) and needs **Node 18+** for the
36
+ built-in `fetch`. Use `import`, not `require`; from CommonJS use
37
+ `await import("@amd-gaia/gaia")`.
38
+
39
+ > `@amd-gaia/gaia` **publishes with this release** — it is not on npm yet. Until
40
+ > the release tag lands, `npx @amd-gaia/gaia` will not resolve. Run the agent from
41
+ > a source checkout in the meantime (see
42
+ > [the guide](https://amd-gaia.ai/docs/guides/gaia)).
43
+
44
+ ## 2. What `npx @amd-gaia/gaia` actually does
45
+
46
+ 1. Resolves the host platform key (`` `${process.platform}-${process.arch}` ``).
47
+ 2. Reads `binaries.lock.json`, the checksum manifest published with this exact
48
+ package version. Each component records its own hub lane, version, artifact
49
+ name and hash — they do not share a base URL.
50
+ 3. Downloads **both** binaries, each from its own lane, and **SHA-256 verifies
51
+ each against the lock**.
52
+ 4. Installs the sidecar into `~/.gaia/agents/gaia/` and the TUI into
53
+ `~/.gaia/npm-cache/gaia-<version>/`.
54
+ 5. Execs the TUI, whose exit code becomes ours.
55
+
56
+ `run` deliberately does **not** spawn a sidecar. The TUI reaches agents through
57
+ the GAIA daemon's relay and never holds a sidecar token, and the daemon is what
58
+ spawns and supervises the sidecar — from exactly the directory step 4 wrote to.
59
+ A second sidecar started here would only fight the daemon's own for port 8141. Use
60
+ `gaia serve` when you want to own the process.
61
+
62
+ Other commands: `gaia fetch` (download + verify, print JSON, exit),
63
+ `gaia serve` (sidecar alone), `gaia version` (per-component version, source URL,
64
+ and platform matrix). Anything after a bare `--` goes to the TUI verbatim.
65
+
66
+ ### Where each binary comes from
67
+
68
+ | Component | Hub lane | Artifact names |
69
+ | --------- | ----------------------------------------- | ------------------------- |
70
+ | `sidecar` | `agents/gaia/<agentVersion>/` | `gaia-agent-<platformKey>[.exe]` |
71
+ | `tui` | `agents/terminal-hub/<componentVersion>/` | `gaia-<goPlatform>[.exe]` |
72
+
73
+ Two things follow from that, and both bite if you assume otherwise:
74
+
75
+ - **The two components version independently.** `lock.agentVersion` is this
76
+ package's version; `components.tui.componentVersion` is the terminal-hub release
77
+ it consumes. They will not match.
78
+ - **The TUI's artifact names use `win-x64` / `win-arm64`, not `win32-*`.** Platform
79
+ *keys* stay in Node's namespace (`process.platform` says `win32`); only the
80
+ `filename` crosses over. Never build a TUI URL by interpolating a platform key —
81
+ read `filename` from the lock entry.
82
+
83
+ ## 3. The integrity gate — it will stop you, by design
84
+
85
+ The SHA-256 check is this package's security boundary, and there is no flag,
86
+ env var, or option that relaxes it.
87
+
88
+ - Bytes are hashed **in memory and compared before anything is written** to the
89
+ cache path. A mismatch raises `IntegrityError` naming expected vs actual and
90
+ leaves nothing on disk.
91
+ - A **placeholder hash blocks the fetch before any network call** — between
92
+ releases every `sha256` in the lock is `PENDING-replace-with-real-sha256`, and
93
+ a value that is all zeros or contains `PENDING` (case-insensitive) is treated
94
+ as a placeholder. You get a `PlatformError`, not a download.
95
+ - A cache hit **re-hashes the on-disk file**. A cached binary whose bytes drifted
96
+ is re-downloaded, not reused.
97
+
98
+ If you need to run against a locally built binary, build it and point
99
+ `startSidecar` / `runTui` at it directly. The fetcher will not be talked into it.
100
+
101
+ ## 4. Platform coverage — the sidecar has two gaps
102
+
103
+ `terminal-hub` publishes the TUI for all six targets. The sidecar is a PyInstaller
104
+ freeze built on the platform it targets, and there is **no arm64 Linux and no arm64
105
+ Windows sidecar build**.
106
+
107
+ | Platform key | Sidecar | TUI |
108
+ | -------------- | :-----: | :-: |
109
+ | `win32-x64` | yes | yes |
110
+ | `darwin-arm64` | yes | yes |
111
+ | `darwin-x64` | yes | yes |
112
+ | `linux-x64` | yes | yes |
113
+ | `linux-arm64` | **no** | yes |
114
+ | `win32-arm64` | **no** | yes |
115
+
116
+ Resolving the sidecar on those two keys raises `PlatformError` naming the
117
+ platform and the supported set. It is not silently skipped, and the TUI is never
118
+ launched with no agent behind it. `npx @amd-gaia/gaia version` prints the matrix
119
+ for the version you have.
120
+
121
+ ## 5. Prerequisite — a local Lemonade server
122
+
123
+ The agent thinks with a model hosted by **Lemonade Server**, which this package
124
+ does not install. Required before any query succeeds:
125
+
126
+ 1. Lemonade **10.2.0 or newer**, running (`lemonade-server serve`).
127
+ 2. The default model downloaded (`gaia download Gemma-4-E4B-it-GGUF`, or
128
+ `gaia init`).
129
+
130
+ Do not guess — ask the sidecar. `GET /v1/gaia/init` is a read-only preflight
131
+ (it never pulls or loads) that probes Lemonade, compares its version to the
132
+ floor, and checks the model is present:
133
+
134
+ ```bash
135
+ curl -s http://127.0.0.1:8141/v1/gaia/init
136
+ ```
137
+
138
+ It answers **200** when ready and **503** when not, with the **same body shape
139
+ either way** — so branch on `.ready` and render `.hint`, never on the status code
140
+ alone:
141
+
142
+ ```jsonc
143
+ {
144
+ "ready": false,
145
+ "lemonade": { "reachable": false, "base_url": "…", "version": null,
146
+ "min_version": "10.2.0", "compatible": null },
147
+ "model": { "id": "Gemma-4-E4B-it-GGUF", "present": false,
148
+ "loadable": null, "ctx_size": null },
149
+ "hint": "Local Lemonade Server is not reachable at … — start it with `lemonade-server serve`, or set LEMONADE_BASE_URL to a running server."
150
+ }
151
+ ```
152
+
153
+ `lemonade.compatible: null` is **indeterminate**, not a pass — the version could
154
+ not be parsed. Render it as unknown.
155
+
156
+ `GET /health` is liveness only. A green `/health` means the REST surface is up;
157
+ it says nothing about whether a query will work.
158
+
159
+ ## 6. Start the sidecar
160
+
161
+ ```ts
162
+ import { fetchAll, startSidecar, shutdown } from "@amd-gaia/gaia";
163
+
164
+ // Fetch + SHA-256 verify both binaries. Build step, not per request.
165
+ const { sidecar, tui } = await fetchAll();
166
+
167
+ // Spawn -> poll /health -> check the contract apiVersion, in one call.
168
+ const proc = await startSidecar({ binaryPath: sidecar.binaryPath }); // port 8141
169
+
170
+ // ... drive proc.baseUrl ...
171
+
172
+ await shutdown(proc); // tree-kill; auto-cleanup also reaps on exit
173
+ ```
174
+
175
+ - `fetchAll(opts?)` returns `{ sidecar, tui, lock }`. Each result carries
176
+ `binaryPath`, `platformKey`, `sha256`, `cached`, `url`. For one component use
177
+ `fetchBinary({ component: "sidecar" | "tui", outDir })`.
178
+ - `startSidecar` throws if the binary can't start, never becomes healthy
179
+ (`HealthTimeoutError`, 60 s default — a cold one-file build unpacks first), or
180
+ reports an `apiVersion` whose **major** differs from this package's
181
+ (`VersionMismatchError`). On any failure it shuts the sidecar down before
182
+ rethrowing, so a failed start never leaks a process.
183
+ - **Tree-kill is not optional.** The frozen sidecar spawns a child uvicorn
184
+ process that `child.kill()` on the parent does not reap, which leaves port 8141
185
+ bound. `shutdown` kills the group (POSIX `SIGTERM` to `-pid`, escalating to
186
+ `SIGKILL`; Windows `taskkill /T /F`). `autoCleanup` (default `true`) also reaps
187
+ on `exit`, `SIGINT`/`SIGTERM`/`SIGHUP`, `uncaughtException`, and
188
+ `unhandledRejection`. A `SIGKILL` of *your* process is the one case nothing
189
+ in-process can catch.
190
+
191
+ Or skip the code entirely and let the CLI own it:
192
+
193
+ ```bash
194
+ npx @amd-gaia/gaia serve --port 8141
195
+ curl http://127.0.0.1:8141/health
196
+ ```
197
+
198
+ ## 7. Call `POST /v1/gaia/query`
199
+
200
+ This is the whole agent surface. There is **no typed query client** in this
201
+ package — call it with plain `fetch`. Contract version **2.12**; the stream is
202
+ `text/event-stream` terminated by **exactly one** `final` or `error`.
203
+
204
+ Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
205
+
206
+ | Field | Required | Notes |
207
+ |---|---|---|
208
+ | `query` | yes | Non-empty. |
209
+ | `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. |
210
+ | `context` | yes | Transcript slice, pushed in the body — may be `[]`, never absent. Each item `{ role, content }`; `role` ∈ `user` / `assistant` / `system` / `tool`. |
211
+ | `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. |
212
+ | `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. |
213
+ | `model` | no | Overrides the model id for this run. |
214
+ | `provider` | no | Local inference only — anything but `"lemonade"` is a **400**. |
215
+ | `max_steps` | no | ≥ 1. |
216
+
217
+ ```ts
218
+ import { randomUUID } from "node:crypto";
219
+
220
+ const runId = randomUUID();
221
+ const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
222
+ method: "POST",
223
+ headers: { "content-type": "application/json", accept: "text/event-stream" },
224
+ body: JSON.stringify({
225
+ query: "Summarize the PDFs in ~/Documents/reports",
226
+ run_id: runId,
227
+ context: [],
228
+ session_id: "s1",
229
+ can_answer_questions: false,
230
+ }),
231
+ });
232
+
233
+ const reader = res.body!.getReader();
234
+ const dec = new TextDecoder();
235
+ let buf = "";
236
+ outer: for (;;) {
237
+ const { value, done } = await reader.read();
238
+ if (done) break;
239
+ buf += dec.decode(value, { stream: true });
240
+ let i: number;
241
+ while ((i = buf.indexOf("\n\n")) >= 0) {
242
+ const frame = buf.slice(0, i);
243
+ buf = buf.slice(i + 2);
244
+ const line = frame.split("\n").find((l) => l.startsWith("data: "));
245
+ if (!line) continue; // ":" frames are keepalive comments
246
+ const ev = JSON.parse(line.slice(6));
247
+ switch (ev.type) {
248
+ case "status": console.log(ev.message); break;
249
+ case "token": process.stdout.write(ev.delta); break;
250
+ case "tool_call": console.log(`→ ${ev.tool}`, ev.args); break;
251
+ case "tool_result": console.log(`← ${ev.tool}`, ev.data); break;
252
+ case "needs_confirmation": break; // see §8 — a refusal follows
253
+ case "needs_input": /* answer it — see below */ break;
254
+ case "final": console.log(ev.answer); break outer; // terminal
255
+ case "error": console.error(ev.detail); break outer; // terminal
256
+ default: console.warn("unsupported event", ev); // future additive type
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ The canonical event shapes, as emitted:
263
+
264
+ | Event | Shape |
265
+ |---|---|
266
+ | `status` | `{ type, message }` — progress and reasoning narration |
267
+ | `token` | `{ type, delta }` — answer text to append |
268
+ | `tool_call` | `{ type, tool, args }` |
269
+ | `tool_result` | `{ type, tool, data, render? }` |
270
+ | `needs_confirmation` | `{ type, run_id, action, summary }` — no `confirm_url`; see §8 |
271
+ | `needs_input` | `{ type, run_id, request_id, question, options[], allow_free_text, sensitive, respond_url, timeout_seconds? }` |
272
+ | `final` | `{ type, answer, usage? }` — terminal |
273
+ | `error` | `{ type, detail, status }` — terminal, surface `detail` verbatim |
274
+
275
+ Rules a client must respect:
276
+
277
+ - **An idle run emits `: keepalive` SSE comments every 10 s.** Skip lines that
278
+ aren't `data:` and reset your read-idle timer on them — a long tool call is not
279
+ a dead stream.
280
+ - **Never treat stream close without a terminal event as success.** The server
281
+ guarantees one; a close without one means something broke on your side.
282
+ - **Answer `needs_input`, don't restart.** The run is parked on the *same*
283
+ stream. `POST /v1/gaia/query/{run_id}/respond` with
284
+ `{ request_id, response }` and keep reading the existing stream — a fresh
285
+ `/query` POST abandons the paused run. Unknown run → **404**; a `request_id` that
286
+ is no longer pending → **409** (both loud, never a silent drop). Render each
287
+ option's `description`, and mask the input when `sensitive` is set.
288
+ - **Cancel with `POST /v1/gaia/query/{run_id}/cancel`.** It returns
289
+ `{ run_id, cancelled }` — an unknown id reports `cancelled: false` with a
290
+ **200**, not a 404, because a cancel racing a normal completion is expected.
291
+ Dropping the HTTP connection also cancels the run.
292
+
293
+ ## 8. Confirmation-gated tools are **refused, not prompted**
294
+
295
+ Read this before you design a workflow around it.
296
+
297
+ Three of the agent's 55 tools write to disk or execute a command, and sit in the
298
+ base `TOOLS_REQUIRING_CONFIRMATION` set: **`write_file`**, **`edit_file`**, and
299
+ **`run_shell_command`**. Everything else — reading, indexing, querying, web
300
+ fetching, memory — runs without asking.
301
+
302
+ Over `/v1/gaia/query` there is **no way to collect an approval**, so the stream
303
+ does not prompt. When the agent reaches one of those tools it emits a
304
+ `needs_confirmation` event, and the server **immediately follows it with a
305
+ terminal `final`** whose `answer` says it stopped before running that action,
306
+ then cancels the run. There is no `confirm_url`, no resume, and no
307
+ `/query/{run_id}/confirm` endpoint — it is a deliberate deny-by-default stub, not
308
+ an oversight.
309
+
310
+ Concretely, your client sees:
311
+
312
+ ```
313
+ data: {"type":"needs_confirmation","run_id":"…","action":"write_file","summary":"Run 'write_file'?"}
314
+ data: {"type":"final","answer":"I stopped before running 'write_file' because it needs your explicit approval, and this streaming surface cannot collect that yet. …"}
315
+ ```
316
+
317
+ So: **`/query` cannot write files, edit files, or run shell commands.** If your
318
+ integration needs that, drive the agent from a surface that can prompt (the
319
+ terminal UI or the Agent UI), or perform the mutation yourself from your own code
320
+ and let the agent do the reading and reasoning. Treat `needs_confirmation` as an
321
+ early warning that the run is about to end, not as a question you can answer.
322
+
323
+ ## 9. File-access scope
324
+
325
+ The agent's file, document, and data tools are confined to a set of allowed
326
+ paths, and **the default is the user's home directory**. That is the honest scope
327
+ for a personal document agent, and it is still a real boundary — system
328
+ directories, program files, and other users' homes are refused, with the check
329
+ run against the *resolved* path so a symlink out of scope doesn't slip through.
330
+
331
+ **In 0.1.1 narrowing it is a construction-time setting only.** The packaged
332
+ sidecar exposes no flag or env var for `allowed_paths` (its CLI accepts only
333
+ `--host` and `--port`), so restricting the scope means embedding `GaiaAgent` in
334
+ your own Python process:
335
+
336
+ ```python
337
+ from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
338
+
339
+ agent = GaiaAgent(config=GaiaAgentConfig(allowed_paths=["/home/me/Documents"]))
340
+ ```
341
+
342
+ ## 10. Skills — opt-in, and empty in 0.1.1
343
+
344
+ The agent is built to host **Agent Skills** (short playbooks loaded into its own
345
+ prompt, grouped into named sets, one set active per launch), and its bundled
346
+ skill directory is the highest-precedence discovery root.
347
+
348
+ Loaded skills are **not** all resident every turn: each turn the agent embeds
349
+ the query against every loaded skill's description and renders only the
350
+ matching bodies in full — the rest collapse to a one-line menu entry, and the
351
+ model (or the user) re-activates one by calling `load_skill` on it again.
352
+ `GAIA_DYNAMIC_SKILLS=0` disables the per-turn selection (every loaded body
353
+ renders every turn); `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides the match
354
+ threshold. Manifest `skills:` entries are always-on and never collapse. If the
355
+ embedder is unavailable, selection disables itself for the session and every
356
+ body renders — capability is never silently lost to a failed match.
357
+
358
+ **Nothing ships enabled in 0.1.1.** The bundled skill library is empty, and
359
+ `gaia-agent.yaml` ships its `skills:` / `skill_sets:` / `default_skill_set:`
360
+ blocks **commented out** — following the email agent's precedent, because skill
361
+ bodies cost prompt tokens and no eval has measured that trade for this agent yet.
362
+ Re-enabling is uncommenting two blocks; no code change.
363
+
364
+ So today: no skill set loads, and there is nothing for `GAIA_SKILL_SET` to
365
+ select — leave it unset. Once a release declares sets, `GAIA_SKILL_SET` is the
366
+ selection channel for the packaged sidecar (its CLI has no `--skill-set` flag),
367
+ and an undeclared name raises naming the valid sets rather than falling back to a
368
+ default. Do not document or design around skills being on by default.
369
+
370
+ ## 11. Ports
371
+
372
+ | Service | Port |
373
+ |---|---|
374
+ | Agent sidecar | `8141` on `127.0.0.1` |
375
+ | GAIA daemon | assigned at start, recorded in `~/.gaia/host/instance.json` |
376
+
377
+ Port **4001 is reserved repo-wide**: `spawnSidecar` throws a `RangeError` and
378
+ `gaia serve --port 4001` exits 2. Both services bind loopback only — this agent
379
+ speaks for the user's documents and memory and has no business on a LAN
380
+ interface.
381
+
382
+ ## 12. Running in a server or long-lived app
383
+
384
+ - **`fetchAll` / `fetchBinary` are a build step**, not per request — network plus
385
+ a full SHA-256 hash of a large artifact. Run once; `resolveSidecarPath` /
386
+ `resolveTuiPath` at runtime.
387
+ - **Spawn once at boot** and hold the `Sidecar` handle for the process lifetime.
388
+ Never per request.
389
+ - **Low concurrency.** One local Lemonade model slot, so parallel queries
390
+ serialize. Cap inflight runs.
391
+ - **The package does not restart a crashed sidecar.** It reaps one; supervision
392
+ is the daemon's job (or yours).
393
+ - **`DEBUG=gaia`** puts download, spawn, and sidecar output on **stderr**. stdout
394
+ belongs to the TUI once exec'd, and to machine-readable JSON for `fetch` /
395
+ `version` — never write diagnostics there.
396
+
397
+ Every failure throws a typed error extending `GaiaError`, so
398
+ `instanceof GaiaError` catches any of ours: `IntegrityError`, `PlatformError`,
399
+ `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, `HttpError`.
400
+ There is no silent null.
401
+
402
+ ## Gotchas — read before debugging
403
+
404
+ - **`/health` green ≠ ready.** It never touches the model server. Use
405
+ `GET /v1/gaia/init` and branch on `.ready`; it returns 503 with a full body and
406
+ a `hint`, not an empty error.
407
+ - **A terminal `error` whose `detail` starts "Local Lemonade Server is not
408
+ reachable"** means Lemonade isn't running or isn't reachable — not a bug in
409
+ this package. Start it, or set `LEMONADE_BASE_URL`.
410
+ - **`needs_confirmation` is followed by a refusal and the run ends.** See §8.
411
+ `write_file` / `edit_file` / `run_shell_command` are unreachable over `/query`.
412
+ - **A placeholder hash in `binaries.lock.json` blocks the fetch before any
413
+ network call.** Between releases that is the *expected* state — it is not a
414
+ broken install, and there is no override.
415
+ - **No `linux-arm64` / `win32-arm64` sidecar.** The TUI has both. A `PlatformError`
416
+ on those hosts is the design, not a missing artifact.
417
+ - **There is no caller-auth token at 0.1.1.** Unlike `@amd-gaia/agent-email`, this
418
+ sidecar mints none and this package sends none — don't add an `Authorization`
419
+ header looking for one, and don't rely on its absence as a security boundary.
420
+ Loopback binding is what protects it.
421
+ - **`run_id` must be a UUID**, and unknown fields in the request body are a
422
+ **422** — the model forbids extras. Typos don't get ignored.
423
+ - **`gaia run` needs the *Python* `gaia` CLI on `PATH`** — the TUI shells out to
424
+ it to start the daemon. The package deliberately strips its own npm bin
425
+ directory from the child's `PATH` so the TUI doesn't re-invoke the npm shim; if
426
+ the Python CLI isn't installed, the daemon never comes up.
427
+ - **The TUI is installed as `gaia-tui`, never `gaia`** — the terminal-hub artifact
428
+ *is* called `gaia-<platform>`, and a file named `gaia` in a cache directory would
429
+ shadow the npm bin shim. The lock's `filename` and `executable` differ for that
430
+ reason. Don't rename it back.
431
+ - **The TUI comes from a lane this package doesn't publish.** If a fetch 404s on
432
+ the TUI but not the sidecar, the pinned `terminal-hub` version is the thing to
433
+ check — `gaia version` prints it and its base URL.
434
+ - **ESM-only.** `require("@amd-gaia/gaia")` fails; use `import` or dynamic
435
+ `import()`.
436
+
437
+ ## Verify the integration
438
+
439
+ Green path, in order:
440
+
441
+ ```bash
442
+ npx @amd-gaia/gaia version # per-component version + source URL + matrix
443
+ npx @amd-gaia/gaia fetch # JSON: one entry per binary with its sha256
444
+ npx @amd-gaia/gaia serve --port 8141
445
+ ```
446
+
447
+ Against a lock that still carries `PENDING-…` hashes, `fetch` is *expected* to
448
+ fail with a `PlatformError` before any download — that is the gate working, not a
449
+ broken install. Only a published release has real hashes.
450
+
451
+ Then, in another terminal:
452
+
453
+ ```bash
454
+ curl -s http://127.0.0.1:8141/health # {"status":"ok","service":"gaia-agent-gaia"}
455
+ curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.12","agentVersion":"0.1.1"}
456
+ curl -s http://127.0.0.1:8141/v1/gaia/init # 200 + "ready":true, or 503 + a "hint"
457
+ curl -N -X POST http://127.0.0.1:8141/v1/gaia/query \
458
+ -H 'content-type: application/json' \
459
+ -d '{"query":"What can you do?","run_id":"00000000-0000-4000-8000-000000000001","context":[],"can_answer_questions":false}'
460
+ ```
461
+
462
+ A healthy run streams `status` / `token` events and ends with one `final`. If
463
+ `/v1/gaia/init` is 503, fix what its `hint` names and retry — the rest of your
464
+ integration is fine.
465
+
466
+ **A 503 from `/query` itself is a different condition**: every retained
467
+ session slot is busy and none is idle enough to evict (SPEC §5.2). Do NOT
468
+ loop on `/v1/gaia/init` — it will report ready. Wait for a running turn to
469
+ finish (or close an idle session) and retry the same `/query`.
470
+
471
+ For the full wire contract, lock schema, exit codes, and timeout table, see
472
+ [`SPEC.md`](./SPEC.md). For the user-facing overview, see [`README.md`](./README.md)
473
+ and <https://amd-gaia.ai/docs/guides/gaia>.