API
API Велеса живет внутри процесса veles gateway. Его используют Nerve, интеграции и служебные клиенты, которым нужен доступ к состоянию шлюза, сессиям, задачам, файлам рабочей области, личностям и вызовам моделей.
Интерфейс делится на два транспорта:
- HTTP-маршруты для простых операций, загрузки вложений и доски задач.
- WebSocket JSON-RPC на
/ws для интерактивного управления сессиями, чатом, файлами, личностями и секретами.
Адрес и авторизация
Хост, порт и токен задаются в gateway-разделе config.json:
{
"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, должны передавать токен в заголовке:
Authorization: Bearer <gateway.token>
При отсутствии или несовпадении токена защищённые HTTP-маршруты отвечают 401 Unauthorized с заголовком WWW-Authenticate: Bearer realm="veles-gateway". GET /health остаётся открытым для внешних проверок доступности. Сравнение токена выполняется в постоянном времени (compare_digest).
WebSocket-клиент сначала подключается к /ws. Сразу после accept gateway присылает событие connect.challenge со случайным одноразовым nonce:
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "kZ8s...generated" },
"seq": 1
}
Затем клиент отправляет JSON-RPC запрос connect с токеном. Поддерживаются две формы параметров: вложенная auth.token (предпочтительно) и плоская token.
{
"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 и возвращает ошибку хранилища. Это нужно, чтобы потеря ключа шифрования не превращалась в обход уже заданного пароля.
Успешный ответ:
{
"type": "res",
"id": "connect-1",
"ok": true,
"payload": {
"protocol": 3,
"server": "veles"
}
}
Формат WebSocket RPC
Запросы имеют общий вид:
{
"type": "req",
"id": "request-id",
"method": "sessions.history",
"params": {}
}
Ответы:
{
"type": "res",
"id": "request-id",
"ok": true,
"payload": {}
}
Ошибки:
{
"type": "res",
"id": "request-id",
"ok": false,
"error": {
"code": -32602,
"message": "validation message"
}
}
Gateway также отправляет события с type: "event". У события всегда есть поля event (имя), payload (данные) и seq (монотонный счётчик событий внутри текущего WebSocket-подключения для упорядочивания и восстановления после переподключения):
{
"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:
{
"query": "Нужно проверить PR и найти регрессии"
}
Ответ:
{
"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 возвращает ошибку вместо тихой отправки сообщения без вложения.
POST /tools/invoke — это HTTP-обёртка над частью операций gateway, удобная для клиентов без WebSocket-соединения. Тело запроса:
{
"tool": "sessions_list",
"args": {},
"sessionKey": "web:default"
}
Ответ:
{
"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 возвращает поставщиков в порядке общего реестра. Поставщик без подключённых учётных данных или без обработчика расхода не попадает в ответ. Интерфейс не должен проверять конкретные имена поставщиков: подписи, полное значение, короткое значение для значка, доля шкалы и время сброса приходят вместе с показателем.
{
"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 имеет одну из двух форм:
{
"kind": "snapshot",
"projectId": "project-id",
"revision": 7,
"snapshot": { "projectId": "project-id", "revision": 7, "status": "running" }
}
{
"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-клиента
Минимальный сценарий «подключиться и отправить сообщение»:
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.
Канонический источник истины — код gateway: HTTP-маршруты в veles/api/routes.py, диспетчер RPC в veles/api/ws_handler.py, схема gateway в veles/config/schema.py. При изменении контрактов обновляйте эту страницу (см. инструкцию в AGENTS.md).