Добавляет второй бэкенд эмбеддингов для 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.
18 KiB
: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-стрим ответов
Переменные окружения
Все переменные читаются 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:
- Перед
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.
Куратор памяти (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