180 lines
6.4 KiB
TypeScript
180 lines
6.4 KiB
TypeScript
// The agent module's page, mounted by the web shell under `/<org>/<workspace>/<project>/agent`.
|
|
//
|
|
// /agent the run list, the approval queue and the start form
|
|
// /agent/<runId> one run: the plan with per-step progress, the budget, the recovery controls
|
|
//
|
|
// Every read is a procedure through the same caller the API uses. Progress is rendered from the
|
|
// durable step log, so a refresh after a restart tells the truth; the progress bar is a real
|
|
// <progress> element, keyboard-operable controls only.
|
|
|
|
import type { Container, TransportCaller } from "@conusai-kit/core"
|
|
import type { I18n } from "@conusai-kit/i18n"
|
|
import type { ProjectPage } from "@conusai-kit/ui"
|
|
import type { ReactElement } from "react"
|
|
import { type ApprovalView, approvalList, type RunView, runList, runStatus } from "../contract/index.js"
|
|
import { ApprovalQueue } from "./approval-queue.js"
|
|
import { RunControls } from "./run-controls.js"
|
|
import { StartForm } from "./start-form.js"
|
|
|
|
export type AgentPage = ProjectPage<TransportCaller, I18n, Container>
|
|
|
|
export const runStatusKey: Record<RunView["status"], string> = {
|
|
queued: "runs.statusQueued",
|
|
running: "runs.statusRunning",
|
|
awaiting_approval: "runs.statusAwaitingApproval",
|
|
completed: "runs.statusCompleted",
|
|
failed: "runs.statusFailed",
|
|
cancelled: "runs.statusCancelled",
|
|
uncertain: "runs.statusUncertain",
|
|
}
|
|
|
|
const stepStatusKey: Record<RunView["plan"][number]["status"], string> = {
|
|
pending: "runs.stepPending",
|
|
committed: "runs.stepCommitted",
|
|
failed: "runs.stepFailed",
|
|
cancelled: "runs.stepCancelled",
|
|
uncertain: "runs.stepUncertain",
|
|
}
|
|
|
|
export const agentPage: AgentPage = async ({ slugs, segments, caller, i18n }): Promise<ReactElement> => {
|
|
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`agent.${key}`, values)
|
|
const failed = i18n.message("app.failed")
|
|
|
|
const [runs, approvals] = await Promise.all([caller.call(runList, slugs), caller.call(approvalList, slugs)])
|
|
|
|
const startLabels = {
|
|
heading: t("start.heading"),
|
|
goalLabel: t("start.goalLabel"),
|
|
goalHint: t("start.goalHint"),
|
|
planLabel: t("start.planLabel"),
|
|
planHint: t("start.planHint"),
|
|
submit: t("start.submit"),
|
|
starting: t("start.starting"),
|
|
failed,
|
|
}
|
|
const approvalLabels = {
|
|
heading: t("approvals.heading"),
|
|
empty: t("approvals.empty"),
|
|
proposed: t("approvals.proposed"),
|
|
review: t("approvals.review"),
|
|
approve: t("approvals.approve"),
|
|
reject: t("approvals.reject"),
|
|
decided: t("approvals.decided"),
|
|
disclosure: t("approvals.disclosure"),
|
|
failed,
|
|
}
|
|
const controlsLabels = {
|
|
cancel: t("runs.cancel"),
|
|
cancelling: t("runs.cancelling"),
|
|
refresh: t("runs.refresh"),
|
|
failedHint: t("recovery.failedHint"),
|
|
cancelledHint: t("recovery.cancelledHint"),
|
|
uncertainHint: t("recovery.uncertainHint"),
|
|
budgetExhausted: t("recovery.budgetExhausted"),
|
|
approvalUnavailable: t("recovery.approvalUnavailable"),
|
|
toolError: t("recovery.toolError"),
|
|
planRefused: t("recovery.planRefused"),
|
|
internal: t("recovery.internal"),
|
|
}
|
|
|
|
if (segments.length === 0) {
|
|
return (
|
|
<>
|
|
<h1>{t("title")}</h1>
|
|
<p>{t("intro")}</p>
|
|
<ApprovalQueue slugs={slugs} approvals={approvals.approvals as ApprovalView[]} labels={approvalLabels} />
|
|
<StartForm slugs={slugs} labels={startLabels} />
|
|
<section aria-labelledby="runs-heading">
|
|
<h2 id="runs-heading">{t("runs.heading")}</h2>
|
|
{runs.runs.length === 0 ? <p>{t("runs.empty")}</p> : null}
|
|
<table>
|
|
<caption>{t("runs.heading")}</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{t("runs.goal")}</th>
|
|
<th scope="col">{t("runs.status")}</th>
|
|
<th scope="col">{t("runs.progress")}</th>
|
|
<th scope="col">{t("runs.startedBy")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{runs.runs.map((run) => (
|
|
<tr key={run.runId}>
|
|
<td>
|
|
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/agent/${run.runId}`}>{run.goal}</a>
|
|
</td>
|
|
<td>{t(runStatusKey[run.status])}</td>
|
|
<td>
|
|
<progress value={run.percent} max={1}>
|
|
{Math.round(run.percent * 100)}%
|
|
</progress>
|
|
</td>
|
|
<td>{run.actorId}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const [runId, ...rest] = segments
|
|
if (runId === undefined || rest.length > 0) throw notFoundPage()
|
|
const run = await caller.call(runStatus, { ...slugs, runId })
|
|
|
|
return (
|
|
<>
|
|
<p>
|
|
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/agent`}>{t("runs.back")}</a>
|
|
</p>
|
|
<h1>{run.goal}</h1>
|
|
<dl>
|
|
<dt>{t("runs.status")}</dt>
|
|
<dd>{t(runStatusKey[run.status])}</dd>
|
|
<dt>{t("runs.startedBy")}</dt>
|
|
<dd>{run.actorId}</dd>
|
|
<dt>{t("runs.usage")}</dt>
|
|
<dd>{t("runs.usage", { steps: run.usageSteps, cost: run.usageCostMicrocents })}</dd>
|
|
</dl>
|
|
<progress value={run.percent} max={1} aria-label={t("runs.progress")}>
|
|
{Math.round(run.percent * 100)}%
|
|
</progress>
|
|
<RunControls slugs={slugs} run={run} labels={controlsLabels} />
|
|
<section aria-labelledby="plan-heading">
|
|
<h2 id="plan-heading">{t("runs.plan")}</h2>
|
|
<table>
|
|
<caption>{t("runs.plan")}</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{t("runs.step")}</th>
|
|
<th scope="col">{t("runs.tool")}</th>
|
|
<th scope="col">{t("runs.status")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{run.plan.map((step) => (
|
|
<tr key={step.key}>
|
|
<td>{step.label}</td>
|
|
<td>
|
|
<code>{step.tool}</code>
|
|
</td>
|
|
<td>
|
|
{t(stepStatusKey[step.status])}
|
|
{step.attempts > 1 ? ` (${t("runs.attempts", { count: step.attempts })})` : ""}
|
|
{step.error !== null ? `: ${step.error}` : ""}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const notFoundPage = () => {
|
|
// A segment that is not a run id is a page that does not exist.
|
|
return Object.assign(new Error("no such page"), { status: 404 })
|
|
}
|