a3581abf84
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.
114 lines
8.3 KiB
Markdown
114 lines
8.3 KiB
Markdown
# 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<BaseEvent> }`.
|
||
|
||
Ключевые типы (из `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<String, JsonElement>` | нестрогий 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<TextPart>().join("")`.
|
||
2. `RunAgentInput(threadId = contextId ?: new, runId = randomRunId(), messages = [Message(USER, text)])`.
|
||
3. `events = engine.run(input)` (синхронно, `runBlocking { ...toList() }`).
|
||
4. `answer = events.filterIsInstance<TextMessageContentEvent>().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`.
|
||
```
|