fix(sync): track only trailing assistant streaming (#1151)
* fix(sync): track only trailing assistant streaming * test(sync): cover streaming completion paths --------- Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
This commit is contained in:
committed by
GitHub
co-authored by
Isaac Sanchez
parent
811aa50312
commit
c1c21fe340
@@ -0,0 +1,83 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "bun:test"
|
||||||
|
import type { Message, SessionStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { INITIAL_STATE, type State } from "./types"
|
||||||
|
import { updateStreamingState, useStreamingStore } from "./streaming"
|
||||||
|
|
||||||
|
const message = (id: string, role: "user" | "assistant"): Message => ({
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
} as unknown as Message)
|
||||||
|
|
||||||
|
const stateWithMessages = (messages: Message[], status: SessionStatus = { type: "busy" } as SessionStatus): State => ({
|
||||||
|
...INITIAL_STATE,
|
||||||
|
session_status: {
|
||||||
|
ses_1: status,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
ses_1: messages,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateStreamingState", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useStreamingStore.setState({
|
||||||
|
streamingMessageIds: new Map(),
|
||||||
|
messageStreamStates: new Map(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not mark a previous assistant message as streaming during a new user turn", () => {
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
]))
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBe("msg_assistant_1")
|
||||||
|
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
message("msg_user_2", "user"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBeNull()
|
||||||
|
expect(useStreamingStore.getState().messageStreamStates.get("msg_assistant_1")?.phase).toBe("completed")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("tracks the trailing assistant message once it appears", () => {
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
]))
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
message("msg_user_2", "user"),
|
||||||
|
]))
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBeNull()
|
||||||
|
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
message("msg_user_2", "user"),
|
||||||
|
message("msg_assistant_2", "assistant"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBe("msg_assistant_2")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("completes the streaming message when the session becomes idle", () => {
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
]))
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBe("msg_assistant_1")
|
||||||
|
|
||||||
|
updateStreamingState(stateWithMessages([
|
||||||
|
message("msg_user_1", "user"),
|
||||||
|
message("msg_assistant_1", "assistant"),
|
||||||
|
], { type: "idle" } as SessionStatus))
|
||||||
|
|
||||||
|
expect(useStreamingStore.getState().streamingMessageIds.get("ses_1")).toBeNull()
|
||||||
|
expect(useStreamingStore.getState().messageStreamStates.get("msg_assistant_1")?.phase).toBe("completed")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -57,20 +57,43 @@ export function updateStreamingState(state: State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const completeStreamingMessage = (sessionID: string, msgId: string) => {
|
||||||
|
nextStreamingIds.set(sessionID, null)
|
||||||
|
const existing = nextStreamStates.get(msgId)
|
||||||
|
if (existing && existing.phase === "streaming") {
|
||||||
|
nextStreamStates.set(msgId, {
|
||||||
|
...existing,
|
||||||
|
phase: "completed",
|
||||||
|
completedAt: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
for (const sessionID of busySessionIds) {
|
for (const sessionID of busySessionIds) {
|
||||||
const messages = state.message[sessionID]
|
const messages = state.message[sessionID]
|
||||||
if (!messages || messages.length === 0) continue
|
if (!messages || messages.length === 0) continue
|
||||||
|
|
||||||
// Find the last assistant message — that's the one streaming
|
// Only the trailing assistant turn can be streaming. If a new user turn is
|
||||||
|
// last, the next assistant message has not arrived yet.
|
||||||
let streamingMsg: Message | null = null
|
let streamingMsg: Message | null = null
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].role === "user") {
|
||||||
|
break
|
||||||
|
}
|
||||||
if (messages[i].role === "assistant") {
|
if (messages[i].role === "assistant") {
|
||||||
streamingMsg = messages[i]
|
streamingMsg = messages[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!streamingMsg) continue
|
if (!streamingMsg) {
|
||||||
|
const prevId = currentStreamingIds.get(sessionID)
|
||||||
|
if (prevId) {
|
||||||
|
completeStreamingMessage(sessionID, prevId)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const prevId = currentStreamingIds.get(sessionID)
|
const prevId = currentStreamingIds.get(sessionID)
|
||||||
if (prevId !== streamingMsg.id) changed = true
|
if (prevId !== streamingMsg.id) changed = true
|
||||||
@@ -100,16 +123,7 @@ export function updateStreamingState(state: State) {
|
|||||||
const isStillBusy = busySessionIds.has(sessionID)
|
const isStillBusy = busySessionIds.has(sessionID)
|
||||||
if (isStillBusy) continue
|
if (isStillBusy) continue
|
||||||
|
|
||||||
nextStreamingIds.set(sessionID, null)
|
completeStreamingMessage(sessionID, msgId)
|
||||||
const existing = nextStreamStates.get(msgId)
|
|
||||||
if (existing && existing.phase === "streaming") {
|
|
||||||
nextStreamStates.set(msgId, {
|
|
||||||
...existing,
|
|
||||||
phase: "completed",
|
|
||||||
completedAt: now,
|
|
||||||
})
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
|||||||
Reference in New Issue
Block a user