API
API Велеса живет внутри процессаveles gateway. Его используют Nerve, интеграции и служебные клиенты, которым нужен доступ к состоянию шлюза, сессиям, задачам, файлам рабочей области, личностям и вызовам моделей.
Интерфейс делится на два транспорта:
- HTTP-маршруты для простых операций, загрузки вложений и доски задач.
- WebSocket JSON-RPC на
/wsдля интерактивного управления сессиями, чатом, файлами, личностями и секретами.
Адрес и авторизация
При запуске из поставкиVeles.exe служебная переменная VELES_PORTABLE_ROOT
закрепляет рабочую область в <каталог установки>/workspace и ограничивает адрес
шлюза значением 127.0.0.1; порт остаётся заданным в gateway.port.
По умолчанию используется workspace/config.json, явно выбранный --config
сохраняет приоритет для расположения файла конфигурации. Это не меняет маршруты
и правила авторизации. Подробности установки.
Хост, порт и токен задаются в gateway-разделе config.json:
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, должны передавать токен в заголовке:
401 Unauthorized с заголовком WWW-Authenticate: Bearer realm="veles-gateway". GET /health остаётся открытым для внешних проверок доступности. Сравнение токена выполняется в постоянном времени (compare_digest).
WebSocket-клиент сначала подключается к /ws. Сразу после accept gateway присылает событие connect.challenge со случайным одноразовым nonce:
connect с токеном. Поддерживаются две формы параметров: вложенная auth.token (предпочтительно) и плоская token.
connect любой другой метод возвращает ошибку -32001 Unauthorized, а входящие/исходящие события не доставляются клиенту.
Исключение до connect составляют методы входа Nerve:
Метод
auth.password.set доступен только после обычного connect с токеном gateway и сохраняет новый пароль Nerve в зашифрованном хранилище Veles.
Если зашифрованное хранилище пароля недоступно или не расшифровывается, auth.login не включает запасной вход через gateway.token и возвращает ошибку хранилища. Это нужно, чтобы потеря ключа шифрования не превращалась в обход уже заданного пароля.
Успешный ответ:
Формат WebSocket RPC
Запросы имеют общий вид:type: "event". У события всегда есть поля event (имя), payload (данные) и seq (монотонный счётчик событий внутри текущего WebSocket-подключения для упорядочивания и восстановления после переподключения):
connect.challenge при подключении, chat для потока сообщений ассистента и пользователя, project для хода проекта, workspace.file.changed для обновления дерева файлов рабочей области в удалённом интерфейсе и workFolders.changed после изменения реестра или содержимого управляемой рабочей папки. Полезная нагрузка workFolders.changed содержит действие action, идентификатор папки workFolderId, относительный путь path и редакцию revision; после переименования или удаления интерфейс также обновляет список чатов. После переподключения клиент заново запрашивает реестр и нужные деревья, потому что события не являются долговременным журналом.
Коды ошибок
Ошибки возвращаются в полеerror с числовым code (совместимым с JSON-RPC) и человекочитаемым message:
Ошибки домена рабочих папок дополнительно содержат строковый
error.data.code, например invalid_path, path_conflict, revision_conflict или active_project_path. Числовой код JSON-RPC при этом сохраняется. Nerve передаёт строковый код браузеру и выбирает по нему сообщение на текущем языке интерфейса; внутренний англоязычный текст шлюза пользователю не показывается.
HTTP endpoints
Для временной совместимости шлюз повторяет эту группу под
/api/kanban: GET и POST /api/kanban соответствуют /api/tasks, а окончания /config, /proposals, /{task_id} и действий задачи соответствуют одноимённым маршрутам выше. Новые клиенты должны использовать только /api/tasks.
Маршруты задач и /api/kanban регистрируются только при modules.tasks.enabled=true. Почтовые двоичные маршруты регистрируются только при modules.mail.enabled=true. Выключенный модуль не создаёт службу и не оставляет маршрут-заглушку; после изменения конфигурации нужен перезапуск шлюза.
Маршруты ядра реализованы в veles/api/routes.py. Прикладные обработчики задач и почты
находятся соответственно в veles/modules/tasks/routes.py и
veles/modules/mail/routes.py; статический диспетчер добавляет их только для включённых
модулей. Общие проверка токена, разбор тела и форма простой ошибки находятся в
veles/api/http.py. Это разделение не меняет адреса или форматы ответов.
Внешние источники задач
GET /api/tasks объединяет локальные задачи Велеса с открытыми задачами YouTrack и Bitrix24. Внешние записи виртуальны: Велес не сохраняет их в workspace/tasks, поэтому повторное получение не создаёт копии. Фильтрация, сортировка, total и постраничная выдача применяются к уже объединённому списку.
Постоянный источник задач Bitrix24 является узким системным адаптером Велеса, а не запуском командного клиента навыка alor-bitrix24. Командный клиент предназначен для интерактивной работы агента с локальным планом и подтверждением, тогда как фоновому чтению доски нужны ограниченные сроки и независимый кэш. Системный адаптер использует те же канонические секреты, закреплённый адрес портала, только методы tasks.task.list и tasks.task.complete, запрет перенаправлений, ограниченный ответ, последовательную запись и аудит. Остальные операции Bitrix24 по-прежнему должны выполняться через навык.
У внешней задачи обычные поля доски дополнены полями:
Настроенное сопоставление состояний изменяет только поле
status, используемое для колонки. Оно не участвует в вычислении capabilities: открытая задача сохраняет разрешённые действия даже при отображении в done, а завершённая или отклонённая не получает их при отображении в todo. Маршрут делегирования повторно проверяет capabilities.delegate на стороне шлюза и не полагается только на кнопку клиента.
Обычные маршруты PATCH, DELETE, reorder, execute, complete, approve, reject и abort не изменяют внешние записи. Единственная операция, которая меняет задачу в исходной системе, — POST /api/tasks/{task_id}/close: непосредственно перед изменением Велес принудительно перечитывает источник, требует состояние ok, повторно проверяет присутствие задачи и возможность завершения, а затем вызывает прикладной интерфейс источника. Для локальной задачи и устаревшего снимка этот маршрут неприменим. Клиент должен получить отдельное подтверждение пользователя перед вызовом.
POST /api/tasks/{task_id}/delegate принимает необязательные параметры model и thinking, создаёт локальную копию внешней задачи в workspace/tasks, переводит её в выполнение и публикует обычный запуск агента. Ответ имеет форму {"task": {...}, "executionStarted": true|false}. Поле локальной задачи delegatedFrom сохраняет taskId, source, externalId и externalUrl. Связь неизменяема через обычное обновление. Для одной пары source и externalId существует не более одной связанной локальной задачи: повторный или параллельный вызов возвращает её с executionStarted=false и не запускает второго исполнителя. При повторном вызове Велес сначала ищет сохранённую локальную связь и не обращается к источнику, поэтому результат остаётся доступен после закрытия или исчезновения внешней задачи. Если публикация первого запуска не удалась, локальная задача возвращается в состояние, допускающее безопасную повторную попытку. Текст внешней задачи передаётся агенту как недоверенные данные и сам по себе не разрешает менять или завершать запись в источнике.
Успешный ответ внешней системы ещё не завершает операцию: Велес снова принудительно читает источник и считает изменение подтверждённым, только если задача исчезла из открытой выборки или больше не допускает close. В Bitrix24 задача с включённым контролем может перейти в review, а не сразу в done. Если после отправки команды записи возникает тайм-аут, серверная ошибка, некорректный ответ или контрольное чтение не подтверждает итоговое состояние, шлюз возвращает 502 external_source_error, записывает в аудит external_close_uncertain и временно запрещает повторное завершение до безопасной сверки с источником.
Ответ списка сохраняет поля items, total, limit, offset, hasMore, добавляет непрозрачный snapshotId и массив sources. Запись источника имеет поля id, enabled, configured, status, taskCount, error, lastAttemptAt, lastSyncedAt; status принимает значения ok, disabled, error или stale. taskCount относится ко всему снимку источника до фильтров доски, lastAttemptAt отмечает последнюю попытку обращения, а lastSyncedAt — последнее успешное чтение. Ошибка одного источника не скрывает локальные задачи и задачи второго источника. При stale Велес отдаёт последний успешный снимок вместе с очищенным от секретов описанием ошибки.
POST /api/tasks/sources/{source_id}/refresh обходит срок кэша и выполняет новое чтение только указанного источника. Ответ {"source": {...}} содержит ту же диагностическую запись; сохранённые снимки объединённого списка аннулируются. Для выключенного источника сетевой запрос не выполняется, а неизвестный source_id даёт 404.
Первая страница запрашивается с offset=0 без snapshot; при необходимости на ней передаётся refresh=true, чтобы принудительно обновить источники, не дожидаясь срока кэша. Каждая следующая страница должна повторять те же фильтры и передавать snapshot=<snapshotId> из первого ответа. Такой снимок не более чем на две минуты фиксирует одновременно задачи и состояния источников, поэтому параллельное создание, удаление или обновление задачи не сдвигает границы страниц. Перезапуск, смена конфигурации и внутренние пределы памяти могут аннулировать его раньше.
Неизвестный или просроченный снимок, изменённые фильтры и ненулевой offset без snapshot дают HTTP 409 с телом {"error":"task_snapshot_expired","snapshotId":...,"message":...}. Клиент должен отбросить уже собранные страницы и один раз начать чтение заново с первой; refresh=true при этом повторять не следует.
Действия close, delegate и refresh_source также доступны через POST /tools/invoke с tool: "tasks". Для close действуют те же предварительная проверка, подтверждение результата и требование отдельного согласия пользователя; delegate использует тот же идемпотентный локальный запуск, а refresh_source принимает sourceId.
Выбор личности
EndpointGET /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:
SOUL.md всех личностей передается модели для выбора, но не возвращается в поле personality.
Загрузка вложений
POST /attachments/upload принимает необработанное тело файла и заголовки:
Ответ содержит объект вложения с
id, workspacePath, absolutePath, mimeType и другими полями. Nerve затем передает такие объекты в chat.send как attachmentRefs. Для обычного вложения чата шлюз игнорирует переданные клиентом папку и подпапку, сам получает workFolderId из метаданных диалога и сохраняет файл в корень связанной папки. Для прямой загрузки требуется одновременно передать x-upload-scope: work-folder и x-work-folder-id; тогда разрешена целевая подпапка. При совпадении имени шлюз добавляет числовой суффикс и не перезаписывает файл.
Для attachmentRefs основным адресом файла считается workspacePath. Поле absolutePath может отсутствовать в запросе от Nerve: шлюз сам находит файл внутри рабочей области Велеса, проверяет, что он существует, и подставляет свой канонический полный путь. Если путь не удается разрешить в рабочей области Велеса, chat.send возвращает ошибку вместо тихой отправки сообщения без вложения.
Вызов tools по HTTP
POST /tools/invoke — это HTTP-обёртка над частью операций gateway, удобная для клиентов без WebSocket-соединения. Тело запроса:
{ "ok": false, "error": { "message": "..." } } (HTTP 400 для невалидного тела или ошибки аргументов). Поддерживаемые значения tool:
Значения
tasks, calendar и mail доступны только при включённых одноимённых модулях. Для выключенного модуля /tools/invoke отвечает 501 как для неподдерживаемого средства. Действие mail.agent_context_prepare возвращает основной объект externalContext вида {"kind":"mail","ref":"..."} и временно дублирует mailContextRef для старых клиентов.
Для действия folders_list поле folders содержит объекты с полями name, displayName, delimiter и flags. name — исходный идентификатор папки в протоколе IMAP; только его следует передавать в последующих почтовых действиях. displayName содержит читаемое название, преобразованное из транспортной кодировки, delimiter задаёт разделитель уровней вложенности или имеет значение null, а flags сохраняет признаки папки, включая запрет прямого выбора. Корневое поле default также содержит исходный идентификатор и сопоставляется с name, а не с отображаемой подписью.
Действие status дополнительно возвращает autoAnalyze, analysisModel, pollIntervalSeconds, remoteImagePolicy и accountFingerprint. analysisModel возвращает нормализованное значение channels.email.clientAnalysisModel без пробелов по краям; пустая строка означает модель agents.defaults.model, а префикс поставщика сохраняется для маршрутизации отдельного вызова llm.chat. accountFingerprint — хэш-отпечаток имени узла, порта, режима защищённого соединения IMAP и имени пользователя; пароль в него не входит. Nerve использует отпечаток только для разделения локального кэша и защиты длительной записи от смены учётной записи. Действие message_read разделяет обычные attachments и встроенные inlineImages. Каждый элемент inlineImages содержит partId, contentId, contentLocation, mimeType, size и name; идентификатор части стабилен в пределах исходного MIME-сообщения. Элементы remoteImages содержат непрозрачный id и исходное значение url, нужное интерфейсу только для сопоставления с очищенным HTML. Клиент не должен открывать этот адрес напрямую: для обоих видов изображений используются защищённые HTTP-маршруты выше.
analysis_get_many принимает не более 100 идентификаторов вида { key, folder, uidValidity, uid, messageId }, необязательный promptVersion и обязательный верхнеуровневый accountFingerprint из последнего ответа status. key нужен только для сопоставления ответа. Если есть messageId, запись связывается с ним и переживает перемещение письма между папками; иначе используется точная тройка из исходного имени папки, действительного ненулевого числового uidValidity и uid. Без messageId и действительного uidValidity чтение и запись сводки отклоняются, потому что локатор нельзя безопасно отличить после смены поколения папки. Если во время операции конфигурация переключилась на другую учётную запись, отсутствующий или несовпадающий отпечаток останавливает чтение или запись. Ответ имеет вид:
analysis_save принимает message того же вида, структурированный объект analysis, положительный promptVersion и тот же обязательный верхнеуровневый accountFingerprint. Успешные результаты записываются по версиям в workspace/.veles/mail/analysis.db; более новая ошибка или более старый успешный результат не уничтожают уже сохранённый новый результат. В базу допускаются только поля категории, важности, сводки, черновика ответа, действий, событий, модели и времени. Исходный текст, HTML, изображения, вложения и секреты отбрасываются. Ответ содержит key и сохранённую запись в том же формате, что элемент analyses.
Для calendar действие events_create принимает sourceId и объект event с полями title, startMs, endMs, allDay, location, description. Запись разрешена только для источника EWS; источники ICS остаются только для чтения.
Для интерактивного управления чатом и сессиями предпочитайте WebSocket RPC — /tools/invoke рассчитан на скриптовые и служебные вызовы.
При создании и обновлении задания cron сохраняет не только текст и расписание, но и поля запуска: sessionKey, personalityId, payload.model и payload.thinking. Если указан personalityId, но нет sessionKey, задание запускается в основной сессии этой личности. При выполнении Велес применяет сохраненные модель и уровень рассуждения к сессии задания перед обращением к модели.
WebSocket RPC methods
Статус
providers.usage возвращает поставщиков в порядке общего реестра. Поставщик без подключённых учётных данных или без обработчика расхода не попадает в ответ. Интерфейс не должен проверять конкретные имена поставщиков: подписи, полное значение, короткое значение для значка, доля шкалы и время сброса приходят вместе с показателем.
status принимает ok, stale или unavailable. При временной ошибке Велес возвращает последнюю успешную запись со значением stale; если успешной записи ещё не было, поставщик возвращается как unavailable без показателей. Успешные ответы хранятся в памяти 30 секунд и удаляются при обновлении конфигурации или учётных данных. resetAt всегда задан в миллисекундах от начала эпохи.
Для OpenAI Codex название окна не определяется его положением primary_window или secondary_window в ответе поставщика. Велес читает limit_window_seconds: 18 000 секунд дают показатель five_hour, 604 800 секунд — weekly, 86 400 секунд — daily, а другие длительности получают идентификатор window_<секунды> и подпись с фактическим числом недель, дней, часов или минут. Если старый ответ не содержит длительность, для совместимости основное окно считается пятичасовым, а дополнительное — недельным.
Конфигурация и перезапуск
config.get не возвращает gateway.token в значении и удаляет это поле из схемы для редактора. Остальные значения, которые являются целями секретов, возвращаются как маркеры вида { "$velesRedacted": true, "targetId": "..." }, если значение уже задано. config.save восстанавливает такие маркеры и скрытые поля из исходной конфигурации перед валидацией, поэтому интерфейс может сохранить остальные изменения, не раскрывая секреты.
interface.preferences.get возвращает { "usageBadgeMetric": "openai_codex:weekly" }. Метод interface.preferences.set принимает объект той же формы. Допустим пустой выбор либо ключ вида <поставщик>:<показатель> длиной не более 200 символов; произвольные поля конфигурации через этот метод изменить нельзя.
Методы config.raw.get и config.raw.save предназначены для полного текстового редактора конфига. Они показывают физическое содержимое файла, поэтому в ответе могут быть gateway.token и секреты, если они записаны в config.json открытым текстом. При сохранении шлюз проверяет, что текст является объектом JSON и проходит схему Велеса, поддерживает проверку expectedMtimeMs от устаревшей записи, сохраняет переданный текст как есть и не планирует перезапуск. После такой правки пользователь перезапускает шлюз вручную.
Чат
Поля метаданных
externalContextKinds и externalSources относятся только к одному разговору. Они сохраняют общую границу недоверенного внешнего содержимого при последующих сообщениях, но удаляются при создании или сбросе разговора. Старое поле mailSource удаляется при сбросе только для совместимости с уже сохранёнными сессиями.
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 определяет фазу:
Поля payload события
chat:
Клиент группирует события по
runId, применяет delta поверх накопленного текста и фиксирует ответ на final.
Событие workspace.file.changed приходит, когда шлюз замечает создание, изменение, удаление или переименование файла в рабочей области. Оно нужно удалённому Nerve, который не может сам наблюдать файловую систему шлюза. Событие не хранится в постоянной очереди: если подключение интерфейса уже закрывается, шлюз прекращает пересылку для этого подключения, а после переподключения интерфейс заново запрашивает дерево файлов.
Поля payload события workspace.file.changed:
Если один диалог личности уже выполняется, а Nerve создает, активирует или отправляет сообщение в другой диалог той же личности, gateway может использовать отдельный ключ выполнения
personality:<id>:conv:<conversationId>. Такой ключ нужен только для живого выполнения: сохраненная история, conversationKey и rootSessionKey остаются привязаны к корневому диалогу личности, а sessions.list не показывает изолированный ключ как отдельную строку.
Проекты
Режим Проект ведёт сложную задачу через уточнение требований, утверждение соразмерного плана, последовательное выполнение и итоговый результат. Исследование обязательно для обычных проектов, но для проекта с пользовательским материалом добавляется только при необходимости. Состояние принадлежит Велесу и связано с одним диалогом; Nerve показывает сохранённые сообщения и канонический снимок, а также отправляет решения пользователя. Основным идентификатором связи служитconversationId. Технический ключ выполнения может меняться между корневой сессией личности и изолированной сессией диалога, поэтому уведомления о состоянии, отмена и итоговое сообщение всегда разрешают текущую сессию заново по идентификатору диалога. Сохранённый при создании проекта sessionKey нельзя использовать как неизменный адрес.
Методы:
Элемент
answers имеет вид { "questionId": "...", "optionId": "..." } для подготовленного варианта или { "questionId": "...", "customText": "..." } для собственного ответа. optionId и customText взаимоисключающие. Требуется ровно один допустимый ответ на каждый обязательный вопрос.
expectedRevision включает оптимистическую защиту от устаревшего интерфейса: если номер не совпадает с текущим снимком, действие отклоняется, а клиент получает актуальное состояние через projects.get. idempotencyKey защищает от повторного применения одного решения после сетевого повтора. Ответ изменяющего метода содержит snapshot; поля content и started передают краткий результат действия и признак запуска фонового этапа, когда они применимы.
Снимок и состояния
Канонический снимок содержит как минимум:
Объект
plan содержит логические поля artifactWorkflow, documentWorkflow и массив deliverableFormats. Для документа, отчёта, книги, предложения, инструкции, презентации, таблицы или другого готового пользовательского файла artifactWorkflow равно true. Такой план содержит хотя бы один этап implementation; первым может быть research или implementation, а последний может быть implementation или validation. Отдельные исследование и независимая проверка не являются обязательными. documentWorkflow отделяет читательские документы от презентаций и электронных таблиц. Массив workPackages необязателен и применяется только к производственному этапу действительно большого или независимо делимого материала; каждый пакет имеет собственные название, цель, критерии, состояние, число попыток и путь к материалу. deliverableFormats содержит от одного до четырёх утверждённых форматов. Явно названные пользователем форматы обязаны сохраняться; при отсутствии формата модель выбирает подходящий и обычно использует DOCX для редактируемого делового документа. Для остальных проектов artifactWorkflow равно false, первым остаётся research, а заключительная проверка разрешена, но не обязательна. Велес отдельно распознаёт явные русские и английские запросы на создание, обновление или перевод таких материалов по исходному запросу, подтверждённым ответам и формату вложения. Отрицание вроде просьбы не создавать отчёт не включает эту последовательность, а явное изменение плана может добавить, заменить или убрать готовый материал. Модель планирования не может передать значение, противоречащее распознанному изменению: несовпадение отклоняется и уходит в обычную попытку исправления JSON. Изменение формата, значения, роли этапа, авторского пакета или критерия готовности считается существенным и требует нового утверждения. Утверждённый план с пользовательским материалом после исследования автоматически не пересоставляется.
Поддерживаются состояния 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.
Общие поля взаимодействия:
Для
questionnaire передаются questions, suggestedOutputPath и outputPath; для plan — title, summary, artifactWorkflow, documentWorkflow, deliverableFormats, steps и planPath; для error — объект error из снимка. Nerve показывает признак artifactWorkflow на карточке плана, чтобы пользователь видел, что этапы создадут и проверят сохраняемый результат. Клиент не должен применять сообщение с меньшей revision поверх уже показанного сообщения с тем же id. Новая редакция плана добавляется в конец диалога, а действия остаются доступны только на последней карточке плана. Если специальное сообщение пропущено, Nerve получает снимок через projects.get и временно строит из него только текущее действие, требующее ответа пользователя.
Работа исполнителя использует существующий поток чата: пояснения приходят в chat со состоянием delta, а вызовы и результаты средств — в toolEvents по тому же контракту, что и в обычном режиме. Полностью завершённые пары «вызов — результат» дополнительно записываются в историю как контрольные точки, поэтому уже показанные результаты не исчезают после аварийного перезапуска и в истории не остаётся незакрытый вызов прерванного средства. После этапа контрольные точки текущей попытки заменяются полным ходом: сообщения помощника, вызовы и результаты средств сохраняются в обычной истории диалога, а окончательный вывод этапа приходит как обычное сообщение помощника. У такого окончательного события поле payload.historyRefresh равно true: клиент сразу объединяет сохранённый ход с живой лентой, чтобы промежуточные пояснения не исчезли после очистки потокового буфера. В проекте с пользовательским материалом последний утверждённый исполнитель формирует естественный текст завершения со ссылками на канонические файлы или папку. После объективной проверки сервер публикует этот текст без дополнительного вызова модели; если блок отсутствует, сервер строит короткое безопасное сообщение только из проверенных путей.
Событие project
Поле payload события project имеет одну из двух форм:
snapshot является уведомлением о новой сохранённой редакции. progress — промежуточный, объединяемый интерфейсом ход текущей редакции; он не изменяет каноническое состояние. Оба вида событий могут быть пропущены при разрыве соединения и не являются постоянной очередью. После переподключения, пропуска редакции или смены диалога Nerve обязан вызвать projects.get, а не восстанавливать проект только из событий.
Папка и последовательность работы
Если пользователь не выбрал папку, Велес предлагаетdocs/<YYYY-MM-DD>-<slug>--<id8>. outputPath — точный относительный путь внутри рабочей области. Запрещены абсолютные пути, выход через .., скрытые пути, символические ссылки за пределы рабочей области, корень рабочей области и служебные каталоги .veles, sessions, memory, tasks. Существующая папка с посторонними файлами допустима, но совпадение с любым путём, которым управляет проект, отклоняется до записи. Проект не перезаписывает и не удаляет посторонние файлы.
Велес создаёт в папке brief.md, plan.md, дополняемый progress.md, полные результаты этапов steps/NN-<slug>.md, при наличии исследовательского этапа — research.md, при наличии отдельных авторских заданий — work-packages/NN-MM-<slug>.md, а при ошибках — безопасные отчёты failures/*.md. Для нового проекта с пользовательским материалом finalization.md, draft.md, quality-report.md и final.md не создаются. План содержит не более десяти выполняемых этапов и начинается только после утверждения. Повторная оценка плана после исследования сохраняется только для проектов без пользовательского материала.
Каждый этап запускается строго после предыдущего, в отдельном свежем контексте и без возможности произвольно создавать вложенных исполнителей. Если утверждённый производственный этап содержит workPackages, координатор запускает их строго по очереди, сохраняет полные материалы и затем запускает редактора родительского этапа. Для читательского документа подходящий исполнитель уже получает загруженный форматно-независимый навык report-authoring; повторно открывать его SKILL.md не требуется. Дополнительные навыки исследования, делового анализа, проверки данных, расчётов, визуализации и сборки формата модель выбирает из фактически доступного каталога только тогда, когда они нужны содержанию этапа или утверждённому формату. Этап получает инструкции личности и рабочей области, исходное описание, ответы, утверждённый план, ограниченное резюме исследования, перечень готовых материалов и краткую передачу только от предыдущего этапа. Точный outputPath передаётся планировщику и каждому исполнителю как обязательная папка результата: канонические материалы, отчёты проверки и реестры доказательств сохраняются внутри неё, а устаревшие абсолютные пути или пути в корне рабочей области исправляются с сохранением имени и назначения файла. Реестр доказательств служит кэшем источников; сырые материалы повторно открываются только при пробеле или противоречии. При продолжении передаются точный прошлый отчёт, краткая передача и обнаруженные пробелы, а не вся история сеанса. Реестр средств создаётся заново для каждого запуска как снимок доступных средств основного цикла; из него исключаются ask_user, message, spawn, model и cron. Ошибка отдельного вызова возвращается модели с предложением выбрать обходной путь и сама по себе не завершает этап.
Для задач по созданию документов краткая передача результата считается только указателем. Полные разделы, основная рукопись, готовые файлы и отчёты проверок создаются на обычных этапах и передаются дальше через пути в рабочей области. Последний этап обязан создать все значения deliverableFormats, исправить подтверждённые дефекты, выполнить или повторно использовать профильные проверки и подготовить сообщение пользователю. Явно запрошенный PDF, Markdown, HTML или другой формат нельзя заменить на DOCX. Если среди форматов есть DOCX, встроенный навык создаёт редактируемый файл через python-docx и формирует настоящий docx-audit.json; исполнитель не должен заменять проверку самостоятельно составленным JSON. DOCX и отчёт сохраняются внутри выбранного outputPath. Успешное завершение требует ok: true и совпадения sourceSha256 с текущим DOCX. После последнего изменения DOCX исполнитель повторяет проверку и больше не изменяет и не перемещает файл. При изменении уже подтверждённого канонического DOCX Велес сохраняет его путь и отдельно сообщает об устаревшем отчёте. Microsoft Word и управление рабочим столом не используются.
После завершения последнего этапа проекта с пользовательским материалом координатор не запускает дополнительных исполнителей. Он берёт канонические пути только из поля Canonical deliverables последнего ## Finalization packet и ранее подтверждённых записей deliverable-*, проверяет нахождение внутри папки проекта, существование, ненулевой размер и наличие каждого утверждённого формата. Для DOCX дополнительно проверяется успешный отчёт о структуре с контрольной суммой текущего файла. Ранние материалы этапов не превращаются в обязательный набор чтения, а наличие или отсутствие строки ARTIFACT_READY не влияет на решение.
Если объективных признаков не хватает, координатор до двух раз переводит последний этап обратно в pending и передаёт новому исполнителю точный перечень недостающих файлов или проверок, предыдущую передачу и путь к прошлому отчёту этапа. После успешного продолжения проект сразу становится completed. После исчерпания внутренних продолжений создаётся возобновляемая ошибка artifact_readiness_failed; неожиданная ошибка самой проверки имеет код artifact_completion_failed. При projects.resume старое состояние с synthesis_repair_failed не возвращается в прежнюю трёхпроходную сборку: существующие результаты проходят новую объективную проверку.
Ошибка этапа останавливает последующие этапы и сохраняет отчёт попытки. projects.resume означает продолжение, а не очистку или откат: Велес не удаляет файлы, материалы или изменения. Новый контекст запускается только для незавершённого этапа и получает точный прошлый отчёт, краткую передачу и обнаруженные пробелы, после чего проверяет фактическое состояние и продолжает с последней подтверждённой точки. Завершённые этапы автоматически не повторяются. projects.cancel отменяет ожидающий или приостановленный проект, а chat.abort также отменяет принятый запрос режима «Проект», в том числе до создания постоянного состояния проекта. sessions.delete сначала отменяет незавершённый или ещё ожидающий обработки проект по conversationId и только затем скрывает или удаляет историю; папка материалов не удаляется.
После перезапуска шлюза состояния ожидания ответов и утверждения остаются без изменения. Подготовка вопросов или плана и активное выполнение переводятся в paused с ошибкой gateway_restarted и требуют явного projects.resume: изменяющий файлы этап никогда не повторяется автоматически после перезапуска. При запуске Велес также сверяет метаданные диалога с каждым сохранённым проектом, включая завершённые и отменённые, чтобы сбой между записью состояния и уведомлением не оставил устаревший признак работы.
Сессии
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. Для связанного с рабочей папкой диалога ответы содержат устойчивый workFolderId и текущий workFolderPath; если каталог или запись недоступны, идентификатор сохраняется, а путь может быть null. Создание диалога без workFolderId всегда очищает прежнюю связь и не наследует её от активной сессии.
Рабочие папки
Реестр рабочих папок принадлежит Велесу и хранится атомарно в.veles/work-folders.json; физические каталоги находятся в docs/work-folders/<название>. Nerve не хранит копию реестра как источник данных. Отсутствующий каталог возвращается как available: false, missing: true, но запись автоматически не удаляется. Реестр версии 2 использует только это расположение и не переносит записи или каталоги из прежнего пути.
Имена нормализуются в Юникоде и проверяются по общим ограничениям Windows и POSIX. Сравнение имён папок и элементов не учитывает регистр, а изменение только регистра проходит через безопасный промежуточный путь. Все пути в HTTP- и WebSocket-контрактах относительны рабочей области и используют
/ даже при запуске на Windows. Символические ссылки, точки повторного анализа с выходом из рабочей области, ./.., зарезервированные имена и перезапись существующего пути запрещены.
Каскадное удаление отменяет активные ответы и проекты связанных диалогов и физически удаляет их транскрипты, резюме памяти, состояния проектов и содержимое папки независимо от sessionMemory.deleteBehavior. До первого разрушительного изменения создаётся служебная транзакция в .veles/work-folder-transactions; после перезапуска наличие записи в реестре приводит к откату, а отсутствие записи — к завершению очистки.
Nerve предоставляет браузеру прокси-маршруты GET/POST /api/work-folders и PATCH/DELETE /api/work-folders/:id. Создание подпапки проходит через POST /api/files/directory, а переименование, перемещение и удаление — через маршруты /api/files/rename, /api/files/move и /api/files/delete. Для операции в управляемой рабочей папке передаётся workFolderId; без него создание, переименование и перемещение относятся к обычному дереву общей рабочей области. Эти маршруты не являются отдельным хранилищем и в основном режиме вызывают шлюз.
Маршруты рабочих папок регистрируются независимо от переключателей встроенных модулей: отключение задач, календаря или почты не скрывает реестр рабочих папок.
Личности
Файлы workspace
Файловые методы работают с общей рабочей областью Велеса.
personalityId не разделяет пользовательские файлы по разным корням: личность меняет промпт, навыки и историю, но не создает отдельного файлового пространства. Исключение — собственные служебные файлы личности внутри personalities/<id>/. Для операций внутри управляемой папки обязателен workFolderId, а пути задаются относительно этой папки. Без него создание каталога, переименование и перемещение относятся к обычному пути общей рабочей области. Перемещение между разными рабочими папками невозможно. Корень и содержимое docs/work-folders/ нельзя обходным путём создавать, переименовывать, перемещать или удалять обычными операциями полного дерева. Каталог docs нельзя переименовать или переместить целиком, поскольку он служит контейнером управляемого корня; его остальные дочерние элементы доступны для обычных файловых операций.
После переименования или перемещения Велес обновляет структурированные ссылки вложений и сохранённые состояния проектов. Если обновить ссылки не удалось, файловая операция откатывается. Путь активного результата проекта нельзя перемещать или удалять до завершения либо отмены проекта. Запись существующего файла по-прежнему сохраняет жёсткие ссылки.
Перед окончательным удалением personalities.files.delete временно переименовывает файл и логически скрывает записи с его точным путём из векторного индекса. Проверка выполняется для любого типа файла: для звука и других неиндексируемых форматов она успешно сообщает об отсутствии записей. Небольшое поколение удаляется в той же операции. Для крупного или унаследованного поколения Велес сначала надёжно записывает очередь очистки и удаляет путь из видимого набора, затем удаляет фрагменты, полнотекстовые строки и векторы подтверждёнными порциями в следующих проходах. Начальная операция остаётся в основном потоке шлюза, которому принадлежит соединение SQLite; перенос её в рабочий поток нарушает привязку соединения к потоку. Если скрытие или постановка в очередь завершаются ошибкой, Велес восстанавливает исходное имя и не удаляет файл. Ошибка более поздней порционной очистки не возвращает уже удалённый исходник: очередь сохраняется и повторяется, а путь остаётся невидимым для поиска.
Успешный ответ содержит вложенный объект vectorCleanup: enabled показывает, была ли векторная память включена, indexed — были ли найдены записи пути, а chunksDeleted — сколько фрагментов логически удалено из видимого поколения. Поле cleanupQueued равно false, если физическая очистка завершилась сразу, и true, если оставшиеся служебные строки поставлены в долговечную очередь порционной очистки.
У 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 обе ссылки продолжают указывать на один файл. Для новых файлов, обычных файлов без дополнительных жестких ссылок и символических ссылок шлюз использует запись через временный файл и замену пути.
Навыки
Методы маркетплейсов не содержат полей конкретного источника на верхнем уровне. При создании тип выбирает внутренний адаптер, а его параметры передаются в
settings. Зарегистрированы типы github и gitlab; оба используют поля repositoryUrl и необязательное branch. Тип github принимает только публичный адрес HTTPS на github.com. Тип gitlab принимает адрес HTTP или HTTPS на любом узле, включая самостоятельные установки, нестандартные порты и вложенные группы. В адресе GitLab запрещены учётные данные, строка запроса и фрагмент; путь должен содержать пространство имён и репозиторий. Неизвестный тип даёт unsupported_marketplace_type. Методы изменения, удаления и синхронизации определяют тип по сохранённой записи.
Доменный модуль каталога расположен в veles/marketplaces/: внешний код импортирует его публичный интерфейс из veles.marketplaces, а реализация каталога и адаптеры GitHub и GitLab находятся в catalog.py. Адаптеры используют общий безопасный запуск Git без командной оболочки; интерактивный запрос учётных данных отключён. Общий безопасный разбор и проверка SKILL.md расположены в veles/utils/skill_manifest.py и используются как каталогом маркетплейсов, так и загрузчиком установленных навыков. Каталог veles/skills/ предназначен только для поставляемых навыков и их ресурсов; исполняемый код Велеса туда не помещается.
У каждого метода свой точный успешный ответ; общего ответа с необязательными полями нет:
Ошибка любого метода имеет форму
{ok: false, error: {code, message}}. HTTP-прокси Nerve сохраняет те же формы ответов для соответствующих маршрутов.
Шлюз сам создаёт id и path. Пользовательская запись всегда сохраняется с editable=true и removable=true; эти поля нельзя передать в мутации. Предопределённая запись принудительно получает оба флага false: синхронизация и установка разрешены, а изменение и удаление возвращают соответственно marketplace_not_editable и marketplace_not_removable.
Мутации маркетплейсов сохраняют актуальную конфигурацию внутри работающего шлюза и не планируют его перезапуск. Разрыв WebSocket после создания, изменения или удаления не является частью контракта.
При разборе SKILL.md обязательные и стандартные поля проверяются по спецификации навыков, а дополнительные служебные поля верхнего уровня разрешены и не ограничиваются закрытым перечнем. В metadata ключи должны быть строками, а значениями могут быть строки или объекты. Объекты являются расширением Велеса и позволяют хранить структурированные сведения клиента, в том числе metadata.veles. Для совместимости Велес также принимает всё поле metadata как строку с корректным JSON-объектом; после разбора к нему применяются те же ограничения. Повреждённая строка или значение JSON другого типа исключает навык из каталога.
При синхронизации шлюз записывает skills.json с version: 1, временем синхронизации, объектом источника, массивом навыков и массивом предупреждений. У каждого навыка обязательны secrets с полями name, targetId, required и settings с нормализованными описаниями настроек. Необязательный объект requirements содержит bins — упорядоченный список требуемых командных программ. Если объекта нет, особых требований к программам у навыка нет; поэтому сохранённые ранее файлы версии 1 остаются допустимыми. Неизвестные поля и некорректная форма присутствующего requirements по-прежнему делают кэш недействительным.
Секреты извлекаются из metadata.veles.requires.env и metadata.veles.secrets.env файла SKILL.md. Первый список обязателен, второй необязателен; при повторении имени обязательность имеет приоритет. Требуемые командные программы извлекаются из metadata.veles.requires.bins. Настройки извлекаются из свойства veles корневого config.json навыка. Поддерживаются строки и числа, обязательность, значение по умолчанию, минимальные и максимальные значения или длины и регулярное выражение для строки. Некорректное описание исключает навык из каталога с предупреждением.
Записи навыков в ответах skills.marketplaces.catalog, create, update и sync содержат available. Если хотя бы одна объявленная командная программа не найдена в системном пути запуска процесса Велеса, значение равно false, а missing.bins перечисляет отсутствующие программы; иначе available равно true, а missing отсутствует. Результат поиска каждой программы хранится в общей памяти процесса 30 секунд и переиспользуется каталогом установленных навыков и маркетплейсами. После истечения этого срока программа лениво проверяется при следующем запросе. Операции синхронизации источника принудительно перепроверяют все программы его навыков, не запуская их.
skills.marketplaces.install обязательно принимает объекты secrets и settings, даже когда они пусты. skills.marketplaces.configure принимает те же объекты и обязательные массивы removeSecrets и removeSettings; отсутствие изменений передаётся пустыми коллекциями. Обязательное значение нельзя удалить. Настройки записываются в skills.entries.<skillId>.config только при фактическом добавлении, изменении или удалении настройки, а секреты — только в зашифрованное хранилище. Установка, настройка и копирование каталога выполняются согласованно; при ошибке шлюз восстанавливает прежнюю конфигурацию и секреты.
Успешные create, update и sync гарантируют итоговый состав папки маркетплейса: source/ и skills.json. Очистка временных каталогов является частью транзакции и учитывает файлы Git с атрибутом «только чтение». На Windows шлюз использует расширенный формат системного пути для глубоко вложенных деревьев, снимает ограничения со всего удаляемого дерева и повторяет очистку при кратковременной блокировке файла. Если очистку завершить нельзя, шлюз не возвращает успешный результат и использует код marketplace_cleanup_failed; при удалении записи эта ошибка безопасно откатывается и возвращается как marketplace_delete_failed.
При полном отсутствии допустимых навыков причины отклонения найденных SKILL.md записываются в журнал шлюза с меткой [skill-marketplace-scan]. Запись содержит идентификатор и тип маркетплейса, относительный путь и причину; итоговая строка сообщает полное количество отклонённых файлов. Велес выводит не более первых 50 причин, а внешние значения переводит в одну строку и ограничивает по длине. Если SKILL.md не найден, итоговая запись явно сообщает об этом.
Ошибки удаления записываются в журнал шлюза с меткой [skill-marketplace-delete]. Сообщение показывает этап операции, идентификатор маркетплейса, затронутые пути, тип системной ошибки, errno и код ошибки Windows. Успешные этапы и отдельные повторные попытки не журналируются. Настройки источника и секреты в сообщение не включаются.
Nerve проксирует этот контракт через GET/POST /api/skill-marketplaces, PATCH/DELETE /api/skill-marketplaces/:id, POST /api/skill-marketplaces/:id/sync, POST /api/skill-marketplaces/:id/skills/:skillId/install и PATCH /api/skill-marketplaces/:id/skills/:skillId/configuration. Все операции с файловой системой, конфигурацией, секретами и источником остаются на стороне Велеса.
Секреты
Ответы
secrets.oauth.status и secrets.oauth.complete со статусом error содержат исходное безопасное описание в error и машинный код в errorCode. Возможные коды: oauth_authorization_failed, authorization_code_missing, oauth_state_mismatch, token_exchange_failed, secret_storage_unavailable и oauth_completion_failed. Ответ со статусом expired использует код flow_expired.
Если OpenAI вернул в адресе возврата параметры error и error_description, Велес передаёт их безопасное описание с кодом oauth_authorization_failed. Ошибка обмена кода содержит код ответа конечной точки токенов и очищенное описание поставщика. Коды авторизации, значения состояния попытки и токены в ответы не попадают.
LLM
llm.chat предназначен для служебных сценариев управления, а не для обычных пользовательских сессий. Для пользовательского чата используйте chat.send.
Пример жизненного цикла WebSocket-клиента
Минимальный сценарий «подключиться и отправить сообщение»: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).