@doclo/flows 0.1.2

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.js ADDED
@@ -0,0 +1,2534 @@
1
+ import {
2
+ simpleSchema
3
+ } from "./chunk-USCWPTGU.js";
4
+
5
+ // src/index.ts
6
+ import { runPipeline as runPipeline4 } from "@doclo/core";
7
+ import { parseNode as parseNode2, extractNode as extractNode2 } from "@doclo/nodes";
8
+
9
+ // src/flow-builder.ts
10
+ import {
11
+ runPipeline,
12
+ FlowExecutionError,
13
+ aggregateMetrics,
14
+ getNodeTypeName,
15
+ validateNodeConnection,
16
+ canStartForEachItemFlow,
17
+ getValidForEachStarters,
18
+ getProviderById,
19
+ validateFlowInputFormat
20
+ } from "@doclo/core";
21
+ import { shouldSkipValidation } from "@doclo/core/runtime/env";
22
+ import { output as createOutputNode } from "@doclo/nodes";
23
+ import {
24
+ mergeConfig,
25
+ shouldSample,
26
+ TraceContextManager,
27
+ generateExecutionId,
28
+ executeHook,
29
+ buildStepAttributes,
30
+ generateSpanId
31
+ } from "@doclo/core/observability";
32
+ function isSingleFlowResult(result) {
33
+ return "output" in result && "artifacts" in result;
34
+ }
35
+ function normalizeFlowInput(input) {
36
+ if (input == null) {
37
+ return input;
38
+ }
39
+ if (typeof input === "object" && (input.base64 || input.url)) {
40
+ return input;
41
+ }
42
+ if (typeof input === "string") {
43
+ if (input.startsWith("data:")) {
44
+ return { base64: input };
45
+ }
46
+ if (input.startsWith("http://") || input.startsWith("https://")) {
47
+ return { url: input };
48
+ }
49
+ return { base64: input };
50
+ }
51
+ return input;
52
+ }
53
+ var Flow = class {
54
+ steps = [];
55
+ observability;
56
+ metadata;
57
+ inputValidation;
58
+ traceContextManager;
59
+ currentExecution;
60
+ constructor(options) {
61
+ if (options?.observability) {
62
+ this.observability = mergeConfig(options.observability);
63
+ this.traceContextManager = new TraceContextManager(this.observability);
64
+ }
65
+ if (options?.metadata) {
66
+ this.metadata = options.metadata;
67
+ }
68
+ if (options?.inputValidation) {
69
+ this.inputValidation = options.inputValidation;
70
+ }
71
+ }
72
+ /**
73
+ * Set accepted input formats for this flow (fluent API).
74
+ * Validates input format before flow execution begins.
75
+ *
76
+ * @param formats - List of accepted MIME types (e.g., ['application/pdf', 'image/jpeg'])
77
+ * @returns This flow instance for chaining
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const pdfOnlyFlow = createFlow()
82
+ * .acceptFormats(['application/pdf'])
83
+ * .step('parse', parse({ provider }))
84
+ * .build();
85
+ *
86
+ * // Throws FlowInputValidationError if input is not a PDF
87
+ * await pdfOnlyFlow.run({ base64: jpegBase64 });
88
+ * ```
89
+ */
90
+ acceptFormats(formats) {
91
+ this.inputValidation = { acceptedFormats: formats };
92
+ return this;
93
+ }
94
+ /**
95
+ * Add a sequential step to the flow
96
+ */
97
+ step(id, node2, name) {
98
+ this.steps.push({
99
+ type: "step",
100
+ id,
101
+ name,
102
+ node: node2
103
+ });
104
+ return this;
105
+ }
106
+ /**
107
+ * Add a conditional step that chooses a node based on input data
108
+ *
109
+ * IMPORTANT: Conditionals must return a NODE, not a promise or executed flow.
110
+ * The SDK will execute the returned node for you.
111
+ *
112
+ * The condition function receives the full wrapped data (e.g., { input, quality })
113
+ * but the returned node should accept the unwrapped input (e.g., just FlowInput).
114
+ * The SDK automatically unwraps the data before passing it to the selected node.
115
+ *
116
+ * ✅ CORRECT - Return a node (declarative):
117
+ * ```typescript
118
+ * .step('qualify', qualify({ provider, levels: ['low', 'medium', 'high'] }))
119
+ * .conditional('parse', (data) => {
120
+ * // data is { input: FlowInput, quality: string }
121
+ * if (data.quality === 'high') {
122
+ * return parse({ provider: fastProvider }); // Return the node
123
+ * }
124
+ * return parse({ provider: accurateProvider }); // Return the node
125
+ * })
126
+ * ```
127
+ *
128
+ * ❌ INCORRECT - Do NOT return a promise (imperative):
129
+ * ```typescript
130
+ * .conditional('parse', (data) => {
131
+ * // This will throw an error!
132
+ * return createFlow()
133
+ * .step('parse', parse({ provider }))
134
+ * .build()
135
+ * .run(data.input) // ❌ Don't call .run() here!
136
+ * .then(r => r.output);
137
+ * })
138
+ * ```
139
+ *
140
+ * 🆕 NEW - Access previous step outputs via context:
141
+ * ```typescript
142
+ * .step('categorize', categorize({ provider, categories }))
143
+ * .conditional('parse', (data) => parse({ provider }))
144
+ * .conditional('extract', (data, context) => {
145
+ * // Access category from earlier step via context.artifacts
146
+ * const category = context?.artifacts.categorize?.category;
147
+ * return extract({ provider, schema: SCHEMAS[category] });
148
+ * })
149
+ * ```
150
+ *
151
+ * Use the declarative pattern (return nodes) for consistent flow execution,
152
+ * proper error tracking, and accurate metrics collection.
153
+ */
154
+ conditional(id, condition, name) {
155
+ this.steps.push({
156
+ type: "conditional",
157
+ id,
158
+ name,
159
+ condition
160
+ });
161
+ return this;
162
+ }
163
+ /**
164
+ * Process each item from previous step (which must return an array) with a child flow
165
+ * Each item is processed in parallel as its own isolated run
166
+ */
167
+ forEach(id, childFlow, name) {
168
+ this.steps.push({
169
+ type: "forEach",
170
+ id,
171
+ name,
172
+ childFlow
173
+ });
174
+ return this;
175
+ }
176
+ /**
177
+ * Add an explicit output node to mark which data to return from the flow
178
+ *
179
+ * By default, flows return the output of the last step. Use output nodes to:
180
+ * - Return data from earlier steps
181
+ * - Return multiple named outputs
182
+ * - Transform outputs before returning
183
+ *
184
+ * @param config - Output configuration
185
+ * @returns Flow with output node added
186
+ *
187
+ * @example
188
+ * // Single output
189
+ * .output({ name: 'invoice_data' })
190
+ *
191
+ * // Select specific source
192
+ * .output({ name: 'result', source: 'step2' })
193
+ *
194
+ * // Multiple outputs
195
+ * .step('extract1', extract({ provider, schema1 }))
196
+ * .output({ name: 'summary', source: 'extract1' })
197
+ * .step('extract2', extract({ provider, schema2 }))
198
+ * .output({ name: 'details', source: 'extract2' })
199
+ */
200
+ output(config) {
201
+ const name = config?.name?.trim();
202
+ const stepId = name || this.generateOutputStepId();
203
+ this.steps.push({
204
+ type: "step",
205
+ id: stepId,
206
+ node: createOutputNode({
207
+ ...config,
208
+ name: stepId
209
+ // Ensure name matches step ID
210
+ })
211
+ });
212
+ return this;
213
+ }
214
+ /**
215
+ * Get current execution context
216
+ *
217
+ * Returns null if not currently executing.
218
+ */
219
+ getExecutionContext() {
220
+ return this.currentExecution ?? null;
221
+ }
222
+ /**
223
+ * Get current trace context
224
+ *
225
+ * Returns null if not currently executing or observability not configured.
226
+ */
227
+ getTraceContext() {
228
+ return this.traceContextManager?.getTraceContext() ?? null;
229
+ }
230
+ /**
231
+ * Set a custom attribute on the current execution
232
+ *
233
+ * Custom attributes appear in execution context and can be accessed by hooks.
234
+ */
235
+ setCustomAttribute(key, value) {
236
+ if (this.currentExecution) {
237
+ this.currentExecution.customAttributes[key] = value;
238
+ }
239
+ }
240
+ /**
241
+ * Record a custom metric for the current execution
242
+ *
243
+ * Custom metrics appear in execution context and can be accessed by hooks.
244
+ */
245
+ recordMetric(name, value, unit) {
246
+ if (this.currentExecution) {
247
+ this.currentExecution.customMetrics.push({
248
+ name,
249
+ value,
250
+ unit,
251
+ timestamp: Date.now()
252
+ });
253
+ }
254
+ }
255
+ /**
256
+ * Build and return the executable flow
257
+ */
258
+ build() {
259
+ return {
260
+ run: async (input, callbacks) => {
261
+ return this.execute(input, callbacks);
262
+ },
263
+ validate: () => {
264
+ return this.validate();
265
+ }
266
+ };
267
+ }
268
+ /**
269
+ * Generate a unique step ID for unnamed output nodes
270
+ * Prevents duplicate IDs when multiple .output() calls without names
271
+ */
272
+ generateOutputStepId() {
273
+ let counter = 0;
274
+ let candidateId = "output";
275
+ while (this.steps.some((step) => step.id === candidateId)) {
276
+ counter++;
277
+ candidateId = `output_${counter}`;
278
+ }
279
+ return candidateId;
280
+ }
281
+ /**
282
+ * Validate the flow configuration
283
+ */
284
+ validate() {
285
+ const errors = [];
286
+ const warnings = [];
287
+ if (this.steps.length === 0) {
288
+ errors.push({
289
+ stepId: "<flow>",
290
+ stepIndex: -1,
291
+ stepType: "flow",
292
+ message: "Flow has no steps. Add at least one step using .step(), .conditional(), or .forEach()"
293
+ });
294
+ }
295
+ for (let stepIndex = 0; stepIndex < this.steps.length; stepIndex++) {
296
+ const step = this.steps[stepIndex];
297
+ const duplicateIndex = this.steps.findIndex((s, i) => i !== stepIndex && s.id === step.id);
298
+ if (duplicateIndex !== -1) {
299
+ errors.push({
300
+ stepId: step.id,
301
+ stepIndex,
302
+ stepType: step.type,
303
+ message: `Duplicate step ID "${step.id}" found at indices ${stepIndex} and ${duplicateIndex}`
304
+ });
305
+ }
306
+ if (step.type === "step") {
307
+ if (!step.node) {
308
+ errors.push({
309
+ stepId: step.id,
310
+ stepIndex,
311
+ stepType: step.type,
312
+ message: "Step node is missing. Use parse(), qualify(), categorize(), extract(), or split()"
313
+ });
314
+ }
315
+ } else if (step.type === "conditional") {
316
+ if (!step.condition || typeof step.condition !== "function") {
317
+ errors.push({
318
+ stepId: step.id,
319
+ stepIndex,
320
+ stepType: step.type,
321
+ message: "Conditional must have a condition function"
322
+ });
323
+ }
324
+ } else if (step.type === "forEach") {
325
+ if (!step.childFlow || typeof step.childFlow !== "function") {
326
+ errors.push({
327
+ stepId: step.id,
328
+ stepIndex,
329
+ stepType: step.type,
330
+ message: "forEach must have a childFlow function"
331
+ });
332
+ }
333
+ if (stepIndex === 0) {
334
+ warnings.push(`forEach step "${step.id}" at index ${stepIndex} is the first step - ensure input is an array`);
335
+ }
336
+ }
337
+ if (!step.id || step.id.trim() === "") {
338
+ errors.push({
339
+ stepId: "<empty>",
340
+ stepIndex,
341
+ stepType: step.type,
342
+ message: "Step ID cannot be empty"
343
+ });
344
+ }
345
+ }
346
+ if (!shouldSkipValidation()) {
347
+ this.validateTypeCompatibility(errors, warnings);
348
+ }
349
+ return {
350
+ valid: errors.length === 0,
351
+ errors,
352
+ warnings
353
+ };
354
+ }
355
+ /**
356
+ * Validate type compatibility between consecutive steps
357
+ */
358
+ validateTypeCompatibility(errors, warnings) {
359
+ for (let i = 0; i < this.steps.length - 1; i++) {
360
+ const currentStep = this.steps[i];
361
+ const nextStep = this.steps[i + 1];
362
+ if (currentStep.type === "step" && nextStep.type === "step") {
363
+ const sourceType = getNodeTypeName(currentStep.node);
364
+ const targetType = getNodeTypeName(nextStep.node);
365
+ if (!sourceType || !targetType) {
366
+ continue;
367
+ }
368
+ const forEachEnabled = false;
369
+ const validation = validateNodeConnection(sourceType, targetType, forEachEnabled);
370
+ if (!validation.valid) {
371
+ errors.push({
372
+ stepId: currentStep.id,
373
+ stepIndex: i,
374
+ stepType: currentStep.type,
375
+ message: `Invalid connection: ${sourceType} \u2192 ${targetType}. ${validation.reason || "Types incompatible"}`
376
+ });
377
+ if (validation.suggestions && validation.suggestions.length > 0) {
378
+ warnings.push(`Suggestions for step "${currentStep.id}":`);
379
+ validation.suggestions.forEach((s) => warnings.push(` ${s}`));
380
+ }
381
+ } else if (validation.warning) {
382
+ warnings.push(`Step "${currentStep.id}" \u2192 "${nextStep.id}": ${validation.warning}`);
383
+ }
384
+ }
385
+ if (currentStep.type === "step" && nextStep.type === "forEach") {
386
+ const sourceType = getNodeTypeName(currentStep.node);
387
+ if (sourceType) {
388
+ const sourceInfo = currentStep.node.__meta;
389
+ const outputsArray = sourceInfo?.outputsArray;
390
+ const isArrayOutput = typeof outputsArray === "function" ? outputsArray(null) : outputsArray;
391
+ if (!isArrayOutput) {
392
+ warnings.push(
393
+ `forEach step "${nextStep.id}" requires array input. Previous step "${currentStep.id}" (${sourceType}) may not output an array. Ensure ${sourceType} is configured to output an array (e.g., parse with chunked:true).`
394
+ );
395
+ }
396
+ if (nextStep.childFlow && typeof nextStep.childFlow === "function") {
397
+ try {
398
+ const childFlowInstance = nextStep.childFlow(null);
399
+ const childSteps = childFlowInstance.steps;
400
+ if (childSteps && Array.isArray(childSteps) && childSteps.length > 0) {
401
+ const firstStep = childSteps[0];
402
+ if (firstStep.type === "step") {
403
+ const firstNodeType = getNodeTypeName(firstStep.node);
404
+ if (firstNodeType) {
405
+ const validation = canStartForEachItemFlow(sourceType, firstNodeType);
406
+ if (!validation.valid) {
407
+ errors.push({
408
+ stepId: nextStep.id,
409
+ stepIndex: i + 1,
410
+ stepType: "forEach",
411
+ message: `Invalid forEach itemFlow starter: ${validation.reason || `${firstNodeType} cannot start forEach itemFlow after ${sourceType}`}`
412
+ });
413
+ if (validation.suggestions && validation.suggestions.length > 0) {
414
+ warnings.push(`Suggestions for forEach "${nextStep.id}":`);
415
+ validation.suggestions.forEach((s) => warnings.push(` ${s}`));
416
+ }
417
+ }
418
+ }
419
+ } else if (firstStep.type === "forEach") {
420
+ if (sourceType === "split") {
421
+ errors.push({
422
+ stepId: nextStep.id,
423
+ stepIndex: i + 1,
424
+ stepType: "forEach",
425
+ message: "Invalid forEach itemFlow: Cannot nest forEach operations. Split nodes cannot appear in forEach itemFlow."
426
+ });
427
+ }
428
+ }
429
+ }
430
+ } catch (error) {
431
+ warnings.push(
432
+ `forEach step "${nextStep.id}": Unable to validate itemFlow structure. Ensure the first node in itemFlow is compatible with ${sourceType} output. Valid starters: ${getValidForEachStarters(sourceType).join(", ")}`
433
+ );
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ this.checkEfficiencyPatterns(warnings);
440
+ }
441
+ /**
442
+ * Check for inefficient flow patterns and add warnings.
443
+ *
444
+ * Detects patterns like:
445
+ * - parse() → extract(raw-document-provider): The extract provider ignores parse output
446
+ */
447
+ checkEfficiencyPatterns(warnings) {
448
+ for (let i = 0; i < this.steps.length - 1; i++) {
449
+ const current = this.steps[i];
450
+ const next = this.steps[i + 1];
451
+ if (current.type !== "step" || next.type !== "step") continue;
452
+ const currNodeType = getNodeTypeName(current.node);
453
+ const nextNodeType = getNodeTypeName(next.node);
454
+ if (currNodeType === "parse" && nextNodeType === "extract") {
455
+ const extractProvider = this.getProviderFromNode(next.node);
456
+ if (extractProvider) {
457
+ const metadata = getProviderById(extractProvider);
458
+ if (metadata?.inputRequirements?.inputType === "raw-document") {
459
+ warnings.push(
460
+ `Efficiency warning: Step "${current.id}" (parse) output may be ignored. "${metadata.name}" processes raw documents directly, not parsed text. Consider: (1) Remove the parse step, or (2) Use an LLM provider for extraction that can use parsed text.`
461
+ );
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+ /**
468
+ * Extract provider ID from a node definition.
469
+ * Returns undefined if provider cannot be determined.
470
+ */
471
+ getProviderFromNode(node2) {
472
+ const config = node2.__meta?.config;
473
+ if (config?.provider) {
474
+ if (typeof config.provider === "string") {
475
+ return config.provider;
476
+ }
477
+ if (typeof config.provider === "object") {
478
+ return config.provider.id ?? config.provider.name;
479
+ }
480
+ }
481
+ return void 0;
482
+ }
483
+ /**
484
+ * Execute the flow with optional progress callbacks
485
+ */
486
+ async execute(input, callbacks) {
487
+ const flowStartTime = Date.now();
488
+ const artifacts = {};
489
+ const metrics = [];
490
+ const completedSteps = [];
491
+ const outputs = {};
492
+ let lastNonOutputData = null;
493
+ let executionId;
494
+ let traceContext;
495
+ let sampled = false;
496
+ if (this.observability) {
497
+ sampled = shouldSample(this.observability);
498
+ if (this.traceContextManager && sampled) {
499
+ traceContext = this.traceContextManager.initialize(sampled);
500
+ }
501
+ const execIdGenerator = this.observability.generateExecutionId ?? generateExecutionId;
502
+ executionId = execIdGenerator();
503
+ this.currentExecution = {
504
+ flowId: "flow",
505
+ // TODO: Add flowId to Flow class
506
+ executionId,
507
+ startTime: flowStartTime,
508
+ status: "running",
509
+ customAttributes: {},
510
+ customMetrics: []
511
+ };
512
+ if (sampled && traceContext) {
513
+ const flowStartContext = {
514
+ flowId: "flow",
515
+ // TODO: Add flowId
516
+ flowVersion: "0.0.1",
517
+ executionId,
518
+ timestamp: flowStartTime,
519
+ input,
520
+ config: {},
521
+ // TODO: Capture flow config
522
+ metadata: this.metadata,
523
+ sdkVersion: "0.0.1",
524
+ observabilityVersion: this.observability.observabilityVersion ?? "1.0.0",
525
+ traceContext
526
+ };
527
+ await executeHook(this.observability.onFlowStart, {
528
+ hookName: "onFlowStart",
529
+ config: this.observability,
530
+ context: flowStartContext
531
+ });
532
+ }
533
+ }
534
+ let currentData = normalizeFlowInput(input);
535
+ if (this.inputValidation?.acceptedFormats?.length) {
536
+ const dataUrl = currentData?.base64 || currentData?.url;
537
+ if (dataUrl) {
538
+ validateFlowInputFormat(dataUrl, this.inputValidation.acceptedFormats);
539
+ }
540
+ }
541
+ try {
542
+ for (let stepIndex = 0; stepIndex < this.steps.length; stepIndex++) {
543
+ const step = this.steps[stepIndex];
544
+ const stepStartTime = Date.now();
545
+ const stepSpanId = this.traceContextManager && sampled ? generateSpanId() : void 0;
546
+ callbacks?.onStepStart?.(step.id, stepIndex, step.type);
547
+ if (this.observability && sampled && traceContext && executionId && stepSpanId) {
548
+ const stepStartContext = {
549
+ flowId: "flow",
550
+ executionId,
551
+ stepId: step.id,
552
+ stepIndex,
553
+ stepType: step.node?.key ?? step.type,
554
+ stepName: step.name ?? step.id,
555
+ timestamp: stepStartTime,
556
+ provider: void 0,
557
+ // Will be populated from step config if available
558
+ model: void 0,
559
+ config: {},
560
+ input: currentData,
561
+ isConsensusEnabled: false,
562
+ // TODO: Check step config for consensus
563
+ isRetry: false,
564
+ metadata: this.metadata,
565
+ traceContext,
566
+ spanId: stepSpanId
567
+ };
568
+ await executeHook(this.observability.onStepStart, {
569
+ hookName: "onStepStart",
570
+ config: this.observability,
571
+ context: stepStartContext
572
+ });
573
+ }
574
+ try {
575
+ if (step.type === "step") {
576
+ const isOutputNode = step.node?.__meta?.isOutputNode === true;
577
+ const outputName = step.node?.__meta?.outputName?.trim() || step.id;
578
+ let result2;
579
+ if (isOutputNode) {
580
+ const ctx = {
581
+ stepId: step.id,
582
+ artifacts,
583
+ emit: (k, v) => {
584
+ artifacts[k] = v;
585
+ },
586
+ metrics: { push: (m) => metrics.push(m) },
587
+ observability: this.observability && sampled ? {
588
+ config: this.observability,
589
+ flowId: "flow",
590
+ executionId,
591
+ stepId: step.id,
592
+ stepIndex,
593
+ traceContext,
594
+ metadata: this.metadata
595
+ } : void 0
596
+ };
597
+ const outputData = await step.node.run(currentData, ctx);
598
+ result2 = { output: outputData, artifacts: {}, metrics: [] };
599
+ outputs[outputName] = outputData;
600
+ artifacts[step.id] = outputData;
601
+ completedSteps.push(step.id);
602
+ const stepDuration = Date.now() - stepStartTime;
603
+ callbacks?.onStepComplete?.(step.id, stepIndex, step.type, stepDuration);
604
+ if (this.observability && sampled && traceContext && executionId && stepSpanId) {
605
+ const stepEndContext = {
606
+ flowId: "flow",
607
+ executionId,
608
+ stepId: step.id,
609
+ stepIndex,
610
+ timestamp: Date.now(),
611
+ startTime: stepStartTime,
612
+ duration: stepDuration,
613
+ output: outputData,
614
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
615
+ cost: 0,
616
+ metricKind: "prep",
617
+ // Output nodes are prep steps (no API call)
618
+ otelAttributes: buildStepAttributes({
619
+ stepType: step.node?.key ?? step.type
620
+ }),
621
+ metadata: this.metadata,
622
+ traceContext,
623
+ spanId: stepSpanId
624
+ };
625
+ await executeHook(this.observability.onStepEnd, {
626
+ hookName: "onStepEnd",
627
+ config: this.observability,
628
+ context: stepEndContext
629
+ });
630
+ }
631
+ currentData = lastNonOutputData !== null ? lastNonOutputData : currentData;
632
+ } else {
633
+ result2 = await runPipeline([step.node], currentData, this.observability && sampled ? {
634
+ config: this.observability,
635
+ flowId: "flow",
636
+ executionId,
637
+ stepId: step.id,
638
+ stepIndex,
639
+ traceContext,
640
+ metadata: this.metadata
641
+ } : {
642
+ // Always pass stepId for metrics tracking, even without observability
643
+ stepId: step.id
644
+ });
645
+ artifacts[step.id] = result2.output;
646
+ metrics.push(...result2.metrics);
647
+ completedSteps.push(step.id);
648
+ const stepDuration = Date.now() - stepStartTime;
649
+ callbacks?.onStepComplete?.(step.id, stepIndex, step.type, stepDuration);
650
+ if (this.observability && sampled && traceContext && executionId && stepSpanId) {
651
+ const leafMetrics = result2.metrics.filter((m) => m.metadata?.kind === "leaf");
652
+ const wrapperMetric = result2.metrics.find((m) => m.metadata?.kind === "wrapper");
653
+ const stepLeafMetrics = result2.metrics.filter(
654
+ (m) => m.configStepId === step.id && m.metadata?.kind === "leaf"
655
+ );
656
+ const isConsensus = stepLeafMetrics.length > 1;
657
+ const stepOwnLeafMetric = isConsensus ? void 0 : stepLeafMetrics[0];
658
+ const hasOtherChildMetrics = result2.metrics.some((m) => m.configStepId !== step.id);
659
+ const hasChildMetrics = isConsensus || hasOtherChildMetrics || leafMetrics.length > 1;
660
+ let metricKind;
661
+ let ownDuration;
662
+ let ownCost;
663
+ let ownInputTokens;
664
+ let ownOutputTokens;
665
+ let ownCacheCreationTokens;
666
+ let ownCacheReadTokens;
667
+ if (isConsensus) {
668
+ metricKind = "wrapper";
669
+ ownDuration = wrapperMetric?.metadata?.overheadMs ?? 0;
670
+ ownCost = 0;
671
+ ownInputTokens = 0;
672
+ ownOutputTokens = 0;
673
+ ownCacheCreationTokens = void 0;
674
+ ownCacheReadTokens = void 0;
675
+ } else if (stepOwnLeafMetric) {
676
+ metricKind = hasOtherChildMetrics ? "wrapper" : "leaf";
677
+ ownDuration = stepOwnLeafMetric.ms ?? 0;
678
+ ownCost = stepOwnLeafMetric.costUSD ?? 0;
679
+ ownInputTokens = stepOwnLeafMetric.inputTokens ?? 0;
680
+ ownOutputTokens = stepOwnLeafMetric.outputTokens ?? 0;
681
+ ownCacheCreationTokens = stepOwnLeafMetric.cacheCreationInputTokens;
682
+ ownCacheReadTokens = stepOwnLeafMetric.cacheReadInputTokens;
683
+ } else if (wrapperMetric || hasChildMetrics) {
684
+ metricKind = "wrapper";
685
+ ownDuration = wrapperMetric?.metadata?.overheadMs ?? 0;
686
+ ownCost = 0;
687
+ ownInputTokens = 0;
688
+ ownOutputTokens = 0;
689
+ ownCacheCreationTokens = void 0;
690
+ ownCacheReadTokens = void 0;
691
+ } else {
692
+ metricKind = "prep";
693
+ ownDuration = stepDuration;
694
+ ownCost = 0;
695
+ ownInputTokens = 0;
696
+ ownOutputTokens = 0;
697
+ ownCacheCreationTokens = void 0;
698
+ ownCacheReadTokens = void 0;
699
+ }
700
+ const firstMetric = result2.metrics.length > 0 ? result2.metrics[0] : void 0;
701
+ const stepEndContext = {
702
+ flowId: "flow",
703
+ executionId,
704
+ stepId: step.id,
705
+ stepIndex,
706
+ timestamp: Date.now(),
707
+ startTime: stepStartTime,
708
+ duration: ownDuration,
709
+ output: result2.output,
710
+ usage: {
711
+ inputTokens: ownInputTokens,
712
+ outputTokens: ownOutputTokens,
713
+ totalTokens: ownInputTokens + ownOutputTokens,
714
+ cacheCreationInputTokens: ownCacheCreationTokens,
715
+ cacheReadInputTokens: ownCacheReadTokens
716
+ },
717
+ cost: ownCost,
718
+ metricKind,
719
+ otelAttributes: buildStepAttributes({
720
+ stepType: step.node?.key ?? step.type,
721
+ provider: firstMetric?.provider,
722
+ model: firstMetric?.model,
723
+ inputTokens: ownInputTokens,
724
+ outputTokens: ownOutputTokens
725
+ }),
726
+ metadata: this.metadata,
727
+ traceContext,
728
+ spanId: stepSpanId
729
+ };
730
+ await executeHook(this.observability.onStepEnd, {
731
+ hookName: "onStepEnd",
732
+ config: this.observability,
733
+ context: stepEndContext
734
+ });
735
+ }
736
+ lastNonOutputData = result2.output;
737
+ const hasNextStep = stepIndex < this.steps.length - 1;
738
+ const nextStep = hasNextStep ? this.steps[stepIndex + 1] : null;
739
+ const shouldUnwrap = hasNextStep && nextStep?.type === "step" && result2.output && typeof result2.output === "object" && "input" in result2.output;
740
+ if (shouldUnwrap) {
741
+ currentData = result2.output.input;
742
+ } else {
743
+ currentData = result2.output;
744
+ }
745
+ }
746
+ } else if (step.type === "conditional") {
747
+ const context = {
748
+ artifacts: { ...artifacts },
749
+ metrics: [...metrics]
750
+ };
751
+ const node2 = step.condition(currentData, context);
752
+ if (!node2 || typeof node2 !== "object" || !node2.key || typeof node2.run !== "function") {
753
+ throw new Error(
754
+ `Conditional step "${step.id}" must return a node (e.g., parse(), categorize(), extract()). Got: ${typeof node2}${node2 && typeof node2 === "object" ? ` with keys: ${Object.keys(node2).join(", ")}` : ""}.
755
+
756
+ A valid node must have 'key' and 'run' properties.
757
+
758
+ Incorrect: .conditional('step', () => flow.run(...).then(r => r.output))
759
+ Correct: .conditional('step', () => parse({ provider }))`
760
+ );
761
+ }
762
+ const nodeInput = currentData && typeof currentData === "object" && "input" in currentData ? currentData.input : currentData;
763
+ const result2 = await runPipeline([node2], nodeInput, this.observability && sampled ? {
764
+ config: this.observability,
765
+ flowId: "flow",
766
+ executionId,
767
+ stepId: step.id,
768
+ stepIndex,
769
+ traceContext,
770
+ metadata: this.metadata
771
+ } : void 0);
772
+ artifacts[step.id] = result2.output;
773
+ metrics.push(...result2.metrics);
774
+ completedSteps.push(step.id);
775
+ const stepDuration = Date.now() - stepStartTime;
776
+ callbacks?.onStepComplete?.(step.id, stepIndex, step.type, stepDuration);
777
+ if (this.observability && sampled && traceContext && executionId && stepSpanId) {
778
+ const leafMetrics = result2.metrics.filter((m) => m.metadata?.kind === "leaf");
779
+ const wrapperMetric = result2.metrics.find((m) => m.metadata?.kind === "wrapper");
780
+ const stepLeafMetrics = result2.metrics.filter(
781
+ (m) => m.configStepId === step.id && m.metadata?.kind === "leaf"
782
+ );
783
+ const isConsensus = stepLeafMetrics.length > 1;
784
+ const stepOwnLeafMetric = isConsensus ? void 0 : stepLeafMetrics[0];
785
+ const hasOtherChildMetrics = result2.metrics.some((m) => m.configStepId !== step.id);
786
+ const hasChildMetrics = isConsensus || hasOtherChildMetrics || leafMetrics.length > 1;
787
+ let metricKind;
788
+ let ownDuration;
789
+ let ownCost;
790
+ let ownInputTokens;
791
+ let ownOutputTokens;
792
+ let ownCacheCreationTokens;
793
+ let ownCacheReadTokens;
794
+ if (isConsensus) {
795
+ metricKind = "wrapper";
796
+ ownDuration = wrapperMetric?.metadata?.overheadMs ?? 0;
797
+ ownCost = 0;
798
+ ownInputTokens = 0;
799
+ ownOutputTokens = 0;
800
+ ownCacheCreationTokens = void 0;
801
+ ownCacheReadTokens = void 0;
802
+ } else if (stepOwnLeafMetric) {
803
+ metricKind = hasOtherChildMetrics ? "wrapper" : "leaf";
804
+ ownDuration = stepOwnLeafMetric.ms ?? 0;
805
+ ownCost = stepOwnLeafMetric.costUSD ?? 0;
806
+ ownInputTokens = stepOwnLeafMetric.inputTokens ?? 0;
807
+ ownOutputTokens = stepOwnLeafMetric.outputTokens ?? 0;
808
+ ownCacheCreationTokens = stepOwnLeafMetric.cacheCreationInputTokens;
809
+ ownCacheReadTokens = stepOwnLeafMetric.cacheReadInputTokens;
810
+ } else if (wrapperMetric || hasChildMetrics) {
811
+ metricKind = "wrapper";
812
+ ownDuration = wrapperMetric?.metadata?.overheadMs ?? 0;
813
+ ownCost = 0;
814
+ ownInputTokens = 0;
815
+ ownOutputTokens = 0;
816
+ ownCacheCreationTokens = void 0;
817
+ ownCacheReadTokens = void 0;
818
+ } else {
819
+ metricKind = "prep";
820
+ ownDuration = stepDuration;
821
+ ownCost = 0;
822
+ ownInputTokens = 0;
823
+ ownOutputTokens = 0;
824
+ ownCacheCreationTokens = void 0;
825
+ ownCacheReadTokens = void 0;
826
+ }
827
+ const firstMetric = result2.metrics.length > 0 ? result2.metrics[0] : void 0;
828
+ const stepEndContext = {
829
+ flowId: "flow",
830
+ executionId,
831
+ stepId: step.id,
832
+ stepIndex,
833
+ timestamp: Date.now(),
834
+ startTime: stepStartTime,
835
+ duration: ownDuration,
836
+ output: result2.output,
837
+ usage: {
838
+ inputTokens: ownInputTokens,
839
+ outputTokens: ownOutputTokens,
840
+ totalTokens: ownInputTokens + ownOutputTokens,
841
+ cacheCreationInputTokens: ownCacheCreationTokens,
842
+ cacheReadInputTokens: ownCacheReadTokens
843
+ },
844
+ cost: ownCost,
845
+ metricKind,
846
+ otelAttributes: buildStepAttributes({
847
+ stepType: "conditional",
848
+ provider: firstMetric?.provider,
849
+ model: firstMetric?.model,
850
+ inputTokens: ownInputTokens,
851
+ outputTokens: ownOutputTokens
852
+ }),
853
+ metadata: this.metadata,
854
+ traceContext,
855
+ spanId: stepSpanId
856
+ };
857
+ await executeHook(this.observability.onStepEnd, {
858
+ hookName: "onStepEnd",
859
+ config: this.observability,
860
+ context: stepEndContext
861
+ });
862
+ }
863
+ lastNonOutputData = result2.output;
864
+ currentData = result2.output;
865
+ } else if (step.type === "forEach") {
866
+ if (!Array.isArray(currentData)) {
867
+ throw new Error(`forEach step "${step.id}" requires array input, got ${typeof currentData}`);
868
+ }
869
+ const items = currentData;
870
+ const batchId = executionId ? `${executionId}-batch-${stepIndex}` : `batch-${stepIndex}`;
871
+ const batchStartTime = Date.now();
872
+ if (this.observability && sampled && traceContext && executionId) {
873
+ const batchStartContext = {
874
+ flowId: "flow",
875
+ executionId,
876
+ batchId,
877
+ stepId: step.id,
878
+ totalItems: items.length,
879
+ timestamp: batchStartTime,
880
+ metadata: this.metadata,
881
+ traceContext
882
+ };
883
+ await executeHook(this.observability.onBatchStart, {
884
+ hookName: "onBatchStart",
885
+ config: this.observability,
886
+ context: batchStartContext
887
+ });
888
+ }
889
+ const results = await Promise.allSettled(
890
+ items.map(async (item, itemIndex) => {
891
+ const itemStartTime = Date.now();
892
+ const itemSpanId = this.traceContextManager && sampled ? generateSpanId() : void 0;
893
+ if (this.observability && sampled && traceContext && executionId) {
894
+ const batchItemContext = {
895
+ flowId: "flow",
896
+ executionId,
897
+ batchId,
898
+ stepId: step.id,
899
+ itemIndex,
900
+ totalItems: items.length,
901
+ timestamp: itemStartTime,
902
+ item,
903
+ metadata: this.metadata,
904
+ traceContext
905
+ };
906
+ await executeHook(this.observability.onBatchItemStart, {
907
+ hookName: "onBatchItemStart",
908
+ config: this.observability,
909
+ context: batchItemContext
910
+ });
911
+ }
912
+ try {
913
+ const childFlow = step.childFlow(item);
914
+ const builtFlow = childFlow.build();
915
+ const flowInput = item && typeof item === "object" && "input" in item ? item.input : item;
916
+ const result2 = await builtFlow.run(flowInput);
917
+ let itemResult;
918
+ if ("results" in result2 && Array.isArray(result2.results)) {
919
+ const aggregatedMetrics = (result2.results || []).flatMap((r) => r && r.metrics || []);
920
+ const aggregatedArtifacts = (result2.results || []).map((r) => r && r.artifacts);
921
+ itemResult = {
922
+ output: (result2.results || []).map((r) => r && r.output),
923
+ metrics: aggregatedMetrics,
924
+ artifacts: aggregatedArtifacts
925
+ };
926
+ } else {
927
+ const flowResult = result2;
928
+ itemResult = {
929
+ output: flowResult.output,
930
+ metrics: flowResult.metrics || [],
931
+ artifacts: flowResult.artifacts || {}
932
+ };
933
+ }
934
+ if (this.observability && sampled && traceContext && executionId) {
935
+ const batchItemEndContext = {
936
+ flowId: "flow",
937
+ executionId,
938
+ batchId,
939
+ stepId: step.id,
940
+ itemIndex,
941
+ totalItems: items.length,
942
+ item,
943
+ timestamp: Date.now(),
944
+ duration: Date.now() - itemStartTime,
945
+ result: itemResult.output,
946
+ status: "success",
947
+ metadata: this.metadata,
948
+ traceContext
949
+ };
950
+ await executeHook(this.observability.onBatchItemEnd, {
951
+ hookName: "onBatchItemEnd",
952
+ config: this.observability,
953
+ context: batchItemEndContext
954
+ });
955
+ }
956
+ return itemResult;
957
+ } catch (error) {
958
+ console.error("[forEach error]", error);
959
+ if (this.observability && sampled && traceContext && executionId) {
960
+ const batchItemEndContext = {
961
+ flowId: "flow",
962
+ executionId,
963
+ batchId,
964
+ stepId: step.id,
965
+ itemIndex,
966
+ totalItems: items.length,
967
+ item,
968
+ timestamp: Date.now(),
969
+ duration: Date.now() - itemStartTime,
970
+ result: null,
971
+ status: "failed",
972
+ error: error instanceof Error ? error : new Error(String(error)),
973
+ metadata: this.metadata,
974
+ traceContext
975
+ };
976
+ await executeHook(this.observability.onBatchItemEnd, {
977
+ hookName: "onBatchItemEnd",
978
+ config: this.observability,
979
+ context: batchItemEndContext
980
+ });
981
+ }
982
+ return {
983
+ output: null,
984
+ error: error.message || String(error),
985
+ metrics: [],
986
+ artifacts: {}
987
+ };
988
+ }
989
+ })
990
+ );
991
+ const flowResults = results.map((result2, index) => {
992
+ if (result2.status === "fulfilled") {
993
+ return result2.value;
994
+ } else {
995
+ return {
996
+ output: null,
997
+ error: result2.reason,
998
+ metrics: [],
999
+ artifacts: {}
1000
+ };
1001
+ }
1002
+ });
1003
+ artifacts[step.id] = flowResults.map((r) => r.artifacts);
1004
+ metrics.push(...flowResults.flatMap((r) => r.metrics));
1005
+ completedSteps.push(step.id);
1006
+ const successfulCount = flowResults.filter((r) => !r.error).length;
1007
+ const failedCount = flowResults.filter((r) => r.error).length;
1008
+ if (this.observability && sampled && traceContext && executionId) {
1009
+ const batchEndContext = {
1010
+ flowId: "flow",
1011
+ executionId,
1012
+ batchId,
1013
+ stepId: step.id,
1014
+ timestamp: Date.now(),
1015
+ startTime: batchStartTime,
1016
+ duration: Date.now() - batchStartTime,
1017
+ totalItems: items.length,
1018
+ successfulItems: successfulCount,
1019
+ failedItems: failedCount,
1020
+ results: flowResults.map((r) => r.output),
1021
+ metadata: this.metadata,
1022
+ traceContext
1023
+ };
1024
+ await executeHook(this.observability.onBatchEnd, {
1025
+ hookName: "onBatchEnd",
1026
+ config: this.observability,
1027
+ context: batchEndContext
1028
+ });
1029
+ }
1030
+ const stepDuration = Date.now() - stepStartTime;
1031
+ callbacks?.onStepComplete?.(step.id, stepIndex, step.type, stepDuration);
1032
+ return {
1033
+ results: flowResults,
1034
+ metrics,
1035
+ aggregated: aggregateMetrics(metrics),
1036
+ artifacts
1037
+ };
1038
+ }
1039
+ } catch (error) {
1040
+ const err = error instanceof Error ? error : new Error(String(error));
1041
+ callbacks?.onStepError?.(step.id, stepIndex, step.type, err);
1042
+ if (this.observability && sampled && traceContext && executionId && stepSpanId) {
1043
+ const stepErrorTime = Date.now();
1044
+ const stepErrorContext = {
1045
+ flowId: "flow",
1046
+ executionId,
1047
+ stepId: step.id,
1048
+ stepIndex,
1049
+ timestamp: stepErrorTime,
1050
+ startTime: stepStartTime,
1051
+ duration: stepErrorTime - stepStartTime,
1052
+ error: err,
1053
+ errorCode: err.code,
1054
+ willRetry: false,
1055
+ // TODO: Determine if will retry
1056
+ metadata: this.metadata,
1057
+ traceContext,
1058
+ spanId: stepSpanId
1059
+ };
1060
+ await executeHook(this.observability.onStepError, {
1061
+ hookName: "onStepError",
1062
+ config: this.observability,
1063
+ context: stepErrorContext
1064
+ });
1065
+ }
1066
+ const completedStepsStr = completedSteps.length > 0 ? `
1067
+ Completed steps: ${completedSteps.join(" \u2192 ")}` : "\n No steps completed before failure";
1068
+ const artifactsStr = Object.keys(artifacts).length > 0 ? `
1069
+ Partial results available in: ${Object.keys(artifacts).join(", ")}` : "";
1070
+ throw new FlowExecutionError(
1071
+ `Flow execution failed at step "${step.id}" (index ${stepIndex}, type: ${step.type})
1072
+ Error: ${err.message}` + completedStepsStr + artifactsStr,
1073
+ step.id,
1074
+ stepIndex,
1075
+ step.type,
1076
+ completedSteps,
1077
+ err,
1078
+ artifacts
1079
+ // Include partial artifacts for debugging
1080
+ );
1081
+ }
1082
+ }
1083
+ const hasOutputNodes = Object.keys(outputs).length > 0;
1084
+ let result;
1085
+ if (hasOutputNodes) {
1086
+ const outputCount = Object.keys(outputs).length;
1087
+ if (outputCount === 1) {
1088
+ const singleOutput = Object.values(outputs)[0];
1089
+ result = {
1090
+ output: singleOutput,
1091
+ outputs,
1092
+ metrics,
1093
+ aggregated: aggregateMetrics(metrics),
1094
+ artifacts
1095
+ };
1096
+ } else {
1097
+ result = {
1098
+ output: outputs,
1099
+ // For backward compatibility, set output to outputs
1100
+ outputs,
1101
+ metrics,
1102
+ aggregated: aggregateMetrics(metrics),
1103
+ artifacts
1104
+ };
1105
+ }
1106
+ } else {
1107
+ result = {
1108
+ output: currentData,
1109
+ metrics,
1110
+ aggregated: aggregateMetrics(metrics),
1111
+ artifacts
1112
+ };
1113
+ }
1114
+ if (this.observability && sampled && traceContext && executionId) {
1115
+ const flowEndTime = Date.now();
1116
+ const aggregated = aggregateMetrics(metrics);
1117
+ const flowStats = {
1118
+ stepsTotal: this.steps.length,
1119
+ stepsCompleted: completedSteps.length,
1120
+ stepsFailed: 0,
1121
+ totalTokens: aggregated.totalInputTokens + aggregated.totalOutputTokens,
1122
+ totalCost: aggregated.totalCostUSD
1123
+ };
1124
+ const flowEndContext = {
1125
+ flowId: "flow",
1126
+ executionId,
1127
+ timestamp: flowEndTime,
1128
+ startTime: flowStartTime,
1129
+ duration: flowEndTime - flowStartTime,
1130
+ output: result.output,
1131
+ stats: flowStats,
1132
+ metadata: this.metadata,
1133
+ traceContext
1134
+ };
1135
+ await executeHook(this.observability.onFlowEnd, {
1136
+ hookName: "onFlowEnd",
1137
+ config: this.observability,
1138
+ context: flowEndContext
1139
+ });
1140
+ if (this.currentExecution) {
1141
+ this.currentExecution.status = "completed";
1142
+ }
1143
+ }
1144
+ return result;
1145
+ } catch (error) {
1146
+ if (this.observability && sampled && traceContext && executionId) {
1147
+ const flowErrorTime = Date.now();
1148
+ const aggregated = aggregateMetrics(metrics);
1149
+ const flowStats = {
1150
+ stepsTotal: this.steps.length,
1151
+ stepsCompleted: completedSteps.length,
1152
+ stepsFailed: 1,
1153
+ totalTokens: aggregated.totalInputTokens + aggregated.totalOutputTokens,
1154
+ totalCost: aggregated.totalCostUSD
1155
+ };
1156
+ const failedStepIndex = completedSteps.length;
1157
+ const flowErrorContext = {
1158
+ flowId: "flow",
1159
+ executionId,
1160
+ timestamp: flowErrorTime,
1161
+ startTime: flowStartTime,
1162
+ duration: flowErrorTime - flowStartTime,
1163
+ error,
1164
+ errorCode: error.code,
1165
+ failedAtStepIndex: failedStepIndex,
1166
+ partialStats: flowStats,
1167
+ metadata: this.metadata,
1168
+ traceContext
1169
+ };
1170
+ await executeHook(this.observability.onFlowError, {
1171
+ hookName: "onFlowError",
1172
+ config: this.observability,
1173
+ context: flowErrorContext
1174
+ });
1175
+ if (this.currentExecution) {
1176
+ this.currentExecution.status = "failed";
1177
+ }
1178
+ }
1179
+ throw error;
1180
+ }
1181
+ }
1182
+ };
1183
+ function createFlow(options) {
1184
+ return new Flow(options);
1185
+ }
1186
+
1187
+ // src/index.ts
1188
+ import { parse as parse2, split as split3, categorize as categorize3, extract as extract2, chunk, combine, trigger as trigger2 } from "@doclo/nodes";
1189
+
1190
+ // src/flow-registry.ts
1191
+ var FLOW_REGISTRY = /* @__PURE__ */ new Map();
1192
+ function registerFlow(id, builder) {
1193
+ if (FLOW_REGISTRY.has(id)) {
1194
+ console.warn(`[Flow Registry] Overwriting existing flow: ${id}`);
1195
+ }
1196
+ FLOW_REGISTRY.set(id, builder);
1197
+ }
1198
+ function getFlow(id) {
1199
+ return FLOW_REGISTRY.get(id);
1200
+ }
1201
+ function hasFlow(id) {
1202
+ return FLOW_REGISTRY.has(id);
1203
+ }
1204
+ function unregisterFlow(id) {
1205
+ return FLOW_REGISTRY.delete(id);
1206
+ }
1207
+ function clearRegistry() {
1208
+ FLOW_REGISTRY.clear();
1209
+ }
1210
+ function listFlows() {
1211
+ return Array.from(FLOW_REGISTRY.keys());
1212
+ }
1213
+ function getFlowCount() {
1214
+ return FLOW_REGISTRY.size;
1215
+ }
1216
+
1217
+ // src/serialization.ts
1218
+ import { parse, extract, split as split2, categorize as categorize2, trigger, output } from "@doclo/nodes";
1219
+
1220
+ // src/composite-nodes.ts
1221
+ import { categorize, split } from "@doclo/nodes";
1222
+ function parseProviderName(name) {
1223
+ const colonIndex = name.indexOf(":");
1224
+ if (colonIndex === -1) {
1225
+ return { provider: name, model: "unknown" };
1226
+ }
1227
+ return {
1228
+ provider: name.substring(0, colonIndex),
1229
+ model: name.substring(colonIndex + 1)
1230
+ };
1231
+ }
1232
+ function parseRef(refString) {
1233
+ if (!refString) return null;
1234
+ const atIndex = refString.indexOf("@");
1235
+ if (atIndex === -1) {
1236
+ return { id: refString, version: void 0 };
1237
+ }
1238
+ return {
1239
+ id: refString.substring(0, atIndex),
1240
+ version: refString.substring(atIndex + 1)
1241
+ };
1242
+ }
1243
+ function flattenMetrics(childMetrics, prefix) {
1244
+ return childMetrics.map((m) => ({
1245
+ ...m,
1246
+ step: `${prefix}.${m.step}`,
1247
+ // @ts-ignore - Add metadata for nested metrics (not in official type but works at runtime)
1248
+ nested: true
1249
+ }));
1250
+ }
1251
+ function createConditionalCompositeNode(config) {
1252
+ const { stepId, categorizeConfig, branches, providers, flows } = config;
1253
+ return {
1254
+ key: "conditional-composite",
1255
+ run: async (input, ctx) => {
1256
+ const t0 = Date.now();
1257
+ let selectedCategory;
1258
+ let phase = "categorize";
1259
+ try {
1260
+ const categorizeNode = categorize({
1261
+ ...categorizeConfig,
1262
+ provider: providers[categorizeConfig.providerRef],
1263
+ categories: categorizeConfig.categories || Object.keys(branches)
1264
+ });
1265
+ const categorizeT0 = Date.now();
1266
+ const categorizeCostTracker = [];
1267
+ const categorizeCtx = {
1268
+ stepId,
1269
+ // Use composite step's ID so categorize metric is attributed to this step
1270
+ metrics: { push: (m) => categorizeCostTracker.push(m) },
1271
+ artifacts: ctx?.artifacts ?? {},
1272
+ emit: ctx?.emit ?? (() => {
1273
+ }),
1274
+ // No-op if emit not provided
1275
+ observability: ctx?.observability
1276
+ };
1277
+ const categorizeResult = await categorizeNode.run(input, categorizeCtx);
1278
+ selectedCategory = categorizeResult.category;
1279
+ categorizeCostTracker.forEach((m) => ctx?.metrics?.push(m));
1280
+ if (ctx?.emit) {
1281
+ ctx.emit(`${stepId}:category`, selectedCategory);
1282
+ }
1283
+ phase = "branch";
1284
+ if (!branches[selectedCategory]) {
1285
+ throw new Error(
1286
+ `No branch defined for category "${selectedCategory}". Available branches: ${Object.keys(branches).join(", ")}`
1287
+ );
1288
+ }
1289
+ const branchFlowDef = resolveBranchFlow(branches[selectedCategory], flows);
1290
+ const branchFlow = buildFlowFromConfig(
1291
+ branchFlowDef,
1292
+ providers,
1293
+ flows,
1294
+ ctx?.observability?.config ? {
1295
+ observability: ctx.observability.config,
1296
+ metadata: {
1297
+ ...ctx.observability?.metadata,
1298
+ parentNode: stepId,
1299
+ phase: "branch",
1300
+ category: selectedCategory
1301
+ }
1302
+ } : void 0
1303
+ );
1304
+ const branchT0 = Date.now();
1305
+ const branchResultRaw = await branchFlow.run(input);
1306
+ if (!isSingleFlowResult(branchResultRaw)) {
1307
+ throw new Error("Branch flow returned batch result instead of single result");
1308
+ }
1309
+ const branchResult = branchResultRaw;
1310
+ if (ctx?.metrics && branchResult.metrics) {
1311
+ const branchMetrics = flattenMetrics(
1312
+ branchResult.metrics,
1313
+ `${stepId}.branch.${selectedCategory}`
1314
+ );
1315
+ branchMetrics.forEach((m) => ctx.metrics.push(m));
1316
+ }
1317
+ if (ctx?.emit) {
1318
+ ctx.emit(`${stepId}:branchOutput`, branchResult.output);
1319
+ ctx.emit(`${stepId}:branchArtifacts`, branchResult.artifacts);
1320
+ }
1321
+ const categorizeCost = categorizeCostTracker.reduce((sum, m) => sum + (m.costUSD ?? 0), 0);
1322
+ const branchCost = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.costUSD ?? 0), 0) : 0;
1323
+ const aggregateCost = categorizeCost + branchCost;
1324
+ const totalMs = Date.now() - t0;
1325
+ const categorizeMs = categorizeCostTracker.reduce((sum, m) => sum + (m.ms ?? 0), 0);
1326
+ const branchMs = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.ms ?? 0), 0) : 0;
1327
+ const overheadMs = totalMs - categorizeMs - branchMs;
1328
+ if (ctx?.metrics) {
1329
+ const provider = providers[categorizeConfig.providerRef];
1330
+ const { provider: providerName, model } = parseProviderName(provider.name ?? "");
1331
+ const promptRefData = parseRef(categorizeConfig.promptRef);
1332
+ const wrapperMetric = {
1333
+ step: stepId,
1334
+ configStepId: ctx.stepId,
1335
+ startMs: t0,
1336
+ provider: providerName,
1337
+ model,
1338
+ ms: totalMs,
1339
+ costUSD: aggregateCost,
1340
+ // Total cost from categorize + branch
1341
+ attemptNumber: 1,
1342
+ // Composite wrappers don't retry, always 1
1343
+ metadata: {
1344
+ kind: "wrapper",
1345
+ // Distinguish wrapper from leaf metrics
1346
+ type: "conditional",
1347
+ rollup: true,
1348
+ // Duration includes child work
1349
+ overheadMs,
1350
+ // Pure wrapper overhead (flow orchestration)
1351
+ category: selectedCategory,
1352
+ branchStepCount: branchResult.metrics?.length || 0,
1353
+ branchFlowId: typeof branches[selectedCategory] === "object" && "flowRef" in branches[selectedCategory] ? branches[selectedCategory].flowRef : "inline",
1354
+ // Include prompt metadata if available
1355
+ ...promptRefData && {
1356
+ promptId: promptRefData.id,
1357
+ ...promptRefData.version && { promptVersion: promptRefData.version }
1358
+ }
1359
+ }
1360
+ };
1361
+ ctx.metrics.push(wrapperMetric);
1362
+ }
1363
+ return branchResult.output;
1364
+ } catch (error) {
1365
+ const err = error instanceof Error ? error : new Error(String(error));
1366
+ if (ctx?.metrics) {
1367
+ ctx.metrics.push({
1368
+ step: stepId,
1369
+ configStepId: ctx.stepId,
1370
+ startMs: t0,
1371
+ ms: Date.now() - t0,
1372
+ costUSD: 0,
1373
+ attemptNumber: 1,
1374
+ // @ts-ignore - Add error field
1375
+ error: err.message,
1376
+ metadata: {
1377
+ kind: "wrapper",
1378
+ type: "conditional",
1379
+ failed: true,
1380
+ category: selectedCategory,
1381
+ failedPhase: phase
1382
+ }
1383
+ });
1384
+ }
1385
+ throw new Error(
1386
+ `Conditional step "${stepId}" failed (${selectedCategory ? `category: ${selectedCategory}, ` : ""}phase: ${phase}): ` + err.message
1387
+ );
1388
+ }
1389
+ }
1390
+ };
1391
+ }
1392
+ function createForEachCompositeNode(config) {
1393
+ const { stepId, splitConfig, itemFlow, providers, flows } = config;
1394
+ return {
1395
+ key: "forEach-composite",
1396
+ run: async (input, ctx) => {
1397
+ const t0 = Date.now();
1398
+ let items;
1399
+ let phase = "split";
1400
+ try {
1401
+ const splitNode = split({
1402
+ provider: providers[splitConfig.providerRef],
1403
+ ...splitConfig
1404
+ });
1405
+ const splitT0 = Date.now();
1406
+ const splitCostTracker = [];
1407
+ const splitCtx = {
1408
+ stepId,
1409
+ // Use composite step's ID for attribution
1410
+ metrics: { push: (m) => splitCostTracker.push(m) },
1411
+ artifacts: ctx?.artifacts ?? {},
1412
+ emit: ctx?.emit ?? (() => {
1413
+ }),
1414
+ // No-op if emit not provided
1415
+ observability: ctx?.observability
1416
+ };
1417
+ const splitResult = await splitNode.run(input, splitCtx);
1418
+ items = splitResult;
1419
+ splitCostTracker.forEach((m) => ctx?.metrics?.push(m));
1420
+ if (!Array.isArray(items)) {
1421
+ throw new Error(
1422
+ `Split node did not return an array. Got: ${typeof items}`
1423
+ );
1424
+ }
1425
+ if (ctx?.emit) {
1426
+ ctx.emit(`${stepId}:itemCount`, items.length);
1427
+ }
1428
+ phase = "forEach";
1429
+ const itemFlowDef = resolveBranchFlow(itemFlow, flows);
1430
+ const itemFlowResults = [];
1431
+ const results = await Promise.allSettled(
1432
+ items.map(async (item, index) => {
1433
+ const flow = buildFlowFromConfig(
1434
+ itemFlowDef,
1435
+ providers,
1436
+ flows,
1437
+ ctx?.observability?.config ? {
1438
+ observability: ctx.observability.config,
1439
+ metadata: {
1440
+ ...ctx.observability?.metadata,
1441
+ parentNode: stepId,
1442
+ phase: "forEach",
1443
+ itemIndex: index,
1444
+ totalItems: items.length
1445
+ }
1446
+ } : void 0
1447
+ );
1448
+ const itemT0 = Date.now();
1449
+ const resultRaw = await flow.run(item.input);
1450
+ if (!isSingleFlowResult(resultRaw)) {
1451
+ throw new Error("Item flow returned batch result instead of single result");
1452
+ }
1453
+ const result = resultRaw;
1454
+ itemFlowResults.push(result);
1455
+ if (ctx?.metrics && result.metrics) {
1456
+ const itemMetrics = flattenMetrics(
1457
+ result.metrics,
1458
+ `${stepId}.item[${index}]`
1459
+ );
1460
+ itemMetrics.forEach((m) => ctx.metrics.push(m));
1461
+ }
1462
+ return result.output;
1463
+ })
1464
+ );
1465
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
1466
+ const failureCount = results.filter((r) => r.status === "rejected").length;
1467
+ const splitCost = splitCostTracker.reduce((sum, m) => sum + (m.costUSD ?? 0), 0);
1468
+ const itemsCost = itemFlowResults.reduce((sum, result) => {
1469
+ const itemCost = result.metrics ? result.metrics.reduce((s, m) => s + (m.costUSD ?? 0), 0) : 0;
1470
+ return sum + itemCost;
1471
+ }, 0);
1472
+ const aggregateCost = splitCost + itemsCost;
1473
+ const totalMs = Date.now() - t0;
1474
+ const splitMs = splitCostTracker.reduce((sum, m) => sum + (m.ms ?? 0), 0);
1475
+ const itemsMs = itemFlowResults.reduce((sum, result) => {
1476
+ const itemMs = result.metrics ? result.metrics.reduce((s, m) => s + (m.ms ?? 0), 0) : 0;
1477
+ return sum + itemMs;
1478
+ }, 0);
1479
+ const overheadMs = totalMs - splitMs - itemsMs;
1480
+ if (ctx?.emit) {
1481
+ ctx.emit(`${stepId}:results`, results);
1482
+ ctx.emit(`${stepId}:successCount`, successCount);
1483
+ ctx.emit(`${stepId}:failureCount`, failureCount);
1484
+ }
1485
+ if (ctx?.metrics) {
1486
+ const provider = providers[splitConfig.providerRef];
1487
+ const { provider: providerName, model } = parseProviderName(provider.name ?? "");
1488
+ const schemaRefData = parseRef(splitConfig.schemaRef);
1489
+ ctx.metrics.push({
1490
+ step: stepId,
1491
+ configStepId: ctx.stepId,
1492
+ startMs: t0,
1493
+ provider: providerName,
1494
+ model,
1495
+ ms: totalMs,
1496
+ costUSD: aggregateCost,
1497
+ // Total cost from split + all items
1498
+ attemptNumber: 1,
1499
+ // Composite wrappers don't retry, always 1
1500
+ metadata: {
1501
+ kind: "wrapper",
1502
+ // Distinguish wrapper from leaf metrics
1503
+ type: "forEach",
1504
+ rollup: true,
1505
+ // Duration includes child work
1506
+ overheadMs,
1507
+ // Pure wrapper overhead (flow orchestration)
1508
+ itemCount: items.length,
1509
+ successCount,
1510
+ failureCount,
1511
+ itemFlowId: typeof itemFlow === "object" && "flowRef" in itemFlow ? itemFlow.flowRef : "inline",
1512
+ // Include schema metadata if available
1513
+ ...schemaRefData && {
1514
+ schemaId: schemaRefData.id,
1515
+ ...schemaRefData.version && { schemaVersion: schemaRefData.version }
1516
+ }
1517
+ }
1518
+ });
1519
+ }
1520
+ return results;
1521
+ } catch (error) {
1522
+ const err = error instanceof Error ? error : new Error(String(error));
1523
+ if (ctx?.metrics) {
1524
+ ctx.metrics.push({
1525
+ step: stepId,
1526
+ configStepId: ctx.stepId,
1527
+ startMs: t0,
1528
+ ms: Date.now() - t0,
1529
+ costUSD: 0,
1530
+ attemptNumber: 1,
1531
+ // @ts-ignore - Add error field
1532
+ error: err.message,
1533
+ metadata: {
1534
+ kind: "wrapper",
1535
+ type: "forEach",
1536
+ failed: true,
1537
+ itemCount: items?.length,
1538
+ failedPhase: phase
1539
+ }
1540
+ });
1541
+ }
1542
+ throw new Error(
1543
+ `ForEach step "${stepId}" failed (${items ? `itemCount: ${items.length}, ` : ""}phase: ${phase}): ` + err.message
1544
+ );
1545
+ }
1546
+ }
1547
+ };
1548
+ }
1549
+ function resolveBranchFlow(flowOrRef, flows) {
1550
+ if (typeof flowOrRef === "object" && flowOrRef !== null && "flowRef" in flowOrRef) {
1551
+ const flowRef = flowOrRef.flowRef;
1552
+ if (!flows[flowRef]) {
1553
+ throw new Error(
1554
+ `Flow reference "${flowRef}" not found in registry. Available flows: ${Object.keys(flows).join(", ")}`
1555
+ );
1556
+ }
1557
+ return flows[flowRef];
1558
+ }
1559
+ return flowOrRef;
1560
+ }
1561
+
1562
+ // src/serialization.ts
1563
+ function extractNodeMetadata(node2) {
1564
+ return null;
1565
+ }
1566
+ var FlowSerializationError = class extends Error {
1567
+ constructor(message) {
1568
+ super(message);
1569
+ this.name = "FlowSerializationError";
1570
+ }
1571
+ };
1572
+ function isFlowReference(value) {
1573
+ return typeof value === "object" && value !== null && "flowRef" in value && typeof value.flowRef === "string";
1574
+ }
1575
+ function resolveFlowReference(flowOrRef, flows) {
1576
+ if (isFlowReference(flowOrRef)) {
1577
+ if (!flows) {
1578
+ throw new FlowSerializationError(
1579
+ `Flow reference "${flowOrRef.flowRef}" found but no flow registry provided`
1580
+ );
1581
+ }
1582
+ const resolvedFlow = flows[flowOrRef.flowRef];
1583
+ if (!resolvedFlow) {
1584
+ throw new FlowSerializationError(
1585
+ `Flow reference "${flowOrRef.flowRef}" not found in registry. Available flows: ${Object.keys(flows).join(", ")}`
1586
+ );
1587
+ }
1588
+ return resolvedFlow;
1589
+ }
1590
+ return flowOrRef;
1591
+ }
1592
+ function buildFlowFromConfig(flowDef, providers, flows, options) {
1593
+ if (flowDef.version !== "1.0.0") {
1594
+ throw new FlowSerializationError(`Unsupported flow version: ${flowDef.version}`);
1595
+ }
1596
+ const mergedOptions = {
1597
+ ...options,
1598
+ inputValidation: flowDef.inputValidation ?? options?.inputValidation
1599
+ };
1600
+ let flow = createFlow(mergedOptions);
1601
+ for (const step of flowDef.steps) {
1602
+ if (step.type === "step") {
1603
+ const node2 = createNodeFromConfig(step.nodeType, step.config, providers, flows);
1604
+ flow = flow.step(step.id, node2, step.name);
1605
+ } else if (step.type === "conditional") {
1606
+ const node2 = createConditionalCompositeNode({
1607
+ stepId: step.id,
1608
+ categorizeConfig: step.config,
1609
+ branches: step.branches,
1610
+ providers,
1611
+ flows: flows || {}
1612
+ });
1613
+ flow = flow.step(step.id, node2, step.name);
1614
+ } else if (step.type === "forEach") {
1615
+ const node2 = createForEachCompositeNode({
1616
+ stepId: step.id,
1617
+ splitConfig: step.config,
1618
+ itemFlow: step.itemFlow,
1619
+ providers,
1620
+ flows: flows || {}
1621
+ });
1622
+ flow = flow.step(step.id, node2, step.name);
1623
+ } else {
1624
+ const exhaustiveCheck = step;
1625
+ throw new FlowSerializationError(`Unknown step type: ${exhaustiveCheck.type}`);
1626
+ }
1627
+ }
1628
+ return flow.build();
1629
+ }
1630
+ function getByPath(obj, path) {
1631
+ return path.reduce((current, key) => {
1632
+ if (current && typeof current === "object" && key in current) {
1633
+ return current[key];
1634
+ }
1635
+ return void 0;
1636
+ }, obj);
1637
+ }
1638
+ function createInputMapper(mappingConfig) {
1639
+ return (input, context) => {
1640
+ switch (mappingConfig.type) {
1641
+ case "passthrough":
1642
+ return input;
1643
+ case "unwrap":
1644
+ if (input && typeof input === "object" && "input" in input) {
1645
+ return input.input;
1646
+ }
1647
+ return input;
1648
+ case "artifact": {
1649
+ const pathParts = mappingConfig.path.split(".");
1650
+ return getByPath(context.artifacts, pathParts);
1651
+ }
1652
+ case "merge": {
1653
+ const pathParts = mappingConfig.artifactPath.split(".");
1654
+ const artifactValue = getByPath(context.artifacts, pathParts);
1655
+ if (typeof input === "object" && input !== null && typeof artifactValue === "object" && artifactValue !== null) {
1656
+ return { ...input, ...artifactValue };
1657
+ }
1658
+ return input;
1659
+ }
1660
+ case "construct": {
1661
+ const result = {};
1662
+ for (const [fieldName, fieldMapping] of Object.entries(mappingConfig.fields)) {
1663
+ switch (fieldMapping.source) {
1664
+ case "input":
1665
+ if (fieldMapping.path) {
1666
+ const pathParts = fieldMapping.path.split(".");
1667
+ result[fieldName] = getByPath(input, pathParts);
1668
+ } else {
1669
+ result[fieldName] = input;
1670
+ }
1671
+ break;
1672
+ case "artifact": {
1673
+ const pathParts = fieldMapping.path.split(".");
1674
+ result[fieldName] = getByPath(context.artifacts, pathParts);
1675
+ break;
1676
+ }
1677
+ case "literal":
1678
+ result[fieldName] = fieldMapping.value;
1679
+ break;
1680
+ }
1681
+ }
1682
+ return result;
1683
+ }
1684
+ default:
1685
+ return input;
1686
+ }
1687
+ };
1688
+ }
1689
+ function hasProviderRef(config) {
1690
+ return "providerRef" in config && typeof config.providerRef === "string";
1691
+ }
1692
+ function createNodeFromConfig(nodeType, config, providers, flows) {
1693
+ if (nodeType === "trigger") {
1694
+ const cfg = config;
1695
+ if (!flows || !flows[cfg.flowRef]) {
1696
+ throw new FlowSerializationError(
1697
+ `Flow "${cfg.flowRef}" not found in flow registry. Available flows: ${flows ? Object.keys(flows).join(", ") : "none"}`
1698
+ );
1699
+ }
1700
+ const overrideProviders = {};
1701
+ if (cfg.providerOverrides) {
1702
+ for (const [childRef, parentRef] of Object.entries(cfg.providerOverrides)) {
1703
+ if (!providers[parentRef]) {
1704
+ throw new FlowSerializationError(
1705
+ `Provider "${parentRef}" not found in provider registry. Available providers: ${Object.keys(providers).join(", ")}`
1706
+ );
1707
+ }
1708
+ overrideProviders[childRef] = providers[parentRef];
1709
+ }
1710
+ }
1711
+ const flowBuilder = flows[cfg.flowRef];
1712
+ return trigger({
1713
+ flow: flowBuilder,
1714
+ flowId: cfg.flowRef,
1715
+ providers: Object.keys(overrideProviders).length > 0 ? overrideProviders : void 0,
1716
+ mapInput: cfg.inputMapping ? createInputMapper(cfg.inputMapping) : void 0,
1717
+ mergeMetrics: cfg.mergeMetrics,
1718
+ timeout: cfg.timeout
1719
+ });
1720
+ }
1721
+ if (nodeType === "output") {
1722
+ const cfg = config;
1723
+ return output({
1724
+ name: cfg.name,
1725
+ source: cfg.source,
1726
+ transform: cfg.transform,
1727
+ fields: cfg.fields
1728
+ });
1729
+ }
1730
+ if (!hasProviderRef(config)) {
1731
+ throw new FlowSerializationError(
1732
+ `Config for node type "${nodeType}" is missing providerRef`
1733
+ );
1734
+ }
1735
+ const provider = providers[config.providerRef];
1736
+ if (!provider) {
1737
+ throw new FlowSerializationError(
1738
+ `Provider "${config.providerRef}" not found in registry. Available providers: ${Object.keys(providers).join(", ")}`
1739
+ );
1740
+ }
1741
+ switch (nodeType) {
1742
+ case "parse": {
1743
+ const cfg = config;
1744
+ return parse({
1745
+ provider,
1746
+ consensus: cfg.consensus
1747
+ });
1748
+ }
1749
+ case "extract": {
1750
+ const cfg = config;
1751
+ return extract({
1752
+ provider,
1753
+ schema: cfg.schema,
1754
+ consensus: cfg.consensus,
1755
+ reasoning: cfg.reasoning
1756
+ });
1757
+ }
1758
+ case "split": {
1759
+ const cfg = config;
1760
+ return split2({
1761
+ provider,
1762
+ schemas: cfg.schemas,
1763
+ includeOther: cfg.includeOther,
1764
+ consensus: cfg.consensus
1765
+ });
1766
+ }
1767
+ case "categorize": {
1768
+ const cfg = config;
1769
+ return categorize2({
1770
+ provider,
1771
+ categories: cfg.categories,
1772
+ consensus: cfg.consensus
1773
+ });
1774
+ }
1775
+ default:
1776
+ throw new FlowSerializationError(`Unknown node type: ${nodeType}`);
1777
+ }
1778
+ }
1779
+ function defineFlowConfig(config) {
1780
+ return {
1781
+ version: "1.0.0",
1782
+ ...config
1783
+ };
1784
+ }
1785
+
1786
+ // src/validation.ts
1787
+ import { isDebugValidation } from "@doclo/core/runtime/env";
1788
+ function validateJSONSchemaStructure(schema, depth = 0) {
1789
+ const MAX_DEPTH = 50;
1790
+ if (depth > MAX_DEPTH) {
1791
+ return `Schema nesting depth exceeds maximum (${MAX_DEPTH})`;
1792
+ }
1793
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
1794
+ return "Schema must be an object";
1795
+ }
1796
+ const schemaObj = schema;
1797
+ if (!schemaObj.type || typeof schemaObj.type !== "string") {
1798
+ return 'Schema missing "type" property';
1799
+ }
1800
+ const validTypes = ["object", "array", "string", "number", "integer", "boolean", "null"];
1801
+ if (!validTypes.includes(schemaObj.type)) {
1802
+ return `Invalid schema type: "${schemaObj.type}". Must be one of: ${validTypes.join(", ")}`;
1803
+ }
1804
+ if (schemaObj.type === "object") {
1805
+ if (schemaObj.properties && typeof schemaObj.properties !== "object") {
1806
+ return "Schema properties must be an object";
1807
+ }
1808
+ if (schemaObj.required && !Array.isArray(schemaObj.required)) {
1809
+ return "Schema required must be an array";
1810
+ }
1811
+ if (schemaObj.properties && typeof schemaObj.properties === "object" && !Array.isArray(schemaObj.properties)) {
1812
+ const properties = schemaObj.properties;
1813
+ for (const [propName, propSchema] of Object.entries(properties)) {
1814
+ const propError = validateJSONSchemaStructure(propSchema, depth + 1);
1815
+ if (propError) {
1816
+ return `Invalid schema for property "${propName}": ${propError}`;
1817
+ }
1818
+ }
1819
+ }
1820
+ }
1821
+ if (schemaObj.type === "array") {
1822
+ if (!schemaObj.items) {
1823
+ return 'Array schema missing "items" property';
1824
+ }
1825
+ const itemsError = validateJSONSchemaStructure(schemaObj.items, depth + 1);
1826
+ if (itemsError) {
1827
+ return `Invalid array items schema: ${itemsError}`;
1828
+ }
1829
+ }
1830
+ return null;
1831
+ }
1832
+ function calculateFlowNestingDepth(flow, currentDepth = 1) {
1833
+ let maxDepth = currentDepth + 1;
1834
+ for (const step of flow.steps) {
1835
+ let stepDepth = currentDepth + 2;
1836
+ if (step.type === "conditional") {
1837
+ const conditionalStep = step;
1838
+ if (conditionalStep.branches) {
1839
+ for (const branchFlowOrRef of Object.values(conditionalStep.branches)) {
1840
+ if ("flowRef" in branchFlowOrRef) {
1841
+ maxDepth = Math.max(maxDepth, stepDepth + 3);
1842
+ } else {
1843
+ const branchDepth = calculateFlowNestingDepth(branchFlowOrRef, stepDepth + 2);
1844
+ maxDepth = Math.max(maxDepth, branchDepth);
1845
+ }
1846
+ }
1847
+ }
1848
+ } else if (step.type === "forEach") {
1849
+ const forEachStep = step;
1850
+ if (forEachStep.itemFlow) {
1851
+ const itemFlowOrRef = forEachStep.itemFlow;
1852
+ if ("flowRef" in itemFlowOrRef) {
1853
+ maxDepth = Math.max(maxDepth, stepDepth + 2);
1854
+ } else {
1855
+ const itemDepth = calculateFlowNestingDepth(itemFlowOrRef, stepDepth + 1);
1856
+ maxDepth = Math.max(maxDepth, itemDepth);
1857
+ }
1858
+ }
1859
+ } else {
1860
+ const config = step.config;
1861
+ let configDepth = stepDepth + 1;
1862
+ if ("schema" in config && config.schema) {
1863
+ configDepth += 4;
1864
+ }
1865
+ if ("schemas" in config && config.schemas) {
1866
+ configDepth += 4;
1867
+ }
1868
+ maxDepth = Math.max(maxDepth, configDepth);
1869
+ }
1870
+ }
1871
+ return maxDepth;
1872
+ }
1873
+ function validateFlow(flowDef, options = {}) {
1874
+ const errors = [];
1875
+ const warnings = [];
1876
+ const opts = {
1877
+ checkProviders: true,
1878
+ checkSchemas: true,
1879
+ checkVersion: true,
1880
+ ...options
1881
+ };
1882
+ if (opts.checkVersion) {
1883
+ if (!flowDef.version) {
1884
+ errors.push({
1885
+ type: "version_mismatch",
1886
+ message: "Flow definition missing version field"
1887
+ });
1888
+ } else if (flowDef.version !== "1.0.0") {
1889
+ errors.push({
1890
+ type: "version_mismatch",
1891
+ message: `Unsupported flow version: ${flowDef.version}. Expected: 1.0.0`,
1892
+ details: { version: flowDef.version }
1893
+ });
1894
+ }
1895
+ }
1896
+ if (!flowDef.steps || !Array.isArray(flowDef.steps)) {
1897
+ errors.push({
1898
+ type: "invalid_config",
1899
+ message: "Flow definition missing or invalid steps array"
1900
+ });
1901
+ return { valid: false, errors, warnings };
1902
+ }
1903
+ if (flowDef.steps.length === 0) {
1904
+ warnings.push({
1905
+ type: "best_practice",
1906
+ message: "Flow has no steps defined"
1907
+ });
1908
+ }
1909
+ const nestingDepth = calculateFlowNestingDepth(flowDef);
1910
+ if (nestingDepth > 14) {
1911
+ errors.push({
1912
+ type: "invalid_config",
1913
+ message: `Flow nesting depth (${nestingDepth} levels) exceeds recommended maximum (14 levels). This will cause issues with Convex and other databases with 16-level JSON nesting limits. Consider using flow references (flowRef) to reduce nesting depth.`,
1914
+ details: { nestingDepth, limit: 14 }
1915
+ });
1916
+ } else if (nestingDepth > 12) {
1917
+ warnings.push({
1918
+ type: "performance",
1919
+ message: `Flow nesting depth (${nestingDepth} levels) is approaching the database limit (16 levels). Consider using flow references to reduce complexity.`,
1920
+ details: { nestingDepth, warningThreshold: 12 }
1921
+ });
1922
+ }
1923
+ for (const step of flowDef.steps) {
1924
+ const stepId = step.id || "unknown";
1925
+ if (!step.type) {
1926
+ errors.push({
1927
+ type: "invalid_config",
1928
+ stepId,
1929
+ message: "Step missing type field"
1930
+ });
1931
+ continue;
1932
+ }
1933
+ if (step.type === "conditional") {
1934
+ const conditionalStep = step;
1935
+ if (!conditionalStep.branches || typeof conditionalStep.branches !== "object") {
1936
+ errors.push({
1937
+ type: "invalid_config",
1938
+ stepId,
1939
+ message: "Conditional step missing or invalid branches field"
1940
+ });
1941
+ } else {
1942
+ for (const [category, branchFlowOrRef] of Object.entries(conditionalStep.branches)) {
1943
+ if ("flowRef" in branchFlowOrRef) {
1944
+ const flowRef = branchFlowOrRef.flowRef;
1945
+ if (typeof flowRef !== "string" || flowRef.trim() === "") {
1946
+ errors.push({
1947
+ type: "invalid_config",
1948
+ stepId: `${stepId}.${category}`,
1949
+ message: `Branch "${category}": flowRef must be a non-empty string`
1950
+ });
1951
+ }
1952
+ } else {
1953
+ const branchResult = validateFlow(branchFlowOrRef, options);
1954
+ for (const error of branchResult.errors) {
1955
+ errors.push({
1956
+ ...error,
1957
+ stepId: `${stepId}.${category}`,
1958
+ message: `Branch "${category}": ${error.message}`
1959
+ });
1960
+ }
1961
+ for (const warning of branchResult.warnings) {
1962
+ warnings.push({
1963
+ ...warning,
1964
+ stepId: `${stepId}.${category}`,
1965
+ message: `Branch "${category}": ${warning.message}`
1966
+ });
1967
+ }
1968
+ }
1969
+ }
1970
+ }
1971
+ } else if (step.type === "forEach") {
1972
+ const forEachStep = step;
1973
+ if (!forEachStep.itemFlow) {
1974
+ errors.push({
1975
+ type: "invalid_config",
1976
+ stepId,
1977
+ message: "ForEach step missing itemFlow field"
1978
+ });
1979
+ } else {
1980
+ const itemFlowOrRef = forEachStep.itemFlow;
1981
+ if ("flowRef" in itemFlowOrRef) {
1982
+ const flowRef = itemFlowOrRef.flowRef;
1983
+ if (typeof flowRef !== "string" || flowRef.trim() === "") {
1984
+ errors.push({
1985
+ type: "invalid_config",
1986
+ stepId: `${stepId}.itemFlow`,
1987
+ message: `itemFlow: flowRef must be a non-empty string`
1988
+ });
1989
+ }
1990
+ } else {
1991
+ const itemResult = validateFlow(itemFlowOrRef, options);
1992
+ for (const error of itemResult.errors) {
1993
+ errors.push({
1994
+ ...error,
1995
+ stepId: `${stepId}.itemFlow`,
1996
+ message: `Item flow: ${error.message}`
1997
+ });
1998
+ }
1999
+ for (const warning of itemResult.warnings) {
2000
+ warnings.push({
2001
+ ...warning,
2002
+ stepId: `${stepId}.itemFlow`,
2003
+ message: `Item flow: ${warning.message}`
2004
+ });
2005
+ }
2006
+ }
2007
+ }
2008
+ }
2009
+ const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output"];
2010
+ if (!step.nodeType || !validNodeTypes.includes(step.nodeType)) {
2011
+ errors.push({
2012
+ type: "invalid_config",
2013
+ stepId,
2014
+ message: `Invalid node type: ${step.nodeType}. Must be one of: ${validNodeTypes.join(", ")}`,
2015
+ details: { nodeType: step.nodeType }
2016
+ });
2017
+ continue;
2018
+ }
2019
+ if (!step.config) {
2020
+ errors.push({
2021
+ type: "invalid_config",
2022
+ stepId,
2023
+ message: "Step missing config field"
2024
+ });
2025
+ continue;
2026
+ }
2027
+ const config = step.config;
2028
+ const hasProviderRef2 = (cfg) => {
2029
+ return "providerRef" in cfg && typeof cfg.providerRef === "string";
2030
+ };
2031
+ if (step.nodeType !== "trigger" && step.nodeType !== "output") {
2032
+ if (!hasProviderRef2(config)) {
2033
+ errors.push({
2034
+ type: "missing_provider",
2035
+ stepId,
2036
+ message: "Step config missing providerRef"
2037
+ });
2038
+ } else if (opts.checkProviders && opts.providers) {
2039
+ if (!opts.providers[config.providerRef]) {
2040
+ errors.push({
2041
+ type: "missing_provider",
2042
+ stepId,
2043
+ message: `Provider "${config.providerRef}" not found in registry`,
2044
+ details: {
2045
+ providerRef: config.providerRef,
2046
+ availableProviders: Object.keys(opts.providers)
2047
+ }
2048
+ });
2049
+ }
2050
+ }
2051
+ }
2052
+ if (opts.checkSchemas) {
2053
+ switch (step.nodeType) {
2054
+ case "extract": {
2055
+ const cfg = config;
2056
+ if (!cfg.schema) {
2057
+ errors.push({
2058
+ type: "invalid_config",
2059
+ stepId,
2060
+ message: "Extract node missing schema"
2061
+ });
2062
+ } else {
2063
+ const schemaError = validateJSONSchemaStructure(cfg.schema);
2064
+ if (schemaError) {
2065
+ errors.push({
2066
+ type: "invalid_schema",
2067
+ stepId,
2068
+ message: `Invalid JSON schema: ${schemaError}`,
2069
+ details: { schema: cfg.schema }
2070
+ });
2071
+ }
2072
+ }
2073
+ if (cfg.reasoning) {
2074
+ if (cfg.reasoning.effort && !["low", "medium", "high"].includes(cfg.reasoning.effort)) {
2075
+ errors.push({
2076
+ type: "invalid_config",
2077
+ stepId,
2078
+ message: `Invalid reasoning effort: ${cfg.reasoning.effort}. Must be: low, medium, or high`
2079
+ });
2080
+ }
2081
+ }
2082
+ break;
2083
+ }
2084
+ case "split": {
2085
+ const cfg = config;
2086
+ if (!cfg.schemas) {
2087
+ errors.push({
2088
+ type: "invalid_config",
2089
+ stepId,
2090
+ message: "Split node missing schemas"
2091
+ });
2092
+ } else if (typeof cfg.schemas !== "object") {
2093
+ errors.push({
2094
+ type: "invalid_config",
2095
+ stepId,
2096
+ message: "Split node schemas must be an object"
2097
+ });
2098
+ } else {
2099
+ for (const [schemaName, schema] of Object.entries(cfg.schemas)) {
2100
+ const schemaError = validateJSONSchemaStructure(schema);
2101
+ if (schemaError) {
2102
+ errors.push({
2103
+ type: "invalid_schema",
2104
+ stepId,
2105
+ message: `Invalid JSON schema for "${schemaName}": ${schemaError}`,
2106
+ details: { schemaName, schema }
2107
+ });
2108
+ }
2109
+ }
2110
+ if (Object.keys(cfg.schemas).length === 0) {
2111
+ warnings.push({
2112
+ type: "best_practice",
2113
+ stepId,
2114
+ message: "Split node has no schemas defined"
2115
+ });
2116
+ }
2117
+ }
2118
+ break;
2119
+ }
2120
+ case "categorize": {
2121
+ const cfg = config;
2122
+ if (!cfg.categories) {
2123
+ errors.push({
2124
+ type: "invalid_config",
2125
+ stepId,
2126
+ message: "Categorize node missing categories"
2127
+ });
2128
+ } else if (!Array.isArray(cfg.categories)) {
2129
+ errors.push({
2130
+ type: "invalid_config",
2131
+ stepId,
2132
+ message: "Categorize node categories must be an array"
2133
+ });
2134
+ } else if (cfg.categories.length === 0) {
2135
+ warnings.push({
2136
+ type: "best_practice",
2137
+ stepId,
2138
+ message: "Categorize node has no categories defined"
2139
+ });
2140
+ }
2141
+ break;
2142
+ }
2143
+ case "trigger": {
2144
+ const cfg = config;
2145
+ if (!cfg.flowRef) {
2146
+ errors.push({
2147
+ type: "invalid_config",
2148
+ stepId,
2149
+ message: "Trigger node missing flowRef"
2150
+ });
2151
+ }
2152
+ if (cfg.providerOverrides) {
2153
+ if (typeof cfg.providerOverrides !== "object") {
2154
+ errors.push({
2155
+ type: "invalid_config",
2156
+ stepId,
2157
+ message: "Trigger node providerOverrides must be an object"
2158
+ });
2159
+ } else if (opts.checkProviders && opts.providers) {
2160
+ for (const [childRef, parentRef] of Object.entries(cfg.providerOverrides)) {
2161
+ if (!opts.providers[parentRef]) {
2162
+ errors.push({
2163
+ type: "missing_provider",
2164
+ stepId,
2165
+ message: `Provider override "${parentRef}" not found in registry`,
2166
+ details: {
2167
+ childRef,
2168
+ parentRef,
2169
+ availableProviders: Object.keys(opts.providers)
2170
+ }
2171
+ });
2172
+ }
2173
+ }
2174
+ }
2175
+ }
2176
+ if (cfg.inputMapping) {
2177
+ if (!cfg.inputMapping.type) {
2178
+ errors.push({
2179
+ type: "invalid_config",
2180
+ stepId,
2181
+ message: "Trigger node inputMapping missing type field"
2182
+ });
2183
+ } else {
2184
+ const validMappingTypes = ["passthrough", "unwrap", "artifact", "merge", "construct"];
2185
+ if (!validMappingTypes.includes(cfg.inputMapping.type)) {
2186
+ errors.push({
2187
+ type: "invalid_config",
2188
+ stepId,
2189
+ message: `Invalid inputMapping type: ${cfg.inputMapping.type}. Must be one of: ${validMappingTypes.join(", ")}`
2190
+ });
2191
+ }
2192
+ if (cfg.inputMapping.type === "artifact" && !("path" in cfg.inputMapping)) {
2193
+ errors.push({
2194
+ type: "invalid_config",
2195
+ stepId,
2196
+ message: 'Trigger node inputMapping type "artifact" requires path field'
2197
+ });
2198
+ }
2199
+ if (cfg.inputMapping.type === "merge" && !("artifactPath" in cfg.inputMapping)) {
2200
+ errors.push({
2201
+ type: "invalid_config",
2202
+ stepId,
2203
+ message: 'Trigger node inputMapping type "merge" requires artifactPath field'
2204
+ });
2205
+ }
2206
+ if (cfg.inputMapping.type === "construct" && !("fields" in cfg.inputMapping)) {
2207
+ errors.push({
2208
+ type: "invalid_config",
2209
+ stepId,
2210
+ message: 'Trigger node inputMapping type "construct" requires fields object'
2211
+ });
2212
+ }
2213
+ }
2214
+ }
2215
+ if (cfg.timeout !== void 0) {
2216
+ if (typeof cfg.timeout !== "number" || cfg.timeout <= 0) {
2217
+ errors.push({
2218
+ type: "invalid_config",
2219
+ stepId,
2220
+ message: "Trigger node timeout must be a positive number"
2221
+ });
2222
+ }
2223
+ }
2224
+ if (cfg.mergeMetrics !== void 0 && typeof cfg.mergeMetrics !== "boolean") {
2225
+ errors.push({
2226
+ type: "invalid_config",
2227
+ stepId,
2228
+ message: "Trigger node mergeMetrics must be a boolean"
2229
+ });
2230
+ }
2231
+ break;
2232
+ }
2233
+ case "output": {
2234
+ const cfg = config;
2235
+ if (cfg.transform) {
2236
+ const validTransforms = ["first", "last", "merge", "pick"];
2237
+ if (!validTransforms.includes(cfg.transform)) {
2238
+ errors.push({
2239
+ type: "invalid_config",
2240
+ stepId,
2241
+ message: `Invalid output transform: ${cfg.transform}. Must be one of: ${validTransforms.join(", ")}`
2242
+ });
2243
+ }
2244
+ if (cfg.transform === "pick" && (!cfg.fields || cfg.fields.length === 0)) {
2245
+ errors.push({
2246
+ type: "invalid_config",
2247
+ stepId,
2248
+ message: 'Output transform "pick" requires fields array'
2249
+ });
2250
+ }
2251
+ }
2252
+ if (cfg.fields && !Array.isArray(cfg.fields)) {
2253
+ errors.push({
2254
+ type: "invalid_config",
2255
+ stepId,
2256
+ message: "Output fields must be an array"
2257
+ });
2258
+ }
2259
+ if (cfg.source) {
2260
+ if (typeof cfg.source !== "string" && !Array.isArray(cfg.source)) {
2261
+ errors.push({
2262
+ type: "invalid_config",
2263
+ stepId,
2264
+ message: "Output source must be a string or array of strings"
2265
+ });
2266
+ } else if (Array.isArray(cfg.source) && cfg.source.length === 0) {
2267
+ warnings.push({
2268
+ type: "best_practice",
2269
+ stepId,
2270
+ message: "Output source array is empty"
2271
+ });
2272
+ }
2273
+ }
2274
+ break;
2275
+ }
2276
+ }
2277
+ if ("consensus" in config && config.consensus) {
2278
+ const consensus = config.consensus;
2279
+ if (!consensus.runs || consensus.runs < 1) {
2280
+ errors.push({
2281
+ type: "invalid_config",
2282
+ stepId,
2283
+ message: "Consensus runs must be >= 1"
2284
+ });
2285
+ }
2286
+ if (consensus.strategy && !["majority", "unanimous"].includes(consensus.strategy)) {
2287
+ errors.push({
2288
+ type: "invalid_config",
2289
+ stepId,
2290
+ message: `Invalid consensus strategy: ${consensus.strategy}. Must be: majority or unanimous`
2291
+ });
2292
+ }
2293
+ if (consensus.onTie && !["random", "fail", "retry"].includes(consensus.onTie)) {
2294
+ errors.push({
2295
+ type: "invalid_config",
2296
+ stepId,
2297
+ message: `Invalid consensus onTie: ${consensus.onTie}. Must be: random, fail, or retry`
2298
+ });
2299
+ }
2300
+ if (consensus.runs > 1) {
2301
+ warnings.push({
2302
+ type: "performance",
2303
+ stepId,
2304
+ message: `Consensus with ${consensus.runs} runs will execute the step ${consensus.runs} times`
2305
+ });
2306
+ }
2307
+ }
2308
+ }
2309
+ }
2310
+ return {
2311
+ valid: errors.length === 0,
2312
+ errors,
2313
+ warnings
2314
+ };
2315
+ }
2316
+ function validateFlowOrThrow(flowDef, options = {}) {
2317
+ const result = validateFlow(flowDef, options);
2318
+ if (!result.valid) {
2319
+ const errorMessages = result.errors.map(
2320
+ (e) => e.stepId ? `[${e.stepId}] ${e.message}` : e.message
2321
+ ).join("\n");
2322
+ throw new Error(`Flow validation failed:
2323
+ ${errorMessages}`);
2324
+ }
2325
+ if (result.warnings.length > 0 && isDebugValidation()) {
2326
+ console.warn("[Flow Validation] Warnings:");
2327
+ for (const warning of result.warnings) {
2328
+ const prefix = warning.stepId ? `[${warning.stepId}]` : "";
2329
+ console.warn(` ${prefix} ${warning.message}`);
2330
+ }
2331
+ }
2332
+ }
2333
+
2334
+ // src/index.ts
2335
+ import { bufferToDataUri, bufferToBase64 } from "@doclo/core";
2336
+
2337
+ // src/multi-provider-flow.ts
2338
+ import { runPipeline as runPipeline2 } from "@doclo/core";
2339
+ import { parseNode, extractNode } from "@doclo/nodes";
2340
+ import { buildLLMProvider } from "@doclo/providers-llm";
2341
+ function buildMultiProviderFlow(opts) {
2342
+ const parse3 = parseNode({ ocr: opts.ocr });
2343
+ const coreLLMProvider = buildLLMProvider({
2344
+ providers: opts.llmConfigs,
2345
+ maxRetries: opts.maxRetries ?? 2,
2346
+ retryDelay: opts.retryDelay ?? 1e3,
2347
+ useExponentialBackoff: true,
2348
+ circuitBreakerThreshold: opts.circuitBreakerThreshold ?? 3
2349
+ });
2350
+ const mkPrompt = (ir) => `Extract JSON matching the schema fields: vessel, port, quantity_mt.
2351
+ Document (first page preview):
2352
+ ${ir.pages[0]?.lines.slice(0, 50).map((l) => l.text).join("\n")}`;
2353
+ const extract3 = extractNode({
2354
+ llm: coreLLMProvider,
2355
+ schema: simpleSchema,
2356
+ makePrompt: mkPrompt
2357
+ });
2358
+ return {
2359
+ async run(input) {
2360
+ const parsed = await runPipeline2([parse3], input);
2361
+ const ir = parsed.output;
2362
+ const result = await runPipeline2([extract3], ir);
2363
+ return {
2364
+ ir,
2365
+ output: result.output,
2366
+ metrics: [...parsed.metrics, ...result.metrics],
2367
+ artifacts: {
2368
+ parse: parsed.artifacts.parse,
2369
+ extract: result.artifacts.extract
2370
+ }
2371
+ };
2372
+ }
2373
+ };
2374
+ }
2375
+
2376
+ // src/vlm-direct-flow.ts
2377
+ import { runPipeline as runPipeline3 } from "@doclo/core";
2378
+ import { node } from "@doclo/core";
2379
+ import { buildLLMProvider as buildLLMProvider2 } from "@doclo/providers-llm";
2380
+ function buildVLMDirectFlow(opts) {
2381
+ const coreLLMProvider = buildLLMProvider2({
2382
+ providers: opts.llmConfigs,
2383
+ maxRetries: opts.maxRetries ?? 2,
2384
+ retryDelay: opts.retryDelay ?? 1e3,
2385
+ useExponentialBackoff: true,
2386
+ circuitBreakerThreshold: opts.circuitBreakerThreshold ?? 3
2387
+ });
2388
+ const vlmExtract = node(
2389
+ "vlm_extract",
2390
+ async (input, ctx) => {
2391
+ const t0 = Date.now();
2392
+ const prompt = {
2393
+ text: `You are a document data extraction expert. Extract the following fields from this maritime document:
2394
+ - vessel: The vessel/ship name
2395
+ - port: The port name or location
2396
+ - quantity_mt: The quantity in metric tons (MT)
2397
+
2398
+ Return ONLY a JSON object with these fields. Use null if a field is not found.`
2399
+ };
2400
+ let isPDF = false;
2401
+ if (input.url) {
2402
+ isPDF = input.url.endsWith(".pdf") || input.url.toLowerCase().includes(".pdf");
2403
+ } else if (input.base64) {
2404
+ isPDF = input.base64.startsWith("data:application/pdf");
2405
+ }
2406
+ if (isPDF) {
2407
+ prompt.pdfs = [];
2408
+ if (input.url) {
2409
+ if (input.url.startsWith("data:")) {
2410
+ const base64Data = input.url.replace(/^data:application\/pdf;base64,/, "");
2411
+ prompt.pdfs.push({ base64: base64Data });
2412
+ } else {
2413
+ prompt.pdfs.push({ url: input.url });
2414
+ }
2415
+ } else if (input.base64) {
2416
+ const base64Data = input.base64.startsWith("data:") ? input.base64.replace(/^data:application\/pdf;base64,/, "") : input.base64;
2417
+ prompt.pdfs.push({ base64: base64Data });
2418
+ }
2419
+ } else {
2420
+ prompt.images = [];
2421
+ if (input.url) {
2422
+ if (input.url.startsWith("data:")) {
2423
+ const base64Data = input.url.replace(/^data:image\/[^;]+;base64,/, "");
2424
+ const mimeType = input.url.match(/^data:(image\/[^;]+);/)?.[1] || "image/jpeg";
2425
+ prompt.images.push({ base64: base64Data, mimeType });
2426
+ } else {
2427
+ prompt.images.push({ url: input.url, mimeType: "image/jpeg" });
2428
+ }
2429
+ } else if (input.base64) {
2430
+ const base64Data = input.base64.startsWith("data:") ? input.base64.replace(/^data:image\/[^;]+;base64,/, "") : input.base64;
2431
+ const mimeType = input.base64.startsWith("data:") ? input.base64.match(/^data:(image\/[^;]+);/)?.[1] || "image/jpeg" : "image/jpeg";
2432
+ prompt.images.push({ base64: base64Data, mimeType });
2433
+ }
2434
+ }
2435
+ const { json, costUSD, inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } = await coreLLMProvider.completeJson({
2436
+ prompt,
2437
+ schema: simpleSchema
2438
+ });
2439
+ ctx.metrics.push({
2440
+ step: "vlm_extract",
2441
+ startMs: t0,
2442
+ provider: coreLLMProvider.name,
2443
+ model: "unknown",
2444
+ ms: Date.now() - t0,
2445
+ costUSD,
2446
+ inputTokens,
2447
+ outputTokens,
2448
+ cacheCreationInputTokens,
2449
+ cacheReadInputTokens,
2450
+ attemptNumber: 1,
2451
+ metadata: { kind: "leaf" }
2452
+ });
2453
+ return json;
2454
+ }
2455
+ );
2456
+ return {
2457
+ async run(input) {
2458
+ const result = await runPipeline3([vlmExtract], input);
2459
+ return {
2460
+ output: result.output,
2461
+ metrics: result.metrics,
2462
+ artifacts: {
2463
+ vlm_extract: result.artifacts.vlm_extract
2464
+ }
2465
+ };
2466
+ }
2467
+ };
2468
+ }
2469
+
2470
+ // src/index.ts
2471
+ function buildTwoProviderFlow(opts) {
2472
+ const parse3 = parseNode2({ ocr: opts.ocr });
2473
+ const mkPrompt = (ir) => `Extract JSON matching the schema fields: vessel, port, quantity_mt.
2474
+ Document (first page preview):
2475
+ ${ir.pages[0]?.lines.slice(0, 50).map((l) => l.text).join("\n")}`;
2476
+ const extractA = extractNode2({ llm: opts.llmA, schema: simpleSchema, makePrompt: mkPrompt });
2477
+ const extractB = extractNode2({ llm: opts.llmB, schema: simpleSchema, makePrompt: mkPrompt });
2478
+ return {
2479
+ async run(input) {
2480
+ const parsed = await runPipeline4([parse3], input);
2481
+ const ir = parsed.output;
2482
+ const [resA, resB] = await Promise.all([
2483
+ runPipeline4([extractA], ir),
2484
+ runPipeline4([extractB], ir)
2485
+ ]);
2486
+ return {
2487
+ ir,
2488
+ outputA: resA.output,
2489
+ outputB: resB.output,
2490
+ metrics: [...parsed.metrics, ...resA.metrics, ...resB.metrics],
2491
+ artifacts: {
2492
+ parse: parsed.artifacts.parse,
2493
+ extractA: resA.artifacts.extract,
2494
+ extractB: resB.artifacts.extract
2495
+ }
2496
+ };
2497
+ }
2498
+ };
2499
+ }
2500
+ export {
2501
+ FLOW_REGISTRY,
2502
+ FlowSerializationError,
2503
+ bufferToBase64,
2504
+ bufferToDataUri,
2505
+ buildFlowFromConfig,
2506
+ buildMultiProviderFlow,
2507
+ buildTwoProviderFlow,
2508
+ buildVLMDirectFlow,
2509
+ categorize3 as categorize,
2510
+ chunk,
2511
+ clearRegistry,
2512
+ combine,
2513
+ createConditionalCompositeNode,
2514
+ createFlow,
2515
+ createForEachCompositeNode,
2516
+ defineFlowConfig,
2517
+ extract2 as extract,
2518
+ extractNodeMetadata,
2519
+ getFlow,
2520
+ getFlowCount,
2521
+ hasFlow,
2522
+ isFlowReference,
2523
+ listFlows,
2524
+ parse2 as parse,
2525
+ registerFlow,
2526
+ resolveFlowReference,
2527
+ simpleSchema,
2528
+ split3 as split,
2529
+ trigger2 as trigger,
2530
+ unregisterFlow,
2531
+ validateFlow,
2532
+ validateFlowOrThrow
2533
+ };
2534
+ //# sourceMappingURL=index.js.map