Commit Graph

15 Commits

Author SHA1 Message Date
subochev 0faad45f3d standalone: persist turn errors so polling clients see them
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).
2026-09-14 00:26:13 +03:00
subochev adcb8f54d6 skills: каталог + ленивая загрузка read_skill
Пользовательские инструкции («навыки») живут в указанной папке
(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, навыки не утекают в
промпт телом).
2026-09-14 00:25:22 +03:00
subochev 3fde5c28e3 config: единая AgentikConfig DTO + сериализация всей конфиг-цепочки
- 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.
2026-09-13 21:49:12 +03:00
subochev f4caab940e skills: парсер YAML-скилов (opencode-формат)
Новый KMP-модуль :skills (9 целей, зеркалит :proto). Парсит скилы
в формате opencode: YAML-фронтматтер с name+description, затем markdown
body.

  - SkillFile(name, description, body) — @Serializable результат.
  - SkillParseError — sealed-иерархия ошибок: Empty, MissingOpeningFence,
    MissingClosingFence, InvalidYaml, SchemaMismatch, BlankName.
  - SkillParser.parse(raw): SkillParseResult + parseOrThrow(raw): SkillFile.
    Обрабатывает BOM, CRLF, имена с двоеточиями (backend:spring:db-base),
    multiline-YAML, пустое тело; неизвестные ключи фронтматтера игнорирует.
  - SkillParserTest — 16 тестов, 0 failures.

YAML-библиотека — com.charleskorn.kaml 0.104.0 (не JetBrains,
интеграция с kotlinx @Serializable). Грабли kaml 0.104: параметр
ignoreUnknownKeys переименован в strictMode (инвертирован);
missing-required-field прилетает как YamlException, не
SerializationException — различаем по сообщению.

Проверка: :skills:jvmTest 16/16, :skills:assemble (9 целей) — зелёные.
2026-09-13 21:28:47 +03:00
subochev cad4d5fc7c AGUI: удалён полностью
Убрана зависимость 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) — зелёные.
2026-09-13 19:10:20 +03:00
subochev d5e3f2dcef proto: drop dead kotlinx-datetime dep, keep only kotlin.time.Instant
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.
2026-09-13 19:04:32 +03:00
subochev 5b4c8ceae8 server: convert :server from kotlin-jvm to KMP with all 9 targets
Punkt 6 of NATIVE-COMPATIBILITY.md. Mirrors :proto's target set:
jvm + macosX64 + macosArm64 + iosX64 + iosArm64 + iosSimulatorArm64
+ linuxX64 + linuxArm64 + mingwX64.

All 9 targets compile via './gradlew :server:assemble'.

No blockers: the only deps (ktor-server-core, ktor-server-content-
negotiation, ktor-serialization-kotlinx-json, kotlinx-coroutines-core,
kotlinx-serialization-json) ship native variants for every target.
kotlin.time.Instant was already in use (not the deprecated
kotlinx.datetime.Instant), so no proto-side work needed.

Only API change vs the JVM version: respondTextWriter (JVM-only) ->
respondBytesWriter + writeStringUtf8 (KMP, ByteWriteChannel API).
SSE behaviour is identical ('data: <json>\n\n' chunks flushed as
they arrive).

Verification: :standalone:jvmTest 44/44 green after the migration
(ChatAgent 15, LlmConfig 6, McpConfig 8, McpRegistry 4, Persistence 11).
No call-site changes needed; :standalone picks up the new :server
jvm artifact automatically.
2026-09-13 18:28:12 +03:00
subochev adab112b5b docs: NATIVE-COMPATIBILITY.md checklist for linuxX64 bring-up 2026-09-13 18:20:10 +03:00
subochev e816d8d9d1 prepare linuxX64: switch to kotlin.uuid.Uuid and ktor-server-cio
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.
2026-09-13 18:16:30 +03:00
subochev 1e4c4d679f mcp+tools: preserve JSON primitive types across the wire
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.
2026-09-13 17:50:41 +03:00
subochev 4c66947c25 standalone: add MCP client + tool-call loop
- Add mcp/McpConfig + McpRegistry wrapping io.modelcontextprotocol:kotlin-sdk-client 0.15.0
  - Supports stdio (uvx/npx/python) and streamable HTTP transports
  - Claude Desktop-compatible JSON config (AGENTIK_MCP_CONFIG)
  - server__tool name prefix to avoid collisions between servers
- Add agent/NamedTool (name + LiteTool pair) and tools: List<NamedTool> on ChatAgent/ChatConversation
- Implement tool-call loop in ChatConversation.runTurn:
  - delta.toolCalls -> emit ToolCall event -> persist audit -> execute tool
    -> emit ToolResult -> persist -> liteConv.addToolResult(callId, name, result)
  - separate tc-/tr- prefixes keep SQL PRIMARY KEY unique while toolCallId FK is preserved
- 8 McpConfig + 4 McpRegistry unit tests; +1 ChatAgentTest tool-loop test (44/44 total)
- e2e verified: real MCP fetch server (mcp-server-fetch) + litellm local/codding
  -> LLM calls fetch__fetch, MCP exec, result fed back, conversation continues

docs/STANDALONE.md: drop 'no tools / no MCP' from §8; replace 'Подключить тул (v2)' stub
with full in-agent + MCP recipe and tool-loop algorithm in §7
2026-09-13 15:59:58 +03:00
subochev afdfb37e35 Upgrade litert-api/litert-google/litert-openai to v7 (litertlm-jvm 0.17.0), drop workarounds 2026-09-13 14:46:09 +03:00
subochev 9e5d61707d standalone v1: dual-backend (openai + litert-google) with SQLDelight dual-log persistence
Replace EchoProtoAgent / EchoAgent / EchoA2aHandler placeholders with a real
stateful agent on top of SQLite (SQLDelight 2.3.2) and litert-api v6.

persistence (commonMain):
- ConversationStore / MessageStore / WorkingMemoryStore — three narrow
  interfaces, all operations suspend, AutoCloseable.
- MessageRecord sealed: UserMessage / AssistantMessage (Body subtype),
  ToolCall / ToolResult (audit-only), Summary / System (working-memory-only
  synthetic). Snake-case @SerialName discriminators.
- WorkingMemoryEntry sealed: System / User(sourceMessageId) /
  Assistant(sourceMessageId); sourceMessageId is null for System.
- Two-table dual-log model: append-only message audit + mutable
  working_memory with monotonic order_idx.

SQLite (jvmMain):
- SQLDelight schema + SqliteConversationStore / SqliteMessageStore /
  SqliteWorkingMemoryStore under src/jvmMain/sqldelight/.
- SqliteStores.open(path) / inMemory(); Schema.create gated on
  sqlite_master probe for idempotency.
- All payload_json is the MessageRecord encoded as JSON; subtype-specific
  fields avoid migrations.

agent (jvmMain):
- ChatAgent — stateful proto.Agent with live in-memory cache, lock-protected,
  AgentEvent bus (Created/Deleted).
- ChatConversation — long-lived LiteConversation handle; created lazily on
  first send from working_memory (system + initial messages), reused across
  all subsequent turns (REQUIRED for litert-google KV-cache).
- Per turn: append User to audit + WM → sendStreamContents (wrapped in
  transformWhile for litert-google-jvm 0.16.1 isDone workaround) → emit
  AppendText deltas → append Assistant to audit + WM + touch conversation.
- isClosed flag so getConversation reconstructs after close.

llm (jvmMain):
- LlmConfig data class with LlmBackend enum (OPENAI / GOOGLE); fromEnv
  parses AGENTIK_LLM_BACKEND and dispatches to backend-specific config.
- OpenAI: litert-openai, OpenAI-compatible endpoint, validated
  baseUrl/apiKey/model.
- Google: litert-google (reflection-resolved pw.binom.litert.google
  factory) on top of litertlm-jvm 0.16.1 native engine;
  visionBackend/audioBackend = null (LiteRT-LM 0.16.1 binds encoder
  graph even with null backend, but a model lacking encoder crashes;
  null is the correct "don't bind" signal).
- foldSystemIntoFirstUser (default true for GOOGLE) folds system prompt
  into the first user message to avoid chat template alternation issues.

build:
- Add sqldelight plugin + runtime + sqlite-driver + coroutines-extensions
  to gradle/libs.versions.toml.
- litert-openai: implementation; litert-google: runtimeOnly (resolved via
  reflection at runtime).
- KMP jvm executable via @OptIn(ExperimentalKotlinGradlePluginApi) +
  jvm { binaries { executable { mainClass.set("...MainKt") } } }.

tests (jvmTest): 30 passing
- PersistenceTest (11): conversation upsert/list/cascade-delete/rename/
  touch; message audit append/list; working-memory order preservation;
  image-content payload roundtrip.
- ChatAgentTest (14): system-prompt seeding; persistent vs temp
  persistence across SqliteStores reopen; multi-turn audit + WM growth;
  interrupt of in-flight slow send; agentEvents Created/Deleted flow;
  closed-conv reconstruct via getConversation.
- LlmConfigTest (6): env happy path, defaults, missing fields throw.

smoke tested e2e:
- openai backend against real llm.binom.pw/v1 (myopenai/local/codding)
  — multi-turn dialogue persisted, kill -9 + restart survives.
- google backend against gemma-4-E2B-it.litertlm — multi-turn
  ("Hello there!" → "2 + 2 = 4"), KV-cache survives across turns,
  SSE start→append_text*→end cleanly closes.

docs/STANDALONE.md updated for v1 architecture, dual-backend env table,
long-lived LiteConversation invariant, and litert-google-jvm 0.16.1
isDone-stream workaround.
2026-09-13 12:39:36 +03:00
subochev a3581abf84 Bring up :proto protocol + :server (Ktor) + :client (HTTP) modules; wire :server into standalone with EchoProtoAgent
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.
2026-09-12 01:10:27 +03:00
subochev d45a35a4af Initial commit 2026-09-09 23:15:39 +00:00