docs: per-module README + root navigation hub + CI/release workflows
ci / JVM build + tests (push) Failing after 1m57s

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