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

This commit is contained in:
Your Name
2026-07-03 07:41:19 +08:00
parent f81debf683
commit dc4f0ecc8e
4 changed files with 621 additions and 6 deletions

View File

@@ -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]",