Изменение выносит типы контента в :content-api и разделяет потоки событий.
This commit is contained in:
+81
-37
@@ -72,9 +72,11 @@ dependencies {
|
||||
|
||||
```kotlin
|
||||
import pw.binom.agentik.client.AgentikAgent
|
||||
import pw.binom.agentik.proto.Content
|
||||
import pw.binom.agentik.proto.Event
|
||||
import pw.binom.agentik.content.Content
|
||||
import pw.binom.agentik.outbox.Event
|
||||
import pw.binom.agentik.outbox.OnlineEvent
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.time.Clock
|
||||
|
||||
@@ -87,21 +89,37 @@ fun main() = runBlocking {
|
||||
token = "s3cret", // или null, если не нужен
|
||||
)
|
||||
|
||||
// 2. Открыть диалог, отправить сообщение.
|
||||
val conv = agent.createConversation(temp = false)
|
||||
conv.send(listOf(Content.Text("Привет")))
|
||||
|
||||
// 3. Собирать streaming-ответ.
|
||||
conv.events(after = Clock.System.now()).collect { ev ->
|
||||
when (ev) {
|
||||
is Event.StartResponse -> println("[start]")
|
||||
is Event.AppendText -> print(ev.body)
|
||||
is Event.End -> println("[end]")
|
||||
is Event.Error -> println("[error: ${ev.message}]")
|
||||
else -> Unit
|
||||
// 2. Два независимых потока событий диалога:
|
||||
// durable (outbox) — целые события, с курсором после переподключения;
|
||||
// online (OnlineOutbox) — стриминг ответа, только live (без курсора).
|
||||
launch {
|
||||
agent.outbox.conversationEvents(after = Clock.System.now(), conversationId = conv.id)
|
||||
.collect { ce ->
|
||||
when (val ev = ce.event) {
|
||||
is Event.AssistantMessage -> println("[answer ready: ${ev.content}]")
|
||||
is Event.Interrupted -> println("[interrupted]")
|
||||
is Event.Error -> println("[error: ${ev.message}]")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
agent.onlineOutbox.onlineEvents(conv.id).collect { ev ->
|
||||
when (ev) {
|
||||
is OnlineEvent.Working -> println("[working]")
|
||||
is OnlineEvent.AppendText -> print(ev.body)
|
||||
is OnlineEvent.StartResponse -> println("[start]")
|
||||
is OnlineEvent.End -> println("\n[end]")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Отправить ход (fire-and-forget — ответ придёт по подпискам выше).
|
||||
conv.send(listOf(Content.Text("Привет")))
|
||||
|
||||
// 4. Чистый shutdown.
|
||||
conv.close()
|
||||
agent.close()
|
||||
@@ -109,7 +127,14 @@ fun main() = runBlocking {
|
||||
```
|
||||
|
||||
**Это весь клиент.** `:server` сам хранит историю, контекст, события.
|
||||
Ты только получаешь типизированный `Flow<Event>` и рендеришь как хочешь.
|
||||
Ты только получаешь два типизированных `Flow` и рендеришь как хочешь.
|
||||
|
||||
> **Durable vs online.** `Event` (в `agent.outbox`) — «целые» события, их
|
||||
> можно перезапросить по курсору `after`. `OnlineEvent` (в
|
||||
> `agent.onlineOutbox`) — поток стриминга (`Working`/`End`/`AppendText`/
|
||||
> `AppendImage`), **никогда не сохраняется** и не реплеится: потерянный при
|
||||
> обрыве фрагмент невосстановим, но целый ответ всегда придёт durable-
|
||||
> `Event.AssistantMessage` и/или ляжет в journal.
|
||||
|
||||
`HttpClient`, `applyAgentikDefaults`, выбор engine'а — всё скрыто
|
||||
внутри `AgentikAgent`. Один вызов — один готовый `Agent`.
|
||||
@@ -172,9 +197,11 @@ history.forEach { rec ->
|
||||
|
||||
```kotlin
|
||||
import pw.binom.agentik.client.AgentikAgent
|
||||
import pw.binom.agentik.proto.Content
|
||||
import pw.binom.agentik.proto.Event
|
||||
import pw.binom.agentik.content.Content
|
||||
import pw.binom.agentik.outbox.Event
|
||||
import pw.binom.agentik.outbox.OnlineEvent
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val agent = AgentikAgent(
|
||||
id = "agentik",
|
||||
@@ -183,16 +210,30 @@ val agent = AgentikAgent(
|
||||
)
|
||||
|
||||
val conv = agent.createConversation(temp = false)
|
||||
conv.send(listOf(Content.Text("Привет, расскажи про себя")))
|
||||
|
||||
conv.events(after = kotlin.time.Clock.System.now()).collect { ev ->
|
||||
when (ev) {
|
||||
is Event.AppendText -> print(ev.body) // streaming чанки
|
||||
is Event.End -> println("\n--- end ---")
|
||||
is Event.Error -> error("agent error: ${ev.message}")
|
||||
else -> Unit
|
||||
// durable-поток (с курсором): terminal-события хода.
|
||||
launch {
|
||||
agent.outbox.conversationEvents(after = kotlin.time.Clock.System.now(), conversationId = conv.id)
|
||||
.collect { ce ->
|
||||
when (ce.event) {
|
||||
is Event.AssistantMessage -> println("\n--- answer ready ---")
|
||||
is Event.Error -> error("agent error: ${(ce.event as Event.Error).message}")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
// online-поток (live-only): стриминг ответа.
|
||||
launch {
|
||||
agent.onlineOutbox.onlineEvents(conv.id).collect { ev ->
|
||||
when (ev) {
|
||||
is OnlineEvent.AppendText -> print(ev.body) // streaming чанки
|
||||
is OnlineEvent.End -> println("\n--- end ---")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conv.send(listOf(Content.Text("Привет, расскажи про себя")))
|
||||
```
|
||||
|
||||
## История с локальным кэшем
|
||||
@@ -207,8 +248,8 @@ conv.events(after = kotlin.time.Clock.System.now()).collect { ev ->
|
||||
|
||||
```kotlin
|
||||
import pw.binom.agentik.journal.inmemory.InMemoryJournalStore
|
||||
import pw.binom.agentik.proto.Content
|
||||
import pw.binom.agentik.proto.Event
|
||||
import pw.binom.agentik.content.Content
|
||||
import pw.binom.agentik.outbox.Event
|
||||
import kotlin.time.Instant
|
||||
|
||||
class ChatSession(
|
||||
@@ -235,10 +276,11 @@ class ChatSession(
|
||||
after = Instant.DISTANT_PAST,
|
||||
).collect { cache.append(it) }
|
||||
}
|
||||
// 2. Live: на каждом `End` хода просим у сервера новые записи.
|
||||
// 2. Live: на каждом завершённом ходе (durable AssistantMessage)
|
||||
// просим у сервера новые записи.
|
||||
scope.launch {
|
||||
agent.getConversation(conversationId)!!.events(Instant.DISTANT_PAST).collect { ev ->
|
||||
if (ev is Event.End) {
|
||||
agent.outbox.conversationEvents(Instant.DISTANT_PAST, conversationId).collect { ce ->
|
||||
if (ce.event is Event.AssistantMessage) {
|
||||
val newest = cache.let {
|
||||
// last-seen курсор — последний createdAt в кэше
|
||||
it.list(conversationId, Instant.DISTANT_PAST, 0, 1).lastOrNull()?.createdAt
|
||||
@@ -342,22 +384,24 @@ escape-hatch. Сам `InMemoryMutableConversationStore` тоже доступе
|
||||
появится в кэше через refresh-блок выше.
|
||||
|
||||
```kotlin
|
||||
import pw.binom.agentik.proto.Event
|
||||
import pw.binom.agentik.outbox.OnlineEvent
|
||||
|
||||
agent.getConversation(convId)!!.events(Instant.DISTANT_PAST).collect { ev ->
|
||||
agent.onlineOutbox.onlineEvents(convId).collect { ev ->
|
||||
when (ev) {
|
||||
is Event.StartResponse -> println("[start]")
|
||||
is Event.AppendText -> print(ev.body)
|
||||
is Event.AppendImage -> showImage(ev.body)
|
||||
is Event.ToolCall -> println("[tool: ${ev.toolName}]")
|
||||
is Event.ToolResult -> println("[result]")
|
||||
is Event.End -> println("[end]")
|
||||
is Event.Error -> println("[error: ${ev.message}]")
|
||||
else -> Unit
|
||||
is OnlineEvent.Working -> println("[working]")
|
||||
is OnlineEvent.StartResponse -> println("[start]")
|
||||
is OnlineEvent.AppendText -> print(ev.body)
|
||||
is OnlineEvent.AppendImage -> showImage(ev.body)
|
||||
is OnlineEvent.End -> println("[end]")
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Инструментальные вызовы и целый ответ — durable-поток
|
||||
(`agent.outbox.conversationEvents`): `Event.ToolCall`/`Event.ToolResult` и
|
||||
`Event.AssistantMessage`/`Event.Interrupted`/`Event.Error`.
|
||||
|
||||
## Прерывание хода
|
||||
|
||||
```kotlin
|
||||
|
||||
Reference in New Issue
Block a user