chat-agent · состояние после C4 · 06.10.2026

chat-agent: от запроса к ответу

БраузерНаш chat-agentvoice / модельОтвет + историяВход → обработка → внешний inference → пользовательский результат

Чат принимает текст или голос, показывает ответ по частям и сохраняет историю. Исполнение ограничено очередями и общим сроком. Распознавание, модель, TURN и OAuth остаются у прежних внешних владельцев.

Код: src_core/main.py · stack/webrtc.drs

chat-agent · состояние после C4 · 06.10.2026

Два стэка связаны общим NATS

chat-agentдиалог + ingressОбщий NATSvoiceASR + файлыPCM в voice; partial/final обратно в единственный Core

Диалогом и аудиоподключениями владеет chat-agent. Voice принимает прежний PCM-контракт и возвращает partial/final. Разделение позволяет менять чат без поставки voice; общий NATS и его доступность остаются общей зависимостью.

Код: stack/webrtc.drs · src_shared/audio.py · src_core/audio_admission.py

chat-agent · состояние после C4 · 06.10.2026

Каждый модуль отвечает за один участок

КаталогОтветственность
src_front / src_coreUI / HTTP, admission, ASR listener
src_agent / src_langgraph_rb_nodeОчередь, история / диалог и сценарии
src_llm / src_api_gatewayМодель / пять tools
src_webrtc_ingress / src_audio_ws_ingressWebRTC / PCM WebSocket

Frontend отображает только свою сессию. Core владеет admission и ASR final, Agent — запросом и историей, Ruby runtime — диалогом и tools. Два ingress содержат медиабиблиотеки; из Core они удалены.

Код: src_core/main.py · src_agent/service.py · src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/dialog.rb

Схемы и контракты · сверка 06.10.2026

Модель вызываем через Ruby, части ответа идут в Agent

Coretext / ASR finalAgentочередь / попыткаRuby + AsyncGraphdialog_step / графLLM adaptermode + contextOllama proxyHTTPS APIRPCRPCRPCHTTPchat.agent.streamJSON delta: LLM → AgentPostgreSQLистория + workflow stateAPI gatewayпять fixture toolsTCP 5432JSON RPCЗапрос к LLM: mode/input/constraints · ответ: response_text + tool_calls либо error

Core передаёт текст или ASR final в Agent. Ruby вызывает LLM и получает проверенный ответ; части текста параллельно идут прямо в Agent через NATS. Обычный разговор требует одной генерации, tools могут потребовать следующих. История хранится в PostgreSQL, текущая попытка - в памяти Agent.

Код: Agent + stream · Ruby RPC · LLM reply / delta · состояние попытки

Схемы и контракты · сверка 06.10.2026

RPC передаёт JSON, NATS доставляет ответ в inbox

NATS subject, request/replyЗапросОтвет в reply inbox
nats.agent.user123AgentInboundRequest: text + envelopeAgentInboundResponse: result / status / errors
nats.workflow.run.ruby.nodeWorkflowRunRequest: text + runtimeWorkflowRunResponse: result + next_runtime
nats.llm.user123LlmRequest: mode / input / constraintsLlmResponse: response_text / tool_calls / error
nats.tools.{name}ToolCallRequest: tool_name / argsToolCallResponse: ok / data / error

Внутренний broker: nats://chat-agent_audio_nats:4222, credentials опущены. Typed payload - UTF-8 JSON с trace_id/correlation_id/request_id/session_id/ts_ms. Ruby получает каталог схем через nats.tools.discover. Управление audio/cancel и фрагменты ответа имеют отдельные компактные схемы.

Код: Agent · Workflow · LLM · Tools

Схемы и контракты · сверка 06.10.2026

LLM передаёт JSON в HTTP proxy

https://nats-proxy-ollama.gis-master.ru/api/chat

Вызов HTTPS POST /api/chatОтправляем JSONПолучаем
dialog_stepmessages + native tools, stream:trueNDJSON: content / tool_calls / done
Структурированные режимыmessages + format:JSON Schema, stream:falseОдин JSON, затем server validation

LLM adapter отправляет model:qwen3:30b, num_ctx:8192, num_predict:2048 и think:false. Текст dialog_step читается по строкам NDJSON; tool calls исполняются только после полной проверки. Внутренний переход proxy → Ollama в этой сверке заново не исследован.

Код: HTTPS / JSON / NDJSON · validation / stream · remote model / endpoint

chat-agent · состояние после C4 · 06.10.2026

Инструменты выбирает модель, проверяет сервер

discoverсхемы пяти toolsМодельnative callsПроверкаимя + аргументыTool → LLMответНет исполнения до полной проверки batch

Ruby получает каталог через nats.tools.discover, проверяет имена и JSON-аргументы до исполнения всего набора вызовов. Затем последовательно вызывает nats.tools.{name}. Пять tools пока возвращают демонстрационные fixtures. Активные и pending-графы сохранены; общий срок 120 s, бюджет до 10 действий.

Код: src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/dialog.rb · src_api_gateway/main.py · src_shared/contracts/tools.py

chat-agent · состояние после C4 · 06.10.2026

Независимые чаты не стоят в одной очереди

УчастокАктивноОжидает
Agent39, максимум 2 на сессию
Ruby runtime33
LLM13
Общий срок120 свключает все очереди

Agent сохраняет порядок внутри сессии и справедливо выбирает независимые чаты. Долгий инструмент не занимает единственного общего обработчика. Модель всё ещё имеет один активный слот: три Agent workers не создают три GPU генерации.

Код: src_shared/execution.py · src_agent/service.py · src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/request_executor.rb · src_llm/utils/base_service.py

Схемы и контракты · сверка 06.10.2026

Два аудиотранспорта сходятся в один PCM-контракт

БраузерWebRTC: основнойWSS: по выборуwebrtc_ingressRTP → decoderresampler + FIFOaudio_ws_ingressPCM16LE, частота браузераresampler + FIFOОбщий NATSheader JSON + PCMmono 16 kHzCorelease + лимит 3 потоковSDP / ICE · JSONDTLS-SRTP · RTPWSS · binary PCM20 ms / 640 PCM bytesТот же wire envelopeCore ↔ ingress: chat.agent.audio.control · JSON admission / heartbeat / end

WebRTC передаёт защищённый RTP; codec согласуется в SDP. WSS передаёт mono PCM16LE с частотой браузера. Каждый ingress отдельно переводит звук в 16 kHz и пакует его для NATS. Общий PCM-модуль - код внутри ingress; Core выдаёт допуск, но не принимает медиапакеты.

Код: WebRTC · WSS · PCM envelope · admission

Схемы и контракты · сверка 06.10.2026

Четыре адреса - четыре разных потока данных

Адрес на chat-agent.gis-master.ruКто принимаетЧто передаётся
HTTPS POST /core/offerWebRTC ingressJSON с SDP offer → SDP answer
WSS /core/ice/{connection_id}WebRTC ingressJSON auth / candidate / end
WSS /audio/wsWSS ingressstart JSON → binary PCM16LE → end JSON
WSS /wsОбщий NATSПротокол NATS; payload событий - JSON

База HTTPS: https://chat-agent.gis-master.ru; для сокетов - wss://chat-agent.gis-master.ru. Все четыре маршрута проходят OAuth. ICE дополнительно проверяет token подключения. /ws возвращает статусы и ответ, /audio/ws передаёт звук. Текст, история и отмена идут в остальные /core/*, которые обслуживает Core.

Код: routes / OAuth · ICE JSON · PCM start/end · NATS over WSS

chat-agent · состояние после C4 · 06.10.2026

Trickle ICE убрал ожидание полного сбора

SDP сразуКандидатыв обе стороныПервая рабочаяICE-параDTLS → RTPдекодированный PCMSTUN/TURN сохранены; legacy offer поддерживается

Браузер и сервер передают SDP и кандидаты по мере появления. Рабочая пара начинает ICE/DTLS до завершения зависшего STUN. После соединения незавершённый сбор не прерывает аудио. Private adapter закреплён на точных версиях и остаётся под нашим сопровождением.

Код: src_webrtc_ingress/main.py · src_webrtc_ingress/ice/trickle.py · tests/test_core_trickle_ice.py

Схемы и контракты · сверка 06.10.2026

PCM передаём в binary envelope

Resampler + FIFOs16le · mono · 16 kHzNATS payloadheader + 640 PCM byteswebrtc.komaroff.asr.stream.{session_id}20 ms на frame; publish сразу4 B: длина JSONuint32 big-endianJSON metadata · UTF-8type / seq / session_id / stream_idsample_rate / channels / sample_widthPCM bytesint16 little-endiantype: frame / end / abort · при end/abort аудиобайтов нет

Один обычный frame содержит 320 samples / 640 PCM bytes за 20 ms. FIFO сохраняет остатки; padding нужен только последнему хвосту, меньше 20 ms. Flush выполняется раз в 250 ms на connection и при end/abort. Очередь ограничена двумя секундами; при переполнении поток завершается с явной ошибкой.

Код: wire / FIFO / flush · voice validation · PCM tests

Схемы и контракты · сверка 06.10.2026

Voice возвращает текст, Core запускает диалог

voice streaming bridgestt_whisper_to_natsVAD + границы фразВнешний GPU ASRfaster-whisper workerPCM → текстto.inference.webrtc.asr.audio.{stream_id}from.inference.webrtc.asr.events.{stream_id}Через общий NATS, account remap и leafnode; на GPU prefix inference.webrtc.asr.*Coreпроверка stream/phraseAgentновый ход диалогаwebrtc.komaroff.asr.text.{session_id} · JSONнепустой finalРезультат: partial / final / rejected / error + session_id, stream_id, phrase_id, revision, text

Ingress публикует звук в webrtc.komaroff.asr.stream.{session_id}. Voice выделяет фразы и отправляет PCM к GPU по NATS; ответ возвращает в Core как JSON. Partial показываем в UI, непустой final запускает Agent один раз. Файловый API voice и диаризация в live-путь не входят.

Код: voice bridge · GPU contract · final → Agent · NATS remap

chat-agent · состояние после C4 · 06.10.2026

Отмена и завершение имеют одного владельца

queued / runningdone либо cancelledevents.metaитог / черновикGET snapshot и DELETE cancel проверяют session; микрофон останавливается отдельно

Agent согласует terminal transition, отменяет downstream и игнорирует поздний результат. Готовый ответ не перезаписывается отменой. Частичный текст сохраняется помеченным черновиком; при отмене редактирования исходная история остаётся.

Код: src_agent/request_state.py · src_agent/service.py · src_agent/utils/db.py

Схемы и контракты · сверка 06.10.2026

Части ответа и окончательный результат идут отдельно

LLMJSON deltaAgentпроверка + snapshotОбщий NATSJSON UI eventsБраузерфильтр сессииstreamUI event/wschat.agent.stream → Agent → nats.events.user123 → NATS protocol over WSS /wsCoreитог HTTP POSTRPC replyHTTP JSON

Agent публикует response_fragment, request_state, итог message и команды client. Core публикует asr_result и audio_status в тот же nats.events.user123. Frontend проверяет session/request/attempt/generation/sequence до показа текста и команд карты. После reconnect берёт snapshot или историю; replay UI-событий здесь нет.

Код: fragments / snapshot · итог / команды · ASR / audio status · фильтр UI

chat-agent · состояние после C4 · 06.10.2026

Телеметрия связывает ожидание и результат

Клиент / CoreAgent → Ruby → LLMCollectorTempo / VMUUID только в spans; без per-frame spans и UUID labels

traceparent проходит NATS headers через наши сервисы. Tempo хранит этапы и UUID attributes, VictoriaMetrics — числовые длительности и очереди. Тексты, аудио, SDP и credentials не экспортируются; клиентские длительности помечены как client_reported.

Код: src_shared/observability.py · src_core/handlers/handle_client_timings.py · src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/telemetry.rb

chat-agent · состояние после C4 · 06.10.2026

Переименование меняет владельца сервисов

Образы + drain15 старых → 0VIP удаленыBackendFrontend = 0Frontend = 1Штатный exact-SHA CI; без stack rm и удаления томов

Поставка e9122e0 / CI 4930 подготовила образы и дождалась завершения работ. Точные 15 старых services удалены; 13 новых проверены до включения Frontend. Физические тома и shared network сохранены. По решению владельца перенос старой истории и backup/restore не входят в приёмку; схему БД можно менять.

Код: stack/release.rb · stack/runtime_probe.py · docker/mirror-postgres.rb · stack/webrtc.drs

Было → исправлено

Основные ошибки были в управлении работой

ПроблемаИзменение
Router генерировал отдельно от ответаdialog_step: текст либо tools за одну генерацию
Agent последовательно ждал независимые чатыТри исполнителя, очередь по сессиям, общий срок
UI принимал ответы и статусы соседейПроверка session/request/generation до отображения
Каждый PCM-пакет вызывал flushFIFO, publish сразу, flush раз в 250 мс

Исправления убрали лишнее ожидание и сделали границы работы явными. Один долгий запрос больше не занимает единственный Agent callback. Общий NATS account сохранён: фильтр UI решает смешивание сообщений, но отдельные права каждого пользователя ещё нужны.

Код: dialog.rb · execution.py · requestState.ts · audio.py

Замеры C3 · 06.10.2026

Диалог ускорился, распознавание сохранило свою задержку

C0 → C3, секундыp50p95
Обычный текст, n=1214,97 → 3,1120,95 → 3,98
ASR final → ответ46,01 → 14,1089,40 → 23,23
RTC connected, n=121,39 → 1,631,56 → 1,88
Начало WAV → ASR final3,80 → 3,823,99 → 4,02

В той же контрольной серии C3 завершил 33/33 хода с 36 генерациями. Обычный ответ ускорился благодаря одной генерации и новым очередям. Trickle был поставлен раньше; подключение здесь немного медленнее C0, но ниже цели 3 с. ASR p95 после конца WAV вырос на 2,8%. Короткая синтетическая речь иногда искажается; WER/DER не измерены.

Evidence: приёмка C3 · browser measurements · cases.json

Исправление C4 · Core и два аудио-ingress

Healthcheck больше не запускает Python

Было · каждые 10 сDockerЗапуск Python → HTTP/healthz + готовность NATSСтало · каждые 10 сDockercurl → полный HTTP 200Тот же /healthz и NATS503, redirect, отказ соединения и зависший ответ → ошибка проверки

Каждые 10 секунд Docker проверяет Core и оба ingress. Для одного локального HTTP-запроса раньше запускался Python; теперь работает curl. Полный HTTP 200 и готовность NATS остались обязательными. В 90 пробах реальных образов CPU одной проверки был ниже 29 мс; это снизило фоновую нагрузку всего стэка.

Код: Core Dockerfile · ingress Dockerfile · проверки отказов · результаты C4

Итог после C4 · весь стэк · 06.10.2026

После healthcheck CPU в простое снизился на 75%

Показатель и база сравненияДо → после
CPU в простое, перед → после C40,197 → 0,049 ядра, -75,0%
Память в простое, перед → после C4547,57 → 553,41 МБ, +1,1%
Сумма rootfs образов, C0 → C43 685,21 → 3 256,25 МБ, -11,6%
Активный production-код, C0 → C414 762 → 15 890 строк, +7,6%

CPU и память сравнили в одинаковых окнах простоя по 180 с: 13 сервисов, без активных запросов. RAM не сократилась. Сама пересборка healthcheck добавила 4,27 МБ rootfs относительно C3. Rootfs повторно учитывает общие слои, без томов и внешних моделей; свежий compressed-размер C4 не подтверждён. Полная серия диалога относится к C3, после C4 прошли короткие проверки текста, RTC и WSS.

Замеры: метод и результат C4 · CPU / RAM / runtime · подсчёт production-кода

1 / 17

Служебные вложения доступны в исходном архиве.