Изменение выносит типы контента в :content-api и разделяет потоки событий.

This commit is contained in:
2026-09-30 12:41:10 +03:00
parent bd7e079780
commit 059c23e3ab
60 changed files with 613 additions and 400 deletions
+19 -12
View File
@@ -47,28 +47,35 @@ agentik
## 3. `:proto` — интерфейсы
`Agent` (см. `proto/src/commonMain/.../Agent.kt`):
- `id: String`
- `id: String`, `info: AgentInfo`
- read-only сторы: `journal: JournalStore`, `outbox: OutboxStore`,
`onlineOutbox: OnlineOutbox`, `conversationStore: ConversationStore`
- `createConversation(temp: Boolean): Conversation`
- `suspend getConversation(id): Conversation?`
- `suspend getConversations(offset, limit): List<Conversation>`
- `getConversations(offset = 0): Flow<Conversation>` — cold-flow paging через suspend-версию, `PAGE_SIZE = 100`.
- `events(after: Instant): Flow<AgentEvent>` — replay-free, бэкфилл через snapshot.
- `deleteConversation(id): Boolean`
- `suspend deleteConversation(id): Boolean`
- `suspend renameConversation(id, title): Instant?`
`Conversation`:
- `isSupportImageInput / Output / isTemporal: Boolean`
- `isSupportImageInput / Output / isTemporal: Boolean`, `title: String?`
- `updatedAt: Instant`
- `send(content: List<Content>)` — write-only, ничего не возвращает.
- `send(content: List<Content>, context: MessageContext? = null)` — write-only, ничего не возвращает.
- `interrupt()` — отмена активного хода.
- `events(after): Flow<Event>` — live, replay-free.
- `getMessages(after, offset, limit)` + `getMessages(after): Flow<Message>` — paging.
- `getMessages(after, offset, limit): List<Message>` — paging.
- `rename(title)` — мутация, бампит `updatedAt`.
- `AutoCloseable` — `close()` идемпотентен.
`Content = Text(body) | Image(data, mime)`.
`Content = Text(body) | Image(data, mime)` — из `:content-api` (там же `MessageContext`/`MessageOrigin`/`TurnTokens`).
`Message = UserMessage | AssistantMessage | ToolCall | ToolResult | Error`.
`Event = StartReasoning | StartResponse | End | AppendText | AppendImage | ToolCall | ToolResult | Error`.
`AgentEvent = Created(conversationId) | Deleted(id) | Renamed(id, title)`.
События разделены на два потока (оба в `:outbox-api`):
- **durable** `Event` (`outbox.conversationEvents(after, id)`): `UserMessage |
AssistantMessage | ToolCall | ToolResult | ToolFailed | Interrupted | Error |
ConversationClosing | CompactionTriggered` — перезапрашиваются по курсору;
- **online** `OnlineEvent` (`onlineOutbox.onlineEvents(id)`): `Working | End |
StartReasoning | StartResponse | AppendText | AppendImage` — live-only,
никогда не сохраняются.
`AgentEvent = Created(conversationId) | Deleted(id) | Renamed(id, title) | Touched(...)`.
Принцип: **агент — источник истины** для транскрипта и сессий. Клиент
лишь рендерит Event-stream и кэширует историю.
+6 -5
View File
@@ -63,9 +63,9 @@
│ 3. ensureLiteConversation: │
│ first turn → create from WM; │
│ next turns → reuse (KV-cache) │
│ 4. sendStreamContents → emit │
│ StartResponse / AppendText / │
│ End │
│ 4. sendStreamContents → online: │
│ StartResponse/AppendText/End; │
│ durable: AssistantMessage │
│ 5. audit + WM: append AssistantMessage│
└──────────────────────────────────────┘
│ │
@@ -151,7 +151,8 @@ fun main() {
| `updatedAt: Instant` | последний `send`/`rename` |
| `send(content: List<Content>)` | **fire-and-forget**: добавить user-сообщение, запустить ход, выйти |
| `interrupt()` | остановить текущий ход (best-effort) |
| `events(after): Flow<Event>` | live-события хода (StartReasoning, StartResponse, AppendText, End, Interrupted, Error) |
| `outbox.conversationEvents(after, id): Flow<Event>` | durable-события (UserMessage, AssistantMessage, ToolCall/Result, Interrupted, Error) — скурсором |
| `onlineOutbox.onlineEvents(id): Flow<OnlineEvent>` | live-only стриминг (Working, End, StartReasoning, StartResponse, AppendText, AppendImage) |
| `getMessages(after, offset, limit)` | страница истории |
| `rename(title)` | переименовать |
| `close()` | освободить ресурсы |
@@ -173,7 +174,7 @@ fun main() {
Подробный контракт — в комментариях к `MessageRecord.kt` и `WorkingMemoryEntry.kt`.
**Ошибки хода персистятся.** Если ход провалился (LLM/движок недоступны — например, HTTP 400 от endpoint'а), `ChatConversation.failTurn` пишет терминальную запись `MessageRecord.Error` в audit и эмитит `Event.Error` + `Event.End`. Благодаря audit-записи ошибка видна не только подписчику live-SSE, но и клиенту, который делает backfill через `getMessages` (polling/переподключение): в истории будет `Message.Error(id, message, code?)`, а для этого user-сообщения не будет `AssistantMessage`. При ошибке стрима живой `LiteConversation` сбрасывается — следующий `send` пересоберёт его из `working_memory`. В working_memory `Error` не пишется (модель не должна видеть ошибки прошлых ходов).
**Ошибки хода персистятся.** Если ход провалился (LLM/движок недоступны — например, HTTP 400 от endpoint'а), `ChatConversation.failTurn` пишет терминальную запись `MessageRecord.Error` в audit и эмитит durable `Event.Error` + онлайн `OnlineEvent.End`. Благодаря audit-записи ошибка видна не только подписчику live-SSE, но и клиенту, который делает backfill через `getMessages` (polling/переподключение): в истории будет `Message.Error(id, message, code?)`, а для этого user-сообщения не будет `AssistantMessage`. При ошибке стрима живой `LiteConversation` сбрасывается — следующий `send` пересоберёт его из `working_memory`. В working_memory `Error` не пишется (модель не должна видеть ошибки прошлых ходов).
### `JournalStore`