12 Commits

Author SHA1 Message Date
subochev d1b4f897b7 Add GET /conversations/{id}/count endpoint for total/filtered message counts, update JournalStore API, and implement client/server support with tests.
release / Publish KMP libraries → caffeine Nexus (release) Successful in 1m4s
2026-09-23 05:43:55 +03:00
Porfiry c486c7f9ab auth: своя Bearer-авторизация agentik (сервер + клиент + standalone)
ci / JVM build + tests (push) Successful in 6m11s
release / Publish KMP libraries → caffeine Nexus (release) Successful in 33s
Добавлена собственная авторизация по токену. Это ОТДЕЛЬНАЯ подсистема:
библиотека A2A (pw.binom.a2a) имеет свой независимый token, общих типов
и общей логики не вводится.

Поведение по умолчанию не меняется: token = null -> авторизация выключена,
сервер открыт (обратная совместимость), CLI/TUI не затронуты.

Сервер (:server):
- новый route-scoped плагин BearerTokenPlugin (BearerTokenConfig);
- agentikAgent(agent, path, token) ставит плагин на всё поддерево /agentik,
  когда token != null; иначе плагин не устанавливается;
- при несовпадении заголовка Authorization: Bearer <token> -> 401 Unauthorized;
- /health всегда открыт (liveness для балансировщика).

Клиент (:client):
- defaultAgentikHttpClient(token) навешивает Authorization: Bearer <token>
  через DefaultRequest на весь HttpClient -> накрывает все 10 вызовов и оба SSE;
- AgentikAgent(id, baseUrl, token, httpClient) — token необязательный,
  9 существующих мест создания агента не тронуты.

Standalone:
- AgentSection.authToken (env AGENTIK_TOKEN) -> /agentik;
- AgentSection.a2aToken (env AGENTIK_A2A_TOKEN) -> /a2a;
- два независимых значения, связи между ними нет.

Тесты: BearerTokenTest (5), BearerHeaderTest (3) — 401 без токена и с чужим,
200 с верным, /health открыт, null -> открыто. Мутационная проверка пройдена.
2026-09-19 20:58:52 +03:00
SubochevAV 8f616f359f docs: per-module READMEs (run vs library) + root navigation hub
ci / JVM build + tests (pull_request) Failing after 54s
Every subproject now has README.md:
- 3 runnable modules (:standalone, :agentik-cli, :agentik-tui):
  quickstart, env table, parameters, known limits
- 11 library modules: what it is, which problem solves, how to
  wire it in, where versions live

Root README.md is the navigation hub (Quickstart, Modules table,
publish + CI/CD notes).

Also: ci.yml prunes the :memory-vector -x excludes now that
text-embedding-kmp artifacts are published to caffeine.

518 tests green.

Verified publish pipeline: :proto:publish to caffeine produces
pom.module + per-target klibs + sources for all 9 KMP targets.

🤖 Generated with [opencode]
2026-09-16 20:44:16 +03:00
subochev 0fdc12695e docs: per-module README + root navigation hub + CI/release workflows
ci / JVM build + tests (push) Failing after 1m57s
- README.md в каждом подмодуле: для библиотек — описание проблемы,
  подключение через maven-central/caffeine, версии в gradle/libs.versions.toml.
  Для запускаемых модулей — команды запуска + переменные среды с дефолтами.
- Корневой README.md переписан как навигационный хаб: что это, где клиенты,
  где серверы, как собрать, как опубликовать.
- build.gradle.kts: per-module POM-description через единую карту в rootProject.extra
  (порядок важен — нужно ДО apply плагина KMP, поэтому beforeEvaluate в subprojects).
- .gitea/workflows/ci.yml (новый): build + jvmTest + shadowJar на PR/push main.
- .gitea/workflows/release.yml (обновлён): публикует библиотеки в caffeine
  Nexus + собирает 3 fatjar'а и крепит их к release как бинарные ассеты.
2026-09-16 16:24:03 +03:00
subochev 18ae6e0619 a2a: подключить A2A-транспорт (POST /a2a + agent-card) через A2aBridge
:standalone декларировал зависимость a2a-server, но mount не было (e2e
нашёл 404 на /a2a). A2aBridge (AgentHandler) гоняет A2A-context на
:proto-диалог: contextId -> Conversation (пустой/неизвестный -> новый),
ответ = склеенные AppendText хода (подписка на events() до send, стоп по
End/Interrupted/Error), id диалога в metadata.agentikConversationId.
2026-09-15 07:31:54 +03:00
subochev df386ef875 skills: SkillMiner — фоновое авто-создание скилов + debug-эндпоинты
SkillMiner (сетка безопасности skill self-improvement): каждые
AGENTIK_SKILL_MINING_INTERVAL user-ходов (default 15) LLM смотрит
последние AGENTIK_SKILL_MINING_MAX_TURNS ходы (default 30) + каталог
существующих скилов и возвращает structured JSON {"skills":[...]}.
Найденное upsert-ится в SkillStore — модель "забыла" вызвать
skill_save в ходе разговора, минер добирает её постфактум.

- SkillMiner.kt: короткий LiteConversation (one-shot), blocking-инференс
  на Dispatchers.IO, defensive парсинг (кривой ответ -> пустой список).
- SkillMiningPrompts/SkillMiningParser: тот же подход, что
  ReflectionParser (structured-output вместо tool-calling).
- ChatConversation.scheduleSkillMining() — хук после каждого хода
  (рядом со scheduleReflection); ChatAgent/Main — прокидывание.
- DebugRoutes.kt: AGENTIK_DEBUG_ENDPOINTS=1 включает POST
  /debug/reflect, /debug/skill-mine, /debug/curate, /debug/compact и
  GET /debug/tokens для ручного триггерирования фоновых фич.
- ChatConversation.forceCompactNow(): принудительный compaction
  без проверки порога (для /debug/compact).
- Тесты: SkillMinerTest (5) + SkillMiningParserTest (9); FakeLiteLlm
  теперь записывает send()/sendContents() в lastContents.

179 jvm-тестов :standalone зелёные, README обновлён.
2026-09-15 06:36:23 +03:00
subochev f4ce82957b standalone: token accounting per assistant turn (input/output → SQLite)
LiteLlm API не отдаёт split prompt/completion наружу через send()
(внутренний OpenAI Usage сидит в pw.binom.litert.openai и недоступен),
поэтому измеряем через LiteConversation.tokenCount():

  input  = tokenCount() до первого send в turn'е
          (= system + вся история + tools + только что добавленное user-сообщение)
  output = tokenCount() после завершения turn'а - input
          (= assistant text + tool calls + tool results за tool loop)

Пишем в assistant-запись как TurnTokens(input, output) в payload_json.
Никаких schema-миграций: payload-формат уже обёрнут в MessageBodyPayload,
просто добавлено опциональное поле tokens.

MessageStore.tokenStats(conversationId) → TokenStats(turns, inputTokens, outputTokens).
На старте агент печатает сводку по всем диалогам:
  tokens: 17 convs, 134 turns, in=523844, out=58290, total=582134

Бэкенды без tokenCount() (off-line LiteRT-LM модели) → tokens=null,
старые assistant-записи без метрики → пропускаются в tokenStats без ошибок.

Tests: TokenStatsTest (5 green) + PersistenceTest (unchanged) → 165 total.
Backward compat: legacy plain-array payload всё ещё читается, tokens=null.
2026-09-15 05:45:40 +03:00
subochev 9afa877e39 tools: подключить skill_save/skill_delete к ChatAgent (Hermes Phase 3 ready)
Тулзы были написаны ранее (SkillSaveTool/SkillDeleteTool, SkillToolsFactory),
но не были подключены к ChatAgent. Этот коммит закрывает пробел:

* ChatAgent: новый параметр skillStore: SkillStore? = null. Когда задан —
  в allTools добавляются SkillToolsFactory.create(skillStore) → агенту
  доступны skill_save и skill_delete (помимо read_skill который всегда
  есть при непустом каталоге).
* Main.kt: если config.skillsDir задан — создаём DiskSkillStore(File(dir))
  и скармливаем агенту. Каталог используется и для чтения (SkillCatalog),
  и для записи (DiskSkillStore.upsert/remove) — одни и те же файлы,
  никаких рассинхронов между read_skill и skill_save.
* SkillLoader.loadDirectory больше не нужен в Main — DiskSkillStore сам
  подгружает каталог в init. Удалён старый импорт.
* Тесты SkillToolsTest (5): SkillSaveTool persists file and surfaces in
  catalog; rejects blank name; SkillDeleteTool archives (rename to
  .archived); errors on missing skill; colon-named skills map to nested
  dirs (backend:spring:db-base → backend/spring/db-base/SKILL.md).
* README: раздел "Навыки" расширен описанием трёх тулов (read_skill /
  skill_save / skill_delete).

Smoke: standalone запускается с пустым AGENTIK_SKILLS_DIR, видит
"skills: 0 loaded from /tmp/skills-smoke" (DiskSkillStore создаёт каталог
при отсутствии). Агент при наличии skillStore имеет в своём распоряжении
все три тула для self-improvement'а.

Теперь Phase 3 (skill self-improvement) реально работает end-to-end:
агент может дёрнуть skill_save когда понимает что задача повторяется,
потом в следующих диалогах использовать новый скил через read_skill.

Tests: 160 standalone JVM (+5), 241 всего JVM, 73 native, all green.
2026-09-15 05:20:41 +03:00
subochev 23da1f6498 reflection: Hermes-style self-reflection (последняя открытая Hermes-фича)
Self-reflection: каждые N пользовательских ходов агент запускает one-shot
LLM-размышление о качестве своих ответов, сохраняет score+weakSpots в SQLite,
подмешивает top-K последних рефлексий в system prompt как "слабые места".

* sqldelight: новая таблица reflection (id, conversation_id?, created_at,
  turns_analyzed, score, summary, weak_spots_json) + индексы по created_at и
  conversation_id.
* persistence: Reflection data class + ReflectionStore interface +
  SqliteReflectionStore (insert/get/listRecent/listForConversation/
  deleteOlderThan/count + events Flow). weakSpots хранятся как JSON-массив,
  парсятся ручным сканером (без kotlinx-serialization в этом модуле).
* agent: LlmReflector (one-shot LiteLlm через createConversation +
  send, structured-output JSON). ReflectionParser (hand-rolled,
  толерантный к ```json fences и лидирующему/завершающему тексту;
  score принимает int или строку; weakSpots — массив).
* agent: ReflectionPrompts (Russian system+user prompts, аналогично
  LlmMemoryReviewer/ReviewPrompts).
* agent: ChatAgent.buildSystemPrompt расширен параметром reflections —
  добавляется секция `## Self-reflection: твои слабые места за последнее время`
  после memory и перед soul-prepend.
* agent: ChatConversation.scheduleReflection — каждые reflectionInterval
  пользовательских ходов (счётчик через workingMemory.list) запускает
  reflector на Dispatchers.IO, результат сохраняет в reflectionStore
  с conversationId. Не блокирует turn.
* AgentikConfig: новые поля reflectionInterval (env AGENTIK_REFLECTION_INTERVAL,
  default 10, clamped 0..1000) и reflectionTopK (env AGENTIK_REFLECTION_TOP_K,
  default 3, clamped 0..20).
* Main.kt: если reflectionInterval > 0 — создаём LlmReflector(llm); загружаем
  top-K из SQLite в system prompt.
* Ids.reflection() — генератор id "refl-<uuid>".
* Tests: ReflectionParserTest (7: clean JSON, fences, лидирующий текст,
  score-as-string, отсутствие score, невалидный JSON, escape-последовательности),
  ReflectionStoreTest (7: round-trip, listRecent с лимитом, фильтр по
  conversation_id, deleteOlderThan, count, encode/decode строк), и
  ChatAgentReflectionTest (3: секция скрыта при пустых, присутствует с
  score+spots, порядок soul→memory→reflection).
* README: новые env-переменные, раздел "Self-reflection", startup output.

Smoke test подтверждает: reflection секция появляется в system prompt когда
в SQLite есть записи (listRecent возвращает непустой список). При
reflectionInterval=0 reflector не создаётся, scheduleReflection — no-op.

Tests: 305 total green (+17: 7+7+3). Fatjar собирается, logback-вывод
работает (logging: см. предыдущий коммит f5a551b).
2026-09-15 05:14:33 +03:00
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
subochev 9e12b22e85 memory: Curator (Phase 4) — фоновая архивация старых неиспользуемых заметок
* :memory-api — MemoryStore.archiveStale(maxAge, maxUseCount, now): default
  имплементация через list + delete (бэкенды могут переопределить).
* :standalone — Curator: фоновая корутина на Dispatchers.IO, раз в сутки
  дёргает archiveStale(90d, 0). start()/stop(), runPass() — однократный
  прогон для тестов.
* :standalone/Main — Curator стартует автоматически если memory включён,
  stop() в shutdown hook.
* :standalone — публичный TestInMemoryMemoryStore вынесен из CompactionTest,
  переиспользуется в CuratorTest.
* README — раздел "Куратор памяти" с описанием семантики для md и vector
  бэкендов.

Tests: 288 total (+4 CuratorTest). Fatjar smoke-tested, curator стартует
на app boot, выводит `[Curator] started` в лог.

Defaults: interval=1d, maxAge=90d, maxUseCount=0. Override через
новые config-флаги отложен.
2026-09-15 04:14:03 +03:00
subochev 65365da89c Phase 5: :memory-vector (JVector + SQLite + LLM-эмбеддинги), memory-abstraction, compaction, MessageContext
- :memory-api — общий контракт MemoryStore/Prefetcher/Reviewer/Tools/MemorySystem
- :memory-md (KMP, kotlinx-io) — Hermes-style §-файлы, keyword overlap
- :memory-vector (JVM-only) — JVector ANN + SQLite + HttpEmbeddingClient
- :standalone — AGENTIK_MEMORY_BACKEND={md,vector,off}, выбор в Main.kt
- :standalone — compaction рабочего контекста (LiteLlmContextCompactor + reviewPreCompaction)
- :proto — MessageContext (origin: user/system/event) на send и в Message
- :server — backward-compat dual-format для POST /messages
- README — env-vars, vector-бэкенд docs
2026-09-15 03:44:15 +03:00