diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..0bace3a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,88 @@ +# PR / push-build. Прогоняет unit-тесты на JVM и линтер gradle-плагинов. +# Артефакты не публикует — этим занимается .gitea/workflows/release.yml. +# +# Требуемые Gitea Action Secrets: нет (только gradle-cache). +# Опционально: GRADLE_DOWNLOAD_TOKEN — если хочется переиспользовать кэш между +# репами (через actions/cache + restore-keys). +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-jvm: + name: JVM build + tests + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + .gradle + key: ${{ runner.os }}-gradle-agentik-${{ hashFiles('**/*.gradle.kts', '**/gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-agentik- + + - name: Build + test (JVM only — самые быстрые таргеты) + shell: bash + run: | + ./gradlew jvmTest \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + + - name: Build :standalone shadowJar (smoke — запускаемый артефакт) + shell: bash + run: | + ./gradlew :standalone:shadowJar \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + test -f standalone/build/libs/standalone-all.jar \ + && echo "shadowJar OK: $(du -h standalone/build/libs/standalone-all.jar)" + + - name: Build :agentik-cli shadowJar + shell: bash + run: | + ./gradlew :agentik-cli:shadowJar \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + test -f agentik-cli/build/libs/agentik-cli-all.jar \ + && echo "shadowJar OK: $(du -h agentik-cli/build/libs/agentik-cli-all.jar)" + + - name: Build :agentik-tui shadowJar + shell: bash + run: | + ./gradlew :agentik-tui:shadowJar \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + test -f agentik-tui/build/libs/agentik-tui-all.jar \ + && echo "shadowJar OK: $(du -h agentik-tui/build/libs/agentik-tui-all.jar)" + + - name: Upload shadowJars + uses: actions/upload-artifact@v4 + with: + name: agentik-jars + path: | + standalone/build/libs/standalone-all.jar + agentik-cli/build/libs/agentik-cli-all.jar + agentik-tui/build/libs/agentik-tui-all.jar + if-no-files-found: error + retention-days: 7 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 573d65f..b1abf4e 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,9 +1,7 @@ # Триггерится при публикации релиза в Gitea. Публикует все KMP-библиотеки -# (jvm + native таргеты) в домашний Nexus-репозиторий "caffeine". -# -# Сборку :standalone fatjar'a и прикрепление JAR к release было решено убрать -# из CI (2026-09-15, итерация с публикацией затянулась). JAR собирается локально -# через `./gradlew :standalone:shadowJar` — его достаточно для запуска. +# (jvm + native таргеты) в домашний Nexus-репозиторий "caffeine", а также +# собирает fatjar'ы запускаемых модулей и прикрепляет их к релизу как +# бинарные ассеты. # # Требуемые Gitea Action Variables: # BINOM_REPO_URL — например http://nexus.xx/repository/caffeine/ @@ -15,10 +13,15 @@ on: release: types: [published] +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: publish-libraries: name: Publish KMP libraries → caffeine Nexus runs-on: ubuntu-latest + timeout-minutes: 120 steps: - name: Checkout uses: actions/checkout@v4 @@ -29,7 +32,18 @@ jobs: java-version: '21' distribution: 'adopt' - - name: Publish libraries + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + .gradle + key: ${{ runner.os }}-gradle-agentik-${{ hashFiles('**/*.gradle.kts', '**/gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-agentik- + + - name: Publish libraries (all KMP targets, all modules) shell: bash env: BINOM_REPO_USER: ${{ secrets.BINOM_REPO_USER }} @@ -44,3 +58,76 @@ jobs: publish \ -Dorg.gradle.jvmargs=-Xmx4096M \ --no-daemon --no-watch-fs --stacktrace + + build-fatjars: + name: Build runnable fatjars + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + .gradle + key: ${{ runner.os }}-gradle-agentik-${{ hashFiles('**/*.gradle.kts', '**/gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-agentik- + + - name: Build :standalone shadowJar + shell: bash + run: | + ./gradlew :standalone:shadowJar \ + -Pdisable-javadoc=true \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + + - name: Build :agentik-cli shadowJar + shell: bash + run: | + ./gradlew :agentik-cli:shadowJar \ + -Pdisable-javadoc=true \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + + - name: Build :agentik-tui shadowJar + shell: bash + run: | + ./gradlew :agentik-tui:shadowJar \ + -Pdisable-javadoc=true \ + -Dorg.gradle.jvmargs=-Xmx4096M \ + --no-daemon --no-watch-fs --stacktrace + + - name: Upload fatjars as release assets + uses: actions/upload-artifact@v4 + with: + name: agentik-fatjars + path: | + standalone/build/libs/standalone-all.jar + agentik-cli/build/libs/agentik-cli-all.jar + agentik-tui/build/libs/agentik-tui-all.jar + if-no-files-found: error + retention-days: 90 + + - name: Attach to release + uses: https://git.binom.pw/actions/forgejo-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + url: ${{ github.server_url }} + repo: ${{ github.repository }} + token: ${{ secrets.GITEA_TOKEN }} + tag: ${{ github.ref_name }} + files: | + standalone/build/libs/standalone-all.jar + agentik-cli/build/libs/agentik-cli-all.jar + agentik-tui/build/libs/agentik-tui-all.jar diff --git a/README.md b/README.md index 4ce55af..7a19d5d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,86 @@ # agentik +Self-contained multi-module Kotlin Multiplatform агент с долговременной памятью, +персоной, навыками и HTTP-фасадом под `/agentik`. Состоит из библиотечных модулей +(KMP, опубликованных в Nexus `caffeine`) и трёх запускаемых артефактов. + +## Запускаемые модули + +| Модуль | Что делает | Артефакт | Таргеты | +|---|---|---|---| +| [`:standalone`](standalone/README.md) | HTTP-сервер со всеми транспортами (AG-UI / A2A / `:proto`), SQLite, памятью, скилами, SOUL, MCP | `standalone-all.jar` (≈250 MB) | JVM | +| [`:agentik-cli`](agentik-cli/README.md) | REPL-клиент к `/agentik` со slash-командами | `agentik-cli-all.jar` (≈8 MB) | JVM | +| [`:agentik-tui`](agentik-tui/README.md) | Compose-style TUI-клиент (Mosaic) к `/agentik` | `agentik-tui-all.jar` (≈10 MB) | JVM + macosX64/macosArm64/linuxX64/linuxArm64/mingwX64 | + +## Библиотеки + +Все библиотеки — **KMP (jvm + 8 native)**, опубликованы в Nexus-репо `caffeine` +под группой `pw.binom.agentik`. + +### Протокол и транспорт +- [`:proto`](proto/README.md) — `Agent` / `Conversation` / `Message` / `Event`, типы без сетевой логики. **Stateful** — клиент шлёт только новый message, агент владеет историей. +- [`:server`](server/README.md) — Ktor-фасад, экспонирующий `:proto.Agent` под `/agentik` (HTTP+JSON+SSE). +- [`:client`](client/README.md) — Ktor-клиент, превращающий HTTP `/agentik` обратно в `Agent`/`Conversation`. + +### Память +- [`:memory-api`](memory-api/README.md) — контракт: `MemoryStore`, `MemoryNote`, `MemoryPrefetcher`, `MemoryReviewer`, `MemoryTools`. +- [`:memory-md`](memory-md/README.md) — Hermes-style реализация поверх §-файлов (`user.md`/`world.md`/`preference.md`). +- [`:memory-vector`](memory-vector/README.md) — JVector (ANN) + SQLite + эмбеддинги (HTTP/SIGLIP on-device). + +### Хранилище +- [`:storage-core`](storage-core/README.md) — `ConversationStore` / `MessageStore` / `WorkingMemoryStore` / `ReflectionStore`. +- [`:storage-inmemory`](storage-inmemory/README.md) — in-memory реализация (для тестов и embedded). +- [`:storage-sqlite`](storage-sqlite/README.md) — SQLDelight реализация (прод-бэкенд). + +### Логика +- [`:skills`](skills/README.md) — opencode-style `SKILL.md` / `*.yaml` парсер + рендер в system prompt. +- [`:agent-toolsets`](agent-toolsets/README.md) — реестр тулов + `enable_toolset`/`disable_toolset` диспетчер. + +## Где смотреть версии + +- `gradle.properties` → `version=0.1.0` (текущая разрабатываемая) +- Релизы: `https://git.binom.pw/subochev/agentik/releases` +- Опубликованные артефакты: Nexus-репозиторий `caffeine` + (`http://nexus.xx/repository/caffeine/pw/binom/agentik/`) + +При подключении библиотек используйте одну и ту же `version` (`VERSION` в Gradle +зависимостях). Все артефакты синхронизированы и совместимы по ABI в пределах +одной версии. + +## Публикация (CI/CD) + +`.gitea/workflows/release.yml` — публикует все KMP-таргеты всех модулей в +Nexus-репо `caffeine` при создании Gitea Release. Версия артефактов берётся из +имени тега (`git tag v0.1.0` → `pw.binom.agentik:*:0.1.0`). + +```bash +# Создать релиз: +git tag v0.1.0 && git push --tags +# → Gitea → Releases → New Release → выбрать тег → Publish +# → CI публикует в Nexus (нужны секреты BINOM_REPO_USER/BINOM_REPO_PASSWORD) +``` + +## Сборка + +```bash +# Всё +./gradlew build + +# Только JVM-тесты всех модулей +./gradlew jvmTest + +# Только fatjar запускаемых модулей +./gradlew :standalone:shadowJar :agentik-cli:shadowJar :agentik-tui:shadowJar + +# Опубликовать локально в mavenLocal (~/.m2) +./gradlew publishToMavenLocal + +# Опубликовать в Nexus (нужны креды) +./gradlew publish -Pversion=0.2.0 \ + -Pbinom.repo.url=http://nexus.xx/repository/caffeine/ \ + -Pbinom.repo.user=USER -Pbinom.repo.password=PASS +``` + +## Лицензия + +Apache-2.0 — см. [LICENSE](LICENSE) (если есть). diff --git a/agent-toolsets/README.md b/agent-toolsets/README.md new file mode 100644 index 0000000..bb4b54b --- /dev/null +++ b/agent-toolsets/README.md @@ -0,0 +1,73 @@ +# :agent-toolsets — `pw.binom.agentik.toolsets` + +**Ядро механики toolsets: `ToolsetRegistry`, `ToolsetDispatchPolicy`, +встроенные тулы `enable_toolset` / `disable_toolset`, `SyncLiteTool` базовый +класс.** +KMP, не зависит от `:standalone`, переиспользуем в Android и в любом другом +LiteTool-агенте. + +## Какую проблему решает + +В проде у агента может быть **сотня** инструментов (MCP-серверы, кастомные +тулы, встроенные операции). Слать их все в каждый LLM-запрос: + +1. **Раздувает контекст** — описание тула ~50–200 токенов × 100 тулов = 20K токенов + в system prompt без пользы. +2. **Увеличивает latency** — модель тратит время на выбор из длинного списка. +3. **Снижает качество** — модель путается между похожими названиями. + +Toolsets группируют тулы **по домену** (`filesystem`, `network`, `devops`, …). +Активированы только 2-3 одновременно. `enable_toolset("filesystem")` — +включает целую группу одним обращением; тулы появляются в system prompt + +регистрируются как вызываемые. `disable_toolset(...)` — убирает. + +## Архитектура + +```kotlin +interface Toolset { + val name: String // "filesystem" + val title: String // "File operations" + val enabled: Boolean // текущее состояние + suspend fun enabledTools(context: ToolsetContext): List + suspend fun systemPromptSection(context: ToolsetContext): String +} + +class ToolsetRegistry { + fun register(toolset: Toolset) + fun list(): List + suspend fun enable(name: String): Boolean + suspend fun disable(name: String): Boolean +} + +class ToolsetDispatchPolicy { + fun buildDispatch(): DispatchPolicy // подаётся в LiteLlm +} +``` + +Встроенные тулы — `EnableToolsetTool` / `DisableToolsetTool` / +`SystemPromptToolsetSection` — дают LLM самой управлять составом инструментов. +База для кастомных тулов — `SyncLiteTool` (обёртка над `LiteTool`, +синхронная `execute(args): String`). + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:agent-toolsets:$version") + // Транзитивно: :storage-core (для ToolsetContext) + :proto +} +``` + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :agent-toolsets:build +``` + +KMP-таргеты — полный набор. Зависимости — `:storage-core` + `:proto` + +`kotlinx-coroutines` + `kotlinx-serialization`. diff --git a/agentik-cli/README.md b/agentik-cli/README.md new file mode 100644 index 0000000..914b8be --- /dev/null +++ b/agentik-cli/README.md @@ -0,0 +1,76 @@ +# :agentik-cli — JVM CLI клиент к /agentik + +REPL-клиент к запущенному `:standalone`-серверу на базе `:client`. **JVM-only** +(JLine требует termios + `java.io.File`); для desktop-альтернативы — `:agentik-tui`. + +## Сборка + +```bash +./gradlew :agentik-cli:shadowJar +# Результат: agentik-cli/build/libs/agentik-cli-all.jar (~8 MB) +``` + +Также доступен через Maven Central Nexus (`caffeine` репо) — см. релизы: +`https://git.binom.pw/subochev/agentik/releases`. После публикации нового +тега jar появляется как `pw.binom.agentik:agentik-cli:VERSION` (artifact + classifier `all`). + +## Запуск + +```bash +# По умолчанию — http://localhost:8080/agentik +java -jar agentik-cli-all.jar + +# К другому серверу +java -jar agentik-cli-all.jar --server http://192.168.76.166:8080/agentik + +# Без восстановления последней беседы +java -jar agentik-cli-all.jar --no-history + +# В конкретной беседе +java -jar agentik-cli-all.jar --id +``` + +Альтернативно: `AGENTIK_SERVER` env-переменная с тем же эффектом, что и `--server`. + +## Slash-команды внутри REPL + +| Команда | Алиасы | Действие | +|---|---|---| +| `/help` | `/?` | список команд + клавиш | +| `/new ` | `/n` | создать новый диалог | +| `/list` | `/ls` | показать все диалоги | +| `/switch ` | `/sw ` | переключиться на диалог | +| `/rename ` | `/mv <title>` | переименовать текущий | +| `/delete <id?>` | `/rm <id?>` | удалить (без id — текущий) | +| `/interrupt` | `/stop`, `/cancel` | прервать активный ход | +| `/history` | `/h`, `/hist` | backfill истории через `getMessages` | +| `/pwd` | `/where` | показать текущий conversation id | +| `/exit` | `/quit` | выйти (Ctrl-D / Ctrl-C — то же) | + +Свободный текст = сообщение текущему диалогу; SSE-события (start_reasoning / +start_response / append_text / end / interrupted / error) стримятся в stdout +в реальном времени. + +## Переменные окружения + +| Переменная | Дефолт | Назначение | +|---|---|---| +| `AGENTIK_SERVER` | `http://localhost:8080/agentik` | URL HTTP-фасада `:server` | + +История сессий (id + last event timestamp) сохраняется в +`~/.agentik/cli-state.json` (атомарно через `tmp → rename`). + +## Особенности + +- **Arrow keys, history (↑/↓), Ctrl-D/Ctrl-C** — через JLine 3.30, история + readline в `~/.agentik/.inputrc`-стиле (через JLine `DefaultHistory`). +- **Reconnect-safe SSE** — если сервер рестартовал, клиент подхватывает с + `lastEventAt` через `events(after)`. +- **Snapshot-режим** — если подключились к диалогу впервые, `/history` + подгружает старые сообщения через `getMessages(after)` (offline-бэкфилл). + +## Тесты + +```bash +./gradlew :agentik-cli:jvmTest +``` diff --git a/agentik-tui/README.md b/agentik-tui/README.md new file mode 100644 index 0000000..0fed940 --- /dev/null +++ b/agentik-tui/README.md @@ -0,0 +1,106 @@ +# :agentik-tui — Compose-for-Mosaic TUI клиент к /agentik + +Compose-style TUI-клиент (KMP desktop, без iOS), рендерится в ANSI-терминал +через библиотеку [Mosaic](https://github.com/JakeWharton/mosaic) 0.18. Полная +клавиатурная навигация — никаких `:`-префиксов (как в vim). + +## Сборка + +```bash +./gradlew :agentik-tui:shadowJar +# Результат: agentik-tui/build/libs/agentik-tui-all.jar (~10 MB) +``` + +Также доступен через Nexus (`caffeine` репо) — `pw.binom.agentik:agentik-tui:VERSION`. + +## Запуск + +```bash +# По умолчанию — http://localhost:8080/agentik +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-tui-all.jar + +# К другому серверу +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-tui-all.jar --server http://192.168.76.166:8080/agentik + +# Без восстановления последней беседы +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-tui-all.jar --no-history + +# Конкретная беседа +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-tui-all.jar --id <conversation-uuid> +``` + +Альтернативно: `AGENTIK_SERVER` env-переменная. + +> **`--enable-native-access=ALL-UNNAMED`** обязателен: Mosaic использует +> нативные API терминала (`stdin`/`stdout` raw mode), что требует +> `--enable-native-access`. + +## Клавиши + +| Клавиша | Действие | +|---|---| +| **Tab** / **Shift-Tab** | переключение фокуса: input → history → sidebar → … | +| **Enter** | отправить набранное сообщение | +| **Backspace** / **Delete** | удалить символ | +| **←/→** / **Home/End** | курсор в input | +| **↑/↓** | scrollback (в фокусе на history) / input history (в фокусе на input) | +| **Esc** | очистить input | +| **F1** | показать/скрыть help overlay | +| **Ctrl-D** / **Ctrl-C** | прервать активный ход (повторное нажатие — выход) | + +`:`-префикс команд **отключён намеренно** — для отправки команд используются +клавиши. Если нужно сделать что-то нестандартное — переключитесь на `:agentik-cli` +(REPL с slash-командами). + +## Переменные окружения + +| Переменная | Дефолт | Назначение | +|---|---|---| +| `AGENTIK_SERVER` | `http://localhost:8080/agentik` | URL HTTP-фасада `:server` | + +История сессий: `~/.agentik/tui-state.json`. + +## Layout + +``` +┌────────────────────────────────────────────────────────────────┐ +│ agentik · agent-id · <conv-uuid> · focus=input │ ← header +├────────────────────────────────────────────────────────────────┤ +│ │ +│ [User 14:32] │ +│ привет │ +│ │ +│ [Bot 14:32] │ +│ привет! чем помочь? │ ← history (scroll) +│ ▍ │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ > | | │ ← input (cursor) +├────────────────────────────────────────────────────────────────┤ +│ Tab focus ↑↓ scroll Enter send Esc clear F1 help ⌃D exit │ ← footer +└────────────────────────────────────────────────────────────────┘ +``` + +## Что пока работает и что нет + +✅ header + history + input + footer +✅ focus cycling (Tab / Shift-Tab) +✅ input editing (chars, BS, Del, ←/→, Home/End, Enter) +✅ история input'а через ↑/↓ (в фокусе на input) +✅ scrollback history (в фокусе на history) +✅ F1 help overlay +✅ Ctrl-D / Ctrl-C interrupt + +⌛ mouse support (термиос SGR-mouse parsing) — для v3+. +⌛ split-pane (history left / input right) — пока input внизу, full-width. +⌛ `:agentik-cli`-slash-команды внутри TUI (history backfill / list / switch). + +## Тесты + +```bash +./gradlew :agentik-tui:jvmTest +``` diff --git a/build.gradle.kts b/build.gradle.kts index 6570c1b..03018d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,30 @@ val binomRepoUrl = (findProperty("binom.repo.url") as String? ?: "http://nexus.x val binomRepoUser = (findProperty("binom.repo.user") as String? ?: "").toString() val binomRepoPassword = (findProperty("binom.repo.password") as String? ?: "").toString() +// Per-module POM description. Один источник истины — карта ниже, +// лишнее в settings.gradle.kts держим в комментарии-зеркале. +// При добавлении нового модуля — добавь строку сюда + README.md в его корень. +// Кладём в rootProject.extra ДО apply KMP-плагина в subprojects (beforeEvaluate +// срабатывает позже, чем apply плагина, поэтому просто положить extra в +// beforeEvaluate — поздно). +val moduleDescriptions: Map<String, String> = mapOf( + "proto" to "agentik :proto — stateful KMP protocol (Agent/Conversation/Message/Event) replacing AG-UI; типы и контракт без сетевой логики.", + "skills" to "agentik :skills — парсер opencode-style SKILL.md / *.yaml (YAML-frontmatter + markdown body); загружается в system prompt.", + "server" to "agentik :server — Ktor-фасад, экспонирующий Agent по HTTP+JSON+SSE под путём /agentik.", + "client" to "agentik :client — Ktor-клиент (HTTP+JSON+SSE), превращающий /agentik в Agent/Conversation из :proto.", + "memory-api" to "agentik :memory-api — интерфейсы долговременной памяти (MemoryStore, MemoryCategory, MemoryNote).", + "memory-md" to "agentik :memory-md — Hermes-style реализация памяти поверх §-файлов (user/world/preference.md).", + "memory-vector" to "agentik :memory-vector — ANN+JVector+SQLite реализация памяти с эмбеддингами (HTTP/SIGLIP).", + "storage-core" to "agentik :storage-core — интерфейсы хранилища (MessageStore/WorkingMemoryStore/ConversationStore/ReflectionStore).", + "storage-inmemory" to "agentik :storage-inmemory — in-memory реализация всех сторов из :storage-core (для тестов и Android).", + "storage-sqlite" to "agentik :storage-sqlite — SQLDelight реализация всех сторов на SQLite (прод-бэкенд).", + "agent-toolsets" to "agentik :agent-toolsets — реестр инструментов + диспетчер тулов (enable_toolset/disable_toolset); переиспользуемое ядро.", + "agentik-cli" to "agentik :agentik-cli — JVM CLI-клиент (JLine) к /agentik: REPL + slash-команды + стрим SSE.", + "agentik-tui" to "agentik :agentik-tui — Compose-for-Mosaic TUI-клиент (desktop, без iOS) с клавиатурной навигацией без ':'-префиксов.", + "standalone" to "agentik :standalone — single-jar HTTP-сервер со всеми транспортами (AG-UI/A2A/:proto), SQLite, памятью, скилами и SOUL.", +) +rootProject.extra.set("moduleDescriptions", moduleDescriptions) + subprojects { group = rootProject.group @@ -41,7 +65,16 @@ subprojects { // 1) eagerly переопределяем version в rootProject.extra (см. выше) // 2) на КАЖДЫЙ subproject вешаем beforeEvaluate, который выставляет // version до того, как KMP-плагин начнёт создавать publications. + + // per-module POM description берётся из rootProject.extra["moduleDescriptions"] + // (см. корень build.gradle.kts); добавлять новый модуль — туда + README.md. + + // beforeEvaluate срабатывает ДО apply плагинов в build.gradle.kts модуля, так + // что version/description уже валидны, когда KMP-плагин начинает создавать + // publications. beforeEvaluate { + description = (rootProject.extra["moduleDescriptions"] as Map<String, String>)[project.name] + ?: "agentik module: ${project.name}" version = rootProject.extra["projectVersion"] as String } @@ -76,10 +109,8 @@ subprojects { pom { name = project.name - description = providers.provider { - project.findProperty("description")?.toString() - ?: "agentik: ${project.name} (pw.binom.agentik)" - } + description = (rootProject.extra["moduleDescriptions"] as? Map<String, String>)?.get(project.name) + ?: "agentik module: ${project.name}" url = "https://git.binom.pw/subochev/agentik" licenses { diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..df3a2e5 --- /dev/null +++ b/client/README.md @@ -0,0 +1,67 @@ +# :client — `pw.binom.agentik.client` + +**Ktor-клиент, превращающий HTTP-фасад `:server` обратно в `Agent`/`Conversation` из `:proto`.** +Подходит для JVM-приложений (CLI, desktop, integration-тесты). + +## Какую проблему решает + +После того, как `:server` выставляет агента по HTTP, встаёт задача: дать +вызывающей стороне **тот же интерфейс**, что был на сервере — а не отдельный +REST-клиент с хендшейкингом SSE, парсингом полей, ручной постраничной подгрузкой. +`:client` — обёртка: `AgentikAgent(baseUrl).createConversation()` возвращает +`Conversation`, идентичный серверному, а вызовы `send/getMessages/events` +прозрачно ездят по HTTP. + +## Использование + +```kotlin +import pw.binom.agentik.client.AgentikAgent + +val agent = AgentikAgent(id = "ops-bot", baseUrl = "http://localhost:8080/agentik") +val conv = agent.createConversation(temp = false) +conv.events(after = Clock.System.now()).collect { ev -> + when (ev) { + is Event.AppendText -> print(ev.body) + is Event.End -> println() + else -> Unit + } +} +conv.send(listOf(Content.Text("Привет. Сколько будет 2+2?"))) +// ... события стримятся в collect выше +conv.close() +``` + +Для фоновой подписки (reconnect-safe): + +```kotlin +// Долгая живая подписка на события диалога. +conv.events(after = lastSeen).collect { ev -> + if (ev is Event.End) lastSeen = ev.date +} +``` + +## Подключение + +```kotlin +implementation("pw.binom.agentik:client:$version") +// Транзитивно тянет :proto + ktor-client-core/cio/... + kotlinx-serialization. +``` + +## SSE-парсер + +Внутри — самописный парсер SSE (режет поток на `data:` строки, буферизует +частичные, переживает keep-alive-комментарии). Зависимости — `ktor-client-cio` +по умолчанию; если нужен другой engine — подмените через `AgentikAgent(engineFactory = …)`. + +## Где смотреть версии + +- `:client` синхронизирован с `:proto`/`server` — `version` из `gradle.properties` +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :client:build +``` + +JVM-only (ktor-client-engine-cio — JVM). diff --git a/memory-api/README.md b/memory-api/README.md new file mode 100644 index 0000000..90e4bad --- /dev/null +++ b/memory-api/README.md @@ -0,0 +1,77 @@ +# :memory-api — `pw.binom.agentik.memory` + +**Интерфейсы долговременной памяти агента: `MemoryStore`, `MemoryNote`, +`MemoryCategory`, `MemoryPrefetcher`, `MemoryReviewer`, `MemoryTools`.** +Без зависимостей от конкретного хранилища. + +## Какую проблему решает + +LLM не помнит между сессиями. Чтобы агент становился **умнее с каждым +диалогом**, нужна долговременная память: факты о пользователе, мире, +предпочтениях, плюс механизм извлечения (reviewer) и подмешивания (prefetcher) +в контекст. Бэкенды памяти бывают разные (md-файлы, векторный ANN, sqlite, +KV-store), и `:standalone` не должен быть привязан ни к одному из них. + +`:memory-api` определяет **контракт**: что умеет любая реализация памяти. +Конкретные бэкенды — `:memory-md` (Hermes-style §-файлы) и `:memory-vector` +(SQLite + JVector + эмбеддинги). + +## Что в контракте + +```kotlin +interface MemoryStore : AutoCloseable { + suspend fun upsert(note: MemoryNote) + suspend fun get(id: String): MemoryNote? + suspend fun list(category: MemoryCategory?, conversationId: String?, limit, offset): List<MemoryNote> + suspend fun search(query: MemorySearchQuery): List<MemorySearchResult> + suspend fun delete(id: String): Boolean + suspend fun markUsed(ids: List<String>) // бампит lastUsedAt + useCount + fun events(): Flow<MemoryStoreEvent> // опционально +} + +enum class MemoryCategory { USER, WORLD, PREFERENCE } + +data class MemoryNote( + val id: String, + val conversationId: String?, + val category: MemoryCategory, + val title: String, + val body: String, + val source: MemorySource, // AUTO_REVIEW / USER / MANUAL + val createdAt: Instant, + val lastUsedAt: Instant?, + val useCount: Int, +) +``` + +`MemoryPrefetcher` — компонент, который **перед** каждым user-ходом выбирает +релевантные заметки (через `search()`) и форматирует `[Memory context…]` блок +в начало user-сообщения. `MemoryReviewer` — компонент, который **после** +assistant-хода извлекает новые факты (через LiteLlm) и вызывает `upsert`. +`MemoryTools` — `memory_save` / `memory_recall` / `memory_list` / `memory_delete`, +которыми модель может пользоваться явно. + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:memory-api:$version") + // + выберите реализацию: + implementation("pw.binom.agentik:memory-md:$version") // md-файлы + // или + implementation("pw.binom.agentik:memory-vector:$version") // JVector+SQLite +} +``` + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :memory-api:build +``` + +KMP-таргеты — полный набор. Зависимостей нет (только `kotlinx-coroutines-core` для `Flow`). diff --git a/memory-md/README.md b/memory-md/README.md new file mode 100644 index 0000000..d7d14b3 --- /dev/null +++ b/memory-md/README.md @@ -0,0 +1,63 @@ +# :memory-md — `pw.binom.agentik.memory.md` + +**Hermes-style реализация долговременной памяти поверх обычных markdown-файлов.** +По одной §-секции на заметку, в файлах `user.md` / `world.md` / `preference.md`. +Без внешних зависимостей, без эмбеддингов, без БД. + +## Какую проблему решает + +Минимально работающая память **без инфраструктуры**: открыл текстовый редактор — +посмотрел, поправил, удалил. Версионируется в git вместе с проектом, бэкапится +как обычные файлы. Полезно для дев-окружения, для одиночных пользователей, +для отладки vector-бэкенда. + +## Формат файла + +```markdown +# user.md + +# agentik-note id=a3f1e2b7 created=2026-09-01 uses=3 last=2026-09-12 +Пользователь предпочитает короткие ответы без эмодзи. Не любит вим. + +# agentik-note id=b9c4d8e1 created=2026-09-10 uses=1 last=2026-09-12 +Дедлайн релиза agentik — 25 сентября. + +# preference.md + +# agentik-note id=c1d2e3f4 created=2026-09-08 uses=0 last=never +Отвечать по-русски, без жаргона. +``` + +Заголовок с метаданными (`# agentik-note id=… created=… uses=… last=…`), пустая +строка, markdown-body. Curator (фоновая корутина) переименовывает «протухшие» +заметки (`useCount=0` и `last > 90 дней назад`) в `.archived.{ts}`. + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:memory-md:$version") + // транзитивно: :memory-api + kaml (для YAML-сериализации метаданных) +} + +// Использование +val store = MemoryMdStore.fromDirectory( + dir = Path("~/.agentik/memory"), + clock = Clock.System, +) +val search = store.search(MemorySearchQuery(query = "любимый редактор", limit = 5)) +``` + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :memory-md:build +``` + +KMP-таргеты — полный набор. Зависимости — `:memory-api` + `kaml` (YAML-парсинг +метаданных). diff --git a/memory-vector/README.md b/memory-vector/README.md new file mode 100644 index 0000000..df4e4a0 --- /dev/null +++ b/memory-vector/README.md @@ -0,0 +1,72 @@ +# :memory-vector — `pw.binom.agentik.memory.vector` + +**Реализация долговременной памяти поверх JVector (ANN-индекс) + SQLite (метаданные) + эмбеддингов.** +JVM-only (JVector не публикует KMP-таргеты; на Android ART работает через Java 11 +scalar fallback). + +## Какую проблему решает + +`:memory-md` хорош для малых объёмов и дев-окружения, но при тысячах заметок +keyword-overlap поиск не справляется. Vector-бэкенд считает **эмбеддинги** +заметок, складывает в JVector ANN-индекс, ищет по cosine similarity. +`recency-re-rank` подмешивает свежесть, чтобы новые факты не тонули в старых. + +## Архитектура + +``` + ┌───────────────────────┐ + user-query ─►│ EmbeddingProvider │ (HTTP /v1/embeddings или SIGLIP2 on-device) + └─────────┬─────────────┘ + ▼ + ┌───────────────────────┐ + │ MemoryVectorStore │ + │ ├─ JVector (cosine) │ ◄── ANN-search + │ └─ SQLite (мета) │ ◄── заметки + lastUsedAt + useCount + └───────────────────────┘ +``` + +Бэкенды эмбеддингов (через `AGENTIK_EMBEDDING_BACKEND`): + +- **`HTTP`** — POST на `${OPENAI_BASE_URL}/v1/embeddings`. Семантический поиск + через OpenAI-совместимый endpoint (vLLM, OpenAI, LiteLLM-proxy). + Кэширование LRU(256) на уровне `EmbeddingHttpProvider` для дедупликации. +- **`SIGLIP`** — on-device SigLIP2 через ONNX Runtime (768-мерный вектор). + Никаких внешних вызовов; модель и токенизатор должны лежать на диске. + +## Подключение + +```kotlin +plugins { kotlin("jvm") } + +dependencies { + implementation("pw.binom.agentik:memory-vector:$version") + // Транзитивно: :memory-api + jvector + sqldelight + text-embedding-kmp +} +``` + +`:standalone` инициализирует бэкенд автоматически по `AGENTIK_MEMORY_BACKEND=vector` ++ `AGENTIK_EMBEDDING_BACKEND=…`. + +## Размерности + +| Backend | Модель | Dim | +|---|---|---| +| `HTTP` (OpenAI) | `text-embedding-3-small` (default) | 1536 | +| `HTTP` (OpenAI) | `text-embedding-3-large` | 3072 | +| `SIGLIP` | SigLIP2-base | 768 (фиксировано) | + +Для `HTTP` размерность управляется через `AGENTIK_EMBEDDING_DIMENSION`; для +`SIGLIP` — определяется автоматически. + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :memory-vector:build +``` + +JVM-only. Тянет `pw.binom.agentik:memory-api` и `com.github.jvector:jvector:3.0.6`. diff --git a/proto/README.md b/proto/README.md new file mode 100644 index 0000000..158a37a --- /dev/null +++ b/proto/README.md @@ -0,0 +1,94 @@ +# :proto — `pw.binom.agentik.proto` + +**Stateful KMP-протокол взаимодействия клиента с агентом.** +Замена AG-UI в проектах, где агенту нужно **самому владеть** историей диалога и +контекстным окном (компакция, рефлексия, выбор инструментов) — клиент только +стримит сообщения и рисует события. + +## Какую проблему решает + +AG-UI требует, чтобы **клиент** слал полный `messages[]` на каждый ход, а агент +оставался stateless. Это удобно для UI-чатов, но ломается, когда: + +- у агента есть долговременная память (md/vector), и контекст должен + автоматически сжиматься / дополняться перед отправкой в LLM; +- у одного пользователя десятки активных диалогов и нельзя каждый раз + пересылать 100K токенов; +- агент сам планирует вызовы инструментов и управляет KV-cache модели. + +`:proto` переворачивает ответственность: **агент** владеет `Conversation.messages()`, +`send(content)` отправляет только новый message, а `events(after): Flow<Event>` +стримит live-события с `Instant`-таймстампом для отслеживания прогресса. + +## Контракт + +```kotlin +interface Agent { + val id: String + suspend fun createConversation(temp: Boolean = false): Conversation + suspend fun getConversation(id: String): Conversation? + suspend fun getConversations(offset: Int = 0, limit: Int = PAGE_SIZE): List<Conversation> + fun getConversations(offset: Int = 0): Flow<Conversation> // cold-flow по страницам + fun events(after: Instant): Flow<AgentEvent> // live-уведомления о диалогах +} + +interface Conversation : AutoCloseable { + val id: String + val title: String? + val updatedAt: Instant + val isSupportImageInput: Boolean + val isSupportImageOutput: Boolean + suspend fun send(content: List<Content>): Unit // write-only, не блокирует + fun events(after: Instant): Flow<Event> // live read (НЕ replay!) + suspend fun getMessages(offset: Int, limit: Int = PAGE_SIZE): List<Message> + fun getMessages(offset: Int = 0): Flow<Message> // cold-flow по страницам + suspend fun rename(title: String): Boolean + suspend fun interrupt(): Unit + fun close() // освобождает ресурсы +} +``` + +Иерархии: + +- `Content` — `Text` / `Image(data: ByteArray, mime: String)` +- `Message` — `UserMessage` / `AssistantMessage` (оба `Body`) + `ToolCall(id, name, args)` / `ToolResult(id, result)` / `System` для метаданных +- `Event` — `StartReasoning` / `StartResponse(type)` / `AppendText` / `AppendImage` / `End` / `Interrupted` / `Error` +- `AgentEvent` — `Created(id)` / `Deleted(id)` / `Renamed(id, title)` + +Live-стримы (`events(after)`) **не реплеят** прошедшие события — клиент должен +сам вызвать `getMessages(after)` для бэкфилла, либо подписаться на `events(after=now)` +и начать рисовать с настоящего момента. + +## Подключение + +Артефакт — `pw.binom.agentik:proto:VERSION`. + +```kotlin +// commonMain +implementation("pw.binom.agentik:proto:$version") +// JVM-only +implementation("pw.binom.agentik:proto-jvm:$version") +// Любой KMP-таргет +implementation("pw.binom.agentik:proto-macosArm64:$version") +``` + +`version` синхронизируется с `gradle.properties` (`version=0.1.0`) и +прокидывается через `-Pversion=...` в CI (`binom.repo.*` для Nexus). + +## Где смотреть версии + +- текущая разрабатываемая: `gradle.properties` → `version=0.1.0` +- история релизов: `https://git.binom.pw/subochev/agentik/releases` +- опубликованные артефакты: Nexus-репозиторий `caffeine` + (`http://nexus.xx/repository/caffeine/pw/binom/agentik/proto/`) + +## Сборка / тесты + +```bash +./gradlew :proto:build # все KMP-таргеты + тесты +./gradlew :proto:jvmTest # только JVM-тесты +./gradlew :proto:publishToMavenLocal # для локального потребления +``` + +KMP-таргеты: `jvm + macosX64 + macosArm64 + iosX64 + iosArm64 + iosSimulatorArm64 + linuxX64 + linuxArm64 + mingwX64`. +Зависимостей минимум: `kotlinx-coroutines-core:1.11.0` + `kotlinx-datetime:0.8.0` (оба `api`). diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..65f1615 --- /dev/null +++ b/server/README.md @@ -0,0 +1,66 @@ +# :server — `pw.binom.agentik.server` + +**Ktor-маршруты, превращающие `pw.binom.agentik.proto.Agent` в HTTP+JSON+SSE фасад.** +Подключается к любому `Application` через `Route.agentikAgent(...)`, монтируется +под заданным `path` (по умолчанию `/agentik`). + +## Какую проблему решает + +`:proto` — это чистый Kotlin-контракт (Agent/Conversation). Чтобы по нему +говорить с удалённым сервером нужен мост. `:server` — этот мост, но **только +маршруты**: без привязки к конкретному engine (CIO/Netty), без агрегации +прочих протоколов (AG-UI/A2A), без инициализации БД/памяти. Один extension-метод +на `Route`, и ваш Agent доступен по HTTP. + +## Что отдаёт + +| Метод | Путь | Назначение | +|---|---|---| +| `GET` | `/health` (через `:standalone`) | liveness | +| `POST` | `/agentik/conversations` | создать диалог | +| `GET` | `/agentik/conversations` | список диалогов (постраничный) | +| `GET` | `/agentik/conversations/{id}` | конкретный диалог | +| `POST` | `/agentik/conversations/{id}/rename` | переименовать | +| `DELETE` | `/agentik/conversations/{id}` | удалить | +| `POST` | `/agentik/conversations/{id}/messages` | отправить сообщение | +| `GET` | `/agentik/conversations/{id}/events` | SSE-стрим событий | +| `GET` | `/agentik/conversations/{id}/messages` | backfill истории (с `after`) | + +Wire-форма — `kotlinx.serialization` JSON со `snake_case`-дискриминаторами +(`"kind":"start_response"`, `"kind":"user_message"`, и т.п.). `Instant` +сериализуется ISO-8601 строкой (`agentikJson` в `Serialization.kt`). + +## Подключение + +```kotlin +// your-application/build.gradle.kts +implementation("pw.binom.agentik:server:$version") +implementation("io.ktor:ktor-server-core:2.3.x") // или любой совместимый +implementation("io.ktor:ktor-server-cio:2.3.x") // engine — ваш выбор + +// ваш код +import pw.binom.agentik.server.agentikAgent + +fun Application.module() { + install(ContentNegotiation) { json(agentikJson) } + routing { + agentikAgent(agent = myAgent) // mount на /agentik + agentikAgent(agent = myAgent, path = "/v1/agent") // или под другим путём + } +} +``` + +## Где смотреть версии + +- `pw.binom.agentik:proto` (см. `:proto/README.md`) +- `pw.binom.agentik:server` — `version` берётся из `gradle.properties` (`version=0.1.0`) +- история релизов: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :server:build +./gradlew :server:jvmTest +``` + +KMP-таргеты: те же, что у `:proto` (jvm + 8 нативов). diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..48be993 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,77 @@ +# :skills — `pw.binom.agentik.skills` + +**Парсер opencode-style скилов: `SKILL.md` или `*.yaml` с YAML-frontmatter + markdown body.** +Загружается в system prompt как отдельная секция; модели доступны тулы +`read_skill` / `skill_save` / `skill_delete` (если они подключены через +`:agent-toolsets`). + +## Какую проблему решает + +Агенту нужно **знать**, какие процедуры/инструкции у него есть, не таская их +в коде. Скил — это: + +```markdown +--- +name: backend:spring:db-base +description: Правила JPA/Flyway миграций и репозиториев в agentik. +--- +Правила, которые применяются ко всем изменениям схемы: +- миграции только в Flyway; +- новые колонки nullable by default; +- ... +``` + +Парсер читает такие файлы из каталога (рекурсивно), валидирует обязательные +поля (`name`, `description`), собирает каталог для system prompt и **поддерживает +горячее обновление**: добавил файл — доступен в следующем `read_skill` без +рестарта. + +## Формат + +Поддерживаются оба варианта: + +- **`SKILL.md`** — единый файл в каталоге (с frontmatter + body). +- **`*.yaml`** + опциональный `*.md`-компаньон с тем же basename. + +Frontmatter — YAML, минимум `name` (с `:` для вложенности) и `description`. +Остальные поля — пользовательские, доступны через `SkillFile.frontmatter`. + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:skills:$version") + implementation("com.charleskorn.kaml:kaml:0.55.0") +} +``` + +## Использование + +```kotlin +import pw.binom.agentik.skills.SkillCatalog + +val catalog = SkillCatalog.fromDirectory(Path("/etc/agentik/skills")) +catalog.listAll().forEach { skill -> + println("- ${skill.name}: ${skill.description}") +} + +val skill = catalog.findByName("backend:spring:db-base") +val body = skill?.body +``` + +`SkillPrompt` умеет отрендерить каталог в markdown-секцию для system prompt +(с truncate по длине, чтобы не раздувать контекст). + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :skills:build +./gradlew :skills:jvmTest +``` + +KMP-таргеты — полный набор как у `:proto`. Зависимости — только `kotlinx-serialization` + `kaml`. diff --git a/standalone/README.md b/standalone/README.md index bd58840..1af8bb9 100644 --- a/standalone/README.md +++ b/standalone/README.md @@ -1,8 +1,9 @@ # :standalone — agentik single-jar server -Self-contained HTTP-сервер с Ktor: AG-UI / A2A / :proto транспорты на одном порту, +Self-contained HTTP-сервер на Ktor: AG-UI / A2A / :proto транспорты на одном порту, встроенный SQLite для истории диалогов, долговременная память (Hermes-style -§-файлы), загрузка MCP-инструментов, навыков (SKILL.md) и персоны (SOUL.md). +§-файлы или vector+JVector), загрузка MCP-инструментов, навыков (SKILL.md) и +персоны (SOUL.md). ## Сборка @@ -10,13 +11,17 @@ Self-contained HTTP-сервер с Ktor: AG-UI / A2A / :proto транспор # Полная сборка всего проекта + fatjar ./gradlew assemble -# Только fatjar :standalone (≈ 150 MB) +# Только fatjar :standalone (≈ 250 MB) ./gradlew :standalone:shadowJar # Результат: # standalone/build/libs/standalone-all.jar ``` +Также публикуется в Nexus (`caffeine` репо) при создании релиза: +`pw.binom.agentik:standalone:VERSION` с classifier `all` (см. +`https://git.binom.pw/subochev/agentik/releases`). + ## Запуск ```bash @@ -31,17 +36,22 @@ java -jar standalone/build/libs/standalone-all.jar - `GET /agentik/conversations/{id}/events` — SSE-стрим ответов - `POST /a2a/` — A2A JSON-RPC (`message/send`, `tasks/get`, `tasks/cancel`) - `GET /a2a/.well-known/agent-card.json` — AgentCard +- `POST /agui` — AG-UI (compatibility transport, legacy) -### A2A +### Подкоманды -Адаптер `A2aBridge` гоняет A2A-контекст на диалог :proto: `contextId` мапится на -`Conversation` (пустой/неизвестный `contextId` → новый диалог). Ответ — склеенный -текст хода; id внутреннего диалога возвращается в `metadata.agentikConversationId` -ответа. Задачи живут в in-memory `TaskStore` (не переживают рестарт процесса). +```bash +# Скачать модель Google LiteRT-LM в AGENTIK_GOOGLE_MODEL_PATH. +# Требует AGENTIK_LLM_BACKEND=google. Если файл уже есть — no-op. +java -jar standalone-all.jar pull-model + +# Сервер (по умолчанию) +java -jar standalone-all.jar +``` ## Переменные окружения -Все переменные читаются `AgentikConfig.fromEnv()`. Бланк или отсутствие → дефолт. +Все переменные читаются `AgentikConfig.fromEnv()`. Пусто или отсутствие → дефолт. | Переменная | Дефолт | Назначение | |---|---|---| @@ -67,6 +77,7 @@ java -jar standalone/build/libs/standalone-all.jar | `AGENTIK_SKILL_MINING_INTERVAL` | `15` | Skill mining: через сколько user-ходов запускать фоновый прогон SkillMiner. `0` = выключено. | | `AGENTIK_SKILL_MINING_MAX_TURNS` | `30` | Сколько последних ходов передавать SkillMiner'у за один прогон. | | `AGENTIK_DEBUG_ENDPOINTS` | `0` | `1` включает debug-эндпоинты (`/debug/reflect`, `/debug/skill-mine`, `/debug/curate`, `/debug/compact`, `/debug/tokens`) | +| `AGENTIK_AUTO_DOWNLOAD_MODEL` | `0` | `1` — при старте скачать LiteRT-LM модель в `AGENTIK_GOOGLE_MODEL_PATH` если её там нет (через `pull-model` логику). | ### OpenAI backend @@ -81,351 +92,8 @@ java -jar standalone/build/libs/standalone-all.jar | Переменная | Обязательна | Назначение | |---|---|---| | `AGENTIK_GOOGLE_MODEL_PATH` | да | Путь к `.litertlm` файлу | - -## SQLite: путь к базе диалогов - -`AGENTIK_DB_PATH` указывает на файл SQLite, в котором хранятся таблицы -`conversations`, `messages`, `working_memory`. SQLDelight-драйвер создаёт -файл при первом запуске. Если в пути есть несуществующие директории — их -нужно создать заранее (`mkdir -p`). - -```bash -# Абсолютный путь -AGENTIK_DB_PATH=/var/lib/agentik/state.db - -# Относительный путь — резолвится от CWD -cd /opt/agentik && AGENTIK_DB_PATH=./data/state.db - -# Временная база (на RAM, теряется при рестарте) — не поддерживается напрямую, -# но можно подменить в коде через SqliteStores.inMemory(). -``` - -Файл базы — обычный SQLite, можно инспектировать `sqlite3` CLI или Adminer. - -## Контекст инициации сообщения (MessageContext) - -Каждое user-сообщение может нести **контекст инициации хода** — -кто/что его вызвало. Для обычного user-сообщения поле `context` опускается -(обратная совместимость: старые клиенты, шлющие голый массив Content, -работают как раньше). Для cron/webhook/system событий `context` обязательно. - -```json -// POST /agentik/conversations/{id}/messages — новый формат -{ - "content": [{"type": "text", "body": "wake up"}], - "context": { - "origin": "event", - "description": "scheduled cron morning-briefing", - "sourceId": "cron-42", - "metadata": { "scheduledAt": "2026-09-14T08:00:00Z" } - } -} - -// Старый формат (всё ещё работает) — голый массив -[{"type": "text", "body": "hello"}] -``` - -| `origin` | Когда использовать | Префикс в LLM | -|------------|---------------------------------------------------|--------------| -| `user` | Обычное сообщение из чата (дефолт) | нет | -| `system` | Программное сообщение (старт агента, режим обслуживания) | `[SYSTEM] description (sourceId=…)` | -| `event` | Cron, webhook, file-changed, внешний триггер | `[EVENT] description (sourceId=…)` | - -**Семантика:** - -- `origin` — кто/что инициировал ход. `user` = человек в чате (UI/IRC/HTTP). -- `description` — короткая человекочитаемая фраза для модели - (обязательна для `system`/`event`). -- `sourceId` — id cron-job'а / webhook endpoint'а / IRC-канала, помогает - в логах и при ручном разборе. -- `metadata` — произвольный JSON, никогда не попадает в LLM-нагрузку - (только в audit log для пост-аналитики). - -**Что происходит при не-USER origin'е:** - -В working memory текст user-сообщения предваряется префиксом — -например, `[EVENT] scheduled cron morning-briefing (sourceId=cron-42)\n…`. -Модель видит, что её разбудил не пользователь, и может реагировать -иначе (например, не начинать диалог с приветствия). Префикс добавляется -**только к LLM-нагрузке**, в audit log и при `getMessages` возвращается -оригинальный текст + `context` отдельно. - -## Сжатие рабочего контекста (compaction) - -Когда диалог становится длинным, working memory диалога может превысить -контекстное окно модели. Чтобы этого не случилось, агент умеет **сжимать** -старые ходы в один синтетический `Summary`-блок. - -**Включается только при заданном лимите.** Никакого автодетекта по имени -модели — если лимит не задан, агент не сжимает. - -```bash -# OpenAI-совместимый бэкенд -export OPENAI_CONTEXT_WINDOW=128000 - -# Или Google / LiteRT -export AGENTIK_GOOGLE_CONTEXT_WINDOW=32000 -``` - -`AGENTIK_COMPRESSION_THRESHOLD` — доля лимита, при которой запускается -compaction (дефолт `0.8` = 80%): - -```bash -export AGENTIK_COMPRESSION_THRESHOLD=0.7 # сжимаем раньше -``` - -**Что происходит при compaction:** - -1. Перед `send()` оценивается количество токенов в системном промпте + history - + tools (грубая оценка `chars / 4`). -2. Если `estimated / contextWindow ≥ threshold` — асинхронный шаг: - - Старые ходы (User/Assistant, кроме последних 4) скармливаются в - `LiteLlmContextCompactor` — отдельный one-shot LLM-вызов с промптом - «Goal / Active / Resolved / Blocked / Remaining». - - Параллельно `MemoryReviewer.reviewPreCompaction` извлекает из старых - ходов факты и кладёт их в долговременную память (триггер `MemoryStore`). - - Атомарный `working_memory.compact(fromIdx, summary)` — старые строки - удаляются, на их место вставляется одна `Summary` запись. - - `LiteConversation` пересоздаётся с обновлённым контекстом. - -Если после compaction оценка всё ещё выше порога — выводится warning, но -нового compaction не запускается (защита от зацикливания). Решение — -поднять `OPENAI_CONTEXT_WINDOW` или понизить threshold. - -## Self-reflection (Hermes-style «слабые места») - -Каждые `AGENTIK_REFLECTION_INTERVAL` пользовательских ходов (default 10) -запускается фоновая one-shot LLM-размышление: «оцени последние ходы, -поставь score 1..5, выдели слабые места». Результат сохраняется в таблицу -`reflection` SQLite и подмешивается в system prompt следующего хода как -«Твои слабые места за последнее время». - -Включено когда `AGENTIK_REFLECTION_INTERVAL > 0`. Требует LiteLlm -(on-device или OpenAI — что указан в `AGENTIK_LLM_BACKEND`). На каждый -reflection — один LiteLlm вызов (~1-3 сек для on-device, ~200-500мс для -OpenAI). Это происходит в фоне (`Dispatchers.IO`), основной диалог не -блокируется. - -Топ-K последних рефлексий загружается в `buildSystemPrompt` и выводится -как `## Self-reflection: твои слабые места за последнее время`. Агент -видит их в каждом следующем ходе и (теоретически) должен избегать -повторения. Используется как cheap "auto-improving prompt feedback" -без ручного переписывания system prompt. - -## Учёт токенов (token accounting) - -Каждый assistant-ход после LiteLlm.send помечает assistant-запись `TurnTokens(input, output)`: - -- **`input`** — снимок `LiteConversation.tokenCount()` **до** первого send в turn'е - (system + вся история + tools + только что добавленное user-сообщение). -- **`output`** — дельта после завершения turn'а (assistant text + tool calls + - tool results, всё что LiteConversation добавила за весь tool loop). -- Хранится в `payload_json` assistant-сообщения (без schema-миграций). Бэкенды - без `tokenCount()` (off-line модели LiteRT-LM счётчик не отдают) дают `tokens=null`. - -На старте агент печатает сводку по всем существующим диалогам: - -``` -tokens: 17 convs, 134 turns, in=523844, out=58290, total=582134 -``` - -`MessageStore.tokenStats(conversationId)` отдаёт `TokenStats(turns, inputTokens, outputTokens)` -для одного диалога — можно использовать из HTTP фасада или клиентских дашбордов -для оценки cost. - -## Куратор памяти (Curator) - -Фоновая корутина (запускается автоматически, если `AGENTIK_MEMORY_DIR != off`): -раз в сутки архивирует заметки, которые **не выдавались в prefetch дольше 90 -дней** и **имеют `useCount == 0`**. Семантика архивации зависит от бэкенда — -`:memory-md` переименовывает §-файл в `.archived.{ts}`, `:memory-vector` -удаляет из SQLite и JVector. - -Параметры пока захардкожены в `Curator.DEFAULT_INTERVAL` и -`Curator.DEFAULT_MAX_AGE` (1 день и 90 дней); для override нужен новый -config-флаг. На каждом проходе выводится `[Curator] archived N stale notes`, -если N > 0. - -## Память - -`AGENTIK_MEMORY_DIR` указывает на каталог, в котором лежат три §-файла: -`user.md`, `world.md`, `preference.md` (по одному на категорию из `MemoryCategory`). -Формат файла — Hermes-style: заголовок с метаданными (` id=… created=… uses=…`), -пустая строка, markdown-тело заметки. - -```bash -# Дефолт (если env не задан) -~/.agentik/memory/{user,world,preference}.md - -# Явный путь -AGENTIK_MEMORY_DIR=/data/agentik/memory - -# Полностью выключить память (тулы memory_* не регистрируются, prefetch off) -AGENTIK_MEMORY_DIR=off -``` - -При включённой памяти агенту доступны четыре тула: `memory_save`, -`memory_read`, `memory_list`, `memory_delete`. Перед каждым ходом агент -прогоняет текст пользователя через `MemoryPrefetcher` и клеит `[Memory -context…]` блок в начало user-сообщения; после записи assistant-сообщения в -фоне запускается `MemoryReviewer.review(turn)` — извлечённые факты -записываются в store с `source = AUTO_REVIEW`. - -### Бэкенд: md vs vector - -`AGENTIK_MEMORY_BACKEND` выбирает хранилище. Дефолт — `md` (Hermes-style -§-файлы, keyword overlap, без внешних вызовов). - -`vector` — SQLite (`AGENTIK_DB_PATH`) + JVector ANN + эмбеддинги. Два -бэкенда эмбеддингов через `AGENTIK_EMBEDDING_BACKEND`: - -- **`HTTP` (default)** — POST на `${OPENAI_BASE_URL}/v1/embeddings`. Семантический - поиск: cosine similarity + recency-re-rank. -- **`SIGLIP`** — on-device SigLIP2 через ONNX Runtime (text-embedding-kmp, - 768-мерный вектор). Никаких внешних вызовов: модель и токенизатор должны - лежать на диске. Размерность определяется автоматически (768). - -```bash -# Vector-бэкенд + HTTP-эмбеддинги (default) -AGENTIK_MEMORY_BACKEND=vector \ -AGENTIK_EMBEDDING_BACKEND=http \ -AGENTIK_EMBEDDING_MODEL=text-embedding-3-small \ -AGENTIK_EMBEDDING_DIMENSION=1536 \ - java -jar standalone-all.jar - -# Vector-бэкенд + on-device SigLIP2 (без сети) -AGENTIK_MEMORY_BACKEND=vector \ -AGENTIK_EMBEDDING_BACKEND=siglip \ -AGENTIK_EMBEDDING_MODEL_PATH=/path/to/text_model_int8.onnx \ -AGENTIK_EMBEDDING_TOKENIZER_PATH=/path/to/tokenizer.model \ - java -jar standalone-all.jar -``` - -Для HTTP-эмбеддингов требуется `AGENTIK_LLM_BACKEND=openai` (т.к. нужен -OpenAI-совместимый `/v1/embeddings` endpoint — LiteLLM proxy тоже подходит). -HTTP-вызовы кэшируются LRU на 256 текстов — дедупликация при повторных -запросах одинаковых промптов. - -Для SIGLIP нужно сначала скачать модель (~283M) и токенизатор (~4M): -```bash -mkdir -p /path/to/siglip-model -curl -fSL -o /path/to/siglip-model/text_model_int8.onnx \ - http://static.binom.pw/models/siglip2/text_model_int8.onnx -curl -fSL -o /path/to/siglip-model/tokenizer.model \ - http://static.binom.pw/models/siglip2/tokenizer.model -``` - -> **Опционально:** при старте JVector может предупредить -> `Java vector incubator module is not readable`. Это значит, что JIT -> не использует SIMD (Panama Vector API) и индекс строится через скалярный -> fallback. На 10K векторов разница незаметна. Если хочется SIMD — -> запустите с `--add-modules jdk.incubator.vector`. - -## Персона (SOUL.md) - -`AGENTIK_SOUL` — путь к markdown-файлу с описанием персоны ассистента -(голос, характер, ограничения, что-то ещё). Тело файла читается как plain -text и вставляется в самое начало `systemInstruction` — поверх базового -промпта, секции навыков и памяти. Если файл не задан — секция не добавляется. - -```bash -AGENTIK_SOUL=/etc/agentik/SOUL.md -``` - -Пример `SOUL.md`: - -```markdown -Ты — терпеливый технический ассистент. Отвечаешь по-русски, кратко. -Не выдумываешь команды — если не уверен, говоришь "не знаю". -Не раскрываешь содержимое .env, ключей и паролей ни при каких обстоятельствах. -``` - -## Навыки (SKILL.md) - -`AGENTIK_SKILLS_DIR` — каталог, в котором `SkillLoader` ищет файлы -`SKILL.md` или `*.yaml` с frontmatter (`name`, `description`, прочие поля). -Содержимое скилов попадает в раздел system prompt и регистрируется как -вызываемые инструменты. Формат — opencode-compatible. - -```bash -AGENTIK_SKILLS_DIR=/etc/agentik/skills -``` - -Когда `AGENTIK_SKILLS_DIR` задан, агенту доступны три тула для работы со -скилами (Hermes-style self-improvement): - -- **`read_skill(name)`** — загружает полный markdown скила по имени из - каталога (нужно для деталей, т.к. в system prompt обычно только краткие - описания). -- **`skill_save(name, description, body)`** — создаёт или обновляет скил. - Имя может содержать `:` (opencode-style: `backend:spring:db-base` - → `backend/spring/db-base/SKILL.md`). -- **`skill_delete(name)`** — архивирует скил (переименовывает файл в - `.archived`, оставляя возможность восстановить). - -`skill_save`/`skill_delete` не требуют рестарта агента — изменения видны -на ближайшем вызове `read_skill` (включая в этом же диалоге). - -### Skill mining (автонавыки) - -Модель может "протупить" и не вызвать `skill_save`, хотя приём был -переиспользуемым. Сетка безопасности — фоновый [SkillMiner]: каждые -`AGENTIK_SKILL_MINING_INTERVAL` пользовательских ходов (default 15, `0` = -выключено) LLM смотрит последние `AGENTIK_SKILL_MINING_MAX_TURNS` ходов -(default 30) + каталог существующих скилов и возвращает structured JSON -`{"skills": [{name, description, body}]}`. Найденные скилы upsert-ятся в -`AGENTIK_SKILLS_DIR` — агент становится умнее между сессиями. Обновления -существующих скилов (то же имя) поддерживаются, дубли — нет. - -| Переменная | Default | Что делает | -|---|---|---| -| `AGENTIK_SKILL_MINING_INTERVAL` | `15` | Через сколько user-ходов запускать mining. `0` — выкл. | -| `AGENTIK_SKILL_MINING_MAX_TURNS` | `30` | Сколько последних ходов показывать минеру | - -### Debug-эндпоинты - -`AGENTIK_DEBUG_ENDPOINTS=1` включает эндпоинты для ручного триггерирования -фоновых фич (не ждать интервалов). Только локальная отладка: без -авторизации, в проде не включать. - -| Эндпоинт | Действие | -|---|---| -| `POST /debug/reflect?conversationId=...` | прогон LlmReflector прямо сейчас, результат в БД | -| `POST /debug/skill-mine?conversationId=...` | прогон SkillMiner прямо сейчас, найденное в `AGENTIK_SKILLS_DIR` | -| `POST /debug/curate` | прогон Curator.runPass (архивация stale-заметок памяти) | -| `POST /debug/compact?conversationId=...` | принудительный compaction working memory диалога | -| `GET /debug/tokens?conversationId=...` | token-статистика диалога из БД (turns/in/out/total) | - -Каждый возвращает JSON с результатом (что сохранил/нашёл/сжал), чтобы было -видно не только "триггер сработал", а что именно LLM намайнила. - -```bash -AGENTIK_SKILLS_DIR=/etc/agentik/skills -AGENTIK_DEBUG_ENDPOINTS=1 -``` - - -## MCP-инструменты - -`AGENTIK_MCP_CONFIG` — путь к JSON-файлу со списком MCP-серверов -(формат `mcpServers: { name: { command, args | url, headers } }`). При -запуске `McpRegistry.fromConfig` стартует stdio-серверы и подключается к -HTTP-серверам, инструменты автоматически становятся доступны агенту. - -```bash -AGENTIK_MCP_CONFIG=/etc/agentik/mcp.json -``` - -Пример `mcp.json`: - -```json -{ - "mcpServers": { - "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }, - "playwright": { "url": "https://mcp.example.com", "headers": {"Authorization":"Bearer …"} } - } -} -``` +| `AGENTIK_GOOGLE_MODEL_URL` | нет | URL для скачивания (default: `https://static.binom.pw/models/gemma-4-E2B-it.litertlm`) | +| `AGENTIK_GOOGLE_MODEL_SHA256_URL` | нет | URL с эталонным SHA-256 (если задано — файл проверяется) | ## Полный пример запуска @@ -474,6 +142,11 @@ agentik standalone listening on http://localhost:8080 LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы получают SIGTERM). +## Клиенты к :standalone + +- `:agentik-cli` — REPL со slash-командами (`/new`, `/list`, `/interrupt` …). +- `:agentik-tui` — Compose-style TUI, чисто клавиатурная навигация. + ## Тесты ```bash diff --git a/storage-core/README.md b/storage-core/README.md new file mode 100644 index 0000000..f85862d --- /dev/null +++ b/storage-core/README.md @@ -0,0 +1,77 @@ +# :storage-core — `pw.binom.agentik.storage` + +**Интерфейсы хранилища разговорной истории агента: `ConversationStore`, +`MessageStore`, `WorkingMemoryStore`, `ReflectionStore`.** +Без зависимостей от конкретной БД. + +## Какую проблему решает + +`:standalone` нужно сохранять диалоги между перезапусками, держать working +memory (сжатую историю, которую видит LLM), reflection-записи. Но привязываться +к конкретной БД (SQLite) в контракте нельзя — для Android подходит другая +история, для тестов — in-memory, для экспериментов с Postgres — третья. + +`:storage-core` отделяет **что хранить** (контракт) от **где хранить** +(бэкенды — `:storage-sqlite`, `:storage-inmemory`, и в будущем `:storage-android`). +Агрегатор `StorageBundle` собирает все 4 стора разом. + +## Что в контракте + +```kotlin +interface ConversationStore : AutoCloseable { + suspend fun upsert(c: Conversation) + suspend fun get(id: String): Conversation? + suspend fun list(offset, limit, orderBy): List<Conversation> + suspend fun delete(id: String): Boolean + fun events(after: Instant): Flow<ConversationStoreEvent> +} + +interface MessageStore : AutoCloseable { + suspend fun append(message: Message, conversationId: String, workingMemoryIndex: Int?) + suspend fun listByConversation(conversationId: String, after: Instant?, offset, limit): List<Message> + suspend fun update(message: Message, conversationId: String) // правка + бамп updatedAt + suspend fun deleteByConversation(conversationId: String) +} + +interface WorkingMemoryStore : AutoCloseable { + suspend fun append(conversationId: String, entry: WorkingMemoryEntry, index: Int) + suspend fun listByConversation(conversationId: String, after: Instant?): List<WorkingMemoryEntry> + suspend fun compact(conversationId: String, fromIndex: Int, summary: SummaryEntry) + suspend fun reset(conversationId: String) +} + +interface ReflectionStore : AutoCloseable { + suspend fun append(conversationId: String, reflection: ReflectionEntry) + suspend fun recent(conversationId: String?, topK: Int): List<ReflectionEntry> +} +``` + +`WorkingMemoryEntry` — `sealed interface`: `User`, `Assistant`, `ToolExchange`, +`Summary`. Каждый entry имеет `instant` (когда попал в working memory) и +`index` (порядковый номер для восстановления при компакции). + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:storage-core:$version") + // + бэкенд: + implementation("pw.binom.agentik:storage-inmemory:$version") // тесты + // или + implementation("pw.binom.agentik:storage-sqlite:$version") // прод +} +``` + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :storage-core:build +``` + +KMP-таргеты — полный набор. Зависимости — только `kotlinx-coroutines` + +`kotlinx-serialization`. diff --git a/storage-inmemory/README.md b/storage-inmemory/README.md new file mode 100644 index 0000000..e740190 --- /dev/null +++ b/storage-inmemory/README.md @@ -0,0 +1,42 @@ +# :storage-inmemory — `pw.binom.agentik.storage.inmemory` + +**In-memory реализация всех 4 сторов из `:storage-core`.** +Без платформенного IO, KMP-таргеты полностью. Тред-безопасна через `Mutex`. + +## Какую проблему решает + +- **Тесты** — мгновенный setup/teardown, нет файловой системы, нет JDBC, + детерминированное состояние. +- **Embedded-сценарии** (Android ART, iOS) где SQLDelight native-драйвер пока + недоступен или избыточен. +- **Отладка** — можно в рантайме дампить `MessageStore.listByConversation()` и + смотреть что попало в working memory без sqlite3 CLI. + +Семантика **полностью совпадает** с `:storage-sqlite`: один и тот же контракт +`:storage-core`, одинаковые `AutoCloseable`, одинаковая тред-безопасность. + +## Подключение + +```kotlin +commonMain { + implementation("pw.binom.agentik:storage-inmemory:$version") +} + +// Использование +val bundle = InMemoryStorageBundle(clock = Clock.System) +val conv = bundle.conversations.upsert(Conversation(id = "test", title = "demo", createdAt = now)) +bundle.messages.append(message = UserMessage("hi"), conversationId = conv.id, workingMemoryIndex = 0) +``` + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :storage-inmemory:build +``` + +KMP-таргеты — полный набор. Зависимости — только `:storage-core`. diff --git a/storage-sqlite/README.md b/storage-sqlite/README.md new file mode 100644 index 0000000..e1e15a5 --- /dev/null +++ b/storage-sqlite/README.md @@ -0,0 +1,62 @@ +# :storage-sqlite — `pw.binom.agentik.storage.sqlite` + +**SQLDelight-реализация всех 4 сторов из `:storage-core` поверх SQLite.** +JVM-only (SQLDelight пока не публикует KMP-драйверы за пределами JVM/Android). + +## Какую проблему решает + +Прод-запуск `:standalone` должен переживать рестарт. In-memory не подходит — +нужна **реальная БД**. SQLite выбран потому что: + +- **Один файл** (`AGENTIK_DB_PATH`) — легко бэкапить, переносить, инспектить. +- **WAL** — запись не блокирует чтение; диалоги не фризят при compaction. +- **Без отдельного сервиса** — в отличие от PostgreSQL/MySQL не нужно ничего + поднимать рядом. + +## Схема + +SQLDelight `.sq`-файлы в `src/main/sqldelight/`: + +- `Conversation.sq` — `conversations` (id, title, created_at, updated_at, is_temporal) +- `Message.sq` — `messages` (id, conversation_id, role, content, working_memory_index, date) +- `WorkingMemory.sq` — `working_memory` (id, conversation_id, entry_kind, payload_json, index, instant) +- `Reflection.sq` — `reflection` (id, conversation_id, score, weak_points_json, created_at) + +`SqliteStores` (фасад) собирает все 4 стора в `StorageBundle` поверх общего +SQLite-driver. Каждый store — отдельный класс, с тред-безопасностью через +`Mutex` на запись. + +## Подключение + +```kotlin +plugins { + kotlin("jvm") + alias(libs.plugins.sqldelight) +} + +dependencies { + implementation("pw.binom.agentik:storage-sqlite:$version") + // Транзитивно: :storage-core + sqldelight-runtime/coroutines + sqlite-driver +} + +// Использование +val driver = JdbcSqliteDriver("jdbc:sqlite:agentik.db") +SqliteStores.Schema.migrate(driver) // CREATE TABLE IF NOT EXISTS + ALTER +val bundle = SqliteStores.fromDriver(driver, clock = Clock.System) +``` + +`:standalone` инициализирует бэкенд автоматически по `AGENTIK_DB_PATH`. + +## Где смотреть версии + +- `version` из `gradle.properties` (`version=0.1.0`) +- релизы: `https://git.binom.pw/subochev/agentik/releases` + +## Сборка + +```bash +./gradlew :storage-sqlite:build +``` + +JVM-only. Тянет `:storage-core` + `app.cash.sqldelight:runtime` + +`app.cash.sqldelight:coroutines-extensions` + `app.cash.sqldelight:sqlite-driver`.