Base URL: http://localhost:8000
GET /healthServer availability check.
Response 200
{
"status": "ok",
"embed": {
"status": "ok",
"model": "nomic-embed-text",
"dimensions": 768
}
}
GET /health/embedEmbedding model health check. Returns the first vector of a test string to verify the embed backend is responsive.
Response 200
{
"status": "ok",
"model": "nomic-embed-text",
"dimensions": 768
}
Full auth documentation: docs/auth.md.
GET /auth/loginRedirect to gnexus-auth OAuth authorization endpoint. Sets PKCE + state internally.
Query params
return_to — URL to redirect back to after login (default: /)platform — browser (default) or android (affects redirect after callback)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 failed503 — OAuth is not configured (missing gnexus_auth_client_id or gnexus_auth_client_secret)GET /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",
"username": "username",
"first_name": "First",
"last_name": "Last",
"phone": "+1234567890",
"birth_date": "1990-01-01",
"country": "US",
"city": "New York",
"locale": "en-US",
"avatar_url": "https://...",
"profile_url": "https://...",
"role": "admin",
"permissions": ["navi.sessions.read_all", "navi.memory.read_all"]
}
Errors
401 — not authenticatedGET /auth/statusCheck if the user is currently authenticated without returning full profile.
Response 200
{ "authenticated": true }
GET /agents/profilesList available agent profiles. Non-admin users do not see is_admin_only profiles.
Response 200
[
{
"id": "secretary",
"name": "Personal Secretary",
"description": "General-purpose assistant",
"enabled_tools": ["todo", "mcp__navi_web__web_search", "filesystem", "..."],
"llm_backend": "ollama",
"model": ["gemma4:31b-cloud", "gemma4:26b-a4b-it-q4_K_M"],
"temperature": 0.65,
"top_k": null,
"top_p": null,
"max_iterations": 10,
"iteration_budget_enabled": true,
"think_enabled": true,
"subagent_think_enabled": null,
"mcp_servers": {"gnexus-book": ["read", "write"]}
}
]
GET /agents/toolsList all registered tools (built-in + user tools).
Response 200
[
{
"name": "mcp__navi_web__web_search",
"description": "Search the web using DuckDuckGo.",
"parameters": {"type": "object", "properties": {...}, "required": [...]}
},
{
"name": "filesystem",
"description": "Read, write and list files.",
"parameters": {"type": "object", "properties": {...}, "required": [...]}
}
]
GET /agents/promptsReturn the fully resolved system prompt for each profile (persona + profile system_prompt + context provider injections + MCP server instructions).
Response 200
{
"secretary": "system prompt text...",
"server_admin": "system prompt text..."
}
GET /agents/mcp_serversReturn all configured MCP servers with their resolved tools per profile.
Response 200
{
"gnexus-book": {
"connected": true,
"tools": [
{"name": "gnexus-book_list_inventory", "description": "..."}
],
"instructions": "MANDATORY: Before answering ANY question..."
}
}
POST /sessionsCreate a new session.
Auth: requires authenticated user.
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
401 — not authenticated404 — profile not foundGET /sessionsList all sessions sorted by activity (pinned first).
Auth: requires authenticated user.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | profile_id | — | Filter by profile |
Response 200 (when pagination params provided)
{
"items": [
{
"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"
}
],
"limit": 50,
"offset": 0,
"has_more": true,
"next_offset": 50
}
Response 200 (plain list when no pagination params)
[
{
"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).
Auth: requires authenticated user (or ownership of the session).
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": "mcp__navi_web__web_search",
"arguments": { "query": "..." }
}
]
},
{
"role": "tool",
"content": "tool result",
"tool_call_id": "abc123",
"name": "mcp__navi_web__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 foundGET /sessions/{session_id}/recallGet the pending recall for a session, if any. Returns the first pending recall only.
Response 200
{
"id": "r1",
"call_type": "once",
"trigger_at": "2026-05-16T14:00:00+00:00",
"interval_seconds": null,
"internal_comment": "Check build logs",
"additional_context_message": "Read /tmp/build.log...",
"status": "pending"
}
Response 200 (no pending recall)
{ "recall": null }
Errors
404 — session not foundDELETE /sessions/{session_id}/recallCancel the pending recall for this session.
Response 200
{ "ok": true }
Errors
404 — session not foundPOST /sessions/{session_id}/recall/skipSkip the next occurrence of a recurring recall (advances trigger_at by interval_seconds).
Response 200
{ "ok": true }
Errors
404 — session not found400 — no recurring pending recallDELETE /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.
Auth: requires admin role.
Response 200
{
"session_id": "...",
"profile_id": "secretary",
"message_count": 8,
"total_chars": 4200,
"context": [ ...same format as messages... ]
}
Errors
403 — not adminGET /sessions/{session_id}/planningAll planning phase debug logs for the session. Each entry is one planning run.
Auth: requires admin role.
Response 200
{ "session_id": "...", "logs": [ { "phase": "...", "output": "..." }, ... ] }
Errors
403 — not adminGET /sessions/{session_id}/contentList published session content (artifacts registered via content_publish tool).
Response 200
{
"content": [
{
"id": "...",
"filename": "report.html",
"path": "/abs/path/to/content/...",
"size": 102400,
"content_type": "text/html",
"created_at": "2026-04-10T18:00:00+00:00"
}
]
}
Errors
404 — session not foundPOST /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": "/abs/path/to/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, plain text and HTML are served inline; everything else as an attachment.
Query params
download — force attachment download regardless of content typeResponse 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": "mcp__navi_web__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": "mcp__navi_web__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.
recall_update{
"type": "recall_update",
"session_id": "550e8400-...",
"recall_id": "r1",
"call_type": "once",
"trigger_at": "2026-05-16T14:00:00+00:00",
"status": "pending",
"action": "scheduled"
}
Recall state changed. Sent when a recall is scheduled, cancelled, fired, or rescheduled. Client should refresh the recall banner via GET /sessions/{id}/recall.
Fields | Field | Type | Description | |-------|------|-------------| | session_id | string | Affected session | | recall_id | string\|null | Recall ID (null for cancel without ID) | | call_type | string\|null | once, recurring, immediate | | trigger_at | string\|null | Next trigger time (ISO 8601) | | status | string\|null | pending, fired, cancelled | | action | string\|null | scheduled, cancelled, fired, rescheduled |
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.
GET /admin/sessions/{session_id}Full session details including messages. Bypasses ownership check.
Response 200
{
"session_id": "...",
"profile_id": "secretary",
"user_id": "user-uuid",
"name": "Research task",
"messages": [...],
"context_token_count": 4913,
"max_context_tokens": 65536,
"pinned": false,
"created_at": "...",
"last_active": "..."
}
Errors
404 — session not foundDELETE /admin/sessions/{session_id}Delete any session (bypasses ownership). Also deletes session files.
Response 204 — no body
Errors
404 — session not foundGET /admin/users/{user_id}Single user details.
Response 200
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"role": "admin",
"permissions": ["navi.sessions.read_all"],
"created_at": "...",
"updated_at": "..."
}
Errors
404 — user not foundGET /admin/users/{user_id}/sessionsSessions owned by a specific user.
Response 200
[
{
"session_id": "...",
"profile_id": "secretary",
"name": "Research task",
"message_count": 12,
"pinned": false,
"created_at": "...",
"last_active": "..."
}
]
POST /admin/ollama/clear-blacklistsManually clear dead-server and dead-model blacklists for the Ollama fallback backend. Useful when a transient failure caused a 5-minute blacklist and you want immediate recovery.
Response 204 — no body
GET /admin/profiles/{profile_id}Full profile configuration including system prompt.
Response 200
{
"id": "secretary",
"name": "Personal Secretary",
"description": "General-purpose assistant",
"short_description": "...",
"full_description": {"specialization": "...", "when_to_use": "...", "key_tools": [...]},
"system_prompt": "...",
"subagent_system_prompt": "...",
"llm_backend": "ollama",
"model": ["gemma4:31b-cloud", "gemma4:26b-a4b-it-q4_K_M"],
"temperature": 0.65,
"top_k": null,
"top_p": null,
"num_thread": null,
"max_iterations": 10,
"planning_enabled": false,
"planning_mandatory": false,
"planning_phase1_enabled": true,
"planning_phase2_enabled": false,
"planning_phase3_enabled": true,
"think_enabled": true,
"iteration_budget_enabled": true,
"goal_anchoring_enabled": true,
"goal_anchoring_interval": 5,
"anti_stall_enabled": true,
"anti_stall_threshold": 8,
"step_validation_enabled": false,
"adaptive_replan_enabled": false,
"subagent_tools": [...],
"subagent_planning_enabled": false,
"subagent_think_enabled": null,
"enabled_tools": [...],
"context_providers": [],
"is_admin_only": false
}
Errors
404 — profile not foundPUT /admin/profiles/{profile_id}Update profile configuration on disk and in-memory. Accepts partial updates — only provided fields are modified.
Request body (partial)
{
"temperature": 0.5,
"max_iterations": 20,
"planning_enabled": true
}
Response 200
{ "ok": true }
Errors
400 — invalid profile data404 — profile not foundPATCH /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 }
POST /webhooks/gnexus-authReceive webhooks from gnexus-auth. Verified via HMAC.
Response 200
{ "ok": true }
Errors
400 — invalid payload503 — OAuth not configuredClient 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 |