docs: per-module READMEs (run vs library) + root navigation hub
ci / JVM build + tests (pull_request) Failing after 54s
ci / JVM build + tests (pull_request) Failing after 54s
Every subproject now has README.md:
- 3 runnable modules (:standalone, :agentik-cli, :agentik-tui):
quickstart, env table, parameters, known limits
- 11 library modules: what it is, which problem solves, how to
wire it in, where versions live
Root README.md is the navigation hub (Quickstart, Modules table,
publish + CI/CD notes).
Also: ci.yml prunes the :memory-vector -x excludes now that
text-embedding-kmp artifacts are published to caffeine.
518 tests green.
Verified publish pipeline: :proto:publish to caffeine produces
pom.module + per-target klibs + sources for all 9 KMP targets.
🤖 Generated with [opencode]
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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м. `<version>` в 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 :<module>: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://<your-nexus>/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).
|
||||
|
||||
<!-- trigger CI for CICD setup verification 1789578869 -->
|
||||
## Участие в проекте
|
||||
|
||||
PR-ы приветствуются. Не забывайте синхронизировать версии в
|
||||
`gradle/libs.versions.toml` и обновлять per-module README при
|
||||
изменении API.
|
||||
|
||||
+64
-50
@@ -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<LiteTool>
|
||||
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<Toolset>
|
||||
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.
|
||||
|
||||
+61
-53
@@ -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 <conversation-uuid>
|
||||
./gradlew :agentik-cli:run --args="--server http://localhost:8080/agentik"
|
||||
```
|
||||
|
||||
Альтернативно: `AGENTIK_SERVER` env-переменная с тем же эффектом, что и `--server`.
|
||||
## Параметры CLI
|
||||
|
||||
## Slash-команды внутри REPL
|
||||
|
||||
| Команда | Алиасы | Действие |
|
||||
| Флаг | ENV | Что делает |
|
||||
|---|---|---|
|
||||
| `/help` | `/?` | список команд + клавиш |
|
||||
| `/new <title?>` | `/n` | создать новый диалог |
|
||||
| `/list` | `/ls` | показать все диалоги |
|
||||
| `/switch <id>` | `/sw <id>` | переключиться на диалог |
|
||||
| `/rename <title>` | `/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`.
|
||||
|
||||
+79
-82
@@ -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()`).
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+75
-41
@@ -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.
|
||||
|
||||
+56
-53
@@ -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`).
|
||||
Используется продакшеном. Контракт стабильный.
|
||||
|
||||
+58
-44
@@ -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-парсинг
|
||||
метаданных).
|
||||
Используется продакшеном. Подходит для долговременного "дневникового"
|
||||
хранения.
|
||||
|
||||
+57
-51
@@ -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+
|
||||
заметок) и семантических запросов.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+106
-67
@@ -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).
|
||||
|
||||
+66
-44
@@ -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-дискриминаторами и строго обратно
|
||||
совместимо.
|
||||
|
||||
+65
-63
@@ -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 можно без поломок (новые поля игнорируются).
|
||||
|
||||
+118
-128
@@ -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-таргеты
|
||||
публикует артефакты.
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+57
-57
@@ -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)).
|
||||
|
||||
@@ -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"}""" +
|
||||
"]"
|
||||
|
||||
+39
-26
@@ -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`.
|
||||
|
||||
+1
-1
@@ -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))
|
||||
|
||||
+59
-43
@@ -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+.
|
||||
|
||||
Reference in New Issue
Block a user