58 lines
2.9 KiB
TypeScript
58 lines
2.9 KiB
TypeScript
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<typeof schema>;
|
|
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;
|
|
}
|