9e12b22e85
* :memory-api — MemoryStore.archiveStale(maxAge, maxUseCount, now): default имплементация через list + delete (бэкенды могут переопределить). * :standalone — Curator: фоновая корутина на Dispatchers.IO, раз в сутки дёргает archiveStale(90d, 0). start()/stop(), runPass() — однократный прогон для тестов. * :standalone/Main — Curator стартует автоматически если memory включён, stop() в shutdown hook. * :standalone — публичный TestInMemoryMemoryStore вынесен из CompactionTest, переиспользуется в CuratorTest. * README — раздел "Куратор памяти" с описанием семантики для md и vector бэкендов. Tests: 288 total (+4 CuratorTest). Fatjar smoke-tested, curator стартует на app boot, выводит `[Curator] started` в лог. Defaults: interval=1d, maxAge=90d, maxUseCount=0. Override через новые config-флаги отложен.
347 lines
17 KiB
Markdown
347 lines
17 KiB
Markdown
# :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.
|
||
|
||
## Куратор памяти (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 + 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
|
||
curator: enabled (interval=1d, maxAge=90d)
|
||
```
|
||
|
||
## Остановка
|
||
|
||
`Ctrl-C` → срабатывает shutdown hook: агент, MCP-серверы, SQLite-стора и
|
||
LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы
|
||
получают SIGTERM).
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
./gradlew :standalone:jvmTest
|
||
```
|