Выносим интерфейсы и data-классы истории диалога (MessageStore / WorkingMemoryStore / ConversationStore / ReflectionStore + соответствующие sealed-иерархии MessageRecord / WorkingMemoryEntry / Content / ConversationRecord / Reflection + payload-утилиты) из :standalone в отдельный KMP-модуль :storage-core (pw.binom.agentik.storage). Цель — подготовка к Android-портированию и подключению альтернативных реализаций хранилища без затягивания всей :standalone. Дальше (commit 2/3) — :storage-inmemory и :storage-sqlite как самостоятельные модули, плюс :storage-android (deferred). Изменения: - Новый :storage-core (KMP, commonMain only, jvm + native таргеты) — 12 файлов - StorageBundle агрегатор (conversationStore + messageStore + workingMemoryStore + reflectionStore; SkillStore живёт в :skills и подключается отдельно) - 11 файлов импортов в :standalone переключены на новый пакет - SqliteReflectionStore оставлен в :standalone до commit 3 (зависит от SQLDelight AgentikDatabase, которую ещё не отвязали от :standalone) - 4 теста перенесены в :standalone/.../storage/ с обновлённым пакетом - PayloadTest переехал в :storage-core/commonTest (тестирует чистые типы) Tests: 264/264 green (179 :standalone + 6 :storage-core + прочие JVM-модули)
:standalone — agentik single-jar server
Self-contained HTTP-сервер с Ktor: AG-UI / A2A / :proto транспорты на одном порту, встроенный SQLite для истории диалогов, долговременная память (Hermes-style §-файлы), загрузка MCP-инструментов, навыков (SKILL.md) и персоны (SOUL.md).
Сборка
# Полная сборка всего проекта + fatjar
./gradlew assemble
# Только fatjar :standalone (≈ 150 MB)
./gradlew :standalone:shadowJar
# Результат:
# standalone/build/libs/standalone-all.jar
Запуск
java -jar standalone/build/libs/standalone-all.jar
По умолчанию слушает на http://localhost:8080. Healthcheck: GET /health.
Транспорты на одном порту:
GET /health— livenessPOST /agentik/conversations— создать беседуGET /agentik/conversations/{id}/events— SSE-стрим ответовPOST /a2a/— A2A JSON-RPC (message/send,tasks/get,tasks/cancel)GET /a2a/.well-known/agent-card.json— AgentCard
A2A
Адаптер A2aBridge гоняет A2A-контекст на диалог :proto: contextId мапится на
Conversation (пустой/неизвестный contextId → новый диалог). Ответ — склеенный
текст хода; id внутреннего диалога возвращается в metadata.agentikConversationId
ответа. Задачи живут в in-memory TaskStore (не переживают рестарт процесса).
Переменные окружения
Все переменные читаются 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-бэкенда (только HTTP) |
AGENTIK_EMBEDDING_DIMENSION |
1536 |
Размерность вектора (только HTTP; SIGLIP определяет автоматически) |
AGENTIK_EMBEDDING_BACKEND |
HTTP |
HTTP (POST /v1/embeddings) или SIGLIP (on-device, без сети) |
AGENTIK_EMBEDDING_MODEL_PATH |
только SIGLIP | Путь к text_model_int8.onnx (SigLIP2) |
AGENTIK_EMBEDDING_TOKENIZER_PATH |
только SIGLIP | Путь к tokenizer.model (sentencepiece) |
AGENTIK_SOUL |
выкл. | Путь к SOUL.md — файл персоны (markdown), вставляется в начало system prompt |
AGENTIK_LLM_BACKEND |
— | openai или google (см. ниже) |
AGENTIK_MCP_CONFIG |
выкл. | Путь к JSON со списком MCP-серверов |
AGENTIK_SYSTEM_PROMPT |
be brief |
Базовый system prompt |
OPENAI_CONTEXT_WINDOW |
выкл. | Лимит контекстного окна в токенах (для compaction'а) |
AGENTIK_GOOGLE_CONTEXT_WINDOW |
выкл. | То же для Google backend |
AGENTIK_COMPRESSION_THRESHOLD |
0.8 |
Доля лимита, при которой запускается compaction |
AGENTIK_REFLECTION_INTERVAL |
10 |
Self-reflection: каждый N-й пользовательский ход агент оценивает себя (LiteLlm) и сохраняет рефлексию. 0 = выключено. |
AGENTIK_REFLECTION_TOP_K |
3 |
Сколько последних рефлексий подмешивать в system prompt как «слабые места». 0 = не подмешивать. |
AGENTIK_SKILL_MINING_INTERVAL |
15 |
Skill mining: через сколько user-ходов запускать фоновый прогон SkillMiner. 0 = выключено. |
AGENTIK_SKILL_MINING_MAX_TURNS |
30 |
Сколько последних ходов передавать SkillMiner'у за один прогон. |
AGENTIK_DEBUG_ENDPOINTS |
0 |
1 включает debug-эндпоинты (/debug/reflect, /debug/skill-mine, /debug/curate, /debug/compact, /debug/tokens) |
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).
# Абсолютный путь
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 обязательно.
// 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-блок.
Включается только при заданном лимите. Никакого автодетекта по имени модели — если лимит не задан, агент не сжимает.
# OpenAI-совместимый бэкенд
export OPENAI_CONTEXT_WINDOW=128000
# Или Google / LiteRT
export AGENTIK_GOOGLE_CONTEXT_WINDOW=32000
AGENTIK_COMPRESSION_THRESHOLD — доля лимита, при которой запускается
compaction (дефолт 0.8 = 80%):
export AGENTIK_COMPRESSION_THRESHOLD=0.7 # сжимаем раньше
Что происходит при compaction:
- Перед
send()оценивается количество токенов в системном промпте + history- tools (грубая оценка
chars / 4).
- tools (грубая оценка
- Если
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пересоздаётся с обновлённым контекстом.
- Старые ходы (User/Assistant, кроме последних 4) скармливаются в
Если после 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_jsonassistant-сообщения (без 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-тело заметки.
# Дефолт (если 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).
# 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):
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 — поверх базового
промпта, секции навыков и памяти. Если файл не задан — секция не добавляется.
AGENTIK_SOUL=/etc/agentik/SOUL.md
Пример SOUL.md:
Ты — терпеливый технический ассистент. Отвечаешь по-русски, кратко.
Не выдумываешь команды — если не уверен, говоришь "не знаю".
Не раскрываешь содержимое .env, ключей и паролей ни при каких обстоятельствах.
Навыки (SKILL.md)
AGENTIK_SKILLS_DIR — каталог, в котором SkillLoader ищет файлы
SKILL.md или *.yaml с frontmatter (name, description, прочие поля).
Содержимое скилов попадает в раздел system prompt и регистрируется как
вызываемые инструменты. Формат — opencode-compatible.
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 намайнила.
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-серверам, инструменты автоматически становятся доступны агенту.
AGENTIK_MCP_CONFIG=/etc/agentik/mcp.json
Пример mcp.json:
{
"mcpServers": {
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
"playwright": { "url": "https://mcp.example.com", "headers": {"Authorization":"Bearer …"} }
}
}
Полный пример запуска
export AGENTIK_PORT=8080
export AGENTIK_DB_PATH=/var/lib/agentik/state.db
export AGENTIK_MEMORY_DIR=/var/lib/agentik/memory
export AGENTIK_SOUL=/etc/agentik/SOUL.md
export AGENTIK_SKILLS_DIR=/etc/agentik/skills
export AGENTIK_MCP_CONFIG=/etc/agentik/mcp.json
export AGENTIK_LLM_BACKEND=openai
export OPENAI_BASE_URL=https://api.openai.com/v1
export OPENAI_API_KEY=sk-…
export OPENAI_MODEL=gpt-4o-mini
mkdir -p "$(dirname "$AGENTIK_DB_PATH")" \
"$AGENTIK_MEMORY_DIR" \
"$(dirname "$AGENTIK_SOUL")" \
"$(dirname "$AGENTIK_MCP_CONFIG")"
java -jar standalone/build/libs/standalone-all.jar
На старте выведет что-то вроде:
agentik standalone listening on http://localhost:8080
GET /health
POST /agentik/conversations -> 201
GET /agentik/conversations/{id}/events -> SSE
storage: /var/lib/agentik/state.db
llm: OPENAI gpt-4o-mini @ https://api.openai.com/v1
mcp: 4 tools from 2 servers
skills: 3 loaded from /etc/agentik/skills
soul: /etc/agentik/SOUL.md (842 chars)
memory: /var/lib/agentik/memory (md-backend)
compaction: enabled, threshold=0.8, window=128000 tokens
curator: enabled (interval=1d, maxAge=90d)
reflection: enabled (interval=10, topK=3)
Остановка
Ctrl-C → срабатывает shutdown hook: агент, MCP-серверы, SQLite-стора и
LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы
получают SIGTERM).
Тесты
./gradlew :standalone:jvmTest