115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
'use client'
|
|
import { useEffect, useReducer } from 'react'
|
|
import { createSSEConnection } from '@/lib/sse'
|
|
import { getRun, getRunStreamUrl } from '@/lib/api-client'
|
|
import { AGENT_STEPS } from '@/lib/types/run'
|
|
import type { AgentStep } from '@/lib/types/run'
|
|
import type { RunStreamState, TokenCount } from '../types'
|
|
|
|
const zeroTokens = (): TokenCount => ({ in: 0, out: 0 })
|
|
|
|
const initialState: RunStreamState = {
|
|
status: 'connecting',
|
|
steps: Object.fromEntries(AGENT_STEPS.map((s) => [s, 'pending'])) as RunStreamState['steps'],
|
|
reports: Object.fromEntries(AGENT_STEPS.map((s) => [s, []])) as RunStreamState['reports'],
|
|
tokensByStep: Object.fromEntries(AGENT_STEPS.map((s) => [s, zeroTokens()])) as RunStreamState['tokensByStep'],
|
|
tokensTotal: zeroTokens(),
|
|
verdict: null,
|
|
error: null,
|
|
}
|
|
|
|
type Action =
|
|
| { type: 'AGENT_START'; step: AgentStep; turn: number }
|
|
| { type: 'AGENT_COMPLETE'; step: AgentStep; turn: number; report: string; tokens_in?: number; tokens_out?: number }
|
|
| { type: 'RUN_COMPLETE'; decision: string }
|
|
| { type: 'RUN_ERROR'; message: string }
|
|
| { type: 'CONNECTED' }
|
|
|
|
function reducer(state: RunStreamState, action: Action): RunStreamState {
|
|
switch (action.type) {
|
|
case 'CONNECTED':
|
|
return { ...state, status: 'running' }
|
|
|
|
case 'AGENT_START':
|
|
if (state.steps[action.step] !== 'pending') return state
|
|
return { ...state, steps: { ...state.steps, [action.step]: 'running' } }
|
|
|
|
case 'AGENT_COMPLETE': {
|
|
const dIn = action.tokens_in ?? 0
|
|
const dOut = action.tokens_out ?? 0
|
|
const prev = state.tokensByStep[action.step] ?? zeroTokens()
|
|
return {
|
|
...state,
|
|
steps: { ...state.steps, [action.step]: 'done' },
|
|
reports: {
|
|
...state.reports,
|
|
[action.step]: [...(state.reports[action.step] ?? []), action.report],
|
|
},
|
|
tokensByStep: {
|
|
...state.tokensByStep,
|
|
[action.step]: { in: prev.in + dIn, out: prev.out + dOut },
|
|
},
|
|
tokensTotal: {
|
|
in: state.tokensTotal.in + dIn,
|
|
out: state.tokensTotal.out + dOut,
|
|
},
|
|
}
|
|
}
|
|
|
|
case 'RUN_COMPLETE':
|
|
return { ...state, status: 'complete', verdict: action.decision as RunStreamState['verdict'] }
|
|
|
|
case 'RUN_ERROR':
|
|
return { ...state, status: 'error', error: action.message }
|
|
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
export function useRunStream(runId: string): RunStreamState {
|
|
const [state, dispatch] = useReducer(reducer, initialState)
|
|
|
|
useEffect(() => {
|
|
let close: (() => void) | undefined
|
|
let aborted = false
|
|
|
|
getRun(runId).then((run) => {
|
|
if (aborted) return
|
|
|
|
if (run.status === 'complete' && run.reports) {
|
|
dispatch({ type: 'CONNECTED' })
|
|
for (const [key, report] of Object.entries(run.reports)) {
|
|
const lastColon = key.lastIndexOf(':')
|
|
const step = key.slice(0, lastColon) as AgentStep
|
|
const turn = parseInt(key.slice(lastColon + 1), 10)
|
|
const tok = run.token_usage?.[key] ?? { tokens_in: 0, tokens_out: 0 }
|
|
dispatch({ type: 'AGENT_START', step, turn })
|
|
dispatch({ type: 'AGENT_COMPLETE', step, turn, report,
|
|
tokens_in: tok.tokens_in, tokens_out: tok.tokens_out })
|
|
}
|
|
dispatch({ type: 'RUN_COMPLETE', decision: run.decision ?? 'HOLD' })
|
|
return
|
|
}
|
|
|
|
const url = getRunStreamUrl(runId)
|
|
close = createSSEConnection(url, {
|
|
onOpen: () => dispatch({ type: 'CONNECTED' }),
|
|
onAgentStart: ({ step, turn }) =>
|
|
dispatch({ type: 'AGENT_START', step: step as AgentStep, turn }),
|
|
onAgentComplete: ({ step, turn, report, tokens_in, tokens_out }) =>
|
|
dispatch({ type: 'AGENT_COMPLETE', step: step as AgentStep, turn, report,
|
|
tokens_in, tokens_out }),
|
|
onRunComplete: ({ decision }) => dispatch({ type: 'RUN_COMPLETE', decision }),
|
|
onRunError: ({ message }) => dispatch({ type: 'RUN_ERROR', message }),
|
|
})
|
|
}).catch(() => {
|
|
if (!aborted) dispatch({ type: 'RUN_ERROR', message: 'Failed to load run' })
|
|
})
|
|
|
|
return () => { aborted = true; close?.() }
|
|
}, [runId])
|
|
|
|
return state
|
|
}
|