refactor(protocol): add toolName to ToolResult, remove proto typealiases, rename id→toolCallId, drop Conversation.events()

Three protocol-level changes from Android-client review (items 1-3, 5-6):

1) toolName denormalization in ToolResult (3 layers):
   - :outbox-api/Event.ToolResult: +toolName: String? = null
   - :journal-api/MessageRecord.ToolResult: +toolName: String? = null
   - :proto/Message.ToolResult: +toolName: String? = null
   - :storage-ksqlite, :journal-ksqlite ResultPayload codec: +toolName
   - :standalone/ToolDispatcher, ConversationLoop: thread toolName = call.name
   Nullable + default = backward-compat for already-persisted histories
   and existing clients.

2) Drop proto/Event.kt, AgentEvent.kt, CommonEvent.kt typealiases.
   is proto.Event.End failed with 'Unresolved reference End' (alias
   loses nested-class access). Use pw.binom.agentik.outbox.{Event,
   AgentEvent, CommonEvent} directly everywhere — :proto already has
   api(:outbox-api), the package is visible to consumers, no shim
   needed. 21 files rewired, 3 files deleted.

3) Rename Event.ToolResult.id → toolCallId (option B per user).
   In :outbox-api Event.ToolResult.id == Event.ToolCall.id (one value,
   one name); the persistent journal keeps MessageRecord.ToolResult.id
   as its own PK + toolCallId as FK to the call — different semantics,
   left untouched. Fixed ToolDispatcher bug: emitted id = resultId
   while KDoc claimed id == ToolCall.id; now emits toolCallId = callId.

4) Remove Conversation.events() from :proto; OutboxStore is sole event source.
   Conversation is a pure per-conversation abstraction (send/getMessages/
   rename/close). Live events only via agent.outbox.conversationEvents/
   agentEvents/events. HTTP route /conversations/{id}/events stays for
   wire-compat but routes through outbox internally (map { it.event }).

jvmTest green (95 tasks).
This commit is contained in:
2026-09-22 02:55:02 +03:00
parent 7865eed836
commit c9995b263e
24 changed files with 129 additions and 118 deletions
@@ -14,9 +14,9 @@ import kotlin.time.Instant
* идентична `OutboxStore.events`: поток **не реплеит** прошлое, для бэкфилла
* используются `Agent.getConversations` / `getConversation`.
*
* **История**: раньше жил в `:proto` (как `pw.binom.agentik.proto.AgentEvent`).
* После миграции в `:outbox-api` — `:proto.AgentEvent` стал typealias'ом,
* backward-compat для существующих импортов сохранён.
* **История**: до 2026-09-21 жил в `:proto` как `pw.binom.agentik.proto.AgentEvent`;
* typealias удалён 2026-09-21 (стирал nested-типы в `is`/`when`) — потребители
* импортируют напрямую из `pw.binom.agentik.outbox.AgentEvent`.
*/
@Serializable
sealed interface AgentEvent {
@@ -9,17 +9,17 @@ import kotlinx.serialization.Serializable
*
* Useful for admin dashboards, debug tools, parent agents: one subscription
* instead of N+1. For regular UI use two separate SSE feeds
* ([AgentEvent] via `/events` и `Event` via `/conversations/{id}/events`);
* ([AgentEvent] via `/events` и [Event] via `/conversations/{id}/events`);
* [CommonEvent] — for those who need everything in one place.
*
* Server endpoint: `GET /events/all` (SSE), or replay via `OutboxStore.events(after)`.
*
* **История**: раньше жил в `:proto` (как `pw.binom.agentik.proto.CommonEvent`).
* После миграции в `:outbox-api` — `:proto.CommonEvent` стал typealias'ом,
* backward-compat для существующих импортов сохранён. `CommonEvent.Conversation`
* ссылается на [Event] (тоже в `:outbox-api` теперь) — раньше был
* `pw.binom.agentik.proto.Event`, теперь это `pw.binom.agentik.outbox.Event`
* (он тоже typealias-нут в `:proto.Event`).
* **История**: до 2026-09-21 жил в `:proto` как `pw.binom.agentik.proto.CommonEvent`;
* при миграции в `:outbox-api` был оставлен typealias в `:proto` для backward-compat,
* но он стирал nested-типы (`CommonEvent.Agent`, `CommonEvent.Conversation`),
* что ломало `is CommonEvent.Agent` на стороне клиента. Typealias'ы
* `Event`/`AgentEvent`/`CommonEvent` из `:proto` удалены — потребители
* импортируют напрямую из `pw.binom.agentik.outbox.*`.
*/
@Serializable
sealed interface CommonEvent {
@@ -15,9 +15,12 @@ import kotlin.time.Instant
* `StartReasoning?` → `StartResponse(TEXT|IMAGE)` → ...контент... → `End` | `Interrupted` | `Error`.
* `StartReasoning` может отсутствовать, если агент не показывал рассуждения.
*
* **История**: раньше жил в `:proto` (как `pw.binom.agentik.proto.Event`).
* После миграции в `:outbox-api` — `:proto.Event` стал typealias'ом,
* backward-compat для существующих импортов сохранён.
* **История**: до 2026-09-21 жил в `:proto` как `pw.binom.agentik.proto.Event`;
* при миграции в `:outbox-api` был оставлен typealias в `:proto` для
* backward-compat, но он стирал nested-типы (`Event.End`, `Event.ToolCall`,
* `Event.ToolResult`), что ломало `is Event.End` на стороне клиента.
* Typealias удалён 2026-09-21 — потребители импортируют напрямую из
* `pw.binom.agentik.outbox.Event`.
*/
@Serializable
sealed interface Event {
@@ -75,12 +78,26 @@ sealed interface Event {
/**
* Результат вызова тула. Приходит целиком после завершения исполнения.
* [id] совпадает с [ToolCall.id], к которому относится результат, и
* с id `Message.ToolResult` в истории.
*
* [toolCallId] = id [ToolCall], к которому относится результат, и
* `MessageRecord.ToolResult.toolCallId` в истории. Один Call → один Result,
* пара `(date, toolCallId)` уникальна — отдельный `id` в live-событии
* не нужен (PK живёт в персистентном журнале).
*
* [toolName] денормализован из соответствующего [ToolCall.toolName] —
* UI рендерит имя тула без локальной `Map<toolCallId, name>` и без риска
* «Result пришёл до Call». `null` допустим для backfill'а старых
* записей, у которых поле отсутствует, или теоретического случая
* Result без предшествующего Call (orphan).
*/
@Serializable
@SerialName("tool_result")
data class ToolResult(override val date: Instant, val id: String, val result: String?) : Event
data class ToolResult(
override val date: Instant,
val toolCallId: String,
val toolName: String? = null,
val result: String?,
) : Event
/**
* Ошибка хода. После неё поток завершается; дальнейшие события могут