Files
agentik/docs/ARCHITECTURE.md
T
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

114 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
```