Files
agentik/standalone/README.md
T
subochev 427ce8a572 memory: SigLIP2 on-device embedding (text-embedding-kmp v3)
Добавляет второй бэкенд эмбеддингов для vector-памяти: on-device SigLIP2
через ONNX Runtime. Не требует сети (HTTP), не светят тексты заметок наружу.

* gradle/libs.versions.toml: text-embedding-kmp = "3.0.0-SNAPSHOT", модули
  api-jvm / siglip-jvm (group переехал с pw.binom.voice.embeddingtext на
  pw.binom.ai.embeddingtext).
* settings.gradle.kts: mavenLocal() добавлен в dependencyResolutionManagement
  (text-embedding-kmp публикуется локально как snapshot).
* memory-vector/SiglipEmbeddingProvider (jvmMain) — адаптер
  pw.binom.voice.embeddingtext.TextEmbeddingExtractor → EmbeddingProvider:
  оборачивает blocking embed() в withContext(Dispatchers.IO) + Mutex (ONNX
  сессия не reentrant), размерность пробируется через probe embed("probe")
  (768 для SigLIP2-base).
* memory-vector/build.gradle.kts: api(libs.text.embedding.api) +
  implementation(libs.text.embedding.siglip); jvmTest получает
  SiglipEmbeddingProviderTest — smoke test (skip если модель не найдена).
* standalone/AgentikConfig: новый enum EmbeddingBackend { HTTP, SIGLIP },
  поля embeddingBackend / embeddingModelPath / embeddingTokenizerPath, env:
  AGENTIK_EMBEDDING_BACKEND, AGENTIK_EMBEDDING_MODEL_PATH,
  AGENTIK_EMBEDDING_TOKENIZER_PATH.
* standalone/Main.kt: switch на embeddingBackend при memory-backend=vector;
  для SIGLIP требуются оба пути, иначе ошибка с понятным сообщением.
* standalone/README.md: обновлены env-vars, добавлены две bash-секции
  (HTTP и SIGLIP) + инструкция скачивания модели с static.binom.pw.
* scripts/install-text-embedding-stub.sh: workaround для upstream бага
  (siglip-jvm/*.module ссылается на api без -jvm variant). Создаёт
  stub-артефакт api:3.0.0-SNAPSHOT в mavenLocal с тем же содержимым.
  Удалить когда upstream починит module-metadata.

Smoke test: AGENTIK_MEMORY_BACKEND=vector AGENTIK_EMBEDDING_BACKEND=siglip
+ несуществующий путь → FileNotFoundException с понятным трейсом (значит
ONNX Runtime инициализирован, путь через factory пробрасывается корректно).

Tests: 288 total green. Fatjar 250MB (вырос из-за onnxruntime ~80MB).

Dropped: старый stub-jar pw.binom.voice.embeddingtext:api:2.0.0-SNAPSHOT.
2026-09-15 04:43:45 +03:00

18 KiB
Raw Blame History

: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-стрим ответов

Переменные окружения

Все переменные читаются 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

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.

Куратор памяти (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

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)

Остановка

Ctrl-C → срабатывает shutdown hook: агент, MCP-серверы, SQLite-стора и LLM-клиент закрываются корректно (SQLite фиксирует WAL, MCP-процессы получают SIGTERM).

Тесты

./gradlew :standalone:jvmTest