Outbound (DurableEvent-driven): - typing дёргается ДО conv.send (а не только по online-событиям); рефреш по online — каждые ≥4 с, чтобы не получить 429-спам (Telegram rate-limit ~250 ms на sendChatAction). - финальный текст — sendMessage(... ParseMode.HTML) на DurableEvent.AssistantMessage, без промежуточных editMessage. - служебные строки ((empty response), ошибки) идут как plain text (без parseMode=HTML), чтобы 400 can't parse entities не выбил предупреждение в чат. Inline markdown → Telegram HTML (MarkdownToTelegram): - **bold** / __bold__ / *italic* / _italic_ / `code` → <b>/<i>/<code>; fenced ``` → <pre>; [text](url) → <a href>. - markdown-таблицы → Unicode-рамка (┌─┬─┐││├─┼─┤└─┴─┘) внутри <pre> (Telegram Bot API <table> не поддерживает). - escapeOutsideTags теперь ведёт стек открытых тегов: раньше брал первый попавшийся закрывающий тег и терял парность (агент вставил <i> внутри <code> → Telegram 400 can't find end tag для <code>). - экранирует только вне наших тегов; внутри — уже подготовленный текст. - сообщения > 4096 режутся по \n вне тегов. Видимость ошибок: - kotlin-logging в build.gradle.kts. - runCatching'и заменены на try/catch + log.error/warn с контекстом (chatId, convId, htmlLen, превью первых ≤120 символов HTML). Раньше Telegram 400 на sendMessage с 'can't parse entities' уходил молча — теперь видно в логе с полным телом запроса. Тесты: 23 + 12 + 28 = 63 в :integrations-telegram, 132 в :standalone. :standalone:assemble собирает fatjar.
:standalone — single-jar HTTP-сервер со всеми транспортами
Что это
Главный исполняемый модуль проекта — single-jar HTTP-сервер с:
- A2A transport на
POST /(JSON-RPC) +GET /.well-known/agent-card.json. :prototransport наPOST /agentik/*(HTTP+JSON+SSE) — наш stateful.- Embedded LLM backend:
GOOGLE(LiteRT) илиOPENAI-совместимый (vLLM, LiteLLM, OpenAI API). - SQLite persistence через
:storage-sqlite. - Memory backend:
md(файловый) илиvector(SQLite+JVector+ HTTP/SIGLIP-embeddings). - Skills из
~/.agentik/skills/*.md. - SOUL из
~/.agentik/SOUL.md. - Background подпроцессы: рефлексия, skill-mining, memory-reviewer.
Решает: даёт пользователю один JAR (10–250 МБ), который запускается
через java -jar agentik-0.1.0-all.jar, и поднимает сразу все
транспорты, которые другие системы могут хавать.
Как запустить
Переменные среды
AGENTIK_SKILLS_DIR=./agentik/skills;AGENTIK_SYSTEM_PROMPT=Ты полезный ассистент.;OPENAI_BASE_URL=http://192.168.88.135:8001/v1;AGENTIK_DB_PATH=./agentik/db.db;OPENAI_CONTEXT_WINDOW=115000;AGENTIK_MEMORY_DIR=./agentik/memory;AGENTIK_LLM_BACKEND=openai;AGENTIK_PORT=8080;OPENAI_API_KEY=sk-76R2p5nxQxflPkIROr6r2xGiuSYzUCYM;OPENAI_MODEL=/root/.cache/huggingface/Qwen3.8-27B-NVFP4-RTX5090;AGENTIK_SOUL=./agentik/SOUL.md;AGENTIK_TELEGRAM_TOKEN=8857094360:AAFeVYMlutSaHptbhpO4mUryR6Je7OUJsm4
Требования
- JVM 21+.
- (Опционально) CUDA-устройство для
:backend=google(LiteRT). - (Опционально) LM через OpenAI-совместимый endpoint (vLLM / Ollama
/ OpenAI) для
:backend=openai.
Запуск из готового fatjar
java --enable-native-access=ALL-UNNAMED \
-jar agentik-0.1.0-all.jar
С дефолтами — встроенный SQLite, OpenAI-compatible backend на
http://localhost:8001/v1, порт 8080.
Запуск через Gradle (dev)
./gradlew :standalone:run
pull-model subcommand (для LiteRT)
# Сначала скачать модель под LiteRT-Gemma-4-E2B
AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
java --enable-native-access=ALL-UNNAMED \
-jar agentik-0.1.0-all.jar pull-model
Скачивает https://static.binom.pw/models/gemma-4-E2B-it.litertlm
(2.5 ГБ, с Range-resume). Поддерживает override через
AGENTIK_GOOGLE_MODEL_URL и verify через
AGENTIK_GOOGLE_MODEL_SHA256_URL.
Переменные среды
Полный список — общий для всего :standalone-процесса:
| Env | Default | Что делает |
|---|---|---|
AGENTIK_PORT |
8080 |
Порт HTTP-сервера |
AGENTIK_DB_PATH |
./agentik.db |
Путь к SQLite |
AGENTIK_TOKEN |
(пусто) | Bearer-токен для HTTP-фасада /agentik. Пусто — авторизация выключена |
AGENTIK_A2A_TOKEN |
(пусто) | Bearer-токен для A2A-фасада /a2a. Пусто — авторизация выключена (независим от AGENTIK_TOKEN) |
AGENTIK_AGENT_ID |
agentik |
ID агента (для multi-instance) |
AGENTIK_LLM_BACKEND |
openai |
openai или google |
AGENTIK_LLM_MODEL |
(выбирается по backend) | Имя модели |
AGENTIK_LLM_API_URL |
http://localhost:8001/v1 |
Endpoint для OpenAI-compatible |
AGENTIK_LLM_API_KEY |
no-key-needed |
Auth header |
AGENTIK_LLM_CONTEXT_TOKENS |
115000 |
Сколько токенов остаётся модели |
AGENTIK_GOOGLE_MODEL_PATH |
/root/gemma-4-E2B-it.litertlm |
Путь к .litertlm файлу |
AGENTIK_GOOGLE_MODEL_URL |
https://static.binom.pw/models/gemma-4-E2B-it.litertlm |
Откуда скачивать |
AGENTIK_GOOGLE_MODEL_SHA256_URL |
— | Если задан — verify по SHA-256 |
AGENTIK_AUTO_DOWNLOAD_MODEL |
0 |
1 = скачать модель если её нет |
AGENTIK_MEMORY_BACKEND |
vector |
md, vector или off |
AGENTIK_EMBEDDING_BACKEND |
http |
http или siglip (только для vector) |
AGENTIK_EMBEDDING_API_URL |
http://localhost:8001/v1 |
Endpoint для эмбеддингов |
AGENTIK_EMBEDDING_MODEL |
text-embedding-3-small |
Имя embedding-модели |
AGENTIK_SOUL_PATH |
~/.agentik/SOUL.md |
Путь к SOUL.md |
AGENTIK_SKILLS_DIR |
~/.agentik/skills/ |
Каталог SKILL.md |
AGENTIK_MEMORY_DIR |
~/.agentik/memory/ |
Каталог для md-памяти |
AGENTIK_TOOLSETS_DEFAULT |
memory,skills,files,web |
Включённые тулы |
AGENTIK_DEBUG |
0 |
1 = verbose logging |
AGENTIK_TELEGRAM_TOKEN |
(пусто) | Bot token от @BotFather. Пусто — Telegram-интеграция выключена, standalone работает ровно как без неё |
AGENTIK_TELEGRAM_POLLING_TIMEOUT |
30 |
Long-poll timeout в секундах. Bot API держит HTTP-соединение открытым до N секунд, возвращает 200 OK с пустым массивом если апдейтов нет. Меньше — больше polling-нагрузки, больше — дольше «висит» соединение. 25–35 обычно ок |
AGENTIK_TELEGRAM_PERSISTENT |
true |
true → диалог из чата сохраняется в SQLite как обычный ConversationRecord и переживает рестарт. false → каждый Telegram-message стартует свежий temp=true диалог (как «temporary chat» в Android-клиенте) |
Значения читаются через AgentikConfig.fromEnv() в :standalone/.../Main.kt.
Telegram-интеграция
Опциональный «мост» между Telegram-чатом и standalone-агентом.
Реализован отдельным модулем :integrations-telegram и подключается
ТОЛЬКО при заданном AGENTIK_TELEGRAM_TOKEN (нет токена — нет polling'а,
нет зависимости в classpath при выключенном флаге, см. ниже).
Что умеет
- Long-polling через Bot API (
getUpdates?timeout=N). Не webhook — не нужен публичный URL, всё работает за NAT. - Маппинг
chatId ↔ ConversationIdв общей SQLite-БД standalone'а (таблицаtg_chat_map). Один Telegram-чат = один диалог с агентом. - «печатает…» в чате через
sendChatAction(TYPING):- одно сразу при получении апдейта (ДО вызова LLM — пользователь видит статус ровно в момент отправки текста);
- рефреш на каждом online-событии для диалога (Working /
StartReasoning / StartResponse / AppendText / AppendImage / End) —
Telegram гасит статус через ~5 с, поэтому для длинных tool-call'ов
(когда дельт текста нет) мост продолжает рефрешить typing по
Working.
- Финальное сообщение приходит одним
sendMessage(...)поDurableEvent.AssistantMessage(целиковый текст изContent.Text). Никакого стриминга черезeditMessage— UX проще, и Telegram нормально индексирует одиночное сообщение. - Markdown → Telegram HTML: мост конвертирует CommonMark-подобный
markdown агента в
ParseMode.HTML(<b>,<i>,<code>,<pre>,<a href>); остальные символы экранируются. Сообщения длиннее 4096 символов режутся по переводам строк (вне HTML-тегов). - Markdown-таблицы (
| col | col |+| --- | --- |) рендерятся через Unicode box-drawing (┌─┬─┐ │ │ │ ├─┼─┤ └─┴─┘) внутри<pre>— Telegram Bot API не поддерживает<table>, поэтому табличный вид даёт только моноширинная рамка. Markdown/HTML-разметка внутри ячеек снимается (в<pre>inline-форматирование не работает). - Group-чаты (
group,supergroup) и команда/newстартуют новый временный диалог (ConversationRecord.isTemporal = true). - Ошибки агента (
DurableEvent.Error, исключение изconv.send) приходят одной строкой, polling не падает.
Как включить
-
Создать бота через
@BotFather, скопировать токен. -
Запустить standalone с токеном:
AGENTIK_TELEGRAM_TOKEN=123456:ABC-DEF... \ java --enable-native-access=ALL-UNNAMED \ -jar agentik-0.1.0-all.jar -
Написать боту в личку (или добавить в группу). В логе standalone'а появится:
telegram: enabled (polling timeout=30s, persistent=true) -
Ответ приходит одним сообщением (после завершения хода ассистента). Пока агент думает / вызывает тулзы / стримит токены — в чате горит «печатает…».
Persistent vs temporary
| Сценарий | AGENTIK_TELEGRAM_PERSISTENT |
Поведение |
|---|---|---|
| Личка с ботом | true (default) |
Один диалог на пользователя, переживает рестарт, /new стартует новый |
| Личка с ботом | false |
Каждое сообщение — свежий temp диалог |
| Group-чат | (любое) | Всегда temp — каждый message может быть от другого юзера, диалоги не персистятся |
/new |
(любое) | Стартует новый temp диалог (для persistent=true это закрывает старый ConversationRecord) |
Технические заметки
- Либа — своя,
caffeine-mgn/telegramClient(pw.binom.telegram:telegramClient), опубликованная черезpublishToMavenLocal. Артефактpw.binom.telegram.*НЕ утекает в classpath standalone'а — host-сайд видит только фабрикуcreateTelegramBridgeComponent, которая возвращаетAutoCloseable. - Polling/stream-джобы живут на отдельном
integrationsScope. В shutdown-hook'е он отменяется доagent.close()— иначе long-poll держит соединение доAGENTIK_TELEGRAM_POLLING_TIMEOUTи блокирует JVM-выход. - Один бот на standalone (таблица
tg_chat_mapглобальная). Если когда-нибудь захочется multi-agent + multi-bot — мапу надо будет разводить по агентам.
Эндпоинты
| Метод | Путь | Transport | Описание |
|---|---|---|---|
GET |
/health |
любой | health-check ({"ok":true}) |
POST |
/ |
A2A | JSON-RPC message/send, tasks/get, tasks/cancel |
GET |
/.well-known/agent-card.json |
A2A | Discovery |
POST |
/agentik/conversations |
:proto | Создать диалог |
GET |
/agentik/conversations |
:proto | Список диалогов |
GET |
/agentik/conversations/:id |
:proto | Snapshot |
GET |
/agentik/conversations/:id/messages |
:proto | История |
POST |
/agentik/conversations/:id/send |
:proto | Send (SSE) |
GET |
/agentik/conversations/:id/events |
:proto | Live-events (SSE) |
POST |
/agentik/conversations/:id/interrupt |
:proto | Прервать |
POST |
/agentik/conversations/:id/rename |
:proto | Переименовать |
DELETE |
/agentik/conversations/:id |
:proto | Удалить |
Тесты
./gradlew :standalone:jvmTest # unit-тесты
./gradlew :standalone:integrationTest # integration (Testcontainers)
./gradlew :standalone:shadowJar # → build/libs/agentik-0.1.0-all.jar
Известные ограничения
- vLLM не поддерживает cancel-inference (
interrupt()только закрывает client SSE-socket; бэкенд всё равно генерирует до конца). - A2A JSON discriminator —
"kind"(text/file/data), а не"type". См.A2aJsonв:standalone. - SSE в не-TTY ssh закрывается на default Ktor timeout.
Текущий статус
Production-ready. Все KMP-модули проекта интегрированы. Полный
manual-test checklist смотрите в MANUAL-TESTS.md
или MANUAL-TESTS.md в корне.
Где скачать
- Source:
git clone https://git.binom.pw/subochev/agentik - Fatjar: Gitea CI artifacts (через
.gitea/workflows/release.ymlна tagv*) или собирается через./gradlew :standalone:shadowJar.
Версии
Все gradle/libs.versions.toml. Поднять версию → release через
git tag v0.2.0 && git push --tags → CI собирает все KMP-таргеты
публикует артефакты.