Skip to main content
Source Code: src/gaia/
Table of Contents

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


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):
Linux:

Step 2: Clone GAIA

Step 3: Create Virtual Environment

Note: GAIA supports Python ≥3.10 (classifiers cover 3.10, 3.11, 3.12 in setup.py), but linting configs (black/mypy in pyproject.toml) are pinned to py312, so 3.12 is recommended for contributors. uv will download 3.12 automatically if it’s not installed.

Step 4: Activate the Environment

Windows (PowerShell):
Windows (Command Prompt):
Linux:
You should see (.venv) in your terminal prompt when activated.

Step 5: Install Dependencies

With optional extras:
Available extras (see 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.
GAIA ships a .claude/settings.json that declares two recommended Claude Code plugins from the official Anthropic marketplace:
  • frontend-design — higher-quality, less generic UI generation
  • superpowers — structured dev methodology (brainstorm → plan → TDD → review)
Plugins are not auto-installed silently. The first time you open this repo in Claude Code (v2.1.0+), you’ll see a prompt:
“This project declares plugins. Install them?”
Accept it once and the plugins become available for the repo. You can verify with:
To opt out, copy the entry into your local .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 daemon supervises it, so there is nothing to launch first. To bring it up ahead of time, or to check on it, use gaia daemon start and gaia daemon status.

Agent UI Development

Start the Agent UI (backend + frontend) with the convenience scripts: Linux / macOS:
Windows PowerShell:
Or start manually:
Changed a backend pydantic model? Regenerate the frontend’s API types, or CI fails:
If 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

The packaged email-agent tests are hermetic and must not invoke an interactive OS keychain. Run them with the null keyring backend (the email test package’s conftest.py and CI workflow apply this setting automatically):
On PowerShell, set the equivalent environment variable first:
Key fixtures live in 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:
It boots 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 same Unauthorized:
It checks bucket scope, a checksummed write, a write at real release size, and a round-trip back through the hub Worker, naming the first that fails. That last check is the one worth understanding: a credential can happily write to a same-named bucket in a different Cloudflare account, and only the Worker serving the bytes back rules that out. It reads .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 what pip 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-agent binaries, frozen from the candidate’s commit and attached to the pre-release with gaia-agent-release.json. The candidate’s gaia init and Windows setup use these, never the download server’s gaia, 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-gaia skips it; pip install --pre amd-gaia or amd-gaia==X.Y.ZrcN gets it.
  • npm — @amd-gaia/[email protected] under the next dist-tag; latest does 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/.rpm packages, which need a numeric-only version.
The candidate reuses the final’s release notes (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:
The full sequence (RC, test pass, -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 a gaia-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.md or .claude/settings.json.
Record each machine’s name, access method (local, SSH host, or container), hardware class (CPU / discrete GPU / Ryzen AI NPU), and any setup or test commands. With nothing declared, the skill targets your local machine.
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’s CLAUDE.md or .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. Set GAIA_TURN_LOG to a file path to turn on per-turn recording:
Each turn appends one JSON object (schema 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:
Unset, recording is off and costs nothing — the recorder is never constructed and the agent’s wire payload is unchanged.
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’s timings, 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:
Solution 3: Use full path:

Virtual Environment Activation Fails (Windows)

If you see a script execution error:

Model Loading Issues

  1. Check available system memory
  2. Verify model compatibility with your hardware
  3. Ensure all dependencies are correctly installed

Environment Variable Issues

  1. Verify the virtual environment is activated (look for (.venv) prefix)
  2. Try restarting your terminal
  3. Re-run the activation command for your platform

Support

Report issues to [email protected] or create an issue on GitHub.