fix(awooop): surface runs ai loop action chain
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m3s
CD Pipeline / build-and-deploy (push) Successful in 4m33s
CD Pipeline / post-deploy-checks (push) Successful in 2m31s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m3s
CD Pipeline / build-and-deploy (push) Successful in 4m33s
CD Pipeline / post-deploy-checks (push) Successful in 2m31s
This commit is contained in:
@@ -60,6 +60,15 @@ type RunState =
|
||||
| "timeout";
|
||||
|
||||
type RunLane = "intake" | "diagnosis" | "approval" | "execution" | "done" | "manual";
|
||||
type AiLoopAgentStatus = "ready" | "queued" | "blocked" | "missing";
|
||||
type AiLoopAgentNodeKey =
|
||||
| "sensor"
|
||||
| "aiLane"
|
||||
| "candidate"
|
||||
| "controlled"
|
||||
| "verifier"
|
||||
| "learning";
|
||||
type AiLoopAgentAssetKey = "km" | "rag" | "playbook" | "mcp" | "verifier" | "aiAgent";
|
||||
type CallbackReplyStatus =
|
||||
| "no_callback"
|
||||
| "sent"
|
||||
@@ -720,6 +729,32 @@ interface AiRouteStatusResponse {
|
||||
checked_at: string;
|
||||
}
|
||||
|
||||
interface AwoooPRunEvidenceSummary {
|
||||
readOnly: number;
|
||||
mcpObserved: number;
|
||||
writeObserved: number;
|
||||
noEvidence: number;
|
||||
controlledQueue: number;
|
||||
callbackObserved: number;
|
||||
callbackFailed: number;
|
||||
}
|
||||
|
||||
interface AiLoopAgentNode {
|
||||
key: AiLoopAgentNodeKey;
|
||||
status: AiLoopAgentStatus;
|
||||
value: string;
|
||||
detail: string;
|
||||
subdetail: string;
|
||||
icon: typeof Activity;
|
||||
}
|
||||
|
||||
interface AiLoopAgentAsset {
|
||||
key: AiLoopAgentAssetKey;
|
||||
value: string;
|
||||
detail: string;
|
||||
status: AiLoopAgentStatus;
|
||||
}
|
||||
|
||||
type GitHubRunReadinessMetric = {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -4338,6 +4373,391 @@ function AiRouteStatusPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function formatAiLoopCount(value?: number | null) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? value.toLocaleString("zh-TW")
|
||||
: "--";
|
||||
}
|
||||
|
||||
function firstAiLoopCount(...values: Array<number | null | undefined>) {
|
||||
const value = values.find((candidate) => typeof candidate === "number" && Number.isFinite(candidate));
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
|
||||
function aiLoopStatusClass(status: AiLoopAgentStatus) {
|
||||
if (status === "ready") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
|
||||
if (status === "queued") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
|
||||
if (status === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
|
||||
}
|
||||
|
||||
function aiLoopStatusFill(status: AiLoopAgentStatus) {
|
||||
if (status === "ready") return "bg-[#4f9d5f]";
|
||||
if (status === "queued") return "bg-[#c58a24]";
|
||||
if (status === "blocked") return "bg-[#c65145]";
|
||||
return "bg-[#b8b2a7]";
|
||||
}
|
||||
|
||||
function statusFromCount(
|
||||
value: number | null | undefined,
|
||||
fallback: AiLoopAgentStatus = "missing"
|
||||
): AiLoopAgentStatus {
|
||||
if (typeof value === "number" && value > 0) return "ready";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function learningBindingReadyCount(
|
||||
registry: AiAlertCardLearningRegistry | null,
|
||||
aliases: string[]
|
||||
): number | null {
|
||||
if (!registry || !Array.isArray(registry.targets)) return null;
|
||||
const targets = registry?.targets ?? [];
|
||||
return targets.reduce((total, binding) => {
|
||||
const target = String(binding.target ?? "").toLowerCase();
|
||||
if (!aliases.some((alias) => target.includes(alias))) return total;
|
||||
return total + (binding.ready_receipt_count ?? (binding.status === "ready" ? 1 : 0));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function AiLoopAgentActionChainPanel({
|
||||
runs,
|
||||
total,
|
||||
evidenceSummary,
|
||||
aiAlertCardSummary,
|
||||
aiAlertLearningRegistry,
|
||||
automationQualitySummary,
|
||||
automationQualityError,
|
||||
callbackAuditSummary,
|
||||
callbackEventsTotal,
|
||||
callbackEventsError,
|
||||
aiAlertCardError,
|
||||
eventRecurrence,
|
||||
}: {
|
||||
runs: Run[];
|
||||
total: number;
|
||||
evidenceSummary: AwoooPRunEvidenceSummary;
|
||||
aiAlertCardSummary: AiAlertCardDeliverySummary | null;
|
||||
aiAlertLearningRegistry: AiAlertCardLearningRegistry | null;
|
||||
automationQualitySummary: AutomationQualitySummary | null;
|
||||
automationQualityError: string | null;
|
||||
callbackAuditSummary: CallbackReplyAuditSummary | null;
|
||||
callbackEventsTotal: number;
|
||||
callbackEventsError: string | null;
|
||||
aiAlertCardError: string | null;
|
||||
eventRecurrence: EventRecurrenceResponse | null;
|
||||
}) {
|
||||
const t = useTranslations("awooop.runs.aiLoopAgent");
|
||||
const sensorTotal = firstAiLoopCount(
|
||||
callbackAuditSummary?.outbound_total,
|
||||
aiAlertCardSummary?.total,
|
||||
eventRecurrence?.summary?.source_event_total,
|
||||
runs.length
|
||||
);
|
||||
const evaluatedTotal = firstAiLoopCount(
|
||||
automationQualitySummary?.evaluated_total,
|
||||
total,
|
||||
runs.length
|
||||
);
|
||||
const candidateTotal = evidenceSummary.mcpObserved + evidenceSummary.readOnly;
|
||||
const controlledQueueTotal = firstAiLoopCount(
|
||||
aiAlertCardSummary?.runtime_write_gate_open_count,
|
||||
evidenceSummary.controlledQueue
|
||||
);
|
||||
const verifierReadyTotal = firstAiLoopCount(
|
||||
automationQualitySummary?.verified_auto_repair_total,
|
||||
aiAlertCardSummary?.post_verifier_ref_ready_total
|
||||
);
|
||||
const learningReadyTotal = firstAiLoopCount(
|
||||
aiAlertLearningRegistry?.ready_target_count,
|
||||
aiAlertCardSummary?.learning_registry_ready_target_count,
|
||||
aiAlertCardSummary?.learning_writeback_ready_total
|
||||
);
|
||||
const learningMissingTotal = firstAiLoopCount(
|
||||
aiAlertLearningRegistry?.missing_target_count,
|
||||
aiAlertCardSummary?.learning_registry_missing_target_count,
|
||||
aiAlertCardSummary?.learning_writeback_missing_total
|
||||
);
|
||||
const hasReadbackError = Boolean(automationQualityError || callbackEventsError || aiAlertCardError);
|
||||
const runtimeControlledAllowed = Boolean(
|
||||
aiAlertCardSummary?.runtime_write_allowed ||
|
||||
(controlledQueueTotal ?? 0) > 0 ||
|
||||
candidateTotal > 0
|
||||
);
|
||||
const callbackTotal = firstAiLoopCount(
|
||||
callbackAuditSummary?.callback_total,
|
||||
callbackEventsTotal
|
||||
);
|
||||
const learningTargetTotal = firstAiLoopCount(
|
||||
aiAlertLearningRegistry?.target_count,
|
||||
aiAlertCardSummary?.learning_registry_target_count
|
||||
);
|
||||
const sourceRefsTotal = firstAiLoopCount(
|
||||
callbackAuditSummary?.outbound_source_refs_total,
|
||||
callbackAuditSummary?.outbound_trace_ref_total,
|
||||
0
|
||||
);
|
||||
const flow = automationQualitySummary?.automation_flow_gates;
|
||||
const flowBlockedCount = flow?.blocked_gates?.length ?? 0;
|
||||
const flowWarningCount = flow?.warning_gates?.length ?? 0;
|
||||
const nodes: AiLoopAgentNode[] = [
|
||||
{
|
||||
key: "sensor",
|
||||
status: callbackEventsError ? "blocked" : statusFromCount(sensorTotal),
|
||||
value: formatAiLoopCount(sensorTotal),
|
||||
detail: t("nodes.sensor.detail", {
|
||||
callbacks: formatAiLoopCount(callbackTotal),
|
||||
cards: formatAiLoopCount(aiAlertCardSummary?.total),
|
||||
}),
|
||||
subdetail: t("nodes.sensor.subdetail", {
|
||||
refs: formatAiLoopCount(sourceRefsTotal),
|
||||
}),
|
||||
icon: Send,
|
||||
},
|
||||
{
|
||||
key: "aiLane",
|
||||
status: automationQualityError ? "blocked" : statusFromCount(evaluatedTotal),
|
||||
value: formatAiLoopCount(evaluatedTotal),
|
||||
detail: t("nodes.aiLane.detail", {
|
||||
mcp: formatAiLoopCount(evidenceSummary.mcpObserved),
|
||||
runs: formatAiLoopCount(runs.length),
|
||||
}),
|
||||
subdetail: t("nodes.aiLane.subdetail", {
|
||||
noEvidence: formatAiLoopCount(evidenceSummary.noEvidence),
|
||||
}),
|
||||
icon: Cpu,
|
||||
},
|
||||
{
|
||||
key: "candidate",
|
||||
status: statusFromCount(candidateTotal, evidenceSummary.noEvidence > 0 ? "queued" : "missing"),
|
||||
value: formatAiLoopCount(candidateTotal),
|
||||
detail: t("nodes.candidate.detail", {
|
||||
dryRun: formatAiLoopCount(evidenceSummary.readOnly),
|
||||
mcp: formatAiLoopCount(evidenceSummary.mcpObserved),
|
||||
}),
|
||||
subdetail: t("nodes.candidate.subdetail", {
|
||||
writeFlags: formatAiLoopCount(evidenceSummary.writeObserved),
|
||||
}),
|
||||
icon: ListChecks,
|
||||
},
|
||||
{
|
||||
key: "controlled",
|
||||
status: runtimeControlledAllowed ? "ready" : "queued",
|
||||
value: formatAiLoopCount(controlledQueueTotal),
|
||||
detail: t("nodes.controlled.detail", {
|
||||
allowed: aiAlertCardSummary?.runtime_write_allowed ? t("values.yes") : t("values.controlled"),
|
||||
queue: formatAiLoopCount(evidenceSummary.controlledQueue),
|
||||
}),
|
||||
subdetail: t("nodes.controlled.subdetail"),
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
key: "verifier",
|
||||
status: statusFromCount(verifierReadyTotal, (evaluatedTotal ?? 0) > 0 ? "queued" : "missing"),
|
||||
value: formatAiLoopCount(verifierReadyTotal),
|
||||
detail: t("nodes.verifier.detail", {
|
||||
verified: formatAiLoopCount(automationQualitySummary?.verified_auto_repair_total),
|
||||
post: formatAiLoopCount(aiAlertCardSummary?.post_verifier_ref_ready_total),
|
||||
}),
|
||||
subdetail: t("nodes.verifier.subdetail", {
|
||||
blocked: formatAiLoopCount(flowBlockedCount),
|
||||
warning: formatAiLoopCount(flowWarningCount),
|
||||
}),
|
||||
icon: SearchCheck,
|
||||
},
|
||||
{
|
||||
key: "learning",
|
||||
status: statusFromCount(learningReadyTotal, (learningMissingTotal ?? 0) > 0 ? "queued" : "missing"),
|
||||
value: formatAiLoopCount(learningReadyTotal),
|
||||
detail: t("nodes.learning.detail", {
|
||||
ready: formatAiLoopCount(learningReadyTotal),
|
||||
targets: formatAiLoopCount(learningTargetTotal),
|
||||
}),
|
||||
subdetail: t("nodes.learning.subdetail", {
|
||||
missing: formatAiLoopCount(learningMissingTotal),
|
||||
}),
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
const readyCount = nodes.filter((node) => node.status === "ready").length;
|
||||
const queuedCount = nodes.filter((node) => node.status === "queued").length;
|
||||
const blockedCount = nodes.filter((node) => node.status === "blocked").length;
|
||||
const overallStatus: AiLoopAgentStatus = blockedCount > 0
|
||||
? "blocked"
|
||||
: readyCount >= 4
|
||||
? "ready"
|
||||
: queuedCount > 0
|
||||
? "queued"
|
||||
: "missing";
|
||||
const kmReady = firstAiLoopCount(
|
||||
aiAlertCardSummary?.km_writeback_ref_ready_total,
|
||||
learningBindingReadyCount(aiAlertLearningRegistry, ["km", "knowledge"])
|
||||
);
|
||||
const ragReady = firstAiLoopCount(
|
||||
aiAlertCardSummary?.rag_chunk_ref_ready_total,
|
||||
learningBindingReadyCount(aiAlertLearningRegistry, ["rag", "vector"])
|
||||
);
|
||||
const playbookReady = firstAiLoopCount(
|
||||
aiAlertCardSummary?.playbook_writeback_ref_ready_total,
|
||||
learningBindingReadyCount(aiAlertLearningRegistry, ["playbook"])
|
||||
);
|
||||
const mcpReady = firstAiLoopCount(
|
||||
aiAlertCardSummary?.mcp_evidence_ref_ready_total,
|
||||
learningBindingReadyCount(aiAlertLearningRegistry, ["mcp"])
|
||||
);
|
||||
const postVerifierReady = firstAiLoopCount(
|
||||
aiAlertCardSummary?.post_verifier_ref_ready_total,
|
||||
learningBindingReadyCount(aiAlertLearningRegistry, ["verifier"])
|
||||
);
|
||||
const aiAgentReady = learningBindingReadyCount(aiAlertLearningRegistry, ["agent", "ai_agent"]);
|
||||
const assets: AiLoopAgentAsset[] = [
|
||||
{
|
||||
key: "km",
|
||||
value: formatAiLoopCount(kmReady),
|
||||
detail: t("assets.km.detail"),
|
||||
status: statusFromCount(kmReady),
|
||||
},
|
||||
{
|
||||
key: "rag",
|
||||
value: formatAiLoopCount(ragReady),
|
||||
detail: t("assets.rag.detail"),
|
||||
status: statusFromCount(ragReady),
|
||||
},
|
||||
{
|
||||
key: "playbook",
|
||||
value: formatAiLoopCount(playbookReady),
|
||||
detail: t("assets.playbook.detail"),
|
||||
status: statusFromCount(playbookReady),
|
||||
},
|
||||
{
|
||||
key: "mcp",
|
||||
value: formatAiLoopCount(mcpReady),
|
||||
detail: t("assets.mcp.detail"),
|
||||
status: statusFromCount(mcpReady),
|
||||
},
|
||||
{
|
||||
key: "verifier",
|
||||
value: formatAiLoopCount(postVerifierReady),
|
||||
detail: t("assets.verifier.detail"),
|
||||
status: statusFromCount(postVerifierReady),
|
||||
},
|
||||
{
|
||||
key: "aiAgent",
|
||||
value: formatAiLoopCount(aiAgentReady),
|
||||
detail: t("assets.aiAgent.detail"),
|
||||
status: statusFromCount(aiAgentReady),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
id="ai-loop-agent-action-chain"
|
||||
data-testid="awooop-runs-ai-loop-agent-chain"
|
||||
className="border border-[#d8d3c7] bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-[#1f5b9b]" aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 border px-2 py-0.5 text-xs font-semibold",
|
||||
aiLoopStatusClass(overallStatus)
|
||||
)}
|
||||
>
|
||||
<span className={cn("h-2 w-2", aiLoopStatusFill(overallStatus))} />
|
||||
{t(`statuses.${overallStatus}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 max-w-4xl text-xs leading-5 text-[#5f5b52]">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span className="border border-[#c8dfcb] bg-[#f4fbf5] px-2 py-1 font-semibold text-[#17602a]">
|
||||
{t("header.ready", { count: readyCount })}
|
||||
</span>
|
||||
<span className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 font-semibold text-[#8a5a08]">
|
||||
{t("header.queued", { count: queuedCount })}
|
||||
</span>
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-1 font-semibold text-[#5f5b52]">
|
||||
{t("header.errors", { count: hasReadbackError ? 1 : 0 })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-6">
|
||||
{nodes.map((node, index) => {
|
||||
const Icon = node.icon;
|
||||
return (
|
||||
<div key={node.key} className="relative min-h-[170px] bg-white px-4 py-4">
|
||||
{index < nodes.length - 1 ? (
|
||||
<ArrowRight
|
||||
className="absolute -right-3 top-1/2 z-10 hidden h-5 w-5 -translate-y-1/2 bg-white text-[#77736a] lg:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-[11px] text-[#77736a]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</p>
|
||||
<h4 className="mt-2 text-xs font-semibold leading-4 text-[#141413]">
|
||||
{t(`nodes.${node.key}.title` as never)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className={cn("flex h-8 w-8 shrink-0 items-center justify-center border", aiLoopStatusClass(node.status))}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-end justify-between gap-3">
|
||||
<span className="font-mono text-2xl font-semibold text-[#141413]">{node.value}</span>
|
||||
<span className={cn("border px-2 py-0.5 text-[11px] font-semibold", aiLoopStatusClass(node.status))}>
|
||||
{t(`statuses.${node.status}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-xs leading-5 text-[#5f5b52]">{node.detail}</p>
|
||||
<p className="mt-1 text-[11px] leading-4 text-[#77736a]">{node.subdetail}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6">
|
||||
{assets.map((asset) => (
|
||||
<div key={asset.key} className="bg-[#fcfbf7] px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#141413]">
|
||||
{t(`assets.${asset.key}.title` as never)}
|
||||
</p>
|
||||
<span className={cn("h-2.5 w-2.5", aiLoopStatusFill(asset.status))} />
|
||||
</div>
|
||||
<div className="mt-2 font-mono text-xl font-semibold text-[#141413]">
|
||||
{asset.value}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-4 text-[#5f5b52]">{asset.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-[#e0ddd4] px-4 py-3 text-xs text-[#5f5b52]">
|
||||
<p className="leading-5">
|
||||
{t("boundary", {
|
||||
runtimeGate: formatAiLoopCount(controlledQueueTotal),
|
||||
missing: formatAiLoopCount(learningMissingTotal),
|
||||
})}
|
||||
</p>
|
||||
<p className="font-mono text-[11px] text-[#77736a]">
|
||||
{t("source", {
|
||||
registry: aiAlertLearningRegistry?.status ?? "--",
|
||||
flow: flow?.overall_status ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Component
|
||||
// =============================================================================
|
||||
@@ -4660,7 +5080,7 @@ export default function RunsPage() {
|
||||
noEvidence: runs.filter(
|
||||
(run) => normalizeRemediationStatus(run.remediation_summary) === "no_evidence"
|
||||
).length,
|
||||
manualGate: runs.filter((run) => run.remediation_summary?.human_gate_open).length,
|
||||
controlledQueue: runs.filter((run) => run.remediation_summary?.human_gate_open).length,
|
||||
callbackObserved: runs.filter(
|
||||
(run) => normalizeCallbackReplyStatus(run.callback_reply_summary) !== "no_callback"
|
||||
).length,
|
||||
@@ -4767,6 +5187,21 @@ export default function RunsPage() {
|
||||
|
||||
<AutonomousRuntimeReceiptPanel mode="compact" />
|
||||
|
||||
<AiLoopAgentActionChainPanel
|
||||
runs={runs}
|
||||
total={total}
|
||||
evidenceSummary={evidenceSummary}
|
||||
aiAlertCardSummary={aiAlertCardSummary}
|
||||
aiAlertLearningRegistry={aiAlertLearningRegistry}
|
||||
automationQualitySummary={automationQualitySummary}
|
||||
automationQualityError={automationQualityError}
|
||||
callbackAuditSummary={callbackAuditSummary}
|
||||
callbackEventsTotal={callbackEventsTotal}
|
||||
callbackEventsError={callbackEventsError}
|
||||
aiAlertCardError={aiAlertCardError}
|
||||
eventRecurrence={eventRecurrence}
|
||||
/>
|
||||
|
||||
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-6">
|
||||
{[
|
||||
{
|
||||
@@ -4785,7 +5220,7 @@ export default function RunsPage() {
|
||||
},
|
||||
{
|
||||
label: tEvidence("summary.manualGate"),
|
||||
value: evidenceSummary.manualGate,
|
||||
value: evidenceSummary.controlledQueue,
|
||||
detail: tEvidence("summary.manualGateDetail"),
|
||||
icon: ShieldCheck,
|
||||
className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
|
||||
Reference in New Issue
Block a user