# Navi API Reference

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

---

## REST API

### Health

#### `GET /health`

Проверка доступности сервера.

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

---

### Profiles & Tools

#### `GET /agents/profiles`

Список доступных профилей агента.

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

#### `GET /agents/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`

Создать новую сессию.

**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` — профиль не найден

---

#### `GET /sessions`

Список всех сессий, отсортированных по активности (закреплённые первыми).

**Response `200`**
```json
[
  {
    "session_id": "550e8400-...",
    "profile_id": "secretary",
    "message_count": 12,
    "preview": "Последние 60 символов последнего сообщения",
    "pinned": false,
    "created_at": "2026-04-10T15:00:00+00:00",
    "last_active": "2026-04-10T18:00:00+00:00"
  }
]
```

---

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

Полная информация о сессии с историей сообщений (display history — никогда не сжимается).

**Response `200`**
```json
{
  "session_id": "550e8400-...",
  "profile_id": "secretary",
  "created_at": "...",
  "last_active": "...",
  "messages": [
    {
      "role": "user",
      "content": "Привет",
      "created_at": "2026-04-10T18:00:00+00:00"
    },
    {
      "role": "assistant",
      "content": "Привет. Чем помочь?",
      "created_at": "2026-04-10T18:00:05+00:00"
    },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "abc123",
          "name": "web_search",
          "arguments": { "query": "..." }
        }
      ]
    },
    {
      "role": "tool",
      "content": "результат инструмента",
      "tool_call_id": "abc123",
      "name": "web_search"
    }
  ]
}
```

Поля сообщения (`role` всегда присутствует, остальные — по наличию):

| Поле           | Тип                  | Описание |
|----------------|----------------------|----------|
| `role`         | `user\|assistant\|tool\|system` | Автор сообщения |
| `content`      | `string\|null`       | Текст сообщения |
| `images`       | `string[]`           | Base64-строки изображений (user/assistant) |
| `tool_calls`   | `ToolCall[]`         | Вызовы инструментов (assistant) |
| `tool_call_id` | `string`             | ID вызова, к которому относится ответ (tool) |
| `name`         | `string`             | Имя инструмента (tool) |
| `created_at`   | `string` (ISO 8601)  | Время создания |
| `is_summary`   | `bool`               | Сжатый блок истории (injected by compressor) |

**Errors**
- `404` — сессия не найдена

---

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

Удалить сессию и её файлы.

**Response `204`** — нет тела

**Errors**
- `404` — сессия не найдена

---

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

Закрепить или открепить сессию.

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

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

---

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

LLM-контекст сессии (то, что модель реально видит). Может отличаться от `messages` — сжатые истории заменяют часть сообщений. Endpoint для отладки.

**Response `200`**
```json
{
  "session_id": "...",
  "profile_id": "secretary",
  "message_count": 8,
  "total_chars": 4200,
  "context": [ ...сообщения в том же формате, что и messages... ]
}
```

---

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

Загрузить файл для сессии. Используется перед отправкой сообщения, если нужно приложить файл.

**Request**: `multipart/form-data`, поле `file`.

**Ограничения**
- Максимальный размер: 200 MB
- Запрещённые расширения: `.exe`, `.dll`, `.so`, `.sh`, `.bat`, `.cmd`, `.ps1`, `.vbs`, `.bin`, `.elf` и другие исполняемые форматы
- При совпадении имён файл переименовывается (`file_1.txt`, `file_2.txt`, ...)
- Файлы удаляются автоматически через 24 часа неактивности сессии

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

**Errors**
- `400` — запрещённое расширение
- `404` — сессия не найдена
- `413` — файл превышает лимит

---

### Messages (non-streaming)

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

Отправить сообщение и получить ответ синхронно (без стриминга). Блокирует до завершения всего цикла агента.

**Request body**
```json
{ "content": "Сколько звёзд в галактике?" }
```

**Response `200`**
```json
{ "role": "assistant", "content": "По оценкам, от 100 до 400 миллиардов." }
```

**Errors**
- `404` — сессия не найдена
- `500` — ошибка агента или превышен лимит итераций

> Для production-клиентов предпочтительнее WebSocket — он даёт стриминг, прогресс выполнения инструментов и мышление модели.

---

## WebSocket

### `WS /ws/sessions/{session_id}`

Основной канал для общения с агентом в реальном времени. Поддерживает стриминг текста, стриминг мышления, события инструментов, прикрепление файлов и изображений.

**Подключение**: если сессия не найдена, сервер закрывает соединение с кодом `4004`.

**Reconnect**: если клиент переподключается во время активного стрима, сервер автоматически дошлёт пропущенные события — никаких дополнительных действий не требуется.

---

### Client → Server

Все сообщения от клиента — JSON-объекты.

#### Отправка сообщения

```json
{
  "type": "message",
  "content": "Текст сообщения",
  "images": ["base64string...", "..."],
  "files": [
    { "name": "report.pdf", "size": 102400, "path": "session_files/.../report.pdf" }
  ]
}
```

| Поле      | Обязательно | Описание |
|-----------|-------------|----------|
| `type`    | да          | Всегда `"message"` |
| `content` | да          | Текст сообщения (непустой) |
| `images`  | нет         | Список base64-строк изображений. Допускается как чистый base64, так и `data:image/...;base64,...` — сервер обрежет префикс автоматически |
| `files`   | нет         | Файлы, загруженные через `POST /sessions/{id}/files`. Сервер дописывает их пути в текст сообщения, чтобы агент знал об их существовании |

---

### Server → Client

Сервер присылает события последовательно в порядке их возникновения.

#### `stream_start`
```json
{ "type": "stream_start" }
```
Начало обработки сообщения. Клиент должен заблокировать ввод.

---

#### `thinking_delta`
```json
{ "type": "thinking_delta", "delta": "фрагмент мышления..." }
```
Фрагмент внутреннего рассуждения модели (streaming). Приходит только если у модели включён режим thinking. Накапливайте `delta` до `thinking_end`.

---

#### `thinking_end`
```json
{ "type": "thinking_end" }
```
Мышление завершено. После этого начнётся либо стриминг текста (`stream_delta`), либо вызов инструментов.

---

#### `turn_thinking`
```json
{
  "type": "turn_thinking",
  "thinking": "полный текст рассуждения...",
  "is_subagent": false
}
```
Блок мышления модели во время выбора инструмента. Приходит целиком (не по кускам). `is_subagent: true` означает, что мышление от субагента внутри `spawn_agent`.

---

#### `tool_started`
```json
{
  "type": "tool_started",
  "tool": "web_search",
  "args": { "query": "погода в москве" },
  "is_subagent": false
}
```
Агент начал выполнение инструмента. Приходит немедленно, до завершения вызова — для показа спиннера. `is_subagent: true` — вызов из субагента.

---

#### `tool_call`
```json
{
  "type": "tool_call",
  "tool": "web_search",
  "args": { "query": "погода в москве" },
  "result": "Сегодня в Москве +12°C, облачно.",
  "success": true,
  "is_subagent": false
}
```
Инструмент завершил работу. Приходит после `tool_started` с тем же `tool` и `args`. `success: false` — инструмент вернул ошибку.

---

#### `stream_delta`
```json
{ "type": "stream_delta", "delta": "фрагмент ответа..." }
```
Фрагмент финального текстового ответа агента (streaming). Накапливайте в строку.

---

#### `stream_end`
```json
{
  "type": "stream_end",
  "content": "полный текст ответа",
  "context_tokens": 4913,
  "max_context_tokens": 65536
}
```
Агент завершил ответ. `content` — полный накопленный текст (дублирует сумму `stream_delta`). `context_tokens` — сколько токенов использовано в контексте. Клиент должен разблокировать ввод.

---

#### `profile_switched`
```json
{
  "type": "profile_switched",
  "profile_id": "server_admin",
  "profile_name": "Server Administrator"
}
```
Агент переключил профиль сессии через инструмент `switch_profile`. Новый профиль (набор инструментов и system prompt) вступит в силу с **следующего** пользовательского сообщения. Клиент должен обновить заголовок и прочие UI-элементы, отображающие текущий профиль. Событие приходит во время стрима — до завершения `tool_call` для `switch_profile`.

---

#### `context_compressed`
```json
{
  "type": "context_compressed",
  "messages_before": 42,
  "messages_after": 12
}
```
Контекст был автоматически сжат после ответа (компрессия срабатывает при заполнении ≥80% контекстного окна). Информационное событие.

---

#### `error`
```json
{ "type": "error", "message": "Session not found" }
```
Ошибка обработки. После некоторых ошибок стрим продолжается, после других — завершается.

---

### Типичная последовательность событий

**Простой вопрос без инструментов:**
```
stream_start
thinking_delta × N   (если модель думает)
thinking_end
stream_delta × N
stream_end
```

**Запрос с вызовом инструментов:**
```
stream_start
turn_thinking         (мышление при выборе инструмента, если есть)
tool_started
tool_call
turn_thinking         (мышление перед следующим инструментом, если есть)
tool_started
tool_call
thinking_delta × N   (финальный ответ)
thinking_end
stream_delta × N
stream_end
context_compressed    (опционально, если контекст переполнен)
```

**Запрос с субагентом (`spawn_agent`):**
```
stream_start
tool_started          (spawn_agent, is_subagent=false)
  turn_thinking       (is_subagent=true)
  tool_started        (инструмент субагента, is_subagent=true)
  tool_call           (is_subagent=true)
tool_call             (spawn_agent завершён, is_subagent=false)
stream_delta × N
stream_end
```

**Переключение профиля (`switch_profile`):**
```
stream_start
tool_started          (switch_profile)
profile_switched      (до tool_call — клиент обновляет UI здесь)
tool_call             (switch_profile завершён)
stream_delta × N      (Нavi сообщает о смене профиля)
stream_end
```

---

## Файлы

**Статика клиента**: `GET /static/**` — раздаётся из директории `client/`. Заголовок `Cache-Control: no-store`.

**Загруженные файлы сессии**: хранятся в `session_files/{session_id}/`. Агент обращается к ним напрямую через инструмент `filesystem`. Удаляются через 24 часа неактивности сессии или при удалении сессии.

---

## Коды ошибок

| HTTP | Причина |
|------|---------|
| `400` | Запрещённый тип файла |
| `404` | Сессия или профиль не найдены |
| `413` | Файл превышает 200 MB |
| `500` | Внутренняя ошибка агента |
| WS `4004` | Сессия не найдена при подключении |
