feat: initial release v0.3.0
This commit is contained in:
347
src/lib/query/hooks/run-stream/event-parser.ts
Normal file
347
src/lib/query/hooks/run-stream/event-parser.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import type { RunStreamEvent } from '@/lib/novel-promotion/run-stream/types'
|
||||
import { TASK_EVENT_TYPE, TASK_SSE_EVENT_TYPE, type SSEEvent } from '@/lib/task/types'
|
||||
import { resolveTaskErrorMessage as resolveUnifiedTaskErrorMessage } from '@/lib/task/error-message'
|
||||
import type { RunResult } from './types'
|
||||
|
||||
export function parseSSEBlock(block: string): { event: string; data: string } | null {
|
||||
const lines = block.split('\n')
|
||||
let event = 'message'
|
||||
const dataLines: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line) continue
|
||||
if (line.startsWith('event:')) {
|
||||
event = line.slice(6).trim()
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).trim())
|
||||
}
|
||||
}
|
||||
|
||||
if (dataLines.length === 0) return null
|
||||
return {
|
||||
event,
|
||||
data: dataLines.join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
export function toObject(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
export function readTextField(payload: Record<string, unknown>, key: string): string | undefined {
|
||||
const value = payload[key]
|
||||
return typeof value === 'string' ? value : undefined
|
||||
}
|
||||
|
||||
export function readStepField(payload: Record<string, unknown>, key: string): number | undefined {
|
||||
const value = payload[key]
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.max(1, Math.floor(value)) : undefined
|
||||
}
|
||||
|
||||
function readStringArrayField(payload: Record<string, unknown>, key: string): string[] {
|
||||
const value = payload[key]
|
||||
if (!Array.isArray(value)) return []
|
||||
const rows: string[] = []
|
||||
for (const item of value) {
|
||||
if (typeof item !== 'string') continue
|
||||
const trimmed = item.trim()
|
||||
if (!trimmed) continue
|
||||
rows.push(trimmed)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function readBoolField(payload: Record<string, unknown>, key: string): boolean | undefined {
|
||||
const value = payload[key]
|
||||
if (value === true) return true
|
||||
if (value === false) return false
|
||||
return undefined
|
||||
}
|
||||
|
||||
function normalizeLifecycleType(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
if (value === TASK_EVENT_TYPE.PROGRESS) return TASK_EVENT_TYPE.PROCESSING
|
||||
if (
|
||||
value === TASK_EVENT_TYPE.CREATED ||
|
||||
value === TASK_EVENT_TYPE.PROCESSING ||
|
||||
value === TASK_EVENT_TYPE.COMPLETED ||
|
||||
value === TASK_EVENT_TYPE.FAILED
|
||||
) {
|
||||
return value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function stageLooksCompleted(stage: string | undefined) {
|
||||
if (!stage) return false
|
||||
return (
|
||||
stage === 'llm_completed' ||
|
||||
stage === 'worker_llm_completed' ||
|
||||
stage === 'worker_llm_complete' ||
|
||||
stage === 'llm_proxy_persist' ||
|
||||
stage === 'completed'
|
||||
)
|
||||
}
|
||||
|
||||
function stageLooksFailed(stage: string | undefined) {
|
||||
if (!stage) return false
|
||||
return stage === 'llm_error' || stage === 'worker_llm_error' || stage === 'error'
|
||||
}
|
||||
|
||||
function resolveTaskErrorMessage(payload: Record<string, unknown>, fallback = 'task failed') {
|
||||
return resolveUnifiedTaskErrorMessage(payload, fallback)
|
||||
}
|
||||
|
||||
function extractTerminalPayload(payload: Record<string, unknown>) {
|
||||
const result = toObject(payload.result)
|
||||
if (Object.keys(result).length > 0) {
|
||||
return result
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
export function mapTaskSSEEventToRunEvents(event: SSEEvent): RunStreamEvent[] {
|
||||
const rawPayload = toObject(event.payload)
|
||||
const payloadMeta = toObject(rawPayload.meta)
|
||||
const runId = readTextField(rawPayload, 'runId')
|
||||
|| readTextField(payloadMeta, 'runId')
|
||||
|| (typeof event.taskId === 'string' ? event.taskId : '')
|
||||
if (!runId) return []
|
||||
const payload = rawPayload
|
||||
const lifecycleType =
|
||||
event.type === TASK_SSE_EVENT_TYPE.LIFECYCLE
|
||||
? normalizeLifecycleType(payload.lifecycleType)
|
||||
: null
|
||||
const ts = typeof event.ts === 'string' ? event.ts : new Date().toISOString()
|
||||
const flowStageTitle = readTextField(payload, 'flowStageTitle')
|
||||
const flowStageIndex = readStepField(payload, 'flowStageIndex')
|
||||
const flowStageTotal = readStepField(payload, 'flowStageTotal')
|
||||
const rawStepId = readTextField(payload, 'stepId')
|
||||
const rawStepTitle = readTextField(payload, 'stepTitle')
|
||||
const stepId =
|
||||
rawStepId ||
|
||||
(
|
||||
event.type === TASK_SSE_EVENT_TYPE.STREAM
|
||||
? `step:${event.taskType || 'llm'}`
|
||||
: undefined)
|
||||
const stepAttempt = readStepField(payload, 'stepAttempt')
|
||||
const stepTitle = rawStepTitle || flowStageTitle || undefined
|
||||
const stepIndex = readStepField(payload, 'stepIndex') ?? flowStageIndex
|
||||
const stepTotal = readStepField(payload, 'stepTotal') ?? flowStageTotal
|
||||
const stage = readTextField(payload, 'stage')
|
||||
const message = readTextField(payload, 'message')
|
||||
const done = payload.done === true
|
||||
const text = readTextField(payload, 'output') || readTextField(payload, 'text')
|
||||
const reasoning = readTextField(payload, 'reasoning') || readTextField(payload, 'thinking')
|
||||
const dependsOn = readStringArrayField(payload, 'dependsOn')
|
||||
const blockedBy = readStringArrayField(payload, 'blockedBy')
|
||||
const groupId = readTextField(payload, 'groupId')
|
||||
const parallelKey = readTextField(payload, 'parallelKey')
|
||||
const retryable = readBoolField(payload, 'retryable')
|
||||
const stale = readBoolField(payload, 'stale')
|
||||
|
||||
if (event.type === TASK_SSE_EVENT_TYPE.STREAM) {
|
||||
const stream = toObject(payload.stream)
|
||||
const kind = stream.kind === 'reasoning' ? 'reasoning' : 'text'
|
||||
const delta = typeof stream.delta === 'string' ? stream.delta : ''
|
||||
if (!delta || !stepId) return []
|
||||
const lane = stream.lane === 'reasoning' || kind === 'reasoning' ? 'reasoning' : 'text'
|
||||
const seq =
|
||||
typeof stream.seq === 'number' && Number.isFinite(stream.seq) ? Math.max(1, Math.floor(stream.seq)) : undefined
|
||||
const streamStepAttempt =
|
||||
typeof stream.attempt === 'number' && Number.isFinite(stream.attempt)
|
||||
? Math.max(1, Math.floor(stream.attempt))
|
||||
: undefined
|
||||
|
||||
return [{
|
||||
runId,
|
||||
event: 'step.chunk',
|
||||
ts,
|
||||
status: 'running',
|
||||
stepId,
|
||||
stepAttempt: stepAttempt ?? streamStepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
lane,
|
||||
seq,
|
||||
textDelta: lane === 'text' ? delta : undefined,
|
||||
reasoningDelta: lane === 'reasoning' ? delta : undefined,
|
||||
message,
|
||||
}]
|
||||
}
|
||||
|
||||
if (event.type !== TASK_SSE_EVENT_TYPE.LIFECYCLE) return []
|
||||
|
||||
const runEvents: RunStreamEvent[] = []
|
||||
|
||||
if (lifecycleType === TASK_EVENT_TYPE.CREATED) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'run.start',
|
||||
ts,
|
||||
status: 'running',
|
||||
message,
|
||||
payload,
|
||||
})
|
||||
return runEvents
|
||||
}
|
||||
|
||||
if (lifecycleType === TASK_EVENT_TYPE.PROCESSING) {
|
||||
if (stepId) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'step.start',
|
||||
ts,
|
||||
status: 'running',
|
||||
stepId,
|
||||
stepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
message,
|
||||
})
|
||||
if (done || stageLooksCompleted(stage)) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'step.complete',
|
||||
ts,
|
||||
status: stale ? 'stale' : 'completed',
|
||||
stepId,
|
||||
stepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
text,
|
||||
reasoning,
|
||||
message,
|
||||
})
|
||||
} else if (stageLooksFailed(stage)) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'step.error',
|
||||
ts,
|
||||
status: 'failed',
|
||||
stepId,
|
||||
stepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
message: resolveTaskErrorMessage(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
return runEvents
|
||||
}
|
||||
|
||||
if (lifecycleType === TASK_EVENT_TYPE.COMPLETED) {
|
||||
if (stepId) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'step.complete',
|
||||
ts,
|
||||
status: 'completed',
|
||||
stepId,
|
||||
stepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
text,
|
||||
reasoning,
|
||||
message,
|
||||
})
|
||||
}
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'run.complete',
|
||||
ts,
|
||||
status: 'completed',
|
||||
message,
|
||||
payload: extractTerminalPayload(payload),
|
||||
})
|
||||
return runEvents
|
||||
}
|
||||
|
||||
if (lifecycleType === TASK_EVENT_TYPE.FAILED) {
|
||||
const errorMessage = resolveTaskErrorMessage(payload)
|
||||
if (stepId) {
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'step.error',
|
||||
ts,
|
||||
status: 'failed',
|
||||
stepId,
|
||||
stepAttempt,
|
||||
stepTitle,
|
||||
stepIndex,
|
||||
stepTotal,
|
||||
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
|
||||
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
|
||||
groupId,
|
||||
parallelKey,
|
||||
retryable,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
runEvents.push({
|
||||
runId,
|
||||
event: 'run.error',
|
||||
ts,
|
||||
status: 'failed',
|
||||
message: errorMessage,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
return runEvents
|
||||
}
|
||||
|
||||
export function toTerminalRunResult(event: RunStreamEvent): RunResult | null {
|
||||
if (event.event !== 'run.complete' && event.event !== 'run.error') return null
|
||||
|
||||
const summaryFromPayload =
|
||||
event.payload &&
|
||||
typeof event.payload.summary === 'object' &&
|
||||
event.payload.summary
|
||||
? (event.payload.summary as Record<string, unknown>)
|
||||
: null
|
||||
|
||||
return {
|
||||
runId: event.runId,
|
||||
status: event.event === 'run.complete' ? 'completed' : 'failed',
|
||||
summary:
|
||||
event.event === 'run.complete'
|
||||
? summaryFromPayload || event.payload || null
|
||||
: summaryFromPayload,
|
||||
payload: event.payload || null,
|
||||
errorMessage: event.event === 'run.error' ? event.message || 'run failed' : '',
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user