# Agentik — архитектура (на основе ограничений AGUI) ## 1. Контекст agentik — runtime агента. Внешне он exposes два фасада: - **AG-UI** — SSE-поток событий для UI-клиентов (`POST /agui`), транспорт Netty. - **A2A** — JSON-RPC для agent↔agent (`POST /`, `message/send`), транспорт CIO. Ядро (сессии, чат-цикл, тулы, LLM, MCP) **не зависит от транспорта**: фасады лишь сериализуют/десериализуют его. Библиотеки из каталога `caffeine`: `agui` 0.1.0, `a2a` 1.0.0-SNAPSHOT; LLM — `pw.binom.openai` (ktor-impl). ## 2. Ограничения AGUI (что диктует протокол) AGUI — push-протокол. Агент = `Agent { RunAgentInput -> Flow }`. Ключевые типы (из `pw.binom.agui.api`): | Тип | Смысл | |---|---| | `RunAgentInput { threadId, runId, state, messages, tools, context, forwardedProps }` | весь ввод хода. Клиент шлёт всё, сервер по умолчанию stateless. | | `threadId` | **сессия/конверсация**. | | `runId` | **один ход** в рамках сессии. | | `Message { id, role: developer\|system\|assistant\|user\|tool, content, toolCalls?, toolCallId? }` | сообщение. У assistant — `toolCalls`; у tool — `toolCallId`. | | `Tool { name, description, parameters: JSONSchema }` | тул (JSON Schema в `parameters`). | | `State = Map` | нестрогий state хода (snapshot/delta). | | `Context { description, value }` | кусок контекста. | События (`BaseEvent`, дискриминатор `type`): - lifecycle: `RUN_STARTED{threadId,runId}` → … → `RUN_FINISHED` / `RUN_ERROR{message,code}` - steps: `STEP_STARTED/FINISHED{stepName}` - text: `TEXT_MESSAGE_START{messageId,role}` → `TEXT_MESSAGE_CONTENT{messageId,delta}` → `TEXT_MESSAGE_END{messageId}` - tool: `TOOL_CALL_START{toolCallId,toolCallName,parentMessageId?}` → `TOOL_CALL_ARGS{toolCallId,delta}` → `TOOL_CALL_END{toolCallId}` → `TOOL_CALL_RESULT{toolCallId,content}` - state: `STATE_SNAPSHOT{snapshot}` / `STATE_DELTA{delta}` / `MESSAGES_SNAPSHOT{messages}` - прочее: `RAW{event}` / `CUSTOM{name,value}` Свободные варианты на стороне либы: - stateless: `Agent { input -> flow }` (использует `input.messages/state/tools`). - stateful: `AbstractAgent(agentId, threadId)` — держит `state` + `messages`, переопределяешь `runAgent(RunAgentParameters)`. Сервер (`aguiAgent(agent, path="/agui")`) не хранит ничего между запросами: декодирует `RunAgentInput`, вызывает `agent.run`, стримит события, оборачивает сбой в `RUN_ERROR`. ## 3. Как вписываемся (принципы) 1. **`threadId` = наша сессия.** Держим `SessionStore[threadId]` на сервере даже при stateless-сервере AGUI: на каждом ходе реконсилируем `input.messages` с сохранёнными и дописываем новые. Это даёт «настоящие сессии» с историей. 2. **`runId` = ход.** Один `RunAgentInput` → один цикл ядра → один поток событий. 3. **Ядро говорит на AGUI-событиях** — это и есть наш протокол вывода; фасады не переопределяют его. 4. **LLM — за интерфейсом.** Ядро не знает, какая LLM. Точка входа `LlmClient.complete(...)`. 5. **Тулы: внутренний реестр + MCP.** `ToolRegistry` объединяет «родные» тулы и тулы MCP-серверов (через bridge). LLM вызывает их; ядро исполняет и возвращает `TOOL_CALL_RESULT`. 6. **А2A = request/response.** Переиспользуем то же ядро, но собираем финальный текст и отдаём одним A2A `Message`. ## 4. Пакеты / модули ``` pw.binom.agentik ├─ core │ ├─ session Session(threadId, messages, state) + SessionStore (in-memory, by threadId) │ ├─ tool ToolSpec, Tool, ToolRegistry (InMemoryToolRegistry, builtin tools) │ ├─ llm LlmClient (interface) + LlmMessage/LlmToolCall/LlmResponse + EchoLlmClient (stub) │ ├─ mcp McpClient (interface: listTools/callTool) + McpTool (bridge) + ToolRegistry.registerMcpClient │ └─ engine AgentEngine: prompt -> LLM -> tool-loop -> AGUI events (RunStarted..RunFinished) └─ fronts ├─ agui AgentikAgent : Agent { input -> engine.run(input) } [вместо EchoAgent] └─ a2a EngineA2aHandler : A2A handle(msg, contextId) -> Message [собирает финальный текст] ``` Wiring (в `standalone`): `Main` собирает `AgentEngine(llm, tools+MCP, sessions)`, отдаёт его `AgentikAgent` (AGUI) и `EngineA2aHandler` (A2A). Конфигурация (LLM, MCP-серверы, тулы) — в одном месте (см. §8). ## 5. Поток данных (AGUI) ``` client ──POST /agui {RunAgentInput}──> AgentikAgent │ engine.run(input) ▼ SessionStore.getOrCreate(threadId) merge input.messages, input.tools │ ┌─> llm.complete(system, history, tools, state) │ └─ toolCalls? -> TOOL_CALL_* + ToolRegistry.call + TOOL_CALL_RESULT -> llm.complete (loop) └─> text? -> TEXT_MESSAGE_* (последнее) │ persist final assistant msg in session │ client <──SSE [BaseEvent]──────┘ (RunStarted .. RunFinished / RunError) ``` ## 6. A2A-фасад (request/response) `A2A handle(request: Message, contextId): Message`: 1. `text = request.parts.filterIsInstance().join("")`. 2. `RunAgentInput(threadId = contextId ?: new, runId = randomRunId(), messages = [Message(USER, text)])`. 3. `events = engine.run(input)` (синхронно, `runBlocking { ...toList() }`). 4. `answer = events.filterIsInstance().join { it.delta }`. 5. вернуть `Message(role=AGENT, parts=[TextPart(answer)])`. `contextId` ↔ `threadId` — общая сессия. A2A не стримит, поэтому события ядра сворачиваются в финальный ответ. ## 7. Зоны ответственности **Ядро (скелет уже закладываем):** `Session/SessionStore`, `Tool/ToolRegistry`, `LlmClient`(iface)+stub, `McpClient`(iface)+bridge, `AgentEngine`, `AgentikAgent`, `EngineA2aHandler`, `Main`. **Ты (поверх ядра):** - реализация `LlmClient` (через `pw.binom.openai:ktor-impl`), - конкретные «родные» тулы, - транспорт `McpClient` (подключение к MCP-серверам). ## 8. Открытые решения (нужны решения) 1. **Персист сессий** — in-memory или на диск/БД (сейчас in-memory). 2. **Конфигурация** — MCP-серверы, LLM endpoint/ключ, системный промпт: env / yaml / файл (сейчас env-заготовки: `AGENTIK_A2A_AGENTS`). 3. **Ограничения** — `maxToolRounds`, таймауты LLM/тулов, лимиты размера. 4. **Аутентификация** — Bearer на обоих фасадах (а2a/agui принимают `token`), пока не включена. 5. **Структура модулей** — держать всё в `standalone` (по пакетам) или вынести `core`/`mcp` в отдельные подмодули (для переиспользования). 6. **Поведение сессии** — «клиент источник правды» (replace) vs «сервер источник правды» (append) при реконсилии `messages`. ```