FN-5761: add workflow IR parsing and serialization contract
Implement the v1 workflow IR contract in core with validation, serialization, and coverage. - add workflow IR type definitions, parser, serializer, and structured validation errors in @fusion/core - export workflow IR APIs from core index and add comprehensive unit tests for valid/invalid shapes and metadata parsing - document the workflow IR contract in workflow step docs and include release tracking via changesets, including plugin-sdk DOM lib prerequisite for Windows compilation Files changed: .changeset/fn-5761-workflow-ir-contract.md | 5 + .changeset/windows-release-plugin-sdk-dom-lib.md | 5 + docs/workflow-steps.md | 24 +++ packages/core/src/__tests__/workflow-ir.test.ts | 70 +++++++ packages/core/src/index.ts | 15 ++ packages/core/src/workflow-ir-types.ts | 103 ++++++++++ packages/core/src/workflow-ir.ts | 244 +++++++++++++++++++++++ packages/plugin-sdk/tsconfig.json | 7 +- 8 files changed, 472 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-5761 Fusion-Task-Lineage: 2cd5491e-89c8-418c-a913-76d0c13b466c
This commit is contained in:
70
packages/core/src/__tests__/workflow-ir.test.ts
Normal file
70
packages/core/src/__tests__/workflow-ir.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BUILTIN_WORKFLOW_IR_FIXTURE,
|
||||
WORKFLOW_IR_SCHEMA_VERSION,
|
||||
parseWorkflowIr,
|
||||
serializeWorkflowIr,
|
||||
WorkflowIrError,
|
||||
} from "../index.js";
|
||||
|
||||
describe("workflow ir", () => {
|
||||
it("round-trips fixture with no data loss", () => {
|
||||
const serialized = serializeWorkflowIr(BUILTIN_WORKFLOW_IR_FIXTURE);
|
||||
const parsed = parseWorkflowIr(serialized);
|
||||
expect(parsed).toEqual(BUILTIN_WORKFLOW_IR_FIXTURE);
|
||||
});
|
||||
|
||||
it("rejects missing or mismatched schemaVersion", () => {
|
||||
expect(() =>
|
||||
parseWorkflowIr({
|
||||
metadata: { name: "missing-version" },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}),
|
||||
).toThrowError(expect.objectContaining({ code: "unsupported_version" }));
|
||||
|
||||
expect(() =>
|
||||
parseWorkflowIr({
|
||||
schemaVersion: "2.0.0",
|
||||
metadata: { name: "wrong-version" },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}),
|
||||
).toThrowError(expect.objectContaining({ code: "unsupported_version" }));
|
||||
});
|
||||
|
||||
it("rejects unknown node kinds and dangling edges", () => {
|
||||
expect(() =>
|
||||
parseWorkflowIr({
|
||||
schemaVersion: WORKFLOW_IR_SCHEMA_VERSION,
|
||||
metadata: { name: "unknown-kind" },
|
||||
nodes: [{ id: "n1", kind: "custom" }],
|
||||
edges: [],
|
||||
}),
|
||||
).toThrowError(WorkflowIrError);
|
||||
|
||||
expect(() =>
|
||||
parseWorkflowIr({
|
||||
schemaVersion: WORKFLOW_IR_SCHEMA_VERSION,
|
||||
metadata: { name: "dangling-edge" },
|
||||
nodes: [{ id: "start", kind: "start" }],
|
||||
edges: [{ id: "e1", from: "start", to: "missing" }],
|
||||
}),
|
||||
).toThrowError(expect.objectContaining({ code: "dangling_edge" }));
|
||||
});
|
||||
|
||||
it("parses fixture JSON string for interpreter parity", () => {
|
||||
const json = JSON.stringify(BUILTIN_WORKFLOW_IR_FIXTURE);
|
||||
const parsed = parseWorkflowIr(json);
|
||||
|
||||
expect(parsed.schemaVersion).toBe(WORKFLOW_IR_SCHEMA_VERSION);
|
||||
expect(parsed.nodes.length).toBeGreaterThanOrEqual(1);
|
||||
expect(parsed.edges.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("exposes workflow ir surface from package entry", () => {
|
||||
expect(typeof parseWorkflowIr).toBe("function");
|
||||
expect(typeof serializeWorkflowIr).toBe("function");
|
||||
expect(BUILTIN_WORKFLOW_IR_FIXTURE.schemaVersion).toBe(WORKFLOW_IR_SCHEMA_VERSION);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,21 @@
|
||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, CompletionHandoffMarker, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY } from "./types.js";
|
||||
export { WORKFLOW_IR_SCHEMA_VERSION, WORKFLOW_IR_NODE_KINDS } from "./workflow-ir-types.js";
|
||||
export type {
|
||||
WorkflowIr,
|
||||
WorkflowIrNode,
|
||||
WorkflowIrEdge,
|
||||
WorkflowIrMetadata,
|
||||
WorkflowIrNodeKind,
|
||||
WorkflowIrSchemaVersion,
|
||||
} from "./workflow-ir-types.js";
|
||||
export {
|
||||
parseWorkflowIr,
|
||||
serializeWorkflowIr,
|
||||
WorkflowIrError,
|
||||
BUILTIN_WORKFLOW_IR_FIXTURE,
|
||||
} from "./workflow-ir.js";
|
||||
export { customProviderRegistryKey } from "./custom-provider-key.js";
|
||||
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
||||
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
|
||||
|
||||
103
packages/core/src/workflow-ir-types.ts
Normal file
103
packages/core/src/workflow-ir-types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Workflow IR schema version supported by this runtime.
|
||||
*/
|
||||
export const WORKFLOW_IR_SCHEMA_VERSION = "1.0.0" as const;
|
||||
|
||||
/**
|
||||
* Supported workflow IR schema version literal.
|
||||
*/
|
||||
export type WorkflowIrSchemaVersion = typeof WORKFLOW_IR_SCHEMA_VERSION;
|
||||
|
||||
/**
|
||||
* Built-in workflow IR node kinds available in v1.
|
||||
*/
|
||||
export const WORKFLOW_IR_NODE_KINDS = [
|
||||
"start",
|
||||
"prompt",
|
||||
"script",
|
||||
"gate",
|
||||
"end",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Union of allowed built-in workflow IR node kinds.
|
||||
*/
|
||||
export type WorkflowIrNodeKind = typeof WORKFLOW_IR_NODE_KINDS[number];
|
||||
|
||||
/**
|
||||
* JSON-serializable primitive value.
|
||||
*/
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
/**
|
||||
* JSON-serializable value.
|
||||
*/
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
|
||||
/**
|
||||
* JSON object map for serializable structures.
|
||||
*/
|
||||
export interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON array for serializable structures.
|
||||
*/
|
||||
export type JsonArray = JsonValue[];
|
||||
|
||||
/**
|
||||
* Workflow IR node.
|
||||
*/
|
||||
export interface WorkflowIrNode {
|
||||
/** Stable node identifier unique within a workflow document. */
|
||||
id: string;
|
||||
/** Built-in workflow node kind. */
|
||||
kind: WorkflowIrNodeKind;
|
||||
/** Optional human-readable label for editor/interpreter diagnostics. */
|
||||
label?: string;
|
||||
/** JSON-safe node configuration payload. */
|
||||
config?: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow IR edge connecting two nodes.
|
||||
*/
|
||||
export interface WorkflowIrEdge {
|
||||
/** Stable edge identifier unique within a workflow document. */
|
||||
id: string;
|
||||
/** Source node id. */
|
||||
from: string;
|
||||
/** Target node id. */
|
||||
to: string;
|
||||
/** Optional condition expression controlling edge traversal. */
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow IR metadata.
|
||||
*/
|
||||
export interface WorkflowIrMetadata {
|
||||
/** Workflow name. */
|
||||
name: string;
|
||||
/** Optional workflow description. */
|
||||
description?: string;
|
||||
/** Optional ISO-8601 creation timestamp string. */
|
||||
createdAt?: string;
|
||||
/** Additional JSON-safe metadata fields. */
|
||||
[k: string]: JsonValue | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Versioned, serializable workflow intermediate representation.
|
||||
*/
|
||||
export interface WorkflowIr {
|
||||
/** Schema version tag for compatibility checks. */
|
||||
schemaVersion: WorkflowIrSchemaVersion;
|
||||
/** Workflow metadata payload. */
|
||||
metadata: WorkflowIrMetadata;
|
||||
/** Workflow node list. */
|
||||
nodes: WorkflowIrNode[];
|
||||
/** Workflow edge list. */
|
||||
edges: WorkflowIrEdge[];
|
||||
}
|
||||
244
packages/core/src/workflow-ir.ts
Normal file
244
packages/core/src/workflow-ir.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
WORKFLOW_IR_NODE_KINDS,
|
||||
WORKFLOW_IR_SCHEMA_VERSION,
|
||||
type JsonValue,
|
||||
type WorkflowIr,
|
||||
type WorkflowIrEdge,
|
||||
type WorkflowIrMetadata,
|
||||
type WorkflowIrNode,
|
||||
type WorkflowIrNodeKind,
|
||||
} from "./workflow-ir-types.js";
|
||||
|
||||
const WORKFLOW_IR_NODE_KIND_SET = new Set<WorkflowIrNodeKind>(WORKFLOW_IR_NODE_KINDS);
|
||||
|
||||
type WorkflowIrErrorCode = "unsupported_version" | "invalid_shape" | "dangling_edge";
|
||||
|
||||
/**
|
||||
* Error thrown when workflow IR parsing or validation fails.
|
||||
*/
|
||||
export class WorkflowIrError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: WorkflowIrErrorCode,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "WorkflowIrError";
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isJsonValue(value: unknown): value is JsonValue {
|
||||
if (value === null) return true;
|
||||
const valueType = typeof value;
|
||||
if (valueType === "string" || valueType === "number" || valueType === "boolean") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.every((entry) => isJsonValue(entry));
|
||||
}
|
||||
if (!isObjectRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(value).every((entry) => isJsonValue(entry));
|
||||
}
|
||||
|
||||
function parseNode(input: unknown, index: number): WorkflowIrNode {
|
||||
if (!isObjectRecord(input)) {
|
||||
throw new WorkflowIrError(`Node at index ${index} must be an object.`, "invalid_shape");
|
||||
}
|
||||
const { id, kind, label, config } = input;
|
||||
if (typeof id !== "string" || id.length === 0) {
|
||||
throw new WorkflowIrError(`Node at index ${index} has an invalid id.`, "invalid_shape");
|
||||
}
|
||||
if (typeof kind !== "string" || !WORKFLOW_IR_NODE_KIND_SET.has(kind as WorkflowIrNodeKind)) {
|
||||
throw new WorkflowIrError(`Node ${id} has unknown kind: ${String(kind)}.`, "invalid_shape");
|
||||
}
|
||||
const parsedKind = kind as WorkflowIrNodeKind;
|
||||
if (label !== undefined && typeof label !== "string") {
|
||||
throw new WorkflowIrError(`Node ${id} label must be a string when provided.`, "invalid_shape");
|
||||
}
|
||||
if (config !== undefined) {
|
||||
if (!isObjectRecord(config) || !isJsonValue(config)) {
|
||||
throw new WorkflowIrError(`Node ${id} config must be a JSON-serializable object.`, "invalid_shape");
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
kind: parsedKind,
|
||||
...(label !== undefined ? { label } : {}),
|
||||
...(config !== undefined ? { config: config as Record<string, JsonValue> } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseEdge(input: unknown, index: number): WorkflowIrEdge {
|
||||
if (!isObjectRecord(input)) {
|
||||
throw new WorkflowIrError(`Edge at index ${index} must be an object.`, "invalid_shape");
|
||||
}
|
||||
const { id, from, to, condition } = input;
|
||||
if (typeof id !== "string" || id.length === 0) {
|
||||
throw new WorkflowIrError(`Edge at index ${index} has an invalid id.`, "invalid_shape");
|
||||
}
|
||||
if (typeof from !== "string" || from.length === 0 || typeof to !== "string" || to.length === 0) {
|
||||
throw new WorkflowIrError(`Edge ${id} must include non-empty from/to node ids.`, "invalid_shape");
|
||||
}
|
||||
if (condition !== undefined && typeof condition !== "string") {
|
||||
throw new WorkflowIrError(`Edge ${id} condition must be a string when provided.`, "invalid_shape");
|
||||
}
|
||||
return {
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
...(condition !== undefined ? { condition } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseMetadata(input: unknown): WorkflowIrMetadata {
|
||||
if (!isObjectRecord(input)) {
|
||||
throw new WorkflowIrError("Workflow metadata must be an object.", "invalid_shape");
|
||||
}
|
||||
if (typeof input.name !== "string" || input.name.length === 0) {
|
||||
throw new WorkflowIrError("Workflow metadata.name must be a non-empty string.", "invalid_shape");
|
||||
}
|
||||
if (input.description !== undefined && typeof input.description !== "string") {
|
||||
throw new WorkflowIrError("Workflow metadata.description must be a string when provided.", "invalid_shape");
|
||||
}
|
||||
if (input.createdAt !== undefined && typeof input.createdAt !== "string") {
|
||||
throw new WorkflowIrError("Workflow metadata.createdAt must be a string when provided.", "invalid_shape");
|
||||
}
|
||||
|
||||
const metadata: WorkflowIrMetadata = {
|
||||
name: input.name,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined) continue;
|
||||
if (!isJsonValue(value)) {
|
||||
throw new WorkflowIrError(`Workflow metadata.${key} must be JSON-serializable.`, "invalid_shape");
|
||||
}
|
||||
metadata[key] = value;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate workflow IR from a JSON string or object.
|
||||
*/
|
||||
export function parseWorkflowIr(input: unknown): WorkflowIr {
|
||||
const parsedInput =
|
||||
typeof input === "string"
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(input) as unknown;
|
||||
} catch {
|
||||
throw new WorkflowIrError("Workflow IR JSON could not be parsed.", "invalid_shape");
|
||||
}
|
||||
})()
|
||||
: input;
|
||||
|
||||
if (!isObjectRecord(parsedInput)) {
|
||||
throw new WorkflowIrError("Workflow IR must be an object.", "invalid_shape");
|
||||
}
|
||||
|
||||
if (parsedInput.schemaVersion !== WORKFLOW_IR_SCHEMA_VERSION) {
|
||||
throw new WorkflowIrError(
|
||||
`Unsupported workflow IR schemaVersion: ${String(parsedInput.schemaVersion)}.`,
|
||||
"unsupported_version",
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsedInput.nodes) || !Array.isArray(parsedInput.edges)) {
|
||||
throw new WorkflowIrError("Workflow IR nodes and edges must be arrays.", "invalid_shape");
|
||||
}
|
||||
|
||||
const metadata = parseMetadata(parsedInput.metadata);
|
||||
const nodes = parsedInput.nodes.map((node, index) => parseNode(node, index));
|
||||
const edges = parsedInput.edges.map((edge, index) => parseEdge(edge, index));
|
||||
|
||||
const nodeIds = new Set(nodes.map((node) => node.id));
|
||||
for (const edge of edges) {
|
||||
if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) {
|
||||
throw new WorkflowIrError(
|
||||
`Edge ${edge.id} references missing node ids: ${edge.from} -> ${edge.to}.`,
|
||||
"dangling_edge",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: WORKFLOW_IR_SCHEMA_VERSION,
|
||||
metadata,
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize workflow IR as JSON.
|
||||
*/
|
||||
export function serializeWorkflowIr(ir: WorkflowIr): string {
|
||||
return JSON.stringify(ir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical built-in workflow IR fixture for v1 interpreter parity tests.
|
||||
*/
|
||||
export const BUILTIN_WORKFLOW_IR_FIXTURE: WorkflowIr = {
|
||||
schemaVersion: WORKFLOW_IR_SCHEMA_VERSION,
|
||||
metadata: {
|
||||
name: "Documentation Review Workflow",
|
||||
description: "Built-in documentation review path using prompt and gate nodes.",
|
||||
createdAt: "2026-05-30T00:00:00.000Z",
|
||||
templateId: "documentation-review",
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: "node-start",
|
||||
kind: "start",
|
||||
label: "Start",
|
||||
},
|
||||
{
|
||||
id: "node-prompt-review",
|
||||
kind: "prompt",
|
||||
label: "Run documentation review prompt",
|
||||
config: {
|
||||
promptTemplate: "documentation-review",
|
||||
reviewLevel: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "node-gate-approval",
|
||||
kind: "gate",
|
||||
label: "Review approved?",
|
||||
config: {
|
||||
mode: "approval",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "node-end",
|
||||
kind: "end",
|
||||
label: "Finish",
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: "edge-start-to-prompt",
|
||||
from: "node-start",
|
||||
to: "node-prompt-review",
|
||||
},
|
||||
{
|
||||
id: "edge-prompt-to-gate",
|
||||
from: "node-prompt-review",
|
||||
to: "node-gate-approval",
|
||||
},
|
||||
{
|
||||
id: "edge-gate-to-end",
|
||||
from: "node-gate-approval",
|
||||
to: "node-end",
|
||||
condition: "approved",
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user