Files
agentik/standalone
subochev 9fcb2da75d Remove system prompt persistence from working_memory
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.
2026-09-16 02:39:19 +03:00
..

: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 — liveness
  • POST /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:

  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-тело заметки.

# Дефолт (если 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