# :standalone — agentik single-jar server Self-contained HTTP-сервер с Ktor: AG-UI / A2A / :proto транспорты на одном порту, встроенный SQLite для истории диалогов, долговременная память (Hermes-style §-файлы), загрузка MCP-инструментов, навыков (SKILL.md) и персоны (SOUL.md). ## Сборка ```bash # Полная сборка всего проекта + fatjar ./gradlew assemble # Только fatjar :standalone (≈ 150 MB) ./gradlew :standalone:shadowJar # Результат: # standalone/build/libs/standalone-all.jar ``` ## Запуск ```bash 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 | | `AGENTIK_REFLECTION_INTERVAL` | `10` | Self-reflection: каждый N-й пользовательский ход агент оценивает себя (LiteLlm) и сохраняет рефлексию. `0` = выключено. | | `AGENTIK_REFLECTION_TOP_K` | `3` | Сколько последних рефлексий подмешивать в system prompt как «слабые места». `0` = не подмешивать. | ### 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`). ```bash # Абсолютный путь 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` обязательно. ```json // 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`-блок. **Включается только при заданном лимите.** Никакого автодетекта по имени модели — если лимит не задан, агент не сжимает. ```bash # OpenAI-совместимый бэкенд export OPENAI_CONTEXT_WINDOW=128000 # Или Google / LiteRT export AGENTIK_GOOGLE_CONTEXT_WINDOW=32000 ``` `AGENTIK_COMPRESSION_THRESHOLD` — доля лимита, при которой запускается compaction (дефолт `0.8` = 80%): ```bash 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. ## Куратор памяти (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-тело заметки. ```bash # Дефолт (если 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). ```bash # 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): ```bash 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` — поверх базового промпта, секции навыков и памяти. Если файл не задан — секция не добавляется. ```bash AGENTIK_SOUL=/etc/agentik/SOUL.md ``` Пример `SOUL.md`: ```markdown Ты — терпеливый технический ассистент. Отвечаешь по-русски, кратко. Не выдумываешь команды — если не уверен, говоришь "не знаю". Не раскрываешь содержимое .env, ключей и паролей ни при каких обстоятельствах. ``` ## Навыки (SKILL.md) `AGENTIK_SKILLS_DIR` — каталог, в котором `SkillLoader` ищет файлы `SKILL.md` или `*.yaml` с frontmatter (`name`, `description`, прочие поля). Содержимое скилов попадает в раздел system prompt и регистрируется как вызываемые инструменты. Формат — opencode-compatible. ```bash 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` (включая в этом же диалоге). ```bash AGENTIK_SKILLS_DIR=/etc/agentik/skills ``` ## MCP-инструменты `AGENTIK_MCP_CONFIG` — путь к JSON-файлу со списком MCP-серверов (формат `mcpServers: { name: { command, args | url, headers } }`). При запуске `McpRegistry.fromConfig` стартует stdio-серверы и подключается к HTTP-серверам, инструменты автоматически становятся доступны агенту. ```bash AGENTIK_MCP_CONFIG=/etc/agentik/mcp.json ``` Пример `mcp.json`: ```json { "mcpServers": { "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }, "playwright": { "url": "https://mcp.example.com", "headers": {"Authorization":"Bearer …"} } } } ``` ## Полный пример запуска ```bash 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). ## Тесты ```bash ./gradlew :standalone:jvmTest ```