Base URL: http://localhost:8000
GET /healthServer availability check.
Response 200
{ "status": "ok" }
Full auth documentation: docs/auth.md.
GET /auth/loginRedirect to gnexus-auth OAuth authorization endpoint. Sets PKCE + state internally.
Response 302 → Location: gnexus-auth /oauth/authorize
GET /auth/callbackOAuth callback. Validates state, exchanges code for tokens, creates DB session.
Query params
code — authorization code from gnexus-authstate — state parameterResponse 302
/ (with Set-Cookie)/auth/mobile-done?sid=<session_id>Errors
400 — invalid state, PKCE failure, or token exchange failedGET /auth/mobile-doneBridge page for Android OAuth. Renders HTML that attempts an automatic deep-link back into the native app via Chrome Intent URL (intent://...), and falls back to a manual button for browsers that block automatic navigation to custom schemes (e.g. DuckDuckGo). Styled with the gnexus UI kit design system.
Query params
sid — session id that will become the navi_auth_session cookieResponse 200 — HTML page
POST /auth/logoutLogout current user. Deletes DB session and clears cookie.
Response 200
{ "ok": true }
GET /auth/meReturn current authenticated user.
Response 200
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"role": "admin",
"permissions": ["navi.sessions.read_all", "navi.memory.read_all"]
}
Errors
401 — not authenticatedGET /agents/profilesList available agent profiles.
Response 200
[
{
"id": "secretary",
"name": "Personal Secretary",
"description": "General-purpose assistant",
"enabled_tools": ["todo", "web_search", "filesystem", "..."],
"llm_backend": "ollama",
"model": "gemma4:31b-cloud"
}
]
GET /agents/toolsList all registered tools (built-in + user tools).
Response 200
[
{ "name": "web_search", "description": "Search the web using DuckDuckGo." },
{ "name": "filesystem", "description": "Read, write and list files." }
]
POST /sessionsCreate a new session.
Request body
{ "profile_id": "secretary" }
Response 201
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"profile_id": "secretary",
"created_at": "2026-04-10T18:00:00+00:00"
}
Errors
404 — profile not foundGET /sessionsList all sessions sorted by activity (pinned first).
Response 200
[
{
"session_id": "550e8400-...",
"profile_id": "secretary",
"name": "Research task",
"message_count": 12,
"preview": "Last 60 chars of the most recent message",
"pinned": false,
"created_at": "2026-04-10T15:00:00+00:00",
"last_active": "2026-04-10T18:00:00+00:00"
}
]
name is null until POST /sessions/{id}/generate-name is called.
GET /sessions/{session_id}Full session with message history (display history — never compressed).
Response 200
{
"session_id": "550e8400-...",
"profile_id": "secretary",
"name": "Research task",
"context_token_count": 4913,
"max_context_tokens": 65536,
"created_at": "...",
"last_active": "...",
"messages": [
{
"role": "user",
"content": "Hello",
"created_at": "2026-04-10T18:00:00+00:00"
},
{
"role": "assistant",
"content": "Hi. How can I help?",
"created_at": "2026-04-10T18:00:05+00:00"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "abc123",
"name": "web_search",
"arguments": { "query": "..." }
}
]
},
{
"role": "tool",
"content": "tool result",
"tool_call_id": "abc123",
"name": "web_search"
}
]
}
Message fields (role is always present, others by availability):
| Field | Type | Description | |||
|---|---|---|---|---|---|
role |
`user\ | assistant\ | tool\ | system` | Message author |
content |
`string\ | null` | Text content | ||
images |
string[] |
Base64 images (user/assistant) | |||
tool_calls |
ToolCall[] |
Tool invocations (assistant) | |||
tool_call_id |
string |
ID of the call this result belongs to (tool) | |||
name |
string |
Tool name (tool messages) | |||
thinking |
`string\ | null` | LLM reasoning captured during a tool-calling turn | ||
is_plan |
bool |
Planning phase output — rendered as a plan card, not text | |||
is_compression |
bool |
Marker injected when context compression ran | |||
is_summary |
bool |
Summary message replacing compressed history | |||
created_at |
string (ISO 8601) |
Creation time | |||
elapsed_seconds |
`number\ | null` | Time to complete the turn (final assistant message) | ||
tool_call_count |
`number\ | null` | Number of tool calls in the turn | ||
token_count |
`number\ | null` | Tokens used in the turn |
Errors
404 — session not foundDELETE /sessions/{session_id}Delete a session and its files.
Response 204 — no body
Errors
404 — session not foundPATCH /sessions/{session_id}/pinPin or unpin a session.
Request body
{ "pinned": true }
Response 200
{ "session_id": "...", "pinned": true }
POST /sessions/{session_id}/generate-nameGenerate a short display name for a session from its message history. Called automatically by the client after the first exchange. No-op if the session already has a name.
Response 200
{ "name": "Web search for recipes" }
Returns {"name": null} if there are no user messages yet.
Errors
404 — session not foundGET /sessions/{session_id}/contextLLM context (what the model actually sees). May differ from messages — compressed history replaces old turns with a summary. Debug endpoint.
Response 200
{
"session_id": "...",
"profile_id": "secretary",
"message_count": 8,
"total_chars": 4200,
"context": [ ...same format as messages... ]
}
GET /sessions/{session_id}/planningAll planning phase debug logs for the session. Each entry is one planning run.
Response 200
{ "session_id": "...", "logs": [ { "phase": "...", "output": "..." }, ... ] }
POST /sessions/{session_id}/filesUpload a file for a session. Call before sending a message to attach the file.
Request: multipart/form-data, field file.
Limits
.exe, .dll, .so, .sh, .bat, .cmd, .ps1, .vbs, .bin, .elf, and other executable formatsResponse 201
{
"name": "report.pdf",
"size": 102400,
"path": "session_files/550e8400-.../report.pdf",
"content_type": "application/pdf"
}
Errors
400 — forbidden extension404 — session not found413 — file exceeds limitGET /sessions/{session_id}/files/{filename}Download or view an uploaded file. Images, PDFs and plain text are served inline; everything else as an attachment.
Response 200 — file bytes
Errors
403 — path traversal attempt404 — session or file not foundPOST /sessions/{session_id}/messagesSend a message and receive a response synchronously (no streaming). Blocks until the full agent loop completes.
Request body
{ "content": "How many stars are in the galaxy?" }
Response 200
{ "role": "assistant", "content": "Estimates range from 100 to 400 billion." }
Errors
404 — session not found500 — agent error or iteration limit exceededFor production clients prefer WebSocket — it provides streaming, tool progress, and model reasoning.
WS /ws/sessions/{session_id}Main channel for real-time agent interaction. Supports text streaming, thinking streaming, tool events, file and image attachment.
Connect: if the session is not found, the server closes with code 4004.
On connect: the server immediately sends session_sync (no active run) or starts the reconnect replay flow (run in progress).
All client messages are JSON objects.
{
"type": "message",
"content": "Message text",
"images": ["base64string...", "..."],
"files": [
{ "name": "report.pdf", "size": 102400, "path": "session_files/.../report.pdf" }
]
}
| Field | Required | Description |
|---|---|---|
type |
yes | Always "message" |
content |
yes | Message text (non-empty) |
images |
no | Base64 image list. Both raw base64 and data:image/...;base64,... are accepted — server strips the prefix |
files |
no | Files uploaded via POST /sessions/{id}/files. Server appends their paths to the message content |
Events arrive in the order they are emitted.
stream_start{ "type": "stream_start" }
Processing started. Client should block input.
thinking_delta{ "type": "thinking_delta", "delta": "reasoning fragment..." }
Streaming chunk of model reasoning. Accumulate until thinking_end.
thinking_end{ "type": "thinking_end" }
Reasoning phase complete. Next will be stream_delta or tool calls.
turn_thinking{
"type": "turn_thinking",
"thinking": "full reasoning text...",
"is_subagent": false
}
Complete reasoning block from a tool-calling turn. Not streamed — arrives whole. is_subagent: true means this reasoning came from a subagent inside spawn_agent.
planning_status{
"type": "planning_status",
"phase": "analysis",
"label": "Analysing request...",
"is_subagent": false
}
Progress update during the planning phase. phase is one of analysis, reflect, plan. is_subagent: true — route into the spawn_agent card, not the top-level UI.
plan_ready{
"type": "plan_ready",
"plan": "1. Step one\n2. Step two\n...",
"is_subagent": false
}
Planning complete — full step list. Rendered as a collapsible plan card. is_subagent: true — route into the spawn_agent card.
tool_started{
"type": "tool_started",
"tool": "web_search",
"args": { "query": "weather in moscow" },
"is_subagent": false
}
Agent started executing a tool. Arrives before execution completes — show a spinner. is_subagent: true — call from a subagent.
tool_call{
"type": "tool_call",
"tool": "web_search",
"args": { "query": "weather in moscow" },
"result": "Today +12°C, cloudy.",
"success": true,
"is_subagent": false
}
Tool finished. Arrives after tool_started with the same tool and args. success: false — tool returned an error.
stream_delta{ "type": "stream_delta", "delta": "response fragment..." }
Streaming chunk of the final text response. Accumulate into a string.
stream_end{
"type": "stream_end",
"content": "full response text",
"context_tokens": 4913,
"max_context_tokens": 65536,
"elapsed_seconds": 12.4,
"tool_call_count": 3,
"token_count": 1842
}
Agent finished. content is the full accumulated text (duplicates the sum of stream_delta). Client should unblock input.
stream_stopped{ "type": "stream_stopped" }
Generation was stopped by POST /sessions/{id}/stop.
profile_switched{
"type": "profile_switched",
"profile_id": "server_admin",
"profile_name": "Server Administrator"
}
Agent switched profile via switch_profile tool. New profile takes effect on the next user message. Client should update the profile indicator. Arrives during the stream — before tool_call for switch_profile.
context_compressed{
"type": "context_compressed",
"messages_before": 42,
"messages_after": 12,
"summary": "User asked about..."
}
Context was automatically compressed (triggers at ≥80% of context window). Informational.
heartbeat{ "type": "heartbeat" }
Keepalive ping sent every 20 s during long silent operations. Client can ignore.
session_sync{ "type": "session_sync" }
Client must reload session history from GET /sessions/{id}. Sent:
replay_start{ "type": "replay_start", "count": 14 }
About to replay count buffered events from a mid-stream reconnect. Client should suppress cursor animations and in-progress effects during replay.
replay_end{ "type": "replay_end" }
Replay complete. Live events will follow.
error{ "type": "error", "message": "Session not found" }
Processing error. Stream may or may not continue after this.
Simple question, no tools:
stream_start thinking_delta × N (if model has thinking enabled) thinking_end stream_delta × N stream_end
Request with tool calls:
stream_start turn_thinking (reasoning before tool selection, if any) tool_started tool_call turn_thinking (before next tool, if any) tool_started tool_call thinking_delta × N (final response reasoning) thinking_end stream_delta × N stream_end context_compressed (optional, if context was near full)
Request with planning enabled:
stream_start planning_status (phase: analysis) planning_status (phase: plan) plan_ready turn_thinking tool_started tool_call ... stream_end
Request with subagent (spawn_agent):
stream_start tool_started (spawn_agent, is_subagent=false) turn_thinking (is_subagent=true) planning_status (is_subagent=true, if subagent has planning) plan_ready (is_subagent=true, if subagent has planning) tool_started (subagent tool, is_subagent=true) tool_call (is_subagent=true) tool_call (spawn_agent done, is_subagent=false) stream_delta × N stream_end
Reconnect mid-stream:
stream_start
replay_start {"count": N}
ev_0 ... ev_N-1 (buffered events replayed verbatim)
replay_end
(live events continue)
...
stream_end
session_sync
Profile switch (switch_profile):
stream_start tool_started (switch_profile) profile_switched (client updates UI here — before tool_call) tool_call (switch_profile done) stream_delta × N stream_end
All admin endpoints require admin role or specific permissions.
GET /admin/sessionsAll sessions across all users. Supports pagination, search, and sorting.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | search | — | Filter by session_id, name, user_id or profile_id (case-insensitive) | | sort_by | last_active | last_active, created_at, name, profile_id, user_id, pinned | | sort_order | desc | asc or desc |
Response 200
{
"total": 128,
"limit": 50,
"offset": 0,
"items": [
{
"session_id": "...",
"profile_id": "secretary",
"user_id": "user-uuid",
"name": "Research task",
"message_count": 12,
"pinned": false,
"created_at": "2026-05-04T10:00:00+00:00",
"last_active": "2026-05-04T10:30:00+00:00"
}
]
}
GET /admin/usersAll registered navi_users.
Response 200
[
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"role": "admin",
"permissions": ["navi.sessions.read_all"],
"created_at": "...",
"updated_at": "..."
}
]
GET /admin/memoryAll memory facts (global view). Requires navi.memory.read_all. Supports pagination, search, and sorting.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | search | — | Filter by key, value or category (case-insensitive) | | sort_by | updated_at | updated_at, category, key, confidence, source | | sort_order | desc | asc or desc | | user_id | — | If set, return only facts for this user instead of global view |
Response 200
{
"total": 128,
"limit": 50,
"offset": 0,
"items": [
{
"id": "...",
"category": "profile",
"key": "name",
"value": "Eugene",
"source": "conversation",
"confidence": 90,
"updated_at": "2026-05-04T10:00:00+00:00"
}
]
}
GET /admin/profilesAll profiles including admin-only ones. Requires navi.profiles.manage.
PATCH /admin/users/{user_id}/roleUpdate cached role. Requires admin.
Request body
{ "role": "admin" }
Response 200
{ "ok": true }
PATCH /admin/profiles/{profile_id}/availabilityToggle is_admin_only. Requires navi.profiles.manage.
Request body
{ "is_admin_only": true }
Response 200
{ "ok": true, "note": "Profile availability is managed via profile config files" }
POST /webhooks/gnexus-authReceive webhooks from gnexus-auth. Verified via HMAC.
Response 200
{ "ok": true }
Errors
400 — invalid payloadClient static: GET /static/** — served from client/ directory. Header Cache-Control: no-store.
Session uploaded files: stored in session_files/{session_id}/. Agent accesses them via the filesystem tool. Deleted after 24 h of session inactivity or when the session is deleted.
| HTTP | Reason |
|---|---|
400 |
Forbidden file type |
404 |
Session or profile not found |
413 |
File exceeds 200 MB |
500 |
Internal agent error |
WS 4004 |
Session not found on connect |