GAIA
v0.1.1 VerifiedThe flagship GAIA agent — conversation, document Q&A, data analysis, and web research, with persistent memory and add-on skills
- general
- chat
- rag
- memory
- skills
- research
Install
Python package · 116.4 MBgaia agent install gaia Recommended — installs into your GAIA app and registers the agent automatically.
pip install gaia-agent-gaia Python package from PyPI. Discovered via the gaia.agent entry-point group.
git clone https://github.com/amd/gaia.git Build from the GAIA repository — clone, then follow the agent README to install it.
A local model must be running first: gaia init then lemonade-server serve.
About GAIA
@amd-gaia/gaia
One command gets you a working GAIA:
npx @amd-gaia/gaia
That fetches the two binaries GAIA needs — the agent sidecar and the terminal UI — verifies both against a checksum manifest that ships inside this package, and drops you into the terminal UI. No Python to install, no repo to clone, no build step. Everything runs locally on your machine; nothing you type or index leaves it.
The terminal UI you get here is the published terminal-hub component — the exact same binary a full GAIA install runs as gaia tui, not a separate build. So however you arrive at the terminal UI, it behaves identically.
What it actually does
- Resolves your platform —
win32-x64,darwin-arm64,darwin-x64,linux-x64(pluslinux-arm64/win32-arm64for the terminal UI). - Reads
binaries.lock.json, the checksum manifest published with this exact package version. It records, per binary, which hub lane it comes from and what it must hash to. - Downloads and SHA-256 verifies both binaries. A hash that does not match is a hard failure — the download is deleted and the run stops. There is no "continue anyway" path and no unverified fallback.
- Launches the terminal UI, which brings up the GAIA daemon and the agent sidecar and hands you the chat. Its exit code becomes ours.
Requirements
- Node.js 18+ (for the built-in
fetch). - Lemonade Server running locally — it hosts the model the agent thinks with. GAIA tells you if it isn't up.
- The
gaiaPython CLI onPATHfor the daemon the terminal UI starts. Install it withcurl -fsSL https://amd-gaia.ai/install.sh | sh(Windows:irm https://amd-gaia.ai/install.ps1 | iex).
Supported platforms
The terminal UI is Go and cross-compiles everywhere. The agent sidecar is a frozen Python build, produced on the machine it targets, and has no arm64 Linux or arm64 Windows build. On those two platforms npx @amd-gaia/gaia stops with an error naming your platform and the ones that do work — it will not start a UI with no agent behind it.
| Platform key | Agent sidecar | Terminal UI |
|---|---|---|
win32-x64 | ✅ | ✅ |
darwin-arm64 | ✅ | ✅ |
darwin-x64 | ✅ | ✅ |
linux-x64 | ✅ | ✅ |
linux-arm64 | — | ✅ |
win32-arm64 | — | ✅ |
npx @amd-gaia/gaia version prints this matrix, plus the version and source URL of each binary, for the release you have installed.
Commands
gaia [run] [options] [-- <tui args>] Fetch + verify both binaries, then launch the TUI
gaia fetch [options] Download + verify only; print JSON and exit
gaia serve [options] Run the agent sidecar alone (REST API, no TUI)
gaia version Print the lock manifest and this host's platform
gaia help Show help
Anything after a bare -- goes to the terminal UI untouched:
npx @amd-gaia/gaia -- --debug
Common options:
| Flag | Meaning |
|---|---|
--base-url <url> | Override the download base URL from binaries.lock.json |
--cache-dir <dir> | Where to cache the terminal UI binary |
--sidecar-dir <dir> | Where to install the agent sidecar (default ~/.gaia/agents/gaia) |
--platform <key> | Fetch for another platform (fetch only) |
--force | Re-download even when a verified binary is already cached |
--port <n> | Sidecar bind port for serve (default 8141) |
Set DEBUG=gaia for download, spawn, and sidecar output on stderr. Diagnostics never touch stdout, which the terminal UI owns.
Where things land
| What | Path |
|---|---|
| Agent sidecar | ~/.gaia/agents/gaia/gaia-agent[.exe] |
| Terminal UI | ~/.gaia/npm-cache/gaia-<version>/gaia-tui[.exe] |
The sidecar goes into the GAIA daemon's own cache directory on purpose: the daemon is what spawns and supervises it, and it does its own SHA-256 check on the way. By putting an already-verified binary there we save a second download rather than racing one.
The terminal UI is installed as gaia-tui, never as gaia — a file named gaia in a cache directory would shadow the gaia shim npm puts on your PATH.
Ports
| Service | Port |
|---|---|
| Agent sidecar | 8141 on 127.0.0.1 |
| GAIA daemon | assigned at start, recorded in ~/.gaia/host/instance.json |
Port 4001 is reserved repo-wide and is refused with an error if you pass it. Both services bind loopback only — this agent speaks for your documents and memory and has no business on a LAN interface.
Running the sidecar on its own
gaia serve skips the terminal UI and gives you the REST surface directly, for integrating GAIA into your own app:
npx @amd-gaia/gaia serve --port 8141
curl http://127.0.0.1:8141/health
It waits for GET /health, checks the contract version, and tears the whole process tree down on Ctrl+C. See SPEC.md for the endpoints.
Programmatic use
import { randomUUID } from "node:crypto";
import { fetchAll, startSidecar, shutdown } from "@amd-gaia/gaia";
const { sidecar } = await fetchAll(); // both binaries, SHA-256 verified
const proc = await startSidecar({ binaryPath: sidecar.binaryPath });
const sessionId = randomUUID(); // reuse across the whole conversation, see below
const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
query: "summarize my notes",
run_id: randomUUID(),
session_id: sessionId,
context: [],
}),
});
await shutdown(proc);
/v1/gaia/query streams Server-Sent Events terminated by exactly one final or error. fetchAll() also returns the TUI's path if you would rather launch that.
Reuse the same session_id for every turn in a conversation. It is what lets a document you had it index, or a skill you had it load, survive to the next question — drop it (or mint a new one per call) and the agent still answers, but it forgets everything from the previous turn. See SPEC.md §5.2 for the retry and eviction behavior.
Every failure throws a typed error (IntegrityError, PlatformError, HealthTimeoutError, VersionMismatchError, BinaryNotFoundError) with a message that names what failed and what to do about it.
Where the binaries come from
The two binaries ship from two different places, and binaries.lock.json records a version and a source URL for each:
| Binary | Published as | Built by |
|---|---|---|
| Agent sidecar | the gaia agent, at this package's version | this package's release |
| Terminal UI | the terminal-hub component, at its own version | the core GAIA release |
The terminal UI is consumed, not rebuilt. It is byte-for-byte the gaia tui binary a core install ships, so there is no second copy that could lag behind or behave differently — which is the entire reason it is sourced this way.
Integrity
binaries.lock.json is the single source of truth for what gets downloaded and what it must hash to. The release pipeline regenerates it from the artifacts actually being served — the sidecars it just published, and the terminal-hub artifacts it downloaded and cross-checked against the hub's own recorded hashes.
Between releases the lock carries PENDING-replace-with-real-sha256 placeholders. A placeholder blocks the fetch outright — before any network call — so an unverifiable binary can never be downloaded, let alone executed. If you need to run against a locally built binary, build it yourself and point the lifecycle helpers at it directly; the fetcher will not be talked into it.
Links
- Guide: <https://amd-gaia.ai/docs/guides/gaia>
- Technical reference:
SPEC.md - Changes:
CHANGELOG.md - Issues: <https://github.com/amd/gaia/issues>
MIT licensed. © 2024-2026 Advanced Micro Devices, Inc.
@amd-gaia/gaia — technical reference
Version 0.1.1. Companion to README.md, which is the user-facing doc. This file specifies the wire and file formats the package depends on and the guarantees it makes.
1. Scope
@amd-gaia/gaia is a binary delivery and process-lifecycle package. It ships no agent logic. It owns:
- resolving the host platform to a lock key,
- downloading and SHA-256 verifying two published binaries — one from this package's own hub lane, one from the
terminal-hubcomponent's, - caching them in stable, versioned locations,
- launching the terminal UI, or the agent sidecar directly.
It does not own the GAIA daemon's lifecycle, the sidecar's supervision, or any agent behaviour. See §6 for where those boundaries sit.
2. binaries.lock.json
Ships inside the published package (files includes it). It is the single source of truth for what is downloaded and what it must hash to.
2.1 Schema (schemaVersion 3.0)
{
"schemaVersion": "3.0",
"agentVersion": "0.1.1",
"components": {
"sidecar": {
"componentVersion": "0.1.1",
"baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.1.1",
"platforms": { "<platformKey>": { /* entry */ } }
},
"tui": {
"componentVersion": "0.23.0",
"baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.23.0",
"platforms": { "<platformKey>": { /* entry */ } }
}
}
}
A component lane:
| Field | Type | Meaning |
|---|---|---|
componentVersion | string | That component's own released version |
baseUrl | string | Where that component's artifacts are served from |
platforms | object | Platform key → entry |
An entry:
| Field | Type | Meaning |
|---|---|---|
filename | string | Artifact name as published under its component's baseUrl |
executable | string | Basename it is written as on disk (with the platform extension) |
sha256 | string | Lowercase hex SHA-256 the download must match |
size | number | Informational. Not enforced |
Why per-component, and why 3.0. 2.0 had one top-level baseUrl shared by both components. That stopped being true once the TUI became the published terminal-hub component (§2.2): the two live in different hub lanes at different versions. A shared base URL cannot express that, so 2.x is rejected at load with an error naming the schema — never read as 3.x with a missing field.
The email agent's lock is schemaVersion 1.0, a flat binaries map, because it delivers one binary from one lane. A 1.x-shaped lock is likewise rejected.
2.2 Where each component comes from
| Component | Hub lane | Published by |
|---|---|---|
sidecar | agents/gaia/<agentVersion>/ | this package's release |
tui | agents/terminal-hub/<componentVersion>/ | the core GAIA release |
The TUI is consumed, not built here. It is the same tui/cmd/gaia binary the core release publishes as the terminal-hub component and a core install runs as gaia tui — so behaviour is identical by construction rather than by convention. Building a second copy under this package's lane would put the same bytes at a different version under a third naming convention, and the two would drift.
The consequence is a real release dependency: this package cannot ship until terminal-hub is published at the version its lock pins. The release fails loudly naming that version; it never falls back to building its own TUI.
The tui lane's origin is pinned in the lock rather than derived from the release pipeline's hub-origin variable, so the release verifies the exact URL the shipped lock will send users to. A release pointed at a non-default origin therefore moves the sidecar lane and not the TUI lane; re-point components.tui.baseUrl too if you need both.
2.3 Platform keys
` ${process.platform}-${process.arch} — e.g. win32-x64, darwin-arm64, linux-x64. This matches gaia.daemon.sidecars.platform.current_platform_key(), which normalises Python's sys.platform / platform.machine()` into the same namespace so the daemon and this package agree on a cache key.
The win32 ↔ win mapping. The terminal-hub lane names its Windows artifacts gaia-win-x64.exe / gaia-win-arm64.exe (Go's GOOS vocabulary), while our keys come from process.platform, which says win32. The lock keeps the win32-* key and carries the hub's spelling in filename:
| Platform key | tui filename |
|---|---|
win32-x64 | gaia-win-x64.exe |
win32-arm64 | gaia-win-arm64.exe |
darwin-x64 | gaia-darwin-x64 |
darwin-arm64 | gaia-darwin-arm64 |
linux-x64 | gaia-linux-x64 |
linux-arm64 | gaia-linux-arm64 |
The mapping therefore lives in data, not in a code path — nothing branches on the platform to build a name. TUI_ARTIFACT_NAMES (in src/platform.ts and in packaging/gen_binaries_lock.py) is the authority both the shipped lock and the release pipeline are checked against, because a wrong name here is not a build failure anywhere: it is a 404 on a user's first run.
2.4 Platform coverage
| Platform key | sidecar | tui |
|---|---|---|
win32-x64 | yes | yes |
darwin-arm64 | yes | yes |
darwin-x64 | yes | yes |
linux-x64 | yes | yes |
linux-arm64 | no | yes |
win32-arm64 | no | yes |
terminal-hub publishes all six. The sidecar is a PyInstaller freeze, produced on the platform it targets, and there is no arm64 Linux or arm64 Windows freeze. Resolving sidecar on those two keys raises PlatformError naming the platform and the supported set — it is not silently skipped, and the TUI is not launched without an agent behind it.
2.5 Placeholder hashes
Between releases every sha256 is PENDING-replace-with-real-sha256. The release pipeline regenerates the file with real hashes: for the sidecar, computed from the artifacts it just published; for the TUI, computed from the terminal-hub artifacts it downloaded and cross-checked against that lane's own recorded hashes (agents/terminal-hub/manifest.json, which the hub computes server-side at publish time). Both are then re-fetched from the public origin and re-hashed before the release is allowed to ship.
isPlaceholderSha() treats a value as a placeholder when it is all zeros or contains PENDING (case-insensitive). A placeholder blocks the fetch before any network call with a PlatformError. There is no flag, env var, or option that relaxes this.
3. Integrity
The SHA-256 check is the package's security boundary.
- Downloaded bytes are hashed in memory and compared against the lock before anything is written to the cache path.
- On mismatch:
IntegrityError, message naming expected vs actual, nothing left on disk. - Writes go to
<path>.download.<pid>and arerenamed into place, so a crash mid-write never leaves a partial file that a later run treats as verified. - A cache hit requires re-hashing the on-disk file and matching the lock. A cached file whose bytes drifted is re-downloaded, not reused.
- POSIX installs
chmod 0o755after the rename.
There is no unverified path, no "warn and continue", and no way to disable the check.
4. Filesystem layout
| Component | Default directory | Executable |
|---|---|---|
sidecar | ~/.gaia/agents/gaia/ | gaia-agent[.exe] |
tui | ~/.gaia/npm-cache/gaia-<agentVersion>/ | gaia-tui[.exe] |
The sidecar directory is a cross-repo contract with gaia.daemon.sidecars.fetch.default_cache_dir("gaia"). The daemon spawns and supervises the sidecar and does its own SHA-256 check on the binary it finds there; installing an already-verified binary at that path turns the daemon's fetch into a cache hit instead of a second multi-hundred-megabyte download.
The TUI directory is keyed by agentVersion — this package's version, not the component's — so a bump of either never reuses the previous release's executable.
The TUI executable is renamed to gaia-tui on install, never gaia: the terminal-hub artifact is gaia-<platform> and npm installs a gaia bin shim on PATH, so keeping the hub's name would shadow the shim. filename (what is downloaded) and executable (what is written) are separate fields for exactly this reason, and test/lock.test.ts asserts it for every platform.
Both defaults are overridable (--sidecar-dir, --cache-dir).
5. Sidecar HTTP surface
Served by gaia_agent.server. Bound to 127.0.0.1 only.
| Property | Value |
|---|---|
| Default port | 8141 (DEFAULT_PORT in server.py) |
| Reserved port | 4001 — refused with a RangeError |
| Contract version | API_VERSION = "2.12" |
| Agent id / prefix | gaia → /v1/gaia/... |
5.1 Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /health | Liveness. { "status": "ok", "service": "gaia-agent-gaia" } |
GET | /version | Contract probe. { "apiVersion", "agentVersion" } |
GET | /v1/gaia/version | The TUI's negotiation probe |
GET | /v1/gaia/init | Readiness detail (Lemonade, model, connectors) |
POST | /v1/gaia/query | The streaming surface (text/event-stream) |
POST | /v1/gaia/query/{run_id}/cancel | Cancel a run by its host-minted run_id |
POST | /v1/gaia/query/{run_id}/respond | Answer a mid-run question |
/health is liveness only. It says nothing about whether Lemonade is up or a model is loaded — /v1/gaia/init answers that.
5.2 session_id and agent retention
POST /v1/gaia/query accepts an optional session_id in the request body. Pass it on every call in a conversation, and reuse the same value for the whole conversation. Contract ≥ 2.12 resolves session_id to a retained agent instead of a throwaway built fresh per call — indexed documents and load_skill state only survive between turns when the same session_id threads them together.
A retained skill stays loaded but its body is not necessarily in the prompt every turn: the agent selects per turn which loaded bodies match the query and collapses the rest to a one-line menu entry (re-activated by calling load_skill again). GAIA_DYNAMIC_SKILLS=0 disables the selection; GAIA_DYNAMIC_SKILLS_TAU=<float> overrides its threshold; an embedder outage disables it for the session and every body renders. Omitting it is a valid, explicit one-shot: nothing persists past that single turn, and the agent is not told otherwise.
A second /query for a session_id that already has a turn in flight gets 409 Conflict — cancel the running turn or wait for it, then retry. A /query that needs a new session while every retained slot is busy and none is idle enough to evict gets 503 with the reason in detail — a temporary, retryable condition, not a bug: wait for a turn to finish (or close an idle session) and retry. A session_id can also be evicted from the retention table under an idle timeout or an LRU cap on concurrent sessions; a /query that lands on an evicted id gets a fresh agent (the conversation is not blocked) but the response stream's first event is a {"type":"status","status":"warning",...} telling the caller that per-turn state — most visibly any loaded skill — did not survive and should be reloaded.
5.3 Version gate
checkVersion() reads /version and compares the major of apiVersion against this package's API_VERSION. A differing major is a breaking contract change and raises VersionMismatchError. A higher minor with the same major is a backward-compatible addition and is accepted.
5.4 No caller-auth token
The email sidecar authenticates callers with a per-session bearer minted into GAIA_EMAIL_SIDECAR_TOKEN. gaia_agent has no equivalent at 0.1.1, so this package mints and sends nothing. When the sidecar grows one, it lands here as a spawn-time env var and a request header — a change to this section, not a new subsystem.
6. Process ownership
Two distinct paths, deliberately:
6.1 gaia run — the normal path
run fetches, verifies, installs, and execs the TUI. It does not spawn a sidecar.
That is not an omission. The TUI reaches agents through the GAIA daemon's relay (/v1/<agent>/*) and holds only the daemon client token, never a sidecar bearer — tui/internal/daemon states this as an invariant. The TUI start-or-attaches the daemon under an advisory lock, and the daemon spawns and supervises the sidecar from ~/.gaia/agents/gaia/. A sidecar spawned here would be a second process the TUI never talks to, competing for port 8141.
So run's contribution to the sidecar is putting a verified binary where the daemon looks. Daemon and sidecar lifecycle belong to the daemon.
6.2 gaia serve — the direct path
serve fetches the sidecar and spawns it itself, for integrators who want the REST surface without a daemon or a UI. This path owns the process fully: spawn → waitForHealth → checkVersion → run until interrupted → tree-kill.
6.3 Tree-kill
The sidecar is a PyInstaller one-file build: it unpacks and spawns a child uvicorn process that child.kill() on the parent does not reap, leaving port 8141 bound. Every teardown kills the whole tree:
- POSIX — spawned
detachedso the child leads its own process group;process.kill(-pid, SIGTERM), escalating toSIGKILLafter the timeout. - Windows —
taskkill /PID <pid> /T /F.
autoCleanup (default true) also reaps on exit, SIGINT, SIGTERM, SIGHUP, uncaughtException, and unhandledRejection. A SIGKILL of the parent is the one case no in-process handler can catch.
startSidecar() shuts the sidecar down before rethrowing on any failure, so a failed start never leaks a process.
7. Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | A typed failure: IntegrityError, PlatformError, HealthTimeoutError, VersionMismatchError, BinaryNotFoundError, or an unexpected error |
2 | Usage error: unknown command, unknown --component, invalid --port |
| other | From run: the TUI's own exit code, propagated verbatim |
A TUI killed by a signal propagates as 128 + signum (the shell convention), so a Ctrl+C is distinguishable from a clean 0.
8. Errors
All extend GaiaError, so instanceof GaiaError catches any of ours.
| Class | Raised when |
|---|---|
IntegrityError | A download's SHA-256 does not match the lock |
PlatformError | Unsupported platform, missing/incomplete entry, placeholder hash, malformed lock |
HealthTimeoutError | The sidecar did not report healthy within the timeout |
VersionMismatchError | apiVersion major differs from this package's |
BinaryNotFoundError | A binary is absent from disk when spawning |
HttpError | A non-2xx from a sidecar probe |
Per the repo's no-silent-fallbacks rule, every message names what failed, what to do, and where to look next.
9. Timeouts
| Operation | Default | Why |
|---|---|---|
| Download (per binary) | 300000ms | The frozen sidecar is a large artifact |
| Health wait | 60000ms | A cold one-file build unpacks before it binds |
| Health poll interval | 250ms | |
/health probe | 1000ms | |
/version probe | 5000ms | |
| Shutdown grace | 5000ms | Then SIGKILL / forced |
10. Public API
Exported from the package root — see src/index.ts.
Fetch: fetchAll, fetchBinary, verifySha256, fileSha256, binaryExists, defaultCacheDir, daemonSidecarCacheDir.
Lifecycle: spawnSidecar, startSidecar, waitForHealth, checkVersion, health, version, shutdown, runTui, resolveSidecarPath, resolveTuiPath, sidecarExecutableName, tuiExecutableName.
Platform: currentPlatformKey, loadLock, resolveEntry, componentLock, componentBaseUrl, platformsFor, defaultLockPath, isPlaceholderSha, COMPONENTS, SCHEMA_MAJOR, TUI_ARTIFACT_NAMES, SUPPORTED_SIDECAR_PLATFORMS, SUPPORTED_TUI_PLATFORMS, SUPPORTED_PLATFORMS.
Constants: AGENT_ID, API_VERSION, DEFAULT_HOST, DEFAULT_PORT, RESERVED_PORT.
11. Logging
DEBUG=gaia (or DEBUG=*) enables debug output. Everything goes to stderr — stdout belongs to the TUI once it is exec'd, and to machine-readable JSON for fetch / version.
name: integrate-gaia 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.
Integrating @amd-gaia/gaia
@amd-gaia/gaia delivers two binaries and owns their process lifecycle: the frozen agent sidecar (gaia-agent) and the Go terminal UI (gaia-tui). It ships no agent logic of its own, and it builds neither binary at install time — both are published artifacts it downloads and verifies. The terminal UI is the published terminal-hub component, the same binary a full GAIA install runs as gaia tui, so its behaviour cannot differ from that one. Everything runs on the local machine against a local model server — nothing you type or index leaves it.
Two ways in:
npx @amd-gaia/gaia— fetch, verify, launch the terminal UI. What a human runs.- The programmatic exports — fetch, spawn the sidecar, drive
POST /v1/gaia/queryyourself. What you use when embedding GAIA in an app.
This file is NOT one of the agent's own skills. It is the integration playbook: how you wire this package into an app. The agent separately loads Agent Skills into its own prompt at runtime from gaia_agent/skills/<name>/SKILL.md. Same filename, different artifact — don't ship this one as an agent skill. See Skills.
1. Install
npx @amd-gaia/gaia # no install step; fetches what it needs on first run
npm install @amd-gaia/gaia # when you want the programmatic exports
The package is ESM-only ("type": "module") and needs Node 18+ for the built-in fetch. Use import, not require; from CommonJS use await import("@amd-gaia/gaia").
@amd-gaia/gaiapublishes with this release — it is not on npm yet. Until the release tag lands,npx @amd-gaia/gaiawill not resolve. Run the agent from a source checkout in the meantime (see the guide).
2. What npx @amd-gaia/gaia actually does
- Resolves the host platform key (`
${process.platform}-${process.arch}`). - Reads
binaries.lock.json, the checksum manifest published with this exact package version. Each component records its own hub lane, version, artifact name and hash — they do not share a base URL. - Downloads both binaries, each from its own lane, and SHA-256 verifies each against the lock.
- Installs the sidecar into
~/.gaia/agents/gaia/and the TUI into~/.gaia/npm-cache/gaia-<version>/. - Execs the TUI, whose exit code becomes ours.
run deliberately does not spawn a sidecar. The TUI reaches agents through the GAIA daemon's relay and never holds a sidecar token, and the daemon is what spawns and supervises the sidecar — from exactly the directory step 4 wrote to. A second sidecar started here would only fight the daemon's own for port 8141. Use gaia serve when you want to own the process.
Other commands: gaia fetch (download + verify, print JSON, exit), gaia serve (sidecar alone), gaia version (per-component version, source URL, and platform matrix). Anything after a bare -- goes to the TUI verbatim.
Where each binary comes from
| Component | Hub lane | Artifact names |
|---|---|---|
sidecar | agents/gaia/<agentVersion>/ | gaia-agent-<platformKey>[.exe] |
tui | agents/terminal-hub/<componentVersion>/ | gaia-<goPlatform>[.exe] |
Two things follow from that, and both bite if you assume otherwise:
- The two components version independently.
lock.agentVersionis this package's version;components.tui.componentVersionis the terminal-hub release it consumes. They will not match. - The TUI's artifact names use
win-x64/win-arm64, notwin32-*. Platform keys stay in Node's namespace (process.platformsayswin32); only thefilenamecrosses over. Never build a TUI URL by interpolating a platform key — readfilenamefrom the lock entry.
3. The integrity gate — it will stop you, by design
The SHA-256 check is this package's security boundary, and there is no flag, env var, or option that relaxes it.
- Bytes are hashed in memory and compared before anything is written to the cache path. A mismatch raises
IntegrityErrornaming expected vs actual and leaves nothing on disk. - A placeholder hash blocks the fetch before any network call — between releases every
sha256in the lock isPENDING-replace-with-real-sha256, and a value that is all zeros or containsPENDING(case-insensitive) is treated as a placeholder. You get aPlatformError, not a download. - A cache hit re-hashes the on-disk file. A cached binary whose bytes drifted is re-downloaded, not reused.
If you need to run against a locally built binary, build it and point startSidecar / runTui at it directly. The fetcher will not be talked into it.
4. Platform coverage — the sidecar has two gaps
terminal-hub publishes the TUI for all six targets. The sidecar is a PyInstaller freeze built on the platform it targets, and there is no arm64 Linux and no arm64 Windows sidecar build.
| Platform key | Sidecar | TUI |
|---|---|---|
win32-x64 | yes | yes |
darwin-arm64 | yes | yes |
darwin-x64 | yes | yes |
linux-x64 | yes | yes |
linux-arm64 | no | yes |
win32-arm64 | no | yes |
Resolving the sidecar on those two keys raises PlatformError naming the platform and the supported set. It is not silently skipped, and the TUI is never launched with no agent behind it. npx @amd-gaia/gaia version prints the matrix for the version you have.
5. Prerequisite — a local Lemonade server
The agent thinks with a model hosted by Lemonade Server, which this package does not install. Required before any query succeeds:
- Lemonade 10.2.0 or newer, running (
lemonade-server serve). - The default model downloaded (
gaia download Gemma-4-E4B-it-GGUF, orgaia init).
Do not guess — ask the sidecar. GET /v1/gaia/init is a read-only preflight (it never pulls or loads) that probes Lemonade, compares its version to the floor, and checks the model is present:
curl -s http://127.0.0.1:8141/v1/gaia/init
It answers 200 when ready and 503 when not, with the same body shape either way — so branch on .ready and render .hint, never on the status code alone:
{
"ready": false,
"lemonade": { "reachable": false, "base_url": "…", "version": null,
"min_version": "10.2.0", "compatible": null },
"model": { "id": "Gemma-4-E4B-it-GGUF", "present": false,
"loadable": null, "ctx_size": null },
"hint": "Local Lemonade Server is not reachable at … — start it with `lemonade-server serve`, or set LEMONADE_BASE_URL to a running server."
}
lemonade.compatible: null is indeterminate, not a pass — the version could not be parsed. Render it as unknown.
GET /health is liveness only. A green /health means the REST surface is up; it says nothing about whether a query will work.
6. Start the sidecar
import { fetchAll, startSidecar, shutdown } from "@amd-gaia/gaia";
// Fetch + SHA-256 verify both binaries. Build step, not per request.
const { sidecar, tui } = await fetchAll();
// Spawn -> poll /health -> check the contract apiVersion, in one call.
const proc = await startSidecar({ binaryPath: sidecar.binaryPath }); // port 8141
// ... drive proc.baseUrl ...
await shutdown(proc); // tree-kill; auto-cleanup also reaps on exit
fetchAll(opts?)returns{ sidecar, tui, lock }. Each result carriesbinaryPath,platformKey,sha256,cached,url. For one component usefetchBinary({ component: "sidecar" | "tui", outDir }).startSidecarthrows if the binary can't start, never becomes healthy (HealthTimeoutError, 60 s default — a cold one-file build unpacks first), or reports anapiVersionwhose major differs from this package's (VersionMismatchError). On any failure it shuts the sidecar down before rethrowing, so a failed start never leaks a process.- Tree-kill is not optional. The frozen sidecar spawns a child uvicorn process that
child.kill()on the parent does not reap, which leaves port 8141 bound.shutdownkills the group (POSIXSIGTERMto-pid, escalating toSIGKILL; Windowstaskkill /T /F).autoCleanup(defaulttrue) also reaps onexit,SIGINT/SIGTERM/SIGHUP,uncaughtException, andunhandledRejection. ASIGKILLof your process is the one case nothing in-process can catch.
Or skip the code entirely and let the CLI own it:
npx @amd-gaia/gaia serve --port 8141
curl http://127.0.0.1:8141/health
7. Call POST /v1/gaia/query
This is the whole agent surface. There is no typed query client in this package — call it with plain fetch. Contract version 2.12; the stream is text/event-stream terminated by exactly one final or error.
Request body (extra: "forbid" — an unknown field is a 422, not ignored):
| Field | Required | Notes |
|---|---|---|
query | yes | Non-empty. |
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. |
context | yes | Transcript slice, pushed in the body — may be [], never absent. Each item { role, content }; role ∈ user / assistant / system / tool. |
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. |
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. |
model | no | Overrides the model id for this run. |
provider | no | Local inference only — anything but "lemonade" is a 400. |
max_steps | no | ≥ 1. |
import { randomUUID } from "node:crypto";
const runId = randomUUID();
const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
method: "POST",
headers: { "content-type": "application/json", accept: "text/event-stream" },
body: JSON.stringify({
query: "Summarize the PDFs in ~/Documents/reports",
run_id: runId,
context: [],
session_id: "s1",
can_answer_questions: false,
}),
});
const reader = res.body!.getReader();
const dec = new TextDecoder();
let buf = "";
outer: for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i: number;
while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i);
buf = buf.slice(i + 2);
const line = frame.split("\n").find((l) => l.startsWith("data: "));
if (!line) continue; // ":" frames are keepalive comments
const ev = JSON.parse(line.slice(6));
switch (ev.type) {
case "status": console.log(ev.message); break;
case "token": process.stdout.write(ev.delta); break;
case "tool_call": console.log(`→ ${ev.tool}`, ev.args); break;
case "tool_result": console.log(`← ${ev.tool}`, ev.data); break;
case "needs_confirmation": break; // see §8 — a refusal follows
case "needs_input": /* answer it — see below */ break;
case "final": console.log(ev.answer); break outer; // terminal
case "error": console.error(ev.detail); break outer; // terminal
default: console.warn("unsupported event", ev); // future additive type
}
}
}
The canonical event shapes, as emitted:
| Event | Shape |
|---|---|
status | { type, message } — progress and reasoning narration |
token | { type, delta } — answer text to append |
tool_call | { type, tool, args } |
tool_result | { type, tool, data, render? } |
needs_confirmation | { type, run_id, action, summary } — no confirm_url; see §8 |
needs_input | { type, run_id, request_id, question, options[], allow_free_text, sensitive, respond_url, timeout_seconds? } |
final | { type, answer, usage? } — terminal |
error | { type, detail, status } — terminal, surface detail verbatim |
Rules a client must respect:
- An idle run emits
: keepaliveSSE comments every 10 s. Skip lines that aren'tdata:and reset your read-idle timer on them — a long tool call is not a dead stream. - Never treat stream close without a terminal event as success. The server guarantees one; a close without one means something broke on your side.
- Answer
needs_input, don't restart. The run is parked on the same stream.POST /v1/gaia/query/{run_id}/respondwith{ request_id, response }and keep reading the existing stream — a fresh/queryPOST abandons the paused run. Unknown run → 404; arequest_idthat is no longer pending → 409 (both loud, never a silent drop). Render each option'sdescription, and mask the input whensensitiveis set. - Cancel with
POST /v1/gaia/query/{run_id}/cancel. It returns{ run_id, cancelled }— an unknown id reportscancelled: falsewith a 200, not a 404, because a cancel racing a normal completion is expected. Dropping the HTTP connection also cancels the run.
8. Confirmation-gated tools are refused, not prompted
Read this before you design a workflow around it.
Three of the agent's 55 tools write to disk or execute a command, and sit in the base TOOLS_REQUIRING_CONFIRMATION set: write_file, edit_file, and run_shell_command. Everything else — reading, indexing, querying, web fetching, memory — runs without asking.
Over /v1/gaia/query there is no way to collect an approval, so the stream does not prompt. When the agent reaches one of those tools it emits a needs_confirmation event, and the server immediately follows it with a terminal final whose answer says it stopped before running that action, then cancels the run. There is no confirm_url, no resume, and no /query/{run_id}/confirm endpoint — it is a deliberate deny-by-default stub, not an oversight.
Concretely, your client sees:
data: {"type":"needs_confirmation","run_id":"…","action":"write_file","summary":"Run 'write_file'?"}
data: {"type":"final","answer":"I stopped before running 'write_file' because it needs your explicit approval, and this streaming surface cannot collect that yet. …"}
So: /query cannot write files, edit files, or run shell commands. If your integration needs that, drive the agent from a surface that can prompt (the terminal UI or the Agent UI), or perform the mutation yourself from your own code and let the agent do the reading and reasoning. Treat needs_confirmation as an early warning that the run is about to end, not as a question you can answer.
9. File-access scope
The agent's file, document, and data tools are confined to a set of allowed paths, and the default is the user's home directory. That is the honest scope for a personal document agent, and it is still a real boundary — system directories, program files, and other users' homes are refused, with the check run against the resolved path so a symlink out of scope doesn't slip through.
In 0.1.1 narrowing it is a construction-time setting only. The packaged sidecar exposes no flag or env var for allowed_paths (its CLI accepts only --host and --port), so restricting the scope means embedding GaiaAgent in your own Python process:
from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
agent = GaiaAgent(config=GaiaAgentConfig(allowed_paths=["/home/me/Documents"]))
10. Skills — opt-in, and empty in 0.1.1
The agent is built to host Agent Skills (short playbooks loaded into its own prompt, grouped into named sets, one set active per launch), and its bundled skill directory is the highest-precedence discovery root.
Loaded skills are not all resident every turn: each turn the agent embeds the query against every loaded skill's description and renders only the matching bodies in full — the rest collapse to a one-line menu entry, and the model (or the user) re-activates one by calling load_skill on it again. GAIA_DYNAMIC_SKILLS=0 disables the per-turn selection (every loaded body renders every turn); GAIA_DYNAMIC_SKILLS_TAU=<float> overrides the match threshold. Manifest skills: entries are always-on and never collapse. If the embedder is unavailable, selection disables itself for the session and every body renders — capability is never silently lost to a failed match.
Nothing ships enabled in 0.1.1. The bundled skill library is empty, and gaia-agent.yaml ships its skills: / skill_sets: / default_skill_set: blocks commented out — following the email agent's precedent, because skill bodies cost prompt tokens and no eval has measured that trade for this agent yet. Re-enabling is uncommenting two blocks; no code change.
So today: no skill set loads, and there is nothing for GAIA_SKILL_SET to select — leave it unset. Once a release declares sets, GAIA_SKILL_SET is the selection channel for the packaged sidecar (its CLI has no --skill-set flag), and an undeclared name raises naming the valid sets rather than falling back to a default. Do not document or design around skills being on by default.
11. Ports
| Service | Port |
|---|---|
| Agent sidecar | 8141 on 127.0.0.1 |
| GAIA daemon | assigned at start, recorded in ~/.gaia/host/instance.json |
Port 4001 is reserved repo-wide: spawnSidecar throws a RangeError and gaia serve --port 4001 exits 2. Both services bind loopback only — this agent speaks for the user's documents and memory and has no business on a LAN interface.
12. Running in a server or long-lived app
fetchAll/fetchBinaryare a build step, not per request — network plus a full SHA-256 hash of a large artifact. Run once;resolveSidecarPath/resolveTuiPathat runtime.- Spawn once at boot and hold the
Sidecarhandle for the process lifetime. Never per request. - Low concurrency. One local Lemonade model slot, so parallel queries serialize. Cap inflight runs.
- The package does not restart a crashed sidecar. It reaps one; supervision is the daemon's job (or yours).
DEBUG=gaiaputs download, spawn, and sidecar output on stderr. stdout belongs to the TUI once exec'd, and to machine-readable JSON forfetch/version— never write diagnostics there.
Every failure throws a typed error extending GaiaError, so instanceof GaiaError catches any of ours: IntegrityError, PlatformError, HealthTimeoutError, VersionMismatchError, BinaryNotFoundError, HttpError. There is no silent null.
Gotchas — read before debugging
/healthgreen ≠ ready. It never touches the model server. UseGET /v1/gaia/initand branch on.ready; it returns 503 with a full body and ahint, not an empty error.- A terminal
errorwhosedetailstarts "Local Lemonade Server is not reachable" means Lemonade isn't running or isn't reachable — not a bug in this package. Start it, or setLEMONADE_BASE_URL. needs_confirmationis followed by a refusal and the run ends. See §8.write_file/edit_file/run_shell_commandare unreachable over/query.- A placeholder hash in
binaries.lock.jsonblocks the fetch before any network call. Between releases that is the expected state — it is not a broken install, and there is no override. - No
linux-arm64/win32-arm64sidecar. The TUI has both. APlatformErroron those hosts is the design, not a missing artifact. - There is no caller-auth token at 0.1.1. Unlike
@amd-gaia/agent-email, this sidecar mints none and this package sends none — don't add anAuthorizationheader looking for one, and don't rely on its absence as a security boundary. Loopback binding is what protects it. run_idmust be a UUID, and unknown fields in the request body are a 422 — the model forbids extras. Typos don't get ignored.gaia runneeds the PythongaiaCLI onPATH— the TUI shells out to it to start the daemon. The package deliberately strips its own npm bin directory from the child'sPATHso the TUI doesn't re-invoke the npm shim; if the Python CLI isn't installed, the daemon never comes up.- The TUI is installed as
gaia-tui, nevergaia— the terminal-hub artifact is calledgaia-<platform>, and a file namedgaiain a cache directory would shadow the npm bin shim. The lock'sfilenameandexecutablediffer for that reason. Don't rename it back. - The TUI comes from a lane this package doesn't publish. If a fetch 404s on the TUI but not the sidecar, the pinned
terminal-hubversion is the thing to check —gaia versionprints it and its base URL. - ESM-only.
require("@amd-gaia/gaia")fails; useimportor dynamicimport().
Verify the integration
Green path, in order:
npx @amd-gaia/gaia version # per-component version + source URL + matrix
npx @amd-gaia/gaia fetch # JSON: one entry per binary with its sha256
npx @amd-gaia/gaia serve --port 8141
Against a lock that still carries PENDING-… hashes, fetch is expected to fail with a PlatformError before any download — that is the gate working, not a broken install. Only a published release has real hashes.
Then, in another terminal:
curl -s http://127.0.0.1:8141/health # {"status":"ok","service":"gaia-agent-gaia"}
curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.12","agentVersion":"0.1.1"}
curl -s http://127.0.0.1:8141/v1/gaia/init # 200 + "ready":true, or 503 + a "hint"
curl -N -X POST http://127.0.0.1:8141/v1/gaia/query \
-H 'content-type: application/json' \
-d '{"query":"What can you do?","run_id":"00000000-0000-4000-8000-000000000001","context":[],"can_answer_questions":false}'
A healthy run streams status / token events and ends with one final. If /v1/gaia/init is 503, fix what its hint names and retry — the rest of your integration is fine.
A 503 from /query itself is a different condition: every retained session slot is busy and none is idle enough to evict (SPEC §5.2). Do NOT loop on /v1/gaia/init — it will report ready. Wait for a running turn to finish (or close an idle session) and retry the same /query.
For the full wire contract, lock schema, exit codes, and timeout table, see SPEC.md. For the user-facing overview, see README.md and <https://amd-gaia.ai/docs/guides/gaia>.
Changelog
All notable changes to @amd-gaia/gaia are documented here. The format follows Keep a Changelog and this package adheres to Semantic Versioning.
[0.1.1] — unreleased
First working release. npx @amd-gaia/gaia is now the single command that gets a user running GAIA: it fetches and verifies everything GAIA needs and drops them into the terminal UI. Before this there was no packaged path at all — the flagship agent had to be run from a repo checkout with a Python environment, and reaching the terminal UI meant building it from source.
Added
503from/queryat session capacity. When every retained session slot is busy and none is idle enough to evict, starting a new session returns503with the reason indetail— retryable, distinct from a bug-shaped500. See SPEC §5.2.- Per-turn skill-body selection. A loaded skill stays loaded, but its body only renders in the prompt on turns whose query matches its description; the rest collapse to a one-line menu the model re-activates with
load_skill.GAIA_DYNAMIC_SKILLS=0turns the selection off,GAIA_DYNAMIC_SKILLS_TAUoverrides the match threshold, and an embedder outage disables it for the session (every body renders — capability is never lost to a failed match). gaia run(the default command) — resolves the host platform, fetches and SHA-256 verifies both binaries, then launches the terminal UI and propagates its exit code. Arguments after a bare--are forwarded to the TUI verbatim.- Dual-binary delivery. The package installs two published artifacts: the frozen agent sidecar (
gaia-agent), published by this package's own release, and the terminal UI (gaia-tui), which is the publishedterminal-hubcomponent. The TUI is consumed, not rebuilt — it is byte-for-byte the binary a full GAIA install runs asgaia tui, so an npm user and a core user cannot end up on terminal UIs that behave differently. A second build under this package's own lane would have been the same bytes at a different version under a third naming convention, and the two would have drifted. binaries.lock.jsonschemaVersion3.0 — a component-keyed checksum manifest where each component carries its owncomponentVersion,baseUrlandplatforms. Component-first rather than the email agent's flatbinariesmap because the two differ in every dimension: hub lane, version, and platform coverage (terminal-hub covers arm64 Linux and arm64 Windows; the PyInstaller sidecar does not). A single shared base URL cannot address two lanes, so a1.x- or2.x-shaped lock is rejected at load with an error naming the schema.- Terminal-hub artifact naming is handled in data. That lane names its Windows builds
gaia-win-x64.exe/gaia-win-arm64.exe, while platform keys come fromprocess.platformand saywin32. The lock keeps thewin32-*key and carries the hub's spelling infilename, so nothing branches on platform to construct a URL. The mapping is asserted on both sides (TUI_ARTIFACT_NAMESinsrc/platform.tsand in the lock generator) because a wrong name there is not a build failure anywhere — it is a 404 on a user's first run. - Mandatory SHA-256 verification. Every download is hashed and compared against the lock before it is written. A mismatch deletes the download and raises
IntegrityErrornaming expected vs actual. A placeholder hash blocks the fetch before any network call. There is no flag that relaxes either. gaia fetch— download and verify without launching; prints JSON. Supports--componentand--platformfor cross-platform staging in CI.gaia serve— run the agent sidecar alone on127.0.0.1:8141for integrators who want the REST surface without a daemon or a UI. Health-pollsGET /health, checks the contract version, and tree-kills on exit. Port4001is refused.gaia version— prints, per component, its version, the URL it is fetched from, and its platform matrix.- Programmatic exports —
fetchAll,startSidecar,shutdown,runTui, the platform helpers, and the typed error classes, for embedding GAIA in another app.
Notes
- The sidecar is installed into
~/.gaia/agents/gaia/, the GAIA daemon's own cache directory. The daemon spawns and supervises the sidecar; putting an already-verified binary where it looks turns its fetch into a cache hit instead of a second large download.runtherefore does not spawn a sidecar itself — the terminal UI reaches agents through the daemon relay and never holds a sidecar token, so a second process would only contend for the port.serveis the direct path for callers who do want to own it. - The TUI is installed as
gaia-tui, never asgaia, so it cannot shadow thegaiabin shim npm places onPATH— the terminal-hub artifact itself is namedgaia-<platform>, which is why the lock separatesfilenamefromexecutable. - Because the TUI comes from the
terminal-hublane, this package cannot be released until that component is published at the version the lock pins. The release fails loudly naming the required version; it never falls back to building its own TUI. Each terminal-hub artifact is additionally cross-checked against the hub's own server-side SHA-256 before its hash enters the lock. - Requires Node.js 18+ (built-in
fetch), a running Lemonade Server for inference, and thegaiaPython CLI onPATHfor the daemon the TUI starts. - The sidecar has no arm64 Linux or arm64 Windows build. On those platforms the run stops with an error naming the platform and the supported set rather than launching a UI with no agent behind it.
gaia_agent0.1.1 has no caller-auth token, so unlike@amd-gaia/agent-emailthis package mints and sends none.- Tracks sidecar contract
apiVersion2.12; a differing major raisesVersionMismatchError.