NATS: текущий путь Chat Agent
Контракт исходников на 04.11.2026. Архитектура, поставка, batch protocol 3. Исторические workflow, Core leases и Trickle ICE не являются рабочими путями. Их прежнее описание доступно в версии до удаления Core.
Веб-виджет Chat Agent
Модуль /widget/chat-agent.js открывает общую панель в документе принимающей
страницы. Она получает UI- и аудиодоступ по POST /auth/nats с полем
client: "widget". Gateway принимает профиль с HTTP(S)-origin за штатной
авторизацией. Транспорт использует /chat-ws, команды и события конкретного
client_id. Аудиодоступ открывается после включения голоса.
Команда browser_context регистрирует context_id и схему page_action по
протоколу 2. Вызов приходит в chat.ui.<client_id>.events.tools, результат
возвращается в chat.ui.<client_id>.cmd.browser_tool_result с теми же
request_id, context_id и tool_call_id. Расширение продолжает передавать
run_js по этому протоколу. Профиль веб-виджета не выдаёт права Monitor.
При TTS_ENABLED=1 озвучка доступна авторизованным веб-виджетам во всех
сессиях. Для расширения список UUID задаёт TTS_PILOT_SESSION через запятую.
При открытии панели веб-виджет регистрирует проигрыватель, если пользователь
не отключил озвучку. При блокировке воспроизведения браузером панель предлагает
включить звук вручную.
На странице Chat Agent, Forms и внутри iframe подключается один модуль:
<script type="module" src="https://chat-agent.gis-master.ru/widget/chat-agent.js"></script>
Все файлы пакета находятся под /widget/ и доступны публично с CORS.
Модуль определяет backend по своему URL. Bootstrap /auth/nats и WSS
/chat-ws проходят Slim Auth. Preflight OPTIONS возвращает только разрешения
CORS. Для профиля widget допускается любой HTTP(S)-origin; cookies браузера
и политика CSP принимающей страницы должны разрешать подключение.
Виджет создаёт свою сессию независимо от маршрута сайта. ID хранится в
localStorage принимающего origin: chat-agent.widget.session.top для
верхнего документа и chat-agent.widget.session.frame для iframe. Страницы
одного origin разделяют соответствующую историю. Каждый открытый экземпляр
имеет свои client/context/document ID и исполняет инструменты только своего
документа. Если хранилище недоступно, ID живёт до перезагрузки и панель
показывает предупреждение. В Monitor используются эти же session/request ID.
Для внешнего скрипта и аудио CSP разрешает origin Chat Agent в script-src,
connect-src и worker-src, его WSS в connect-src, wasm-unsafe-eval в
script-src, blob: в worker-src и media-src. Родитель iframe разрешает
microphone и autoplay через allow и Permissions Policy. Микрофон требует
действия пользователя и защищённого контекста. При блокировке сторонних
cookies подключение остаётся недоступным до разрешённого входа браузера.
Транспорт и владельцы
Браузер получает защищённый HTTPS bootstrap /auth/nats или bootstrap расширения,
затем использует NATS WebSocket /chat-ws (расширение — свой существующий маршрут).
Capability, срок и per-client reply subject проверяет Gateway. Credentials здесь не приводятся.
Backend использует TCP NATS: CHAT_NATS_URL для чата, ASR_NATS_URL для аудио.
Production разделяет эти брокеры; локальная конфигурация может явно использовать один.
| Откуда → куда | Subject | Назначение |
|---|---|---|
| UI → Gateway | chat.ui.<client_id>.cmd.<command> |
Команды, история, Monitor и диагностика; JSON request/reply |
| Audio client → Gateway | chat.audio.<client_id>.cmd.<command> |
start/phrase/end/abort; capability принадлежит владельцу |
| Audio client → Ingress | chat.audio.<client_id>.pcm |
Бинарные Opus/PCM-пакеты существующего протокола |
| Gateway → Ingress | chat.browser.audio |
Проверенная команда управления потоком |
| Gateway → Agent | chat.internal.agent.widget |
Submit, tool result, cancel; ответ о приёме не является итогом |
| Agent → LLM | nats.llm.<user_id> |
Один agent_step, подготовленные сообщения и инструменты |
| LLM → Agent | chat.internal.model.capture |
Ограниченные диагностические входы/выходы; без повторной генерации |
| Agent / Ingress → Gateway | nats.events.<user_id> |
Результат команды/инструмента или транскрипция исходному клиенту |
| Gateway → UI | chat.ui.<client_id>.events.ui, .events.tools |
События и выполнение инструмента текущего документа |
| Ingress → Audio client | chat.audio.<client_id>.events.audio |
READY, ошибки, состояние и диагностика захвата |
| Ingress → ASR bridge | webrtc.komaroff.asr.stream.<session_id> |
Готовый mono PCM16 16 кГц: begin, блоки ≤128 КиБ, commit RPC |
| ASR bridge → Ingress | webrtc.komaroff.asr.text.<session_id> |
final/rejected/error, измерения и исходные наблюдения worker |
| Bridge → worker → bridge | to.inference.webrtc.asr.sample.<rpc_id> / from.inference.webrtc.asr.sample.result.<rpc_id> |
Существующий account import/remap во внешнем ASR |
| Delivery → Gateway / Ingress | chat.internal.gateway.lifecycle / chat.internal.audio.lifecycle |
state/drain/resume каждого владельца; браузеру запрещены |
Допуск и результат
Gateway проверяет каталог страницы в WidgetStore, передаёт Agent проверенный
WidgetBinding и ограниченный admission deadline. Неизвестный исход не повторяет
команду. Очереди, receipts, отмена и PostgreSQL fencing остаются у Agent.
Ingress владеет тремя живыми потоками и одной replay-резервацией. Повторный контекст,
чужая сессия, истёкший grant, пропущенные пакеты и неподдерживаемый протокол отклоняются.
Для batch до первой отправки регистрируется ожидаемый phrase_id. Единственный
listener проверяет session/stream/phrase/revision и принимает терминальный результат
один раз; архив и live-измерения получают тот же принятый результат.
После end ожидание результата ограничено 20 секундами. Abort закрывает только владельца.
ASR не запускает Agent самостоятельно. Ingress возвращает транскрипцию проверенному
widget_route; панель добавляет текст и отправляет обычную команду с тем же request ID.
chat.browser.core, chat.agent.audio.control и Core lifecycle больше не имеют
действующих consumers. Старый lifecycle используется лишь процедурой удаления Core
до остановки старого контейнера и при её откате.
Replay остаётся на chat.internal.audio.replay. Gateway подготавливает архив,
Ingress резервирует допуск и получает результаты через общий listener. Для полного
монотонного Opus-повтора используется один decoder iterator; новый повтор начинает
декодирование сначала. Резервация освобождается после подтверждения idle от ASR.
Compare/replay не порождают пользовательские команды или новые архивные записи.
Наблюдения и readiness
nats_log хранит реальные передачи, связки ID и исходные часы. Бинарный звук туда
не попадает. Producer сохраняет назначение операции; Monitor использует одну проекцию
для журнала и схемы. Неизвестный отправитель и потерянные записи остаются неизвестными.
Историческая метка bus означает отсутствие имени участника, а не сервис NATS.
RPC-квитанция содержит reply_to_event_id исходной передачи: квитанция результата
инструмента не является новым выполнением или окончательным ответом.
Расширение передаёт в существующей widget_client_receipt необязательные
parent_event_id, subject, received_at, displayed_at вместе с client_ms.
Четыре новых поля принимаются только вместе. Gateway сохраняет первую квитанцию
в существующем receipt с серверным saved_at. Monitor проверяет владельца,
сессию, запрос, subject и ссылку на отправку Gateway, затем показывает получение
и локальное добавление ответа в чат. Тело ответа остаётся по payload_ref;
время браузера не используется как время сервера или сетевая задержка.
Старый client_ms без наблюдений не порождает выдуманные события получения.
Ingress readiness отдельно показывает доступность chat/ASR, server ID, один result
listener и два ограниченных диагностических адаптера на отдельной ASR-шине:
chat.agent.health.agent и nats.llm.user123. Адаптер LLM принимает только явный
agent_step без инструментов и с deadline. Он не является общим прокси чата.
Health RPC Agent/LLM — chat.agent.health.agent / chat.agent.health.llm.
Отсутствующий ответ не считается нулевой очередью. Сбой ASR не выключает обычный чат.
Источник точных полей: browser API, Gateway, Ingress, results, batch.
Сессии Monitor и переписка виджета
- UUID страницы хранится в
window.sessionStorageпод ключомchat-agent.session_id. Переоткрытие панели и reload используют тот же UUID; старыйlocalStorageне переносится. - Через существующую команду
monitor(source=postgres) доступныaction=sessions,requests,request_events,service_operationsиoperation_events.selected_session_idвыбирает сессию журнала и не заменяет авторизованныйsession_idклиента.nullвыбирает события без сессии. Список отбирает сессии с активностью за окно; запросы читаются с начала выбранной сессии до фиксированногоuntil. Сводки возвращаются страницами до 100 элементов (UI — по 25), новые сверху; доступныsearch(текст или точный ID),stateиinput=text|audio|unknown. Неизвестный итог обозначаетсяunknown, отсутствующая длительность —null.request_eventsтребуетrequest_id,operation_events—operation_id; события читаются только после открытия выбранной цепочки, порциями до 100 и 192 KiB. В списках и цепочке нет JSON-тел.event_contentчитает их частями и разрешаетpayload_refвне текущей страницы, сохраняя границы сессии и явного request_id. Владение событием определяется сохранённым request_id, однозначным operation_id или причинным родителем, без группировки по времени. Непривязанные наблюдения доступны в служебных операциях. Устаревшиеexchanges,session_eventsиexchange_eventsудалены; используютсяrequests/request_events/operation_events. - Monitor: в сессии «События / Аудиопотоки», в запросе «Данные / Диагностика / Последовательность событий».
view=eventsоткрывает список;request_idилиoperation_id— отдельную цепочку,request_tab=lanes|events— её представление. Старыйview=events&request_id=…открывает события запроса. Ссылки между Audio и событиями сохраняют request_id. - При явном чтении Monitor проверяет действующее подключение и срок grant; потерянный доступ восстанавливается через защищённый bootstrap с тем же UUID страницы. Timeout не вызывает повтор команды. Фонового опроса данных нет.
- Журнал сохраняет структуру переданного JSON, включая каталог, кэш и маршрут;
credentials удаляются только из полей протокола.
payload_refповторно использует тело только при совпадении переданных байтов. Метаданныеpayload_scope=messageотличают новые записи от прежней проекции, которая могла удалять поля. Полные пакеты старых событий не восстанавливаются искусственно. chat.ui.<client_id>.cmd.widget_history— служебное чтение переписки профилями расширения и веб-виджета. Gateway берёт UUID из проверенного grant, не из параметров запроса.action=listвозвращает до 25 пар сnextи фиксированнымuntil;action=content,request_id,role=user|assistant,offsetдочитывают большое значение. Ответы используют существующиеcell/fragment, без незаметного обрезания.- Необязательный
include_asr_outcomes: trueвaction=listвключает завершённыеasr_resultсresult_type=rejected|errorиз того же журнала. По умолчанию false: старые клиенты получают прежние пары. Карточка содержитrequest_id,created_at,asr(session_id, result_type, code и сохранённые stream_id/phrase_id/revision),question=null,answer=null. Хронология и пагинация общие, повторные события одного request_id не создают карточки. Это только представление истории; оно не вызывает модель и инструменты. Offscreen передаёт тот же отказ соседним панелям через существующийhistory_viewбез текста, PCM и аргументов инструментов. - Переписка — проекция исходных вопросов и терминальных ответов
nats_log, а не история модели. Чтение не повторяет операции и не журналирует собственный результат. Потерянные диагностические события не восстанавливаются искусственно. - User-Agent bootstrap хранится в
browser_access.user_agentи в метаданных событий Gateway; профиль клиента также сохраняется в журнале. Истёкшая привязка доступа не удаляет эти метаданные изnats_log. Старым событиям User-Agent не приписывается.
Аудиодиагностика расширения
Серверный monitor source:audio и три chat.internal.monitor.audio* RPC удалены.
Monitor сохраняет source:postgres. Диагностика использует конечную owner-scoped
команду audio_diagnostics и существующий .events.audio; описание действий,
metadata, admission ACK и исключений журнала — AUDIO_DIAGNOSTICS.
Monitor: путь отдельной фразы
request_events с event_kind=transcription и stream_events читают одну проекцию
nats_log и существующего аудиоархива. Принадлежность определяется сохранёнными
session_id, stream_id, phrase_id и детерминированным request_id; время не
используется для присоединения соседних фраз. Захват и допуск относятся к контексту
потока и не входят в сводку обработки фразы. Пакеты одного диапазона показываются
одной записью со ссылкой на существующие audio_chunks.packets/manifest;
подробности читаются лениво, без бинарного звука и второй копии метаданных.
Bridge и worker отправляют диагностические конверты type=monitor_observation
через существующий ответный ASR-канал. Конверт содержит исходные ID, автора,
время наблюдения, назначение операции и причинного родителя. Он не является
результатом ASR и не запускает команду. Audio Ingress проверяет идентичность и передаёт
наблюдение в существующий ограниченный регистратор. Потеря наблюдений отмечается
отдельно, без повторного распознавания. Ожидаемая фраза регистрируется локально
в BatchCapture до отправки; фактическая очередь измеряется bridge. Исторический
queued обозначал регистрацию в Core и не является измерением очереди ASR.
Заголовки X-Monitor-Transfer, X-Monitor-Source и X-Monitor-Subject связывают
отправку с приёмом, включая account remap GPU. Клиентская квитанция архива может
содержать parent_event_id, received_at и subject конкретной отправки Gateway.
Проекция принимает эту связь только в той же сессии и запросе. displayed_at
фиксируется сразу после добавления текста, до вычисления SHA-256. Разные часы
сервисов сохраняются как есть; разность их timestamps не является измерением сети.
Старые записи без этих наблюдений остаются неполными, без предполагаемых стрелок.
Monitor: уведомления о сохранении
Существующий общий subject chat.monitor.audio_live.streams передаёт также
{version:1, kind:"monitor_audio", event:"changed", data:{session_id, request_id?, recording_id?}}.
Gateway публикует его после commit запроса, ASR или архива. Текст, JSON событий и PCM
в уведомление не включаются. Monitor объединяет изменения за 250 мс, выполняет
одну сверку одновременно и хранит только признак повторной сверки. После повторной
подписки выполняется одна полная сверка видимых данных. Скрытая вкладка и live=0
приостанавливают подписки; старый until ограничивает только снимок на паузе.
Параметр Monitor batch_bytes совместим с прежним чтением: по умолчанию 24000,
максимум 131072 байт PCM. Чтение диапазона возвращает несколько исходных блоков
с проверкой каждого хеша, манифеста, последовательности и непрерывности; next
остаётся индексом следующего исходного блока. Фраза активной записи доступна после
сохранения её блоков и правой секунды контекста. Полный replay требует полной записи.
Если манифест пришёл раньше PCM, вставка блока атомарно переносит его из ожидающих
событий в audio_chunks. Повтор фразы не ждёт завершения всего потока.
finish и finalize идемпотентно сохраняют итоговые счётчики и завершение владельца.
Полная проверка хешей выполняется существующей сверкой архива (до 8 записей каждые
10 секунд), вне двухсекундного RPC записи. На проверку одной записи отведено до
60 секунд; курсор держит не более 32 исходных блоков PCM. До проверки статус — finalizing.
Блокировки строк изолируют записи: завершение одного потока не останавливает запись
соседнего. Поздние блоки и манифесты получают прежние 30 секунд на доставку; конфликт
итоговых счётчиков сохраняется как finish_conflict и автоматически не очищается.
Browser audio protocol 4 (Opus)
Клиентские subjects и соединение сохранены: START принимает audio_protocol=4,
format=opus; protocol 3/PCM отклоняется с audio_upgrade_required. Бинарный пакет и точные
координаты описаны в BROWSER_BATCH_ASR.
Ingress декодирует один раз; внутренний binary batch по-прежнему передаёт PCM16LE
16 кГц. Архив хранит Opus-пакеты, а playback/replay использует общий декодер.
Текущий повтор Monitor читает диапазон на сервере и сразу отправляет batch;
описанная выше историческая загрузка PCM в браузер с возвратом 1× больше не применяется.
Ручная очистка Monitor
chat.ui.<client_id>.cmd.monitor_cleanup доступен только профилю web с действующей авторизацией. Профили extension, widget и replay его не получают. preview возвращает числа, защищённые объекты и подписанную фиксированную выборку; ничего не удаляет. apply требует command_id, selected_session_id, before, snapshot, proof и номер порции step. Повтор номера не запускает новую порцию. cancel сохраняет уже выполненное и останавливает следующие порции.
Удаляются только завершённые запросы и закрытые потоки, не пересекающие границу. Используемый replay, активные операции, каталоги подключённой панели и изменённые после preview строки защищены. Ссылочные тела остающихся событий, квитанции, владельцы, доступы, заметки и прикладные данные сохраняются. Квитанция в существующей таблице фиксирует выборку и прогресс. Тайм-аут откатывает только текущую порцию. Установка версии не запускает очистку. Старые PCM-архивы сохраняют метаданные, но воспроизведение и повтор не поддерживаются.
Термины диагностического ASR
audio_replay с action: terms возвращает фиксированный каталог ASR bridge через
существующий chat.internal.audio_replay_probe. start принимает необязательный
terms: {mode, profile, version, sha256}; off — отсутствие поля. Режимы hotwords
для двух Whisper и LinTO, replacements для GigaAM/Nemotron. Профили — maps,
weather, aviation. Произвольные слова, regex и эталон не принимаются.
Gateway проверяет версию/хеш и capabilities worker, связывает профиль с replay.
Ingress берёт профиль из этой reservation, а не из обычного аудио START.
batch_begin.terms входит в идентичность задания. Bridge проверяет реальный
parameters.hotwords либо применяет точные замены один раз после модели.
diagnostic.text и оценки остаются исходными; text — итог, terms содержит
профиль, applied, список замен и отдельное postprocess_ms. Старые клиенты без
поля работают без подсказок. Режим не меняет голосовой чат и каталог инструментов.
Compare загружает тот же каталог, независимые флажки A/B по умолчанию выключены. Переименование модели снимает её флажок. Эталон применяется только в браузере для WER/CER. Ошибка несовпадения профиля не переключает модель или режим.
Редактор ASR/TTS
chat.ui.<client_id>.cmd.text_editor — диагностика только для web-профиля.
Результаты: существующий .events.model_test; preview: .events.speech.
В существующей команде synthesize появился необязательный prepared: boolean:
true запрещает повторную нормализацию чисел в worker, отсутствие сохраняет
прежнее поведение. Живой ASR final может содержать original_text, text_editor;
квитанция дополнительно поддерживает source_text_sha256.
Проверки владельца и совместимость описаны в VOICE_EDITOR.md.