mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-248623 agent-workbench: support action prompts in ACP threads
Auth-required ACP failures need to present real runtime actions inside the thread transcript, not only text or hard-coded IDE actions. Add an action prompt transcript entry, keep auth-required threads sendable, and let the thread screen render prompt buttons that either invoke an IDE action or delegate to the owning runtime sender. This gives ACP integrations a typed path for buttons such as authentication choices while keeping the engine UI independent from ACP-specific implementation details. GitOrigin-RevId: a9fb023d56faf0a75a5b1dd793ba1dc44b808358
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8ec01bb649
commit
5b6c3cb096
@@ -10,6 +10,7 @@ acp.screen.row.command=COMMAND
|
||||
acp.screen.row.plan=PLAN
|
||||
acp.screen.row.diff=DIFF
|
||||
acp.screen.row.context=CONTEXT
|
||||
acp.screen.row.authorization=AUTHORIZATION
|
||||
acp.screen.row.status={0} - {1}
|
||||
acp.screen.approval.status=Approval: {0}
|
||||
acp.screen.message.streaming=streaming
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.agent.workbench.engine.core
|
||||
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -136,6 +137,25 @@ data class ThreadContextCompaction(
|
||||
override val updatedAt: Long = 0L,
|
||||
) : ThreadTranscriptEntry
|
||||
|
||||
@Serializable
|
||||
data class ThreadActionPromptButton(
|
||||
val id: String,
|
||||
val text: @NlsSafe String,
|
||||
val actionId: String? = null,
|
||||
val actionKind: String? = null,
|
||||
val authMethodId: String? = null,
|
||||
val description: @NlsSafe String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ThreadActionPrompt(
|
||||
override val id: String,
|
||||
val title: @NlsSafe String,
|
||||
val message: @NlsSafe String? = null,
|
||||
val buttons: List<ThreadActionPromptButton> = emptyList(),
|
||||
override val updatedAt: Long = 0L,
|
||||
) : ThreadTranscriptEntry
|
||||
|
||||
@Serializable
|
||||
data class ThreadUsage(
|
||||
val inputTokens: Long? = null,
|
||||
@@ -231,7 +251,7 @@ fun reduce(state: ThreadProjection, event: ThreadEventEnvelope): ThreadProjectio
|
||||
return when (event.type) {
|
||||
ThreadEventType.ThreadCreated -> withSeq.withThreadCreated(payload, event.timestamp)
|
||||
ThreadEventType.RuntimeSessionBound -> withSeq.withRuntimeBinding(payload, event.timestamp)
|
||||
ThreadEventType.ThreadStarted -> withSeq.withStatus(ThreadStatus.Running, event.timestamp)
|
||||
ThreadEventType.ThreadStarted -> withSeq.withThreadStarted(event.timestamp)
|
||||
ThreadEventType.MessageDelta -> withSeq.withMessageDelta(payload, event.timestamp)
|
||||
ThreadEventType.MessageCompleted -> withSeq.withMessageCompleted(payload, event.timestamp)
|
||||
ThreadEventType.ToolCallStarted -> withSeq.withToolCallStarted(payload, event.timestamp)
|
||||
@@ -262,7 +282,7 @@ fun reduce(state: ThreadProjection, event: ThreadEventEnvelope): ThreadProjectio
|
||||
),
|
||||
)
|
||||
ThreadEventType.ThreadFailed -> withSeq.withStatus(ThreadStatus.Failed, event.timestamp)
|
||||
ThreadEventType.ThreadDisconnected -> withSeq.withStatus(ThreadStatus.Disconnected, event.timestamp)
|
||||
ThreadEventType.ThreadDisconnected -> withSeq.withDisconnected(payload, event.timestamp)
|
||||
ThreadEventType.ThreadCancelled -> withSeq.withStatus(ThreadStatus.Cancelled, event.timestamp)
|
||||
else -> withSeq
|
||||
}
|
||||
@@ -275,12 +295,34 @@ fun replay(seed: ThreadProjection, events: Iterable<ThreadEventEnvelope>): Threa
|
||||
private fun ThreadProjection.withStatus(status: ThreadStatus, at: Long): ThreadProjection =
|
||||
copy(thread = thread.copy(status = status, updatedAt = at))
|
||||
|
||||
private fun ThreadProjection.withThreadStarted(at: Long): ThreadProjection {
|
||||
val next = withStatus(ThreadStatus.Running, at)
|
||||
val transcript = next.transcript.filterNot { it is ThreadActionPrompt }
|
||||
return if (transcript.size == next.transcript.size) next else next.copy(transcript = transcript)
|
||||
}
|
||||
|
||||
private fun ThreadProjection.withThreadUpdated(at: Long): ThreadProjection =
|
||||
copy(thread = thread.copy(updatedAt = at))
|
||||
|
||||
private fun ThreadProjection.withWaitingStatus(at: Long): ThreadProjection =
|
||||
withStatus(if (pendingApprovals > 0) ThreadStatus.WaitingForApproval else ThreadStatus.WaitingForUser, at)
|
||||
|
||||
private fun ThreadProjection.withDisconnected(payload: JsonObject, at: Long): ThreadProjection {
|
||||
val authRequired = payload.boolean("authRequired") == true
|
||||
val next = withStatus(if (authRequired) ThreadStatus.WaitingForUser else ThreadStatus.Disconnected, at)
|
||||
if (!authRequired) return next
|
||||
val title = payload.string("title") ?: return next
|
||||
val prompt = ThreadActionPrompt(
|
||||
id = payload.string("promptId") ?: "auth-required",
|
||||
title = title,
|
||||
message = payload.string("message") ?: payload.string("details")?.takeIf { it != title },
|
||||
buttons = payload.buttons(),
|
||||
updatedAt = at,
|
||||
)
|
||||
val transcript = next.transcript.filterNot { it is ThreadActionPrompt && it.id == prompt.id } + prompt
|
||||
return next.copy(transcript = transcript).withThreadUpdated(at)
|
||||
}
|
||||
|
||||
private fun ThreadProjection.withThreadCreated(payload: JsonObject, at: Long): ThreadProjection {
|
||||
val created = copy(
|
||||
thread = thread.copy(
|
||||
@@ -631,6 +673,24 @@ private fun JsonObject.array(key: String): JsonArray? = this[key]?.let {
|
||||
runCatching { it.jsonArray }.getOrNull()
|
||||
}
|
||||
|
||||
private fun JsonObject.buttons(): List<ThreadActionPromptButton> {
|
||||
return array("buttons")?.mapIndexedNotNull { index, element ->
|
||||
val button = runCatching { element.jsonObject }.getOrNull() ?: return@mapIndexedNotNull null
|
||||
val text = button.string("text") ?: return@mapIndexedNotNull null
|
||||
val actionId = button.string("actionId")
|
||||
val actionKind = button.string("actionKind")
|
||||
if (actionId == null && actionKind == null) return@mapIndexedNotNull null
|
||||
ThreadActionPromptButton(
|
||||
id = button.string("id") ?: actionId ?: button.string("authMethodId") ?: "button:$index",
|
||||
text = text,
|
||||
actionId = actionId,
|
||||
actionKind = actionKind,
|
||||
authMethodId = button.string("authMethodId"),
|
||||
description = button.string("description"),
|
||||
)
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> JsonObject.enum(key: String): T? {
|
||||
val raw = string(key) ?: return null
|
||||
return enumValues<T>().firstOrNull { it.name == raw }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.agent.workbench.engine.platform
|
||||
|
||||
import com.intellij.agent.workbench.engine.core.ThreadActionPromptButton
|
||||
import com.intellij.agent.workbench.engine.core.ThreadId
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
@@ -21,6 +22,9 @@ interface EnginePromptSender {
|
||||
/** Sends [text] as the next user prompt in [threadId]; connects the runtime session on first use. */
|
||||
fun sendPrompt(project: Project, threadId: ThreadId, text: String)
|
||||
|
||||
/** Handles a runtime-specific action prompt button rendered in the thread transcript. */
|
||||
fun handleActionPromptButton(project: Project, threadId: ThreadId, button: ThreadActionPromptButton): Boolean = false
|
||||
|
||||
companion object {
|
||||
private val EP = ExtensionPointName<EnginePromptSender>("com.intellij.agent.workbench.engine.promptSender")
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
package com.intellij.agent.workbench.engine.ui
|
||||
|
||||
import com.intellij.agent.workbench.engine.core.RuntimeKind
|
||||
import com.intellij.agent.workbench.engine.core.ThreadActionPrompt
|
||||
import com.intellij.agent.workbench.engine.core.ThreadActionPromptButton
|
||||
import com.intellij.agent.workbench.engine.core.ThreadEventEnvelope
|
||||
import com.intellij.agent.workbench.engine.core.ThreadCommand
|
||||
import com.intellij.agent.workbench.engine.core.ThreadContextCompaction
|
||||
@@ -19,11 +21,20 @@ import com.intellij.agent.workbench.engine.platform.EngineProjectService
|
||||
import com.intellij.agent.workbench.engine.platform.EnginePromptSender
|
||||
import com.intellij.agent.workbench.engine.platform.EngineRuntimeConnector
|
||||
import com.intellij.agent.workbench.engine.platform.EngineUnreadTracker
|
||||
import com.intellij.ide.setToolTipText
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.ActionUiKind
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.text.HtmlChunk
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
@@ -34,6 +45,7 @@ import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.Font
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
@@ -44,6 +56,7 @@ import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.Scrollable
|
||||
@@ -423,7 +436,8 @@ internal class AgentAcpThreadScreen(
|
||||
val manualTailPauseActive = isManualTranscriptTailPauseActive()
|
||||
val followTailBefore = followTranscriptTail
|
||||
val keepFollowingTailAcrossLayoutDrift = followTailBefore && !manualTailPauseActive && scrollState.isWithinTailDrift
|
||||
val shouldStickToBottom = previousComponents == 0 || latestIsUserMessage || (atBottom && !manualTailPauseActive) || keepFollowingTailAcrossLayoutDrift
|
||||
val shouldStickToBottom =
|
||||
previousComponents == 0 || latestIsUserMessage || (atBottom && !manualTailPauseActive) || keepFollowingTailAcrossLayoutDrift
|
||||
val reason = when {
|
||||
previousComponents == 0 -> "initial-or-empty-panel"
|
||||
latestIsUserMessage -> "latest-entry-is-user-message"
|
||||
@@ -651,6 +665,7 @@ internal class AgentAcpThreadScreen(
|
||||
is ThreadPlan -> "plan(id=$id, complete=$complete, items=${items.size}, updatedAt=$updatedAt)"
|
||||
is ThreadFileDiff -> "diff(id=$id, status=$status, newTextLength=${newText?.length ?: 0}, updatedAt=$updatedAt)"
|
||||
is ThreadContextCompaction -> "context(id=$id, summaryLength=${summary?.length ?: 0}, updatedAt=$updatedAt)"
|
||||
is ThreadActionPrompt -> "actionPrompt(id=$id, buttons=${buttons.size}, updatedAt=$updatedAt)"
|
||||
}
|
||||
|
||||
private fun ThreadProjection.debugSummary(): String =
|
||||
@@ -671,6 +686,7 @@ internal class AgentAcpThreadScreen(
|
||||
is ThreadPlan -> planRow(entry)
|
||||
is ThreadFileDiff -> diffRow(entry)
|
||||
is ThreadContextCompaction -> contextRow(entry)
|
||||
is ThreadActionPrompt -> actionPromptRow(entry)
|
||||
}
|
||||
|
||||
private fun messageRow(message: ThreadMessage): JComponent {
|
||||
@@ -791,6 +807,91 @@ internal class AgentAcpThreadScreen(
|
||||
)
|
||||
}
|
||||
|
||||
private fun actionPromptRow(prompt: ThreadActionPrompt): JComponent {
|
||||
val row = TranscriptRowPanel().apply {
|
||||
isOpaque = false
|
||||
alignmentX = LEFT_ALIGNMENT
|
||||
}
|
||||
val roleLabel = JBLabel(EngineBundle.message("acp.screen.row.authorization")).apply {
|
||||
font = JBFont.small().asBold()
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
border = JBUI.Borders.emptyBottom(2)
|
||||
}
|
||||
val body = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
isOpaque = false
|
||||
add(roleLabel)
|
||||
add(rowTextArea(prompt.title, monospace = false))
|
||||
prompt.message?.takeIf { it.isNotBlank() }?.let { message ->
|
||||
add(Box.createVerticalStrut(2))
|
||||
add(rowTextArea(message, monospace = false))
|
||||
}
|
||||
val buttonPanel = promptButtonPanel(prompt.buttons)
|
||||
if (buttonPanel != null) {
|
||||
add(buttonPanel)
|
||||
}
|
||||
}
|
||||
row.add(body, BorderLayout.CENTER)
|
||||
return row
|
||||
}
|
||||
|
||||
private fun promptButtonPanel(buttons: List<ThreadActionPromptButton>): JComponent? {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val panel = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
isOpaque = false
|
||||
alignmentX = LEFT_ALIGNMENT
|
||||
border = JBUI.Borders.emptyTop(6)
|
||||
}
|
||||
for (button in buttons) {
|
||||
val actionId = button.actionId
|
||||
val actionKind = button.actionKind
|
||||
val component = when {
|
||||
actionId != null -> {
|
||||
val action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
LOG.warn("[$threadId] ACP action prompt skipped missing action: $actionId")
|
||||
null
|
||||
}
|
||||
else {
|
||||
promptButton(button) {
|
||||
val dataContext = DataContext { dataId ->
|
||||
if (CommonDataKeys.PROJECT.`is`(dataId)) project else null
|
||||
}
|
||||
val event = AnActionEvent.createEvent(action, dataContext, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
|
||||
ActionUtil.performAction(action, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
actionKind != null -> {
|
||||
promptButton(button) {
|
||||
val sender = EnginePromptSender.forThread(project, threadId)
|
||||
if (sender == null || !sender.handleActionPromptButton(project, threadId, button)) {
|
||||
LOG.warn("[$threadId] ACP action prompt skipped unhandled action kind: $actionKind")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LOG.warn("[$threadId] ACP action prompt skipped button without action: ${button.id}")
|
||||
null
|
||||
}
|
||||
}
|
||||
if (component != null) {
|
||||
if (panel.componentCount > 0) panel.add(Box.createHorizontalStrut(JBUI.scale(6)))
|
||||
panel.add(component)
|
||||
}
|
||||
}
|
||||
return panel.takeIf { it.componentCount > 0 }
|
||||
}
|
||||
|
||||
private fun promptButton(button: ThreadActionPromptButton, action: () -> Unit): JButton {
|
||||
return JButton(button.text).apply {
|
||||
button.description?.let { setToolTipText(HtmlChunk.text(it)) }
|
||||
accessibleContext.accessibleName = button.text
|
||||
accessibleContext.accessibleDescription = button.description
|
||||
addActionListener { action() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun entryRow(
|
||||
label: @NlsSafe String,
|
||||
title: String?,
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSource
|
||||
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSourceUpdateEvent
|
||||
import com.intellij.agent.workbench.engine.core.MessageRole
|
||||
import com.intellij.agent.workbench.engine.core.RuntimeKind
|
||||
import com.intellij.agent.workbench.engine.core.ThreadActionPrompt
|
||||
import com.intellij.agent.workbench.engine.core.ThreadCommand
|
||||
import com.intellij.agent.workbench.engine.core.ThreadContextCompaction
|
||||
import com.intellij.agent.workbench.engine.core.ThreadFileDiff
|
||||
@@ -119,7 +120,7 @@ internal class EngineSessionSource : AgentSessionSource {
|
||||
else -> AgentSessionOutlineItemKind.METADATA
|
||||
}
|
||||
is ThreadToolCall, is ThreadCommand -> AgentSessionOutlineItemKind.TOOL_CALL
|
||||
is ThreadPlan, is ThreadFileDiff, is ThreadContextCompaction -> AgentSessionOutlineItemKind.METADATA
|
||||
is ThreadPlan, is ThreadFileDiff, is ThreadContextCompaction, is ThreadActionPrompt -> AgentSessionOutlineItemKind.METADATA
|
||||
}
|
||||
|
||||
private fun outlineTitle(entry: ThreadTranscriptEntry): String = when (entry) {
|
||||
@@ -129,6 +130,7 @@ internal class EngineSessionSource : AgentSessionSource {
|
||||
is ThreadPlan -> entry.title ?: entry.id
|
||||
is ThreadFileDiff -> entry.title ?: entry.path ?: entry.id
|
||||
is ThreadContextCompaction -> entry.title ?: entry.id
|
||||
is ThreadActionPrompt -> entry.title
|
||||
}
|
||||
|
||||
private fun outlinePreview(entry: ThreadTranscriptEntry): String = when (entry) {
|
||||
@@ -138,5 +140,6 @@ internal class EngineSessionSource : AgentSessionSource {
|
||||
is ThreadPlan -> entry.items.joinToString(separator = "\n") { it.title }
|
||||
is ThreadFileDiff -> entry.newText.orEmpty()
|
||||
is ThreadContextCompaction -> entry.summary.orEmpty()
|
||||
is ThreadActionPrompt -> entry.message ?: entry.buttons.joinToString(separator = "\n") { it.text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.intellij.agent.workbench.engine.platform
|
||||
|
||||
import com.intellij.agent.workbench.engine.core.EventSource
|
||||
import com.intellij.agent.workbench.engine.core.EventVisibility
|
||||
import com.intellij.agent.workbench.engine.core.ThreadActionPrompt
|
||||
import com.intellij.agent.workbench.engine.core.StartThreadRequest
|
||||
import com.intellij.agent.workbench.engine.core.ThreadApprovalStatus
|
||||
import com.intellij.agent.workbench.engine.core.ThreadCommand
|
||||
@@ -14,6 +15,8 @@ import com.intellij.agent.workbench.engine.core.ThreadToolCall
|
||||
import com.intellij.agent.workbench.engine.core.RuntimeKind
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.addJsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@@ -314,6 +317,84 @@ class EngineEventStoreTest {
|
||||
assertThat(projection.messages.single().text).isEqualTo("hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-required disconnected event shows an action prompt and keeps the thread sendable`(@TempDir dir: Path) {
|
||||
val store = JsonlEventStore(dir)
|
||||
store.append(
|
||||
threadId, EventSource.Runtime, ThreadEventType.ThreadDisconnected,
|
||||
buildJsonObject {
|
||||
put("authRequired", true)
|
||||
put("promptId", "auth-required")
|
||||
put("title", "Authentication required")
|
||||
put("message", "Sign in, then send the message again.")
|
||||
put("details", "Authentication is required before this operation can be performed.")
|
||||
put("buttons", buildJsonArray {
|
||||
addJsonObject {
|
||||
put("id", "acp-auth-jetbrains-account")
|
||||
put("text", "Junie Account")
|
||||
put("actionKind", "acpAuthenticate")
|
||||
put("authMethodId", "jetbrains-account")
|
||||
put("description", "Log in via Junie Account in a browser.")
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
val projection = store.projection(threadId)
|
||||
assertThat(projection.thread.status).isEqualTo(ThreadStatus.WaitingForUser)
|
||||
val prompt = projection.transcript.single() as ThreadActionPrompt
|
||||
assertThat(prompt.title).isEqualTo("Authentication required")
|
||||
assertThat(prompt.message).isEqualTo("Sign in, then send the message again.")
|
||||
val button = prompt.buttons.single()
|
||||
assertThat(button.id).isEqualTo("acp-auth-jetbrains-account")
|
||||
assertThat(button.text).isEqualTo("Junie Account")
|
||||
assertThat(button.actionId).isNull()
|
||||
assertThat(button.actionKind).isEqualTo("acpAuthenticate")
|
||||
assertThat(button.authMethodId).isEqualTo("jetbrains-account")
|
||||
assertThat(button.description).isEqualTo("Log in via Junie Account in a browser.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ThreadStarted clears stale action prompts`(@TempDir dir: Path) {
|
||||
val store = JsonlEventStore(dir)
|
||||
store.append(
|
||||
threadId, EventSource.Runtime, ThreadEventType.ThreadDisconnected,
|
||||
buildJsonObject {
|
||||
put("authRequired", true)
|
||||
put("promptId", "auth-required")
|
||||
put("title", "Authentication required")
|
||||
put("message", "Sign in, then send the message again.")
|
||||
put("buttons", buildJsonArray {
|
||||
addJsonObject {
|
||||
put("id", "open-ai-assistant-settings")
|
||||
put("text", "Open AI Assistant Settings")
|
||||
put("actionId", "AIAssistant.LLMSettings")
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
assertThat(store.projection(threadId).transcript.filterIsInstance<ThreadActionPrompt>()).hasSize(1)
|
||||
|
||||
store.append(
|
||||
threadId, EventSource.User, ThreadEventType.MessageDelta,
|
||||
buildJsonObject {
|
||||
put("messageId", "m1")
|
||||
put("role", "User")
|
||||
put("contentDelta", "hey")
|
||||
},
|
||||
)
|
||||
store.append(
|
||||
threadId, EventSource.User, ThreadEventType.MessageCompleted,
|
||||
buildJsonObject { put("messageId", "m1") },
|
||||
)
|
||||
store.append(threadId, EventSource.Runtime, ThreadEventType.ThreadStarted)
|
||||
|
||||
val projection = store.projection(threadId)
|
||||
assertThat(projection.thread.status).isEqualTo(ThreadStatus.Running)
|
||||
assertThat(projection.transcript.filterIsInstance<ThreadActionPrompt>()).isEmpty()
|
||||
assertThat(projection.messages.single().text).isEqualTo("hey")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ThreadWaiting after disconnected recovers the thread status`(@TempDir dir: Path) {
|
||||
val store = JsonlEventStore(dir)
|
||||
|
||||
Reference in New Issue
Block a user