diff --git a/docs/diagrams/01-module-layers.md b/docs/diagrams/01-module-layers.md new file mode 100644 index 0000000..e3b5bab --- /dev/null +++ b/docs/diagrams/01-module-layers.md @@ -0,0 +1,158 @@ +# 01 — Слои модулей (целевое состояние) + +Целевая модульная структура agentik. Снизу вверх: +**приложения → runtime → домен → абстракции → платформенные impl**. + +![Module Layers](./01-module-layers.svg) + +PlantUML source (для редактирования; требует Graphviz `dot` для рендеринга): + +```plantuml +@startuml agentik-module-layers +skinparam componentStyle rectangle +skinparam ranksep 60 +skinparam nodesep 30 +skinparam packageStyle rectangle + +title agentik — слои модулей (целевое состояние) + +' --- Applications: entry points (thin wrappers) --- +package "Applications\n(entry points, тонкие)" { + [Standalone\nHTTP+AG-UI+A2A] as Standalone + [AgentikCli\nREPL] as Cli + [AgentikAndroid\nCompose UI] as Android +} + +' --- Agent runtime --- +package "Agent Runtime\n(композиция, lifecycle)" { + [AgentCore\nBaseAgent] as AgentCore + [AgentBuilder\nDSL] as Builder +} + +' --- Background work --- +package "Background Work\n(event-driven triggers)" { + [BackgroundEvents\nbus + events] as Ev + [BackgroundScheduler\npolicy] as Sched +} + +' --- Domain logic (generic, переиспользуется) --- +package "Domain Logic\n(generic tools)" { + [LlmTools\nReflector/Reviewer/Miner] as LlmT + [McpBridge\nMCP-SDK → LiteTool] as Mcp + [Skills\nparse + store] as Skills +} + +' --- Storage abstractions + impls --- +package "Storage\n(abstractions)" as StoragePkg { + [StorageCore\ninterfaces] as StorageCore +} + +package "Storage\n(JVM impls)" { + [StorageSqlite\nJDBC] as StorageSql + [StorageInmemory\ntests] as StorageInmem +} + +package "Storage\n(Android impl)" { + [StorageSqliteAndroid\nRoom/sqlite] as StorageSqlA +} + +' --- Memory backends --- +package "Memory\n(abstractions)" { + [MemoryApi\nMemorySystem/MemoryTools] as MemApi +} + +package "Memory\n(impls)" { + [MemoryMd\nHermes §-files] as MemMd + [MemoryVector\nJVector+JVM] as MemVec + [MemoryVectorAndroid\nONNX+ANN] as MemVecA +} + +' --- LLM backends --- +package "LLM\n(abstractions)" { + [LitertApi\nLiteLlm контракт] as Litert +} + +package "LLM\n(impls)" { + [LitertOpenai\nHTTP] as LitertO + [LitertGoogle\nLiteRT JVM] as LitertG + [LitertAndroid\nLiteRT Android] as LitertA +} + +' --- Inter-app protocol --- +package "Inter-app" { + [Proto\nAgent/Conversation] as Proto + [A2AServer] as A2A +} + +' --- Зависимости (приложения → runtime → домен → абстракции → платформенные импл) --- +Standalone ..> Builder +Cli ..> Builder +Android ..> Builder + +Builder ..> AgentCore +AgentCore ..> Proto +AgentCore ..> StorageCore +AgentCore ..> MemApi +AgentCore ..> Litert +AgentCore ..> Mcp +AgentCore ..> Skills + +Sched ..> Ev +AgentCore ..> Sched +AgentCore ..> Ev + +Mcp ..> Litert +LlmT ..> Litert + +MemMd ..> MemApi +MemVec ..> MemApi +MemVecA ..> MemApi + +StorageSql ..> StorageCore +StorageInmem ..> StorageCore +StorageSqlA ..> StorageCore + +LitertO ..> Litert +LitertG ..> Litert +LitertA ..> Litert + +Standalone ..> A2A +Standalone ..> LitertO +Standalone ..> LitertG +Standalone ..> StorageSql +Standalone ..> MemMd +Standalone ..> MemVec +Standalone ..> Mcp + +Android ..> LitertA +Android ..> StorageSqlA +Android ..> MemMd +Android ..> MemVecA + +@enduml +``` + +## Что показывает + +- **Applications** — три точки входа: web-сервер, CLI REPL, Android-приложение. Каждое тонкое, не содержит бизнес-логики. +- **Agent Runtime** — `BaseAgent` + `AgentBuilder` DSL. Вся композиция и lifecycle. +- **Background Work** — `BackgroundEvents` (event-bus) + `BackgroundScheduler` (policy подписки). Event-driven, не interval-polling. +- **Domain Logic** — generic переиспользуемые модули (`:llm-tools`, `:mcp-bridge`, `:skills`). +- **Storage / Memory / LLM** — каждая с абстракцией и одним или несколькими impl (JVM-only или Android-only). +- **Inter-app** — `:proto` контракты + `:a2a-server` для межагентного общения. + +## Текущее состояние vs целевое + +✅ Уже сделано (в этом цикле правок): +- `:llm-tools` extracted +- `:mcp-bridge` extracted +- `BackgroundScheduler` стал event-driven +- `ConversationLoop` стал отдельным компонентом (typealias `ChatConversation`) + +⏳ Не сделано: +- `:agent-core` (выделить `BaseAgent` + builder в отдельный KMP-модуль) +- `:background-events` (выделить events + scheduler — пока в `:standalone`) +- `:storage-sqlite-android` +- `:memory-vector-android` +- `:litert-android` +- `:agentik-android` (само приложение) diff --git a/docs/diagrams/01-module-layers.svg b/docs/diagrams/01-module-layers.svg new file mode 100644 index 0000000..53a2eca --- /dev/null +++ b/docs/diagrams/01-module-layers.svg @@ -0,0 +1 @@ +agentik — слои модулей (целевое состояние)Applications(entry points, тонкие)Agent Runtime(композиция, lifecycle)Background Work(event-driven triggers)Domain Logic(generic tools)Storage(abstractions)Storage(JVM impls)Storage(Android impl)Memory(abstractions)Memory(impls)LLM(abstractions)LLM(impls)Inter-appStandaloneHTTP+AG-UI+A2AAgentikCliREPLAgentikAndroidCompose UIAgentCoreBaseAgentAgentBuilderDSLBackgroundEventsbus + eventsBackgroundSchedulerpolicyLlmToolsReflector/Reviewer/MinerMcpBridgeMCP-SDK → LiteToolSkillsparse + storeStorageCoreinterfacesStorageSqliteJDBCStorageInmemorytestsStorageSqliteAndroidRoom/sqliteMemoryApiMemorySystem/MemoryToolsMemoryMdHermes §-filesMemoryVectorJVector+JVMMemoryVectorAndroidONNX+ANNLitertApiLiteLlm контрактLitertOpenaiHTTPLitertGoogleLiteRT JVMLitertAndroidLiteRT AndroidProtoAgent/ConversationA2AServer \ No newline at end of file diff --git a/docs/diagrams/02-agent-composition.md b/docs/diagrams/02-agent-composition.md new file mode 100644 index 0000000..317011e --- /dev/null +++ b/docs/diagrams/02-agent-composition.md @@ -0,0 +1,127 @@ +# 02 — Agent Builder: композиция (целевое API) + +Как `AgentBuilder` собирает `BaseAgent` из компонентов. **Memory backend сам объявляет свои tools** — builder их авто-мержит. BackgroundScheduler подписан на события, не interval-poll. + +![Agent Composition](./02-agent-composition.svg) + +PlantUML source (для редактирования; требует Graphviz `dot` для рендеринга): + +```plantuml +@startuml agent-composition +skinparam componentStyle rectangle + +title Agent Builder — композиция (целевое API) + +' --- Builder --- +rectangle "AgentBuilder" as Builder { + rectangle "llm: LiteLlm (обязательно)" as Llm + rectangle "storage: StorageBundle (обязательно)" as Storage + rectangle "memory: MemorySystem (обязательно)" as Mem + rectangle "soul: SoulProvider (default NoopSoul)" as Soul + rectangle "tools: List (авто-сборка из backends)" as Tools + rectangle "background: BackgroundConfig (default EmptyBg)" as Bg + rectangle "toolset: List (default empty)" as Ts +} + +' --- Backends with their tool side-effects --- +rectangle "MemoryMd" as MdMem { + interface "MemorySystem" as MemSys + interface "List" as MdTools + note right + MemoryMd.exposesTools() → + memory_save / memory_read / + memory_list / memory_delete + end note +} + +rectangle "McpRegistry" as McpReg { + interface "List" as McpTools + note right + McpRegistry.namedTools → + server__tool1, server__tool2, + ... + end note +} + +rectangle "BackgroundEvents" as Events { + interface "MutableSharedFlow" as Flow + note right + Эмитится из: + - CompactionCoordinator + - ToolDispatcher + - ConversationLoop.close() + end note +} + +rectangle "BackgroundScheduler" as Sched { + interface "policy: trigger + debounce" as Policy + note right + Подписан на Events. + НИКАКОГО interval-polling. + end note +} + +' --- Получаемый Agent --- +rectangle "BaseAgent\n(impl: ConversationLoop)" as Agent { + rectangle "send / interrupt / events" as API + rectangle "BackgroundScheduler\nподписка" as Sub +} + +' --- Стрелки зависимостей --- +Builder --> Llm +Builder --> Storage +Builder --> Mem +Builder --> Soul +Builder --> Tools +Builder --> Bg +Builder --> Ts + +MdMem --> Mem : implements +MdMem --> MdTools : exposes + +McpReg --> McpTools : exposes + +Bg --> Events : subscribes-to +Bg --> Sched : holds + +Tools <-- MdTools : auto-merge +Tools <-- McpTools : auto-merge + +Builder --> Agent : build() +Agent --> API +Agent --> Sub + +@enduml +``` + +## Целевой Kotlin DSL + +```kotlin +val agent = agentBuilder { + // Обязательные + llm(OpenAiLlm.fromEnv()) // или LitertAndroid.onDevice(context) + storage(SqliteStorage(path)) // или SqliteStorage.android(context) + memory(MemoryMd(root = "~/memory")) // или MemoryVector(embedding = HttpEmbedding(...)) + + // Опциональные + soul(FileSoul("~/SOUL.md")) // или HttpSoul(url), NoopSoul() + background { + // triggers: OnClosing (reflection+mining), OnCompaction(minTurns=10, mining=true) + // event-driven, не interval + } + tools { + // memoryMd.exposesTools() + mcpRegistry.namedTools авто-подцепляются + +FileReadTool(root = "/data") + } + toolset { + +MemoryToolsToolset(memoryMd) + } +}.build() +``` + +## Ключевые решения + +- **`MemoryBackend.exposesTools()`** — backend декларирует свои tools. Не «подставить любой backend», а «backend сообщает что он умеет». Это убирает coupling «какие tools совместимы с какими backends». +- **Builder требует только `llm + storage + memory`** как обязательные. Всё остальное — опционально с разумными default'ами (`NoopSoul`, `EmptyBackground`, `empty toolset`). +- **`BaseAgent`** — реализация `ConversationLoop` через builder. Конструктор принимает все нужные компоненты. **Один и тот же `BaseAgent` в `:standalone`, `:agentik-cli`, `:agentik-android`** — отличается только wiring через builder. +- **BackgroundScheduler подписан на `BackgroundEvents`** — это даёт event-driven по умолчанию. `OnEvery(n)` interval-режим — опциональный fallback (не default). diff --git a/docs/diagrams/02-agent-composition.svg b/docs/diagrams/02-agent-composition.svg new file mode 100644 index 0000000..c6891e0 --- /dev/null +++ b/docs/diagrams/02-agent-composition.svg @@ -0,0 +1 @@ +Agent Builder — композиция (целевое API)AgentBuilderMemoryMdMcpRegistryBackgroundEventsBackgroundSchedulerBaseAgent(impl: ConversationLoop)llm: LiteLlm (обязательно)storage: StorageBundle (обязательно)memory: MemorySystem (обязательно)soul: SoulProvider (default NoopSoul)tools: List<NamedTool> (авто-сборка из backends)background: BackgroundConfig (default EmptyBg)toolset: List<ToolsetContribution> (default empty)MemorySystemList<NamedTool>MemoryMd.exposesTools() →memory_save / memory_read /memory_list / memory_deleteList<NamedTool>McpRegistry.namedTools →servertool1, servertool2,...MutableSharedFlow<CompactionEvent|ToolCallEvent|LifecycleEvent>Эмитится из:- CompactionCoordinator- ToolDispatcher- ConversationLoop.close()policy: trigger + debounceПодписан на Events.НИКАКОГО interval-polling.send / interrupt / eventsBackgroundSchedulerподпискаimplementsexposesexposessubscribes-toholdsauto-mergeauto-mergebuild() \ No newline at end of file diff --git a/docs/diagrams/03-multi-user-chat.md b/docs/diagrams/03-multi-user-chat.md new file mode 100644 index 0000000..607af1d --- /dev/null +++ b/docs/diagrams/03-multi-user-chat.md @@ -0,0 +1,129 @@ +# 03 — Multi-user chat с mention-detection + +Точка 1 из планов. Один `BaseAgent` обслуживает N пользователей. Отвечает только когда addressed (mention или admin-команда). + +![Multi-user chat](./03-multi-user-chat.svg) + +PlantUML source (для редактирования; требует Graphviz `dot` для рендеринга): + +```plantuml +@startuml multi-user-chat +skinparam componentStyle rectangle +skinparam participantPadding 15 +skinparam boxPadding 10 + +title Multi-user chat с mention-detection (точка 1 из планов) + +' --- Участники --- +actor "User A" as UA +actor "User B" as UB +actor "User C\n(админ)" as UC +participant "Telegram /\nSlack /\nMatrix" as Channel +participant "AgentRuntime\n(BaseAgent)" as Runtime +participant "MentionDetector" as Detector +participant "SoulProvider" as Soul +participant "MemorySystem\n(MdMemory)" as Memory +participant "LlmBackend\n(LiteLlm)" as Llm + +' --- Сценарий --- +UA -> Channel : "@bot, что нового?" +UB -> Channel : "люблю котов" +UC -> Channel : "/bot status" +Channel -> Runtime : событие чата + +' --- Внутри Runtime --- +Runtime -> Detector : isMentioned(message, botName) + +note right of Detector + variants: + - SimpleMentionDetector (regex: @bot) + - LlmMentionDetector (mini-classifier) + - AdminCommandDetector (/command) +end note + +Detector --> Runtime : MatchResult{isMentioned, isCommand} + +alt isMentioned или isCommand + Runtime -> Soul : read() + Runtime -> Memory : prefetch(query, topK) + Runtime -> Llm : send(system + history + memory + user) + Llm --> Runtime : response + tool_calls + Runtime -> Memory : save(decision) + Runtime --> Channel : ответ в нужный канал/thread +else NOT mentioned и NOT command + Runtime -> Runtime : drop (если не админ) + note right + Не отвечаем, но возможно: + - запоминаем факт (memory-only update) + - summary на long conversation + end note +end + +@enduml +``` + +## Ключевые модули (что нужно будет добавить) + +### `MentionDetector` interface + +```kotlin +interface MentionDetector { + data class Result( + val isMentioned: Boolean, + val isAdminCommand: Boolean, + val isPrivateMessage: Boolean, // DM — всегда отвечаем + ) + + fun detect(message: ChatMessage, botName: String): Result +} +``` + +Имплементации: +- `SimpleMentionDetector` — regex `@bot`, `/command` (дешёво, latency ~0) +- `LlmMentionDetector` — маленькая классификация через тот же LLM (точнее, но +1 LLM-вызов на каждое сообщение) +- `HybridMentionDetector` — fast regex → fallback на LLM только если ambiguous + +### `ChatAdapter` interface + +```kotlin +interface ChatAdapter { + val channel: String // "telegram" / "slack" / "matrix" + suspend fun listen(onMessage: (ChatMessage) -> Unit): Job + suspend fun reply(messageId: String, text: String, threadId: String? = null) + suspend fun isAdmin(userId: String): Boolean +} +``` + +Имплементации per platform. Каждая адаптирует формат platform → `ChatMessage`. + +### Конфигурация builder'а + +```kotlin +agentBuilder { + llm(...) + storage(...) + memory(...) + soul(...) + background { ... } + chat { + mentionDetector = HybridMentionDetector(regex = "@bot|@Agent", llmClassifier = false) + chatAdapter = TelegramChatAdapter(token = "...") + // На каждое сообщение: + // 1. mentionDetector.detect() + // 2. если isMentioned || isAdminCommand || isPrivate → process + // 3. иначе — опционально memory-only save (тихий режим) + } +} +``` + +## Что это даёт + +- Один `BaseAgent` обслуживает чат целиком (один LLM, одна память — общий контекст команды) +- `@bot` — explicit invocation, не «agent отвечает на всё подряд» +- `/bot status` / `/bot clear-memory` — admin-команды (отдельный канал, без LLM) +- DM — всегда отвечает (это личное обращение) +- В групповом чате без mention — agent может **молча учить** (memory update без ответа). Полезно для «запомнил что Вася любит котов». + +## Текущее состояние vs целевое + +⏳ Ничего из этого нет. Сейчас `:standalone` — это HTTP API, к которому подключаются clients. Для multi-user chat нужен новый `:chat-adapter-telegram` (или -slack / -matrix) модуль + `MentionDetector` interface. diff --git a/docs/diagrams/03-multi-user-chat.svg b/docs/diagrams/03-multi-user-chat.svg new file mode 100644 index 0000000..8cce72a --- /dev/null +++ b/docs/diagrams/03-multi-user-chat.svg @@ -0,0 +1 @@ +Multi-user chat с mention-detection (точка 1 из планов)Please use CSS style instead of skinparam ParticipantPaddingUser AUser BUser CTelegram .AgentRuntimeMentionDetectorSoulProviderMemorySystemLlmBackendUser AUser BUser C(админ)Telegram /Slack /MatrixAgentRuntime(BaseAgent)MentionDetectorSoulProviderMemorySystem(MdMemory)LlmBackend(LiteLlm)User AUser BUser C(админ)Telegram /Slack /MatrixAgentRuntime(BaseAgent)MentionDetectorSoulProviderMemorySystem(MdMemory)LlmBackend(LiteLlm)"@bot, что нового?""люблю котов""/bot status"событие чатаisMentioned(message, botName)variants:- SimpleMentionDetector (regex: @bot)- LlmMentionDetector (mini-classifier)- AdminCommandDetector (/command)MatchResult{isMentioned, isCommand}alt[isMentioned или isCommand]read()prefetch(query, topK)send(system + history + memory + user)response + tool_callssave(decision)ответ в нужный канал/thread[NOT mentioned и NOT command]drop (если не админ)Не отвечаем, но возможно:- запоминаем факт (memory-only update)- summary на long conversation \ No newline at end of file diff --git a/docs/diagrams/04-sub-agents.md b/docs/diagrams/04-sub-agents.md new file mode 100644 index 0000000..34d8d59 --- /dev/null +++ b/docs/diagrams/04-sub-agents.md @@ -0,0 +1,119 @@ +# 04 — Sub-agents + A2A между агентами + +Точки 2 и 3 из планов. Orchestrator-агент spawn'ит sub-агентов с изолированным контекстом. Независимые агенты общаются через A2A. + +![Sub-agents + A2A](./04-sub-agents.svg) + +PlantUML source (для редактирования; требует Graphviz `dot` для рендеринга): + +```plantuml +@startuml sub-agents-and-a2a +skinparam componentStyle rectangle + +title Sub-agents + A2A между агентами (точка 2+3 из планов) + +' --- Orchestrator --- +rectangle "OrchestratorAgent\n(BaseAgent + tools)" as Orch { + rectangle "ConversationLoop\n(main user)" as MainConv +} + +' --- Sub-agent spawn --- +rectangle "subAgent(\n task: String,\n config: AgentConfig\n): Flow" as SpawnAPI +note right of SpawnAPI + Spawn API — НЕ отдельный модуль, + а convenience поверх BaseAgent: + val sub = agent.spawnChild(config) { + systemPrompt = "..." + tools = [ReadTool, WriteTool] + memory = EmptyMemory // изолированно + } + sub.events.collect { ... } +end note + +' --- Дочерний агент (изолированный контекст) --- +rectangle "SubAgent\n(изолированный scope)" as Sub { + rectangle "ConversationLoop\n(child)" as SubConv + rectangle "backgroundScope\n(lifecycle scoped)" as SubBg +} + +' --- A2A между независимыми агентами --- +rectangle "Agent A\n(BaseAgent)" as AgentA +rectangle "Agent B\n(BaseAgent)" as AgentB +rectangle "A2A Server\n(:a2a-server)" as A2ASrv + +AgentA -> A2ASrv : POST /\n(application/json) +A2ASrv -> AgentB : dispatch(message) +AgentB --> A2ASrv : response +A2ASrv --> AgentA : SSE / JSON-RPC + +' --- Стрелки --- +Orch -> SpawnAPI : calls +SpawnAPI -> Sub : creates with custom config +Sub -> SubBg : has its own +Orch -> Orch : main flow continues +Sub --> Orch : Flow emits\n(Started / ToolCalled / ToolResult /\nAssistantMessage / Done / Failed) + +Orch -> A2ASrv : can also delegate to remote agent + +@enduml +``` + +## Sub-agents API + +```kotlin +sealed interface SubAgentEvent { + data class Started(val taskId: String) : SubAgentEvent + data class AssistantMessage(val text: String) : SubAgentEvent + data class ToolCalled(val toolName: String, val args: JsonObject) : SubAgentEvent + data class ToolResult(val toolName: String, val result: String) : SubAgentEvent + data class Done(val taskId: String, val finalResult: String) : SubAgentEvent + data class Failed(val taskId: String, val error: String) : SubAgentEvent +} + +interface BaseAgent { + // ... existing methods ... + + /** + * Spawn дочерний агент с изолированным контекстом (memory, system prompt, + * tools). Возвращает Flow событий жизненного цикла + результата. + * Cancellation родителя НЕ отменяет sub-agent — sub-agent живёт до Done/Failed. + */ + fun spawnChild(config: SubAgentConfig): Flow +} + +data class SubAgentConfig( + val systemPrompt: String, + val tools: List = emptyList(), + val memory: MemorySystem = EmptyMemory(), + val model: LiteLlm? = null, // если null — делит LLM родителя + val maxTurns: Int = 10, + val timeoutMs: Long = 60_000, +) +``` + +## Зачем изолированный scope + +Sub-agent получает **свою копию контекста**, не делит memory с родителем. Это критично: +- `research_subagent` — должен исследовать тему, не отвечать на основные сообщения пользователя +- `summarize_subagent` — суммаризировать документ, не трогать основной диалог +- `code_review_subagent` — ревьюить PR, не видеть разговор + +Если нужно расшарить контекст — это explicit через `sharedMemory: SharedMemoryHandle` параметр, не default. + +## A2A между независимыми агентами + +Уже есть `:a2a-server` модуль (см. `standalone/build.gradle.kts` — `implementation(libs.a2a.server)`). Использовался для AG-UI/A2A протокола в `:standalone`. Можно переиспользовать для межагентного общения. + +Сценарий: orchestrator-agent не может сам решить задачу → делегирует remote-агенту через A2A → получает response → продолжает. Это уже работающая инфраструктура. + +## Текущее состояние vs целевое + +✅ Уже есть: +- `:a2a-server` подключён +- `BaseAgent.spawnChild` — **не существует**, но `ConversationLoop` уже умеет создавать изолированный scope через свой `agentScope` — нужна только обёртка + +⏳ Не сделано: +- `SubAgentConfig` + `Flow` API +- `EmptyMemory` (null-object для изолированного scope) +- Lifecycle management (parent dies → child должен complete or be cancelled?) +- Сериализация sub-agent state для отладки (event log) diff --git a/docs/diagrams/04-sub-agents.svg b/docs/diagrams/04-sub-agents.svg new file mode 100644 index 0000000..f1ef972 --- /dev/null +++ b/docs/diagrams/04-sub-agents.svg @@ -0,0 +1 @@ +Sub-agents + A2A между агентами (точка 2+3 из планов)OrchestratorAgent(BaseAgent + tools)SubAgent(изолированный scope)ConversationLoop(main user)ConversationLoop(child)backgroundScope(lifecycle scoped)subAgent(task: String,config: AgentConfig): Flow<SubAgentEvent>Spawn API — НЕ отдельный модуль,а convenience поверх BaseAgent:val sub = agent.spawnChild(config) {systemPrompt = "..."tools = [ReadTool, WriteTool]memory = EmptyMemory // изолированно}sub.events.collect { ... }Agent A(BaseAgent)Agent B(BaseAgent)A2A Server(:a2a-server)POST /(application/json)SSE / JSON-RPCdispatch(message)responsecallscreates with custom confighas its ownmain flow continuesFlow<SubAgentEvent> emits(Started / ToolCalled / ToolResult /AssistantMessage / Done / Failed)can also delegate to remote agent \ No newline at end of file diff --git a/docs/diagrams/05-android-stack.md b/docs/diagrams/05-android-stack.md new file mode 100644 index 0000000..fb3c8d8 --- /dev/null +++ b/docs/diagrams/05-android-stack.md @@ -0,0 +1,171 @@ +# 05 — Android Agent Stack + +Что меняется vs `:standalone`. Цель: `BaseAgent` тот же самый, но platform-impl разные (Storage, LLM, Vector Memory, MCP). + +![Android Agent Stack](./05-android-stack.svg) + +PlantUML source (для редактирования; требует Graphviz `dot` для рендеринга): + +```plantuml +@startuml android-agent-stack +skinparam componentStyle rectangle + +title Android Agent Stack — что меняется vs Standalone + +' --- Android side --- +package "Android Application" { + [MainActivity\n(Compose)] as Activity + [AndroidAgentRunner\n(workmanager / service)] as Runner + [AndroidAgentBuilder] as AndroidBuilder +} + +package "Android-specific impls" { + [StorageSqliteAndroid\n(Room/sqlite)] as StorageA + [MemoryVectorAndroid\n(ONNX runtime + ANN)] as MemVecA + [LitertAndroid\n(NNAPI delegate)] as LitertA + [SoulFileAndroid\n(context.filesDir)] as SoulA + [McpRegistry\nstdio: ProcessBuilder] as McpA +} + +' --- Shared (KMP) --- +package "Agent Runtime (shared)" { + [AgentCore\nBaseAgent] as AgentCore + [AgentBuilder] as Builder +} + +package "Domain (shared)" { + [LlmTools\ncommonMain] as LlmT + [BackgroundEvents\ncommonMain] as Ev + [McpBridge\njvmMain] as McpB + [Skills\ncommonMain] as Skills +} + +package "Memory (shared impl)" { + [MemoryMd\n(commonMain)] as MemMd + [MemoryApi\ninterfaces] as MemApi +} + +' --- Зависимости --- +Activity --> Runner +Runner --> AndroidBuilder +AndroidBuilder --> AgentCore + +AndroidBuilder --> StorageA +AndroidBuilder --> MemVecA +AndroidBuilder --> LitertA +AndroidBuilder --> SoulA +AndroidBuilder --> McpA + +AgentCore --> LlmT +AgentCore --> Ev +AgentCore --> McpB +AgentCore --> Skills +AgentCore --> MemMd + +' --- Главные отличия от Standalone --- +note right of LitertA + On-device inference. + LiteRT с NNAPI delegate → + работает на CPU/GPU/NPU + прямо на устройстве, без сети. + + vs Standalone: HTTP-only + (OpenAI-compatible). +end note + +note right of StorageA + android.database.sqlite + через Room или сырой API. + + vs Standalone: JDBC + + Sqlite-JDBC driver + (только JVM). +end note + +note right of MemVecA + JVector JVM-only. На Android + нужна альтернатива — + ONNX Runtime + какой-нибудь + ANN (Annoy/HNSW). + + Или пока без vector memory, + только MemoryMd. +end note + +note right of McpA + MCP через ProcessBuilder + на Android работает, но + subprocess lifecycle + сложнее (foreground service + нужен для долгого subprocess). +end note + +@enduml +``` + +## Что общего с `:standalone` + +**`BaseAgent`, `BackgroundScheduler`, `LlmTools`, `McpBridge`, `Skills`, `MemoryMd` — всё KMP (commonMain).** Android-agent = `:standalone` с другим wiring'ом. Не нужно переписывать agent logic. + +## Что другое + +| Компонент | `:standalone` (JVM) | `:agentik-android` (Android) | Сложность | +|---|---|---|---| +| Storage | `:storage-sqlite` (JDBC + Sqlite-JDBC) | `:storage-sqlite-android` (Room или raw) | Низкая — тот же `StorageBundle` interface | +| LLM | `:litert-openai` (HTTP), `:litert-google` (LiteRT JVM) | `:litert-android` (LiteRT Android, NNAPI delegate) | Средняя — нужен новый модуль | +| Vector memory | `:memory-vector` (JVector) | `:memory-vector-android` (ONNX Runtime + HNSW/Annoy) | Высокая — JVector JVM-only, нужна альтернатива | +| SOUL provider | `FileSoulProvider` (path) | `SoulFileAndroid` (`context.filesDir`) | Низкая | +| MCP | `McpRegistry` (ProcessBuilder, stdio subprocess) | Тот же `McpRegistry`, но subprocess в foreground service | Средняя — нужен Android service | +| Embedding | `HttpEmbeddingClient` (HTTP) | Тот же ИЛИ on-device (ONNX) | Средняя | + +## Минимальный Android agent (v1) + +Если не нужны все фичи сразу — минимум: + +```kotlin +val agent = androidAgentBuilder(context) { + llm(LitertAndroid.onDevice(context, modelPath = "/data/local/tmp/model.litertlm")) + storage(SqliteStorage.android(context, "agent.db")) + memory(MemoryMd.root(context.filesDir.resolve("memory"))) + soul(FileSoul(context.filesDir.resolve("SOUL.md"))) + background { + // OnClosing + OnCompaction работают так же как на JVM + } +} +``` + +Без MCP, без vector memory (только MemoryMd на файлах), только on-device LLM. Достаточно для off-line агента. + +## Foreground service для MCP + +Если нужны MCP-серверы (например, локальный file-system MCP) — subprocess нужен foreground service чтобы Android не убил его при выключении экрана. Это добавляет сложности: + +```kotlin +class McpForegroundService : Service() { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForeground(NOTIFICATION_ID, notification) + val proc = ProcessBuilder(command, args).start() + // ... route stdio to McpLiteToolAdapter ... + return START_STICKY + } +} +``` + +Пока можно без этого (только если MCP нужен на Android). + +## Текущее состояние vs целевое + +✅ KMP-ready: +- `:llm-tools` (commonMain, платформо-агностик) +- `:mcp-bridge` (jvmMain — Android-вариант через `:mcp-bridge-android`) +- `:skills` (commonMain) +- `:memory-md` (commonMain) +- `:proto` (commonMain) + +⏳ Не существует: +- `:storage-sqlite-android` +- `:memory-vector-android` +- `:litert-android` +- `:agentik-android` (само приложение) +- `:agent-core` (выделить BaseAgent + builder) +- `:background-events` (выделить events + scheduler) diff --git a/docs/diagrams/05-android-stack.svg b/docs/diagrams/05-android-stack.svg new file mode 100644 index 0000000..93e224b --- /dev/null +++ b/docs/diagrams/05-android-stack.svg @@ -0,0 +1 @@ +Android Agent Stack — что меняется vs StandaloneAndroid ApplicationAndroid-specific implsAgent Runtime (shared)Domain (shared)Memory (shared impl)MainActivity(Compose)AndroidAgentRunner(workmanager / service)AndroidAgentBuilderStorageSqliteAndroid(Room/sqlite)MemoryVectorAndroid(ONNX runtime + ANN)LitertAndroid(NNAPI delegate)SoulFileAndroid(context.filesDir)McpRegistrystdio: ProcessBuilderAgentCoreBaseAgentAgentBuilderLlmToolscommonMainBackgroundEventscommonMainMcpBridgejvmMainSkillscommonMainMemoryMd(commonMain)MemoryApiinterfacesOn-device inference.LiteRT с NNAPI delegate →работает на CPU/GPU/NPUпрямо на устройстве, без сети. vs Standalone: HTTP-only(OpenAI-compatible).android.database.sqliteчерез Room или сырой API. vs Standalone: JDBC +Sqlite-JDBC driver(только JVM).JVector JVM-only. На Androidнужна альтернатива —ONNX Runtime + какой-нибудьANN (Annoy/HNSW). Или пока без vector memory,только MemoryMd.MCP через ProcessBuilderна Android работает, ноsubprocess lifecycleсложнее (foreground serviceнужен для долгого subprocess). \ No newline at end of file diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md new file mode 100644 index 0000000..989567d --- /dev/null +++ b/docs/diagrams/README.md @@ -0,0 +1,64 @@ +# agentik — диаграммы архитектуры + +PlantUML-схемы для обсуждения будущей структуры (Android agent, multi-user chat, sub-agents, A2A). Это **целевое состояние**, не текущее. + +## Файлы + +Каждый `.md` содержит: +- Краткое описание (что показывает) +- **Пред-рендеренный SVG** (`![Diagram](file.svg)`) — гарантированно показывается **везде** +- PlantUML source в ` ```plantuml ` блоке — для редактирования (требует Graphviz `dot` для рендеринга) +- Дополнительный markdown-текст (что нужно сделать, текущее vs целевое) + +| Файл | Что показывает | +|---|---| +| [01-module-layers.md](./01-module-layers.md) | Целевая модульная структура (приложения → runtime → домен → абстракции → платформенные impl). Что в каком слое и кто от кого зависит. | +| [02-agent-composition.md](./02-agent-composition.md) | Как `AgentBuilder` собирает `BaseAgent` из компонентов. Memory backend сам объявляет свои tools. BackgroundScheduler подписан на события (НЕ interval-poll). | +| [03-multi-user-chat.md](./03-multi-user-chat.md) | Сценарий: чат с N пользователями, mention-detection, админ-команды, agent отвечает только когда addressed. | +| [04-sub-agents.md](./04-sub-agents.md) | Orchestrator spawn'ит sub-agent с изолированным контекстом, получает `Flow`. A2A между независимыми агентами через `:a2a-server`. | +| [05-android-stack.md](./05-android-stack.md) | Что меняется на Android: on-device LLM (NNAPI), Room/sqlite, ONNX-based vector memory, foreground-service для MCP subprocess. | + +## Почему SVG + PlantUML source + +PlantUML требует Java + (для component/class/deployment диаграмм) Graphviz `dot`. Если `dot` не установлен — рендерер падает с ошибкой "Executable dot does not exist". + +Решение: **пре-рендерим в SVG один раз** и вставляем как ``. Диаграмма гарантированно показывается в любом markdown-viewer (GitHub, IntelliJ, VSCode, GitLab) без зависимостей. PlantUML source в code block остаётся для редактирования. + +## Как редактировать диаграмму + +1. Меняешь PlantUML-source в ` ```plantuml ` блоке `.md` файла. +2. Ре-рендеришь SVG: + ```bash + mkdir -p /tmp/plantuml-work && chmod 777 /tmp/plantuml-work + cp docs/diagrams/*.md /tmp/plantuml-work/ + docker run --rm -v /tmp/plantuml-work:/work plantuml/plantuml -tsvg /work/*.md + cp /tmp/plantuml-work/*.svg docs/diagrams/ + ``` +3. Проверяешь что SVG обновился: + ```bash + ls -la docs/diagrams/*.svg + ``` +4. Коммитишь оба файла: `.md` (source) и `.svg` (rendered). + +Требует Docker (или локального PlantUML+Graphviz). `apt install graphviz` для Arch/Manjaro. + +## Контекст + +Текущий код движется в эту сторону: +- `:llm-tools` extracted ✅ +- `:mcp-bridge` extracted ✅ +- `BackgroundScheduler` стал event-driven ✅ +- `ConversationLoop` стал отдельным компонентом ✅ + +Не сделано (см. детали в каждом .md): +- `:agent-core` (выделить `BaseAgent` + builder) +- `:background-events` (выделить events + scheduler) +- `:storage-sqlite-android`, `:memory-vector-android`, `:litert-android` +- `:agentik-android` (само приложение) +- `MentionDetector` interface + adapters для multi-user chat +- `BaseAgent.spawnChild` + `Flow` + +Подробнее: +- `STANDALONE-REVIEW.md` — что плохо в текущем коде +- `MEMORY-DESIGN.md` — детали memory архитектуры +- `STANDALONE.md` — текущий standalone