Phase 5: :memory-vector (JVector + SQLite + LLM-эмбеддинги), memory-abstraction, compaction, MessageContext
- :memory-api — общий контракт MemoryStore/Prefetcher/Reviewer/Tools/MemorySystem
- :memory-md (KMP, kotlinx-io) — Hermes-style §-файлы, keyword overlap
- :memory-vector (JVM-only) — JVector ANN + SQLite + HttpEmbeddingClient
- :standalone — AGENTIK_MEMORY_BACKEND={md,vector,off}, выбор в Main.kt
- :standalone — compaction рабочего контекста (LiteLlmContextCompactor + reviewPreCompaction)
- :proto — MessageContext (origin: user/system/event) на send и в Message
- :server — backward-compat dual-format для POST /messages
- README — env-vars, vector-бэкенд docs
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
# :standalone — agentik single-jar server
|
||||
|
||||
Self-contained HTTP-сервер с Ktor: AG-UI / A2A / :proto транспорты на одном порту,
|
||||
встроенный SQLite для истории диалогов, долговременная память (Hermes-style
|
||||
§-файлы), загрузка MCP-инструментов, навыков (SKILL.md) и персоны (SOUL.md).
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
# Полная сборка всего проекта + fatjar
|
||||
./gradlew assemble
|
||||
|
||||
# Только fatjar :standalone (≈ 150 MB)
|
||||
./gradlew :standalone:shadowJar
|
||||
|
||||
# Результат:
|
||||
# standalone/build/libs/standalone-all.jar
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
java -jar standalone/build/libs/standalone-all.jar
|
||||
```
|
||||
|
||||
По умолчанию слушает на `http://localhost:8080`. Healthcheck: `GET /health`.
|
||||
|
||||
Транспорты на одном порту:
|
||||
- `GET /health` — liveness
|
||||
- `POST /agentik/conversations` — создать беседу
|
||||
- `GET /agentik/conversations/{id}/events` — SSE-стрим ответов
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Все переменные читаются `AgentikConfig.fromEnv()`. Бланк или отсутствие → дефолт.
|
||||
|
||||
| Переменная | Дефолт | Назначение |
|
||||
|---|---|---|
|
||||
| `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-бэкенда |
|
||||
| `AGENTIK_EMBEDDING_DIMENSION` | `1536` | Размерность вектора (должна совпадать с моделью) |
|
||||
| `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 |
|
||||
|
||||
### OpenAI backend
|
||||
|
||||
| Переменная | Обязательна | Назначение |
|
||||
|---|---|---|
|
||||
| `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` файлу |
|
||||
|
||||
## 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.
|
||||
|
||||
## Память
|
||||
|
||||
`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 + LLM-эмбеддинги
|
||||
(запросы на `${OPENAI_BASE_URL}/v1/embeddings`). Семантический поиск:
|
||||
cosine similarity + recency-re-rank. Те же четыре тула; та же семантика.
|
||||
|
||||
```bash
|
||||
# Vector-бэкенд
|
||||
AGENTIK_MEMORY_BACKEND=vector \
|
||||
AGENTIK_EMBEDDING_MODEL=text-embedding-3-small \
|
||||
AGENTIK_EMBEDDING_DIMENSION=1536 \
|
||||
java -jar standalone-all.jar
|
||||
```
|
||||
|
||||
Для vector требуется `AGENTIK_LLM_BACKEND=openai` (т.к. нужен
|
||||
OpenAI-совместимый `/v1/embeddings` endpoint — LiteLLM proxy тоже подходит).
|
||||
Embedding-вызовы кэшируются LRU на 256 текстов — дедупликация при повторных
|
||||
запросах одинаковых промптов.
|
||||
|
||||
> **Опционально:** при старте 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
|
||||
```
|
||||
|
||||
## 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 …"} }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Полный пример запуска
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Остановка
|
||||
|
||||
`Ctrl-C` → срабатывает shutdown hook: агент, MCP-серверы, SQLite-стора и
|
||||
LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы
|
||||
получают SIGTERM).
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
./gradlew :standalone:jvmTest
|
||||
```
|
||||
Reference in New Issue
Block a user