Source Code:
src/gaia/Introduction
GAIA is an open-source framework that runs generative AI applications on AMD hardware. GAIA uses ONNX Runtime GenAI via Lemonade Server for running Large Language Models (LLMs). GAIA utilizes both NPU and iGPU on Ryzen AI systems for optimal performance on 300 series processors or above.Before You Start
System Requirements
Driver Requirements:
Software Requirements
- Git - For version control
- Lemonade Server - Download from lemonade-server.ai
Setup and Installation
GAIA uses uv, a fast Python package manager that handles virtual environments, Python installation, and dependencies automatically.Step 1: Install uv
Windows (PowerShell):Step 2: Clone GAIA
Step 3: Create Virtual Environment
Note: GAIA supports Python ≥3.10 (classifiers cover 3.10, 3.11, 3.12 insetup.py), but linting configs (black/mypyinpyproject.toml) are pinned topy312, so 3.12 is recommended for contributors.uvwill download 3.12 automatically if it’s not installed.
Step 4: Activate the Environment
Windows (PowerShell):(.venv) in your terminal prompt when activated.
Step 5: Install Dependencies
setup.py for the full list):
Step 6: Claude Code Plugins (Optional)
The same
.claude/settings.json registers a SessionStart hook that runs
scripts/cloud_bootstrap.sh.
It builds the editable dev install for Claude Code cloud sessions,
where the environment’s own setup script runs before the repo is cloned and so
cannot do it. On a local machine and in CI it exits immediately without output —
it is gated on CLAUDE_CODE_REMOTE and CI..claude/settings.json that declares two recommended Claude Code plugins from the official Anthropic marketplace:
frontend-design— higher-quality, less generic UI generationsuperpowers— structured dev methodology (brainstorm → plan → TDD → review)
“This project declares plugins. Install them?”Accept it once and the plugins become available for the repo. You can verify with:
.claude/settings.local.json with false:
Deactivating
When done working:See also: Agent SDK Documentation
Running GAIA
Verify Installation
Start Chatting
Note: GAIA starts Lemonade Server when needed —gaia daemonsupervises it, so there is nothing to launch first. To bring it up ahead of time, or to check on it, usegaia daemon startandgaia daemon status.
Agent UI Development
Start the Agent UI (backend + frontend) with the convenience scripts: Linux / macOS:tsc then flags src/types/apiContract.ts, update the matching interface in src/types/index.ts.
Linting and Formatting
util/lint.py drives all repo lint tooling. Running it with no flags runs
every check in sequence; pass a specific flag to run one:
--workflow-triggers blocks a branches: filter under a workflow’s
on.pull_request. That key matches the PR’s base branch, so branches: [ main ]
means “only when merging into main” — every PR stacked on a feature branch skips the
workflow, with nothing on its checks page to say so
(#2767). Scope a workflow with paths:
instead.--mypy runs twice. The full src/gaia run is advisory. A second run over
MYPY_CLEAN_MODULES in util/lint.py — modules with zero errors — blocks, so a
clean module can’t regress. Add a module to that list once you bring it to zero; fix
a new error rather than removing a module to get CI green. The mypy version pin in
TOOL_VERSIONS only applies when uvx is on your PATH; without it, lint.py runs
whatever mypy you have installed, which may disagree with CI.--test-lanes fails when a test file under tests/ or a hub agent’s tests/ is
not named by any workflow’s pytest step, because a file nothing runs looks green on
every PR whether it works or not. Add a new file to a lane (offline integration
files go in test_unit.yml’s offline-integration job), or list it in
util/test_lane_allowlist.yml with the reason CI can’t run it.Being named isn’t the same as running. A file with a module-level
pytest.importorskip (for a hub wheel like gaia_agent_chat, or an optional
extra) skips wholesale in any lane that lacks the package. The unit lane and the
lanes that install hub wheels run util/check_module_skips.py right after their
install step. It fails on such a skip unless the file is listed
under module_skips: in the same allowlist, with runs_in naming the lane that
installs the package. That lane must name the file and run the check too.--ci-paths fails when a workflow’s paths: filter would not fire on a file the
workflow runs, imports directly, installs, or reads by literal path. A PR changing
only that file would merge with the lane never running. It also fails when a file
under src/, hub/, tui/, installer/, util/ or tests/ triggers no
path-scoped workflow. Add the path to the lane’s filter, or list it in
util/ci_path_allowlist.yml with the reason the lane can’t be affected. To see
which workflows a change would run:Running Tests
conftest.py and CI workflow apply this setting automatically):
tests/conftest.py (api_client, api_server,
require_lemonade, lemonade_available). The require_lemonade fixture
auto-skips integration tests when no Lemonade server is running.
Verifying the download-server publish pipeline
Publishing to the hub is irreversible —hub.amd-gaia.ai has no unpublish
route, so a bad release is permanent and public. Before running any
release_*.yml workflow that publishes, prove the pipeline locally:
workers/agent-hub under wrangler dev with Miniflare’s simulated R2,
drives the real publishers against it, and asserts on behaviour — the happy path,
idempotent re-runs, immutability, the oversized direct-to-R2 lane, and the
pre-flight checks that must refuse to publish anything at all. It refuses to run
against any non-loopback URL, needs no credentials, and cleans up after itself.
It skips with a message (not a failure) when Node.js is unavailable.
There are two publisher scripts and they are different programs.
hub/agents/email/python/packaging/publish_to_r2.py backs the email, chat, and
components releases; hub/agents/gaia/python/packaging/publish_to_r2.py backs
release_agent_gaia.yml. Proving one says nothing about the other, so the
harness runs both by default and labels every row with the publisher it came
from. Use --publisher email or --publisher gaia to narrow it. A case that
needs a capability a publisher does not have is reported N/A, never passed.
Read the module docstring for what it deliberately cannot prove — real R2
credentials from GitHub’s IP space, real installer sizes, and the macOS freeze
legs still only get exercised in CI.
Verifying the R2 credentials
The harness runs on bogus credentials against a local Worker, so it proves the publish logic and nothing about whether the real key works. Every artifact over the Worker’s request-body cap — today the flagship agent’s sidecar builds — is PUT straight to R2 instead, and R2 answers both a bad credential and a malformed request with the sameUnauthorized:
.env (gitignored) or the environment, writes probes under
agents/_credential-probe/ and deletes them, and prints no secret value. It
still cannot prove the credential works from GitHub’s runners — a token with
Client IP Address Filtering passes locally and fails in CI.
Version fields
src/gaia/version.py is the only version you edit for a core release. One
command writes every field that follows it, and lint fails when one drifts,
naming the file, the value it found, and the value it expected:
--stamp does the work of installer/version/bump-ui-version.mjs, the flagship’s
stamp_version.py, and the hand edits to the Terminal UI manifest and the navbar.
Those scripts still work, and release workflows still call them.
--stamp vX.Y.Z-rcN writes a release candidate’s versions into the same files
as util/release_tag.py stamp, with the same result.
The flagship and chat agents keep their own versions until they join core’s
(#5172). Until then, two fields print as
HELD and don’t fail: the chat agent’s __init__.py version and the flagship npm
lockfile’s root version.
The flagship agent in a core release
gaia init, the one-line installers (install.sh / install.ps1) and the
Windows setup install the flagship agent’s gaia-agent binary from the download server (hub.amd-gaia.ai) and from the pin in
hub/agents/gaia/npm/binaries.lock.json. Only release_agent_gaia.yml (tag
agent-pkg-gaia-vX.Y.Z) fills either, so a core release does not rebuild it.
A final core release therefore fails unless that flagship release was cut from
the same code: same frozen sources at its tag, its hashes committed to the
lock, and it is the hub’s latest. The flagship release opens a PR that commits
those hashes to the lock once it publishes; merge it, then check before tagging:
Release candidates
A release can go out as a candidate first, so testers get the exact build before it becomes whatpip install amd-gaia and the website hand everyone. Tag
vX.Y.Z-rcN on a commit whose src/gaia/version.py already says X.Y.Z (the
release PR has landed); publish.yml runs the usual pipeline, behind the same
approval, and keeps the result off every stable channel:
- GitHub — a pre-release, never marked latest, with the one-step Windows
setup (
gaia-X.Y.Z-rc.N-win-x64-setup.exe), the desktop app installers, the terminal binaries, and the wheel attached. If the Windows setup fails to build or fails its smoke test, the pre-release still ships without it, its notes say so, and the run fails. - GAIA agent — the flagship
gaia-agentbinaries, frozen from the candidate’s commit and attached to the pre-release withgaia-agent-release.json. The candidate’sgaia initand Windows setup use these, never the download server’sgaia, which can be many commits older. A candidate whose flagship build fails is not published; an install that cannot reach the pre-release’s agent fails with the reason. - PyPI —
X.Y.ZrcN.pip install amd-gaiaskips it;pip install --pre amd-gaiaoramd-gaia==X.Y.ZrcNgets it. - npm —
@amd-gaia/[email protected]under thenextdist-tag;latestdoes not move. - Website — a collapsed “Try the release candidate” entry under the stable downloads, shown only while the candidate is newer than the latest release.
- Not published — Download-server components (the catalog has no pre-release
channel) and the terminal’s
.pkg/.deb/.rpmpackages, which need a numeric-only version.
docs/releases/vX.Y.Z.mdx), and
the -rcN suffix is stamped from the tag at build time, so the final vX.Y.Z
can be tagged on the same commit once testing passes. Check a tag before pushing
it:
-rc2 if needed, final) is the RC phase of the
gaia-release skill in .claude/skills/gaia-release/SKILL.md.
Real-World Testing with Claude Code
Unit and integration tests prove code paths; they don’t prove a feature works end-to-end on real hardware. GAIA ships agaia-testing skill (under
.claude/skills/) for Claude Code: it drives
the real interface — the Agent UI in a browser, the CLI, or the Agent UI MCP
server (gaia mcp serve) — and brings back screenshots, logs, traces, and
per-operation timing as evidence, all collected to your local machine. In a
session, ask it to “test / validate / QA this feature with screenshots”; it
scales from a unit-only run up to a full real-world run gated by a single
approval prompt.
Declaring test machines
The real-world tier runs on a machine you nominate. The skill discovers candidates from your loaded Claude config only — it never scans your filesystem. Declare machines under a## Dev Machines (or ## Test Machines)
heading in either:
- your user-level
~/.claude/CLAUDE.md(shared across all your projects) — which may summarise them inline and point to a detail file it loads, such as~/.claude/memory/dev-machines.md; or - the project
./CLAUDE.mdor.claude/settings.json.
Keep credentials out of committed files. Put sudo passwords, tokens, and keys only in your user-level config (or a memory file it points to) — never in the repo’sCLAUDE.mdor.claude/settings.json. The skill reads them write-only and never echoes them into logs, screenshots, or reports.
Measuring where a turn’s time goes
A slow local turn is almost always prefill: the system prompt and tool schemas are re-sent on every LLM call, so prompt size converts directly into seconds. None of that is visible in the default stats line, which shows only elapsed time. SetGAIA_TURN_LOG to a file path to turn on per-turn recording:
gaia.turn/1) with the fixed prefill
size, cached-vs-new input tokens per call, output tokens, the wall-time split
across model / tools / agent overhead, and absolute timestamps — so a session
is greppable and two builds are diffable.
With --dev, the TUI also draws the same breakdown under each answer:
Token counts marked cached / new come from a client-side estimator
(
tiktoken when installed), not from the model’s own tokenizer. Read them as
ratios and deltas — how much a prompt shrank, what share was cache-eligible —
not as exact model token counts. out is the backend’s own count.Is GAIA keeping the model server busy?
On a local llama.cpp model, each call in the record also carries the server’s own accounting for that request, read from the stream’stimings, so it never
mixes in another client’s request:
server_compute_s and non_model_s appear only when every call reported
timings; a partial sum would understate the model and overstate GAIA. Work that
runs after the answer (memory writes) is appended as a second line with schema
gaia.turn_post/1 and the same turn_id.
The flagship sidecar also serves the last records at
GET /v1/gaia/debug/turns?limit=N (with its caller token) while GAIA_TURN_LOG
is set. The route is off the versioned contract and returns 404 otherwise. It
returns every session’s records, queries included, to anyone holding the
caller token, so keep turn recording on only on a machine you use alone.
To watch the server itself, util/perf_turns.py drives /v1/gaia/query and
samples the llama.cpp server behind Lemonade: /slots for per-slot busy time
and idle gaps, /metrics for prefill and decode seconds, and, on Windows, GPU
utilization per process. It waits for an idle GPU before each session and flags
a run during which the model was reloaded or another client sent requests. It
approves every tool confirmation by itself, so only give it queries whose tools
you would approve:
Seeing which tools the model was given
A--trace run writes agent_output_<timestamp>.json, and its tool_schema
block records what went out to the backend as tools= on that run — the tool
names, the per-turn filter (null when the full registry was sent), the
serialized schema size in chars and tokens, and the schema text itself. With
the file’s system_prompt and conversation alongside it, a prompt can be
split into system / tools / history shares from one artifact, and a tool that
never reached the model is visible by its absence from tool_names.
The schema text is the bulky part. Set GAIA_TRACE_TOOL_SCHEMA=0 to drop just
that text — the names and sizes stay, and the artifact records a
schemas_omitted note so a smaller trace never looks like a full one.
Troubleshooting
”uv” command not found (Windows)
After installing uv, the command may not be recognized because PATH hasn’t updated. Solution 1: Restart PowerShell (simplest) Solution 2: Update PATH in current session:Virtual Environment Activation Fails (Windows)
If you see a script execution error:Model Loading Issues
- Check available system memory
- Verify model compatibility with your hardware
- Ensure all dependencies are correctly installed
Environment Variable Issues
- Verify the virtual environment is activated (look for
(.venv)prefix) - Try restarting your terminal
- Re-run the activation command for your platform
Support
Report issues to[email protected] or create an issue on GitHub.