12 KiB
12 KiB
agentik-desktop — десктопный клиент агента agentik
Клиент агента agentik на Kotlin Multiplatform + Compose Desktop (цель сборки —
только JVM). UI общается с агентом через готовую библиотеку pw.binom.agentik:client:
она даёт Agent / Conversation, живой поток событий (SSE), авто-reconnect,
кэш списка диалогов и interrupt() для кнопки «Стоп».
Проект уже собирается и запускается (./gradlew run), а не только спроектирован.
Что где лежит
sketches/index.html # ВСЕ экраны одним документом: список диалогов, чат, запись,
# группы, агенты, проверка связи, пустые состояния.
# Это и черновик, и «одобренное» — отдельного approved/ больше нет.
sketches/style.css # палитра и стили макетов (цвета числами — так и надо, это макет)
src/jvmMain/kotlin/... # код приложения
src/jvmTest/kotlin/... # тесты
Документы рядом:
REQUIREMENTS.md— требования (накиданы, ни один пункт не обязателен в текущей форме).STORAGE.md— что и где хранится на клиенте (SQLite + JSON).CACHE.md— кэш сообщений: что есть в библиотеке, схема «только новое».THEMES.md— цвета не пишем в коде, только из семантической схемы.MARKDOWN-SOURCE.md— откуда взята отрисовка Markdown.BORROW-FROM-ASSISTENT.md— что берём изai/assistent, а что нет.MIC-ASR-SEARCH.md— библиотеки микрофона/распознавания (mic-kmp,asr-kmp,vad-kmp).
Структура кода
pw.binom.agentik.desktop
├── Main.kt # точка входа: окно, реестр, состояние
├── settings/ # AppPaths, AppSettings/AgentConfig, SettingsRepository
├── persistence/ # ConversationMetaRepository (группы/lastSeen/preview/watermark)
├── session/ # LiveTurn, ChatSession, AgentConnection, AgentRegistry
├── model/ # UiModels + Mappers (журнал/стрим → UI)
├── media/ # AvatarStorage (копия картинки агента)
├── net/ # ConnectionChecker (проверка связи)
├── markdown/ # вендоренный парсер Markdown (из assistent)
└── ui/ # App, AppState, theme/, components/, screens/
Ключевые решения по слоям — в STORAGE.md; кратко:
- Транспорт целиком в
:client. HTTP/JSON/SSE/Bearer, reconnect, кэш списка диалогов — не переписываем. - Сообщения —
KsqliteJournalStoreиз:journal-ksqlite(offline-история). - Группы,
lastSeen, превью, watermark синхронизации — нашConversationMetaRepository(сервер про это не знает). - Отдельная SQLite-база на агента: id диалогов серверные, у двух агентов могут совпасть; плюс группы локальны для агента.
Текущее состояние реализации
Готово и покрыто тестами:
- Настройки —
settings.json(агенты, тема, выбранная группа, последний диалог),clientIdгенерируется один раз. Атомарная запись. - Живой список диалогов —
AgentConnection.conversationsFlowопрашивает встроенный кэш либы (conversationStore.listFlow, warmup-цикл на старте) и дополнительно подписан на SSE-поток агента: диалог, созданный с другого клиента (или самим сервером), появляется в списке сам; свои создания и удаления отражаются сразу. Ошибки сервера не считаются «пустым списком» — последний известный список сохраняется. - Реестр агентов — добавление/правка/удаление, подключение, деградация в «не отвечает» без падения приложения.
- Стриминг —
LiveTurnприменяет событияWorking → StartReasoning → AppendText → ToolCall/ToolResult → End/Interrupted/Error;ChatSessionдогоняет историю (after = synced_at), пишет в локальный кэш, склеивает live + history, обновляет превью иlastSeen. - История/UI-модели — мапперы
MessageRecord → UiMessageсо склейкойToolCall+ToolResultв одну карточку. - Группы — CRUD групп + раскладка диалогов (каскад: удаление группы не удаляет диалоги).
- Тема — семантические цвета (
AgentikTheme.colors), тёмная + светлая схемы; отдельно пользовательские цвета агентов. - Markdown — вендоренный рендер из
ai/assistent(см.MARKDOWN-SOURCE.md). - Аватары — копирование выбранного файла в
~/.agentik-desktop/avatars/. - Проверка связи — «ответ сервера» + создание/удаление временного диалога, с таймингами.
- UI (Compose) — рейка агентов, список диалогов (поиск, папки, бейджи),
чат с живым стримингом, композер, окна «Агенты», «Группы», «Новый диалог»
(при нескольких агентах — выбор, при одном — сразу) и «Параметры диалога»
(кнопка
⋯в шапке чата: имя, обслуживающий агент, признак «Временный», даты создания/обновления, удаление), пустые состояния, адаптивная раскладка (узкое окно — список или чат). - Голос — микрофон → VAD → распознавание (
voice/): кнопка микрофона илиCtrl+Mиз чата запускает запись (mic-api), Silero VAD (vad-kmp) отмечает наличие речи, Vosk (asr-vosk, модель внутри артефакта) отдаёт промежуточный текст над полем ввода, финал дописывается в черновик. Тишина текстом не становится. Нативные модели считаются на одном выделенном потоке. На стартеmain()делает [VoiceNative.warmUp()] — обязательный прогрев Vosk/ONNX ДО инициализации skia/Compose (иначе создание модели после отрисовки окна роняет JVM в C++-логах; подробности вMIC-ASR-SEARCH.md). - Реальная интеграция — интеграционные тесты гоняются против живого
:standalone-агента (Qwen через vLLM), а не только in-memory-фейков:RealServerIT(диалог → отправка → живой ответ LLM → локальный кэш) иRealAppStateIT(весьAppState: реестр → подключение → живой список → создание диалога → стриминг → превью/lastSeen),RealSpeechIT(речь: прогрев нативных ASR → инициализация Compose → распознавание реального wav → «раз два три четыре пять»). См.AGENTIK_E2E_URL.
Проверено вживую (окно приложения + живой агент): список диалогов подтягивается
с сервера, кнопка «+» создаёт диалог, сообщение уходит и ответ приходит в UI,
Ctrl+M переводит микрофон в запись и обратно без падения JVM.
Не начато / заглушки:
- Свёрнутый вид хода работы агента — сейчас живой ход показывается развёрнуто (reasoning, карточки ToolCall/ToolResult, спиннер). Как его складывать — вопрос дизайна, не решён (REQUIREMENTS §9).
- Кусок прерванного ответа в кэше — R39 выполнен (прерванный ответ в историю не пишется), а вот хранить ли его обрывок в кэше — вопрос не решён (REQUIREMENTS §9).
- Светлая тема визуально не проверялась глазами (
THEMES.md); вui/ThemeRenderTest.ktпроверено программно: фон код-блоков берётся из слота темы, порядок яркостей в светлой палитре корректный. - Голос: движок зашит на Vosk (
VoskVoiceRecognizer) — Qwen3/Whisper изasr-kmpподключаются заменойVoiceRecognizerFactory; выбор движка/модели в настройках не сделан.
Как запустить
./gradlew run # окно приложения
./gradlew jvmTest # тесты (реальная SQLite, фейковый транспорт)
# Интеграционные тесты против живого агента (по умолчанию пропускаются):
AGENTIK_E2E_URL=http://127.0.0.1:8080/agentik ./gradlew jvmTest \
--tests '*RealServerIT' --tests '*RealAppStateIT'
./gradlew jvmJar # собрать jar
Что решено
- Сначала десктоп, затем Android — по тем же лекалам. Десктоп делаем образцом: понятный и читаемый по коду. Потом Android повторяет решения.
- Несколько агентов — различение цветом или картинкой (выбирает пользователь). Картинку клиент копирует себе, а не берёт по адресу.
- Markdown — показывать разметкой. Готовый рендер взят из
ai/assistent. - Папки над списком диалогов — сохранены.
- Проверка связи — отдельным окном, с таймингами шагов.
- Ориентир по размеру: ~9 небольших кусков логики поверх библиотеки, а не
собственная инфраструктура (см.
STORAGE.md,CACHE.md).
Что предстоит решить
- Запись — по нажатию или на удержание.
- Форма показа хода работы агента в свёрнутом виде.
- Различение агентов: одного цвета может оказаться мало.
- Что делать с куском прерванного ответа в кэше (
CACHE.md, п. 3.2). - Где показывать несколько агентов в одном списке диалогов (сейчас список — у активного агента).