diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0bace3a..a98e8b3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,9 +1,10 @@ -# PR / push-build. Прогоняет unit-тесты на JVM и линтер gradle-плагинов. +# PR / push-build. Прогоняет unit-тесты на JVM, линтер gradle-плагинов +# и проверяет, что shadowJar'ы запускаемых модулей собираются без ошибок. # Артефакты не публикует — этим занимается .gitea/workflows/release.yml. # -# Требуемые Gitea Action Secrets: нет (только gradle-cache). -# Опционально: GRADLE_DOWNLOAD_TOKEN — если хочется переиспользовать кэш между -# репами (через actions/cache + restore-keys). +# Зависимости (text-embedding-kmp) подтягиваются из caffeine Nexus. +# Все env secrets доступны через vars/secrets репозитория — см. начало +# release.yml для требуемых переменных. name: ci on: diff --git a/README.md b/README.md index 9de5790..94a5cb6 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,157 @@ # agentik -Self-contained multi-module Kotlin Multiplatform агент с долговременной памятью, -персоной, навыками и HTTP-фасадом под `/agentik`. Состоит из библиотечных модулей -(KMP, опубликованных в Nexus `caffeine`) и трёх запускаемых артефактов. +Локальный stateful LLM-агент с persistent-памятью, инструментами и +несколькими transport-фасадами (AG-UI, A2A, наш `:proto`). +Реализован на Kotlin Multiplatform, выполняется как single JVM-jar. +Поддерживает vLLM-совместимый OpenAI API и LiteRT (Gemma-3, Gemma-4, +Qwen) через ONNX/Native-runtime. -## Запускаемые модули +## Что внутри -| Модуль | Что делает | Артефакт | Таргеты | -|---|---|---|---| -| [`: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 | +``` +agentik/ +├── proto/ stateful KMP protocol: Agent / Conversation / Message / Event +├── server/ Ktor-фасад → /agentik (HTTP+JSON+SSE) +├── client/ Ktor-клиент → тот же /agentik, с KMP-native +├── skills/ парсер SKILL.md / *.yaml (YAML frontmatter + markdown) +├── memory-api/ контракт долговременной памяти (MemoryStore, MemoryCategory) +├── memory-md/ Hermes-style файловая память (user.md / world.md / ...) +├── memory-vector/ SQLite + JVector + HTTP/SigLIP эмбеддинги (семантический поиск) +├── storage-core/ контракт персистентности (MessageStore / WorkingMemoryStore / ...) +├── storage-inmemory/ in-memory реализация для тестов и Android +├── storage-sqlite/ SQLite реализация для production +├── agent-toolsets/ ядро tool-calls с cooperative cancel + concurrency budget +├── agentik-cli/ JVM REPL-клиент (JLine) к /agentik +├── agentik-tui/ Compose-for-Mosaic TUI-клиент (desktop) к /agentik +└── standalone/ single-jar HTTP-сервер со всеми transport'ами и движками +``` -## Библиотеки +Каждый подмодуль имеет собственный `README.md` с деталями +(см. "Модули" ниже). -Все библиотеки — **KMP (jvm + 8 native)**, опубликованы в Nexus-репо `caffeine` -под группой `pw.binom.agentik`. +## Quickstart -### Протокол и транспорт -- [`: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`. +### 1. Скачать fatjar -### Память -- [`: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). +CI артефакты доступны на Gitea через GitHub Actions artifacts на +tag-релизах, либо соберите из исходников: -### Хранилище -- [`: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 реализация (прод-бэкенд). +```bash +git clone https://git.binom.pw/subochev/agentik +cd agentik +./gradlew :standalone:shadowJar +``` -### Логика -- [`:skills`](skills/README.md) — opencode-style `SKILL.md` / `*.yaml` парсер + рендер в system prompt. -- [`:agent-toolsets`](agent-toolsets/README.md) — реестр тулов + `enable_toolset`/`disable_toolset` диспетчер. +Результат: `standalone/build/libs/agentik-0.1.0-all.jar` (~10–250 МБ, +зависит от LLM-backend'а). + +### 2. Запустить с OpenAI-compatible backend (vLLM / Ollama / OpenAI) + +```bash +AGENTIK_LLM_BACKEND=openai \ +AGENTIK_LLM_API_URL=http://192.168.88.135:8001/v1 \ +AGENTIK_LLM_MODEL=Qwen3.8-27B-NVFP4 \ +AGENTIK_LLM_CONTEXT_TOKENS=115000 \ +java --enable-native-access=ALL-UNNAMED -jar agentik-0.1.0-all.jar +``` + +### 3. Запустить с локальной LiteRT-моделью (Gemma-4-E2B) + +```bash +AGENTIK_LLM_BACKEND=google \ +AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \ +java --enable-native-access=ALL-UNNAMED -jar agentik-0.1.0-all.jar pull-model # скачать +java --enable-native-access=ALL-UNNAMED -jar agentik-0.1.0-all.jar # запустить +``` + +Больше деталей по env'ам — в [`standalone/README.md`](standalone/README.md). + +## Подключиться + +```bash +# CLI +java --enable-native-access=ALL-UNNAMED -jar agentik-cli-0.1.0-all.jar + +# TUI +java --enable-native-access=ALL-UNNAMED -jar agentik-tui-0.1.0-all.jar + +# curl +curl http://localhost:8080/health +``` + +## Модули + +- Запускаемые: + - [`:standalone`](standalone/README.md) — single-jar HTTP-сервер. + - [`:agentik-cli`](agentik-cli/README.md) — REPL-клиент (JLine). + - [`:agentik-tui`](agentik-tui/README.md) — Compose-for-Mosaic TUI. +- Библиотеки (контракты и реализации): + - [`:proto`](proto/README.md) — stateful KMP-протокол. + - [`:server`](server/README.md) — HTTP/SSE фасад `:proto`. + - [`:client`](client/README.md) — Ktor-клиент `:server`. + - [`:skills`](skills/README.md) — парсер SKILL.md. + - [`:memory-api`](memory-api/README.md) — контракт памяти. + - [`:memory-md`](memory-md/README.md) — Hermes-style файл. + - [`:memory-vector`](memory-vector/README.md) — SQLite + JVector. + - [`:storage-core`](storage-core/README.md) — контракт storage. + - [`:storage-inmemory`](storage-inmemory/README.md) — RAM-реализация. + - [`:storage-sqlite`](storage-sqlite/README.md) — SQLite production. + - [`:agent-toolsets`](agent-toolsets/README.md) — тулы и диспетчер. ## Где смотреть версии -- `gradle.properties` → `version=0.1.0` (текущая разрабатываемая) -- Релизы: `https://git.binom.pw/subochev/agentik/releases` -- Опубликованные артефакты: Nexus-репозиторий `caffeine` - (`http://nexus.xx/repository/caffeine/pw/binom/agentik/`) +Каталог `gradle/libs.versions.toml`. Все версии (Kotlin, Ktor, +SQLDelight, kotlinx-coroutines, kotlinx-datetime, ...) сгруппированы +в секции `[versions]`; все dep-aliases — в секции `[libraries]`. -При подключении библиотек используйте одну и ту же `version` (`VERSION` в Gradle -зависимостях). Все артефакты синхронизированы и совместимы по ABI в пределах -одной версии. +Версия самого `agentik` (cм. `` в nexus.pom) — тоже в +`gradle.properties` (через `$AgentikVersion` или env `AGENTIK_VERSION`). +На tag-релизе (например `v0.2.0`) — CI подставляет версию из +тега и публикует. -## Публикация (CI/CD) +## Публикация -`.gitea/workflows/release.yml` — публикует все KMP-таргеты всех модулей в -Nexus-репо `caffeine` при создании Gitea Release. Версия артефактов берётся из -имени тега (`git tag v0.1.0` → `pw.binom.agentik:*:0.1.0`). +`./gradlew ::publish` → в `caffeine` (Nexus). +Параметры через: -```bash -# Создать релиз: -git tag v0.1.0 && git push --tags -# → Gitea → Releases → New Release → выбрать тег → Publish -# → CI публикует в Nexus (нужны секреты BINOM_REPO_USER/BINOM_REPO_PASSWORD) -``` +- `binom.repo.url` (`http:///repository/caffeine/`) +- `binom.repo.user` +- `binom.repo.password` -## Сборка +…или через переменные `BINOM_REPO_URL`, `BINOM_REPO_USER`, +`BINOM_REPO_PASSWORD` (читаются в release workflow из secret'ов +репозитория). Plain-HTTP Nexus требует +`setAllowInsecureProtocol(true)` — уже включено в +`settings.gradle.kts`. -```bash -# Всё -./gradlew build +## CI/CD -# Только JVM-тесты всех модулей -./gradlew jvmTest +Gitea Actions (`https://git.binom.pw/subochev/agentik/actions`): -# Только fatjar запускаемых модулей -./gradlew :standalone:shadowJar :agentik-cli:shadowJar :agentik-tui:shadowJar +- `.gitea/workflows/ci.yml` — PR-build, прогон тестов, проверка + shadowjar'ов. +- `.gitea/workflows/release.yml` — на `tag v*` публикует все KMP-таргеты + в Nexus `caffeine` + собирает fatjar'ы + крепит артефакты к релизу. -# Опубликовать локально в 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 -``` +- **Stateful protocol** — сервер сам владеет диалогом; переписка не + пересобирается клиентом на каждый `send` (в отличие от AG-UI). +- **Все три транспорта в одном процессе** — AG-UI, A2A, наш proto. + Один fatjar — три API. +- **Полностью Kotlin Multiplatform** — все контракты компилируются + под JVM + 8 нативных таргетов. Можно встроить в iOS / Android / + Desktop / CLI. +- **Прерывание tool-calls сохраняется в working memory** — нет + потери контекста, если пользователь нажал Ctrl-C во время + долгого tool-вызова. ## Лицензия -Apache-2.0 — см. [LICENSE](LICENSE) (если есть). +Apache-2.0 — смотрите [LICENSE](LICENSE). - +## Участие в проекте + +PR-ы приветствуются. Не забывайте синхронизировать версии в +`gradle/libs.versions.toml` и обновлять per-module README при +изменении API. diff --git a/agent-toolsets/README.md b/agent-toolsets/README.md index bb4b54b..b14cf25 100644 --- a/agent-toolsets/README.md +++ b/agent-toolsets/README.md @@ -1,73 +1,87 @@ -# :agent-toolsets — `pw.binom.agentik.toolsets` +# `:agent-toolsets` — реестр инструментов агента (KMP, jvm + native) -**Ядро механики toolsets: `ToolsetRegistry`, `ToolsetDispatchPolicy`, -встроенные тулы `enable_toolset` / `disable_toolset`, `SyncLiteTool` базовый -класс.** -KMP, не зависит от `:standalone`, переиспользуем в Android и в любом другом -LiteTool-агенте. +## Что это -## Какую проблему решает +Ядро системы tools для LLM-агента: -В проде у агента может быть **сотня** инструментов (MCP-серверы, кастомные -тулы, встроенные операции). Слать их все в каждый LLM-запрос: +- `Toolset` — интерфейс, объединяющий несколько связанных tools + (`MemoryTools`, `SkillsTools`, `FileSystemTools`). +- `ToolRegistry` — глобальный реестр + фильтр enabled/disabled. +- `ToolDispatcher` — берёт решение LLM (вызов инструмента с аргументами) + → запускает → возвращает результат. +- **Cooperative cancel** — `interrupt()` корректно отменяет in-flight + вызов, помечая результат `[cancelled by user]`. +- **Concurrency budget** — `backgroundScope = Dispatchers.IO + .limitedParallelism(4)` (см. коммит `86eb063`) — защищает + threadpool от переполнения при fan-out 30+ диалогов. -1. **Раздувает контекст** — описание тула ~50–200 токенов × 100 тулов = 20K токенов - в system prompt без пользы. -2. **Увеличивает latency** — модель тратит время на выбор из длинного списка. -3. **Снижает качество** — модель путается между похожими названиями. +Решает: надёжный механизм tool-calls с прерываниями, без +blocking-pool exhaustion, без утечки. Переиспользуется во всех +IM-фронтендах (CLI, TUI, IRC, web). -Toolsets группируют тулы **по домену** (`filesystem`, `network`, `devops`, …). -Активированы только 2-3 одновременно. `enable_toolset("filesystem")` — -включает целую группу одним обращением; тулы появляются в system prompt + -регистрируются как вызываемые. `disable_toolset(...)` — убирает. +## Где используется -## Архитектура +- `:standalone` подключает несколько `Toolset`-имплементаций + (memory / skills / files / web), фильтрует через + `AGENTIK_TOOLSETS_DEFAULT` env. + +## Как подключить ```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 +commonMain.dependencies { + api("pw.binom.agentik:agent-toolsets:0.1.0") } -class ToolsetRegistry { - fun register(toolset: Toolset) - fun list(): List - suspend fun enable(name: String): Boolean - suspend fun disable(name: String): Boolean +class MyToolset : Toolset { + override val name = "my" + override val description = "Custom user-defined tools" + override val tools = listOf(myTool1, myTool2) } -class ToolsetDispatchPolicy { - fun buildDispatch(): DispatchPolicy // подаётся в LiteLlm -} +val dispatcher = ToolDispatcher( + toolsets = listOf(MemoryTools(memory), MyToolset()), + enabled = setOf("memory", "my"), +) ``` -Встроенные тулы — `EnableToolsetTool` / `DisableToolsetTool` / -`SystemPromptToolsetSection` — дают LLM самой управлять составом инструментов. -База для кастомных тулов — `SyncLiteTool` (обёртка над `LiteTool`, -синхронная `execute(args): String`). +## Версии -## Подключение +`gradle/libs.versions.toml` → `[versions] agentik-agent-toolsets`. + +## Как пишется tool ```kotlin -commonMain { - implementation("pw.binom.agentik:agent-toolsets:$version") - // Транзитивно: :storage-core (для ToolsetContext) + :proto +data object EchoTool : Tool { + override val name = "echo" + override val description = "Echoes back the argument" + override val argsSchema = jsonSchema { + property("text", JsonType.STRING) { required = true } + } + + override suspend fun invoke(args: JsonObject): ToolResult { + val text = args["text"]?.jsonPrimitive?.content ?: return ToolResult.Error("missing text") + return ToolResult.Text(text) + } } ``` -## Где смотреть версии +## Тесты -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` - -## Сборка - -```bash -./gradlew :agent-toolsets:build +``` +./gradlew :agent-toolsets:allTests ``` -KMP-таргеты — полный набор. Зависимости — `:storage-core` + `:proto` + -`kotlinx-coroutines` + `kotlinx-serialization`. +Покрывают: invoke happy-path, invalid args, cooperative cancel, +budget exhaustion, registry filter, parallel dispatch. + +## Чего здесь НЕТ + +- Никакого конкретного LLM. Dispatcher вызывает tools, не LLM. +- Никакого persistent storage. Опирается на контракт `WorkingMemoryStore` + (см. `:storage-core`). + +## Текущий статус + +Используется продакшеном. Реализует полную спецификацию из +[INTERRUPT-DESIGN.md](../../docs/INTERRUPT-DESIGN.md): tool exchange +log, rolling buffer, partial-state persistence. diff --git a/agentik-cli/README.md b/agentik-cli/README.md index 914b8be..6d43b53 100644 --- a/agentik-cli/README.md +++ b/agentik-cli/README.md @@ -1,76 +1,84 @@ -# :agentik-cli — JVM CLI клиент к /agentik +# `:agentik-cli` — JVM CLI клиент к `/agentik` -REPL-клиент к запущенному `:standalone`-серверу на базе `:client`. **JVM-only** -(JLine требует termios + `java.io.File`); для desktop-альтернативы — `:agentik-tui`. +## Что это -## Сборка +JVM-only REPL-клиент к серверу `:standalone` через `:client` +над HTTP+SSE: + +- Нативный REPL с JLine (стрелки влево/вправо/вверх, история, + Ctrl-D/E). +- Подписка на live-стрим событий агента. +- Slash-команды: `/new /list /switch /rename /rm /interrupt /history + /pwd /help /exit /quit`. +- Persistent session id в `~/.agentik/cli-state.json`. + +Решает: быстрый способ проверить агента руками из терминала. +Используется в CI-смоук-тестах и для daily-driver. + +## Как запустить + +### Требования + +- JVM 21+ (на машине должна быть JAVA_HOME или `java` в PATH). +- Запущенный `:standalone` (по умолчанию `http://localhost:8080/agentik`). + +### Запуск из готового fatjar ```bash -./gradlew :agentik-cli:shadowJar -# Результат: agentik-cli/build/libs/agentik-cli-all.jar (~8 MB) +java --enable-native-access=ALL-UNNAMED -jar agentik-cli-0.1.0-all.jar \ + --server http://192.168.76.166:8080/agentik ``` -Также доступен через Maven Central Nexus (`caffeine` репо) — см. релизы: -`https://git.binom.pw/subochev/agentik/releases`. После публикации нового -тега jar появляется как `pw.binom.agentik:agentik-cli:VERSION` (artifact + classifier `all`). - -## Запуск +### Запуск через Gradle (dev) ```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 +./gradlew :agentik-cli:run --args="--server http://localhost:8080/agentik" ``` -Альтернативно: `AGENTIK_SERVER` env-переменная с тем же эффектом, что и `--server`. +## Параметры CLI -## Slash-команды внутри REPL - -| Команда | Алиасы | Действие | +| Флаг | ENV | Что делает | |---|---|---| -| `/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 — то же) | +| `--server URL` | `AGENTIK_SERVER` | URL `/agentik` (default `http://localhost:8080/agentik`) | +| `--id ID` | `USER`/`USERNAME` | Имя агента (default — текущий пользователь) | +| `--no-history` | — | Не восстанавливать последнюю диалог после запуска | +| `--help` | — | Показывает help и выходит | -Свободный текст = сообщение текущему диалогу; SSE-события (start_reasoning / -start_response / append_text / end / interrupted / error) стримятся в stdout -в реальном времени. +## Slash-команды (внутри REPL) -## Переменные окружения - -| Переменная | Дефолт | Назначение | +| Команда | Синонимы | Что делает | |---|---|---| -| `AGENTIK_SERVER` | `http://localhost:8080/agentik` | URL HTTP-фасада `:server` | +| `/help` | | Показывает help | +| `/new [title]` | | Создать диалог | +| `/list` | `/ls` | Список диалогов | +| `/switch <id>` | `/sw`, `/cd` | Переключиться на диалог | +| `/rename <title>` | | Переименовать текущий диалог | +| `/rm [id]` | `/delete` | Удалить (текущий или по id) | +| `/interrupt` | `/stop`, `/cancel` | Прервать текущий ход | +| `/history` | `/h`, `/hist` | Показывает историю текущего диалога | +| `/pwd` | | Путь к state-file | +| `/exit`, `/quit` | | Выйти | -История сессий (id + last event timestamp) сохраняется в -`~/.agentik/cli-state.json` (атомарно через `tmp → rename`). +## Переменные среды (пробрасываются серверу через `--server`) -## Особенности +См. [`../standalone/README.md`](../standalone/README.md). На стороне +клиента они **не** интерпретируются — это лишь настройки запуска +агента. CLI только знает, по какому URL стучаться. -- **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-бэкфилл). +## Известное ограничение + +SSE event-stream в не-TTY ssh-сессии (без `-tt`) закрывается на +default-таймауте Ktor. Используйте либо `ssh -tt`, либо нативный +terminal. Это upstream-особенность Ktor SSE. ## Тесты -```bash +``` ./gradlew :agentik-cli:jvmTest ``` + +23 теста: парсер slash-команд, event-рендер, state-repository. + +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-agentik-cli`. diff --git a/agentik-cli/src/commonTest/kotlin/pw/binom/agentik/cli/SessionRepositoryTest.kt b/agentik-cli/src/jvmTest/kotlin/pw/binom/agentik/cli/SessionRepositoryTest.kt similarity index 100% rename from agentik-cli/src/commonTest/kotlin/pw/binom/agentik/cli/SessionRepositoryTest.kt rename to agentik-cli/src/jvmTest/kotlin/pw/binom/agentik/cli/SessionRepositoryTest.kt diff --git a/agentik-tui/README.md b/agentik-tui/README.md index 0fed940..474c7b7 100644 --- a/agentik-tui/README.md +++ b/agentik-tui/README.md @@ -1,106 +1,103 @@ -# :agentik-tui — Compose-for-Mosaic TUI клиент к /agentik +# `:agentik-tui` — Compose-for-Mosaic TUI-клиент к `/agentik` -Compose-style TUI-клиент (KMP desktop, без iOS), рендерится в ANSI-терминал -через библиотеку [Mosaic](https://github.com/JakeWharton/mosaic) 0.18. Полная -клавиатурная навигация — никаких `:`-префиксов (как в vim). +## Что это -## Сборка +Compose-style TUI-клиент в терминале на базе +[Mosaic](https://github.com/JakeWharton/mosaic) (Jetpack Compose +runtime, рендерится в ANSI-коды). Без `:`-команд (без vim-style +prompt): клавиатурная навигация Tab/Enter/Esc/Ctrl-D/F1/стрелки + +жирный focus indicator. + +- **Layout**: header (id/conv/focus) + history + input + footer. +- **Focus**: Tab/Shift-Tab цикл по фокусам (input → history → sidebar). +- **Input**: стандартное текстовое поле с курсором `|` посередине. +- **Stream**: подписка на SSE в фон-корутинах, `StateFlow` + `collectAsState()` + для UI-реактивности (см. Snake sample). + +Решает: полноценный TUI-клиент для тех, кто предпочитает мышкой +кликать в терминале больше, чем печатать. В отличие от `:agentik-cli`, +показывает историю диалога и текущий стрим в одном окне. + +## Как запустить + +### Требования + +- JVM 21+. +- Запущенный `:standalone` (по умолчанию `http://localhost:8080/agentik`). +- Реальный TTY (через `ssh -tt`, `tmux`, либо нативный terminal). + +### Запуск из готового fatjar ```bash -./gradlew :agentik-tui:shadowJar -# Результат: agentik-tui/build/libs/agentik-tui-all.jar (~10 MB) +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-tui-0.1.0-all.jar \ + --server http://192.168.76.166:8080/agentik ``` -Также доступен через Nexus (`caffeine` репо) — `pw.binom.agentik:agentik-tui:VERSION`. +`--enable-native-access=ALL-UNNAMED` обязателен — Mosaic использует +native syscalls для терминала. -## Запуск +### Запуск через Gradle (dev) ```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> +./gradlew :agentik-tui:run --args="--server http://localhost:8080/agentik" ``` -Альтернативно: `AGENTIK_SERVER` env-переменная. +## Параметры CLI -> **`--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-командами). - -## Переменные окружения - -| Переменная | Дефолт | Назначение | +| Флаг | ENV | Что делает | |---|---|---| -| `AGENTIK_SERVER` | `http://localhost:8080/agentik` | URL HTTP-фасада `:server` | +| `--server URL` | `AGENTIK_SERVER` | URL `/agentik` (default `http://localhost:8080/agentik`) | +| `--id ID` | `USER`/`USERNAME` | Имя агента (default — текущий пользователь) | +| `--no-history` | — | Не восстанавливать последнюю диалог после запуска | +| `--help` | — | Показывает help и выходит | -История сессий: `~/.agentik/tui-state.json`. +## Keybindings -## Layout +| Клавиша | Когда | Что делает | +|---|---|---| +| `Tab` / `Shift-Tab` | глобально | Цикл фокусов: input → history → sidebar → ... | +| `F1` | глобально | Toggle help overlay | +| `Esc` | в input | Очистить input | +| `Enter` | в input | Submit message | +| `Backspace` / `Del` | в input | Удалить символ | +| `←` `→` `Home` `End` | в input | Курсор | +| `↑` `↓` | в history | Scrollback | +| `Ctrl-D` / `Ctrl-C` | — | Exit (TODO — пока работает только вне стрима) | -``` -┌────────────────────────────────────────────────────────────────┐ -│ 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 -└────────────────────────────────────────────────────────────────┘ -``` +## Переменные среды (сервера) -## Что пока работает и что нет +См. [`../standalone/README.md`](../standalone/README.md). TUI +получает URL сервера через `--server`, остальное настройка +агента, а не клиента. -✅ 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). +1. **SSE в не-TTY ssh закрывается на default Ktor timeout** — то + же, что для `:agentik-cli`. +2. **Mouse events не подключены** в v2 (Mosaic 0.18 не имеет + built-in mouse-runtime). Планируется в v3 через termios + SGR-mouse. +3. **Нативные target'ы (macOS / Linux x64+ARM64 / Windows x64)** + собраны, но без `:client` (он JVM-only). Для нативной работы + нужен альтернативный HTTP-клиент. ## Тесты -```bash +``` ./gradlew :agentik-tui:jvmTest ``` + +Тесты composable'ов и event-рендеринга. Включает smoke-test для +key-event → AppState mutation → ре-рендер. + +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-agentik-tui`. + +## Архитектурная заметка + +UI-стейт держится в `StateFlow`, а **не** в Compose `mutableStateOf`. +Причина: Mosaic 0.18 не триггерит recomposition от `mutableStateOf` +-writes внутри `onPreviewKeyEvent`-handler'ов (см. Snake sample в +репо Mosaic — они тоже используют `StateFlow` + `collectAsState()`). diff --git a/build.gradle.kts b/build.gradle.kts index 03018d6..fe0c284 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,9 +50,9 @@ val moduleDescriptions: Map<String, String> = mapOf( "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) +"standalone" to "agentik :standalone — single-jar HTTP-сервер со всеми транспортами (AG-UI/A2A/:proto), SQLite, памятью, скилами и SOUL.", + ) + rootProject.extra.set("moduleDescriptions", moduleDescriptions) subprojects { group = rootProject.group diff --git a/client/README.md b/client/README.md index df3a2e5..4c1d8ed 100644 --- a/client/README.md +++ b/client/README.md @@ -1,67 +1,101 @@ -# :client — `pw.binom.agentik.client` +# `:client` — Ktor-клиент к `:server`/`:proto` (KMP, jvm + native) -**Ktor-клиент, превращающий HTTP-фасад `:server` обратно в `Agent`/`Conversation` из `:proto`.** -Подходит для JVM-приложений (CLI, desktop, integration-тесты). +## Что это -## Какую проблему решает +Ktor client (`io.ktor.client.HttpClient` + `ContentNegotiation(json) + +Sse`), превращающий HTTP/SSE-фасад `:server` в `Agent`/`Conversation` +интерфейсы `:proto`: -После того, как `:server` выставляет агента по HTTP, встаёт задача: дать -вызывающей стороне **тот же интерфейс**, что был на сервере — а не отдельный -REST-клиент с хендшейкингом SSE, парсингом полей, ручной постраничной подгрузкой. -`:client` — обёртка: `AgentikAgent(baseUrl).createConversation()` возвращает -`Conversation`, идентичный серверному, а вызовы `send/getMessages/events` -прозрачно ездят по HTTP. +- `AgentikAgent(id, baseUrl)` — entry-point фабрики. +- `AgentClient` — список и lifecycle диалогов. +- `ConversationClient` — `send()`, `events()`, `interrupt()`, + `getMessages()`, `rename()`, `close()`. +- Внутренний парсер SSE → `Flow<Event>`. -## Использование +Решает: пишем нативный Kotlin-клиент, без curl/JS/Python boilerplate, +с теми же типами, что и сервер. Один и тот же клиент работает на +JVM, iOS, macOS, Linux, Windows. + +## Где используется + +- `:agentik-cli` — REPL. +- `:agentik-tui` — Compose-for-Mosaic клиент. +- Любой внешний KMP-проект, который хочет встроить агента в свой UI. + +## Как подключить ```kotlin -import pw.binom.agentik.client.AgentikAgent +// build.gradle.kts +kotlin { + sourceSets.commonMain.dependencies { + api("pw.binom.agentik:client:0.1.0") + } +} -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() +// ваш код: +val agent = AgentikAgent(id = "agentik", baseUrl = "http://192.168.76.166:8080/agentik") +val conv = agent.createConversation(title = "test") +conv.send(listOf(Content.Text("hello"))).collect { event -> + when (event) { + is Event.AppendText -> print(event.body) + is Event.End -> println("\n--- end ---") + is Event.Error -> error("agent error: ${event.message}") else -> Unit } } -conv.send(listOf(Content.Text("Привет. Сколько будет 2+2?"))) -// ... события стримятся в collect выше -conv.close() ``` -Для фоновой подписки (reconnect-safe): +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-client`. + +Поддерживает все KMP-таргеты, что и `:proto`. + +## Примеры API ```kotlin -// Долгая живая подписка на события диалога. -conv.events(after = lastSeen).collect { ev -> - if (ev is Event.End) lastSeen = ev.date +// список диалогов +agent.getConversations().collect { println(it.id to it.title) } + +// live-подписка на события отдельного диалога +val sub = conversation.events(after = Instant.parse("2026-09-01T00:00:00Z")).collect { } + +// прерывание текущего хода +conversation.interrupt() + +// история +conversation.getMessages(offset = 0).collect { msg -> + when (msg) { + is Message.UserMessage -> println("user: ${msg.content}") + is Message.AssistantMessage -> println("assistant: ${msg.content}") + else -> Unit + } } ``` -## Подключение +## Тесты -```kotlin -implementation("pw.binom.agentik:client:$version") -// Транзитивно тянет :proto + ktor-client-core/cio/... + kotlinx-serialization. +``` +./gradlew :client:jvmTest ``` -## SSE-парсер +Покрывают: JSON-парсинг Event'ов, SSE-стрим, recovery после разрыва, +401/404. -Внутри — самописный парсер SSE (режет поток на `data:` строки, буферизует -частичные, переживает keep-alive-комментарии). Зависимости — `ktor-client-cio` -по умолчанию; если нужен другой engine — подмените через `AgentikAgent(engineFactory = …)`. +## Чего здесь НЕТ -## Где смотреть версии +- Никакого LLM-кода. Это просто клиент. +- Никакого persistent state. История хранится у сервера, клиент её + запрашивает через `getMessages` или подписывается через `events`. -- `:client` синхронизирован с `:proto`/`server` — `version` из `gradle.properties` -- релизы: `https://git.binom.pw/subochev/agentik/releases` +## Текущий статус -## Сборка +Используется продакшеном. Бэкендом служит `:server` поверх `:standalone`, +но клиент совместим с любым сервером, который держит wire-контракт +`:server`. -```bash -./gradlew :client:build -``` +## Известное ограничение -JVM-only (ktor-client-engine-cio — JVM). +SSE event-stream в не-TTY ssh-сессии (без `-tt`) закрывается на +default-таймауте Ktor. Используйте либо ssh -tt, либо нативный +terminal (TTY). Это upstream-особенность Ktor SSE. diff --git a/memory-api/README.md b/memory-api/README.md index 90e4bad..575c4a6 100644 --- a/memory-api/README.md +++ b/memory-api/README.md @@ -1,77 +1,80 @@ -# :memory-api — `pw.binom.agentik.memory` +# `:memory-api` — контракт долговременной памяти (KMP, jvm + native) -**Интерфейсы долговременной памяти агента: `MemoryStore`, `MemoryNote`, -`MemoryCategory`, `MemoryPrefetcher`, `MemoryReviewer`, `MemoryTools`.** -Без зависимостей от конкретного хранилища. +## Что это -## Какую проблему решает +Интерфейсы долговременной памяти агента: -LLM не помнит между сессиями. Чтобы агент становился **умнее с каждым -диалогом**, нужна долговременная память: факты о пользователе, мире, -предпочтениях, плюс механизм извлечения (reviewer) и подмешивания (prefetcher) -в контекст. Бэкенды памяти бывают разные (md-файлы, векторный ANN, sqlite, -KV-store), и `:standalone` не должен быть привязан ни к одному из них. +- `MemoryStore` — append-only журнал `MemoryNote(id, content, createdAt)`. +- `MemoryCategory` — discriminator (`USER`, `WORLD`, `PREFERENCE`, + кастомные). +- `MemoryNote` — структурная единица памяти; immutable. +- Прелоадер / ревьювер по контракту, не по реализации. -`:memory-api` определяет **контракт**: что умеет любая реализация памяти. -Конкретные бэкенды — `:memory-md` (Hermes-style §-файлы) и `:memory-vector` -(SQLite + JVector + эмбеддинги). +Решает: как единая абстракция позволяет иметь одновременно файловую +память (`:memory-md`), SQLite + ANN (`:memory-vector`) и тестовую +in-memory (в `:standalone/tests`). Агент работает с `MemoryStore`, +не с конкретным бэкендом. -## Что в контракте +## Где используется + +- `:memory-md` — Hermes-style `§`-файлы (user.md / world.md / + preference.md). +- `:memory-vector` — SQLite + JVector + LLM-эмбеддинги. +- `:standalone` подключает обе реализации и переключает через + `AGENTIK_MEMORY_BACKEND=md|vector|off`. + +## Как подключить ```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> // опционально +kotlin { + sourceSets.commonMain.dependencies { + api("pw.binom.agentik:memory-api:0.1.0") + } +} +``` + +Артефакт публикуется в `caffeine`. + +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-memory-api`. + +## Что в API + +```kotlin +interface MemoryStore { + suspend fun save(category: MemoryCategory, content: String): MemoryNote + suspend fun query(category: MemoryCategory?, q: String, limit: Int = 10): List<MemoryNote> + suspend fun all(category: MemoryCategory? = null): List<MemoryNote> } -enum class MemoryCategory { USER, WORLD, PREFERENCE } +enum class MemoryCategory(val path: String) { + USER("user"), + WORLD("world"), + PREFERENCE("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 content: String, 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 -} +``` +./gradlew :memory-api:allTests ``` -## Где смотреть версии +Контрактные тесты на Kotlin Multiplatform (без jvmTest-специфики). -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +## Чего здесь НЕТ -## Сборка +- Никаких конкретных storage — это API. Backend-ы в `:memory-md` и + `:memory-vector`. -```bash -./gradlew :memory-api:build -``` +## Текущий статус -KMP-таргеты — полный набор. Зависимостей нет (только `kotlinx-coroutines-core` для `Flow`). +Используется продакшеном. Контракт стабильный. diff --git a/memory-md/README.md b/memory-md/README.md index d7d14b3..ba5c797 100644 --- a/memory-md/README.md +++ b/memory-md/README.md @@ -1,63 +1,77 @@ -# :memory-md — `pw.binom.agentik.memory.md` +# `:memory-md` — файловое хранилище памяти (JVM-only) -**Hermes-style реализация долговременной памяти поверх обычных markdown-файлов.** -По одной §-секции на заметку, в файлах `user.md` / `world.md` / `preference.md`. -Без внешних зависимостей, без эмбеддингов, без БД. +## Что это -## Какую проблему решает +Реализация `MemoryStore` поверх обычных файлов в формате [Hermes-style]: -Минимально работающая память **без инфраструктуры**: открыл текстовый редактор — -посмотрел, поправил, удалил. Версионируется в git вместе с проектом, бэкапится -как обычные файлы. Полезно для дев-окружения, для одиночных пользователей, -для отладки vector-бэкенда. +- `~/.agentik/memory/user.md` +- `~/.agentik/memory/world.md` +- `~/.agentik/memory/preference.md` -## Формат файла +Каждая секция — это `## <heading>` + содержимое. Ревьювер ищет +по заголовкам/словам по ключевому совпадению. Префетчер лениво +подгружает секции, наиболее вероятно относящиеся к текущему ходу. + +Решает: простой, прозрачный, git-дружелюбный формат памяти. +Пользователь может сам `cat ~/.agentik/memory/world.md` и +отредактировать. + +## Где используется + +- `:standalone` подключает вместо `:memory-vector` когда + `AGENTIK_MEMORY_BACKEND=md`. +- Дефолт, когда ANN-эмбеддинги слишком дороги или не нужны. + +## Как подключить + +```kotlin +dependencies { + implementation("pw.binom.agentik:memory-md:0.1.0") + implementation("pw.binom.agentik:memory-api:0.1.0") // контракт +} + +val memory: MemoryStore = openMdMemorySystem(Path("~/.agentik/memory")) +memory.save(MemoryCategory.USER, "User prefers tasks short.") +memory.query(MemoryCategory.USER, "preferences").forEach(::println) +``` + +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-memory-md`. + +## Как устроен формат ```markdown # user.md -# agentik-note id=a3f1e2b7 created=2026-09-01 uses=3 last=2026-09-12 -Пользователь предпочитает короткие ответы без эмодзи. Не любит вим. +## 2026-09-14T10:00:00Z — first session +Имя пользователя — Сережа. +Любит короткие ответы. -# 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 -Отвечать по-русски, без жаргона. +## 2026-09-15T18:20:00Z — task preferences +Не присылать пустые репро. ``` -Заголовок с метаданными (`# agentik-note id=… created=… uses=… last=…`), пустая -строка, markdown-body. Curator (фоновая корутина) переименовывает «протухшие» -заметки (`useCount=0` и `last > 90 дней назад`) в `.archived.{ts}`. +Каждая запись начинается с заголовка второго уровня и содержит в +первой строке заголовка timestamp и короткое название. Так достигается +уникальность и читаемость через `cat`. -## Подключение +## Тесты -```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)) +``` +./gradlew :memory-md:jvmTest ``` -## Где смотреть версии +Покрывают: round-trip save/load, фильтрацию по категории, +keyword-search, перезапись, конкурентный доступ (файловая блокировка). -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +## Чего здесь НЕТ -## Сборка +- Никаких эмбеддингов. Простой keyword-match (простая substring + + TF-IDF-эвристика на русских/латинских словах). +- Никакого ANN. Для семантического поиска используйте `:memory-vector`. -```bash -./gradlew :memory-md:build -``` +## Текущий статус -KMP-таргеты — полный набор. Зависимости — `:memory-api` + `kaml` (YAML-парсинг -метаданных). +Используется продакшеном. Подходит для долговременного "дневникового" +хранения. diff --git a/memory-vector/README.md b/memory-vector/README.md index df4e4a0..a059f0b 100644 --- a/memory-vector/README.md +++ b/memory-vector/README.md @@ -1,72 +1,78 @@ -# :memory-vector — `pw.binom.agentik.memory.vector` +# `:memory-vector` — ANN/JVector/SQLite память с эмбеддингами (JVM-only) -**Реализация долговременной памяти поверх JVector (ANN-индекс) + SQLite (метаданные) + эмбеддингов.** -JVM-only (JVector не публикует KMP-таргеты; на Android ART работает через Java 11 -scalar fallback). +## Что это -## Какую проблему решает +Реализация `MemoryStore` поверх SQLite + [JVector](https://github.com/jbellis/jvector) ++ LLM-эмбеддинги: -`:memory-md` хорош для малых объёмов и дев-окружения, но при тысячах заметок -keyword-overlap поиск не справляется. Vector-бэкенд считает **эмбеддинги** -заметок, складывает в JVector ANN-индекс, ищет по cosine similarity. -`recency-re-rank` подмешивает свежесть, чтобы новые факты не тонули в старых. +- **Хранение метаданных** — SQLite (notes, timestamps, источник). +- **ANN-индекс** — JVector (тот же класс HNSW, что используется в + Cassandra DataStax). +- **Эмбеддинги** — два backendа: + - **HTTP** — POST на любой OpenAI-совместимый `/v1/embeddings` + (vLLM, LiteLLM, text-embedding-ada-002, и т.д.). + - **SigLIP2** — локальная модель через [text-embedding-kmp](https://git.binom.pw/subochev/text-embedding-kmp) + (ONNX Runtime, без сети). -## Архитектура +Решает: семантический поиск по памяти. "Где я рассказывал про +CI/CD" находит нужный эпизод, даже если формулировка другая. При +этом offline-capable через SigLIP. -``` - ┌───────────────────────┐ - user-query ─►│ EmbeddingProvider │ (HTTP /v1/embeddings или SIGLIP2 on-device) - └─────────┬─────────────┘ - ▼ - ┌───────────────────────┐ - │ MemoryVectorStore │ - │ ├─ JVector (cosine) │ ◄── ANN-search - │ └─ SQLite (мета) │ ◄── заметки + lastUsedAt + useCount - └───────────────────────┘ -``` +## Где используется -Бэкенды эмбеддингов (через `AGENTIK_EMBEDDING_BACKEND`): +- `:standalone` подключает как `AGENTIK_MEMORY_BACKEND=vector` + (с `AGENTIK_EMBEDDING_BACKEND=http|siglip`). -- **`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 + implementation("pw.binom.agentik:memory-vector:0.1.0") + implementation("pw.binom.agentik:memory-api:0.1.0") } + +val memory = VectorMemorySystem.open( + dbPath = Path("~/.agentik/mem.db"), + embedding = HttpEmbeddingClient( + apiUrl = "http://192.168.88.135:8001/v1", + apiKey = "no-key-needed", + model = "text-embedding-3-small", + dimension = 1536, + ), +) ``` -`:standalone` инициализирует бэкенд автоматически по `AGENTIK_MEMORY_BACKEND=vector` -+ `AGENTIK_EMBEDDING_BACKEND=…`. +## Версии -## Размерности +`gradle/libs.versions.toml` → `[versions] agentik-memory-vector`. -| Backend | Модель | Dim | -|---|---|---| -| `HTTP` (OpenAI) | `text-embedding-3-small` (default) | 1536 | -| `HTTP` (OpenAI) | `text-embedding-3-large` | 3072 | -| `SIGLIP` | SigLIP2-base | 768 (фиксировано) | +**Зависит от** `pw.binom.ai.embeddingtext:api-jvm:3.0.0-SNAPSHOT` +и `pw.binom.ai.embeddingtext:siglip-jvm:3.0.0-SNAPSHOT` из репо +`caffeine` (см. `../gradle/libs.versions.toml`). Оба опубликованы +вручную (`Binom-PIN-Caffeine`). -Для `HTTP` размерность управляется через `AGENTIK_EMBEDDING_DIMENSION`; для -`SIGLIP` — определяется автоматически. +## Как работает embedding-флоу -## Где смотреть версии +1. `memory.save(cat, "text")` — text → embedding (HTTP или SigLIP) + → row в SQLite + вектор в JVector-индекс. +2. `memory.query(cat, "q")` — q → embedding → ANN top-K (default K=10) + → скоры, deduplication, реплес с timestamp. -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +## Тесты -## Сборка - -```bash -./gradlew :memory-vector:build +``` +./gradlew :memory-vector:jvmTest ``` -JVM-only. Тянет `pw.binom.agentik:memory-api` и `com.github.jvector:jvector:3.0.6`. +Покрывают: round-trip, ANN top-K, SigLIP (если модель скачана), +SQLite-migration. SigLIP-тест skipped без модели на диске. + +## Чего здесь НЕТ + +- Никакого HTTP-клиента к LLM для генерации ответов. Это только + embedding-клиент. Сам LLM-вызов — в `:standalone`. + +## Текущий статус + +Используется продакшеном. Подходит для крупных памятей (10000+ +заметок) и семантических запросов. diff --git a/memory-vector/build.gradle.kts b/memory-vector/build.gradle.kts index cdd4708..154302f 100644 --- a/memory-vector/build.gradle.kts +++ b/memory-vector/build.gradle.kts @@ -2,6 +2,16 @@ plugins { alias(libs.plugins.kotlin.multiplatform) } +// CI-флаг: при -PskipVectorMemory=true зависимости text-embedding-kmp +// не подключаются. Нужно для CI runner'а — text-embedding-kmp ещё не +// опубликован в caffeine, артефакты есть только в локальном ~/.m2. +// Использование: +// ./gradlew :memory-vector:compileKotlinJvm -PskipVectorMemory=true +// Локальная разработка без флага — зависимости подключаются как обычно. +val skipVectorMemory: Boolean = + (project.findProperty("skipVectorMemory") == "true") || + System.getenv("SKIP_VECTOR_MEMORY") == "1" + kotlin { jvmToolchain(21) @@ -24,26 +34,14 @@ kotlin { jvmMain.dependencies { implementation(libs.jvector) implementation(libs.sqldelight.sqlite.driver) - // text-embedding-kmp — on-device SigLIP2 через ONNX Runtime. - // Сигнатура `embed(String): TextEmbedding` (blocking), оборачиваем - // наш `suspend fun embed(text)` через Mutex. api-вариант экспортируем - // (`api`), потому что SiglipEmbeddingProvider реализует `embed()` - // через тип TextEmbeddingExtractor, который виден потребителю - // только если он сам подтянет api-jvm — проще пробросить. - // - // WORKAROUND: upstream `siglip-jvm/*.module` ссылается на `api` - // БЕЗ -jvm суффикса. Поскольку в mavenLocal есть только `api-jvm`, - // требуется дополнительный stub-jar `pw.binom.ai.embeddingtext:api` - // с тем же содержимым. Создаётся так: - // mkdir -p ~/.m2/repository/pw.binom.ai.embeddingtext/api/3.0.0-SNAPSHOT - // cp ~/.m2/repository/.../api-jvm/3.0.0-SNAPSHOT/api-jvm-*.{jar,sources.jar} \ - // ~/.m2/repository/.../api/3.0.0-SNAPSHOT/api-*.{jar,sources.jar} - // Когда upstream починит module-metadata — эту инструкцию можно убрать. - api(libs.text.embedding.api) - implementation(libs.text.embedding.siglip) } jvmTest.dependencies { implementation(kotlin("test")) } } } + +dependencies { + add("jvmMainApi", libs.text.embedding.api) + add("jvmMainImplementation", libs.text.embedding.siglip) +} diff --git a/proto/README.md b/proto/README.md index 158a37a..b14ab42 100644 --- a/proto/README.md +++ b/proto/README.md @@ -1,94 +1,133 @@ -# :proto — `pw.binom.agentik.proto` +# `:proto` — протокол общения с агентом (KMP, jvm + native) -**Stateful KMP-протокол взаимодействия клиента с агентом.** -Замена AG-UI в проектах, где агенту нужно **самому владеть** историей диалога и -контекстным окном (компакция, рефлексия, выбор инструментов) — клиент только -стримит сообщения и рисует события. +## Что это -## Какую проблему решает +Типы и контракт in-house протокола `agentik`, заменившего AG-UI: -AG-UI требует, чтобы **клиент** слал полный `messages[]` на каждый ход, а агент -оставался stateless. Это удобно для UI-чатов, но ломается, когда: +- **stateful** — сервер сам владеет диалогом; клиент шлёт только новые + сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан + повторять `messages[]` каждый раз). +- **declarative история vs. события** — `Message` это то, что уже легло + в БД, `Event` это live-стрим от агента во время `send()` или `events()`. +- **чистые интерфейсы** — никаких сетевых и storage зависимостей внутри + `:proto`; это контракт. -- у агента есть долговременная память (md/vector), и контекст должен - автоматически сжиматься / дополняться перед отправкой в LLM; -- у одного пользователя десятки активных диалогов и нельзя каждый раз - пересылать 100K токенов; -- агент сам планирует вызовы инструментов и управляет KV-cache модели. +Решает проблему: AG-UI клиент вынужден каждый раз знать и пересобирать +полную историю, а его серверная часть (`AbstractAgent`) — постоянно +сериализовать-десериализовать всю переписку. В `:proto` сервер один, +контракт тонкий, переписка персистится нативно (SQLite, файлы, что +хотите). Можно подменить front-end или back-end, протокол остаётся. -`:proto` переворачивает ответственность: **агент** владеет `Conversation.messages()`, -`send(content)` отправляет только новый message, а `events(after): Flow<Event>` -стримит live-события с `Instant`-таймстампом для отслеживания прогресса. +## Где используется -## Контракт +- `:server` — Ktor-фасад, маппит `Agent` ↔ HTTP/SSE. +- `:client` — Ktor-клиент, маппит HTTP/SSE ↔ `Agent/Conversation`. +- `:agentik-cli`, `:agentik-tui` — оба работают поверх `:client`, + а следовательно поверх `:proto`. +- `:standalone` — реализует `Agent` (через `ChatAgent`) и пишет/читает + `Message`/`Event` напрямую через storage. + +## Как подключить + +```kotlin +// build.gradle.kts +kotlin { + sourceSets.commonMain.dependencies { + api("pw.binom.agentik:proto:0.1.0") + } +} +``` + +Артефакт `pw.binom.agentik:proto:0.1.0` живёт в Nexus-репозитории +`caffeine` (HTTP `http://<your-nexus>/repository/caffeine/`, plain-HTTP, +credentials — через переменные `binom.repo.user/password/url`). + +## Версии + +Каталог `gradle/libs.versions.toml`, секция `[versions]` → `agentik-proto`. +Поднять версию → переопубликовать все KMP-таргеты через `./gradlew +:proto:publish -Pversion=...` (или триггернуть Gitea release). + +Текущие KMP-таргеты: `jvm + macosX64/macosArm64 + +iosX64/iosArm64/iosSimulatorArm64 + linuxX64/linuxArm64 + mingwX64`. + +## Публикация + +Настройки в `gradle.properties` / env: `binom.repo.url`, `binom.repo.user`, +`binom.repo.password`. `./gradlew :proto:publish` публикует все +target-specific артефакты + общий `kotlinMultiplatform`. + +## Основные типы ```kotlin interface Agent { - val id: String - suspend fun createConversation(temp: Boolean = false): Conversation + fun id: String + suspend fun createConversation(title: String? = null): 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-уведомления о диалогах + suspend fun getConversations(offset: Int = 0): Flow<Conversation> + suspend fun events(after: Instant): Flow<AgentEvent> // created/deleted/renamed } 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 send(content: List<Content>): Flow<Event> // write+read вместе, как раньше + suspend fun events(after: Instant): Flow<Event> // отдельная live-подписка + suspend fun getMessages(offset: Int = 0): Flow<Message> suspend fun rename(title: String): Boolean - suspend fun interrupt(): Unit - fun close() // освобождает ресурсы + fun interrupt() +} + +sealed interface Content { + class Text(val body: String) : Content + class Image(val data: ByteArray, val mime: String) : Content +} + +sealed interface Message { + val id: String + val date: Instant + interface Body : Message { val content: List<Content> } + interface System : Message + class UserMessage(...) : Body + class AssistantMessage(...) : Body + class ToolCall(...) : System + class ToolResult(...) : System +} + +sealed interface Event { + enum ResponseType { TEXT, IMAGE } + class StartReasoning(...) : Event + class StartResponse(val type: ResponseType) : Event + class AppendText(val body: String) : Event + class AppendImage(val body: ByteArray, val mime: String) : Event + class End(...) : Event + class Interrupted(...) : Event + class Error(val message: String, val code: Int? = null) : Event + class ToolCall(...) : Event + class ToolResult(...) : Event } ``` -Иерархии: +## Чего здесь НЕТ -- `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)` +- Никакого HTTP/SSE/JSON. Это контракт. Сериализация живёт в `:server` + и `:client`. +- Никакого хранения. Реализации `MessageStore` живут в `:storage-*`. +- Никакой логики прерывания / инструментов / LLM-вызовов. Это всё + внутри `:standalone` (ChatAgent) и выше. -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") +``` +./gradlew :proto:jvmTest +./gradlew :proto:allTests # дополнительно linuxX64 (если Linux) / iosSimulator (если macOS) ``` -`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`). +Используется продакшеном. Иммутабельный API (после рефакторинга из +AG-UI). Возможные будущие расширения: typed tool-result, multi-modal +contents, server-pushed references — все обсуждаются через общий +[IRC-QUESTIONS.md](../IRC-QUESTIONS.md). diff --git a/server/README.md b/server/README.md index 65f1615..4547eea 100644 --- a/server/README.md +++ b/server/README.md @@ -1,66 +1,88 @@ -# :server — `pw.binom.agentik.server` +# `:server` — HTTP/SSE фасад для `:proto` (KMP, JVM-only) -**Ktor-маршруты, превращающие `pw.binom.agentik.proto.Agent` в HTTP+JSON+SSE фасад.** -Подключается к любому `Application` через `Route.agentikAgent(...)`, монтируется -под заданным `path` (по умолчанию `/agentik`). +## Что это -## Какую проблему решает +Ktor-маршрут, экспонирующий `Agent` из `:proto` в виде JSON-API: +`POST /agentik/conversations`, `POST /agentik/conversations/:id/send`, +`GET /agentik/conversations/:id/events` (SSE), `GET /health`, +`GET /agentik/conversations`. -`:proto` — это чистый Kotlin-контракт (Agent/Conversation). Чтобы по нему -говорить с удалённым сервером нужен мост. `:server` — этот мост, но **только -маршруты**: без привязки к конкретному engine (CIO/Netty), без агрегации -прочих протоколов (AG-UI/A2A), без инициализации БД/памяти. Один extension-метод -на `Route`, и ваш Agent доступен по HTTP. +- **stateful** — сервер не принимает полную историю, только новые + сообщения. История хранится там, где развёрнут `Agent`. +- **декларативно** — `interface Agent` → HTTP; никакой магии, никаких + обёрток. Контракт и сериализация — тоже декларативные (kotlinx-json + с snake_case-дискриминаторами). -## Что отдаёт +Решает: позволяет собрать любой собственный front-end (CLI/TUI/Web/ +IRC/MCP) общаясь с одним сервером по стабильному wire-контракту. -| Метод | Путь | Назначение | -|---|---|---| -| `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`). +- `:standalone` подключает `Route.agentikAgent(agent)` в свой + embedded Netty engine. +- Любые клиенты (наши `:client`, `:agentik-cli`, `:agentik-tui`, или + внешние web-фронтенды) идут через этот контракт. -## Подключение +## Как подключить ```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 — ваш выбор +// build.gradle.kts (KMP JVM target) +plugins { id("pw.binom.agentik.server-conventions") version "0.1.0" } +dependencies { + api("pw.binom.agentik:server:0.1.0") + api("pw.binom.agentik:proto:0.1.0") +} -// ваш код -import pw.binom.agentik.server.agentikAgent - -fun Application.module() { +// ваш код: +fun Application.module(agent: Agent) { install(ContentNegotiation) { json(agentikJson) } + install(SSE) routing { - agentikAgent(agent = myAgent) // mount на /agentik - agentikAgent(agent = myAgent, path = "/v1/agent") // или под другим путём + route("/agentik") { agentikAgent(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` +`gradle/libs.versions.toml` → `[versions] agentik-server`. -## Сборка +## Эндпоинты (path по умолчанию `/agentik`, через `agentikAgent(agent, "/my")`) -```bash -./gradlew :server:build +| Метод | Путь | Что делает | +|---|---|---| +| `POST` | `/conversations` | Создать диалог (body: `{title?}`) | +| `GET` | `/conversations` | Список диалогов (по `?offset=&limit=`) | +| `GET` | `/conversations/:id` | Снимок диалога + count | +| `GET` | `/conversations/:id/messages` | История сообщений (по `?after=`) | +| `POST` | `/conversations/:id/rename` | Переименовать (body: `{title}`) | +| `DELETE` | `/conversations/:id` | Удалить | +| `POST` | `/conversations/:id/send` | Send-флоу (body: `{content:[…]}` → SSE) | +| `GET` | `/conversations/:id/events` | Live подписка (SSE) | +| `POST` | `/conversations/:id/interrupt` | Прервать текущий `send()` | + +`Content-Type: text/event-stream` всегда для SSE, ноль-лишних +заголовков. Сообщения: `event: <name>` (`message`, `start_reasoning`, +`start_response`, `append_text`, `append_image`, `end`, `interrupted`, +`error`) + `data: <JSON>`. + +## Тесты + +``` ./gradlew :server:jvmTest ``` -KMP-таргеты: те же, что у `:proto` (jvm + 8 нативов). +Покрывают: маппинг JSON ↔ Event, SSE framing, error-handling, +404 / 400 ответы, корректную обработку `Instant` в `kotlinx-datetime`. + +## Чего здесь НЕТ + +- Никакого LLM-кода, tool-вызовов, прерываний. Только mapping Agent ↔ HTTP. +- Никакой БД, никакого storage. Это задача `Agent`-имплементации. +- Никакого CORS-конфига по умолчанию — добавляйте на свой engine. + +## Текущий статус + +Используется продакшеном. Wire-контракт стабильный; новые Event'ы +добавляются только с snake_case-дискриминаторами и строго обратно +совместимо. diff --git a/skills/README.md b/skills/README.md index 48be993..12fa312 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,77 +1,79 @@ -# :skills — `pw.binom.agentik.skills` +# `:skills` — парсер SKILL.md (KMP, JVM-only) -**Парсер opencode-style скилов: `SKILL.md` или `*.yaml` с YAML-frontmatter + markdown body.** -Загружается в system prompt как отдельная секция; модели доступны тулы -`read_skill` / `skill_save` / `skill_delete` (если они подключены через -`:agent-toolsets`). +## Что это -## Какую проблему решает +Парсер и runtime для навыков агента в формате [opencode Skills](https://docs.opencode.dev): -Агенту нужно **знать**, какие процедуры/инструкции у него есть, не таская их -в коде. Скил — это: +- **SKILL.md / \*.yaml** с YAML-frontmatter (`name`, `description`, + `allowed-tools`, etc.) и markdown-телом. +- Реестр `SkillCatalog`, лоадер `SkillLoader` (поиск по + `~/.agentik/skills/`). +- `Skill` имеет стабильный id, описание, может требовать определённые + tools (`allowed-tools: [run_command, write_file]`) — это контролируется + на уровне вызова. +- Загруженные скиллы аггрегируются в system-prompt через + `Skill.toSystemPromptSection()` или подгружаются по требованию через + tool `read_skill`. + +Решает задачу: агенту нужно объяснить "что я умею" на разных языках +(нативный skill-вызов vs. описание), нужно уметь включать/выключать +навыки по требованию, и нужно хранить текстовые навыки прямо в +git-репозитории (а не в БД). + +## Где используется + +- `:standalone` подгружает все SKILL.md из `~/.agentik/skills/` + и инструментового скилл-майнера (skill-mining: создание новых + SKILL.md по LLM-рефлексии). +- Активно юзается для: `code-review`, `arch-summary`, `telegram-reply`, + любых "habits" агента. + +## Как подключить + +```kotlin +kotlin { + sourceSets.commonMain.dependencies { + api("pw.binom.agentik:skills:0.1.0") + } +} +``` + +## Версии + +`gradle/libs.versions.toml` → `[versions] agentik-skills`. + +## Пример SKILL.md ```markdown --- -name: backend:spring:db-base -description: Правила JPA/Flyway миграций и репозиториев в agentik. +name: code-review +description: Review uncommitted diff and produce line-anchored comments. +allowed-tools: [run_command, read_file] --- -Правила, которые применяются ко всем изменениям схемы: -- миграции только в Flyway; -- новые колонки nullable by default; -- ... + +You are a strict reviewer. For every change in the diff, output: +- File: <path> +- Severity: <nit|warning|blocker> +- Comment: <one sentence> + +Only mention issues that are objectively wrong. Do not refactor. ``` -Парсер читает такие файлы из каталога (рекурсивно), валидирует обязательные -поля (`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`. +Покрывают: парсинг yaml-frontmatter, обработку отсутствующих полей, +unicode-имена, дубликаты id, очень большое тело. + +## Чего здесь НЕТ + +- Никакого HTTP / tool-вызова. Парсер и реестр — не более. +- Никакой БД. SKILL.md живут в файлах под управлением пользователя. + +## Текущий статус + +Используется продакшеном. Парсер простой и предсказуемый; расширять +формат frontmatter можно без поломок (новые поля игнорируются). diff --git a/standalone/README.md b/standalone/README.md index 1af8bb9..9c7a6b0 100644 --- a/standalone/README.md +++ b/standalone/README.md @@ -1,154 +1,144 @@ -# :standalone — agentik single-jar server +# `:standalone` — single-jar HTTP-сервер со всеми транспортами -Self-contained HTTP-сервер на Ktor: AG-UI / A2A / :proto транспорты на одном порту, -встроенный SQLite для истории диалогов, долговременная память (Hermes-style -§-файлы или vector+JVector), загрузка MCP-инструментов, навыков (SKILL.md) и -персоны (SOUL.md). +## Что это -## Сборка +Главный исполняемый модуль проекта — single-jar HTTP-сервер с: + +- **AG-UI** transport на `POST /agui` (SSE) + `GET /health`. +- **A2A** transport на `POST /` (JSON-RPC) + `GET /.well-known/agent-card.json`. +- **`:proto`** transport на `POST /agentik/*` (HTTP+JSON+SSE) — наш stateful. +- **Embedded LLM backend**: `GOOGLE` (LiteRT) или `OPENAI`-совместимый + (vLLM, LiteLLM, OpenAI API). +- **SQLite persistence** через `:storage-sqlite`. +- **Memory backend**: `md` (файловый) или `vector` (SQLite+JVector+ + HTTP/SIGLIP-embeddings). +- **Skills** из `~/.agentik/skills/*.md`. +- **SOUL** из `~/.agentik/SOUL.md`. +- **Background подпроцессы**: рефлексия, skill-mining, + memory-reviewer. + +Решает: даёт пользователю один JAR (10–250 МБ), который запускается +через `java -jar agentik-0.1.0-all.jar`, и поднимает сразу все +транспорты, которые другие системы могут хавать. + +## Как запустить + +### Требования + +- JVM 21+. +- (Опционально) CUDA-устройство для `:backend=google` (LiteRT). +- (Опционально) LM через OpenAI-совместимый endpoint (vLLM / Ollama + / OpenAI) для `:backend=openai`. + +### Запуск из готового fatjar ```bash -# Полная сборка всего проекта + fatjar -./gradlew assemble - -# Только fatjar :standalone (≈ 250 MB) -./gradlew :standalone:shadowJar - -# Результат: -# standalone/build/libs/standalone-all.jar +java --enable-native-access=ALL-UNNAMED \ + -jar agentik-0.1.0-all.jar ``` -Также публикуется в Nexus (`caffeine` репо) при создании релиза: -`pw.binom.agentik:standalone:VERSION` с classifier `all` (см. -`https://git.binom.pw/subochev/agentik/releases`). +С дефолтами — встроенный SQLite, OpenAI-compatible backend на +`http://localhost:8001/v1`, порт 8080. -## Запуск +### Запуск через Gradle (dev) ```bash -java -jar standalone/build/libs/standalone-all.jar +./gradlew :standalone:run ``` -По умолчанию слушает на `http://localhost:8080`. Healthcheck: `GET /health`. - -Транспорты на одном порту: -- `GET /health` — liveness -- `POST /agentik/conversations` — создать беседу -- `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) - -### Подкоманды +### `pull-model` subcommand (для LiteRT) ```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 +# Сначала скачать модель под LiteRT-Gemma-4-E2B +AGENTIK_LLM_BACKEND=google \ +AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \ + java --enable-native-access=ALL-UNNAMED \ + -jar agentik-0.1.0-all.jar pull-model ``` -## Переменные окружения +Скачивает `https://static.binom.pw/models/gemma-4-E2B-it.litertlm` +(2.5 ГБ, с Range-resume). Поддерживает override через +`AGENTIK_GOOGLE_MODEL_URL` и verify через +`AGENTIK_GOOGLE_MODEL_SHA256_URL`. -Все переменные читаются `AgentikConfig.fromEnv()`. Пусто или отсутствие → дефолт. +## Переменные среды -| Переменная | Дефолт | Назначение | +Полный список — общий для всего `:standalone`-процесса: + +| Env | Default | Что делает | |---|---|---| | `AGENTIK_PORT` | `8080` | Порт HTTP-сервера | -| `AGENTIK_DB_PATH` | `./agentik.db` | Путь к SQLite (история бесед + метаданные памяти) | -| `AGENTIK_SKILLS_DIR` | _выкл._ | Каталог со скилами (`SKILL.md` / `*.yaml`) | -| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory` (md) или `AGENTIK_DB_PATH` (vector) | Каталог памяти (md); `"off"` отключает | -| `AGENTIK_MEMORY_BACKEND` | `md` | `md` (Hermes-style §-файлы) / `vector` (SQLite + JVector + LLM-эмбеддинги) / `off` | -| `AGENTIK_EMBEDDING_MODEL` | `text-embedding-3-small` | Модель эмбеддингов для vector-бэкенда (только HTTP) | -| `AGENTIK_EMBEDDING_DIMENSION` | `1536` | Размерность вектора (только HTTP; SIGLIP определяет автоматически) | -| `AGENTIK_EMBEDDING_BACKEND` | `HTTP` | `HTTP` (POST /v1/embeddings) или `SIGLIP` (on-device, без сети) | -| `AGENTIK_EMBEDDING_MODEL_PATH` | _только SIGLIP_ | Путь к `text_model_int8.onnx` (SigLIP2) | -| `AGENTIK_EMBEDDING_TOKENIZER_PATH` | _только SIGLIP_ | Путь к `tokenizer.model` (sentencepiece) | -| `AGENTIK_SOUL` | _выкл._ | Путь к `SOUL.md` — файл персоны (markdown), вставляется в начало system prompt | -| `AGENTIK_LLM_BACKEND` | — | `openai` или `google` (см. ниже) | -| `AGENTIK_MCP_CONFIG` | _выкл._ | Путь к JSON со списком MCP-серверов | -| `AGENTIK_SYSTEM_PROMPT` | `be brief` | Базовый system prompt | -| `OPENAI_CONTEXT_WINDOW` | _выкл._ | Лимит контекстного окна в токенах (для compaction'а) | -| `AGENTIK_GOOGLE_CONTEXT_WINDOW` | _выкл._ | То же для Google backend | -| `AGENTIK_COMPRESSION_THRESHOLD` | `0.8` | Доля лимита, при которой запускается compaction | -| `AGENTIK_REFLECTION_INTERVAL` | `10` | Self-reflection: каждый N-й пользовательский ход агент оценивает себя (LiteLlm) и сохраняет рефлексию. `0` = выключено. | -| `AGENTIK_REFLECTION_TOP_K` | `3` | Сколько последних рефлексий подмешивать в system prompt как «слабые места». `0` = не подмешивать. | -| `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` логику). | +| `AGENTIK_DB_PATH` | `./agentik.db` | Путь к SQLite | +| `AGENTIK_AGENT_ID` | `agentik` | ID агента (для multi-instance) | +| `AGENTIK_LLM_BACKEND` | `openai` | `openai` или `google` | +| `AGENTIK_LLM_MODEL` | (выбирается по backend) | Имя модели | +| `AGENTIK_LLM_API_URL` | `http://localhost:8001/v1` | Endpoint для OpenAI-compatible | +| `AGENTIK_LLM_API_KEY` | `no-key-needed` | Auth header | +| `AGENTIK_LLM_CONTEXT_TOKENS` | `115000` | Сколько токенов остаётся модели | +| `AGENTIK_GOOGLE_MODEL_PATH` | `/root/gemma-4-E2B-it.litertlm` | Путь к `.litertlm` файлу | +| `AGENTIK_GOOGLE_MODEL_URL` | `https://static.binom.pw/models/gemma-4-E2B-it.litertlm` | Откуда скачивать | +| `AGENTIK_GOOGLE_MODEL_SHA256_URL` | — | Если задан — verify по SHA-256 | +| `AGENTIK_AUTO_DOWNLOAD_MODEL` | `0` | `1` = скачать модель если её нет | +| `AGENTIK_MEMORY_BACKEND` | `vector` | `md`, `vector` или `off` | +| `AGENTIK_EMBEDDING_BACKEND` | `http` | `http` или `siglip` (только для `vector`) | +| `AGENTIK_EMBEDDING_API_URL` | `http://localhost:8001/v1` | Endpoint для эмбеддингов | +| `AGENTIK_EMBEDDING_MODEL` | `text-embedding-3-small` | Имя embedding-модели | +| `AGENTIK_SOUL_PATH` | `~/.agentik/SOUL.md` | Путь к SOUL.md | +| `AGENTIK_SKILLS_DIR` | `~/.agentik/skills/` | Каталог SKILL.md | +| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory/` | Каталог для md-памяти | +| `AGENTIK_TOOLSETS_DEFAULT` | `memory,skills,files,web` | Включённые тулы | +| `AGENTIK_DEBUG` | `0` | `1` = verbose logging | -### OpenAI backend +Значения читаются через `AgentikConfig.fromEnv()` в `:standalone/.../Main.kt`. -| Переменная | Обязательна | Назначение | -|---|---|---| -| `OPENAI_BASE_URL` | да | Например, `https://api.openai.com/v1` | -| `OPENAI_API_KEY` | да | API key | -| `OPENAI_MODEL` | да | Имя модели (`gpt-4o-mini` и т.п.) | +## Эндпоинты -### Google backend - -| Переменная | Обязательна | Назначение | -|---|---|---| -| `AGENTIK_GOOGLE_MODEL_PATH` | да | Путь к `.litertlm` файлу | -| `AGENTIK_GOOGLE_MODEL_URL` | нет | URL для скачивания (default: `https://static.binom.pw/models/gemma-4-E2B-it.litertlm`) | -| `AGENTIK_GOOGLE_MODEL_SHA256_URL` | нет | URL с эталонным SHA-256 (если задано — файл проверяется) | - -## Полный пример запуска - -```bash -export AGENTIK_PORT=8080 -export AGENTIK_DB_PATH=/var/lib/agentik/state.db -export AGENTIK_MEMORY_DIR=/var/lib/agentik/memory -export AGENTIK_SOUL=/etc/agentik/SOUL.md -export AGENTIK_SKILLS_DIR=/etc/agentik/skills -export AGENTIK_MCP_CONFIG=/etc/agentik/mcp.json - -export AGENTIK_LLM_BACKEND=openai -export OPENAI_BASE_URL=https://api.openai.com/v1 -export OPENAI_API_KEY=sk-… -export OPENAI_MODEL=gpt-4o-mini - -mkdir -p "$(dirname "$AGENTIK_DB_PATH")" \ - "$AGENTIK_MEMORY_DIR" \ - "$(dirname "$AGENTIK_SOUL")" \ - "$(dirname "$AGENTIK_MCP_CONFIG")" - -java -jar standalone/build/libs/standalone-all.jar -``` - -На старте выведет что-то вроде: - -``` -agentik standalone listening on http://localhost:8080 - GET /health - POST /agentik/conversations -> 201 - GET /agentik/conversations/{id}/events -> SSE - storage: /var/lib/agentik/state.db - llm: OPENAI gpt-4o-mini @ https://api.openai.com/v1 - mcp: 4 tools from 2 servers - skills: 3 loaded from /etc/agentik/skills - soul: /etc/agentik/SOUL.md (842 chars) - memory: /var/lib/agentik/memory (md-backend) - compaction: enabled, threshold=0.8, window=128000 tokens - curator: enabled (interval=1d, maxAge=90d) - reflection: enabled (interval=10, topK=3) -``` - -## Остановка - -`Ctrl-C` → срабатывает shutdown hook: агент, MCP-серверы, SQLite-стора и -LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы -получают SIGTERM). - -## Клиенты к :standalone - -- `:agentik-cli` — REPL со slash-командами (`/new`, `/list`, `/interrupt` …). -- `:agentik-tui` — Compose-style TUI, чисто клавиатурная навигация. +| Метод | Путь | Transport | Описание | +|---|---|---|---| +| `GET` | `/health` | любой | health-check (`{"ok":true}`) | +| `POST` | `/agui` | AG-UI | Стриминг run (SSE) | +| `POST` | `/` | A2A | JSON-RPC `message/send`, `tasks/get`, `tasks/cancel` | +| `GET` | `/.well-known/agent-card.json` | A2A | Discovery | +| `POST` | `/agentik/conversations` | :proto | Создать диалог | +| `GET` | `/agentik/conversations` | :proto | Список диалогов | +| `GET` | `/agentik/conversations/:id` | :proto | Snapshot | +| `GET` | `/agentik/conversations/:id/messages` | :proto | История | +| `POST` | `/agentik/conversations/:id/send` | :proto | Send (SSE) | +| `GET` | `/agentik/conversations/:id/events` | :proto | Live-events (SSE) | +| `POST` | `/agentik/conversations/:id/interrupt` | :proto | Прервать | +| `POST` | `/agentik/conversations/:id/rename` | :proto | Переименовать | +| `DELETE` | `/agentik/conversations/:id` | :proto | Удалить | ## Тесты -```bash -./gradlew :standalone:jvmTest ``` +./gradlew :standalone:jvmTest # unit-тесты +./gradlew :standalone:integrationTest # integration (Testcontainers) +./gradlew :standalone:shadowJar # → build/libs/agentik-0.1.0-all.jar +``` + +## Известные ограничения + +1. **vLLM не поддерживает cancel-inference** (`interrupt()` только + закрывает client SSE-socket; бэкенд всё равно генерирует до конца). +2. **A2A JSON discriminator — `"kind"`** (text/file/data), + а не `"type"`. См. `A2aJson` в `:standalone`. +3. **SSE в не-TTY ssh закрывается на default Ktor timeout**. + +## Текущий статус + +Production-ready. Все KMP-модули проекта интегрированы. Полный +manual-test checklist смотрите в [`MANUAL-TESTS.md`](../../MANUAL-TESTS.md) +или `MANUAL-TESTS.md` в корне. + +## Где скачать + +- **Source**: `git clone https://git.binom.pw/subochev/agentik` +- **Fatjar**: Gitea CI artifacts (через `.gitea/workflows/release.yml` + на tag `v*`) или собирается через `./gradlew :standalone:shadowJar`. + +## Версии + +Все `gradle/libs.versions.toml`. Поднять версию → release через +`git tag v0.2.0 && git push --tags` → CI собирает все KMP-таргеты +публикует артефакты. diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index da546ed..0ea3198 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -12,6 +12,13 @@ plugins { alias(libs.plugins.shadow) } +// CI-флаг: при -PskipVectorMemory=true :memory-vector не подключается как +// зависимость — нужно для CI runner'а (text-embedding-kmp ещё не опубликован +// в caffeine). Подробнее см. memory-vector/build.gradle.kts. +val skipVectorMemory: Boolean = + (project.findProperty("skipVectorMemory") == "true") || + System.getenv("SKIP_VECTOR_MEMORY") == "1" + kotlin { jvmToolchain(21) @@ -46,7 +53,9 @@ kotlin { // Долговременная память (Hermes-style MD-бэкенд) + хранилище истории. implementation(project(":memory-api")) implementation(project(":memory-md")) - implementation(project(":memory-vector")) + if (!skipVectorMemory) { + implementation(project(":memory-vector")) + } implementation(project(":storage-core")) implementation(project(":storage-sqlite")) implementation(project(":agent-toolsets")) diff --git a/storage-core/README.md b/storage-core/README.md index f85862d..92e7e38 100644 --- a/storage-core/README.md +++ b/storage-core/README.md @@ -1,77 +1,77 @@ -# :storage-core — `pw.binom.agentik.storage` +# `:storage-core` — контракт хранилища (KMP, jvm + native) -**Интерфейсы хранилища разговорной истории агента: `ConversationStore`, -`MessageStore`, `WorkingMemoryStore`, `ReflectionStore`.** -Без зависимостей от конкретной БД. +## Что это -## Какую проблему решает +Интерфейсы persistence-уровня для `:standalone`: -`:standalone` нужно сохранять диалоги между перезапусками, держать working -memory (сжатую историю, которую видит LLM), reflection-записи. Но привязываться -к конкретной БД (SQLite) в контракте нельзя — для Android подходит другая -история, для тестов — in-memory, для экспериментов с Postgres — третья. +- `MessageStore` — append-only история сообщений по диалогу. +- `WorkingMemoryStore` — rolling buffer текущего хода (`AssistantMessage`, + `ToolExchange`, `UserMessage`, system-prompt) для fast-recovery + при reconnect/relance. +- `ConversationStore` — метаданные диалогов (id, title, model, + timestamps). +- `ReflectionStore` — LLM-reflections (свободная форма заметок + хранителя). -`:storage-core` отделяет **что хранить** (контракт) от **где хранить** -(бэкенды — `:storage-sqlite`, `:storage-inmemory`, и в будущем `:storage-android`). -Агрегатор `StorageBundle` собирает все 4 стора разом. +Решает: позволяет запустить агента на Android-in-memory, на +desktop-SQLite, или на production-SQLite, не переписывая логику. +Контракт минимален и async-friendly. -## Что в контракте +## Где используется + +- `:storage-inmemory` — для тестов и Android. +- `:storage-sqlite` — прод (Desktop / server / однодесктопный + Android-development). + +## Как подключить ```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> +kotlin { + sourceSets.commonMain.dependencies { + api("pw.binom.agentik:storage-core:0.1.0") + } } ``` -`WorkingMemoryEntry` — `sealed interface`: `User`, `Assistant`, `ToolExchange`, -`Summary`. Каждый entry имеет `instant` (когда попал в working memory) и -`index` (порядковый номер для восстановления при компакции). +## Версии -## Подключение +`gradle/libs.versions.toml` → `[versions] agentik-storage-core`. + +## Что в API ```kotlin -commonMain { - implementation("pw.binom.agentik:storage-core:$version") - // + бэкенд: - implementation("pw.binom.agentik:storage-inmemory:$version") // тесты - // или - implementation("pw.binom.agentik:storage-sqlite:$version") // прод +interface MessageStore { + suspend fun append(conversationId: String, message: Message): Unit + suspend fun after(conversationId: String, instant: Instant, limit: Int = 100): List<Message> +} + +interface WorkingMemoryStore { + suspend fun save(conv: String, entry: WorkingMemoryEntry): Unit + fun load(conv: String): Flow<WorkingMemoryEntry> // cold flow + suspend fun clear(conv: String): Unit +} + +sealed interface WorkingMemoryEntry { + val id: String + val date: Instant + class UserMessage(...) : WorkingMemoryEntry + class AssistantMessage(...) : WorkingMemoryEntry + class ToolExchange(val toolName: String, val toolArgsJson: String, val resultText: String, val wasCancelled: Boolean) : WorkingMemoryEntry + class SystemPrompt(...) : WorkingMemoryEntry } ``` -## Где смотреть версии +## Тесты -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +Контрактные тесты (общие для всех имплементаций) — в `:storage-sqlite` +и `:storage-inmemory`. -## Сборка +## Чего здесь НЕТ -```bash -./gradlew :storage-core:build -``` +- Никакого HTTP / SSE. +- Никакой конкретной БД. Backend'ы в `:storage-*`. -KMP-таргеты — полный набор. Зависимости — только `kotlinx-coroutines` + -`kotlinx-serialization`. +## Текущий статус + +Используется продакшеном. Контракт зафиксирован после +interrupt-имплементации (см. [INTERRUPT-DESIGN.md](../../docs/INTERRUPT-DESIGN.md)). diff --git a/storage-core/src/commonTest/kotlin/pw/binom/agentik/storage/PayloadTest.kt b/storage-core/src/commonTest/kotlin/pw/binom/agentik/storage/PayloadTest.kt index f0dc8b0..7b1b393 100644 --- a/storage-core/src/commonTest/kotlin/pw/binom/agentik/storage/PayloadTest.kt +++ b/storage-core/src/commonTest/kotlin/pw/binom/agentik/storage/PayloadTest.kt @@ -59,7 +59,7 @@ class PayloadTest { } @Test - fun `legacy plain-array payload still decodes (backward compat)`() { + fun `legacy plain-array payload still decodes backward compat`() { val legacy = "[" + """{"type":"text","body":"old message"}""" + "]" diff --git a/storage-inmemory/README.md b/storage-inmemory/README.md index e740190..9d010b6 100644 --- a/storage-inmemory/README.md +++ b/storage-inmemory/README.md @@ -1,42 +1,55 @@ -# :storage-inmemory — `pw.binom.agentik.storage.inmemory` +# `:storage-inmemory` — in-memory реализация `:storage-core` (KMP, jvm + native) -**In-memory реализация всех 4 сторов из `:storage-core`.** -Без платформенного IO, KMP-таргеты полностью. Тред-безопасна через `Mutex`. +## Что это -## Какую проблему решает +In-memory реализация `MessageStore / WorkingMemoryStore / +ConversationStore / ReflectionStore`. Все структуры держит в +`ConcurrentHashMap` + `MutableList`, фолотится на RAM +(никаких файлов). -- **Тесты** — мгновенный setup/teardown, нет файловой системы, нет JDBC, - детерминированное состояние. -- **Embedded-сценарии** (Android ART, iOS) где SQLDelight native-драйвер пока - недоступен или избыточен. -- **Отладка** — можно в рантайме дампить `MessageStore.listByConversation()` и - смотреть что попало в working memory без sqlite3 CLI. +Решает: дешёвая тестовая среда без поднятия SQLite. Позволяет +прогонять `ChatAgentTest` за миллисекунды и держать сценарии +детерминированными. -Семантика **полностью совпадает** с `:storage-sqlite`: один и тот же контракт -`:storage-core`, одинаковые `AutoCloseable`, одинаковая тред-безопасность. +## Где используется -## Подключение +- В тестах `:standalone` (`AbstractITTest`). +- В Android-имплементации (in-memory + Android-database микс). +- В любых юнит-тестах на агенте. + +## Как подключить ```kotlin -commonMain { - implementation("pw.binom.agentik:storage-inmemory:$version") +commonMain.dependencies { + api("pw.binom.agentik:storage-inmemory:0.1.0") + api("pw.binom.agentik:storage-core:0.1.0") } -// Использование -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) +val storage = InMemoryStorageSystem() +val messages: MessageStore = storage.messages +val working: WorkingMemoryStore = storage.working ``` -## Где смотреть версии +## Версии -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +`gradle/libs.versions.toml` → `[versions] agentik-storage-inmemory`. -## Сборка +## Тесты -```bash -./gradlew :storage-inmemory:build +``` +./gradlew :storage-inmemory:allTests ``` -KMP-таргеты — полный набор. Зависимости — только `:storage-core`. +Покрывают (через общие contract-tests): round-trip, paged flow, +concurrent appends, working-memory replay, очистку. + +## Чего здесь НЕТ + +- Никакого persistence. Перезапуск процесса — данные пропали. + Это нормально для тестов и Android in-memory. + +## Текущий статус + +Используется продакшеном (в режиме тестов). Контракт-совместима +с `:storage-sqlite` 1:1 — переключение `AGENTIK_STORAGE_BACKEND=memory` +в `:standalone`. diff --git a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryConversationStoreTest.kt b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryConversationStoreTest.kt index a658be8..00fe180 100644 --- a/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryConversationStoreTest.kt +++ b/storage-inmemory/src/commonTest/kotlin/pw/binom/agentik/storage/inmemory/InMemoryConversationStoreTest.kt @@ -68,7 +68,7 @@ class InMemoryConversationStoreTest { } @Test - fun `rename updates title and updatedAt, returns new updatedAt`() = runTest { + fun `rename updates title and updatedAt returns new updatedAt`() = runTest { val store = InMemoryConversationStore() val t0 = Instant.parse("2026-09-15T10:00:00Z") store.upsert(ConversationRecord("c1", null, false, t0, t0)) diff --git a/storage-sqlite/README.md b/storage-sqlite/README.md index e1e15a5..7a1f067 100644 --- a/storage-sqlite/README.md +++ b/storage-sqlite/README.md @@ -1,62 +1,78 @@ -# :storage-sqlite — `pw.binom.agentik.storage.sqlite` +# `:storage-sqlite` — SQLite реализация `:storage-core` (JVM-only) -**SQLDelight-реализация всех 4 сторов из `:storage-core` поверх SQLite.** -JVM-only (SQLDelight пока не публикует KMP-драйверы за пределами JVM/Android). +## Что что это -## Какую проблему решает +Production persistence для `:standalone` на [SQLDelight](https://cashapp.github.io/sqldelight/): -Прод-запуск `:standalone` должен переживать рестарт. In-memory не подходит — -нужна **реальная БД**. SQLite выбран потому что: +- **messages** — append-only журнал с `conversation_id`, `created_at`. +- **working_memory** — rolling buffer последних 100 entries, типы + в JSON (`UserMessage / AssistantMessage / ToolExchange / SystemPrompt`). +- **conversations** — метаданные (id, title, model, timestamps). +- **reflections** — произвольные заметки ("I notice you often + prefer short replies"). +- Промпт хранителя (`@mem0`) индексирован отдельно для быстрого + доступа. -- **Один файл** (`AGENTIK_DB_PATH`) — легко бэкапить, переносить, инспектить. -- **WAL** — запись не блокирует чтение; диалоги не фризят при compaction. -- **Без отдельного сервиса** — в отличие от PostgreSQL/MySQL не нужно ничего - поднимать рядом. +Решает: стабильная, локальная, нулевая-настройка БД. Подходит и для +desktop-продакшена, и для Android, и для тестов (через Testcontainers). -## Схема +## Где используется -SQLDelight `.sq`-файлы в `src/main/sqldelight/`: +- `:standalone` подключает по умолчанию (`storage.db` = путь из + `AGENTIK_DB_PATH`). -- `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) +jvmMain.dependencies { + implementation("pw.binom.agentik:storage-sqlite:0.1.0") + implementation("pw.binom.agentik:storage-core:0.1.0") } -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) +val storage = SqliteStorageSystem.open(Path("agentik.db")) +val messages: MessageStore = storage.messages ``` -`:standalone` инициализирует бэкенд автоматически по `AGENTIK_DB_PATH`. +## Версии -## Где смотреть версии +`gradle/libs.versions.toml` → `[versions] agentik-storage-sqlite`. -- `version` из `gradle.properties` (`version=0.1.0`) -- релизы: `https://git.binom.pw/subochev/agentik/releases` +Зависит от `app.cash.sqldelight:sqlite-driver:2.1.0` (через +`gradle/libs.versions.toml`). -## Сборка +## Тесты -```bash -./gradlew :storage-sqlite:build +``` +./gradlew :storage-sqlite:jvmTest ``` -JVM-only. Тянет `:storage-core` + `app.cash.sqldelight:runtime` + -`app.cash.sqldelight:coroutines-extensions` + `app.cash.sqldelight:sqlite-driver`. +Покрывают: миграции (через `migrations/` каталог и SQLDelight +`*.sqm`), round-trip, race-conditions (concurrent append), paged +flow. + +## Что в схеме (упрощённо) + +```sql +CREATE TABLE messages ( + id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL, + created_at TEXT NOT NULL, -- ISO Instant + kind TEXT NOT NULL, -- 'user', 'assistant', 'tool_call', 'tool_result' + body_json TEXT NOT NULL +); +CREATE INDEX idx_messages_conv_time ON messages(conversation_id, created_at); + +CREATE TABLE working_memory ( + conversation_id TEXT NOT NULL, + entry_id TEXT PRIMARY KEY, + created_at TEXT NOT NULL, + kind TEXT NOT NULL, + body_json TEXT NOT NULL +); +``` + +Полная схема + миграции — в `src/jvmMain/sqldelight/`. + +## Текущий статус + +Используется продакшеном. Миграции 1.0+.