Files

164 lines
7.4 KiB
Markdown

# `:proto` — протокол общения с агентом (KMP, jvm + native)
## Что это
Типы и контракт in-house протокола `agentik`, заменившего AG-UI:
- **stateful** — сервер сам владеет диалогом; клиент шлёт только новые
сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан
повторять `messages[]` каждый раз).
- **declarative история vs. события** — `Message` это то, что уже легло
в БД, `Event` это live-стрим от агента во время `send()` или `events()`.
- **чистые интерфейсы** — никаких сетевых и storage зависимостей внутри
`:proto`; это контракт.
Решает проблему: AG-UI клиент вынужден каждый раз знать и пересобирать
полную историю, а его серверная часть (`AbstractAgent`) — постоянно
сериализовать-десериализовать всю переписку. В `:proto` сервер один,
контракт тонкий, переписка персистится нативно (SQLite, файлы, что
хотите). Можно подменить front-end или back-end, протокол остаётся.
## Где используется
- `:server` — Ktor-фасад, маппит `Agent` ↔ HTTP/SSE.
- `:client` — Ktor-клиент, маппит HTTP/SSE ↔ `Agent/Conversation`.
- `:agentik-cli` — работает поверх `:client`, а следовательно поверх `:proto`.
*(`:agentik-tui` был исключён из сборки 2026-09-17.)*
- `:standalone` — реализует `Agent` (через `ChatAgent`) и пишет/читает
`Message`/`Event` напрямую через storage.
## Как подключить
```kotlin
// build.gradle.kts
kotlin {
sourceSets.commonMain.dependencies {
api("pw.binom.agentik:proto:0.1.0")
}
}
```
Артефакт `pw.binom.agentik:proto:0.1.0` живёт в Nexus-репозитории
`caffeine` (HTTP `http://<your-nexus>/repository/caffeine/`, plain-HTTP,
credentials — через переменные `binom.repo.user/password/url`).
## Версии
Каталог `gradle/libs.versions.toml`, секция `[versions]` → `agentik-proto`.
Поднять версию → переопубликовать все KMP-таргеты через `./gradlew
:proto:publish -Pversion=...` (или триггернуть Gitea release).
Текущие KMP-таргеты: `jvm + macosX64/macosArm64 +
iosX64/iosArm64/iosSimulatorArm64 + linuxX64/linuxArm64 + mingwX64`.
## Публикация
Настройки в `gradle.properties` / env: `binom.repo.url`, `binom.repo.user`,
`binom.repo.password`. `./gradlew :proto:publish` публикует все
target-specific артефакты + общий `kotlinMultiplatform`.
## Основные типы
`:proto` — **тонкий** контракт: сами интерфейсы `Agent`/`Conversation`/`Message`,
а общие типы содержимого и события живут в нижележащих модулях:
`Content`/`MessageContext`/`MessageOrigin`/`TurnTokens` — в `:content-api`,
`Event`/`OnlineEvent`/`OutboxStore`/`OnlineOutbox` — в `:outbox-api`.
```kotlin
interface Agent : AutoCloseable {
val id: String
val info: AgentInfo
val journal: JournalStore // append-only audit (read-only)
val outbox: OutboxStore // durable-события: catchup+live по курсору
val onlineOutbox: OnlineOutbox // live-only: стриминг, без курсора
val conversationStore: ConversationStore
fun createConversation(temp: Boolean): Conversation
suspend fun getConversation(id: String): Conversation?
suspend fun deleteConversation(id: String): Boolean
suspend fun renameConversation(id: String, title: String?): Instant?
}
interface Conversation : AutoCloseable {
val id: String
val updatedAt: Instant
val isTemporal: Boolean
val title: String?
val isSupportImageInput: Boolean
val isSupportImageOutput: Boolean
suspend fun send(content: List<Content>, context: MessageContext? = null) // fire-and-forget
suspend fun getMessages(after: Instant, offset: Int, limit: Int): List<Message>
suspend fun rename(title: String)
suspend fun interrupt()
}
// :content-api
sealed interface Content {
data class Text(val body: String) : Content
data class Image(val data: ByteArray, val mime: String) : Content
}
enum class MessageOrigin { USER, SYSTEM, EVENT }
data class MessageContext(origin: MessageOrigin, description: String?, sourceId: String?, metadata: JsonElement?)
// :proto
sealed interface Message {
val id: String
val date: Instant
class UserMessage(id, content: List<Content>, date, context: MessageContext?) : Message
class AssistantMessage(id, content: List<Content>, date, tokens: TurnTokens?, reasoning: String?) : Message
class ToolCall(...) : Message
class ToolResult(...) : Message
class Error(...) : Message
}
// :outbox-api — durable (перезапрашиваются по курсору `after`)
sealed interface Event {
val date: Instant
class UserMessage(date, id, content: List<Content>, context: MessageContext?) : Event
class AssistantMessage(date, id, content: List<Content>, reasoning: String?, tokens: TurnTokens?) : Event
class ToolCall(...) : Event
class ToolResult(...) : Event
class ToolFailed(...) : Event
class Interrupted(date) : Event
class Error(date, message, code) : Event
class ConversationClosing(...) : Event
class CompactionTriggered(...) : Event
}
// :outbox-api — online (live-only, НИКОГДА не сохраняются)
sealed interface OnlineEvent {
val date: Instant
enum ResponseType { TEXT, IMAGE }
class Working(date) : OnlineEvent
class End(date) : OnlineEvent
class StartReasoning(date) : OnlineEvent
class StartResponse(date, responseType: ResponseType) : OnlineEvent
class AppendText(date, body: String) : OnlineEvent
class AppendImage(date, body: ByteArray, mime: String) : OnlineEvent
}
```
Ход в терминах маркеров: онлайн `Working` → … → онлайн `End`; durable —
`UserMessage` в начале и `AssistantMessage`/`Interrupted`/`Error` в конце.
## Чего здесь НЕТ
- Никакого HTTP/SSE/JSON. Это контракт. Сериализация живёт в `:server`
и `:client`.
- Никакого хранения. Реализации `JournalStore` живут в `:storage-*`.
- Никакой логики прерывания / инструментов / LLM-вызовов. Это всё
внутри `:standalone` (ChatAgent) и выше.
## Тесты
```
./gradlew :proto:jvmTest
./gradlew :proto:allTests # дополнительно linuxX64 (если Linux) / iosSimulator (если macOS)
```
## Текущий статус
Используется продакшеном. Иммутабельный API (после рефакторинга из
AG-UI). Возможные будущие расширения: typed tool-result, multi-modal
contents, server-pushed references — все обсуждаются через общий
[IRC-QUESTIONS.md](../IRC-QUESTIONS.md).