@amd-gaia/gaia 0.1.1 → 0.3.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 +711 -6
- package/README.md +64 -17
- package/SCORECARD.md +180 -0
- package/SKILL.md +335 -65
- package/SPEC.md +360 -20
- package/binaries.lock.json +25 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +160 -23
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/fetch.d.ts +5 -4
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +174 -22
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +24 -7
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +266 -44
- package/dist/lifecycle.js.map +1 -1
- package/package.json +2 -1
package/SKILL.md
CHANGED
|
@@ -23,7 +23,8 @@ Two ways in:
|
|
|
23
23
|
> playbook: how *you* wire this package into an app. The agent separately loads
|
|
24
24
|
> **Agent Skills** into its own prompt at runtime from
|
|
25
25
|
> `gaia_agent/skills/<name>/SKILL.md`. Same filename, different artifact —
|
|
26
|
-
> don't ship this one as an agent skill. See
|
|
26
|
+
> don't ship this one as an agent skill. See
|
|
27
|
+
> [Skills](#10-skills--one-always-on-the-rest-opt-in).
|
|
27
28
|
|
|
28
29
|
## 1. Install
|
|
29
30
|
|
|
@@ -51,7 +52,10 @@ built-in `fetch`. Use `import`, not `require`; from CommonJS use
|
|
|
51
52
|
each against the lock**.
|
|
52
53
|
4. Installs the sidecar into `~/.gaia/agents/gaia/` and the TUI into
|
|
53
54
|
`~/.gaia/npm-cache/gaia-<version>/`.
|
|
54
|
-
5.
|
|
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.
|
|
55
59
|
|
|
56
60
|
`run` deliberately does **not** spawn a sidecar. The TUI reaches agents through
|
|
57
61
|
the GAIA daemon's relay and never holds a sidecar token, and the daemon is what
|
|
@@ -123,9 +127,22 @@ for the version you have.
|
|
|
123
127
|
The agent thinks with a model hosted by **Lemonade Server**, which this package
|
|
124
128
|
does not install. Required before any query succeeds:
|
|
125
129
|
|
|
126
|
-
1. Lemonade **10.2.0 or newer**, running
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
1. Lemonade **10.2.0 or newer**, running. GAIA's daemon starts and supervises
|
|
131
|
+
one, so `gaia daemon start` is normally all that is needed (`gaia init`
|
|
132
|
+
also installs and starts it on first run). Qwen3.8 Flash Next needs
|
|
133
|
+
**v2026.39.1 or newer**, the version `gaia init` installs; older servers
|
|
134
|
+
cannot load it.
|
|
135
|
+
2. The machine's default chat model downloaded — run `gaia init`. It picks
|
|
136
|
+
`Qwen3.6-35B-A3B-GGUF` where a GPU holds it (~27 GB for models,
|
|
137
|
+
weights plus context cache — a 64 GB+ Strix Halo or a 32 GB GPU) and
|
|
138
|
+
`Gemma-4-E4B-it-GGUF` everywhere else, CPU-only PCs included, and records the pick as
|
|
139
|
+
`default_model` in `~/.gaia/config.json`. On 128 GB-class PCs the user may
|
|
140
|
+
switch to the multimodal `user.Qwen3.8-Flash-Next-GGUF` with `gaia config set
|
|
141
|
+
default_model`. `model.id` below names whichever this machine uses.
|
|
142
|
+
`gaia download` takes **no** model argument — naming one makes it exit 2. To
|
|
143
|
+
pull a single model instead, repeat the command `GET /v1/gaia/init` gives
|
|
144
|
+
you: it names the Lemonade client this machine actually has. Do not invent
|
|
145
|
+
one.
|
|
129
146
|
|
|
130
147
|
Do not guess — ask the sidecar. `GET /v1/gaia/init` is a read-only preflight
|
|
131
148
|
(it never pulls or loads) that probes Lemonade, compares its version to the
|
|
@@ -146,7 +163,9 @@ alone:
|
|
|
146
163
|
"min_version": "10.2.0", "compatible": null },
|
|
147
164
|
"model": { "id": "Gemma-4-E4B-it-GGUF", "present": false,
|
|
148
165
|
"loadable": null, "ctx_size": null },
|
|
149
|
-
|
|
166
|
+
// The manual fallback ("Otherwise: …") is resolved for the host's Lemonade
|
|
167
|
+
// install (tray app, macOS app, systemd service, or CLI) — render it verbatim.
|
|
168
|
+
"hint": "Local Lemonade Server is not reachable at …. GAIA starts it automatically — run `gaia daemon start` if the background service is not running. Otherwise: Start the Lemonade app from Applications, then retry. Or set LEMONADE_BASE_URL to a running server. See https://amd-gaia.ai/docs/guides/gaia."
|
|
150
169
|
}
|
|
151
170
|
```
|
|
152
171
|
|
|
@@ -187,6 +206,36 @@ await shutdown(proc); // tree-kill; auto-cleanup also reaps on exit
|
|
|
187
206
|
on `exit`, `SIGINT`/`SIGTERM`/`SIGHUP`, `uncaughtException`, and
|
|
188
207
|
`unhandledRejection`. A `SIGKILL` of *your* process is the one case nothing
|
|
189
208
|
in-process can catch.
|
|
209
|
+
- **Mint a caller token, or you are running the sidecar unauthenticated.** The
|
|
210
|
+
sidecar requires `Authorization: Bearer <token>` on every `/v1/gaia/*`
|
|
211
|
+
request, and skips the check only when neither token env var is set — dev
|
|
212
|
+
mode, which is what `startSidecar` gives you, because this package mints
|
|
213
|
+
nothing. Loopback binding is not the boundary: this agent has shell and file
|
|
214
|
+
tools. Mint your own and pass it through the `env` option (`StartOptions`
|
|
215
|
+
extends `SpawnOptions`, so it merges over `process.env`):
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { randomBytes } from "node:crypto";
|
|
219
|
+
const token = randomBytes(32).toString("hex");
|
|
220
|
+
const proc = await startSidecar({
|
|
221
|
+
binaryPath: sidecar.binaryPath,
|
|
222
|
+
env: { GAIA_GAIA_SIDECAR_TOKEN: token }, // or ..._TOKEN_FILE, a 0600 path
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Then send `authorization: "Bearer " + token` on every `/v1/gaia/*` call.
|
|
227
|
+
`/health`, `/version`, and `/v1/gaia/version` are exempt, so `startSidecar`'s
|
|
228
|
+
own health-and-version handshake works either way. **If you did not spawn the
|
|
229
|
+
sidecar** — you are talking to one the GAIA daemon started — the token is the
|
|
230
|
+
daemon's, delivered to the sidecar as `GAIA_GAIA_SIDECAR_TOKEN_FILE` (a `0600`
|
|
231
|
+
file path); read it from there, don't invent one. A 401 whose `detail` names
|
|
232
|
+
both env vars means you sent the wrong token or none.
|
|
233
|
+
|
|
234
|
+
The programs the agent starts (shell commands, MCP servers, CLI installs and
|
|
235
|
+
sign-ins, native hub agents, media tools) never inherit this token
|
|
236
|
+
or GAIA's other internal credentials. To withhold your own variables from
|
|
237
|
+
them too, list the names in `GAIA_CHILD_ENV_DENY` (comma or space separated);
|
|
238
|
+
`GAIA_NO_DOTENV=1` stops the sidecar loading `.env` files.
|
|
190
239
|
|
|
191
240
|
Or skip the code entirely and let the CLI own it:
|
|
192
241
|
|
|
@@ -198,7 +247,7 @@ curl http://127.0.0.1:8141/health
|
|
|
198
247
|
## 7. Call `POST /v1/gaia/query`
|
|
199
248
|
|
|
200
249
|
This is the whole agent surface. There is **no typed query client** in this
|
|
201
|
-
package — call it with plain `fetch`. Contract version **2.
|
|
250
|
+
package — call it with plain `fetch`. Contract version **2.17**; the stream is
|
|
202
251
|
`text/event-stream` terminated by **exactly one** `final` or `error`.
|
|
203
252
|
|
|
204
253
|
Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
|
|
@@ -208,10 +257,11 @@ Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
|
|
|
208
257
|
| `query` | yes | Non-empty. |
|
|
209
258
|
| `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
259
|
| `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. |
|
|
260
|
+
| `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. 1–128 characters from `A-Z a-z 0-9 . _ -` (a UUID works); anything else is a **400**. |
|
|
261
|
+
| `documents` | no | Contract ≥ 2.16. Absolute paths of the files the user attached to this conversation — send the whole set **every turn**. The agent indexes any it has not read yet before the turn runs, granting each exact file and not its folder. A relative path is a **422**. A missing file, a folder, or a refused secret is named as not indexed in the turn's `index_documents` step, and the turn still runs. |
|
|
212
262
|
| `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
|
|
214
|
-
| `provider` | no |
|
|
263
|
+
| `model` | no | Overrides the model id. On a retained `session_id` a different model is **switched in place** (contract ≥ 2.14), keeping the conversation and any loaded skills; a switch that fails is a **409** and leaves the session on its previous model. |
|
|
264
|
+
| `provider` | no | `"lemonade"` (the default backend) or `"claude"`, which sends the conversation to Anthropic's API instead of the local server. Anything else is a **400**. When you name it, `model` must agree: under `"claude"` it must be a `claude-*` id, under `"lemonade"` it must not be. A mismatch is a **400**, on a new session or an existing one. Omit it and the `model` decides instead — a `claude-*` id alone reaches Anthropic, any other id runs locally — so there is nothing to mismatch. On a retained `session_id`, naming a different provider switches the session to it in place, onto `model` or, without one, the provider's default model; omitting both leaves the session where it is. |
|
|
215
265
|
| `max_steps` | no | ≥ 1. |
|
|
216
266
|
|
|
217
267
|
```ts
|
|
@@ -220,7 +270,11 @@ import { randomUUID } from "node:crypto";
|
|
|
220
270
|
const runId = randomUUID();
|
|
221
271
|
const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
|
|
222
272
|
method: "POST",
|
|
223
|
-
headers: {
|
|
273
|
+
headers: {
|
|
274
|
+
"content-type": "application/json",
|
|
275
|
+
accept: "text/event-stream",
|
|
276
|
+
authorization: `Bearer ${token}`, // required unless the sidecar is in dev mode — §6
|
|
277
|
+
},
|
|
224
278
|
body: JSON.stringify({
|
|
225
279
|
query: "Summarize the PDFs in ~/Documents/reports",
|
|
226
280
|
run_id: runId,
|
|
@@ -263,11 +317,11 @@ The canonical event shapes, as emitted:
|
|
|
263
317
|
|
|
264
318
|
| Event | Shape |
|
|
265
319
|
|---|---|
|
|
266
|
-
| `status` | `{ type, message }` — progress and reasoning narration |
|
|
320
|
+
| `status` | `{ type, message, phase?, words?, chars? }` — progress and reasoning narration. `phase` (`loading_model`, `downloading_model`, `reading`, `reasoning`, `tool_call`) names what the model is doing; show it as the live line, not as narration |
|
|
267
321
|
| `token` | `{ type, delta }` — answer text to append |
|
|
268
322
|
| `tool_call` | `{ type, tool, args }` |
|
|
269
323
|
| `tool_result` | `{ type, tool, data, render? }` |
|
|
270
|
-
| `needs_confirmation` | `{ type, run_id, action, summary }` —
|
|
324
|
+
| `needs_confirmation` | `{ type, run_id, action, summary, confirm_id?, always_scope? }` — answer it, or it ends the run; see §8 |
|
|
271
325
|
| `needs_input` | `{ type, run_id, request_id, question, options[], allow_free_text, sensitive, respond_url, timeout_seconds? }` |
|
|
272
326
|
| `final` | `{ type, answer, usage? }` — terminal |
|
|
273
327
|
| `error` | `{ type, detail, status }` — terminal, surface `detail` verbatim |
|
|
@@ -281,7 +335,7 @@ Rules a client must respect:
|
|
|
281
335
|
guarantees one; a close without one means something broke on your side.
|
|
282
336
|
- **Answer `needs_input`, don't restart.** The run is parked on the *same*
|
|
283
337
|
stream. `POST /v1/gaia/query/{run_id}/respond` with
|
|
284
|
-
`{ request_id,
|
|
338
|
+
`{ request_id, value }` and keep reading the existing stream — a fresh
|
|
285
339
|
`/query` POST abandons the paused run. Unknown run → **404**; a `request_id` that
|
|
286
340
|
is no longer pending → **409** (both loud, never a silent drop). Render each
|
|
287
341
|
option's `description`, and mask the input when `sensitive` is set.
|
|
@@ -289,36 +343,116 @@ Rules a client must respect:
|
|
|
289
343
|
`{ run_id, cancelled }` — an unknown id reports `cancelled: false` with a
|
|
290
344
|
**200**, not a 404, because a cancel racing a normal completion is expected.
|
|
291
345
|
Dropping the HTTP connection also cancels the run.
|
|
346
|
+
- **Add to a running turn with `POST /v1/gaia/query/{run_id}/followup`**
|
|
347
|
+
(contract ≥ 2.15, body `{ text }`). The run is not interrupted and no second
|
|
348
|
+
turn starts: the agent folds the text in at its next step boundary and
|
|
349
|
+
answers it alongside what it was already doing. Unknown run → **404**, an
|
|
350
|
+
agent that cannot take one → **409**; both are loud, so hold the message
|
|
351
|
+
rather than telling the user it was sent. `/query` is stateless, so put a
|
|
352
|
+
delivered follow-up into the next turn's `context` yourself, between that
|
|
353
|
+
turn's question and its answer.
|
|
354
|
+
|
|
355
|
+
## 8. Over `/v1/gaia/query`, a gated tool asks — when you can answer
|
|
356
|
+
|
|
357
|
+
Read this before you design a workflow around it. This section is about the HTTP
|
|
358
|
+
surface — the agent's other transport can collect an approval; see SPEC §5.5.
|
|
359
|
+
|
|
360
|
+
Twelve of the agent's 95 tools mutate the machine and need explicit approval
|
|
361
|
+
before they run. Nine sit in the base `TOOLS_REQUIRING_CONFIRMATION` set —
|
|
362
|
+
**`write_file`**, **`edit_file`**, **`run_shell_command`**,
|
|
363
|
+
**`wait_for_condition`**, which re-runs a shell command until it succeeds,
|
|
364
|
+
**`execute_python_file`**, **`run_python`**, **`notify_desktop`**, which spawns
|
|
365
|
+
a PowerShell child on Windows to draw the notification, and **`install_cli`** /
|
|
366
|
+
**`sign_in_cli`**, which install software and sign a CLI in to the user's
|
|
367
|
+
account — and the flagship adds three of its own
|
|
368
|
+
(`CONFIRMATION_REQUIRED_TOOLS`): **`install_skill`**, **`capture_skill`**, and
|
|
369
|
+
**`remove_skill`**, because installing or capturing a skill writes third-party
|
|
370
|
+
content under `~/.gaia/skills` and removing one deletes it. A capture that does
|
|
371
|
+
land is additionally **code-inert**: its instructions load, but any
|
|
372
|
+
`tools.py`/scripts stay unregistered until a human runs
|
|
373
|
+
`gaia skill promote <name>` in a terminal. Everything else — reading,
|
|
374
|
+
indexing, querying, web fetching, memory, and the read-only `check_cli_setup`
|
|
375
|
+
— runs without asking. A write or edit inside the agent's own scratch
|
|
376
|
+
folder (`gaia-scratch-…` under the system temp folder) runs without asking too,
|
|
377
|
+
so running a script from it asks every time and carries no `always_scope`. So does a shell command run there or naming a path there, and a script handed a scratch path as an argument.
|
|
378
|
+
|
|
379
|
+
The eight live-browser tools are gated by a **hook**, not a set, so a name
|
|
380
|
+
alone does not tell you whether a call will ask. `browser_login` always asks.
|
|
381
|
+
`browser_click` / `browser_type` ask when the call acts inside a session the
|
|
382
|
+
run signed in to, or when the target element's label reads irreversible, and
|
|
383
|
+
run unasked on the open web. The hook **fails closed**: if the browser cannot
|
|
384
|
+
be asked what state it is in, the call is treated as authenticated and prompts.
|
|
385
|
+
Design a workflow around the approval path, not around a fixed tool list.
|
|
386
|
+
|
|
387
|
+
**Contract ≥ 2.14 can answer one.** Send a `session_id` and leave
|
|
388
|
+
`can_answer_questions` unset (or `true`). The stream emits `needs_confirmation`
|
|
389
|
+
carrying a `confirm_id` and then **stays open** while the agent waits:
|
|
292
390
|
|
|
293
|
-
|
|
391
|
+
```
|
|
392
|
+
data: {"type":"needs_confirmation","run_id":"…","action":"run_shell_command","summary":"python -m pytest -q tests/\nCan run any code, with your permissions.","confirm_id":"…","always_scope":"pytest (runs the test code it finds)","risk":"execute"}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Answer it on the same run, then keep reading the stream:
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
POST /v1/gaia/query/{run_id}/tool_decision
|
|
399
|
+
{ "decision": "allow" | "deny" | "always" | "timeout", "confirm_id": "…" }
|
|
400
|
+
```
|
|
294
401
|
|
|
295
|
-
|
|
402
|
+
`summary` is the command or call itself, shown once — your UI supplies the
|
|
403
|
+
question. For a shell command it can carry two more lines: `in <folder>` when
|
|
404
|
+
the command runs outside the workspace, and one plain sentence on what a `write`
|
|
405
|
+
or `execute` command can do. Show it as-is. A CLI a skill would grant (`git
|
|
406
|
+
commit`, `python script.py`) with no such skill loaded arrives here as a prompt,
|
|
407
|
+
not a refusal. So does a command GAIA's own checks don't cover (`git push`,
|
|
408
|
+
`python -c`, a path outside the project): its `summary` is the exact line the
|
|
409
|
+
shell will run, unshortened, then `in <folder>`, then a risk sentence, and it
|
|
410
|
+
carries no `always_scope` — offer allow-once and deny only. `allow` runs that
|
|
411
|
+
line once; the next call asks again. `risk` says what the call does (`read`, `write`, `execute`,
|
|
412
|
+
`destructive`); label by it rather than by tool name, since `run_shell_command`
|
|
413
|
+
is `pytest` on one call and `rm -rf` on the next. `always` grants the family
|
|
414
|
+
named in `always_scope` for the rest of the session (`pytest` covers every
|
|
415
|
+
spelling of a test run) — say so in your UI, because it stops asking; no
|
|
416
|
+
`always_scope` means don't offer it. If your prompt expires unanswered, send
|
|
417
|
+
`timeout`, not `deny`: it still refuses, but the agent tells the user the
|
|
418
|
+
request timed out instead of claiming they said no. Unknown run → **404**; a
|
|
419
|
+
prompt that is no longer pending → **409**; any other decision → **422**. All loud,
|
|
420
|
+
never a silent drop. Send `confirm_id`: without it a late answer resolves
|
|
421
|
+
whichever prompt replaced the one it was typed against.
|
|
422
|
+
|
|
423
|
+
To stop being asked for a whole session:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
POST /v1/gaia/sessions/{session_id}/bypass
|
|
427
|
+
{ "enabled": true }
|
|
428
|
+
```
|
|
296
429
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
fetching, memory — runs without asking.
|
|
430
|
+
It applies to the very next gated tool, including one in a turn already running,
|
|
431
|
+
and an unknown session is a **404** rather than a new one. A malformed
|
|
432
|
+
`session_id` is a **400**, same as on `/query`.
|
|
301
433
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
434
|
+
**That bypass stops the prompts; it does not open the shell.** The stdio
|
|
435
|
+
transport's `--bypass-permissions` does both — it runs gated tools unasked *and*
|
|
436
|
+
lifts the shell tool's guardrails (redirection and the other shell-only
|
|
437
|
+
operators, the read-only binary allowlist, the rate limit). That second half is
|
|
438
|
+
arbitrary code execution, appropriate for one local parent on a private pipe and
|
|
439
|
+
not for a bound socket, so it stays on stdio: HTTP sessions never lift the shell
|
|
440
|
+
gates, and the request body rejects unknown fields so a client cannot ask. See
|
|
441
|
+
SPEC §5.5.
|
|
309
442
|
|
|
310
|
-
|
|
443
|
+
**A run nobody can answer is still refused.** With `can_answer_questions: false`,
|
|
444
|
+
or with no `session_id`, the server emits `needs_confirmation`, follows it
|
|
445
|
+
immediately with a terminal `final` saying it stopped before running the action,
|
|
446
|
+
and cancels the run:
|
|
311
447
|
|
|
312
448
|
```
|
|
313
|
-
data: {"type":"needs_confirmation","run_id":"…","action":"write_file","summary":"Run 'write_file'?"}
|
|
314
449
|
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
450
|
```
|
|
316
451
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
and let the agent do the reading and reasoning.
|
|
321
|
-
early warning that the run is about to end, not as a question you can answer.
|
|
452
|
+
That is deny-by-default, not an oversight: parking a batch run on a prompt
|
|
453
|
+
nobody will ever see reads as a hang. So a one-shot integration cannot run those
|
|
454
|
+
nine tools — pass a `session_id` and answer, or perform the mutation from your
|
|
455
|
+
own code and let the agent do the reading and reasoning.
|
|
322
456
|
|
|
323
457
|
## 9. File-access scope
|
|
324
458
|
|
|
@@ -328,10 +462,31 @@ for a personal document agent, and it is still a real boundary — system
|
|
|
328
462
|
directories, program files, and other users' homes are refused, with the check
|
|
329
463
|
run against the *resolved* path so a symlink out of scope doesn't slip through.
|
|
330
464
|
|
|
331
|
-
**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
465
|
+
**Being in scope is not the same as being safe, and two denylists apply inside
|
|
466
|
+
it.** Reads refuse secrets — `.env`, `id_rsa`, `credentials.json`, `.netrc`,
|
|
467
|
+
`.pem`/`.key`, and everything under `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube` —
|
|
468
|
+
even in an allowed directory. Writes additionally refuse anything that executes
|
|
469
|
+
on its own: shell startup files, PowerShell profiles, `~/.config/autostart/*`,
|
|
470
|
+
systemd user units, `LaunchAgents`, and a repo's `.git/` (hooks and config). Both
|
|
471
|
+
come back as a structured error naming the file and the reason, so do not plan an
|
|
472
|
+
integration around reading a credential file or editing a shell rc — perform
|
|
473
|
+
those from your own code.
|
|
474
|
+
|
|
475
|
+
The agent also gets its own scratch directory for throwaway scripts and
|
|
476
|
+
intermediate files, so they stay out of the user's project. It is created per
|
|
477
|
+
agent under the system temp dir, deleted when the agent closes, and is the only
|
|
478
|
+
part of the temp dir the agent may use.
|
|
479
|
+
|
|
480
|
+
**To narrow it for a run, set `GAIA_ALLOWED_PATHS`** in the environment the
|
|
481
|
+
sidecar starts in — absolute folders joined with `;` on Windows and `:` elsewhere.
|
|
482
|
+
The scope becomes the overlap with the default; an entry outside it is ignored
|
|
483
|
+
with a warning and never widens it, and out-of-scope paths are refused without a
|
|
484
|
+
prompt, even under full access. The scratch directory stays writable. An empty,
|
|
485
|
+
relative or missing entry stops the sidecar at startup. It confines the agent's
|
|
486
|
+
tools, not code they run: a `run_python` script or shell command can still read
|
|
487
|
+
anything the OS user can, so isolating a run from files it must not see needs a
|
|
488
|
+
separate OS user, container or VM. Embedding `GaiaAgent` lets you pass an explicit
|
|
489
|
+
list instead (the variable still narrows it):
|
|
335
490
|
|
|
336
491
|
```python
|
|
337
492
|
from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
|
|
@@ -339,7 +494,7 @@ from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
|
|
|
339
494
|
agent = GaiaAgent(config=GaiaAgentConfig(allowed_paths=["/home/me/Documents"]))
|
|
340
495
|
```
|
|
341
496
|
|
|
342
|
-
## 10. Skills —
|
|
497
|
+
## 10. Skills — one always-on, the rest opt-in
|
|
343
498
|
|
|
344
499
|
The agent is built to host **Agent Skills** (short playbooks loaded into its own
|
|
345
500
|
prompt, grouped into named sets, one set active per launch), and its bundled
|
|
@@ -351,23 +506,75 @@ matching bodies in full — the rest collapse to a one-line menu entry, and the
|
|
|
351
506
|
model (or the user) re-activates one by calling `load_skill` on it again.
|
|
352
507
|
`GAIA_DYNAMIC_SKILLS=0` disables the per-turn selection (every loaded body
|
|
353
508
|
renders every turn); `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides the match
|
|
354
|
-
threshold. Manifest `skills:` entries are always-on and never collapse
|
|
509
|
+
threshold. Manifest `skills:` entries are always-on and never collapse, and a
|
|
510
|
+
turn with only always-on skills loaded embeds nothing. If the
|
|
355
511
|
embedder is unavailable, selection disables itself for the session and every
|
|
356
512
|
body renders — capability is never silently lost to a failed match.
|
|
357
513
|
|
|
358
|
-
**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
514
|
+
**One skill ships enabled: `gaia-voice`.** It is a manifest `skills:` entry, so
|
|
515
|
+
it is always on, always rendered in full, and paid on every LLM call of every
|
|
516
|
+
turn — budget for it. It is not a task recipe but the agent's honesty floor: do
|
|
517
|
+
not claim work you did not do, do not present empty output as a result, do not
|
|
518
|
+
substitute a near-miss and report success. Those failures corrupt an answer
|
|
519
|
+
whatever the task is, which is why it cannot live in an opt-in bundle. It
|
|
520
|
+
declares no tools, and its body measures 790 tokens (tiktoken `cl100k`).
|
|
521
|
+
|
|
522
|
+
**`document-extract` ships bundled but not enabled.** `gaia-voice` routes a
|
|
523
|
+
request for every item in a document to it, and it drives
|
|
524
|
+
`extract_document_items` and `save_extracted_items` so a long transcript yields
|
|
525
|
+
a complete, source-quoted inventory rather than a summary. A skill turns that
|
|
526
|
+
inventorying on only by listing `extract_document_items` in `tools_required`;
|
|
527
|
+
wording such as "find every call site" in another skill does not. Loading it
|
|
528
|
+
registers the tools at once, so `load_skill` never reports them missing; coverage
|
|
529
|
+
is enforced only when the request itself names a source document or asks for
|
|
530
|
+
every item, and a file the user named as a deliverable is never a source.
|
|
531
|
+
|
|
532
|
+
**No skill *set* loads.** `gaia-agent.yaml` ships its `skill_sets:` and
|
|
533
|
+
`default_skill_set:` blocks **commented out** — following the email agent's
|
|
534
|
+
precedent, because loading several skill bodies into every prompt costs tokens
|
|
535
|
+
and no eval has measured that trade for this agent yet. Re-enabling is
|
|
536
|
+
uncommenting two blocks; no code change.
|
|
537
|
+
|
|
538
|
+
So today there is nothing for `GAIA_SKILL_SET` to select — leave it unset; any
|
|
539
|
+
value stops the agent at startup (the sidecar exits non-zero before binding its
|
|
540
|
+
port). Once a release declares sets, `GAIA_SKILL_SET` is the selection channel
|
|
541
|
+
for the packaged sidecar (its CLI accepts only `--host` and `--port`), and an
|
|
542
|
+
undeclared name stops startup naming the valid sets rather than falling back to
|
|
543
|
+
a default. Beyond
|
|
544
|
+
`gaia-voice`, do not design around a skill being on by default.
|
|
545
|
+
|
|
546
|
+
## 11. The project map — two things it costs you
|
|
547
|
+
|
|
548
|
+
When the agent's working directory is a code repository, every task starts with
|
|
549
|
+
a **project map** in the system prompt: the root, the directory shape, the
|
|
550
|
+
likely entry points, which commands are installed, and the three platform
|
|
551
|
+
differences that change command syntax. It exists so the agent stops burning
|
|
552
|
+
round trips on "no such file" and "command not found".
|
|
553
|
+
|
|
554
|
+
Two consequences an integrator needs to plan for:
|
|
555
|
+
|
|
556
|
+
- **Up to 600 prompt tokens, every turn.** That is the enforced ceiling
|
|
557
|
+
(1.8% of the NPU profile's 32K window), not a typical value — budget it
|
|
558
|
+
alongside `gaia-voice`'s 790.
|
|
559
|
+
- **An embedding pass on the first semantic code search.** If the repo has no
|
|
560
|
+
[code index](https://amd-gaia.ai/docs/guides/code-index), the first
|
|
561
|
+
`search_code_index` builds it, then searches. On a large monorepo that one
|
|
562
|
+
search takes minutes of local embedding; a task that never searches pays
|
|
563
|
+
nothing. `GAIA_PROJECT_MAP_AUTO_INDEX=1` builds it in the background at task
|
|
564
|
+
start instead.
|
|
565
|
+
|
|
566
|
+
The sidecar's CLI accepts only `--host` and `--port`, so pointing the map at a
|
|
567
|
+
specific project means `GAIA_PROJECT_ROOT=/path/to/repo` in its environment, or
|
|
568
|
+
`GaiaAgentConfig(project_root=...)` when embedding. A directory that is neither
|
|
569
|
+
a VCS checkout nor holds a recognised manifest gets **no map** — that is the
|
|
570
|
+
designed answer, not a failure.
|
|
571
|
+
|
|
572
|
+
The same root check also decides whether the shell rides along: when it
|
|
573
|
+
resolves to a repository, `run_shell_command` is offered on every turn instead
|
|
574
|
+
of only when semantic selection guesses the request sounds like a shell
|
|
575
|
+
request. No repository, no change.
|
|
576
|
+
|
|
577
|
+
## 12. Ports
|
|
371
578
|
|
|
372
579
|
| Service | Port |
|
|
373
580
|
|---|---|
|
|
@@ -379,11 +586,14 @@ Port **4001 is reserved repo-wide**: `spawnSidecar` throws a `RangeError` and
|
|
|
379
586
|
speaks for the user's documents and memory and has no business on a LAN
|
|
380
587
|
interface.
|
|
381
588
|
|
|
382
|
-
##
|
|
589
|
+
## 13. Running in a server or long-lived app
|
|
383
590
|
|
|
384
591
|
- **`fetchAll` / `fetchBinary` are a build step**, not per request — network plus
|
|
385
|
-
a full SHA-256 hash of a large artifact. Run once
|
|
386
|
-
|
|
592
|
+
a full SHA-256 hash of a large artifact. Run once at install time.
|
|
593
|
+
- **`resolveSidecarPath` / `resolveTuiPath` are startup, not per request.** They
|
|
594
|
+
re-hash the binary against `binaries.lock.json` before handing back a path that
|
|
595
|
+
gets spawned, so they cost a full read of a large file. Resolve once and keep
|
|
596
|
+
the path. `{ verify: false }` skips the check for a binary you built yourself.
|
|
387
597
|
- **Spawn once at boot** and hold the `Sidecar` handle for the process lifetime.
|
|
388
598
|
Never per request.
|
|
389
599
|
- **Low concurrency.** One local Lemonade model slot, so parallel queries
|
|
@@ -408,22 +618,31 @@ There is no silent null.
|
|
|
408
618
|
reachable"** means Lemonade isn't running or isn't reachable — not a bug in
|
|
409
619
|
this package. Start it, or set `LEMONADE_BASE_URL`.
|
|
410
620
|
- **`needs_confirmation` is followed by a refusal and the run ends.** See §8.
|
|
411
|
-
|
|
621
|
+
The nine gated tools are unreachable **over `/query`** — the agent itself can
|
|
622
|
+
run them on a transport that can prompt (SPEC §5.5).
|
|
412
623
|
- **A placeholder hash in `binaries.lock.json` blocks the fetch before any
|
|
413
624
|
network call.** Between releases that is the *expected* state — it is not a
|
|
414
625
|
broken install, and there is no override.
|
|
415
626
|
- **No `linux-arm64` / `win32-arm64` sidecar.** The TUI has both. A `PlatformError`
|
|
416
627
|
on those hosts is the design, not a missing artifact.
|
|
417
|
-
- **
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
628
|
+
- **A `401` from `/v1/gaia/*` is the caller-auth token, not a bug.** The sidecar
|
|
629
|
+
requires `Authorization: Bearer <token>`; `/health`, `/version`, and
|
|
630
|
+
`/v1/gaia/version` are exempt, which is why a green health check sits happily
|
|
631
|
+
in front of a 401 on `/query`. It skips the check only in dev mode — neither
|
|
632
|
+
token env var set — which is what `spawnSidecar` and `gaia serve` produce,
|
|
633
|
+
because this package mints nothing. Do not treat loopback binding as the
|
|
634
|
+
boundary; mint a token and pass it (§6).
|
|
421
635
|
- **`run_id` must be a UUID**, and unknown fields in the request body are a
|
|
422
636
|
**422** — the model forbids extras. Typos don't get ignored.
|
|
423
637
|
- **`gaia run` needs the *Python* `gaia` CLI on `PATH`** — the TUI shells out to
|
|
424
|
-
it to start the daemon.
|
|
425
|
-
|
|
426
|
-
|
|
638
|
+
it to start the daemon. So the TUI doesn't re-invoke our own npm shim, the
|
|
639
|
+
child's `PATH` is rewritten: a directory holding nothing but our shim (an npx
|
|
640
|
+
temp dir) is dropped, and a **shared** bin directory is moved to the end
|
|
641
|
+
instead of removed, so the `python3` / `lemonade-server` / real `gaia` beside
|
|
642
|
+
it stay reachable. If the Python CLI isn't installed anywhere, the daemon never
|
|
643
|
+
comes up. It must also be **0.25.0+**: an older core's daemon starts fine but
|
|
644
|
+
cannot run this agent, which reads as a UI with a dead agent rather than as
|
|
645
|
+
a version problem.
|
|
427
646
|
- **The TUI is installed as `gaia-tui`, never `gaia`** — the terminal-hub artifact
|
|
428
647
|
*is* called `gaia-<platform>`, and a file named `gaia` in a cache directory would
|
|
429
648
|
shadow the npm bin shim. The lock's `filename` and `executable` differ for that
|
|
@@ -452,7 +671,7 @@ Then, in another terminal:
|
|
|
452
671
|
|
|
453
672
|
```bash
|
|
454
673
|
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.
|
|
674
|
+
curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.17","agentVersion":"0.1.1"}
|
|
456
675
|
curl -s http://127.0.0.1:8141/v1/gaia/init # 200 + "ready":true, or 503 + a "hint"
|
|
457
676
|
curl -N -X POST http://127.0.0.1:8141/v1/gaia/query \
|
|
458
677
|
-H 'content-type: application/json' \
|
|
@@ -463,11 +682,62 @@ A healthy run streams `status` / `token` events and ends with one `final`. If
|
|
|
463
682
|
`/v1/gaia/init` is 503, fix what its `hint` names and retry — the rest of your
|
|
464
683
|
integration is fine.
|
|
465
684
|
|
|
685
|
+
That `/query` call carries no `Authorization` header because `gaia serve` starts
|
|
686
|
+
the sidecar in dev mode. Against one started with a token, add
|
|
687
|
+
`-H "authorization: Bearer $TOKEN"` — a **401** here and a green `/health` is
|
|
688
|
+
that and nothing else (§6).
|
|
689
|
+
|
|
466
690
|
**A 503 from `/query` itself is a different condition**: every retained
|
|
467
691
|
session slot is busy and none is idle enough to evict (SPEC §5.2). Do NOT
|
|
468
692
|
loop on `/v1/gaia/init` — it will report ready. Wait for a running turn to
|
|
469
693
|
finish (or close an idle session) and retry the same `/query`.
|
|
470
694
|
|
|
695
|
+
**Three more refusals are yours to avoid**, each naming its fix in `detail`
|
|
696
|
+
(SPEC §5.2 has the reasoning):
|
|
697
|
+
|
|
698
|
+
- **409 — the `run_id` is still in flight.** You mint it, so mint a fresh UUID
|
|
699
|
+
per request; reusing one would leave the earlier run with no way to be
|
|
700
|
+
cancelled.
|
|
701
|
+
- **409 — the model switch itself failed** (a missing Claude credential, an
|
|
702
|
+
unknown local model). The session stays on its previous model, so this is a
|
|
703
|
+
failed request rather than a broken session; `detail` names the reason.
|
|
704
|
+
- **400 — the `Host` header is absent or empty.** The loopback check fails
|
|
705
|
+
closed, so omitting the header is refused rather than served. Send
|
|
706
|
+
`Host: 127.0.0.1:<port>`; every real HTTP client already does.
|
|
707
|
+
|
|
708
|
+
**After cancelling a turn, send the next `/query` on that session straight
|
|
709
|
+
away.** It is held until the stopped turn's model call returns (up to 240 s),
|
|
710
|
+
then runs. A session whose running turn was NOT cancelled still answers `409`.
|
|
711
|
+
|
|
471
712
|
For the full wire contract, lock schema, exit codes, and timeout table, see
|
|
472
713
|
[`SPEC.md`](./SPEC.md). For the user-facing overview, see [`README.md`](./README.md)
|
|
473
714
|
and <https://amd-gaia.ai/docs/guides/gaia>.
|
|
715
|
+
|
|
716
|
+
## TUI inference providers
|
|
717
|
+
|
|
718
|
+
The stdio TUI supports `/provider` for Local, Fireworks AI, and AMD LLM Gateway.
|
|
719
|
+
Keys are entered in a masked field and sent directly to local Lemonade's runtime
|
|
720
|
+
auth API, never as agent queries. `/model` lists supported discovered cloud and
|
|
721
|
+
downloaded local models, marking the configured default with `(default)`;
|
|
722
|
+
`/model fireworks.gemma-4-31b-it` selects Gemma 4 31B IT
|
|
723
|
+
when available. Cloud chat sends conversation history to the selected provider;
|
|
724
|
+
embeddings remain on Lemonade. The status event names the actual provider and
|
|
725
|
+
marks remote inference. This is a TUI/stdio capability, not an HTTP query command.
|
|
726
|
+
|
|
727
|
+
## Developer engineering mode
|
|
728
|
+
|
|
729
|
+
Available only when GAIA was started with `--developer-mode` (or
|
|
730
|
+
`GAIA_DEVELOPER_MODE=1`). If the engineering tools are missing, say that developer
|
|
731
|
+
mode is off; don't route around it with shell or file tools. When describing a
|
|
732
|
+
handoff, claim only what happened:
|
|
733
|
+
|
|
734
|
+
- Context was shared only after the user approved that exact snapshot in the prompt.
|
|
735
|
+
Approval can't be remembered, so each share or append asks again.
|
|
736
|
+
- Opening the coding app never creates or submits a task, and for Codex it doesn't
|
|
737
|
+
prefill a prompt either. Never say the context was "posted" or "sent" to Codex;
|
|
738
|
+
give the user the directory and the prompt to paste.
|
|
739
|
+
- Preview and test results come from the coding app. Report them as app-reported,
|
|
740
|
+
not as checks GAIA ran.
|
|
741
|
+
- Revoking stops future reads; it cannot recall data already delivered.
|
|
742
|
+
|
|
743
|
+
See the [usage guide](https://amd-gaia.ai/docs/guides/harness-engineering).
|