Skip to content
Zachary BENSALEM edited this page Oct 20, 2026 · 5 revisions

HTTP API

Active contributors: Zachary Bensalem

The API is the transport between the terminal client (or any other client) and a Fleet process. It is a single FastAPI application created by src/fleet_rlm/api/app.py. It owns no reasoning logic: every route either reads durable state from PostgreSQL through the Store, or asks a SessionRuntime to do something, including running a Turn.

Purpose

The API exposes five capabilities:

  • Session discovery, configuration, mode, model selection, context, and history.
  • A Session's workspace files and attachments.
  • Memory and Skill synchronization.
  • One streaming Turn endpoint.
  • A health endpoint used to decide whether an already-running server can be reused.

There is no caller authentication. The server binds to 127.0.0.1 by default, and a non-loopback bind is an explicit opt-in that still does not authenticate callers. See Security for the trust boundary.

Application object

create_app in src/fleet_rlm/api/app.py takes optional settings, an optional Store, and two flags (initialize_db, configure_observability). The Store is not opened at construction time. The lifespan acquires it, so importing the app or generating the OpenAPI document does not touch PostgreSQL, Daytona, or MLflow.

The lifespan does, in order:

  1. Open the Store (Store.open) unless a Store was injected. The runtime pool opens without waiting so a cold Lakebase connect does not block startup.
  2. Run pending schema migrations (Store.initialize) in a worker thread, then close the dedicated migration pool.
  3. Initialize MLflow tracing (_configure_tracing) and store the experiment ID on app.state. A local profile that cannot reach its tracking server, or any initialization error, raises RuntimeError and aborts startup.
  4. Build the SessionRuntimeRegistry.
  5. On shutdown, close the registry (sandboxes then leases), close the Store, and flush MLflow's async trace logging. The flush is suppressed on error because uvicorn re-raises SIGTERM and would otherwise skip MLflow's exit-time flush.

Route dependencies live in src/fleet_rlm/api/deps.py. get_settings returns app.state.settings, get_store and get_registry answer 503 when the corresponding resource is missing, and get_runtime resolves the path's Session ID through the registry, translating a bad ID into 422.

Route modules

Module Prefix area Router
src/fleet_rlm/api/routes/sessions.py Sessions, models, mode, history, Turn status, context sessions.router
src/fleet_rlm/api/routes/sessions.py Memory sessions.memory_router
src/fleet_rlm/api/routes/sessions.py Skills sessions.skills_router
src/fleet_rlm/api/routes/workspace.py Workspace and attachments workspace.router
src/fleet_rlm/api/routes/turns.py Turn stream turns.router

All routers are included without a global prefix, so paths are absolute. The route inventory is in Endpoints and the generated contract is openapi.yaml, checked with make api-check.

Request handling model

All handlers except the Turn stream are plain def functions. FastAPI runs them in its bounded threadpool, which is what makes blocking calls (psycopg, Daytona volume access, model discovery) safe without turning them async. Only POST /sessions/{session_id}/turn is async def; it multiplexes a queue and runs the Turn itself on a worker thread.

Request and response bodies are Pydantic models in src/fleet_rlm/api/schemas.py. Every request model inherits _Request, which sets extra="forbid", so a misspelled option is a 422 instead of being silently ignored. FleetTask text is capped at MAX_TASK_CHARS; workspace writes and uploads are capped at 25 MB.

Errors are translated at the route boundary by translate in src/fleet_rlm/api/deps.py:

  • a ValueError becomes 422 (invalid message or the exception text);
  • any other exception becomes the route's sanitized 503 message, logged by type only;
  • an HTTPException raised inside the block passes through unchanged.

The translate helper is also used with status=404 for workspace reads, stat, and delete, where an invalid path and a missing file are the same client answer.

Health and server reuse

GET /health is a synchronous route that reads app.state. It returns status, version (package metadata for fleet-rlm), and an mlflow object with the non-secret destination: mode, tracking URI, workspace host, experiment names, trace location, resolved experiment ID, and ready.

The health payload contains no credentials. fleet cli and evaluation preflight compare this destination before reusing an already-running server (src/fleet_rlm/launcher.py). A server pointed at a different MLflow experiment or workspace is rejected rather than silently reused.

Session lock

All Session-scoped state changes and reads that touch the sandbox take SessionRuntime.turn_lock through session_lock. The wait is two seconds; a Turn can hold the lock for the whole Turn timeout, and queueing requests on a bounded threadpool would pin workers. When the wait expires the route answers 409 with a retry message. Memory, Skill, context, mode, model, workspace, and attach routes declare this response in their OpenAPI entry through LOCKED.

The lock also fences a stale worker: after acquiring it, session_lock calls assert_lease_held() and answers 503 if this process no longer owns the Session's PostgreSQL advisory lease.

Turn status and history deliberately do not take the lock. They are durable PostgreSQL reads that stay available while another worker owns the runtime.

graph TD
    Client["TUI or HTTP client"] --> App["FastAPI app (api/app.py)"]
    App --> Deps["deps.py<br/>settings, Store, registry, runtime"]
    Deps --> Store["Store<br/>PostgreSQL"]
    Deps --> Registry["SessionRuntimeRegistry<br/>advisory leases"]
    Registry --> Runtime["SessionRuntime"]
    Runtime --> Engine["AdaptiveFleetEngine"]
    Runtime --> Sandbox["DaytonaSandboxManager"]
    App --> Health["GET /health<br/>version + MLflow destination"]
    App --> Turns["POST /sessions/{id}/turn<br/>SSE"]
Loading

Integration points

  • The terminal client consumes the same routes documented here; see src/fleet_rlm/tui/app.py.
  • The Store schema and migration ledger are described in Store schema and migrations.
  • Turn orchestration inside the worker thread is described in Turn pipeline.
  • Event envelope construction is described in SSE streaming.

Entry points for modification

  • New route area: add a module under src/fleet_rlm/api/routes/, include its router in create_app, and regenerate openapi.yaml with make api-sync.
  • New request/response shape: add a model to src/fleet_rlm/api/schemas.py; request models must inherit _Request.
  • New dependency or error translation rule: src/fleet_rlm/api/deps.py.
  • New lifecycle resource: the lifespan context in src/fleet_rlm/api/app.py, with cleanup in the reverse order of acquisition.

Key source files

File Role
src/fleet_rlm/api/app.py create_app, lifespan, router inclusion, /health, MLflow startup check
src/fleet_rlm/api/deps.py Dependencies, translate, session_lock, 409 constant
src/fleet_rlm/api/schemas.py Pydantic request and response models
src/fleet_rlm/api/events.py DSPy callback and SSE envelope
src/fleet_rlm/api/routes/sessions.py Session, model, mode, history, context, memory, Skills routes
src/fleet_rlm/api/routes/workspace.py File listing, stat, read, write, delete, attach
src/fleet_rlm/api/routes/turns.py Streaming Turn handler
openapi.yaml Generated API contract
docs/reference/http-api.md User-facing route reference

Related

Clone this wiki locally