Изменения вводят live-канал OnlineOutbox и фоновую синхронизацию диалогов.

This commit is contained in:
2026-09-30 11:43:49 +03:00
parent 2ad2d65f06
commit 26f94ea83d
12 changed files with 699 additions and 109 deletions
@@ -5,6 +5,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
import pw.binom.agentik.outbox.Event
import pw.binom.agentik.outbox.OnlineEvent
import kotlin.time.Clock
import kotlin.time.Instant
@@ -13,8 +14,14 @@ class LiveTurnTest {
private val now = Clock.System.now()
private fun at() = Instant.fromEpochMilliseconds(now.toEpochMilliseconds())
private fun LiveTurn.feed(vararg events: Event) {
events.forEach { apply(it) }
private fun LiveTurn.feed(vararg events: Any) {
events.forEach {
when (it) {
is Event -> apply(it)
is OnlineEvent -> applyOnline(it)
else -> error("unexpected event: $it")
}
}
}
@Test
@@ -22,12 +29,12 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartReasoning(at()),
Event.AppendText(at(), "сначала "),
Event.AppendText(at(), "подумаем"),
Event.StartResponse(at(), Event.ResponseType.TEXT),
Event.AppendText(at(), "Привет, "),
Event.AppendText(at(), "мир!"),
OnlineEvent.StartReasoning(at()),
OnlineEvent.AppendText(at(), "сначала "),
OnlineEvent.AppendText(at(), "подумаем"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "Привет, "),
OnlineEvent.AppendText(at(), "мир!"),
Event.End(at()),
)
@@ -46,7 +53,7 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
Event.ToolCall(at(), id = "t1", title = "grep", toolName = "grep", toolArgs = "{\"q\":\"x\"}"),
Event.ToolResult(at(), toolCallId = "t1", toolName = "grep", result = "found 3"),
Event.End(at()),
@@ -66,7 +73,7 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
Event.ToolResult(at(), toolCallId = "missing", toolName = null, result = "ok"),
)
@@ -94,8 +101,8 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
Event.AppendText(at(), "partial"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "partial"),
Event.Error(at(), message = "llm down", code = "E500"),
)
@@ -112,8 +119,8 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
Event.AppendText(at(), "не до конца"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "не до конца"),
Event.Interrupted(at()),
)
@@ -127,10 +134,10 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
Event.AppendText(at(), "до"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "до"),
Event.ToolCall(at(), id = "t1", title = null, toolName = "grep", toolArgs = "{}"),
Event.AppendText(at(), "после"),
OnlineEvent.AppendText(at(), "после"),
)
val answers = turn.blocks.filterIsInstance<LiveBlock.Answer>()
@@ -146,8 +153,8 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.IMAGE),
Event.AppendImage(at(), body = bytes, mime = "image/png"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.IMAGE),
OnlineEvent.AppendImage(at(), body = bytes, mime = "image/png"),
Event.End(at()),
)
@@ -162,11 +169,11 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.StartResponse(at(), Event.ResponseType.TEXT),
Event.AppendText(at(), "старое"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "старое"),
Event.End(at()),
)
turn.feed(Event.Working(at()), Event.AppendText(at(), "новое"))
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "новое"))
assertEquals(1, turn.blocks.size)
assertEquals("новое", turn.answerText())
@@ -175,7 +182,7 @@ class LiveTurnTest {
@Test
fun `append without explicit start response still streams as answer`() {
val turn = LiveTurn()
turn.feed(Event.Working(at()), Event.AppendText(at(), "без старта"))
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "без старта"))
assertEquals(TurnPhase.RESPONDING, turn.phase)
assertEquals("без старта", turn.answerText())
@@ -186,7 +193,7 @@ class LiveTurnTest {
val turn = LiveTurn()
var count = 0
turn.onChange = { count++ }
turn.feed(Event.Working(at()), Event.AppendText(at(), "x"))
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "x"))
assertEquals(2, count)
}
@@ -196,12 +203,12 @@ class LiveTurnTest {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
Event.AppendText(at(), "Первая строка\n\nвторая строка "),
OnlineEvent.AppendText(at(), "Первая строка\n\nвторая строка "),
)
assertEquals("Первая строка вторая строка", turn.previewText())
val long = LiveTurn()
long.feed(Event.Working(at()), Event.AppendText(at(), "слово ".repeat(60)))
long.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "слово ".repeat(60)))
val preview = long.previewText(maxLength = 30)
assertTrue(preview.length <= 31)
assertTrue(preview.endsWith("…"))