Files
agentik/standalone/README.md
T
subochev 88dabd49a3 feat(standalone): :integrations-telegram — bridge stability + markdown rendering
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.
2026-10-06 00:51:28 +03:00

14 KiB
Raw Blame History

:standalone — single-jar HTTP-сервер со всеми транспортами

Что это

Главный исполняемый модуль проекта — single-jar HTTP-сервер с:

  • A2A transport на POST / (JSON-RPC) + GET /.well-known/agent-card.json.
  • :proto transport на 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 не падает.

Как включить

  1. Создать бота через @BotFather, скопировать токен.

  2. Запустить standalone с токеном:

    AGENTIK_TELEGRAM_TOKEN=123456:ABC-DEF... \
        java --enable-native-access=ALL-UNNAMED \
             -jar agentik-0.1.0-all.jar
    
  3. Написать боту в личку (или добавить в группу). В логе standalone'а появится:

    telegram: enabled (polling timeout=30s, persistent=true)
    
  4. Ответ приходит одним сообщением (после завершения хода ассистента). Пока агент думает / вызывает тулзы / стримит токены — в чате горит «печатает…».

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

Известные ограничения

  1. vLLM не поддерживает cancel-inference (interrupt() только закрывает client SSE-socket; бэкенд всё равно генерирует до конца).
  2. A2A JSON discriminator — "kind" (text/file/data), а не "type". См. A2aJson в :standalone.
  3. 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 на tag v*) или собирается через ./gradlew :standalone:shadowJar.

Версии

Все gradle/libs.versions.toml. Поднять версию → release через git tag v0.2.0 && git push --tags → CI собирает все KMP-таргеты публикует артефакты.