> ## Documentation Index
> Fetch the complete documentation index at: https://docs.velesagent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API

> Справочник по HTTP и WebSocket JSON-RPC серверного шлюза Велеса.

# API

API Велеса живет внутри процесса `veles gateway`. Его используют Nerve, интеграции и служебные клиенты, которым нужен доступ к состоянию шлюза, сессиям, задачам, файлам рабочей области, личностям и вызовам моделей.

Интерфейс делится на два транспорта:

* HTTP-маршруты для простых операций, загрузки вложений и доски задач.
* WebSocket JSON-RPC на `/ws` для интерактивного управления сессиями, чатом, файлами, личностями и секретами.

## Адрес и авторизация

Хост, порт и токен задаются в `gateway`-разделе `config.json`:

```json theme={null}
{
  "gateway": {
    "host": "127.0.0.1",
    "port": 18790,
    "token": "..."
  }
}
```

Токен можно задать и через переменную окружения `VELES_GATEWAY_TOKEN` (она переопределяет значение из `config.json`, в том числе при запуске с уже существующим файлом конфигурации). Если токен не задан вообще, gateway не запускается, а любые запросы получают `401`.

Nerve не обязан хранить этот токен в своём окружении. Для входа пользователя Nerve вызывает до `connect` отдельный WebSocket-метод `auth.login`: Veles проверяет пароль, возвращает эффективный `gateway.token` только серверному процессу Nerve, а браузер получает только cookie-сессию Nerve. Если пароль Nerve ещё не задан, первый вход можно выполнить самим `gateway.token`; после сохранения пароля этот запасной вход отключается.

HTTP-запросы, кроме `GET /health`, должны передавать токен в заголовке:

```http theme={null}
Authorization: Bearer <gateway.token>
```

При отсутствии или несовпадении токена защищённые HTTP-маршруты отвечают `401 Unauthorized` с заголовком `WWW-Authenticate: Bearer realm="veles-gateway"`. `GET /health` остаётся открытым для внешних проверок доступности. Сравнение токена выполняется в постоянном времени (`compare_digest`).

WebSocket-клиент сначала подключается к `/ws`. Сразу после `accept` gateway присылает событие `connect.challenge` со случайным одноразовым `nonce`:

```json theme={null}
{
  "type": "event",
  "event": "connect.challenge",
  "payload": { "nonce": "kZ8s...generated" },
  "seq": 1
}
```

Затем клиент отправляет JSON-RPC запрос `connect` с токеном. Поддерживаются две формы параметров: вложенная `auth.token` (предпочтительно) и плоская `token`.

```json theme={null}
{
  "type": "req",
  "id": "connect-1",
  "method": "connect",
  "params": {
    "auth": {
      "token": "<gateway.token>"
    }
  }
}
```

До успешного `connect` любой другой метод возвращает ошибку `-32001 Unauthorized`, а входящие/исходящие события не доставляются клиенту.

Исключение до `connect` составляют методы входа Nerve:

| Method        | Назначение                                                                                                       |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| `auth.status` | Возвращает, задан ли пароль Nerve и разрешён ли первичный вход через `gateway.token`.                            |
| `auth.login`  | Проверяет пароль Nerve или первичный `gateway.token`, затем возвращает `gatewayToken` только серверному клиенту. |

Метод `auth.password.set` доступен только после обычного `connect` с токеном gateway и сохраняет новый пароль Nerve в зашифрованном хранилище Veles.

Если зашифрованное хранилище пароля недоступно или не расшифровывается, `auth.login` не включает запасной вход через `gateway.token` и возвращает ошибку хранилища. Это нужно, чтобы потеря ключа шифрования не превращалась в обход уже заданного пароля.

Успешный ответ:

```json theme={null}
{
  "type": "res",
  "id": "connect-1",
  "ok": true,
  "payload": {
    "protocol": 3,
    "server": "veles"
  }
}
```

## Формат WebSocket RPC

Запросы имеют общий вид:

```json theme={null}
{
  "type": "req",
  "id": "request-id",
  "method": "sessions.history",
  "params": {}
}
```

Ответы:

```json theme={null}
{
  "type": "res",
  "id": "request-id",
  "ok": true,
  "payload": {}
}
```

Ошибки:

```json theme={null}
{
  "type": "res",
  "id": "request-id",
  "ok": false,
  "error": {
    "code": -32602,
    "message": "validation message"
  }
}
```

Gateway также отправляет события с `type: "event"`. У события всегда есть поля `event` (имя), `payload` (данные) и `seq` (монотонный счётчик событий внутри текущего WebSocket-подключения для упорядочивания и восстановления после переподключения):

```json theme={null}
{
  "type": "event",
  "event": "chat",
  "payload": { "...": "..." },
  "seq": 42
}
```

Основные события: `connect.challenge` при подключении, `chat` для потока сообщений ассистента и пользователя, `project` для хода проекта и `workspace.file.changed` для обновления дерева файлов рабочей области в удалённом интерфейсе.

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

Ошибки возвращаются в поле `error` с числовым `code` (совместимым с JSON-RPC) и человекочитаемым `message`:

| Code     | Когда возникает                                           |
| -------- | --------------------------------------------------------- |
| `-32700` | Тело запроса не является валидным JSON.                   |
| `-32601` | Неизвестный метод.                                        |
| `-32602` | Ошибка валидации параметров (`ValueError` в обработчике). |
| `-32001` | Не пройдена авторизация: отсутствует или неверен токен.   |
| `-32000` | Внутренняя ошибка обработчика.                            |

## HTTP endpoints

| Method       | Route                                        | Назначение                                                                                                                                                                          |
| ------------ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`        | `/health`                                    | Открытая проверка доступности gateway, токен не нужен.                                                                                                                              |
| `GET`        | `/status`                                    | Общий статус gateway, модели, thinking, каналов, сессий и task storage. В `config.models` возвращаются настроенные модели с `id`, `label` и `modalities`.                           |
| `POST`       | `/tools/invoke`                              | Совместимый HTTP-вызов отдельных gateway tools.                                                                                                                                     |
| `POST`       | `/attachments/upload`                        | Загрузка вложения в workspace с привязкой к `x-session-key`.                                                                                                                        |
| `GET`/`POST` | `/api/personalities/select`                  | LLM-выбор лучшей Personality для пользовательского запроса.                                                                                                                         |
| `GET`        | `/push/config`                               | Публичная (для браузера) часть конфигурации Firebase web push: `configured`, `firebase`, `vapidKey`.                                                                                |
| `POST`       | `/push/subscriptions`                        | Регистрация FCM-токена устройства: `{"token", "platform", "userAgent"}`.                                                                                                            |
| `DELETE`     | `/push/subscriptions`                        | Удаление подписки устройства: `{"token"}`.                                                                                                                                          |
| `POST`       | `/push/presence`                             | Отметка видимости вкладки/PWA: `{"clientId", "visible"}` — гасит зеркалирование ответов в push на срок `channels.push.presenceTtlSeconds`, пока пользователь смотрит на приложение. |
| `GET`        | `/api/tasks`                                 | Список задач с фильтрами `status`, `priority`, `assignee`, `label`, `q`, `limit`, `offset`.                                                                                         |
| `POST`       | `/api/tasks`                                 | Создание задачи.                                                                                                                                                                    |
| `GET`        | `/api/tasks/config`                          | Конфигурация task board.                                                                                                                                                            |
| `PUT`        | `/api/tasks/config`                          | Обновление конфигурации task board.                                                                                                                                                 |
| `GET`        | `/api/tasks/proposals`                       | Список предложений ассистента для задач.                                                                                                                                            |
| `POST`       | `/api/tasks/proposals`                       | Создание предложения.                                                                                                                                                               |
| `POST`       | `/api/tasks/proposals/{proposal_id}/approve` | Принять предложение.                                                                                                                                                                |
| `POST`       | `/api/tasks/proposals/{proposal_id}/reject`  | Отклонить предложение.                                                                                                                                                              |
| `GET`        | `/api/tasks/{task_id}`                       | Получить задачу.                                                                                                                                                                    |
| `PATCH`      | `/api/tasks/{task_id}`                       | Обновить задачу.                                                                                                                                                                    |
| `DELETE`     | `/api/tasks/{task_id}`                       | Удалить задачу.                                                                                                                                                                     |
| `POST`       | `/api/tasks/{task_id}/reorder`               | Переместить задачу между колонками или позициями.                                                                                                                                   |
| `POST`       | `/api/tasks/{task_id}/execute`               | Запустить выполнение задачи в выбранной сессии.                                                                                                                                     |
| `POST`       | `/api/tasks/{task_id}/complete`              | Записать результат выполнения задачи.                                                                                                                                               |
| `POST`       | `/api/tasks/{task_id}/approve`               | Подтвердить выполненную задачу.                                                                                                                                                     |
| `POST`       | `/api/tasks/{task_id}/reject`                | Отклонить выполненную задачу.                                                                                                                                                       |
| `POST`       | `/api/tasks/{task_id}/abort`                 | Прервать выполнение задачи.                                                                                                                                                         |

### Выбор личности

Endpoint `GET /api/personalities/select?query=...` и `POST /api/personalities/select` используют модель по умолчанию, чтобы выбрать подходящую Personality по тексту запроса. Этот служебный выбор вызывается без reasoning/thinking effort, чтобы не тратить расширенное рассуждение на маршрутизацию. Промпт ограничивает выбор списком реально настроенных `personalityId`, а backend проверяет, что выбранный идентификатор существует.

В ответе `GET /status` поле `config.models` содержит настроенные модели. Для каждой записи передаются `id`, `label`, `modalities` и, если он явно задан, список `reasoningEfforts`. Nerve использует этот список как приоритетный набор уровней рассуждения для модели.

Nerve также проксирует этот contract через свой `POST /api/personalities/select`, чтобы окно нового диалога могло предложить режим **Подобрать** без прямого доступа браузера к gateway token.

Тело `POST`:

```json theme={null}
{
  "query": "Нужно проверить PR и найти регрессии"
}
```

Ответ:

```json theme={null}
{
  "personalityId": "reviewer",
  "personality": {
    "personalityId": "reviewer",
    "id": "reviewer",
    "name": "Reviewer",
    "model": null,
    "thinkingLevel": "high",
    "isDefault": false,
    "rootSessionKey": "personality:reviewer:main"
  },
  "reason": "Запрос требует ревью кода и поиска рисков.",
  "confidence": 0.91,
  "model": "openrouter/...",
  "candidateCount": 3,
  "usage": {}
}
```

`SOUL.md` всех личностей передается модели для выбора, но не возвращается в поле `personality`.

### Загрузка вложений

`POST /attachments/upload` принимает необработанное тело файла и заголовки:

| Header                   | Значение                                             |
| ------------------------ | ---------------------------------------------------- |
| `x-session-key`          | Сессия, к которой относится вложение.                |
| `x-attachment-name`      | Имя файла.                                           |
| `x-attachment-mime-type` | Тип содержимого файла.                               |
| `x-attachment-kind`      | Опциональный тип: `image`, `audio`, `voice`, `file`. |

Ответ содержит объект вложения с `id`, `workspacePath`, `absolutePath`, `mimeType` и другими полями. Nerve затем передает такие объекты в `chat.send` как `attachmentRefs`.

Для `attachmentRefs` основным адресом файла считается `workspacePath`. Поле `absolutePath` может отсутствовать в запросе от Nerve: шлюз сам находит файл внутри рабочей области Велеса, проверяет, что он существует, и подставляет свой канонический полный путь. Если путь не удается разрешить в рабочей области Велеса, `chat.send` возвращает ошибку вместо тихой отправки сообщения без вложения.

### Вызов tools по HTTP

`POST /tools/invoke` — это HTTP-обёртка над частью операций gateway, удобная для клиентов без WebSocket-соединения. Тело запроса:

```json theme={null}
{
  "tool": "sessions_list",
  "args": {},
  "sessionKey": "web:default"
}
```

Ответ:

```json theme={null}
{
  "ok": true,
  "result": { "...": "..." }
}
```

Ошибки возвращаются как `{ "ok": false, "error": { "message": "..." } }` (HTTP `400` для невалидного тела или ошибки аргументов). Поддерживаемые значения `tool`:

| `tool`             | Назначение                                                                            |
| ------------------ | ------------------------------------------------------------------------------------- |
| `sessions_list`    | Список сессий (аналог `sessions.list`).                                               |
| `sessions_history` | История диалога по `sessionKey`/`conversationId`/`sessionId`.                         |
| `session_status`   | Изменить `model`/`thinkingLevel` сессии.                                              |
| `sessions_spawn`   | Создать дочернюю сессию и отправить в неё задачу.                                     |
| `cron`             | Управление расписаниями: `list`, `add`, `update`, `delete`, `run`, `runs`.            |
| `tasks`            | Операции с доской задач (зеркало части `/api/tasks`).                                 |
| `memory_store`     | No-op (`{ "stored": false, "mode": "noop" }`); память хранится навыком, а не gateway. |
| `subagents`        | Возвращает пустой список (зарезервировано).                                           |

Для интерактивного управления чатом и сессиями предпочитайте WebSocket RPC — `/tools/invoke` рассчитан на скриптовые и служебные вызовы.

При создании и обновлении задания `cron` сохраняет не только текст и расписание, но и поля запуска: `sessionKey`, `personalityId`, `payload.model` и `payload.thinking`. Если указан `personalityId`, но нет `sessionKey`, задание запускается в основной сессии этой личности. При выполнении Велес применяет сохраненные модель и уровень рассуждения к сессии задания перед обращением к модели.

## WebSocket RPC methods

### Статус

| Method               | Назначение                                                                                                 |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `status`             | Статус gateway и текущей или указанной сессии.                                                             |
| `providers.usage`    | Показатели расхода и остатка для всех подключённых поставщиков, которые умеют их сообщать.                 |
| `openrouter.balance` | Устаревшее представление баланса OpenRouter для совместимости; новые клиенты используют `providers.usage`. |

`providers.usage` возвращает поставщиков в порядке общего реестра. Поставщик без подключённых учётных данных или без обработчика расхода не попадает в ответ. Интерфейс не должен проверять конкретные имена поставщиков: подписи, полное значение, короткое значение для значка, доля шкалы и время сброса приходят вместе с показателем.

```json theme={null}
{
  "providers": [
    {
      "providerId": "openai_codex",
      "label": { "en": "OpenAI Codex", "ru": "OpenAI Codex" },
      "status": "ok",
      "lastUpdated": 1783764000000,
      "metrics": [
        {
          "id": "five_hour",
          "label": { "en": "5-hour limit", "ru": "Лимит на 5 часов" },
          "value": 72.5,
          "maximum": 100,
          "progress": 0.725,
          "displayValue": { "en": "72.5% remaining", "ru": "Осталось 72.5%" },
          "badgeValue": { "en": "72.5%", "ru": "72.5%" },
          "resetAt": 1783771200000,
          "defaultBadge": true
        }
      ]
    }
  ],
  "checkedAt": 1783764000000
}
```

`status` принимает `ok`, `stale` или `unavailable`. При временной ошибке Велес возвращает последнюю успешную запись со значением `stale`; если успешной записи ещё не было, поставщик возвращается как `unavailable` без показателей. Успешные ответы хранятся в памяти 30 секунд и удаляются при обновлении конфигурации или учётных данных. `resetAt` всегда задан в миллисекундах от начала эпохи.

### Конфигурация и перезапуск

| Метод             | Назначение                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `config.get`      | Вернуть путь к `config.json`, JSON-схему, версию файла, очищенное содержимое конфигурации и метаданные скрытых секретов. |
| `config.save`     | Проверить и сохранить конфигурацию. При `restart: true` шлюз планирует собственный перезапуск после отправки ответа.     |
| `config.raw.get`  | Вернуть полный текст активного `config.json` без скрытия `gateway.token` и секретных значений.                           |
| `config.raw.save` | Проверить и сохранить полный текст активного `config.json` без перезапуска шлюза.                                        |
| `gateway.restart` | Запланировать перезапуск процесса шлюза без изменения конфигурации.                                                      |

`config.get` не возвращает `gateway.token` в значении и удаляет это поле из схемы для редактора. Остальные значения, которые являются целями секретов, возвращаются как маркеры вида `{ "$velesRedacted": true, "targetId": "..." }`, если значение уже задано. `config.save` восстанавливает такие маркеры и скрытые поля из исходной конфигурации перед валидацией, поэтому интерфейс может сохранить остальные изменения, не раскрывая секреты.

Методы `config.raw.get` и `config.raw.save` предназначены для полного текстового редактора конфига. Они показывают физическое содержимое файла, поэтому в ответе могут быть `gateway.token` и секреты, если они записаны в `config.json` открытым текстом. При сохранении шлюз проверяет, что текст является объектом JSON и проходит схему Велеса, поддерживает проверку `expectedMtimeMs` от устаревшей записи, сохраняет переданный текст как есть и не планирует перезапуск. После такой правки пользователь перезапускает шлюз вручную.

### Чат

| Method         | Назначение                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chat.send`    | Отправить сообщение в сессию. Поддерживает `sessionKey`, `conversationId`, `message`, `attachmentRefs`, `idempotencyKey`, `deliver`, `executionMode` и `clientLocale`. Вложенные изображения берутся из рабочей области Велеса, уменьшаются до ограниченного размера для запроса к модели и повторно добавляются в ближайшую историю, если пользователь продолжает обсуждать уже отправленные изображения. Если выбранная настроенная модель не содержит `image` или `vision` в `agents.defaults.models[].modalities`, Велес не передает данные изображения провайдеру и оставляет модели только текстовое описание вложения. |
| `chat.abort`   | Остановить активный ответ в сессии.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `chat.history` | Получить историю сообщений по `sessionKey`, `conversationId` или `sessionId`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

`executionMode` принимает `direct` или `project`; если поле не передано, используется `direct`. Режим `project` разрешён только для первого сообщения пустого нового диалога. `clientLocale` принимает `en` или `ru` и задаёт запасной язык вопросов и материалов, если язык исходного сообщения определить не удалось. Для проекта шлюз сохраняет исходное сообщение, структурированные ссылки на вложения, текущую модель и уже приведённый к каноническому виду уровень рассуждений. Служебные значения `auto`, `default` и `off` не передаются поставщику как `reasoning_effort`; устаревшее значение `uhigh` преобразуется в `xhigh`. Связь с созданным проектом приходит в событии `project`, сводке сессии и ответе `projects.get`.

После создания проекта последующий `chat.send` с `executionMode: "direct"` не отклоняется. Велес находит активный проект по стабильному идентификатору диалога, добавляет в системный контекст его текущее состояние и подключает к этому ходу только средства `project_get_state`, `project_submit_answers`, `project_approve_plan`, `project_revise_plan`, `project_continue` и `project_cancel`. Они не регистрируются в общем реестре и не передаются модели в обычном диалоге. Изменяющие средства применяют те же проверки состояния, номера редакции и идемпотентности, что и явные вызовы `projects.*`.

События ответа ассистента приходят через WebSocket event `chat`. Поле `state` определяет фазу:

| `state`   | Значение                                                                                                                            |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `started` | Ассистент начал новый ответ (новый `runId`).                                                                                        |
| `delta`   | Промежуточный фрагмент ответа или обновление прогресса инструментов.                                                                |
| `final`   | Финальное сообщение. Приходит и для сообщений ассистента, и для входящих сообщений пользователя из других каналов (`role: "user"`). |
| `aborted` | Ответ остановлен через `chat.abort`.                                                                                                |

Поля payload события `chat`:

| Поле                                                  | Описание                                                                                                                                                                                                                                  |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionKey`, `sourceSessionKey`                      | Ключ сессии события. Для изолированного диалога `sessionKey` может иметь вид `personality:<id>:conv:<conversationId>`, а `sourceSessionKey` остается корневым ключом `personality:<id>:main`.                                             |
| `state`                                               | Фаза ответа (см. выше).                                                                                                                                                                                                                   |
| `runId`                                               | Идентификатор текущего ответа; стабилен для всех событий одного ответа.                                                                                                                                                                   |
| `seq`                                                 | Порядковый номер события внутри текущего WebSocket-подключения (для упорядочивания/дедупликации).                                                                                                                                         |
| `totalTokens`, `contextTokens`                        | `totalTokens` показывает текущую оценку занятого контекста сессии. `contextTokens` берётся из текущего `agents.defaults.contextWindowTokens`, а не из сохранённых метаданных диалога. Для пустого нового диалога `totalTokens` равен `0`. |
| `conversationId`, `conversationKey`, `rootSessionKey` | Привязка к корневому диалогу Nerve. Поля приходят и для обычного корневого ключа, и для изолированного ключа `personality:<id>:conv:<conversationId>`.                                                                                    |
| `toolEvents`, `toolHint`                              | Прогресс инструментов и флаг активности инструмента.                                                                                                                                                                                      |
| `message`                                             | Объект сообщения: `role`, `content`, `timestamp` (ms), а также `toolEvents`, `toolHint`, `attachments` при наличии. Присутствует для `delta`/`final`.                                                                                     |
| `error`, `errorMessage`                               | Текст ошибки, если ответ завершился ошибкой.                                                                                                                                                                                              |

Клиент группирует события по `runId`, применяет `delta` поверх накопленного текста и фиксирует ответ на `final`.

Событие `workspace.file.changed` приходит, когда шлюз замечает создание, изменение, удаление или переименование файла в рабочей области. Оно нужно удалённому Nerve, который не может сам наблюдать файловую систему шлюза. Событие не хранится в постоянной очереди: если подключение интерфейса уже закрывается, шлюз прекращает пересылку для этого подключения, а после переподключения интерфейс заново запрашивает дерево файлов.

Поля `payload` события `workspace.file.changed`:

| Поле            | Описание                                                                                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `path`          | Относительный путь внутри рабочей области, с `/` как разделителем.                                                                                                 |
| `personalityId` | Идентификатор личности для совместимости с текущим обработчиком интерфейса. Файлы рабочей области остаются общими; это поле не означает отдельный файловый корень. |

Если один диалог личности уже выполняется, а Nerve создает, активирует или отправляет сообщение в другой диалог той же личности, gateway может использовать отдельный ключ выполнения `personality:<id>:conv:<conversationId>`. Такой ключ нужен только для живого выполнения: сохраненная история, `conversationKey` и `rootSessionKey` остаются привязаны к корневому диалогу личности, а `sessions.list` не показывает изолированный ключ как отдельную строку.

### Проекты

Режим **Проект** ведёт сложную задачу через уточнение требований, утверждение плана, обязательное исследование, последовательное выполнение и итоговый документ. Состояние принадлежит Велесу и связано с одним диалогом; Nerve показывает сохранённые сообщения и канонический снимок, а также отправляет решения пользователя.

Основным идентификатором связи служит `conversationId`. Технический ключ выполнения может меняться между корневой сессией личности и изолированной сессией диалога, поэтому уведомления о состоянии, отмена и итоговое сообщение всегда разрешают текущую сессию заново по идентификатору диалога. Сохранённый при создании проекта `sessionKey` нельзя использовать как неизменный адрес.

Методы:

| Метод                    | Параметры                                                                                      | Назначение                                                                                                                                                       |
| ------------------------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `projects.get`           | Необязательный `projectId` либо идентификатор диалога: `sessionKey`/`key` или `conversationId` | Вернуть канонический снимок связанного проекта либо `null`, если проект не найден.                                                                               |
| `projects.submitAnswers` | `projectId`, `answers`, `outputPath`; необязательные `expectedRevision`, `idempotencyKey`      | Сохранить ответы анкеты и точную относительную папку результатов, затем начать подготовку плана.                                                                 |
| `projects.planDecision`  | `projectId`, `decision`; необязательные `guidance`, `expectedRevision`, `idempotencyKey`       | Утвердить план (`decision: "approve"`) либо запросить новую редакцию (`decision: "revise"`). Для `revise` поле `guidance` обязательно.                           |
| `projects.resume`        | `projectId`; необязательные `guidance`, `expectedRevision`, `idempotencyKey`                   | Продолжить приостановленную или незавершённую часть работы в новом контексте без очистки сохранённого состояния. Пояснение пользователя передаётся в `guidance`. |
| `projects.cancel`        | `projectId`; необязательные `expectedRevision`, `idempotencyKey`                               | Отменить проект без удаления уже созданных материалов.                                                                                                           |

Элемент `answers` имеет вид `{ "questionId": "...", "optionId": "..." }` для подготовленного варианта или `{ "questionId": "...", "customText": "..." }` для собственного ответа. `optionId` и `customText` взаимоисключающие. Требуется ровно один допустимый ответ на каждый обязательный вопрос.

`expectedRevision` включает оптимистическую защиту от устаревшего интерфейса: если номер не совпадает с текущим снимком, действие отклоняется, а клиент получает актуальное состояние через `projects.get`. `idempotencyKey` защищает от повторного применения одного решения после сетевого повтора. Ответ изменяющего метода содержит `snapshot`; поля `content` и `started` передают краткий результат действия и признак запуска фонового этапа, когда они применимы.

#### Снимок и состояния

Канонический снимок содержит как минимум:

| Поле                                        | Описание                                                                                                     |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `projectId`, `sessionKey`, `conversationId` | Связь проекта с диалогом.                                                                                    |
| `revision`                                  | Монотонный номер сохранённой редакции состояния.                                                             |
| `status`, `phase`                           | Общее состояние и текущая фаза.                                                                              |
| `questions`, `answers`                      | Анкета и принятые ответы. Каждый вопрос содержит ровно три элемента `options` и разрешает собственный ответ. |
| `suggestedOutputPath`, `outputPath`         | Предложенная и подтверждённая папки материалов.                                                              |
| `planPath`, `steps`                         | Путь к плану и упорядоченные этапы с состояниями и материалами.                                              |
| `artifacts`, `finalPath`                    | Список готовых материалов и путь к итоговому документу.                                                      |
| `currentStepId`, `currentStepIndex`         | Текущий этап, если выполняется работа.                                                                       |
| `error`                                     | Безопасное описание ошибки: `code`, `message`, `phase`, `stepId`, `attempt`, `retryable`, `detailsPath`.     |
| `updatedAt`                                 | Время последнего сохранённого изменения.                                                                     |

Поддерживаются состояния `preparing_questions`, `awaiting_answers`, `planning`, `awaiting_plan_approval`, `running`, `paused`, `failed`, `completed` и `cancelled`. В сводках `sessions.list`, `sessions.history` и `sessions.get` связь с проектом отражают поля `executionMode`, `projectId` и `projectStatus`; по ним Nerve находит проект после перезагрузки.

Служебное состояние хранится отдельно от пользовательских материалов под `.veles/project-runs/<projectId>/`. Снимок состояния записывается атомарно, переходы дополнительно фиксируются в журнале, а подробности попыток очищаются от секретов. Новая редакция сохраняется до отправки соответствующего события.

#### Специальные сообщения проекта в чате

Анкета, утверждение плана и ошибка передаются как обычные окончательные сообщения помощника в событии `chat`. Дополнительное поле `payload.message.projectInteraction` сохраняется вместе с сообщением и без изменений возвращается через `chat.history`. Поэтому Nerve размещает действие в хронологии диалога, а не восстанавливает отдельную панель только из события `project`.

Общие поля взаимодействия:

| Поле                    | Описание                                                                                                                                                                                                                                                              |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | Стабильный идентификатор взаимодействия. Повторная доставка одной редакции обновляет то же сообщение. Каждая редакция плана получает идентификатор `<projectId>:plan:<planRevision>` и отдельное место в хронологии; ошибка получает отдельный идентификатор попытки. |
| `kind`                  | `questionnaire`, `plan` или `error`.                                                                                                                                                                                                                                  |
| `projectId`, `revision` | Проект и редакция канонического состояния, к которой относится сообщение.                                                                                                                                                                                             |
| `status`                | Состояние проекта в момент публикации сообщения.                                                                                                                                                                                                                      |

Для `questionnaire` передаются `questions`, `suggestedOutputPath` и `outputPath`; для `plan` — `title`, `summary`, `steps` и `planPath`; для `error` — объект `error` из снимка. Клиент не должен применять сообщение с меньшей `revision` поверх уже показанного сообщения с тем же `id`. Новая редакция плана добавляется в конец диалога, а действия остаются доступны только на последней карточке плана. Если специальное сообщение пропущено, Nerve получает снимок через `projects.get` и временно строит из него только текущее действие, требующее ответа пользователя.

Работа исполнителя использует существующий поток чата: пояснения приходят в `chat` со состоянием `delta`, а вызовы и результаты средств — в `toolEvents` по тому же контракту, что и в обычном режиме. Полностью завершённые пары «вызов — результат» дополнительно записываются в историю как контрольные точки, поэтому уже показанные результаты не исчезают после аварийного перезапуска и в истории не остаётся незакрытый вызов прерванного средства. После этапа контрольные точки текущей попытки заменяются полным ходом: сообщения помощника, вызовы и результаты средств сохраняются в обычной истории диалога, а окончательный вывод этапа приходит как обычное сообщение помощника. У такого окончательного события поле `payload.historyRefresh` равно `true`: клиент сразу объединяет сохранённый ход с живой лентой, чтобы промежуточные пояснения не исчезли после очистки потокового буфера. Для итоговой сборки в историю попадает ход использования средств, а полное содержимое сохраняется в `final.md` и заменяется в чате краткой ссылкой.

#### Событие `project`

Поле `payload` события `project` имеет одну из двух форм:

```json theme={null}
{
  "kind": "snapshot",
  "projectId": "project-id",
  "revision": 7,
  "snapshot": { "projectId": "project-id", "revision": 7, "status": "running" }
}
```

```json theme={null}
{
  "kind": "progress",
  "projectId": "project-id",
  "revision": 7,
  "progress": { "message": "...", "stepId": "step-02", "toolName": "..." }
}
```

`snapshot` является уведомлением о новой сохранённой редакции. `progress` — промежуточный, объединяемый интерфейсом ход текущей редакции; он не изменяет каноническое состояние. Оба вида событий могут быть пропущены при разрыве соединения и не являются постоянной очередью. После переподключения, пропуска редакции или смены диалога Nerve обязан вызвать `projects.get`, а не восстанавливать проект только из событий.

#### Папка и последовательность работы

Если пользователь не выбрал папку, Велес предлагает `docs/<YYYY-MM-DD>-<slug>--<id8>`. `outputPath` — точный относительный путь внутри рабочей области. Запрещены абсолютные пути, выход через `..`, скрытые пути, символические ссылки за пределы рабочей области, корень рабочей области и служебные каталоги `.veles`, `sessions`, `memory`, `tasks`. Существующая папка с посторонними файлами допустима, но совпадение с любым путём, которым управляет проект, отклоняется до записи. Проект не перезаписывает и не удаляет посторонние файлы.

Велес создаёт в папке `brief.md`, `plan.md`, `research.md`, дополняемый `progress.md`, отчёты `steps/NN-<slug>.md`, безопасные отчёты ошибок `failures/*.md` и итоговый `final.md`. План содержит не более десяти выполняемых этапов; первым всегда идёт исследование. Выполнение начинается только после утверждения. Если исследование изменило определения оставшихся этапов, новый план снова требует утверждения; иначе работа продолжается без лишней остановки.

Каждый этап запускается строго после предыдущего, в отдельном свежем контексте и без возможности создавать вложенных исполнителей. Он получает обычные инструкции личности и рабочей области, исходное описание, ответы, утверждённый план, ограниченное резюме исследования, перечень готовых материалов и краткую передачу только от предыдущего этапа. Реестр средств создаётся заново для каждого этапа как снимок доступных средств основного цикла: поэтому поиск по памяти и документации, разбор документов и уже подключённые средства MCP доступны без отдельного списка, но отсутствующие средства не объявляются модели. Из снимка исключаются `ask_user`, `message`, `spawn`, `model` и `cron`; итоговая обработка дополнительно получает только средства с известным режимом чтения. Ошибка отдельного вызова возвращается модели с предложением выбрать обходной путь и сама по себе не завершает этап. Полный ход этапа виден и сохраняется в истории диалога, но в новый контекст следующего исполнителя передаётся только ограниченное резюме предыдущего результата. После всех этапов отдельная итоговая обработка собирает `final.md` и краткое сообщение диалога.

Ошибка этапа останавливает последующие этапы и сохраняет отчёт попытки. `projects.resume` означает продолжение, а не очистку или откат: Велес не удаляет файлы, материалы, изменения или сохранённый ход прерванной попытки. Новый контекст запускается только для незавершённого этапа, проверяет фактическое состояние, отчёт об ошибке и прошлый ход диалога, после чего продолжает с последнего подтверждённого состояния. Завершённые этапы автоматически не повторяются. `projects.cancel` отменяет ожидающий или приостановленный проект, а `chat.abort` также отменяет принятый запрос режима «Проект», в том числе до создания постоянного состояния проекта. `sessions.delete` сначала отменяет незавершённый или ещё ожидающий обработки проект по `conversationId` и только затем скрывает или удаляет историю; папка материалов не удаляется.

После перезапуска шлюза состояния ожидания ответов и утверждения остаются без изменения. Подготовка вопросов или плана и активное выполнение переводятся в `paused` с ошибкой `gateway_restarted` и требуют явного `projects.resume`: изменяющий файлы этап никогда не повторяется автоматически после перезапуска. При запуске Велес также сверяет метаданные диалога с каждым сохранённым проектом, включая завершённые и отменённые, чтобы сбой между записью состояния и уведомлением не оставил устаревший признак работы.

### Сессии

| Method              | Назначение                                         |
| ------------------- | -------------------------------------------------- |
| `sessions.list`     | Активные и технические сессии.                     |
| `sessions.history`  | История корневых диалогов Nerve.                   |
| `sessions.create`   | Создать новый корневой диалог.                     |
| `sessions.activate` | Активировать сохраненный корневой диалог.          |
| `sessions.get`      | Получить summary сессии или диалога.               |
| `sessions.patch`    | Обновить label, model, thinkingLevel или parentId. |
| `sessions.delete`   | Удалить или скрыть сессию/диалог.                  |

`sessions.patch` может принимать `model` и `thinkingLevel` вместе. Nerve использует такое атомарное обновление при смене модели. Служебное значение `thinkingLevel: "auto"` сохраняется в метаданных сессии, но не передаётся поставщику как уровень рассуждения: модель применяет собственное поведение по умолчанию.

Ключи корневых сессий Personality имеют вид `personality:<id>:main`.

Когда корневая сессия личности занята другим ответом, `sessions.create` создает новую запись истории без очистки занятой live-сессии, а `sessions.activate` загружает выбранный диалог в изолированный ключ `personality:<id>:conv:<conversationId>`. Если корневая сессия свободна, сохраняется прежнее поведение: активный диалог работает прямо под `personality:<id>:main`.

В ответах `sessions.list`, `sessions.history` и `sessions.get` поле `contextTokens` всегда отражает текущее значение `agents.defaults.contextWindowTokens`. Сохранённые метаданные старых диалогов не должны переопределять этот лимит после изменения настройки или перезапуска шлюза. Для диалога с проектом те же ответы содержат `executionMode: "project"`, `projectId` и `projectStatus`.

### Личности

| Method                 | Назначение                                            |
| ---------------------- | ----------------------------------------------------- |
| `personalities.list`   | Список Personality из workspace.                      |
| `personalities.select` | LLM-выбор лучшей Personality для запроса.             |
| `personalities.get`    | Получить одну Personality.                            |
| `personalities.create` | Создать Personality и root session.                   |
| `personalities.patch`  | Обновить имя, `SOUL.md`, модель или thinkingLevel.    |
| `personalities.delete` | Удалить не-main Personality и связанную root-history. |

### Файлы workspace

| Method                              | Назначение                                                                                                                                                 |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `personalities.files.list`          | Список top-level файлов workspace.                                                                                                                         |
| `personalities.files.get`           | Legacy-чтение workspace-файла по имени.                                                                                                                    |
| `personalities.files.set`           | Legacy-запись workspace-файла по имени.                                                                                                                    |
| `personalities.files.tree`          | Дерево файлов workspace. Необязательный `includeHidden: true` включает папки с точкой; по умолчанию они скрыты. Правила показа скрытых файлов не меняются. |
| `personalities.files.read`          | Чтение файла по workspace path.                                                                                                                            |
| `personalities.files.write`         | Запись файла по workspace path.                                                                                                                            |
| `personalities.files.downloadInfo`  | Метаданные для скачивания файла.                                                                                                                           |
| `personalities.files.downloadChunk` | Чтение чанка файла.                                                                                                                                        |
| `personalities.files.delete`        | Удаление файла.                                                                                                                                            |

Файловые методы работают с общей рабочей областью Велеса. `personalityId` не разделяет пользовательские файлы по разным корням: личность меняет промпт, навыки и историю, но не создает отдельное файловое пространство. Исключение - собственные служебные файлы личности внутри `personalities/<id>/`.

У `personalities.files.tree` есть ограниченный одноуровневый режим для каталогов с большим числом файлов. Чтобы включить его, передайте положительное целое `maxEntries` вместе с `depth: 1`. Значения больше `2000` шлюз уменьшает до `2000`; сочетание `maxEntries` с другой глубиной отклоняется как неверный запрос. Ограничение относится к числу просмотренных записей каталога, поэтому скрытые и исключённые пути тоже расходуют этот предел. Шлюз просматривает не более ещё одной записи сверх предела, только чтобы определить наличие продолжения, и не запускает рекурсивный обход.

`includeHidden` действует как в обычном, так и в ограниченном режиме. Он не отменяет обычные запреты на выход за пределы рабочей области и служебные исключения.

В ограниченном режиме ответ содержит следующие поля:

* `entries` — прочитанные файлы и каталоги текущего уровня;
* `truncated` — `true`, если предел был достигнут и часть каталога не просмотрена;
* `missing` — `true`, если запрошенный каталог отсутствует;
* `unavailable` — `true`, если путь не является каталогом или сам каталог нельзя прочитать полностью;
* `errors` — пути относительно рабочей области, для которых не удалось прочитать сведения. Частичная ошибка отдельного файла не удаляет успешно прочитанные записи из ответа.

Если `maxEntries` не указан, сохраняется прежний ответ с `entries` и рекурсивной глубиной от `1` до `5`; дополнительные поля состояния в него не добавляются.

Для `personalities.files.write` можно передать `expectedMtimeMs`. Если файл был изменен после чтения, шлюз возвращает ошибку конфликта и не перезаписывает более свежую версию. Это поле нужно для операций «прочитал-изменил-записал», например для правки памяти через Nerve.

При записи существующего файла с несколькими жесткими ссылками `personalities.files.write` сохраняет тот же файловый узел. Это важно для файлов, которые пользователь связал жесткой ссылкой, например `workspace/config.json` -> `config.json`: после сохранения через Nerve обе ссылки продолжают указывать на один файл. Для новых файлов, обычных файлов без дополнительных жестких ссылок и символических ссылок шлюз использует запись через временный файл и замену пути.

### Навыки

| Method                         | Назначение                                                                                              |
| ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `personalities.skills.catalog` | Эффективный каталог навыков для Personality с учетом overlay порядка: personality, workspace, built-in. |

### Секреты

| Method                   | Назначение                                                  |
| ------------------------ | ----------------------------------------------------------- |
| `secrets.catalog`        | Каталог secret targets и статусов.                          |
| `secrets.set`            | Сохранить секрет.                                           |
| `secrets.delete`         | Удалить секрет.                                             |
| `secrets.refresh`        | Перечитать runtime secrets и hot-swap provider/tool config. |
| `secrets.oauth.start`    | Начать OAuth flow.                                          |
| `secrets.oauth.status`   | Проверить OAuth flow.                                       |
| `secrets.oauth.complete` | Завершить OAuth flow.                                       |
| `secrets.oauth.delete`   | Удалить OAuth profile.                                      |

| Method              | Назначение                                              |
| ------------------- | ------------------------------------------------------- |
| `auth.password.set` | Сохранить пароль Nerve в зашифрованном хранилище Veles. |

### LLM

| Method     | Назначение                                                                                                                                             |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `llm.chat` | Небольшой authenticated LLM chat request для control API callers. Поддерживает `messages`, `tools`, `model`, `maxTokens`, `temperature`, `toolChoice`. |

`llm.chat` предназначен для control flows, а не для обычных пользовательских сессий. Для пользовательского чата используйте `chat.send`.

## Пример жизненного цикла WebSocket-клиента

Минимальный сценарий «подключиться и отправить сообщение»:

```text theme={null}
1. Открыть WebSocket на ws://<host>:<port>/ws
2. Получить event "connect.challenge" с nonce
3. Отправить req "connect" с { auth: { token } }
   → res ok: { protocol: 3, server: "veles" }
4. (опц.) Отправить req "status" → текущая модель, thinking, активная сессия
5. Отправить req "chat.send" с { sessionKey, message }
   → res ok: { runId, status: "started" }
6. Принимать event "chat":
     state "started" → начался ответ runId
     state "delta"   → дописать payload.message.content
     state "final"   → ответ готов; зафиксировать сообщение
7. (опц.) req "chat.abort" с { sessionKey } для остановки
```

`runId` и `seq` позволяют клиенту переподключиться и восстановить состояние без дублирования сообщений: события одного ответа разделяют `runId`, а `seq` задаёт их порядок внутри текущего WebSocket-подключения. После переподключения счётчик начинается заново, поэтому клиент не должен сравнивать `seq` между разными WebSocket-соединениями.

## Совместимость с Nerve

Nerve поверх этого API имеет собственные HTTP routes, но состояние задач, Personality, файлов, сессий и чата остается на стороне Veles gateway. Если добавляется новая backend-возможность для Nerve, сначала стоит определить gateway RPC или HTTP contract здесь, а затем проксировать его на стороне Nerve.

<Note>
  Канонический источник истины — код gateway: HTTP-маршруты в `veles/api/routes.py`, диспетчер RPC в `veles/api/ws_handler.py`, схема `gateway` в `veles/config/schema.py`. При изменении контрактов обновляйте эту страницу (см. инструкцию в `AGENTS.md`).
</Note>
