feat(server): carry latencyMs + totalTokens through ServerMessage.ChatTurn

ChatTurn wire frame now includes latencyMs: Long? and totalTokens: Int?.
DomainEventMapper reads them from ChatTurnEvent.latencyMs and
tokensUsed?.totalTokens. Positional call site in DomainEventMapperTest
converted to named args. Two new serialization tests cover the ROUTER
(metrics present) and USER (metrics absent) paths.
This commit is contained in:
2026-06-03 15:33:43 +04:00
parent 44c26affe5
commit c2fb17f6ee
4 changed files with 46 additions and 1 deletions
@@ -82,6 +82,8 @@ suspend fun domainEventToServerMessage(
turnId = p.turnId,
role = p.role.name,
content = p.content,
latencyMs = p.latencyMs,
totalTokens = p.tokensUsed?.totalTokens,
sequence = seq,
sessionSequence = sessionSequence,
)
@@ -67,6 +67,8 @@ sealed interface ServerMessage {
val turnId: String,
val role: String,
val content: String,
val latencyMs: Long? = null,
val totalTokens: Int? = null,
override val sequence: Long,
override val sessionSequence: Long,
) : ServerMessage, SessionMessage
@@ -113,7 +113,14 @@ class DomainEventMapperTest {
)
val result = domainEventToServerMessage(event, noopStore, sessionSequence = 3L)
assertEquals(
ServerMessage.ChatTurn(sessionId, "turn-1", "ROUTER", "hello", event.sequence, 3L),
ServerMessage.ChatTurn(
sessionId = sessionId,
turnId = "turn-1",
role = "ROUTER",
content = "hello",
sequence = event.sequence,
sessionSequence = 3L,
),
result,
)
}
@@ -45,6 +45,40 @@ class ServerMessageSerializationTest {
assert(jsonStr.contains("\"content\":\"hi there\"")) { "expected content" }
}
@Test
fun `ChatTurn with metrics encodes latencyMs and totalTokens`() {
val msg = ServerMessage.ChatTurn(
sessionId = SessionId("sess-1"),
turnId = "t9",
role = "ROUTER",
content = "hi",
latencyMs = 3000,
totalTokens = 943,
sequence = 7,
sessionSequence = 2,
)
val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
assert(jsonStr.contains("\"type\":\"chat.turn\"")) { "expected type=chat.turn" }
assert(jsonStr.contains("\"latencyMs\":3000")) { "expected latencyMs=3000" }
assert(jsonStr.contains("\"totalTokens\":943")) { "expected totalTokens=943" }
}
@Test
fun `ChatTurn without metrics encodes latencyMs and totalTokens as absent or null`() {
val msg = ServerMessage.ChatTurn(
sessionId = SessionId("sess-1"),
turnId = "t10",
role = "USER",
content = "hello",
sequence = 8,
sessionSequence = 3,
)
val jsonStr = ProtocolSerializer.encodeServerMessage(msg)
assert(jsonStr.contains("\"type\":\"chat.turn\"")) { "expected type=chat.turn" }
assert(!jsonStr.contains("\"latencyMs\":3000")) { "latencyMs should not be 3000" }
assert(!jsonStr.contains("\"totalTokens\":943")) { "totalTokens should not be 943" }
}
@Test
fun `SnapshotComplete encodes with only type field`() {
val msg = ServerMessage.SnapshotComplete