fix(runtime-fallback): recognize completion progress events
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
|
||||
import { describe, expect, it } from "bun:test"
|
||||
|
||||
import { observeEventForWatchdog, type FirstPromptWatchdog } from "./first-prompt-watchdog"
|
||||
|
||||
interface RecordedWatchdogCalls {
|
||||
readonly user: string[]
|
||||
readonly progress: string[]
|
||||
readonly terminal: string[]
|
||||
}
|
||||
|
||||
function createRecordingWatchdog(calls: RecordedWatchdogCalls): FirstPromptWatchdog {
|
||||
return {
|
||||
onUserMessage(sessionID) {
|
||||
calls.user.push(sessionID)
|
||||
},
|
||||
onAssistantProgress(sessionID) {
|
||||
calls.progress.push(sessionID)
|
||||
},
|
||||
onSessionTerminal(sessionID) {
|
||||
calls.terminal.push(sessionID)
|
||||
},
|
||||
dispose() {},
|
||||
}
|
||||
}
|
||||
|
||||
function freshCalls(): RecordedWatchdogCalls {
|
||||
return { user: [], progress: [], terminal: [] }
|
||||
}
|
||||
|
||||
describe("observeEventForWatchdog progress markers", () => {
|
||||
const sessionID = "session-observed-progress"
|
||||
|
||||
for (const [label, marker] of [
|
||||
["finished boolean", { finished: true }],
|
||||
["completed boolean", { completed: true }],
|
||||
["completed time", { time: { completed: 1779267000000 } }],
|
||||
] as const) {
|
||||
it(`#given a message.updated assistant event with ${label} #when observed #then onAssistantProgress is called`, () => {
|
||||
const calls = freshCalls()
|
||||
observeEventForWatchdog(
|
||||
{
|
||||
type: "message.updated",
|
||||
properties: { info: { sessionID, role: "assistant", ...marker } },
|
||||
},
|
||||
createRecordingWatchdog(calls),
|
||||
)
|
||||
expect(calls.progress).toEqual([sessionID])
|
||||
})
|
||||
}
|
||||
|
||||
it("#given a message.updated assistant event with false completion markers and no parts #when observed #then no progress is signalled", () => {
|
||||
const calls = freshCalls()
|
||||
observeEventForWatchdog(
|
||||
{
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
completed: false,
|
||||
finish: false,
|
||||
finished: false,
|
||||
},
|
||||
parts: [],
|
||||
},
|
||||
},
|
||||
createRecordingWatchdog(calls),
|
||||
)
|
||||
expect(calls.progress).toEqual([])
|
||||
})
|
||||
|
||||
for (const [eventType] of [
|
||||
["session.next.text.delta"],
|
||||
["session.next.reasoning.delta"],
|
||||
["session.next.tool.called"],
|
||||
["session.next.tool.success"],
|
||||
["session.next.step.ended"],
|
||||
] as const) {
|
||||
it(`#given a ${eventType} event with a sessionID #when observed #then onAssistantProgress is called`, () => {
|
||||
const calls = freshCalls()
|
||||
observeEventForWatchdog(
|
||||
{ type: eventType, properties: { sessionID } },
|
||||
createRecordingWatchdog(calls),
|
||||
)
|
||||
expect(calls.progress).toEqual([sessionID])
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import { resolveFallbackBootstrapModel } from "./fallback-bootstrap-model"
|
||||
import { dispatchFallbackRetry } from "./fallback-retry-dispatcher"
|
||||
|
||||
const SOURCE = "first-prompt-watchdog"
|
||||
const SESSION_NEXT_EVENT_PREFIX = "session.next."
|
||||
|
||||
declare function setTimeout(callback: () => void | Promise<void>, delay?: number): RuntimeFallbackTimeout
|
||||
declare function clearTimeout(timeout: RuntimeFallbackTimeout): void
|
||||
@@ -29,6 +30,19 @@ const TERMINAL_EVENT_TYPES = new Set([
|
||||
"session.error",
|
||||
])
|
||||
|
||||
function isCompletionMarker(value: unknown): boolean {
|
||||
if (typeof value === "boolean") return value
|
||||
return value !== undefined && value !== null
|
||||
}
|
||||
|
||||
function hasAssistantCompletionMarker(info: Record<string, unknown>): boolean {
|
||||
const time = isRecord(info.time) ? info.time : undefined
|
||||
return isCompletionMarker(info.finish)
|
||||
|| isCompletionMarker(info.finished)
|
||||
|| isCompletionMarker(info.completed)
|
||||
|| isCompletionMarker(time?.completed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an OpenCode session event into the appropriate watchdog signal.
|
||||
*
|
||||
@@ -48,6 +62,12 @@ export function observeEventForWatchdog(
|
||||
const props = isRecord(event.properties) ? event.properties : undefined
|
||||
if (!props) return
|
||||
|
||||
if (event.type.startsWith(SESSION_NEXT_EVENT_PREFIX)) {
|
||||
const sessionID = resolveSessionEventID(props) ?? resolveMessageEventSessionID(props)
|
||||
if (sessionID) watchdog.onAssistantProgress(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "message.part.updated" || event.type === "message.part.delta") {
|
||||
const sessionID = resolveMessageEventSessionID(props)
|
||||
const part = isRecord(props.part) ? props.part : undefined
|
||||
@@ -63,6 +83,7 @@ export function observeEventForWatchdog(
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
const info = isRecord(props.info) ? props.info : undefined
|
||||
if (!info) return
|
||||
const sessionID = typeof info?.sessionID === "string" ? info.sessionID : undefined
|
||||
const role = typeof info?.role === "string" ? info.role : undefined
|
||||
if (!sessionID || !role) return
|
||||
@@ -76,7 +97,7 @@ export function observeEventForWatchdog(
|
||||
|
||||
if (role === "assistant") {
|
||||
const hasError = info?.error !== undefined
|
||||
const hasFinish = info?.finish !== undefined
|
||||
const hasFinish = hasAssistantCompletionMarker(info)
|
||||
const eventParts = Array.isArray(props.parts) ? props.parts : undefined
|
||||
const infoParts = Array.isArray(info?.parts) ? info.parts : undefined
|
||||
const parts = eventParts ?? infoParts ?? []
|
||||
|
||||
Reference in New Issue
Block a user