import { z } from "zod"; const id = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/); const schema = z.object({ schema_version: z.literal("execution-plan@1"), session_id: z.string().min(1), revision: z.number().int().positive(), updated_at: z.string().datetime(), title: z.string().min(1).max(120), reason: z.string().min(1).max(1000), nodes: z.array(z.object({ id, title: z.string().min(1).max(120), status: z.enum(["pending", "running", "waiting", "completed", "failed", "skipped", "cancelled"]), owner: z.enum(["agent", "human"]), detail: z.string().max(2000), evidence: z.array(z.string().min(1).max(1000)).max(12) })).min(1).max(40), edges: z.array(z.object({ from: id, to: id, label: z.string().max(120) })).max(80) }).refine((plan) => { const ids = new Set(plan.nodes.map((node) => node.id)); return ids.size === plan.nodes.length && plan.edges.every((edge) => ids.has(edge.from) && ids.has(edge.to) && edge.from !== edge.to); }); export type AgentExecutionPlan = z.infer; export function parseExecutionPlan(value: unknown, sessionId: string): AgentExecutionPlan | null { const parsed = schema.safeParse(value); return parsed.success && parsed.data.session_id === sessionId && executionPlanStages(parsed.data).length ? parsed.data : null; } export function readExecutionPlanHistory(messages: unknown[], sessionId: string) { const plans = messages.flatMap((message) => { const value = (message as { executionPlans?: unknown } | null)?.executionPlans; const parts = (message as { parts?: unknown } | null)?.parts; const data = Array.isArray(parts) ? parts.filter((part) => part?.type === "data-execution_plan").map((part) => part.data) : []; return [...(Array.isArray(value) ? value : []), ...data]; }).map((value) => parseExecutionPlan(value, sessionId)).filter((plan): plan is AgentExecutionPlan => plan !== null); return [...new Map(plans.map((plan) => [plan.revision, plan])).values()].sort((a, b) => a.revision - b.revision); } export const executionStatusLabels = { pending: "待执行", running: "执行中", waiting: "等待资料/人工", completed: "已完成", failed: "失败", skipped: "不适用", cancelled: "已取消" }; /** Same-stage nodes have no dependency on one another; this describes eligibility, not a scheduler. */ export function executionPlanStages(plan: AgentExecutionPlan): AgentExecutionPlan["nodes"][] { const remaining = new Set(plan.nodes.map((node) => node.id)); const stages: AgentExecutionPlan["nodes"][] = []; while (remaining.size) { const nodes = plan.nodes.filter((node) => remaining.has(node.id) && plan.edges.filter((edge) => edge.to === node.id).every((edge) => !remaining.has(edge.from))); if (!nodes.length) return []; // Malformed cyclic history is never converted into a guessed sequence. stages.push(nodes); nodes.forEach((node) => remaining.delete(node.id)); } return stages; }