Добавляет Cursor/OffsetSequencer в :outbox-api и интегрирует PersistentOffsetSequencer через :outbox-ksqlite.
Введение монотонного 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) удалён.
This commit is contained in:
+3
-3
@@ -8,7 +8,7 @@
|
||||
сообщения, а не всю историю (в отличие от AG-UI, где клиент обязан
|
||||
повторять `messages[]` каждый раз).
|
||||
- **declarative история vs. события** — `Message` это то, что уже легло
|
||||
в БД, `Event` это live-стрим от агента во время `send()` или `events()`.
|
||||
в БД, `DurableEvent` это live-стрим от агента во время `send()` или `events()`.
|
||||
- **чистые интерфейсы** — никаких сетевых и storage зависимостей внутри
|
||||
`:proto`; это контракт.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- `:agentik-cli` — работает поверх `:client`, а следовательно поверх `:proto`.
|
||||
*(`:agentik-tui` был исключён из сборки 2026-09-17.)*
|
||||
- `:standalone` — реализует `Agent` (через `ChatAgent`) и пишет/читает
|
||||
`Message`/`Event` напрямую через storage.
|
||||
`Message`/`DurableEvent` напрямую через storage.
|
||||
|
||||
## Как подключить
|
||||
|
||||
@@ -62,7 +62,7 @@ target-specific артефакты + общий `kotlinMultiplatform`.
|
||||
`:proto` — **тонкий** контракт: сами интерфейсы `Agent`/`Conversation`/`Message`,
|
||||
а общие типы содержимого и события живут в нижележащих модулях:
|
||||
`Content`/`MessageContext`/`MessageOrigin`/`TurnTokens` — в `:content-api`,
|
||||
`Event`/`OnlineEvent`/`OutboxStore`/`OnlineOutbox` — в `:outbox-api`.
|
||||
`DurableEvent`/`OnlineEvent`/`OutboxStore`/`OnlineOutbox` — в `:outbox-api`.
|
||||
|
||||
```kotlin
|
||||
interface Agent : AutoCloseable {
|
||||
|
||||
@@ -135,6 +135,27 @@ interface Agent : AutoCloseable {
|
||||
*/
|
||||
suspend fun renameConversation(id: String, title: String?): Instant?
|
||||
|
||||
/**
|
||||
* Снапшот **всего состояния агента** (список диалогов) + курсор, на котором
|
||||
* он валиден. Точка входа resync'а: клиент после [OutboxGapException] чистит
|
||||
* локальный кэш, берёт этот снапшот, применяет его, затем подписывается
|
||||
* `outbox.agentEvents(after = snapshot.cursor)` и накатывает дельты.
|
||||
*
|
||||
* Курсор читается **до** состояния (cursor-first) — это не гонка: любое
|
||||
* изменение, случившееся во время чтения, имеет offset `> cursor` и придёт
|
||||
* потоком. См. README `:client` (протокол синхронизации).
|
||||
*/
|
||||
suspend fun conversationsSnapshot(): ConversationsSnapshot
|
||||
|
||||
/**
|
||||
* Снапшот сообщений диалога [conversationId] + курсор, на котором он валиден.
|
||||
* Сообщения отсечены `seq <= cursor.offset` (см. [ChatSnapshot]).
|
||||
*
|
||||
* Парный к [conversationsSnapshot] для per-chat resync'а: подписка —
|
||||
* `outbox.conversationEvents(after = snapshot.cursor, conversationId)`.
|
||||
*/
|
||||
suspend fun chatSnapshot(conversationId: String): ChatSnapshot
|
||||
|
||||
companion object {
|
||||
|
||||
const val PAGE_SIZE: Int = 100
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlin.time.Instant
|
||||
*
|
||||
* **Live-события** диалога НЕ часть этого интерфейса. Их два независимых
|
||||
* потока:
|
||||
* - **durable** ([pw.binom.agentik.outbox.Event]: UserMessage / AssistantMessage /
|
||||
* - **durable** ([pw.binom.agentik.outbox.DurableEvent]: UserMessage / AssistantMessage /
|
||||
* Interrupted / Error / ToolCall / ToolResult / ToolFailed) — из
|
||||
* [pw.binom.agentik.outbox.OutboxStore], перезапрашивается по курсору:
|
||||
* ```
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package pw.binom.agentik.proto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import pw.binom.agentik.journal.ConversationRecord
|
||||
import pw.binom.agentik.journal.MessageRecord
|
||||
import pw.binom.agentik.outbox.Cursor
|
||||
|
||||
/**
|
||||
* Снапшот списка диалогов + его [cursor].
|
||||
*
|
||||
* Курсор — **commit point** на момент чтения (см. `OutboxStore.currentCursor`):
|
||||
* состояние со `seq <= cursor.offset` отражено в [conversations], всё, что
|
||||
* появится позже, придёт потоком outbox-событий. Клиент после применения
|
||||
* снапшота подписывается `agentEvents(after = cursor)` (или
|
||||
* `conversationEvents(after = cursor)`) и накатывает дельты поверх.
|
||||
*
|
||||
* Таблица `conversation` мала (реестр диалогов), поэтому отсечки по `seq`
|
||||
* внутри снапшота нет — читается целиком; протокол от этого не страдает
|
||||
* (absolute-события + идемпотентный apply).
|
||||
*/
|
||||
@Serializable
|
||||
data class ConversationsSnapshot(
|
||||
val conversations: List<ConversationRecord>,
|
||||
val cursor: Cursor,
|
||||
)
|
||||
|
||||
/**
|
||||
* Снапшот сообщений одного диалога + его [cursor].
|
||||
*
|
||||
* Сообщения прочитаны с отсечкой `seq <= cursor.offset` — конечное и
|
||||
* стабильное множество, отражающее состояние диалога на момент [cursor].
|
||||
* Всё, что появится позже (`seq > cursor.offset`), придёт потоком
|
||||
* `conversationEvents(after = cursor, conversationId)`.
|
||||
*/
|
||||
@Serializable
|
||||
data class ChatSnapshot(
|
||||
val conversationId: String,
|
||||
val messages: List<MessageRecord>,
|
||||
val cursor: Cursor,
|
||||
)
|
||||
Reference in New Issue
Block a user