@@ -0,0 +1,77 @@
|
||||
// What the run page shows, from the server's first render (`agent.run.status`) and the run's AG-UI
|
||||
// events as `@conusai-kit/agent/client` folds them. Until the first event arrives the seed is the
|
||||
// answer, so the hydrated page matches the server's; from then on the events are: the stream replays
|
||||
// the run from its start, so the view holds every step that began.
|
||||
|
||||
import type { AgentRunView } from "@conusai-kit/agent/client"
|
||||
import type { RunView } from "../contract/index.js"
|
||||
|
||||
const FAILURE_CODES = ["tool_error", "budget_exhausted", "plan_refused", "approval_unavailable", "internal"] as const
|
||||
type FailureCode = (typeof FAILURE_CODES)[number]
|
||||
const isFailureCode = (code: string): code is FailureCode => (FAILURE_CODES as readonly string[]).includes(code)
|
||||
|
||||
export interface LiveRunState {
|
||||
readonly status: RunView["status"]
|
||||
readonly failureCode: RunView["failureCode"]
|
||||
readonly failureDetail: string | null
|
||||
readonly steps: readonly {
|
||||
readonly key: string
|
||||
readonly status: RunView["plan"][number]["status"]
|
||||
readonly error: string | null
|
||||
}[]
|
||||
readonly percent: number
|
||||
/** The AI-generated answer of a completed run. */
|
||||
readonly answer: string | null
|
||||
/**
|
||||
* The answer's provenance (AI Act Art. 50(2)), from the server's render: a run that completes
|
||||
* while the page is open shows its answer marked without it until the page is loaded again.
|
||||
*/
|
||||
readonly answerProvenance: RunView["answerProvenance"]
|
||||
/** The stream could not be read: the page still shows the last state it knew. */
|
||||
readonly streamError: string | null
|
||||
}
|
||||
|
||||
export function liveRunState(seed: RunView, view: AgentRunView): LiveRunState {
|
||||
const streamError = view.error?.source === "transport" ? view.error.code : null
|
||||
if (view.events === 0) {
|
||||
return {
|
||||
status: seed.status,
|
||||
failureCode: seed.failureCode,
|
||||
failureDetail: seed.failureDetail,
|
||||
steps: seed.plan.map((step) => ({ key: step.key, status: step.status, error: step.error })),
|
||||
percent: seed.percent,
|
||||
answer: seed.answer,
|
||||
answerProvenance: seed.answerProvenance,
|
||||
streamError,
|
||||
}
|
||||
}
|
||||
const steps = seed.plan.map((planned) => {
|
||||
const step = view.steps.find((candidate) => candidate.key === planned.key)
|
||||
if (step === undefined || !step.finished) return { key: planned.key, status: "pending" as const, error: null }
|
||||
return { key: planned.key, status: step.outcome ?? ("committed" as const), error: step.error }
|
||||
})
|
||||
const committed = steps.filter((step) => step.status === "committed").length
|
||||
const runError = view.error?.source === "run" ? view.error : null
|
||||
return {
|
||||
status: statusOf(seed, view),
|
||||
failureCode: runError !== null && isFailureCode(runError.code) ? runError.code : null,
|
||||
failureDetail: runError?.message ?? null,
|
||||
steps,
|
||||
percent: seed.plan.length === 0 ? 0 : committed / seed.plan.length,
|
||||
answer: view.phase === "completed" && view.text !== "" ? view.text : null,
|
||||
answerProvenance: view.phase === "completed" ? seed.answerProvenance : null,
|
||||
streamError,
|
||||
}
|
||||
}
|
||||
|
||||
function statusOf(seed: RunView, view: AgentRunView): RunView["status"] {
|
||||
switch (view.phase) {
|
||||
case "connecting":
|
||||
return seed.status
|
||||
// The stream opens every run with RUN_STARTED; a run the worker has not claimed is still queued.
|
||||
case "running":
|
||||
return seed.status === "queued" && view.steps.length === 0 ? "queued" : "running"
|
||||
default:
|
||||
return view.phase
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user