:standalone декларировал зависимость a2a-server, но mount не было (e2e
нашёл 404 на /a2a). A2aBridge (AgentHandler) гоняет A2A-context на
:proto-диалог: contextId -> Conversation (пустой/неизвестный -> новый),
ответ = склеенные AppendText хода (подписка на events() до send, стоп по
End/Interrupted/Error), id диалога в metadata.agentikConversationId.
kaml encodeToString не ставит завершающий перевод строки, из-за чего
сериализованный SKILL.md выглядел так:
---
name: x
description: "y"---
и SkillParser.parse находил MissingClosingFence — каждый скил,
сохранённый через skill_save / SkillMiner, становился нечитаемым после
рестарта агента (каталог терял скил).
Поставлен явный '\n' перед закрывающим fence. Добавлены юнит-тесты
round-trip serialize->parse (обычный, пустой body, спецсимволы YAML).
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 обновлён.
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.
Тулзы были написаны ранее (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.
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).
Заменил все System.err.println / println на структурное логирование
(kotlin-logging, пакет mu) — теперь логи идут с timestamp/level/thread/logger.
* gradle/libs.versions.toml: kotlin-logging = "3.0.5" (в прокси доступна
только эта версия; новые 7.x пока не подтянуты), logback-classic = "1.5.18".
* standalone/build.gradle.kts: implementation(libs.kotlin.logging) +
implementation(libs.logback.classic) в jvmMain.
* standalone/src/jvmMain/resources/logback.xml: консольный appender,
pattern с timestamp/level/thread/logger, level управляется через
${AGENTIK_LOG_LEVEL:-INFO} (env override на старте JVM), уровни
io.netty/ai.onnxruntime уведены в WARN чтобы не забивать канал.
* Заменены все System.err.println в: ChatConversation (14 callsites),
Curator (2), McpRegistry (5), McpConfig (2), Main (2). Startup banner
в Main оставлен на println — это user-facing output, не log.
* Tests: 288 зелёных (только замена log-вызовов, без изменения семантики).
Smoke: `04:53:49.313 INFO [DefaultDispatcher-worker-4] p.b.a.s.agent.memory.Curator - started (interval=1d, maxAge=90d, maxUseCount=0)`
подтверждает структурный лог вместо println. AGENTIK_LOG_LEVEL=DEBUG работает.
Добавляет второй бэкенд эмбеддингов для 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.
* :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-флаги отложен.
Event.Error уже эмитился в live-SSE, но если клиент подключился
после провала хода (или опрашивает историю через getMessages
вместо SSE), он видел только user-сообщение без следа, что ход
провалился. Это и нужно было поправить.
* :proto
- Message.Error(id, message, code?, date) — терминальная
персистентная проекция Event.Error. Audit-only; в live-стриме
по-прежнему приходит Event.Error.
* :standalone
- MessageRecord.Error с тем же контрактом.
- SqliteMessageStore: kind "error", JSON-payload {message, code};
encoding-ошибки round-trip покрыты тестом (с code и без).
- ChatConversation.failTurn(message, code?): пишет MessageRecord.Error
в audit (кроме temp-диалогов), затем эмитит Event.Error + Event.End.
Все три exit-точки из runTurn (пустой parts, init-catch,
stream-catch) теперь через failTurn.
- ChatConversation: при ошибке стрима живой LiteConversation
сбрасывается — следующий send пересоберёт его из working_memory.
Раньше оставляли битую инстанцию вопреки KDoc класса.
- toProto: маппит MessageRecord.Error → Message.Error.
* docs
- STANDALONE.md: добавлен Message.Error в таблицу dual-log + абзац
про персист ошибок (audit/working_memory, сброс liteConv).
- ARCHITECTURE.md: Message.Error в сигнатуре Message, failTurn
в ChatConversation, Message.Error в audit log.
- Поправлен пример describe() в доке тулов (реальный формат —
OpenAI-style function wrapper, а не голый JSON Schema).
Тесты:
- ChatAgentTest: LLM failure → Event.Error + Event.End + Error
record в audit + backfill через getMessages.
- PersistenceTest: MessageRecord.Error round-trip (с code и без).
- :standalone jvmTest 70 (было 69).
E2E: неверный model id (HTTP 400) → polling GET /messages теперь
возвращает user_message + error (без assistant_message).
Пользовательские инструкции («навыки») живут в указанной папке
(AGENTIK_SKILLS_DIR), рекурсивно читаются при старте и попадают в
системный промпт в сжатом виде: только имя + краткое описание.
Полный текст модель подгружает по требованию, вызывая встроенный
инструмент read_skill(name).
*:skills
- SkillCatalog + SkillPrompt (commonMain): рендер секции системного
промпта; тело навыка в промпт не течёт.
- SkillParser.parseAuto(): теперь читает и opencode-стиль SKILL.md
(YAML frontmatter + markdown тело), и голый *.yaml/*.yml
(поля name, description, опц. body). parseOrThrow для strict-путей.
- SkillParseError.render(): человекочитаемое описание ошибки для
логов и диагностики.
- SkillLoader (jvmMain): рекурсивный обход папки, детерминированный
порядок (по пути), ошибки отдельных файлов не валят загрузку;
дубликаты имён → ошибка, выигрывает первый по пути.
* :standalone
- AgentikConfig.skillsDir + env AGENTIK_SKILLS_DIR.
- ChatAgent: параметр skills (SkillCatalog); системный промпт
автоматически дополняется секцией «## Навыки» и в working memory
сидится вместе с базовым промптом.
- При непустом каталоге в tools автоматически добавляется
SkillReadTool (имя read_skill) — модель может загрузить полный
текст навыка, как обычный LiteTool.
- Main.kt: загружает навыки и шумно логирует ошибки загрузки в stderr.
* docs
- STANDALONE.md: секция «Навыки (skills)», env-переменная в таблице.
- Формат SKILL.md (opencode frontmatter) + голый *.yaml/*.yml.
Тесты: :skills jvmTest 34, :standalone jvmTest 69 (новые — состав
системного промпта, регистрация read_skill, навыки не утекают в
промпт телом).
- AgentikConfig(port, dbPath, llm, mcp) — единая точка входа для всего,
что настраивается снаружи; Main.kt больше не читает System.getenv,
только config.*. Наполняется пока из env (AgentikConfig.fromEnv).
- @Serializable на AgentikConfig/LlmConfig/GoogleConfig/McpConfig/
McpServerSpec(Stdio,Http). Внешний litert OpenAiConfig (не сериализуемый)
заменён нашим pw.binom.agentik.standalone.llm.OpenAiConfig с маппером
toLitertConfig() — чтобы позже читать конфиг из yaml.
- AgentikConfigTest: дефолты, env-override, делегирование в LlmConfig/McpConfig,
JSON round-trip с полиморфным McpServerSpec.
Проверка: :standalone:jvmTest 54/54; смок через DTO — /health=ok.
Убрана зависимость pw.binom.agui:server из проекта — AGUI больше
не нужен (см. Memory #3675; agentik переходит на свой протокол :proto
и HTTP-фасад :server).
Чистка:
- standalone/build.gradle.kts: implementation(libs.agui.server) → удалено
- gradle/libs.versions.toml: [versions] agui + agui-api/agui-client/
agui-server entries → удалены
- settings.gradle.kts: убран 'AG-UI' из комментария к Nexus-репо
- proto/build.gradle.kts: комментарий 'Зеркалит набор AG-UI api' →
'Полный набор KMP-целей'
- proto/src/.../Agent.kt: KDoc '(замена AG-UI)' → удалено
- settings.gradle.kts: то же
- docs/ARCHITECTURE.md: переписан (описывал старую AGUI-centric
архитектуру с AbstractAgent/SessionStore/
AgentEngine — ничего этого в коде уже
нет; теперь отражает текущее состояние:
:proto + :server + :standalone + LiteLlm +
MCP + SqliteStores)
- NATIVE-COMPATIBILITY.md: пункт 10 'agui-server KMP-готовность' →
отменён (см. -); убран из 'Жёсткие блокеры'
В коде не осталось ни одного обращения к AGUI. После чистки в репо
больше нет ни одной зависимости от pw.binom.agui.*.
Проверка: ./gradlew :server:assemble (9/9 KMP-целей) +
:standalone:jvmTest 44/44 (ChatAgent 15, LlmConfig 6, McpConfig 8,
McpRegistry 4, Persistence 11) — зелёные.
Punkt 7 of NATIVE-COMPATIBILITY.md. Proto sources were already on
kotlin.time.Instant (migrated earlier); the kotlinx-datetime api dep
in :proto/build.gradle.kts and the implementation dep in
:standalone/build.gradle.kts were dead weight.
Dropped:
- api(libs.kotlinx.datetime) from proto/build.gradle.kts
- implementation(libs.kotlinx.datetime) from standalone/build.gradle.kts
- [versions] kotlinx-datetime and [libraries] kotlinx-datetime from
gradle/libs.versions.toml
Verification: ./gradlew :server:assemble (all 9 native targets) +
./gradlew :standalone:jvmTest (44/44 green, 0 failures) — both pass.
The kotlinx-datetime typealias was deprecated in 0.8.0 (memory #3708);
now nothing in agentik pulls the library.
1. UUID: java.util.UUID.randomUUID() → commonMain helper Ids.new(prefix)
wrapping kotlin.uuid.Uuid.random(). kotlin.uuid is stdlib (KMP: jvm +
all native targets), so the call sites (ChatAgent, ChatConversation,
SqliteWorkingMemoryStore) are now ready for native builds. Format kept:
'<prefix>-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
2. HTTP engine: ktor-server-netty → ktor-server-cio. Netty is JVM-only;
CIO is KMP (jvm, linuxX64, ios, etc.) and feature-complete for our
needs (POST + SSE). standalone/build.gradle.kts drops netty and adds
cio; Main.kt imports io.ktor.server.cio.CIO and calls
embeddedServer(CIO, port = port).
Netty is left in libs.versions.toml (catalog entry kept) in case anyone
needs to fall back; only :standalone stopped depending on it.
E2E verified end-to-end against litellm local/codding: standalone-1.0.0
tarball starts on AGENTIK_PORT, GET /health -> 'ok', POST
/agentik/conversations -> 201 with new UUID-format id, POST .../messages
returns real LLM answer ('Ассистент'). 44/44 unit tests green.
McpLiteToolAdapter.parseArgsJson() used JsonPrimitive.content for every value
(always String), so MCP servers saw e.g. max_length:"500" and rejected
calls with '500' is not of type 'integer'. Replaced with kotlinx.serialization
booleanOrNull/intOrNull/longOrNull/doubleOrNull ladder that mirrors the
litert-openai parser.
ChatConversation.encodeArgsJson was also still calling encodeToolArgs
(stub from earlier pass). Inlined a small Any?.toJsonElement() helper
that preserves Boolean/Number/String/Map/List types when re-serializing
the LiteToolCall.arguments map into the argsJson string fed to
tool.invoke().
E2E re-verified: litellm local/codding + mcp-server-fetch now invokes
fetch__fetch({max_length:2000,url:...}) once, MCP gets a real
'Failed to fetch robots.txt' from the actual network sandbox instead
of rejecting the schema, LLM reports the real cause to the user.
Major additions:
* :proto (KMP submodule) — in-house stateful protocol replacing AG-UI.
Agent owns conversation transcript; Conversation.events(after) is a live,
replay-free stream; backfill via Conversation.getMessages(after, offset, limit).
Each Event carries an Instant date for client-side resume tracking.
Sealed hierarchies (Content/Message/Event/AgentEvent) annotated @Serializable
with snake_case @SerialName JSON discriminators so the wire format is
decoupled from Kotlin class names.
* :server (JVM, Ktor 3.1.3) — REST+SSE facade for Agent.
Public entry: Route.agentikAgent(agent, path = "/agentik").
Endpoints: create/list/get/patch/delete conversations, POST messages (202),
POST interrupt, GET messages, GET conversation events (SSE),
GET agent events (SSE), GET /health. Custom Instant serializer for
kotlin.time.Instant registered contextually on agentikJson (ISO-8601,
ignoreUnknownKeys=true, explicitNulls=false).
* :client (JVM, Ktor HTTP Client + CIO) — mirror of :server returning
a pw.binom.agentik.proto.Agent backed by HTTP calls. Custom SSE parser
since ktor-client-sse is not on the 3.1.3 client classpath.
* standalone — EchoProtoAgent (in-memory Agent for :proto), EchoAgent
(existing AG-UI echo), both mounted on the same Netty embedded server
on port 8080 (/agui and /agentik); A2A stays on its own CIO engine on
8081. EchoProtoAgent smoke-tested end-to-end against :server: all 11
endpoints, including live SSE delivery of StartResponse/AppendText/End
event triplets and Agent-level Created/Deleted events.
Design notes pinned in:
* agentik/IRC-QUESTIONS.md — closed 13-item checklist for the upcoming
:irc-server transport (channel = conversation, CTCP for structural
events, draft/chathistory for backfill, ImageStore side-channel, etc).
* docs/ARCHITECTURE.md — overall layout snapshot.