# Navi API Reference

Base URL: `http://localhost:8000`

---

## REST API

### Health

#### `GET /health`

Server availability check.

**Response `200`**
```json
{ "status": "ok" }
```

---

### Profiles & Tools

#### `GET /agents/profiles`

List available agent profiles.

**Response `200`**
```json
[
  {
    "id": "secretary",
    "name": "Personal Secretary",
    "description": "General-purpose assistant",
    "enabled_tools": ["todo", "web_search", "filesystem", "..."],
    "llm_backend": "ollama",
    "model": "gemma4:31b-cloud"
  }
]
```

#### `GET /agents/tools`

List all registered tools (built-in + user tools).

**Response `200`**
```json
[
  { "name": "web_search", "description": "Search the web using DuckDuckGo." },
  { "name": "filesystem",  "description": "Read, write and list files." }
]
```

---

### Sessions

#### `POST /sessions`

Create a new session.

**Request body**
```json
{ "profile_id": "secretary" }
```

**Response `201`**
```json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "profile_id": "secretary",
  "created_at": "2026-04-10T18:00:00+00:00"
}
```

**Errors**
- `404` — profile not found

---

#### `GET /sessions`

List all sessions sorted by activity (pinned first).

**Response `200`**
```json
[
  {
    "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`**
```json
{
  "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 found

---

#### `DELETE /sessions/{session_id}`

Delete a session and its files.

**Response `204`** — no body

**Errors**
- `404` — session not found

---

#### `PATCH /sessions/{session_id}/pin`

Pin or unpin a session.

**Request body**
```json
{ "pinned": true }
```

**Response `200`**
```json
{ "session_id": "...", "pinned": true }
```

---

#### `POST /sessions/{session_id}/generate-name`

Generate 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`**
```json
{ "name": "Web search for recipes" }
```

Returns `{"name": null}` if there are no user messages yet.

**Errors**
- `404` — session not found

---

#### `GET /sessions/{session_id}/context`

LLM context (what the model actually sees). May differ from `messages` — compressed history replaces old turns with a summary. Debug endpoint.

**Response `200`**
```json
{
  "session_id": "...",
  "profile_id": "secretary",
  "message_count": 8,
  "total_chars": 4200,
  "context": [ ...same format as messages... ]
}
```

---

#### `GET /sessions/{session_id}/planning`

All planning phase debug logs for the session. Each entry is one planning run.

**Response `200`**
```json
{ "session_id": "...", "logs": [ { "phase": "...", "output": "..." }, ... ] }
```

---

#### `POST /sessions/{session_id}/files`

Upload a file for a session. Call before sending a message to attach the file.

**Request**: `multipart/form-data`, field `file`.

**Limits**
- Max size: 200 MB
- Forbidden extensions: `.exe`, `.dll`, `.so`, `.sh`, `.bat`, `.cmd`, `.ps1`, `.vbs`, `.bin`, `.elf`, and other executable formats
- Duplicate filenames get a numeric suffix

**Response `201`**
```json
{
  "name": "report.pdf",
  "size": 102400,
  "path": "session_files/550e8400-.../report.pdf",
  "content_type": "application/pdf"
}
```

**Errors**
- `400` — forbidden extension
- `404` — session not found
- `413` — file exceeds limit

---

#### `GET /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 attempt
- `404` — session or file not found

---

### Messages (non-streaming)

#### `POST /sessions/{session_id}/messages`

Send a message and receive a response synchronously (no streaming). Blocks until the full agent loop completes.

**Request body**
```json
{ "content": "How many stars are in the galaxy?" }
```

**Response `200`**
```json
{ "role": "assistant", "content": "Estimates range from 100 to 400 billion." }
```

**Errors**
- `404` — session not found
- `500` — agent error or iteration limit exceeded

> For production clients prefer WebSocket — it provides streaming, tool progress, and model reasoning.

---

## WebSocket

### `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).

---

### Client → Server

All client messages are JSON objects.

#### Send a message

```json
{
  "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 |

---

### Server → Client

Events arrive in the order they are emitted.

#### `stream_start`
```json
{ "type": "stream_start" }
```
Processing started. Client should block input.

---

#### `thinking_delta`
```json
{ "type": "thinking_delta", "delta": "reasoning fragment..." }
```
Streaming chunk of model reasoning. Accumulate until `thinking_end`.

---

#### `thinking_end`
```json
{ "type": "thinking_end" }
```
Reasoning phase complete. Next will be `stream_delta` or tool calls.

---

#### `turn_thinking`
```json
{
  "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`
```json
{
  "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`
```json
{
  "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`
```json
{
  "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`
```json
{
  "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`
```json
{ "type": "stream_delta", "delta": "response fragment..." }
```
Streaming chunk of the final text response. Accumulate into a string.

---

#### `stream_end`
```json
{
  "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`
```json
{ "type": "stream_stopped" }
```
Generation was stopped by `POST /sessions/{id}/stop`.

---

#### `profile_switched`
```json
{
  "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`
```json
{
  "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`
```json
{ "type": "heartbeat" }
```
Keepalive ping sent every 20 s during long silent operations. Client can ignore.

---

#### `session_sync`
```json
{ "type": "session_sync" }
```
Client must reload session history from `GET /sessions/{id}`. Sent:
1. On connect when no run is active (agent may have finished while disconnected).
2. After a reconnect-replay flow completes (ensures client sees the fully saved response).

---

#### `replay_start`
```json
{ "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`
```json
{ "type": "replay_end" }
```
Replay complete. Live events will follow.

---

#### `error`
```json
{ "type": "error", "message": "Session not found" }
```
Processing error. Stream may or may not continue after this.

---

### Typical event sequences

**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
```

---

## Files

**Client 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.

---

## Error codes

| 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 |
