Files
agentik/standalone
subochev 408caee261 feat(standalone): agentik pull-model subcommand + AGENTIK_AUTO_DOWNLOAD_MODEL=1 trigger for LiteRT-LM
Добавляет ModelDownloader (HTTP с Range/докачкой, опциональной SHA-256 проверкой)
и два сценария запуска скачивания встроенной модели gemma-4-E2B-it.litertlm:

  java -jar agentik.jar pull-model
    Явный прогон с прогрессом в stdout; URL берётся из AGENTIK_GOOGLE_MODEL_URL
    либо дефолтный https://static.binom.pw/models/gemma-4-E2B-it.litertlm.

  AGENTIK_AUTO_DOWNLOAD_MODEL=1 java -jar agentik.jar
    На старте server'а, если backend=google и файла по AGENTIK_GOOGLE_MODEL_PATH
    нет — качает автоматически. Без флага — exit 2 с понятным сообщением и
    подсказкой вызвать pull-model.

Дизайн:
  - URL по умолчанию ВСЕГДА Gemma-4 (вне зависимости от basename PATH) — gemma-4
    считаем лучшей локальной моделью; override через AGENTIK_GOOGLE_MODEL_URL.
  - SHA-256 проверка через опциональный AGENTIK_GOOGLE_MODEL_SHA256_URL.
  - Resume: HEAD → если есть .part и Accept-Ranges=bytes → GET с Range: bytes=N-,
    иначе restart с нуля.
  - Прогресс каждые ~8 MB, финальный rename через Files.move(ATOMIC_MOVE).

Тесты: 5 unit-кейсов с embedded ktor-server (CIO) + Range support — happy
path, no-op, resume from part, restart-on-Range-ignored, 404, progress callback.

Документация: новый раздел §18 в MANUAL-TESTS.md (subcommand, auto-trigger,
resume, override URL, SHA-256 verify).

178/178 tests green.
2026-09-16 12:55:43 +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