standalone: persist turn errors so polling clients see them

Event.Error уже эмитился в live-SSE, но если клиент подключился
после провала хода (или опрашивает историю через getMessages
вместо SSE), он видел только user-сообщение без следа, что ход
провалился. Это и нужно было поправить.

* :proto
  - Message.Error(id, message, code?, date) — терминальная
    персистентная проекция Event.Error. Audit-only; в live-стриме
    по-прежнему приходит Event.Error.

* :standalone
  - MessageRecord.Error с тем же контрактом.
  - SqliteMessageStore: kind "error", JSON-payload {message, code};
    encoding-ошибки round-trip покрыты тестом (с code и без).
  - ChatConversation.failTurn(message, code?): пишет MessageRecord.Error
    в audit (кроме temp-диалогов), затем эмитит Event.Error + Event.End.
    Все три exit-точки из runTurn (пустой parts, init-catch,
    stream-catch) теперь через failTurn.
  - ChatConversation: при ошибке стрима живой LiteConversation
    сбрасывается — следующий send пересоберёт его из working_memory.
    Раньше оставляли битую инстанцию вопреки KDoc класса.
  - toProto: маппит MessageRecord.Error → Message.Error.

* docs
  - STANDALONE.md: добавлен Message.Error в таблицу dual-log + абзац
    про персист ошибок (audit/working_memory, сброс liteConv).
  - ARCHITECTURE.md: Message.Error в сигнатуре Message, failTurn
    в ChatConversation, Message.Error в audit log.
  - Поправлен пример describe() в доке тулов (реальный формат —
    OpenAI-style function wrapper, а не голый JSON Schema).

Тесты:
  - ChatAgentTest: LLM failure → Event.Error + Event.End + Error
    record в audit + backfill через getMessages.
  - PersistenceTest: MessageRecord.Error round-trip (с code и без).
  - :standalone jvmTest 70 (было 69).

E2E: неверный model id (HTTP 400) → polling GET /messages теперь
возвращает user_message + error (без assistant_message).
This commit is contained in:
2026-09-14 00:26:13 +03:00
parent adcb8f54d6
commit 0faad45f3d
8 changed files with 156 additions and 12 deletions
+5 -2
View File
@@ -66,7 +66,7 @@ agentik
- `AutoCloseable` — `close()` идемпотентен.
`Content = Text(body) | Image(data, mime)`.
`Message = UserMessage | AssistantMessage | ToolCall | ToolResult | Summary | System`.
`Message = UserMessage | AssistantMessage | ToolCall | ToolResult | Error`.
`Event = StartReasoning | StartResponse | End | AppendText | AppendImage | ToolCall | ToolResult | Error`.
`AgentEvent = Created(conversationId) | Deleted(id) | Renamed(id, title)`.
@@ -104,6 +104,9 @@ Main.kt
- Tool-loop: на `delta.toolCalls` → emit `Event.ToolCall` → execute
(`tool.invoke(argsJson)`) → emit `Event.ToolResult` →
`liteConv.addToolResult(callId, name, result)` → продолжение стрима.
- На ошибку хода (init/стрим LLM): `failTurn` → `messageStore.append(Error)`
(audit; в working_memory не пишется) + `Event.Error` + `Event.End`, живой
`LiteConversation` сбрасывается и пересобирается на следующем `send`.
- `LiteConversation` живёт **один на весь `ChatConversation`** для обоих
бэкендов: KV-cache движка сохраняется между ходами. Это контракт
litert-api, не только Google-specific.
@@ -116,7 +119,7 @@ Main.kt
summarization-вставка отложена (нужен дизайн-проработка).
**`MessageStore`** — append-only аудит. На каждый ход дописываются
`UserMessage`, `AssistantMessage`, `ToolCall`, `ToolResult`. Никаких
`UserMessage`, `AssistantMessage`, `ToolCall`, `ToolResult`, `Error`. Никаких
update/delete кроме каскада из `ConversationStore.delete`.
## 5. Слои персистентности