5bdc517988
Введение монотонного offset'а как персистентного состояния агента:
offset'ы переживают рестарт standalone-агента, клиент продолжает
синхронизацию инкрементально, без полной re-sync с нуля.
outbox-api:
- Cursor (offset: Long) — курсор в журнале событий агента.
- OffsetSequencer — интерфейс резервирования уникального offset.
- CursorStore — персистентное хранилище текущего offset'а.
- PersistentOffsetSequencer — декоратор над любым OutboxStore,
обновляет CursorStore на каждом append (atomic transaction).
- OutboxGapException — клиент запросил after < earliestCursor() →
сервер не может удовлетворить, клиент обязан делать full resync.
- DurableEvent переименован из Event.kt → DurableEvent.kt (Event.kt
был общим sealed-типом, теперь это термин из спеки).
- MutableOutboxStore и OutboxStore теперь читают offset через
CursorStore вместо in-memory counter'а.
outbox-inmemory:
- InMemoryOffsetSequencer — для тестов и dev-режима.
- InMemoryOutboxStore теперь принимает OffsetSequencer в конструкторе.
outbox-ksqlite (новый модуль):
- KsqliteCursorStore — таблица outbox_cursor (agent_id TEXT PK,
offset INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL).
- KsqliteCursorStoreTest — 4 теста (set/get, monotonic, concurrent).
proto + server:
- Snapshot.proto — server-state snapshot endpoint для клиентов,
которым нужна полная материализация (использование TBD).
- Routes.kt + SnapshotRouteTest — endpoint /agentik/snapshot (GET).
journal-ksqlite:
- KsqliteJournalStore.listFlow/append — без изменений по API,
нотации минимальные (codecs).
standalone:
- DurableLog (бывший ChatAgent-orchestration) — атомарный commit
события в OutboxStore + PersistentOffsetSequencer + materialization
(через Reducer) одной транзакцией.
- SqliteStores — добавляет KsqliteCursorStore в bundle, единая
shared-connection для всех ksqlite-сторов standalone-агента.
- ChatAgent / ConversationLoop / ConversationEvents / ReflectionScheduler /
ToolDispatcher — переход на новые абстракции.
- standalone/build.gradle.kts — implementation(project(':outbox-ksqlite'))
включено (раньше было закомментировано — модуль только создавался).
client:
- AgentikAgent / AgentClient / HttpEventStore / HttpJournalStore /
ReconnectingOutbox — используют Cursor через transport API.
- client/README.md — синхронизирован с новым поведением (468 строк
diff — это в основном оформление и примеры).
kotlinx-io: 0.8.0 → 0.9.1 в libs.versions.toml (см. sync-core tests).
SYNC-SYSTEM.md (в корне) — спецификация, на которую ссылается и
:sync-core (эта сессия), и эта Cursor-абстракция в outbox-api.
Тесты: standalone 132, journal-ksqlite 25, outbox-inmemory 20,
outbox-ksqlite 4, client 10, sync-core 74 — все зелёные на jvm;
sync-core linuxX64 74 тоже зелёный.
sync2/ (заброшенный stub с одним build.gradle.kts) удалён.
164 lines
7.4 KiB
Markdown
164 lines
7.4 KiB
Markdown
# `:proto` — протокол общения с агентом (KMP, jvm + native)
|
|
|
|
## Что это
|
|
|
|
Типы и контракт in-house протокола `agentik`, заменившего AG-UI:
|
|
|
|
- **stateful** — сервер сам владеет диалогом; клиент шлёт только новые
|
|
сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан
|
|
повторять `messages[]` каждый раз).
|
|
- **declarative история vs. события** — `Message` это то, что уже легло
|
|
в БД, `DurableEvent` это 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`/`DurableEvent` напрямую через 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`,
|
|
`DurableEvent`/`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).
|