@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/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.md +225 -6
- package/SKILL.md +597 -0
- package/SPEC.md +537 -0
- package/binaries.lock.json +78 -0
- package/dist/cli.d.ts +41 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +493 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch.d.ts +111 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +369 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +162 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +613 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +30 -0
- package/dist/logger.js.map +1 -0
- package/dist/platform.d.ts +100 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +202 -0
- package/dist/platform.js.map +1 -0
- package/dist/url.d.ts +4 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +22 -0
- package/dist/url.js.map +1 -0
- package/package.json +66 -15
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.
|