System prompt is now built fresh at conversation create/load time (in `buildSystemPrompt` capturing current SOUL/skills/toolsets/reflections) and passed into LiteConversationConfig.systemInstruction. It is NOT written to working_memory anymore. Why: ChatAgent was freezing the system prompt into a WorkingMemoryEntry.System row at createConversation, then reading it back on every getOrCreateLiteConversation. This meant changing SOUL, activating toolsets, adding skills or new reflections between agent restarts did not propagate to existing conversations without re-running createConversation. Fix: - ChatAgent.createConversation: dropped the workingMemoryStore.append(System(...)) - ChatConversation.getOrCreateLiteConversation: replaces the WM-based lookup with the in-memory systemPrompt field directly - ChatConversation.compactPreTurn: same simplification — compaction operates only on User/Assistant rows (plus future Summary rows); system prompt is excluded Migration: none. Old DBs may contain dead System rows from prior versions — they are simply ignored by the new lookup, and compaction never reads them. Tests: 340/340 green. Updated 7 tests across ChatAgentTest + MemoryWiringTest that asserted the old System-in-working-memory contract; they now verify the system prompt via LiteConversationConfig.systemInstruction (what LLM actually sees). E2E verified: 0 system rows in working_memory across all conversations, multi-turn history reconstructs correctly after agent restart with the updated in-memory system prompt.
: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