@doclo/flows 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -468,7 +468,7 @@ type SerializableFlow = {
468
468
  /**
469
469
  * Serializable step definition
470
470
  */
471
- type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep;
471
+ type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep | SerializableRouteStep;
472
472
  /**
473
473
  * Standard sequential step
474
474
  */
@@ -514,6 +514,56 @@ type SerializableForEachStep = {
514
514
  config: SplitConfig;
515
515
  itemFlow: SerializableFlow | FlowReference;
516
516
  };
517
+ /**
518
+ * Route branch configuration
519
+ * Maps multiple MIME types to a single branch
520
+ */
521
+ type RouteBranchConfig = {
522
+ /**
523
+ * MIME types that route to this branch.
524
+ * Supports exact matches (e.g., 'application/pdf')
525
+ * and glob patterns (e.g., 'image/*')
526
+ */
527
+ mimeTypes: string[];
528
+ /**
529
+ * Optional description for documentation/debugging
530
+ */
531
+ description?: string;
532
+ };
533
+ /**
534
+ * Route step configuration
535
+ * Routes documents based on detected MIME type (no provider required)
536
+ */
537
+ type RouteConfig = {
538
+ type: 'route';
539
+ /**
540
+ * Branch definitions mapping branch names to MIME type configurations.
541
+ * Order matters for matching - first matching branch wins.
542
+ */
543
+ branches: Record<string, RouteBranchConfig>;
544
+ };
545
+ /**
546
+ * Route step (MIME-based routing + branches)
547
+ *
548
+ * Unlike conditional (which uses VLM categorization), route uses
549
+ * deterministic MIME detection from magic bytes - no provider needed.
550
+ *
551
+ * Branches can be either inline flows or references to separate flows.
552
+ * Use references to avoid hitting database JSON nesting limits.
553
+ */
554
+ type SerializableRouteStep = {
555
+ type: 'route';
556
+ id: string;
557
+ name?: string;
558
+ nodeType: 'route';
559
+ config: RouteConfig;
560
+ branches: Record<string, SerializableFlow | FlowReference>;
561
+ /**
562
+ * Fallback branch for unmatched MIME types.
563
+ * If not provided and no branch matches, throws error with details.
564
+ */
565
+ others?: SerializableFlow | FlowReference;
566
+ };
517
567
  /**
518
568
  * Input mapping configuration for trigger nodes
519
569
  * Declarative alternatives to mapInput functions (for serialization)
@@ -555,6 +605,9 @@ type ParseConfig = {
555
605
  onTie?: 'random' | 'fail' | 'retry';
556
606
  };
557
607
  maxTokens?: number;
608
+ promptRef?: string;
609
+ promptVariables?: Record<string, any>;
610
+ additionalInstructions?: string;
558
611
  };
559
612
  type ExtractConfig = {
560
613
  type: 'extract';
@@ -571,6 +624,9 @@ type ExtractConfig = {
571
624
  max_tokens?: number;
572
625
  };
573
626
  maxTokens?: number;
627
+ promptRef?: string;
628
+ promptVariables?: Record<string, any>;
629
+ additionalInstructions?: string;
574
630
  };
575
631
  type SplitConfig = {
576
632
  type: 'split';
@@ -606,6 +662,8 @@ type CategorizeConfig = {
606
662
  onTie?: 'random' | 'fail' | 'retry';
607
663
  };
608
664
  promptRef?: string;
665
+ promptVariables?: Record<string, any>;
666
+ additionalInstructions?: string;
609
667
  maxTokens?: number;
610
668
  };
611
669
  type TriggerConfig = {
@@ -781,6 +839,26 @@ interface ForEachCompositeConfig {
781
839
  * Includes full observability, metrics merging, and error context.
782
840
  */
783
841
  declare function createForEachCompositeNode(config: ForEachCompositeConfig): NodeDef<FlowInput, unknown[]>;
842
+ /**
843
+ * Configuration for route composite node
844
+ */
845
+ interface RouteCompositeConfig {
846
+ stepId: string;
847
+ routeConfig: RouteConfig;
848
+ branches: Record<string, SerializableFlow | FlowReference>;
849
+ others?: SerializableFlow | FlowReference;
850
+ providers: ProviderRegistry;
851
+ flows: FlowRegistry;
852
+ }
853
+ /**
854
+ * Creates a composite node that:
855
+ * 1. Detects MIME type from input (base64 or URL)
856
+ * 2. Routes to appropriate branch based on MIME type
857
+ * 3. Returns the branch flow's output
858
+ *
859
+ * No provider required - uses deterministic MIME detection from magic bytes.
860
+ */
861
+ declare function createRouteCompositeNode(config: RouteCompositeConfig): NodeDef<FlowInput, unknown>;
784
862
 
785
863
  /**
786
864
  * Flow Validation
@@ -961,4 +1039,4 @@ declare function buildTwoProviderFlow(opts: {
961
1039
  }>;
962
1040
  };
963
1041
 
964
- export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
1042
+ export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type RouteBranchConfig, type RouteCompositeConfig, type RouteConfig, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableRouteStep, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, createRouteCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
package/dist/index.js CHANGED
@@ -53,6 +53,15 @@ function normalizeFlowInput(input) {
53
53
  }
54
54
  return input;
55
55
  }
56
+ function filterInternalArtifacts(artifacts) {
57
+ const filtered = {};
58
+ for (const [key, value] of Object.entries(artifacts)) {
59
+ if (!key.startsWith("__")) {
60
+ filtered[key] = value;
61
+ }
62
+ }
63
+ return filtered;
64
+ }
56
65
  var Flow = class {
57
66
  steps = [];
58
67
  observability;
@@ -1051,7 +1060,7 @@ Correct: .conditional('step', () => parse({ provider }))`
1051
1060
  results: flowResults,
1052
1061
  metrics,
1053
1062
  aggregated: aggregateMetrics(metrics),
1054
- artifacts
1063
+ artifacts: filterInternalArtifacts(artifacts)
1055
1064
  };
1056
1065
  }
1057
1066
  } catch (error) {
@@ -1115,7 +1124,7 @@ Correct: .conditional('step', () => parse({ provider }))`
1115
1124
  outputs,
1116
1125
  metrics,
1117
1126
  aggregated: aggregateMetrics(metrics),
1118
- artifacts
1127
+ artifacts: filterInternalArtifacts(artifacts)
1119
1128
  };
1120
1129
  } else {
1121
1130
  result = {
@@ -1124,7 +1133,7 @@ Correct: .conditional('step', () => parse({ provider }))`
1124
1133
  outputs,
1125
1134
  metrics,
1126
1135
  aggregated: aggregateMetrics(metrics),
1127
- artifacts
1136
+ artifacts: filterInternalArtifacts(artifacts)
1128
1137
  };
1129
1138
  }
1130
1139
  } else {
@@ -1132,7 +1141,7 @@ Correct: .conditional('step', () => parse({ provider }))`
1132
1141
  output: currentData,
1133
1142
  metrics,
1134
1143
  aggregated: aggregateMetrics(metrics),
1135
- artifacts
1144
+ artifacts: filterInternalArtifacts(artifacts)
1136
1145
  };
1137
1146
  }
1138
1147
  if (this.observability && sampled && traceContext && executionId) {
@@ -1243,6 +1252,7 @@ import { parse, extract, split as split2, categorize as categorize2, trigger, ou
1243
1252
 
1244
1253
  // src/composite-nodes.ts
1245
1254
  import { FlowExecutionError as FlowExecutionError2 } from "@doclo/core";
1255
+ import { detectMimeTypeFromBase64Async, detectMimeTypeFromBase64 } from "@doclo/core";
1246
1256
  import { categorize, split } from "@doclo/nodes";
1247
1257
  function parseProviderName(name) {
1248
1258
  const colonIndex = name.indexOf(":");
@@ -1622,6 +1632,214 @@ function resolveBranchFlow(flowOrRef, flows) {
1622
1632
  }
1623
1633
  return flowOrRef;
1624
1634
  }
1635
+ function matchesMimePattern(mimeType, pattern) {
1636
+ if (pattern === mimeType) return true;
1637
+ if (pattern.endsWith("/*")) {
1638
+ const prefix = pattern.slice(0, -2);
1639
+ return mimeType.startsWith(prefix + "/");
1640
+ }
1641
+ return false;
1642
+ }
1643
+ function findMatchingBranch(mimeType, branches) {
1644
+ for (const [branchName, config] of Object.entries(branches)) {
1645
+ for (const pattern of config.mimeTypes) {
1646
+ if (matchesMimePattern(mimeType, pattern)) {
1647
+ return branchName;
1648
+ }
1649
+ }
1650
+ }
1651
+ return void 0;
1652
+ }
1653
+ function extractBase64Data(input) {
1654
+ if (input.startsWith("data:")) {
1655
+ const commaIndex = input.indexOf(",");
1656
+ if (commaIndex !== -1) {
1657
+ return input.substring(commaIndex + 1);
1658
+ }
1659
+ }
1660
+ return input;
1661
+ }
1662
+ function createRouteCompositeNode(config) {
1663
+ const { stepId, routeConfig, branches, others, providers, flows } = config;
1664
+ return {
1665
+ key: "route-composite",
1666
+ run: async (input, ctx) => {
1667
+ const t0 = Date.now();
1668
+ let detectedMimeType;
1669
+ let selectedBranch;
1670
+ let phase = "detect";
1671
+ try {
1672
+ let base64Data;
1673
+ if (input.base64) {
1674
+ base64Data = extractBase64Data(input.base64);
1675
+ } else if (input.url) {
1676
+ const response = await fetch(input.url);
1677
+ if (!response.ok) {
1678
+ throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
1679
+ }
1680
+ const arrayBuffer = await response.arrayBuffer();
1681
+ const bytes = new Uint8Array(arrayBuffer);
1682
+ let binary = "";
1683
+ for (let i = 0; i < bytes.byteLength; i++) {
1684
+ binary += String.fromCharCode(bytes[i]);
1685
+ }
1686
+ base64Data = btoa(binary);
1687
+ } else {
1688
+ throw new Error(
1689
+ "Route node requires either url or base64 input for MIME detection"
1690
+ );
1691
+ }
1692
+ try {
1693
+ detectedMimeType = await detectMimeTypeFromBase64Async(base64Data);
1694
+ } catch {
1695
+ try {
1696
+ detectedMimeType = detectMimeTypeFromBase64(base64Data);
1697
+ } catch (syncErr) {
1698
+ throw new Error(
1699
+ `Unable to detect MIME type from input. Ensure the document is a supported format. Error: ${syncErr instanceof Error ? syncErr.message : String(syncErr)}`
1700
+ );
1701
+ }
1702
+ }
1703
+ if (ctx?.emit) {
1704
+ ctx.emit(`${stepId}:detectedMimeType`, detectedMimeType);
1705
+ }
1706
+ phase = "route";
1707
+ selectedBranch = findMatchingBranch(detectedMimeType, routeConfig.branches);
1708
+ if (!selectedBranch && others) {
1709
+ selectedBranch = "others";
1710
+ }
1711
+ if (!selectedBranch) {
1712
+ const availableBranches = Object.entries(routeConfig.branches).map(([name, cfg]) => `${name}: [${cfg.mimeTypes.join(", ")}]`).join("; ");
1713
+ throw new Error(
1714
+ `No branch matches MIME type "${detectedMimeType}". Available branches: ${availableBranches}. Add an 'others' fallback branch to handle unmatched types.`
1715
+ );
1716
+ }
1717
+ if (ctx?.emit) {
1718
+ ctx.emit(`${stepId}:selectedBranch`, selectedBranch);
1719
+ }
1720
+ phase = "branch";
1721
+ const branchFlowOrRef = selectedBranch === "others" ? others : branches[selectedBranch];
1722
+ if (!branchFlowOrRef) {
1723
+ throw new Error(
1724
+ `Branch "${selectedBranch}" not found in route configuration`
1725
+ );
1726
+ }
1727
+ const branchFlowDef = resolveBranchFlow(branchFlowOrRef, flows);
1728
+ const branchFlow = buildFlowFromConfig(
1729
+ branchFlowDef,
1730
+ providers,
1731
+ flows,
1732
+ ctx?.observability?.config ? {
1733
+ observability: ctx.observability.config,
1734
+ metadata: {
1735
+ ...ctx.observability?.metadata,
1736
+ parentNode: stepId,
1737
+ phase: "branch",
1738
+ detectedMimeType,
1739
+ selectedBranch
1740
+ }
1741
+ } : void 0
1742
+ );
1743
+ const branchT0 = Date.now();
1744
+ const branchResultRaw = await branchFlow.run(input);
1745
+ if (!isSingleFlowResult(branchResultRaw)) {
1746
+ throw new Error("Branch flow returned batch result instead of single result");
1747
+ }
1748
+ const branchResult = branchResultRaw;
1749
+ if (ctx?.metrics && branchResult.metrics) {
1750
+ const branchMetrics = flattenMetrics(
1751
+ branchResult.metrics,
1752
+ `${stepId}.branch.${selectedBranch}`
1753
+ );
1754
+ branchMetrics.forEach((m) => ctx.metrics.push(m));
1755
+ }
1756
+ if (ctx?.emit) {
1757
+ ctx.emit(`${stepId}:branchOutput`, branchResult.output);
1758
+ ctx.emit(`${stepId}:branchArtifacts`, branchResult.artifacts);
1759
+ }
1760
+ const branchCost = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.costUSD ?? 0), 0) : 0;
1761
+ const totalMs = Date.now() - t0;
1762
+ const branchMs = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.ms ?? 0), 0) : 0;
1763
+ const overheadMs = totalMs - branchMs;
1764
+ if (ctx?.metrics) {
1765
+ const wrapperMetric = {
1766
+ step: stepId,
1767
+ configStepId: ctx.stepId,
1768
+ startMs: t0,
1769
+ provider: "internal",
1770
+ // No external provider - uses built-in MIME detection
1771
+ model: "mime-detection",
1772
+ ms: totalMs,
1773
+ costUSD: branchCost,
1774
+ // Total cost from branch (MIME detection is free)
1775
+ attemptNumber: 1,
1776
+ // Composite wrappers don't retry, always 1
1777
+ metadata: {
1778
+ kind: "wrapper",
1779
+ // Distinguish wrapper from leaf metrics
1780
+ type: "route",
1781
+ rollup: true,
1782
+ // Duration includes child work
1783
+ overheadMs,
1784
+ // Pure wrapper overhead (MIME detection + routing)
1785
+ detectedMimeType,
1786
+ selectedBranch,
1787
+ branchStepCount: branchResult.metrics?.length || 0,
1788
+ branchFlowId: selectedBranch === "others" ? others && typeof others === "object" && "flowRef" in others ? others.flowRef : "inline" : typeof branches[selectedBranch] === "object" && "flowRef" in branches[selectedBranch] ? branches[selectedBranch].flowRef : "inline"
1789
+ }
1790
+ };
1791
+ ctx.metrics.push(wrapperMetric);
1792
+ }
1793
+ return branchResult.output;
1794
+ } catch (error) {
1795
+ const err = error instanceof Error ? error : new Error(String(error));
1796
+ const isNestedFlowError = err instanceof FlowExecutionError2;
1797
+ if (ctx?.metrics) {
1798
+ ctx.metrics.push({
1799
+ step: stepId,
1800
+ configStepId: ctx.stepId,
1801
+ startMs: t0,
1802
+ ms: Date.now() - t0,
1803
+ costUSD: 0,
1804
+ attemptNumber: 1,
1805
+ // @ts-ignore - Add error field
1806
+ error: err.message,
1807
+ metadata: {
1808
+ kind: "wrapper",
1809
+ type: "route",
1810
+ failed: true,
1811
+ detectedMimeType,
1812
+ selectedBranch,
1813
+ failedPhase: phase
1814
+ }
1815
+ });
1816
+ }
1817
+ const flowPath = [{
1818
+ stepId,
1819
+ stepIndex: 0,
1820
+ stepType: "route",
1821
+ branch: selectedBranch || void 0
1822
+ }];
1823
+ if (isNestedFlowError && err.flowPath) {
1824
+ flowPath.push(...err.flowPath);
1825
+ }
1826
+ const rootCauseMessage = isNestedFlowError ? err.getRootCause().message : err.message;
1827
+ throw new FlowExecutionError2(
1828
+ `Route step "${stepId}" failed${detectedMimeType ? ` (mimeType: ${detectedMimeType})` : ""}${selectedBranch ? ` (branch: ${selectedBranch})` : ""} in phase: ${phase}
1829
+ Error: ${rootCauseMessage}`,
1830
+ stepId,
1831
+ 0,
1832
+ "route",
1833
+ [],
1834
+ isNestedFlowError ? err.originalError : err,
1835
+ void 0,
1836
+ flowPath,
1837
+ isNestedFlowError ? err.allCompletedSteps : void 0
1838
+ );
1839
+ }
1840
+ }
1841
+ };
1842
+ }
1625
1843
 
1626
1844
  // src/serialization.ts
1627
1845
  function extractNodeMetadata(node2) {
@@ -1684,6 +1902,16 @@ function buildFlowFromConfig(flowDef, providers, flows, options) {
1684
1902
  flows: flows || {}
1685
1903
  });
1686
1904
  flow = flow.step(step.id, node2, step.name);
1905
+ } else if (step.type === "route") {
1906
+ const node2 = createRouteCompositeNode({
1907
+ stepId: step.id,
1908
+ routeConfig: step.config,
1909
+ branches: step.branches,
1910
+ others: step.others,
1911
+ providers,
1912
+ flows: flows || {}
1913
+ });
1914
+ flow = flow.step(step.id, node2, step.name);
1687
1915
  } else {
1688
1916
  const exhaustiveCheck = step;
1689
1917
  throw new FlowSerializationError(`Unknown step type: ${exhaustiveCheck.type}`);
@@ -1808,7 +2036,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
1808
2036
  return parse({
1809
2037
  provider,
1810
2038
  consensus: cfg.consensus,
1811
- maxTokens: cfg.maxTokens
2039
+ maxTokens: cfg.maxTokens,
2040
+ promptRef: cfg.promptRef,
2041
+ promptVariables: cfg.promptVariables,
2042
+ additionalInstructions: cfg.additionalInstructions
1812
2043
  });
1813
2044
  }
1814
2045
  case "extract": {
@@ -1818,7 +2049,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
1818
2049
  schema: cfg.schema,
1819
2050
  consensus: cfg.consensus,
1820
2051
  reasoning: cfg.reasoning,
1821
- maxTokens: cfg.maxTokens
2052
+ maxTokens: cfg.maxTokens,
2053
+ promptRef: cfg.promptRef,
2054
+ promptVariables: cfg.promptVariables,
2055
+ additionalInstructions: cfg.additionalInstructions
1822
2056
  });
1823
2057
  }
1824
2058
  case "split": {
@@ -1840,7 +2074,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
1840
2074
  provider,
1841
2075
  categories: cfg.categories,
1842
2076
  consensus: cfg.consensus,
1843
- maxTokens: cfg.maxTokens
2077
+ maxTokens: cfg.maxTokens,
2078
+ promptRef: cfg.promptRef,
2079
+ promptVariables: cfg.promptVariables,
2080
+ additionalInstructions: cfg.additionalInstructions
1844
2081
  });
1845
2082
  }
1846
2083
  default:
@@ -1916,6 +2153,26 @@ function calculateFlowNestingDepth(flow, currentDepth = 1) {
1916
2153
  }
1917
2154
  }
1918
2155
  }
2156
+ } else if (step.type === "route") {
2157
+ const routeStep = step;
2158
+ if (routeStep.branches) {
2159
+ for (const branchFlowOrRef of Object.values(routeStep.branches)) {
2160
+ if ("flowRef" in branchFlowOrRef) {
2161
+ maxDepth = Math.max(maxDepth, stepDepth + 3);
2162
+ } else {
2163
+ const branchDepth = calculateFlowNestingDepth(branchFlowOrRef, stepDepth + 2);
2164
+ maxDepth = Math.max(maxDepth, branchDepth);
2165
+ }
2166
+ }
2167
+ }
2168
+ if (routeStep.others) {
2169
+ if ("flowRef" in routeStep.others) {
2170
+ maxDepth = Math.max(maxDepth, stepDepth + 3);
2171
+ } else {
2172
+ const othersDepth = calculateFlowNestingDepth(routeStep.others, stepDepth + 2);
2173
+ maxDepth = Math.max(maxDepth, othersDepth);
2174
+ }
2175
+ }
1919
2176
  } else if (step.type === "forEach") {
1920
2177
  const forEachStep = step;
1921
2178
  if (forEachStep.itemFlow) {
@@ -2076,8 +2333,111 @@ function validateFlow(flowDef, options = {}) {
2076
2333
  }
2077
2334
  }
2078
2335
  }
2336
+ } else if (step.type === "route") {
2337
+ const routeStep = step;
2338
+ if (!routeStep.branches || typeof routeStep.branches !== "object") {
2339
+ errors.push({
2340
+ type: "invalid_config",
2341
+ stepId,
2342
+ message: "Route step missing or invalid branches field"
2343
+ });
2344
+ } else if (Object.keys(routeStep.branches).length === 0) {
2345
+ errors.push({
2346
+ type: "invalid_config",
2347
+ stepId,
2348
+ message: "Route step must have at least one branch"
2349
+ });
2350
+ }
2351
+ if (routeStep.config?.branches && routeStep.branches) {
2352
+ for (const branchName of Object.keys(routeStep.config.branches)) {
2353
+ if (!routeStep.branches[branchName]) {
2354
+ errors.push({
2355
+ type: "invalid_config",
2356
+ stepId,
2357
+ message: `Branch "${branchName}" defined in config but missing flow definition`
2358
+ });
2359
+ }
2360
+ }
2361
+ for (const [branchName, branchConfig] of Object.entries(routeStep.config.branches)) {
2362
+ if (!branchConfig.mimeTypes || branchConfig.mimeTypes.length === 0) {
2363
+ errors.push({
2364
+ type: "invalid_config",
2365
+ stepId: `${stepId}.${branchName}`,
2366
+ message: `Branch "${branchName}" has no MIME types defined`
2367
+ });
2368
+ } else {
2369
+ for (const mimeType of branchConfig.mimeTypes) {
2370
+ if (!mimeType.includes("/")) {
2371
+ errors.push({
2372
+ type: "invalid_config",
2373
+ stepId: `${stepId}.${branchName}`,
2374
+ message: `Invalid MIME type pattern: "${mimeType}". Expected format: "type/subtype" or "type/*"`
2375
+ });
2376
+ }
2377
+ }
2378
+ }
2379
+ }
2380
+ }
2381
+ if (routeStep.branches) {
2382
+ for (const [branchName, branchFlowOrRef] of Object.entries(routeStep.branches)) {
2383
+ if ("flowRef" in branchFlowOrRef) {
2384
+ const flowRef = branchFlowOrRef.flowRef;
2385
+ if (typeof flowRef !== "string" || flowRef.trim() === "") {
2386
+ errors.push({
2387
+ type: "invalid_config",
2388
+ stepId: `${stepId}.${branchName}`,
2389
+ message: `Branch "${branchName}": flowRef must be a non-empty string`
2390
+ });
2391
+ }
2392
+ } else {
2393
+ const branchResult = validateFlow(branchFlowOrRef, options);
2394
+ for (const error of branchResult.errors) {
2395
+ errors.push({
2396
+ ...error,
2397
+ stepId: `${stepId}.${branchName}`,
2398
+ message: `Branch "${branchName}": ${error.message}`
2399
+ });
2400
+ }
2401
+ for (const warning of branchResult.warnings) {
2402
+ warnings.push({
2403
+ ...warning,
2404
+ stepId: `${stepId}.${branchName}`,
2405
+ message: `Branch "${branchName}": ${warning.message}`
2406
+ });
2407
+ }
2408
+ }
2409
+ }
2410
+ }
2411
+ if (routeStep.others) {
2412
+ if ("flowRef" in routeStep.others) {
2413
+ const flowRef = routeStep.others.flowRef;
2414
+ if (typeof flowRef !== "string" || flowRef.trim() === "") {
2415
+ errors.push({
2416
+ type: "invalid_config",
2417
+ stepId: `${stepId}.others`,
2418
+ message: "Others branch flowRef must be a non-empty string"
2419
+ });
2420
+ }
2421
+ } else {
2422
+ const othersResult = validateFlow(routeStep.others, options);
2423
+ for (const error of othersResult.errors) {
2424
+ errors.push({
2425
+ ...error,
2426
+ stepId: `${stepId}.others`,
2427
+ message: `Others branch: ${error.message}`
2428
+ });
2429
+ }
2430
+ for (const warning of othersResult.warnings) {
2431
+ warnings.push({
2432
+ ...warning,
2433
+ stepId: `${stepId}.others`,
2434
+ message: `Others branch: ${warning.message}`
2435
+ });
2436
+ }
2437
+ }
2438
+ }
2079
2439
  }
2080
- const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output"];
2440
+ const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output", "route"];
2081
2441
  if (!step.nodeType || !validNodeTypes.includes(step.nodeType)) {
2082
2442
  errors.push({
2083
2443
  type: "invalid_config",
@@ -2099,7 +2459,7 @@ function validateFlow(flowDef, options = {}) {
2099
2459
  const hasProviderRef2 = (cfg) => {
2100
2460
  return "providerRef" in cfg && typeof cfg.providerRef === "string";
2101
2461
  };
2102
- if (step.nodeType !== "trigger" && step.nodeType !== "output") {
2462
+ if (step.nodeType !== "trigger" && step.nodeType !== "output" && step.nodeType !== "route") {
2103
2463
  if (!hasProviderRef2(config)) {
2104
2464
  errors.push({
2105
2465
  type: "missing_provider",
@@ -2609,6 +2969,7 @@ export {
2609
2969
  createConditionalCompositeNode,
2610
2970
  createFlow,
2611
2971
  createForEachCompositeNode,
2972
+ createRouteCompositeNode,
2612
2973
  defineFlowConfig,
2613
2974
  extract2 as extract,
2614
2975
  extractNodeMetadata,