Commit Graph

111 Commits

Author SHA1 Message Date
SubochevAV 098c97c7bd ci: fix forgejo-release action URL — use code.forgejo.org/actions/forgejo-release@v1
release / Build runnable fatjars (release) Failing after 2m8s
ci / JVM build + tests (push) Failing after 1m27s
release / Publish KMP libraries → caffeine Nexus (release) Failing after 1m17s
2026-09-16 21:49:51 +03:00
SubochevAV 4b8e5bb0bd ci: fix release.yml — use GITHUB_REF_NAME for tag version
ci / JVM build + tests (push) Failing after 32s
release / Publish KMP libraries → caffeine Nexus (release) Successful in 4m48s
release / Build runnable fatjars (release) Failing after 4s
Forgejo (Gitea Actions engine) exposes env vars under GITHUB_-prefix,
not GITEA_-prefix. ${GITEA_REF_NAME} was empty → maven-publish
failed with "Invalid publication 'iosArm64': version cannot be empty".

Use $GITHUB_REF_NAME inside bash (with explicit TAG= assignment for
debug echo).
2026-09-16 21:41:24 +03:00
subochev d75289ac56 merge: per-module READMEs + root navigation + CI/CD fixes
ci / JVM build + tests (push) Failing after 31s
release / Publish KMP libraries → caffeine Nexus (release) Failing after 18s
release / Build runnable fatjars (release) Failing after 3s
2026-09-16 20:47:10 +03:00
SubochevAV 05f7b8fd04 ci: fix fatjar file paths in ci.yml + release.yml
ci / JVM build + tests (pull_request) Failing after 32s
shadowJar produces `standalone-0.1.0-all.jar` (archiveBaseName +
classifier.all), not `standalone-all.jar`. Use `*-all.jar` glob
patterns in upload + attach steps; works regardless of version.

🤖 Generated with [opencode]
2026-09-16 20:46:54 +03:00
SubochevAV 8f616f359f docs: per-module READMEs (run vs library) + root navigation hub
ci / JVM build + tests (pull_request) Failing after 54s
Every subproject now has README.md:
- 3 runnable modules (:standalone, :agentik-cli, :agentik-tui):
  quickstart, env table, parameters, known limits
- 11 library modules: what it is, which problem solves, how to
  wire it in, where versions live

Root README.md is the navigation hub (Quickstart, Modules table,
publish + CI/CD notes).

Also: ci.yml prunes the :memory-vector -x excludes now that
text-embedding-kmp artifacts are published to caffeine.

518 tests green.

Verified publish pipeline: :proto:publish to caffeine produces
pom.module + per-target klibs + sources for all 9 KMP targets.

🤖 Generated with [opencode]
2026-09-16 20:44:16 +03:00
subochev 5ad972767d ci: trigger CI workflow to verify CICD setup end-to-end
ci / JVM build + tests (pull_request) Failing after 18s
2026-09-16 20:14:29 +03:00
subochev 0fdc12695e docs: per-module README + root navigation hub + CI/release workflows
ci / JVM build + tests (push) Failing after 1m57s
- README.md в каждом подмодуле: для библиотек — описание проблемы,
  подключение через maven-central/caffeine, версии в gradle/libs.versions.toml.
  Для запускаемых модулей — команды запуска + переменные среды с дефолтами.
- Корневой README.md переписан как навигационный хаб: что это, где клиенты,
  где серверы, как собрать, как опубликовать.
- build.gradle.kts: per-module POM-description через единую карту в rootProject.extra
  (порядок важен — нужно ДО apply плагина KMP, поэтому beforeEvaluate в subprojects).
- .gitea/workflows/ci.yml (новый): build + jvmTest + shadowJar на PR/push main.
- .gitea/workflows/release.yml (обновлён): публикует библиотеки в caffeine
  Nexus + собирает 3 fatjar'а и крепит их к release как бинарные ассеты.
2026-09-16 16:24:03 +03:00
Agent e68db11aaa feat(agentik-tui): TUI client v2 на Mosaic — без vim-клавиш, только Tab/Enter/Esc/Ctrl-D/F1/стрелки
Вместо v1 (history-список + slash-команды) делаем сразу v2:
- header (id/conv/focus), history, input, footer
- переключение фокуса Tab/Shift-Tab (history/input/sidebar)
- ↑↓ scrollback, ←→ курсор в input
- Enter submit (User-сообщение в history), Backspace/Del удаление, Esc clear
- Ctrl-D/Ctrl-C выход (заглушка — пишет в history, реальный exit добавим)
- F1 toggle help-оверлея

Архитектурно: используем StateFlow+collectAsState вместо mutableStateOf, потому что
в Mosaic 0.18 recompose от mutableStateOf-writes из key-handler не триггерится
автоматически (требует ручного Snapshot.apply). StateFlow через collectAsState
работает out-of-the-box (см. samples/snake в репо Mosaic).

Compose Compiler plugin (org.jetbrains.kotlin.plugin.compose) обязателен —
без него @Composable-лямбды компилятся в Function0 вместо Function2 и
runMosaicBlocking не находит сигнатуру.

Цели сборки: jvm + macosX64/Arm64 + linuxX64/Arm64 + mingwX64 (iOS не нужен).
Бэкенд (:client, ktor-cio) — jvmMain only пока, nativeMain заглушка.

Tests: 370/370 green.
2026-09-16 14:41:05 +03:00
subochev b0bbc57880 feat(agentik-cli): REPL-клиент на базе :client для всех KMP-целей
Новый KMP-модуль :agentik-cli — REPL поверх HTTP-фасада :server.

commonMain (~900 строк):
- Main.kt — точка входа + парсинг --server/--id/--no-history/--help
- CliPlatform.kt — expect-фабрика Agent + CliTerminal + SessionIo + env()
- SlashCommand.kt — 11 slash-команд: /help /new /list /sw /rename /rm
  /interrupt /history /pwd /exit /quit
- AgentikCli.kt (~350 строк) — главный REPL-цикл: readLine → parse →
  send → render events. Сохраняет lastEventAt в ~/.agentik/cli-state.json.
- EventRenderer.kt — печатает SSE-события (StartResponse/AppendText/AppendImage/
  End/Interrupted/Error) с правильным разделением text/image и переводом
  строки на end.
- SessionRepository.kt — JSON-state (conversationId, lastEventAt) +
  IO-интерфейс SessionIo.

jvmMain — JLine-терминал (LineReader+history, стрелки, Ctrl-D/E, автосейв
истории), java.io-based atomic-IO для state-файла, real System.getenv.

nativeMain — stub actuals (kotlin.Result-error с подсказкой куда копать):
подключение native ktor-движков (darwin/curl/okhttp) и termios через
kotlinx.cinterop — отдельная задача. Все 8 KMP-целей (jvm/macosX64/macosArm64/
iosX64/iosArm64/iosSimulatorArm64/linuxX64/linuxArm64/mingwX64) компилируются.

shadowJar собирает self-contained fatjar (~8.5 MB). 23 unit-теста зелёные.

Заодно фикс бага в :client — InstantSerializer.descriptor имел имя
'kotlin.time.Instant', которое kotlinx-serialization 1.6+ резервирует за
встроенным сериализатором, из-за чего client падал на старте с
'there already exists InstantSerializer'. Переименовано в
'pw.binom.agentik.Instant' — зеркально с :server.

Smoke-тест на 192.168.76.166: /new + 'привет'/'2+2' отвечает корректно,
state-файл создаётся в /root/.agentik/cli-state.json, /list возвращает
125 диалогов.
2026-09-16 13:42:12 +03:00
subochev 408caee261 feat(standalone): agentik pull-model subcommand + AGENTIK_AUTO_DOWNLOAD_MODEL=1 trigger for LiteRT-LM
Добавляет ModelDownloader (HTTP с Range/докачкой, опциональной SHA-256 проверкой)
и два сценария запуска скачивания встроенной модели gemma-4-E2B-it.litertlm:

  java -jar agentik.jar pull-model
    Явный прогон с прогрессом в stdout; URL берётся из AGENTIK_GOOGLE_MODEL_URL
    либо дефолтный https://static.binom.pw/models/gemma-4-E2B-it.litertlm.

  AGENTIK_AUTO_DOWNLOAD_MODEL=1 java -jar agentik.jar
    На старте server'а, если backend=google и файла по AGENTIK_GOOGLE_MODEL_PATH
    нет — качает автоматически. Без флага — exit 2 с понятным сообщением и
    подсказкой вызвать pull-model.

Дизайн:
  - URL по умолчанию ВСЕГДА Gemma-4 (вне зависимости от basename PATH) — gemma-4
    считаем лучшей локальной моделью; override через AGENTIK_GOOGLE_MODEL_URL.
  - SHA-256 проверка через опциональный AGENTIK_GOOGLE_MODEL_SHA256_URL.
  - Resume: HEAD → если есть .part и Accept-Ranges=bytes → GET с Range: bytes=N-,
    иначе restart с нуля.
  - Прогресс каждые ~8 MB, финальный rename через Files.move(ATOMIC_MOVE).

Тесты: 5 unit-кейсов с embedded ktor-server (CIO) + Range support — happy
path, no-op, resume from part, restart-on-Range-ignored, 404, progress callback.

Документация: новый раздел §18 в MANUAL-TESTS.md (subcommand, auto-trigger,
resume, override URL, SHA-256 verify).

178/178 tests green.
2026-09-16 12:55:43 +03:00
subochev 86eb0632e0 fix interrupt: race in flag reset + no-op when no active turn + bounded background scope
Три фикса в runTurn/interrupt:

1. **Race condition в finally-блоке.** Раньше сбрасывал
   interrupted.set(false) только если флаг был установлен при чтении
   wasInterrupted в начале finally. Если interrupt() приходил между
   этими двумя точками — флаг оставался true и следующий turn видел
   wasInterruptedAtEntry=true → сразу short-circuit'ил без вызова LLM.
   Теперь всегда сбрасываем (compareAndSet атомарен, гарантирует
   следующий turn чистый).

2. **interrupt() отравлял следующий send.** Если вызывали interrupt()
   в пустоту (нет активного turn'а — флаг всё равно ставился → следующий
   send сразу short-circuit'ил, пользователь не получал ответа на
   своё 'Ок.' после явного cancel). Теперь interrupt() проверяет
   activeTurn?.isActive и при отсутствии активного turn'а — no-op.

3. **Bounded background scope для review/reflection/skill-mining.**
   OpenAiLlm.send() использует runBlocking — если запустить 30+
   параллельных review (по одному на беседу), IO-thread pool
   голодает и ассистент висит. Вынес в отдельный scope с
   Dispatchers.IO.limitedParallelism(4) — не больше 4 sync LLM
   вызовов одновременно.

Тест 27/27 (см. /tmp/test-interrupt.py и /tmp/run-manual-tests.py).
2026-09-16 07:46:30 +03:00
subochev c42a6027a4 docs: add MANUAL-TESTS.md — manual test cases for running agentik 2026-09-16 05:49:16 +03:00
subochev b1ae8bbd20 fix(agent): trigger post-tool continuation sendStreamContents for stateless OpenAI backend
После addToolResult (например memory_save result) LiteRT-LM (stateful)
возвращает дельту с финальным текстом модели. Но OpenAI-бэкенд
(stateless, litert-openai) просто дописывает tool-result в history и
возвращает пустую дельту — следующий ответ модели приходит только
при следующем send.

Без этого фикса ассистент после tool-call'а выдавал пустой текст
"\n\n" (например после memory_save).

Что меняется:
- runTurn: после addToolResult вызываем sendStreamContents с пустым
  placeholder'ом (" "), который для OpenAI триггерит continuation,
  а для LiteRT-LM просто даёт no-op-ответ (соберём, отбросим).
- tool_calls из continuation НЕ обрабатываем в текущем inner-while —
  кладём в pendingPostToolCalls и обрабатываем на следующей outer
  итерации. Иначе можно попасть в бесконечный tool-loop (fake
  LiteLlm-тесты это показывают).
- emptyList() нельзя — LiteMessage требует непустой contents, поэтому
  используем пробел как placeholder.

Тесты:
- tool-call loop test: toolCallCount == 2 (user send + post-tool continuation)
- live e2e на удалённой машине (192.168.76.166) с OpenAI vLLM бэкендом:
  - простая арифметика (12+34=46) ✓
  - memory_save + recall в той же беседе ✓
  - memory persists across conversations ✓
  - прерывание mid-task (генерация рассказа про космос) → partial assistant
    + ToolExchange в working memory ✓
  - SSE events: start_reasoning, start_response, append_text, end ✓

Total: 341/341 green.
2026-09-16 05:44:58 +03:00
subochev e3f20f07d9 feat(agent): persist tool-calls in working memory + interrupt-safe close-recreate
Radical redesign of interrupt semantics (plan: docs/TOOLSETS-PLAN.md,
phase commit 7):

1. Storage (:storage-core + :storage-sqlite + :storage-inmemory):
   add WorkingMemoryEntry.ToolExchange(toolName, toolArgsJson, resultText,
   wasCancelled) — one row per tool-call. Survives restarts.

2. ChatConversation:
   - new fields: interrupted (AtomicBoolean), currentToolJob (Job?)
   - interrupt() теперь только сигнал: ставит флаг, cancel LiteConv +
     cancel currentToolJob. НЕ cancel activeTurn — пусть runTurn finally
     отработает.
   - runTurn обёрнут в try/finally: даже при CancellationException (от
     LiteConv.cancel()) и при early-return (interrupt до старта LLM) —
     finally закрывает LiteConv и эмитит Interrupted (если была отмена) + End.
   - runToolAndPersist возвращает WorkingMemoryEntry.ToolExchange вместо
     Pair(callId, resultText); инструмент запускается в scope.async, его
     Job = currentToolJob, cooperative cancellation через Job.cancel.
     Если инструмент броает CancellationException/InterruptedException →
     resultText = '[cancelled by user]', wasCancelled = true.

3. GetOrCreateLiteConversation теперь мапит ToolExchange →
   LiteMessage(TOOL, ToolResult, name, response) в initialMessages —
   при следующем send() LLM видит честный результат вызова tool'а
   через LiteRT-LM (callId не требуется, матчится по name).

4. LiteConv lifecycle: создаётся новый на каждом turn (close+recreate
   семантика). Это ~2s prefill на Gemma-4-E2B, но гарантирует полную
   предсказуемость: нет рекурсивных cancel-drain'ов, KV-cache всегда
   консистентен с WM.

5. Тесты:
   - multi-turn: 2 LiteConv-а (один на turn)
   - interrupt mid-slow-stream: пустой assistant в WM, только user, события
     Interrupted + End.
   - interrupt after-tool: ToolExchange в WM (result=echo output, wasCancelled=false),
     ToolCall + ToolResult в audit.

Total: 341/341 green.
2026-09-16 05:03:36 +03:00
subochev 9fcb2da75d Remove system prompt persistence from working_memory
System prompt is now built fresh at conversation create/load time
(in `buildSystemPrompt` capturing current SOUL/skills/toolsets/reflections)
and passed into LiteConversationConfig.systemInstruction. It is NOT
written to working_memory anymore.

Why: ChatAgent was freezing the system prompt into a WorkingMemoryEntry.System
row at createConversation, then reading it back on every getOrCreateLiteConversation.
This meant changing SOUL, activating toolsets, adding skills or new
reflections between agent restarts did not propagate to existing conversations
without re-running createConversation.

Fix:
- ChatAgent.createConversation: dropped the workingMemoryStore.append(System(...))
- ChatConversation.getOrCreateLiteConversation: replaces the WM-based lookup with
  the in-memory systemPrompt field directly
- ChatConversation.compactPreTurn: same simplification — compaction operates only
  on User/Assistant rows (plus future Summary rows); system prompt is excluded

Migration: none. Old DBs may contain dead System rows from prior versions — they
are simply ignored by the new lookup, and compaction never reads them.

Tests: 340/340 green. Updated 7 tests across ChatAgentTest + MemoryWiringTest
that asserted the old System-in-working-memory contract; they now verify the
system prompt via LiteConversationConfig.systemInstruction (what LLM actually sees).

E2E verified: 0 system rows in working_memory across all conversations,
multi-turn history reconstructs correctly after agent restart with the updated
in-memory system prompt.
2026-09-16 02:39:19 +03:00
subochev 88af57182f ci: убираю build-standalone job из release workflow
standalone fatjar собирается локально через ./gradlew :standalone:shadowJar —
CI-прикрепление к релизу через softprops/action-gh-release оказалось лишней
обвязкой и сильно усложнило отладку публикации KMP-библиотек в Nexus
(та упорно падала с 'Invalid publication kotlinMultiplatform: version cannot
be empty' на разных subprojects при каждом фиксе). Теперь release.yml делает
ровно одну вещь: ./gradlew publish → caffeine Nexus.

debug-println в publications.configureEach тоже убран — он свою задачу
выполнил (показал что configureEach срабатывает с правильной version,
но KMP-plugin всё равно создаёт публикацию с пустой version в CI).
2026-09-15 23:31:07 +03:00
subochev b2d5684192 build: debug println в publications.configureEach
release / Publish KMP libraries → caffeine Nexus (release) Failing after 23s
release / Build standalone fatjar (release) Has been skipped
2026-09-15 23:29:14 +03:00
subochev 31c4b1cfc4 ci: debug-логирование для diagnosis CI version=empty issue
release / Publish KMP libraries → caffeine Nexus (release) Failing after 20s
release / Build standalone fatjar (release) Has been skipped
2026-09-15 23:24:22 +03:00
subochev 202d379f5a build: явно выставляем version/groupId в каждой MavenPublication
release / Publish KMP libraries → caffeine Nexus (release) Failing after 20s
release / Build standalone fatjar (release) Has been skipped
beforeEvaluate { version = ... } не помог — KMP-плагин всё равно фиксирует
publication 'kotlinMultiplatform' с пустой version до того, как это присваивание
срабатывает. В CI порядок обработки модулей отличается от локального (там
Gradle Daemon прогревает метаданные): сперва падало на :standalone, на
следующем ране — на :memory-vector.

Фикс: publications.withType<MavenPublication>().configureEach { groupId =
..., version = rootProject.extra['projectVersion'] } — это гарантирует,
что у КАЖДОЙ публикации (включая 'kotlinMultiplatform' для JVM-only KMP
модулей вроде :memory-vector, :storage-sqlite, :standalone) group/artifact/
version выставлены явно, а не взяты из project.version (которое может быть
не инициализировано в момент создания publication).
2026-09-15 23:18:55 +03:00
subochev a3f82f875d build: projectVersion выставляется через beforeEvaluate (ловит KMP)
release / Publish KMP libraries → caffeine Nexus (release) Failing after 18s
release / Build standalone fatjar (release) Has been skipped
В предыдущей версии subprojects { version = ... } выставляло version
ПОСЛЕ того как KMP-плагин создал publications. Для большинства модулей
это работало (Gradle пересчитывал version в публикации lazy), но для
:standalone плагин shadow + ленивая KMP-инициализация приводили к тому,
что publication 'kotlinMultiplatform' всё-таки создавалась с пустой
version → 'InvalidMavenPublicationException: version cannot be empty'
ТОЛЬКО в CI (локально работало — потому что Gradle Daemon прогревал
метаданные и не доходил до этого пути).

Фикс: subprojects.beforeEvaluate { version = rootProject.extra['projectVersion'] }
выполняется до применения любых plugins → project.version гарантированно
не 'unspecified' к моменту создания публикации.
2026-09-15 23:13:02 +03:00
subochev 5fbe865a29 build: projectVersion прокидывается в subprojects через rootProject.extra
release / Publish KMP libraries → caffeine Nexus (release) Failing after 26s
release / Build standalone fatjar (release) Has been skipped
Gitea Actions workflow упал с 'Invalid publication kotlinMultiplatform:
version cannot be empty' для :memory-vector и :storage-sqlite. Root cause:

  if (version == 'unspecified') { version = providers.gradleProperty('version')... }

  subprojects { version = rootProject.version }  // <-- lazy: rootProject.version
                                                  // ещё 'unspecified' в этот момент

Фикс: вычисляем projectVersion eagerly через Provider.map().getOrElse(),
кладём в rootProject.extra, subprojects читают из extra (а не через
rootProject.version, которое ещё не выставлено). Также strip 'v' prefix
из tag-имени (CI передаёт -Pversion=v0.1.0 через GITEA_REF_NAME).
2026-09-15 23:05:41 +03:00
subochev 87742cf60b ci: передаём -Pbinom.repo.url явно (build.gradle.kts читает только -P-свойства)
release / Publish KMP libraries → caffeine Nexus (release) Failing after 2m10s
release / Build standalone fatjar (release) Has been skipped
Раньше URL репозитория брался через env var BINOM_REPO_URL, но build.gradle.kts
использует findProperty('binom.repo.url'), который читает только -P gradle
properties, не env vars. Без явного -Pbinom.repo.url Gradle фоллбэчился на
placeholder 'http://nexus.xx/repository/caffeine/' и публикация шла в
несуществующий репозиторий.

Передаю все три креды (url/user/password) через -P-свойства. Убрал
-Pdisable-javadoc=true — он был скопирован из devops/publish action, но
build.gradle.kts его не читает.
2026-09-15 22:35:56 +03:00
subochev 6c53e1c87d ci: release workflow читает креды из Secrets (а не из subochev/devops/publish)
subochev/devops/publish хардкодно берёт BINOM_REPO_USER/PASSWORD через
${{ vars.* }}, но безопаснее хранить их в Secrets (шифрованные).
Заменил вызов devops/publish на inline './gradlew publish' с теми же
-Pbinom.repo.user/password — функционально идентично, но читает из
secrets напрямую.

Также убрал JDK setup шаг из publish-libraries (был лишним, т.к.
agentik не использует Android SDK).
2026-09-15 22:35:11 +03:00
subochev 04276dec0e ci: maven-publish всех модулей + Gitea release workflow
- build.gradle.kts (root): настроен maven-publish для всех сабпроектов;
  репо 'caffeine' (Nexus) с setAllowInsecureProtocol=true, POM-метаданные
  (Apache-2.0, subochev as developer, scm). Version берётся из -Pversion=<tag>
  с fallback 0.1.0.
- gradle.properties: дефолтная version=0.1.0 для локальных билдов.
- .gitea/workflows/release.yml: триггер на release.published; две job'ы —
  publish-libraries (subochev/devops/publish action с BINOM_REPO_* env-vars)
  и build-standalone (собирает :standalone shadowJar, прикрепляет
  standalone-<version>-all.jar и sources.jar к release assets через
  softprops/action-gh-release + GITEA_TOKEN).
- В пяти KMP-модулях (skills, storage-core, agent-toolsets, server, client)
  добавлен api(libs.kotlinx.serialization.core) — раньше commonMain
  компилировался только на JVM, и эта зависимость была пропущена; теперь
  commonMain корректно публикуется как Gradle Module Metadata.

Локальная проверка:
  ./gradlew publishToMavenLocal — все 11 модулей × 9-10 таргетов
  ./gradlew jvmTest — все тесты зелёные
  ./gradlew :standalone:shadowJar — 240MB fatjar, Main-Class загружается
2026-09-15 21:58:56 +03:00
subochev 21bb9e6f8f standalone: убрать demo-toolset после e2e-проверки
Удаляю DemoToolset.kt, поле AgentikConfig.demoToolset и парсер
AGENTIK_DEMO_TOOLSET — после ночной e2e-проверки toolsets
они больше не нужны в проде. Механика покрыта ChatAgentToolsetsTest
через stub-тулы inline. Если потребуется live e2e — проще
прокинуть свой ToolsetContribution-список из main/test.
2026-09-15 21:10:52 +03:00
subochev 1441b7da9f standalone: AGENTIK_DEMO_TOOLSET=1 + DemoToolset для e2e проверки mechanics 2026-09-15 15:52:05 +03:00
subochev 9ee942428d standalone: ChatAgent принимает StorageBundle вместо SqliteStores
Финальный swap — ChatAgent/ChatConversation теперь работают через абстрактный
StorageBundle (pw.binom.agentik.storage), а не через конкретный SqliteStores.
Подготовка к Android-портированию (там будет :storage-android вместо
:storage-sqlite).

Изменения:
- SqliteStores.asBundle() — convenience для превращения конкретного
  SQLite-импла в StorageBundle
- ChatAgent(private val storage: StorageBundle) — было stores: SqliteStores
- ChatConversation(private val storage: StorageBundle) — то же
- Main.kt, DebugRoutes.kt — вызовы обновлены, используется .asBundle()
- Все 5 тестовых файлов с ChatAgent(... stores = ...) — обновлены на
  ChatAgent(... storage = ...asBundle())
- StorageBundle : AutoCloseable — закрывает все 4 store'а; в тестах
  tearDown { storage.close() }

Конфиг не менялся: toolsets остаётся emptyList() по умолчанию (полная
невидимость механики тулсетов для модели). Подключение тулсетов — opt-in
через параметр ChatAgent(toolsets = ...) для будущего e2e-теста в post-implementation.

Tests: 340/340 green. Fatjar 240 MB. Без регрессий.
2026-09-15 15:25:36 +03:00
subochev 1dc5552f98 agent-toolsets: SystemPromptToolsetSection + интеграция в ChatAgent
Добавлен SystemPromptToolsetSection — рендер markdown-секции для system prompt.
Контракт:
- toolsets пустой → null (секция не добавляется, агент не знает о механике)
- иначе → краткое описание концепции + список 'name — description' для
  активных и неактивных (одинаковый формат per design contract)
- auto-activation НЕ упоминается в промпте (только в dispatch)

Интеграция в ChatAgent:
- Добавлен параметр toolsets: List<ToolsetContribution> = emptyList()
- При пустом списке — enable_toolset/disable_toolset НЕ регистрируются,
  секция в system prompt НЕ появляется (полная невидимость per A1-α)
- При непустом — тулы регистрируются, секция добавляется
- ToolsetRegistry + ToolsetDispatchPolicy создаются per-agent (один реестр
  на все диалоги — состояние 'активные тулсеты' общее)

Интеграция в ChatConversation:
- Новый параметр toolsetDispatch: ToolsetDispatchPolicy? = null
- runToolAndPersist: если задан — вызов идёт через policy (auto-activate
  неактивных тулсетов, fallback в base dispatcher для плоских тулов)
- Иначе — старое поведение через toolsByName

Тесты:
- 7 новых в :agent-toolsets (SystemPromptToolsetSection): пустые списки,
  только активные, только неактивные, оба, проверка отсутствия auto-activation
  упоминания, registry-based рендер, пустой реестр
- 5 новых в :standalone (ChatAgentToolsetsTest): default (пустой) — нет
  тулов и секции; non-empty — тулы и секция есть; enable_toolset активирует;
  вызов тула из неактивного тулсета — auto-activate; disable_toolset
  снимает из active set (но auto-activate на следующем вызове — by design)

Tests: 340/340 green (335 ранее + 5 новых ChatAgent integration)
2026-09-15 15:06:23 +03:00
subochev 2e1387273a agent-toolsets: ядро механики toolsets (реестр, диспетчер, встроенные тулы)
Новый KMP-модуль :agent-toolsets с основными абстракциями для тулсетов:

- ToolsetContribution(name, description, tools: List<ToolEntry>) — декларация
  тулсета: имя + описание + список входящих LiteTool'ов с именами.
- ToolsetContext + Logger + NoOpLogger — что тулсеты получают при активации.
- ToolsetRegistry — реестр тулсетов с Mutex-защитой; методы
  activate/deactivate/isActive/activeNames/inactiveNames/activeTools/
  findByName/findOwnerByToolName.
- ToolsetDispatchPolicy — диспетчер с прощающей auto-activation: если тул из
  неактивного тулсета вызван — молча активирует тулсет и выполняет. Если тул
  вообще неизвестен — fallback в BaseToolDispatcher (плоские тулы вне toolsets).
- EnableToolsetTool / DisableToolsetTool — встроенные LiteTool'ы (4-case
  контракт зафиксирован в docs/TOOLSETS-PLAN.md): activate/deactivate с
  равномерным сообщением 'X deactivated' независимо от того, был ли он активен.
- SyncLiteTool — обёртка suspend-handler'а в синхронный LiteTool (через
  runBlocking). LiteTool.invoke синхронен по контракту litert-kmp.

Дизайн:
- :agent-toolsets НЕ зависит от :standalone — может быть переиспользован в
  Android-сборке и любом LiteTool-агенте.
- Модуль KMP (jvm + native), общие интерфейсы в commonMain, JVM-специфика
  только в SyncLiteTool (runBlocking).
- ToolsetContext минимален (logger); storage/skill добавятся в commit 5+.

Тесты: 29 новых покрывают activate/deactivate/idempotency, activeTools,
findOwnerByToolName, auto-activation в диспетчере, fallback в base, оба
контракта enable/disable со всеми 4 кейсами.

Tests: 328/328 green (299 ранее + 29 в :agent-toolsets)
2026-09-15 14:55:51 +03:00
subochev a7d8cbe713 storage-sqlite: выделить SQLDelight + SQLite-импл в отдельный модуль
Перенесён SQLDelight (4 .sq файла, конфигурация databases { AgentikDatabase })
и 5 SQLite-импл классов (SqliteStores, SqliteConversationStore, SqliteMessageStore,
SqliteWorkingMemoryStore, SqliteReflectionStore) из :standalone в новый JVM-only
модуль :storage-sqlite под пакетом pw.binom.agentik.storage.sqlite.

Изменения:
- Новый :storage-sqlite модуль с sqldelight-плагином + sqlite JDBC driver
- Все .sq файлы и Kotlin-классы переехали с переименованием пакета
- ReflectionStore.kt в :standalone (только SQLite-импл) удалён — функционал
  живёт в :storage-sqlite/SqliteReflectionStore.kt
- :standalone/build.gradle.kts: убран sqldelight-плагин и конфигурация,
  добавлена зависимость :storage-sqlite
- Все импорты в :standalone (8 main + 7 test) перенаправлены на новый пакет
- ReflectionStoreTest.kt переехал в :storage-sqlite/jvmTest (тестирует
  internal fun encode/decodeStringArray в :storage-sqlite)

Совместимость:
- SqliteStores доступен по новому пути pw.binom.agentik.storage.sqlite.SqliteStores
- Старые импорты в тестах обновлены (минимум diff — 1 строка на файл)
- В commit 6 ChatAgent переключится на StorageBundle API; SqliteStores
  станет деталью реализации :standalone

Тесты: 299/299 green. Fatjar standalone-all.jar 240 MB.

Преимущества:
- :standalone больше не зависит от SQLDelight плагина (легче поддерживать)
- :storage-sqlite может быть заменён/расширен (например, :storage-android)
- Тесты storage-слоя сгруппированы по модулю реализации
2026-09-15 14:51:58 +03:00
subochev 61f8205f40 storage-inmemory: in-memory импл 4 store'ов
Новый KMP-модуль :storage-inmemory с тред-безопасными (Mutex) in-memory
имплами для всех 4 store'ов из :storage-core:
- InMemoryConversationStore (Map по id, sortedByDescending(updatedAt))
- InMemoryMessageStore (List per conversation, с tokenStats)
- InMemoryWorkingMemoryStore (List с order_idx, atomic compact + summary)
- InMemoryReflectionStore (List по conversationId, FIFO для listRecent)

Фабрика InMemoryStorage.create() возвращает готовый StorageBundle.

Семантика 1:1 с SQLite-имплами — параллельные suspend-вызовы атомарны
через kotlinx.coroutines.sync.Mutex (lost-update race невозможен, как в
SQLite-driver-locked версии).

Тесты: 35 новых, проверяют round-trip всех CRUD-операций, тред-безопасность,
tokenStats агрегацию, compact+summary, events-flow.

Планируемое использование:
- :standalone тесты (вместо SqliteStores.inMemory() с JDBC)
- Android ART-сборка (commit 7+; SQLite требует JDBC драйвера, недоступного
  в Android base classes)
- embedded/cold-start сценарии без SQLite-инициализации

Tests: 299/299 green (264 ранее + 35 новых в :storage-inmemory)
2026-09-15 14:48:29 +03:00
subochev 294837daa0 storage-core: новый KMP-модуль с интерфейсами хранилища
Выносим интерфейсы и data-классы истории диалога (MessageStore / WorkingMemoryStore /
ConversationStore / ReflectionStore + соответствующие sealed-иерархии MessageRecord /
WorkingMemoryEntry / Content / ConversationRecord / Reflection + payload-утилиты) из
:standalone в отдельный KMP-модуль :storage-core (pw.binom.agentik.storage).

Цель — подготовка к Android-портированию и подключению альтернативных реализаций
хранилища без затягивания всей :standalone. Дальше (commit 2/3) — :storage-inmemory
и :storage-sqlite как самостоятельные модули, плюс :storage-android (deferred).

Изменения:
- Новый :storage-core (KMP, commonMain only, jvm + native таргеты) — 12 файлов
- StorageBundle агрегатор (conversationStore + messageStore + workingMemoryStore +
  reflectionStore; SkillStore живёт в :skills и подключается отдельно)
- 11 файлов импортов в :standalone переключены на новый пакет
- SqliteReflectionStore оставлен в :standalone до commit 3 (зависит от
  SQLDelight AgentikDatabase, которую ещё не отвязали от :standalone)
- 4 теста перенесены в :standalone/.../storage/ с обновлённым пакетом
- PayloadTest переехал в :storage-core/commonTest (тестирует чистые типы)

Tests: 264/264 green (179 :standalone + 6 :storage-core + прочие JVM-модули)
2026-09-15 14:39:30 +03:00
subochev e192f58cd0 docs: TOOLSETS-PLAN.md — implementation roadmap for toolsets + storage refactor
Captures the locked architectural decisions, module layout, and 6-commit
sequence. Implementation proceeds autonomously per this plan.

Refs: decisions from session 2026-09-15.
2026-09-15 14:31:59 +03:00
subochev f1cd2e3d42 litert-8: тул-цикл без фантомного trigger-сообщения
litert-api 7 -> 8. addToolResult(callId, name, result: Unit) ->
LiteDelta (несёт текст пост-тул ответа модели + возможные вложенные
tool-calls). caffeine не публикует parent-аггрегатор, поэтому алиасы
в libs.versions.toml указывают на -jvm flavor напрямую.

ChatConversation.runTurn: убран хак currentParts=[Text(' ')] —
вместо него runToolAndPersist(call) -> (callId, resultText) ->
addToolResult() возвращает LiteDelta, цикл идёт по delta.toolCalls.
Никакого 'призрачного' ответа модели в KV-cache после каждого тула.

Live smoke-test (gemma-4-E2B + SigLIP vector backend):
- 'Запомни: работаю на macOS' -> 'Я сохранил информацию о том, что вы
  работаете на macOS' (раньше: 'Чем я могу помочь?')
- 'На чём работаю?' -> 'Вы работаете на macOS'
- цепочка имя->а necdoт -> модель осмысленно продолжает, не сбрасывается
- тесты: 264/264 зелёных
2026-09-15 13:36:40 +03:00
subochev 135c6a419d fix(memory-vector): seed JVector index from SQLite on VectorMemorySystem.open()
После рестарта in-RAM граф JVector создавался пустым (seedEntries не
проходились из metaStore), поэтому search возвращал [], пока не
появлялись новые upsert'ы — память терялась после каждого рестарта.

- VectorMemorySystem.open(): JVectorMemoryIndex(dimension, metaStore.allEntries())
- SqliteMemoryMetaStore.open(): убрал случайное двойное конструирование
- регресс-тест openSeedsIndexFromSqliteAfterRestart (save → close → open → search)
2026-09-15 08:17:42 +03:00
subochev 18ae6e0619 a2a: подключить A2A-транспорт (POST /a2a + agent-card) через A2aBridge
:standalone декларировал зависимость a2a-server, но mount не было (e2e
нашёл 404 на /a2a). A2aBridge (AgentHandler) гоняет A2A-context на
:proto-диалог: contextId -> Conversation (пустой/неизвестный -> новый),
ответ = склеенные AppendText хода (подписка на events() до send, стоп по
End/Interrupted/Error), id диалога в metadata.agentikConversationId.
2026-09-15 07:31:54 +03:00
subochev 9b37edd92e fix(skills): SkillParser.serialize — закрывающий fence прилипал к последней YAML-строке
kaml encodeToString не ставит завершающий перевод строки, из-за чего
сериализованный SKILL.md выглядел так:
  ---
  name: x
  description: "y"---
и SkillParser.parse находил MissingClosingFence — каждый скил,
сохранённый через skill_save / SkillMiner, становился нечитаемым после
рестарта агента (каталог терял скил).

Поставлен явный '\n' перед закрывающим fence. Добавлены юнит-тесты
round-trip serialize->parse (обычный, пустой body, спецсимволы YAML).
2026-09-15 07:01:23 +03:00
subochev df386ef875 skills: SkillMiner — фоновое авто-создание скилов + debug-эндпоинты
SkillMiner (сетка безопасности skill self-improvement): каждые
AGENTIK_SKILL_MINING_INTERVAL user-ходов (default 15) LLM смотрит
последние AGENTIK_SKILL_MINING_MAX_TURNS ходы (default 30) + каталог
существующих скилов и возвращает structured JSON {"skills":[...]}.
Найденное upsert-ится в SkillStore — модель "забыла" вызвать
skill_save в ходе разговора, минер добирает её постфактум.

- SkillMiner.kt: короткий LiteConversation (one-shot), blocking-инференс
  на Dispatchers.IO, defensive парсинг (кривой ответ -> пустой список).
- SkillMiningPrompts/SkillMiningParser: тот же подход, что
  ReflectionParser (structured-output вместо tool-calling).
- ChatConversation.scheduleSkillMining() — хук после каждого хода
  (рядом со scheduleReflection); ChatAgent/Main — прокидывание.
- DebugRoutes.kt: AGENTIK_DEBUG_ENDPOINTS=1 включает POST
  /debug/reflect, /debug/skill-mine, /debug/curate, /debug/compact и
  GET /debug/tokens для ручного триггерирования фоновых фич.
- ChatConversation.forceCompactNow(): принудительный compaction
  без проверки порога (для /debug/compact).
- Тесты: SkillMinerTest (5) + SkillMiningParserTest (9); FakeLiteLlm
  теперь записывает send()/sendContents() в lastContents.

179 jvm-тестов :standalone зелёные, README обновлён.
2026-09-15 06:36:23 +03:00
subochev f4ce82957b standalone: token accounting per assistant turn (input/output → SQLite)
LiteLlm API не отдаёт split prompt/completion наружу через send()
(внутренний OpenAI Usage сидит в pw.binom.litert.openai и недоступен),
поэтому измеряем через LiteConversation.tokenCount():

  input  = tokenCount() до первого send в turn'е
          (= system + вся история + tools + только что добавленное user-сообщение)
  output = tokenCount() после завершения turn'а - input
          (= assistant text + tool calls + tool results за tool loop)

Пишем в assistant-запись как TurnTokens(input, output) в payload_json.
Никаких schema-миграций: payload-формат уже обёрнут в MessageBodyPayload,
просто добавлено опциональное поле tokens.

MessageStore.tokenStats(conversationId) → TokenStats(turns, inputTokens, outputTokens).
На старте агент печатает сводку по всем диалогам:
  tokens: 17 convs, 134 turns, in=523844, out=58290, total=582134

Бэкенды без tokenCount() (off-line LiteRT-LM модели) → tokens=null,
старые assistant-записи без метрики → пропускаются в tokenStats без ошибок.

Tests: TokenStatsTest (5 green) + PersistenceTest (unchanged) → 165 total.
Backward compat: legacy plain-array payload всё ещё читается, tokens=null.
2026-09-15 05:45:40 +03:00
subochev 9afa877e39 tools: подключить skill_save/skill_delete к ChatAgent (Hermes Phase 3 ready)
Тулзы были написаны ранее (SkillSaveTool/SkillDeleteTool, SkillToolsFactory),
но не были подключены к ChatAgent. Этот коммит закрывает пробел:

* ChatAgent: новый параметр skillStore: SkillStore? = null. Когда задан —
  в allTools добавляются SkillToolsFactory.create(skillStore) → агенту
  доступны skill_save и skill_delete (помимо read_skill который всегда
  есть при непустом каталоге).
* Main.kt: если config.skillsDir задан — создаём DiskSkillStore(File(dir))
  и скармливаем агенту. Каталог используется и для чтения (SkillCatalog),
  и для записи (DiskSkillStore.upsert/remove) — одни и те же файлы,
  никаких рассинхронов между read_skill и skill_save.
* SkillLoader.loadDirectory больше не нужен в Main — DiskSkillStore сам
  подгружает каталог в init. Удалён старый импорт.
* Тесты SkillToolsTest (5): SkillSaveTool persists file and surfaces in
  catalog; rejects blank name; SkillDeleteTool archives (rename to
  .archived); errors on missing skill; colon-named skills map to nested
  dirs (backend:spring:db-base → backend/spring/db-base/SKILL.md).
* README: раздел "Навыки" расширен описанием трёх тулов (read_skill /
  skill_save / skill_delete).

Smoke: standalone запускается с пустым AGENTIK_SKILLS_DIR, видит
"skills: 0 loaded from /tmp/skills-smoke" (DiskSkillStore создаёт каталог
при отсутствии). Агент при наличии skillStore имеет в своём распоряжении
все три тула для self-improvement'а.

Теперь Phase 3 (skill self-improvement) реально работает end-to-end:
агент может дёрнуть skill_save когда понимает что задача повторяется,
потом в следующих диалогах использовать новый скил через read_skill.

Tests: 160 standalone JVM (+5), 241 всего JVM, 73 native, all green.
2026-09-15 05:20:41 +03:00
subochev 23da1f6498 reflection: Hermes-style self-reflection (последняя открытая Hermes-фича)
Self-reflection: каждые N пользовательских ходов агент запускает one-shot
LLM-размышление о качестве своих ответов, сохраняет score+weakSpots в SQLite,
подмешивает top-K последних рефлексий в system prompt как "слабые места".

* sqldelight: новая таблица reflection (id, conversation_id?, created_at,
  turns_analyzed, score, summary, weak_spots_json) + индексы по created_at и
  conversation_id.
* persistence: Reflection data class + ReflectionStore interface +
  SqliteReflectionStore (insert/get/listRecent/listForConversation/
  deleteOlderThan/count + events Flow). weakSpots хранятся как JSON-массив,
  парсятся ручным сканером (без kotlinx-serialization в этом модуле).
* agent: LlmReflector (one-shot LiteLlm через createConversation +
  send, structured-output JSON). ReflectionParser (hand-rolled,
  толерантный к ```json fences и лидирующему/завершающему тексту;
  score принимает int или строку; weakSpots — массив).
* agent: ReflectionPrompts (Russian system+user prompts, аналогично
  LlmMemoryReviewer/ReviewPrompts).
* agent: ChatAgent.buildSystemPrompt расширен параметром reflections —
  добавляется секция `## Self-reflection: твои слабые места за последнее время`
  после memory и перед soul-prepend.
* agent: ChatConversation.scheduleReflection — каждые reflectionInterval
  пользовательских ходов (счётчик через workingMemory.list) запускает
  reflector на Dispatchers.IO, результат сохраняет в reflectionStore
  с conversationId. Не блокирует turn.
* AgentikConfig: новые поля reflectionInterval (env AGENTIK_REFLECTION_INTERVAL,
  default 10, clamped 0..1000) и reflectionTopK (env AGENTIK_REFLECTION_TOP_K,
  default 3, clamped 0..20).
* Main.kt: если reflectionInterval > 0 — создаём LlmReflector(llm); загружаем
  top-K из SQLite в system prompt.
* Ids.reflection() — генератор id "refl-<uuid>".
* Tests: ReflectionParserTest (7: clean JSON, fences, лидирующий текст,
  score-as-string, отсутствие score, невалидный JSON, escape-последовательности),
  ReflectionStoreTest (7: round-trip, listRecent с лимитом, фильтр по
  conversation_id, deleteOlderThan, count, encode/decode строк), и
  ChatAgentReflectionTest (3: секция скрыта при пустых, присутствует с
  score+spots, порядок soul→memory→reflection).
* README: новые env-переменные, раздел "Self-reflection", startup output.

Smoke test подтверждает: reflection секция появляется в system prompt когда
в SQLite есть записи (listRecent возвращает непустой список). При
reflectionInterval=0 reflector не создаётся, scheduleReflection — no-op.

Tests: 305 total green (+17: 7+7+3). Fatjar собирается, logback-вывод
работает (logging: см. предыдущий коммит f5a551b).
2026-09-15 05:14:33 +03:00
subochev f5a551b2ae logging: kotlin-logging 3.0.5 + logback-classic + AGENTIK_LOG_LEVEL env
Заменил все System.err.println / println на структурное логирование
(kotlin-logging, пакет mu) — теперь логи идут с timestamp/level/thread/logger.

* gradle/libs.versions.toml: kotlin-logging = "3.0.5" (в прокси доступна
  только эта версия; новые 7.x пока не подтянуты), logback-classic = "1.5.18".
* standalone/build.gradle.kts: implementation(libs.kotlin.logging) +
  implementation(libs.logback.classic) в jvmMain.
* standalone/src/jvmMain/resources/logback.xml: консольный appender,
  pattern с timestamp/level/thread/logger, level управляется через
  ${AGENTIK_LOG_LEVEL:-INFO} (env override на старте JVM), уровни
  io.netty/ai.onnxruntime уведены в WARN чтобы не забивать канал.
* Заменены все System.err.println в: ChatConversation (14 callsites),
  Curator (2), McpRegistry (5), McpConfig (2), Main (2). Startup banner
  в Main оставлен на println — это user-facing output, не log.
* Tests: 288 зелёных (только замена log-вызовов, без изменения семантики).

Smoke: `04:53:49.313 INFO  [DefaultDispatcher-worker-4] p.b.a.s.agent.memory.Curator - started (interval=1d, maxAge=90d, maxUseCount=0)`
подтверждает структурный лог вместо println. AGENTIK_LOG_LEVEL=DEBUG работает.
2026-09-15 04:54:16 +03:00
subochev 427ce8a572 memory: SigLIP2 on-device embedding (text-embedding-kmp v3)
Добавляет второй бэкенд эмбеддингов для vector-памяти: on-device SigLIP2
через ONNX Runtime. Не требует сети (HTTP), не светят тексты заметок наружу.

* gradle/libs.versions.toml: text-embedding-kmp = "3.0.0-SNAPSHOT", модули
  api-jvm / siglip-jvm (group переехал с pw.binom.voice.embeddingtext на
  pw.binom.ai.embeddingtext).
* settings.gradle.kts: mavenLocal() добавлен в dependencyResolutionManagement
  (text-embedding-kmp публикуется локально как snapshot).
* memory-vector/SiglipEmbeddingProvider (jvmMain) — адаптер
  pw.binom.voice.embeddingtext.TextEmbeddingExtractor → EmbeddingProvider:
  оборачивает blocking embed() в withContext(Dispatchers.IO) + Mutex (ONNX
  сессия не reentrant), размерность пробируется через probe embed("probe")
  (768 для SigLIP2-base).
* memory-vector/build.gradle.kts: api(libs.text.embedding.api) +
  implementation(libs.text.embedding.siglip); jvmTest получает
  SiglipEmbeddingProviderTest — smoke test (skip если модель не найдена).
* standalone/AgentikConfig: новый enum EmbeddingBackend { HTTP, SIGLIP },
  поля embeddingBackend / embeddingModelPath / embeddingTokenizerPath, env:
  AGENTIK_EMBEDDING_BACKEND, AGENTIK_EMBEDDING_MODEL_PATH,
  AGENTIK_EMBEDDING_TOKENIZER_PATH.
* standalone/Main.kt: switch на embeddingBackend при memory-backend=vector;
  для SIGLIP требуются оба пути, иначе ошибка с понятным сообщением.
* standalone/README.md: обновлены env-vars, добавлены две bash-секции
  (HTTP и SIGLIP) + инструкция скачивания модели с static.binom.pw.
* scripts/install-text-embedding-stub.sh: workaround для upstream бага
  (siglip-jvm/*.module ссылается на api без -jvm variant). Создаёт
  stub-артефакт api:3.0.0-SNAPSHOT в mavenLocal с тем же содержимым.
  Удалить когда upstream починит module-metadata.

Smoke test: AGENTIK_MEMORY_BACKEND=vector AGENTIK_EMBEDDING_BACKEND=siglip
+ несуществующий путь → FileNotFoundException с понятным трейсом (значит
ONNX Runtime инициализирован, путь через factory пробрасывается корректно).

Tests: 288 total green. Fatjar 250MB (вырос из-за onnxruntime ~80MB).

Dropped: старый stub-jar pw.binom.voice.embeddingtext:api:2.0.0-SNAPSHOT.
2026-09-15 04:43:45 +03:00
subochev 9e12b22e85 memory: Curator (Phase 4) — фоновая архивация старых неиспользуемых заметок
* :memory-api — MemoryStore.archiveStale(maxAge, maxUseCount, now): default
  имплементация через list + delete (бэкенды могут переопределить).
* :standalone — Curator: фоновая корутина на Dispatchers.IO, раз в сутки
  дёргает archiveStale(90d, 0). start()/stop(), runPass() — однократный
  прогон для тестов.
* :standalone/Main — Curator стартует автоматически если memory включён,
  stop() в shutdown hook.
* :standalone — публичный TestInMemoryMemoryStore вынесен из CompactionTest,
  переиспользуется в CuratorTest.
* README — раздел "Куратор памяти" с описанием семантики для md и vector
  бэкендов.

Tests: 288 total (+4 CuratorTest). Fatjar smoke-tested, curator стартует
на app boot, выводит `[Curator] started` в лог.

Defaults: interval=1d, maxAge=90d, maxUseCount=0. Override через
новые config-флаги отложен.
2026-09-15 04:14:03 +03:00
subochev 227d14b7e4 memory-vector: LlmMemoryReviewer + SkillStore/SkillSaveTool/SkillDeleteTool
Phase 3 (Hermes-style self-improvement) and Phase 5.2 (LLM-driven review):

* :skills — SkillStore interface + DiskSkillStore (upsert/remove, file<->catalog sync)
* :skills — SkillParser.serialize for write-back path
* :standalone — SkillSaveTool/SkillDeleteTool + SkillToolsFactory
* :standalone — LlmMemoryReviewer: one-shot LiteLlm review via structured-output
  JSON prompt ({toSave:[...], toDelete:[...]}); reuses MemorySystem store
* :standalone — ReviewDecisionParser (lenient, handles json fences, missing
  arrays, malformed numbers)
* :standalone — ReviewPrompts (Russian system+user prompts, fact categories)
* :standalone/Main — wires LlmMemoryReviewer instead of KeywordMdReviewer when
  LLM is available, falls back to keyword for off/md-only mode
* :client — send(content, context) overload + SendPayload wrapper
* :proto — ExperimentalNativeApi opt-in for MessageContextTest (native targets)

Bug fixes:
* SkillTools.kt: error() shadowed kotlin.error(); renamed to Nothing
* ChatConversation: .map { when(...); error() } → .mapNotNull { when ... else -> null }
  (Kotlin type inference of LUB LiteMessage | Nothing failed across when-expr)

Tests: 284 total green (memory-vector: 17, standalone: 149).

Dropped: IRC-QUESTIONS.md (irc-server design rejected — user decision 2026-09-14).
2026-09-15 04:08:46 +03:00
subochev 65365da89c Phase 5: :memory-vector (JVector + SQLite + LLM-эмбеддинги), memory-abstraction, compaction, MessageContext
- :memory-api — общий контракт MemoryStore/Prefetcher/Reviewer/Tools/MemorySystem
- :memory-md (KMP, kotlinx-io) — Hermes-style §-файлы, keyword overlap
- :memory-vector (JVM-only) — JVector ANN + SQLite + HttpEmbeddingClient
- :standalone — AGENTIK_MEMORY_BACKEND={md,vector,off}, выбор в Main.kt
- :standalone — compaction рабочего контекста (LiteLlmContextCompactor + reviewPreCompaction)
- :proto — MessageContext (origin: user/system/event) на send и в Message
- :server — backward-compat dual-format для POST /messages
- README — env-vars, vector-бэкенд docs
2026-09-15 03:44:15 +03:00
subochev 0faad45f3d standalone: persist turn errors so polling clients see them
Event.Error уже эмитился в live-SSE, но если клиент подключился
после провала хода (или опрашивает историю через getMessages
вместо SSE), он видел только user-сообщение без следа, что ход
провалился. Это и нужно было поправить.

* :proto
  - Message.Error(id, message, code?, date) — терминальная
    персистентная проекция Event.Error. Audit-only; в live-стриме
    по-прежнему приходит Event.Error.

* :standalone
  - MessageRecord.Error с тем же контрактом.
  - SqliteMessageStore: kind "error", JSON-payload {message, code};
    encoding-ошибки round-trip покрыты тестом (с code и без).
  - ChatConversation.failTurn(message, code?): пишет MessageRecord.Error
    в audit (кроме temp-диалогов), затем эмитит Event.Error + Event.End.
    Все три exit-точки из runTurn (пустой parts, init-catch,
    stream-catch) теперь через failTurn.
  - ChatConversation: при ошибке стрима живой LiteConversation
    сбрасывается — следующий send пересоберёт его из working_memory.
    Раньше оставляли битую инстанцию вопреки KDoc класса.
  - toProto: маппит MessageRecord.Error → Message.Error.

* docs
  - STANDALONE.md: добавлен Message.Error в таблицу dual-log + абзац
    про персист ошибок (audit/working_memory, сброс liteConv).
  - ARCHITECTURE.md: Message.Error в сигнатуре Message, failTurn
    в ChatConversation, Message.Error в audit log.
  - Поправлен пример describe() в доке тулов (реальный формат —
    OpenAI-style function wrapper, а не голый JSON Schema).

Тесты:
  - ChatAgentTest: LLM failure → Event.Error + Event.End + Error
    record в audit + backfill через getMessages.
  - PersistenceTest: MessageRecord.Error round-trip (с code и без).
  - :standalone jvmTest 70 (было 69).

E2E: неверный model id (HTTP 400) → polling GET /messages теперь
возвращает user_message + error (без assistant_message).
2026-09-14 00:26:13 +03:00
subochev adcb8f54d6 skills: каталог + ленивая загрузка read_skill
Пользовательские инструкции («навыки») живут в указанной папке
(AGENTIK_SKILLS_DIR), рекурсивно читаются при старте и попадают в
системный промпт в сжатом виде: только имя + краткое описание.
Полный текст модель подгружает по требованию, вызывая встроенный
инструмент read_skill(name).

*:skills
  - SkillCatalog + SkillPrompt (commonMain): рендер секции системного
    промпта; тело навыка в промпт не течёт.
  - SkillParser.parseAuto(): теперь читает и opencode-стиль SKILL.md
    (YAML frontmatter + markdown тело), и голый *.yaml/*.yml
    (поля name, description, опц. body). parseOrThrow для strict-путей.
  - SkillParseError.render(): человекочитаемое описание ошибки для
    логов и диагностики.
  - SkillLoader (jvmMain): рекурсивный обход папки, детерминированный
    порядок (по пути), ошибки отдельных файлов не валят загрузку;
    дубликаты имён → ошибка, выигрывает первый по пути.

* :standalone
  - AgentikConfig.skillsDir + env AGENTIK_SKILLS_DIR.
  - ChatAgent: параметр skills (SkillCatalog); системный промпт
    автоматически дополняется секцией «## Навыки» и в working memory
    сидится вместе с базовым промптом.
  - При непустом каталоге в tools автоматически добавляется
    SkillReadTool (имя read_skill) — модель может загрузить полный
    текст навыка, как обычный LiteTool.
  - Main.kt: загружает навыки и шумно логирует ошибки загрузки в stderr.

* docs
  - STANDALONE.md: секция «Навыки (skills)», env-переменная в таблице.
  - Формат SKILL.md (opencode frontmatter) + голый *.yaml/*.yml.

Тесты: :skills jvmTest 34, :standalone jvmTest 69 (новые — состав
системного промпта, регистрация read_skill, навыки не утекают в
промпт телом).
2026-09-14 00:25:22 +03:00
subochev 3fde5c28e3 config: единая AgentikConfig DTO + сериализация всей конфиг-цепочки
- AgentikConfig(port, dbPath, llm, mcp) — единая точка входа для всего,
    что настраивается снаружи; Main.kt больше не читает System.getenv,
    только config.*. Наполняется пока из env (AgentikConfig.fromEnv).
  - @Serializable на AgentikConfig/LlmConfig/GoogleConfig/McpConfig/
    McpServerSpec(Stdio,Http). Внешний litert OpenAiConfig (не сериализуемый)
    заменён нашим pw.binom.agentik.standalone.llm.OpenAiConfig с маппером
    toLitertConfig() — чтобы позже читать конфиг из yaml.
  - AgentikConfigTest: дефолты, env-override, делегирование в LlmConfig/McpConfig,
    JSON round-trip с полиморфным McpServerSpec.

Проверка: :standalone:jvmTest 54/54; смок через DTO — /health=ok.
2026-09-13 21:49:12 +03:00
subochev f4caab940e skills: парсер YAML-скилов (opencode-формат)
Новый KMP-модуль :skills (9 целей, зеркалит :proto). Парсит скилы
в формате opencode: YAML-фронтматтер с name+description, затем markdown
body.

  - SkillFile(name, description, body) — @Serializable результат.
  - SkillParseError — sealed-иерархия ошибок: Empty, MissingOpeningFence,
    MissingClosingFence, InvalidYaml, SchemaMismatch, BlankName.
  - SkillParser.parse(raw): SkillParseResult + parseOrThrow(raw): SkillFile.
    Обрабатывает BOM, CRLF, имена с двоеточиями (backend:spring:db-base),
    multiline-YAML, пустое тело; неизвестные ключи фронтматтера игнорирует.
  - SkillParserTest — 16 тестов, 0 failures.

YAML-библиотека — com.charleskorn.kaml 0.104.0 (не JetBrains,
интеграция с kotlinx @Serializable). Грабли kaml 0.104: параметр
ignoreUnknownKeys переименован в strictMode (инвертирован);
missing-required-field прилетает как YamlException, не
SerializationException — различаем по сообщению.

Проверка: :skills:jvmTest 16/16, :skills:assemble (9 целей) — зелёные.
2026-09-13 21:28:47 +03:00