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
+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