Skip to main content
Source Code: src/gaia/ui/
Import:
See also: User Guide | Agent SDK | API Specification
Tested Configuration: The Agent UI has been tested on AMD Ryzen AI MAX+ 395 with Qwen3.5-35B-A3B-GGUF. Other configurations are not officially verified. See the User Guide for full details and how to report issues on other hardware.

Overview

The Agent UI SDK is the Python backend that powers the GAIA Agent UI. It provides:
  • FastAPI REST server with session, chat, document, and memory endpoints
  • SQLite database for persistent sessions, messages, and document metadata
  • SSE streaming for real-time chat responses
  • RAG integration for document Q&A
  • Memory dashboard with knowledge browser, tool stats, and observability (Memory SDK)
  • Pydantic models for request/response validation
The backend runs on port 4200 by default and serves both the Electron desktop app and browser-based clients.
End users don’t need to interact with this SDK directly — prebuilt .exe / .deb desktop installers are on the GitHub Releases page, and npm install -g @amd-gaia/agent-ui gives the same app on any Node-capable platform. This page is for developers embedding or extending the backend. See the Agent UI guide for install options.

Quick Start

Start the Server

Run with uvicorn:
Or from the command line:

Use the Database Directly


Core Classes

ChatDatabase

The persistence layer for all Agent UI data. Uses SQLite with WAL mode for concurrent read access.
Constructor:

Session Methods

Example:

Message Methods

In-process chat and autonomous turns save the agent’s model_messages on the assistant or autonomous row. These contain the current turn’s user message, assistant tool calls, tool results and final answer, without duplicating prior history or the system prompt. The nullable JSON column is added automatically to existing databases; legacy rows retain their original text-only context. Restoration budgets complete turns against the resolved model/device window (including GAIA_CTX_SIZE and the NPU ceiling), reserving the current prompt, native tool schemas, output allowance and 2K estimated tokens of headroom. History uses at most half the resolved window. Counts use GAIA’s shared token estimator, not a guarantee against the model’s exact tokenizer. On overflow, old turns are removed in a block toward half the history budget; the newest turn is retained if it fits. The full stored trace remains in SQLite, and exclusions produce a warning in the backend log. When a streamed turn hides more turns than the session’s last streamed turn did (a longer chat, or a smaller model’s budget), the stream sends one status event with status: "history_cut", turns and tokens (the turns no longer replayed and their estimated size), which the chat shows as an inline note. A stable cut is not reported again. A relayed sidecar agent receives only the last 5 exchanges; its stream sends the same event once, on the turn the sixth arrives. get_messages and the ordinary transcript API do not expose the internal trace. Deleting a transcript message also deletes its evidence. Example:

Document Methods

Example:

Statistics


create_app()

Factory function that creates and configures the FastAPI application with all endpoints.
Shared state is stored on app.state and is accessible in tests:

Pydantic Models

All request and response bodies use Pydantic models from gaia.ui.models.

System

Sessions

Chat

Messages

Documents


REST API Endpoints

System

Check system readiness for the agent UI.Response:
Health check with database statistics.Response:
Which Lemonade GAIA talks to and what it runs, read from Lemonade’s /health and /system-info. Always 200: an unreachable server comes back as reachable: false with a one-line error. A field Lemonade did not report is null.Response:
Save the llama.cpp backend for chat models as llamacpp_backend in ~/.gaia/config.json. It applies the next time a chat model loads; the embedding model keeps its own backend.Request:
backend is auto or one of the names in available_backends. Returns the refreshed GET /api/system/model-server body. 400 when the backend is not available on this machine, 503 when Lemonade is not answering.
Trigger loading a model on the Lemonade server. Returns 202 immediately; loading proceeds in the background. Poll GET /api/system/status to detect when loading completes.Request:
When llamacpp_backend is set in ~/.gaia/config.json and the model runs on llama.cpp, the load asks Lemonade for that backend.Response (202):
Trigger downloading a model via the Lemonade server. Returns 202 immediately; the download proceeds in the background. Poll GET /api/system/status to detect when the model becomes available. Set force to true to re-download even if the file already exists (repairs corrupted or incomplete downloads).Request:
Response (202):
Read a new chat’s system prompt and tool schemas into Lemonade’s prompt cache, so the first message doesn’t wait on them. The UI calls this when a new chat opens. Only one warm-up runs at a time, a repeat within 30 s of a finished one does nothing, and none starts while a streamed chat turn runs. Cloud models and models that aren’t downloaded are skipped, so it never starts a download or a paid request. It is also skipped when the GAIA agent runs in its daemon sidecar (a Hub install), which warms its own prompt, and on a PC with under 24 GB of RAM, where the model loads with the first message instead. Returns the warm-up state. GET /api/system/warm-up returns the same state without starting one, and GET /api/system/status carries it as warm_up.Response:
state is one of idle, warming, ready, failed, or skipped.
Get current user settings including the custom model override and its status on the Lemonade server.Response:
Update user settings. Set custom_model to a model ID to override the default, or to an empty string / null to clear the override and revert to the default model.Request:
Response: Same shape as GET /api/settings.

Sessions

Create a new chat session.Request:
Response: SessionResponseFrom the Agent UI (X-Gaia-Client: agent-ui) a chat is always the GAIA agent: an omitted agent_type becomes gaia and any other value is a 422. See Flagship only.
List all sessions, ordered by most recently updated.Query params: limit (default 50), offset (default 0)Response: SessionListResponse
Get session details including message count and attached document IDs.Response: SessionResponse
Update session title or system prompt.Request:
Changing agent_type, device, or mail_provider while a chat turn is running on the session returns 409; retry once the turn finishes. Renames are always accepted.
Delete a session and all its messages (cascading delete). If a chat turn is running on the session, it is stopped and the request returns once that turn has finished.
Get messages for a session, ordered oldest first.Query params: limit (default 100), offset (default 0)Response: MessageListResponse
Export a session to Markdown or JSON.Query params: format (“markdown” or “json”, default “markdown”)
SSE stream of system-level session events (e.g. set_active_session) — the frontend subscribes on mount.
Signal the frontend to switch to this session, for automation callers (e.g. open_session_in_browser() in the MCP bridge) rather than a user click. Emits the set_active_session event on /api/sessions/events.
Toggle a session’s private (incognito) mode.

Chat

Send a message and receive a response. Supports both streaming (SSE) and non-streaming modes.From the Agent UI (X-Gaia-Client: agent-ui) a non-gaia agent_type is a 422, and sending into a chat stored under another agent is a 409 (“start a new chat”). See Flagship only.Request:
Streaming response (SSE events):When stream: true, the server returns a text/event-stream response. Each line follows the SSE format data: <JSON>. The SSEOutputHandler (src/gaia/ui/sse_handler.py) bridges agent console events to the following typed events:Thinking and ProgressTool Executionresult_data variants in tool_result:
  • File list: {"type": "file_list", "files": [...], "total": int} — up to 20 file entries
  • Search results: {"type": "search_results", "count": int, "scores": float[], "previews": string[]} — top 5 chunk previews (200 chars each)
command_output shape in tool_result:
success is false when the tool reports status: "error" or a non-zero return_code. The exception is an exit the tool names in expected_exit, such as grep’s exit 1 for “no match”: that result stays success: true.Response ContentStream TerminationExample stream showing a typical multi-step interaction:
Non-streaming response:
Session ids with a currently-running chat turn — a run continues server-side after the user navigates away, so this (not an open SSE connection) is the source of truth for a “still running” indicator.
Re-attach to an in-flight background run and stream its events over SSE (?session_id=). Read-only — replays every event emitted so far, then streams live events to completion; takes no session lock.
Cancel an active streaming turn without tearing down the HTTP connection — sets the SSE handler’s cancelled flag and, for an email-relay turn parked in a blocking socket read, forces it to error out.
Answer a mid-run needs_input question from the agent. Tries an in-process agent first, then an email-relay run blocked on the sidecar’s own query loop; a session with neither pending is a 404, never a silent no-op accept.

Documents

List all documents in the global library.Response:
Index a document by file path. The file is hashed for deduplication — if the same file was already indexed, the existing document is returned.Request:
Response: DocumentResponse
Remove a document from the library and all session attachments.
Attach a document from the library to a session.Request:
Detach a document from a session (does not delete the document). Returns 409 while a chat turn is running on the session.

Files

Open a file or folder in the system file explorer. On Windows this launches Explorer, on macOS it uses open, and on Linux it uses xdg-open. Symbolic links are rejected for security.Request:
Response:
Error responses:
Upload an arbitrary file for use as a chat attachment (not added to the RAG library). Use POST /api/documents/upload instead if you want the file indexed for Q&A.
Browse filesystem contents. Returns entries under a directory with size, mtime, and type metadata.
Search files by name across ~/Documents, ~/Downloads, ~/Desktop, ~/OneDrive, then the rest of the home directory. The walk is bounded: it stops at max_results matches, after 20,000 directory entries, or after about 10 seconds. truncated: true in the response means a bound cut the walk short and more matches may exist.
Return a preview (first N bytes / first N lines) of a text file.
Stream an image file from the filesystem for display in the UI.

Agents

List all agents registered via AgentRegistry — built-in agents plus any custom agents discovered under ~/.gaia/agents/. See plugin-registry for the registration format.
To move custom agents between machines, use the gaia agent export and gaia agent import CLI commands — see Custom Agent.

MCP

The MCP router is read-only status for external MCP servers, plus lifecycle for GAIA’s own MCP server (the one that exposes Agent UI tools to clients like Claude Code). Registering, enabling, and disabling an external MCP server connector lives under Connectors — mcp_server is one of that router’s connector types.
List configured MCP servers (from ~/.gaia/mcp_servers.json) and their enabled state.
List tools exposed by a specific MCP server (opens a transient connection).
Runtime MCP client status from the most recent chat session — connected servers, aggregate tool count, last-error details.
Status of GAIA’s own MCP server — whether it’s running, its port, and the URL MCP clients connect to.
Start GAIA’s MCP server as a background subprocess so external MCP clients (e.g. Claude Code) can call Agent UI tools (sessions, chat, files, memory). Optional JSON body {"port": <int>}; defaults to AGENT_UI_MCP_PORT.
Stop GAIA’s MCP server, if it was started by the UI.

Tunnel

Expose the local Agent UI over the internet via an ngrok/Cloudflare tunnel. Used to reach the UI from a phone.
Start a tunnel. Returns the public URL.
Stop the active tunnel.
Current tunnel status — active/inactive, public URL, provider, uptime.

Chat (advanced)

Tool-confirmation handshake. When a tool in the agent’s confirmation_required_tools() set (the generic base set merged with the agent’s own CONFIRMATION_REQUIRED_TOOLS) makes the agent pause, the stream emits a permission_request event carrying confirm_id and, when “always allow” can be offered, always_scope (what the grant would cover, e.g. gh issue list). The UI answers here with allow once, always allow, or deny. An unanswered prompt is denied after 600 seconds.Request:
always (with approved: true) grants this exact invocation for the rest of the chat — see gaia.agents.base.tool_grants. confirm_id echoes the prompt being answered.Response: {"status": "ok", "approved": true, "granted": "gh issue list"} — granted is the new grant’s label, or null.Errors: 404 when the chat has no running turn; 409 when confirm_id no longer matches the live prompt, or always was requested for a call with no scope narrow enough to grant.
Every chat in this backend run that has full access on or at least one “always allow” grant, for the Settings → Permissions pane.Response: {"default_mode", "sessions": [{"session_id", "mode", "grants": [{"key", "label"}]}]} — default_mode is the mode a new chat starts in: full_access when it is set in ~/.gaia/config.json, otherwise ask.
One chat’s permission mode (ask or full_access) and its grants. A chat created in this backend run starts in ask unless full_access is set in ~/.gaia/config.json; a chat from an earlier run reopens in ask. Permission state lives in memory: it ends when the chat is deleted or the backend restarts. 404 when no such chat exists (also for PUT and DELETE below).
Switch the chat between ask (every gated tool prompts) and full_access (gated tools run unasked and shell guardrails are lifted, as the TUI’s /full-access). Takes effect on the next gated tool, including mid-turn.Request: {"mode": "full_access"} — any value other than ask or full_access is a 422.
Revoke one “always allow” grant (?key=<grant key>) or, without key, all of them. Applies to a running turn too. 404 when key names no grant in the chat. Returns the chat’s permission view.
Eval-only, gated by GAIA_EVAL_SCRIPTED_USER=1 (403 otherwise). Scripts a user who declines the shell commands in decline_commands and answers no questions, so gaia eval agent can score what the agent does after a “no”. An empty list turns it off. The runner sets it from a scenario’s setup.decline_commands and clears it afterwards.

Documents (advanced)

Multipart drag-and-drop upload. Stores the file in the library and enqueues it for indexing.
Status of the background document_monitor.py file-watcher.
Indexing progress for a single document.
Cancel an in-flight indexing job.
Recursively index all supported files under a folder.
Re-run the RAG indexing pipeline for a document — clears any previous failure, then always ends in complete or failed (never 200 while leaving the document in a bad state).

Sessions (advanced)

Delete a single message from a session.
Delete a message and every message after it (the “resend from here” pattern).

System (advanced)

Inspect the background dispatch queue (model downloads, indexing jobs, etc.) used by src/gaia/ui/dispatch.py.

Memory

The Memory endpoints expose the agent’s persistent knowledge system. See the Memory SDK Reference for the underlying MemoryStore and MemoryMixin APIs.
Aggregate statistics across all memory tables.Response:
Paginated, filterable knowledge browser.Query params:Response:
Create a knowledge entry from the dashboard. Items created via the UI use source='user' and confidence=0.8. The entry is embedded right away with the embedder that produced the stored vectors, so recall by meaning finds it without a rebuild.Request:
Response:
If embedding fails the entry is still saved, and the response carries "embedded": false plus an embed_error saying what to do (run Rebuild Embeddings; keyword recall still finds it).
Edit a knowledge entry. Only provided fields are updated. A content change re-embeds the entry and the response includes embedded (and embed_error on failure), as for create; other edits keep the existing vector and omit the field.Request:
Delete a knowledge entry.
Time-sensitive items due within N days, including overdue.Query params: days (default 7)Response:
List all unique entities with knowledge counts.Response:
Per-tool performance statistics.Response:
List conversation sessions with turn counts and first message preview.Query params: limit (default 20)
Get all turns for a specific conversation session.
Daily activity counts for the activity timeline chart.Query params: days (default 30)Response:
Embedding status for the knowledge base.Response:
Manually trigger conversation consolidation. Distills old sessions (>14 days, >=5 turns) into durable knowledge items.Response:
Manually trigger background memory reconciliation. Checks for contradictory facts across sessions.Response:
Trigger embedding backfill for items missing embeddings.Response:
Rebuild FTS5 indexes if search results seem wrong.Response:
Fetch one knowledge entry by ID, including superseded rows — the list endpoint hides those by default, so this is what verifies a supersession chain (superseded_by).
List every distinct context tag with its knowledge count.
All knowledge linked to one entity name.
Recent tool errors across every tool, most recent first.
Recent call history for a single tool (arguments, result, latency).
Full-text search across every stored conversation.
Delete tool history, conversations, and low-confidence knowledge older than days (default 90). Returns per-table row counts.
Permanently delete all knowledge, tool history, and conversation data. Irreversible.
Wipe all memory, then re-run initialization from scratch — re-collects system context if discovery consent is granted. Irreversible.
Re-collect OS, hardware, installed-app, and version facts. Deletes stale category="system" entries and stores fresh ones — no LLM call, runs in a few seconds.
SSE stream of the local system-discovery scan (git identity, installed apps, recent file types, shell config, …) that seeds first-run memory.
SSE stream of LLM-assisted profile inference over the discovery findings — each insight event is a candidate fact awaiting approval. It runs on the machine’s default local chat model (default_model in ~/.gaia/config.json), never the chat’s cloud model; a cloud default_model ends the stream with an error event.
Store approved discovery findings from the onboarding wizard. Items are validated as knowledge creates, so a disallowed category 422s the whole batch before anything is written.
Store approved inference insights, replacing the prior inferred profile (old source="inferred" rows are deleted first — a failure here leaves the old profile in place rather than partially wiped).
Memory feature flags: memory_enabled (default true), mcp_memory_enabled (expose read tools to MCP clients) and system_discovery_consent (both default false).
Update the flags above. Turning system_discovery_consent on also persists it to ~/.gaia/memory_settings.json and triggers an immediate system-context refresh.
Eval-only, gated by GAIA_MEMORY_ADMIN=1 (403 otherwise). Wipes memory for a scenario reset — scope is all / knowledge / conversations.
Eval-only, gated by GAIA_MEMORY_ADMIN=1. Bulk-inserts knowledge items verbatim, bypassing dedup, to plant a known state for stress scenarios.

Connectors

OAuth and MCP-server connections for the Settings page and per-agent grants (issue #915). Every mutating route requires the X-Gaia-UI: 1 header — see Security.
Catalog specs merged with live state for every connector.
Long-lived SSE stream of connector lifecycle events — configured, disconnected, tested, enabled/disabled, OAuth completed/errored, grant changed, activation changed.
MCP servers declared by custom-Python agents (issue #1020).
Eval-only, gated by GAIA_EVAL_MAILBOX (403 otherwise). Reports the offline fixture mailbox the gaia_email scenarios read: its path, whether the file exists, and whether it is attached.
Eval-only, gated by GAIA_EVAL_MAILBOX; requires X-Gaia-UI: 1. {"attached": false} makes the mail tools answer as if no mailbox were connected, for sessions that start reading mail afterwards; true restores the fixture. The eval runner calls it from each scenario’s setup.mailbox_fixture.
Diagnostics dump, gated by GAIA_DEBUG=1 (404 otherwise).
Summary for one connector (type, configured/enabled state, account info).
Per-agent grant ledger for this connector — which agents may use it and with what scopes.
Per-agent activation state for this connector. Type-agnostic — returns {} for OAuth connectors, which have no MCP tool surface to activate.
Store credentials for a connector — for an mcp_server connector this also writes ~/.gaia/mcp_servers.json.
Run the connector’s health check.
Disconnect a connector — removes stored credentials and, for mcp_server, its mcp_servers.json entry.
Re-enable a previously-disabled mcp_server connector. Its tools materialize live.
Disable a configured mcp_server connector without clearing its credentials.
Start an OAuth PKCE flow. Returns {flow_id, authorization_url} — the UI opens the URL and polls for completion.
Start a device-code flow (no browser redirect, no registered OAuth app needed).
Cancel a pending OAuth flow without waiting for the callback.
Write the per-agent grant ledger — the scope ceiling an activation may request. Rejects an id the catalog doesn’t publish (404) or scopes outside the connector’s ceiling (400).
Revoke a grant.
Activate agent_id for connector_id (mcp_server only — OAuth connectors 400). Auto-grants when no grant exists and the body supplies scopes; 400 if it doesn’t.
Deactivate agent_id for connector_id. Idempotent, and non-destructive — the grant is preserved so re-activating is one call; delete the grant separately to wipe it.
/v1/connections/* is a separate, related surface — pre-authenticated connections forwarded from another process (issue #1292). See the Connectors infrastructure doc for it.

Goals

Autonomous goal/task tracking (GoalStore) behind the Agent UI’s Goals panel.
List goals. Optional status, source, and approved_only query filters.
Create a user goal — starts queued and auto-approved (only agent-inferred goals need approval).
Aggregate counts by status, for the dashboard header cards.
Agent-inferred goals awaiting user review.
A single goal with its tasks.
Hard-delete a goal and all its tasks.
Approve an agent-inferred goal for autonomous execution.
Reject an agent-inferred goal.
Cancel a queued or in-progress goal.
Update goal status and optionally append progress notes.
Add a task to an existing goal.
Update a task’s status and optionally record its result.

Schedules

Recurring prompts run on a cron-like interval — see gaia schedule for the CLI equivalent. The scheduler runs prompts only; it cannot yet run a skill directly (gaia schedule add --skill is rejected — #1019).
List all scheduled tasks.
Create a scheduled task from a name, interval, and prompt. An optional timezone (IANA name, e.g. America/Los_Angeles) sets the zone its times are in; it defaults to the server PC’s zone. An unknown zone returns 400.
Parse a natural-language schedule description (e.g. “every morning at 8am”) into structured interval config, without creating anything. Takes the same optional timezone and echoes the zone it used.
A single scheduled task.
Pause, resume, or cancel a scheduled task via {"status": ...}.
Delete a scheduled task.
Past execution results for a scheduled task.

Onboarding

First-run hardware scan (issues #1726, #1727).
Scan hardware (NPU/GPU via Lemonade’s /system-info, RAM) and report whether this machine can run the first-run model, with a coarse tier recommendation. recommended_model is the model chat will load on this machine (the default_model gaia init chose, else Gemma-4-E4B), and required_disk_gb is sized for that model’s download.

Setup

First-run setup. The Agent UI asks and runs the same gaia init the TUI does, so the two can never disagree about whether GAIA is set up.
Runs gaia init --check --profile gaia --json and returns its result plus the step list: {"ready": bool, "stage": ..., "reasons": [...], "steps": [...]}.Query params: skip_chat_model (the chat runs on a cloud provider, so only the embedding model is needed locally), load (also check that the models load).Errors: 403 without the X-Gaia-UI: 1 header — the check can start the model server, so it is guarded like a mutating route. 503 when the check could not be answered — a timeout or an unexpected exit. That is never reported as “not set up”.
Start gaia init --profile gaia --yes in the background; follow it with /api/setup/status. Body: {"skip_chat_model": false}. 409 when setup is already running.
The running (or last) setup: state (running, verifying, ready, failed, cancelled, or idle when none has run), the three steps with their status, progress text and percent, error, the equivalent command, and the last 40 output lines.
Stop a running setup. Anything already downloaded is kept. Returns {"cancelled": bool}.

Lemonade server

Which Lemonade Server GAIA uses: its own, or one the user already runs. Same choice and checks as gaia lemonade connect and gaia config set lemonade_mode. All three routes need the X-Gaia-UI: 1 header (403 without it).
The server GAIA uses now and who chose it: mode (gaia or external), saved_mode (null until someone chooses), source, base_url, saved_base_url, api_key_stored, env_override (a warning when LEMONADE_BASE_URL overrides the setting), gaia_installed, offer (no choice saved and the environment isn’t choosing, so the UI should offer the switch), and what_gaia_does. 409 when the config is unreadable or the saved choice is unusable; detail names the fix.
A Lemonade the user already runs on this PC’s default ports: {"found": {"url", "status", "version", "minimum", "message"}}, or {"found": null} when none answers. status is ok, needs_key, bad_key, too_old, not_lemonade, or unreachable.
Switch servers. Body: {"mode": "gaia" | "external", "base_url": "...", "api_key": "..."}; the URL and key are only for external. Nothing is saved unless the user’s server checks out. A running daemon is told to stop (or start) GAIA’s own server, and cached agents are dropped. Returns the GET shape plus daemon. 422 when the server can’t be used (detail.message, detail.status), including status: "not_installed" when switching from the user’s server to GAIA’s own before it is installed; nothing is saved. 409 for an unreadable config, 403 over mobile access, 502 when the daemon did not switch. A failed switch to GAIA’s own puts the previous mode back; a failed switch to the user’s server stays saved.

Providers

Where the chat model runs: Local, Fireworks AI, or the AMD LLM Gateway, all served through the local Lemonade — the TUI’s /provider. Keys go to the local Lemonade only and, once they discover models, into the OS credential store (the slot the TUI uses). A key is never echoed back or logged.
Every provider with its state — registered, key_source (environment, lemonade, stored, or null), models_discovered — and the active model. A stored key is handed back to a Lemonade that lost it. lemonade_error is set when Lemonade could not be reached. Requires X-Gaia-UI: 1 (403 without it).
The model new turns run. On the first call of a backend run it also restores last_model from ~/.gaia/config.json; restore.status is restored, failed (with the reason — another model is never switched in quietly), none, or pending while the local model server is still starting (not cached; ask again). A saved key is never sent to a Lemonade that is not on this PC. Requires X-Gaia-UI: 1 (403 without it).
Register a cloud provider (fireworks or amd) in Lemonade and give it the key. Body: {"api_key", "base_url", "auth_header_name", "auth_header_prefix"}; the last three apply to amd only. A blank api_key keeps the current key.While GAIA uses the user’s own Lemonade (lemonade_mode external) it sends that server nothing: it succeeds only when the server already has the provider working, and a pasted key is not sent anywhere.Errors: 401 when the provider rejects the key or discovers no models (a key that discovers nothing is removed from Lemonade again); 403 when a key is sent through the mobile tunnel; 409 when an environment key takes precedence, Lemonade is not on this PC, or the user’s own server has no working provider.
Clear the key from Lemonade and from the OS credential store. Returns {"removed": bool}. On the user’s own Lemonade only GAIA’s stored copy is removed; the server keeps its key.
Chat models for a provider (embedding, reranker, speech, and image models are left out). local lists downloaded models plus the default model; cloud providers list their own. Recommended models come first with a rank, note, and the measured evidence behind the pick.
Make a model the one new turns run and remember it as last_model / last_provider in ~/.gaia/config.json (other keys are kept). Body: {"model": "fireworks.glm-5p3-flash"}. 409 when the model cannot run now; 503 while the local model server is still starting. A successful pick clears this run’s restore notice.

Skills

The skills the GAIA agent can load, found in the same roots the agent searches. Returns {"skills": [{"name", "description", "version", "tier", "tools", "path"}], "invalid": {...}}. 503 when the GAIA agent package is not installed.

Hub

Install and uninstall agents from the Agent Hub catalog — the backend for the desktop app’s gaia://hub/install/<id> deep link.
Unified catalog: the remote hub merged with what’s installed locally.
Start an install in the background (body names the agent id). Poll install-status for progress.
Progress of an in-flight or completed install.
Uninstall a hub-installed agent. Refuses a builtin (400).

Security

The backend binds loopback and has no authentication while the tunnel is off, so any web page the user’s browser visits could otherwise drive it. Three layers close that off — see src/gaia/ui/security.py for the enforcing code.
  1. CSRF header. Every mutating (non-GET/HEAD/OPTIONS) request to /api/* or /v1/* must carry X-Gaia-UI: 1. A cross-origin page cannot set a custom header without triggering a CORS preflight, and the app’s CORS policy only approves preflights from its own first-party dev origins. Missing the header is a 403. Three reads with side effects carry the same requirement: GET /api/setup/check, GET /api/providers and GET /api/providers/active.
  2. Origin allowlist. A mutating request whose Origin names anything other than this server, loopback, or the live tunnel is refused, so a CORS misconfiguration alone isn’t enough to get through.
  3. Host allowlist. DNS rebinding needs a hostname that resolves to loopback, so a Host that is neither an IP literal, loopback, nor the live tunnel is refused.
Origin: null and a missing Origin are both accepted: the packaged Electron shell serves the SPA from file:// (an opaque origin), and non-browser callers send no Origin at all. Neither can be produced by a cross-site page that also carries X-Gaia-UI, so layer 1 already stops those. GAIA_UI_ALLOWED_ORIGINS and GAIA_UI_ALLOWED_HOSTS (comma-separated) opt a reverse-proxied or named-host deployment in; neither is populated by default.

Flagship only

The Agent UI frontend sends X-Gaia-Client: agent-ui and runs the GAIA agent only. With that header, POST /api/sessions and POST /api/chat/send reject any other agent_type (422), an omitted one becomes gaia, and sending into a chat stored under another agent is a 409. Requests without the header — the eval harness, the MCP bridge — may still pick any registered agent.

Database Schema

The Agent UI uses SQLite with four tables:
SQLite settings: Foreign keys enabled, WAL journal mode for concurrent reads.
The indexing_status, file_mtime, agent_steps, and inference_stats columns (plus the settings table) are added via migrations for databases created before these existed. New databases include them in the initial schema.

Testing

Unit Testing with In-Memory Database

Database Testing


Integration with the Agent

The Agent UI server delegates to the GAIA Agent for LLM communication and tool execution:
Document indexing uses the RAG SDK:

npm Package

GAIA Agent UI is also available as an npm package for quick installation:
This provides the gaia-ui CLI command:
On first run, gaia-ui automatically installs the Python backend (uv, Python 3.12, amd-gaia) if not already present. On subsequent runs, it auto-updates if the version doesn’t match.

Package Contents

The npm package includes:

Release Management

The package version is sourced from src/gaia/version.py (single source of truth for all of GAIA):
Tags matching v* trigger the automated npm publish workflow.