@bratsos/workflow-engine 0.1.0 → 0.2.0

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.
Files changed (37) hide show
  1. package/README.md +270 -513
  2. package/dist/chunk-HL3OJG7W.js +1033 -0
  3. package/dist/chunk-HL3OJG7W.js.map +1 -0
  4. package/dist/{chunk-7IITBLFY.js → chunk-NYKMT46J.js} +268 -25
  5. package/dist/chunk-NYKMT46J.js.map +1 -0
  6. package/dist/chunk-SPXBCZLB.js +17 -0
  7. package/dist/chunk-SPXBCZLB.js.map +1 -0
  8. package/dist/{client-5vz5Vv4A.d.ts → client-D4PoxADF.d.ts} +3 -143
  9. package/dist/client.d.ts +3 -2
  10. package/dist/{index-DmR3E8D7.d.ts → index-DAzCfO1R.d.ts} +20 -1
  11. package/dist/index.d.ts +234 -601
  12. package/dist/index.js +46 -2034
  13. package/dist/index.js.map +1 -1
  14. package/dist/{interface-Cv22wvLG.d.ts → interface-MMqhfQQK.d.ts} +69 -2
  15. package/dist/kernel/index.d.ts +26 -0
  16. package/dist/kernel/index.js +3 -0
  17. package/dist/kernel/index.js.map +1 -0
  18. package/dist/kernel/testing/index.d.ts +44 -0
  19. package/dist/kernel/testing/index.js +85 -0
  20. package/dist/kernel/testing/index.js.map +1 -0
  21. package/dist/persistence/index.d.ts +2 -2
  22. package/dist/persistence/index.js +2 -1
  23. package/dist/persistence/prisma/index.d.ts +2 -2
  24. package/dist/persistence/prisma/index.js +2 -1
  25. package/dist/plugins-BCnDUwIc.d.ts +415 -0
  26. package/dist/ports-tU3rzPXJ.d.ts +245 -0
  27. package/dist/stage-BPw7m9Wx.d.ts +144 -0
  28. package/dist/testing/index.d.ts +23 -1
  29. package/dist/testing/index.js +156 -13
  30. package/dist/testing/index.js.map +1 -1
  31. package/package.json +11 -1
  32. package/skills/workflow-engine/SKILL.md +234 -348
  33. package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
  34. package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
  35. package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
  36. package/skills/workflow-engine/references/08-common-patterns.md +118 -431
  37. package/dist/chunk-7IITBLFY.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { createAIHelper } from './chunk-P4KMGCT3.js';
1
+ import { ModelKey } from './chunk-P4KMGCT3.js';
2
2
  export { AVAILABLE_MODELS, AnthropicBatchProvider, DEFAULT_MODEL_KEY, GoogleBatchProvider, ModelKey, ModelStatsTracker, NoInputSchema, OpenAIBatchProvider, calculateCost, createAIHelper, defineAsyncBatchStage, defineStage, getBestProviderForModel, getDefaultModel, getModel, getModelById, getRegisteredModel, listModels, listRegisteredModels, modelSupportsBatch, printAvailableModels, registerModels, requireStageOutput, resolveModelForProvider } from './chunk-P4KMGCT3.js';
3
3
  import './chunk-D7RVRRM2.js';
4
- export { PrismaAICallLogger, PrismaJobQueue, PrismaWorkflowPersistence, createPrismaAICallLogger, createPrismaJobQueue, createPrismaWorkflowPersistence } from './chunk-7IITBLFY.js';
5
- import { createLogger } from './chunk-MUWP5SF2.js';
4
+ export { PrismaAICallLogger, PrismaJobQueue, PrismaWorkflowPersistence, createPrismaAICallLogger, createPrismaJobQueue, createPrismaWorkflowPersistence } from './chunk-NYKMT46J.js';
5
+ import './chunk-MUWP5SF2.js';
6
+ export { StaleVersionError } from './chunk-SPXBCZLB.js';
7
+ export { IdempotencyInProgressError, createKernel, createPluginRunner, definePlugin } from './chunk-HL3OJG7W.js';
6
8
  import { z } from 'zod';
7
- import { EventEmitter } from 'events';
8
- import os from 'os';
9
9
 
10
10
  var Workflow = class {
11
11
  constructor(id, name, description, inputSchema, outputSchema, stages, contextType) {
@@ -350,2038 +350,50 @@ var WorkflowBuilder = class {
350
350
  return this.currentExecutionGroup;
351
351
  }
352
352
  };
353
- z.object({
354
- batchId: z.string(),
355
- statusUrl: z.string().optional(),
356
- apiKey: z.string().optional(),
357
- submittedAt: z.string(),
358
- // ISO date string
359
- pollInterval: z.number(),
360
- // milliseconds
361
- maxWaitTime: z.number(),
362
- // milliseconds
363
- metadata: z.record(z.string(), z.unknown()).optional()
364
- });
365
- function isSuspendedResult(result) {
366
- return "suspended" in result && result.suspended === true;
367
- }
368
- var logger = createLogger("WorkflowEventBus");
369
- var WorkflowEventBus = class _WorkflowEventBus extends EventEmitter {
370
- static instance;
371
- pgNotify = null;
372
- static PG_CHANNEL = "workflow_events";
373
- pgListenerUnsubscribe = null;
374
- // PostgreSQL NOTIFY has an 8000 byte limit for payloads
375
- // We use 7500 to leave room for encoding overhead
376
- static MAX_PAYLOAD_SIZE = 7500;
377
- constructor() {
378
- super();
379
- this.setMaxListeners(1e3);
380
- }
381
- static getInstance() {
382
- if (!_WorkflowEventBus.instance) {
383
- _WorkflowEventBus.instance = new _WorkflowEventBus();
384
- }
385
- return _WorkflowEventBus.instance;
386
- }
387
- /**
388
- * Enable cross-process event publishing via PostgreSQL NOTIFY
389
- *
390
- * Call this during process initialization to enable events to propagate
391
- * across multiple workers and the React Router app.
392
- *
393
- * @param pgNotify - A connected PgNotify instance from @zertai/database
394
- */
395
- async enablePgNotify(pgNotify) {
396
- if (this.pgNotify) {
397
- logger.warn("PgNotify already enabled, skipping");
398
- return;
399
- }
400
- this.pgNotify = pgNotify;
401
- this.pgListenerUnsubscribe = await pgNotify.listen(
402
- _WorkflowEventBus.PG_CHANNEL,
403
- (_channel, payload) => {
404
- try {
405
- const event = JSON.parse(payload);
406
- this.emitLocally(event);
407
- } catch (err) {
408
- logger.error("Failed to parse pg notification:", err);
409
- }
410
- }
411
- );
412
- logger.info("Cross-process events enabled via PostgreSQL NOTIFY");
413
- }
414
- /**
415
- * Disable cross-process events (for cleanup)
416
- */
417
- disablePgNotify() {
418
- if (this.pgListenerUnsubscribe) {
419
- this.pgListenerUnsubscribe();
420
- this.pgListenerUnsubscribe = null;
421
- }
422
- this.pgNotify = null;
423
- }
424
- /**
425
- * Check if cross-process events are enabled
426
- */
427
- isPgNotifyEnabled() {
428
- return this.pgNotify !== null && this.pgNotify.isConnected();
429
- }
430
- /**
431
- * Truncate event payload to fit within PostgreSQL NOTIFY size limits.
432
- * Large data fields (like workflow output) are replaced with a truncation marker.
433
- */
434
- truncatePayloadForNotify(event) {
435
- const serialized = JSON.stringify(event);
436
- if (serialized.length <= _WorkflowEventBus.MAX_PAYLOAD_SIZE) {
437
- return event;
438
- }
439
- const truncatedData = {};
440
- const keysToPreserve = [
441
- "workflowRunId",
442
- "stageId",
443
- "stageName",
444
- "stageNumber",
445
- "error",
446
- "level",
447
- "message",
448
- "duration",
449
- "cost"
450
- ];
451
- for (const key of keysToPreserve) {
452
- if (key in event.data) {
453
- truncatedData[key] = event.data[key];
454
- }
455
- }
456
- truncatedData._truncated = true;
457
- truncatedData._originalSize = serialized.length;
458
- return {
459
- ...event,
460
- data: truncatedData
461
- };
462
- }
463
- /**
464
- * Emit event locally only (used for re-emitting pg notifications)
465
- */
466
- emitLocally(event) {
467
- const eventName = `workflow:${event.workflowRunId}:${event.type}`;
468
- this.emit(eventName, event);
469
- this.emit(`workflow:${event.workflowRunId}:*`, event);
470
- this.emit(event.type, event);
471
- }
472
- /**
473
- * Emit a workflow event with proper namespacing
474
- *
475
- * When PgNotify is enabled, also publishes to PostgreSQL for cross-process
476
- * consumption by other workers and the React Router app.
477
- */
478
- emitWorkflowEvent(workflowRunId, eventType, payload) {
479
- const sseEvent = {
480
- type: eventType,
481
- workflowRunId,
482
- timestamp: /* @__PURE__ */ new Date(),
483
- data: payload
484
- };
485
- this.emitLocally(sseEvent);
486
- if (this.pgNotify) {
487
- const notifyEvent = this.truncatePayloadForNotify(sseEvent);
488
- this.pgNotify.notify(_WorkflowEventBus.PG_CHANNEL, JSON.stringify(notifyEvent)).catch((err) => {
489
- logger.error("Pg notify failed:", err);
490
- });
491
- }
492
- }
493
- /**
494
- * Subscribe to all events for a specific workflow run
495
- */
496
- subscribeToWorkflow(workflowRunId, handler) {
497
- const eventName = `workflow:${workflowRunId}:*`;
498
- this.on(eventName, handler);
499
- return () => {
500
- this.off(eventName, handler);
501
- };
502
- }
503
- /**
504
- * Subscribe to a specific event type globally (across all workflows)
505
- */
506
- subscribeGlobal(eventType, handler) {
507
- this.on(eventType, handler);
508
- return () => {
509
- this.off(eventType, handler);
510
- };
511
- }
512
- /**
513
- * Subscribe to a specific event type for a workflow
514
- */
515
- subscribeToEvent(workflowRunId, eventType, handler) {
516
- const eventName = `workflow:${workflowRunId}:${eventType}`;
517
- this.on(eventName, handler);
518
- return () => {
519
- this.off(eventName, handler);
520
- };
521
- }
522
- };
523
- var workflowEventBus = WorkflowEventBus.getInstance();
524
-
525
- // src/core/executor.ts
526
- var logger2 = createLogger("Executor");
527
- var NoOpAICallLogger = class {
528
- logCall() {
529
- }
530
- async logBatchResults() {
531
- }
532
- async getStats() {
533
- return {
534
- totalCalls: 0,
535
- totalInputTokens: 0,
536
- totalOutputTokens: 0,
537
- totalCost: 0,
538
- perModel: {}
539
- };
540
- }
541
- async isRecorded() {
542
- return false;
543
- }
544
- };
545
- var WorkflowExecutor = class extends EventEmitter {
546
- constructor(workflow, workflowRunId, workflowType, storageProviderOrOptions) {
547
- super();
548
- this.workflow = workflow;
549
- this.workflowRunId = workflowRunId;
550
- this.workflowType = workflowType;
551
- let persistence;
552
- let aiLogger;
553
- if (typeof storageProviderOrOptions === "object" && storageProviderOrOptions !== null) {
554
- persistence = storageProviderOrOptions.persistence;
555
- aiLogger = storageProviderOrOptions.aiLogger;
556
- }
557
- if (!persistence) {
558
- throw new Error(
559
- "WorkflowExecutor requires persistence to be provided via options. Create an instance using PrismaWorkflowPersistence or InMemoryWorkflowPersistence."
560
- );
561
- }
562
- this.persistence = persistence;
563
- this.aiLogger = aiLogger ?? new NoOpAICallLogger();
564
- }
565
- cancelled = false;
566
- persistence;
567
- aiLogger;
568
- /**
569
- * Override emit to also forward events to the global event bus for SSE
570
- */
571
- emit(eventName, ...args) {
572
- const eventType = String(eventName);
573
- if (args[0] && typeof args[0] === "object") {
574
- workflowEventBus.emitWorkflowEvent(
575
- this.workflowRunId,
576
- eventType,
577
- args[0]
578
- );
579
- }
580
- return super.emit(eventName, ...args);
581
- }
582
- /**
583
- * Check if the workflow has been interrupted (cancelled or suspended) externally
584
- * This checks the database status to detect external requests
585
- */
586
- async checkExternalInterruption() {
587
- if (this.cancelled) {
588
- return { type: "cancelled", reason: "Cancelled by local request" };
589
- }
590
- try {
591
- const status = await this.persistence.getRunStatus(this.workflowRunId);
592
- if (status === "CANCELLED") {
593
- this.cancelled = true;
594
- return { type: "cancelled", reason: "Cancelled by external request" };
595
- }
596
- if (status === "SUSPENDED") {
597
- return { type: "suspended", reason: "Suspended by external request" };
598
- }
599
- return null;
600
- } catch (error) {
601
- logger2.error("Error checking interruption status:", error);
602
- return null;
603
- }
604
- }
605
- /**
606
- * Execute the workflow
607
- *
608
- * @param input - Workflow input data
609
- * @param config - Configuration for each stage (keyed by stage ID)
610
- * @param options - Execution options (resume, etc.)
611
- * @returns Final output or 'suspended' if workflow is suspended
612
- */
613
- async execute(input, config, options = {}) {
614
- try {
615
- const configValidation = this.workflow.validateConfig(config);
616
- if (!configValidation.valid) {
617
- const errorMessages = configValidation.errors.map((e) => ` - ${e.stageId}: ${e.error}`).join("\n");
618
- throw new Error(`Workflow config validation failed:
619
- ${errorMessages}`);
620
- }
621
- await this.persistence.updateRun(this.workflowRunId, {
622
- status: "RUNNING",
623
- startedAt: /* @__PURE__ */ new Date()
624
- });
625
- this.emit("workflow:started", {
626
- workflowRunId: this.workflowRunId,
627
- workflowName: this.workflow.name
628
- });
629
- this.log("INFO", `Starting workflow: ${this.workflow.name}`);
630
- let startGroupNumber = 1;
631
- let currentOutput = input;
632
- let workflowContext = {};
633
- if (options.resume) {
634
- const resumeData = await this.loadResumeState();
635
- if (resumeData) {
636
- startGroupNumber = resumeData.lastCompletedGroup + 1;
637
- currentOutput = resumeData.lastOutput;
638
- workflowContext = await this.loadWorkflowContext();
639
- this.log("INFO", `Resuming from execution group ${startGroupNumber}`);
640
- this.log(
641
- "INFO",
642
- `Loaded ${Object.keys(workflowContext).length} previous stage outputs into context`
643
- );
644
- }
645
- }
646
- if (options.fromStage) {
647
- const fromStageData = await this.loadFromStageState(options.fromStage);
648
- startGroupNumber = fromStageData.executionGroup;
649
- currentOutput = fromStageData.input;
650
- workflowContext = fromStageData.workflowContext;
651
- this.log(
652
- "INFO",
653
- `Rerunning from stage "${options.fromStage}" (group ${startGroupNumber})`
654
- );
655
- this.log(
656
- "INFO",
657
- `Loaded ${Object.keys(workflowContext).length} previous stage outputs into context`
658
- );
659
- }
660
- const executionPlan = this.workflow.getExecutionPlan();
661
- for (let groupIdx = 0; groupIdx < executionPlan.length; groupIdx++) {
662
- const group = executionPlan[groupIdx];
663
- const interruption = await this.checkExternalInterruption();
664
- if (interruption) {
665
- if (interruption.type === "cancelled") {
666
- this.log("WARN", "Workflow cancelled by external request");
667
- this.emit("workflow:cancelled", {
668
- workflowRunId: this.workflowRunId,
669
- reason: interruption.reason || "Cancelled by user"
670
- });
671
- await this.persistence.updateRun(this.workflowRunId, {
672
- status: "CANCELLED",
673
- completedAt: /* @__PURE__ */ new Date()
674
- });
675
- return "cancelled";
676
- } else if (interruption.type === "suspended") {
677
- this.log("WARN", "Workflow suspended by external request");
678
- return "suspended";
679
- }
680
- }
681
- const groupNumber = group[0].executionGroup;
682
- if (groupNumber < startGroupNumber) {
683
- this.log("INFO", `Skipping already completed group ${groupNumber}`);
684
- continue;
685
- }
686
- if (group.length === 1) {
687
- const node = group[0];
688
- const result = await this.executeStage(
689
- node,
690
- currentOutput,
691
- config[node.stage.id] || {},
692
- node.executionGroup,
693
- workflowContext
694
- );
695
- if (result === "suspended") {
696
- return "suspended";
697
- }
698
- currentOutput = result.output;
699
- workflowContext[node.stage.id] = result.output;
700
- } else {
701
- const results = await Promise.all(
702
- group.map(
703
- (node) => this.executeStage(
704
- node,
705
- currentOutput,
706
- config[node.stage.id] || {},
707
- node.executionGroup,
708
- workflowContext
709
- )
710
- )
711
- );
712
- const suspendedIdx = results.findIndex((r) => r === "suspended");
713
- if (suspendedIdx !== -1) {
714
- return "suspended";
715
- }
716
- currentOutput = results.reduce(
717
- (acc, result, idx) => {
718
- if (result !== "suspended") {
719
- acc[idx] = result.output;
720
- const stageId = group[idx].stage.id;
721
- workflowContext[stageId] = result.output;
722
- }
723
- return acc;
724
- },
725
- {}
726
- );
727
- }
728
- }
729
- const endTime = /* @__PURE__ */ new Date();
730
- const run = await this.persistence.getRun(this.workflowRunId);
731
- const startTime = run?.startedAt;
732
- const duration = startTime ? endTime.getTime() - startTime.getTime() : void 0;
733
- const aggregatedStats = await this.getAggregatedStats();
734
- await this.persistence.updateRun(this.workflowRunId, {
735
- status: "COMPLETED",
736
- completedAt: endTime,
737
- duration,
738
- output: currentOutput,
739
- totalCost: aggregatedStats.totalCost,
740
- totalTokens: aggregatedStats.totalTokens
741
- });
742
- this.emit("workflow:completed", {
743
- workflowRunId: this.workflowRunId,
744
- output: currentOutput
745
- });
746
- this.log("INFO", `Workflow completed in ${duration}ms`);
747
- return currentOutput;
748
- } catch (error) {
749
- const errorMessage = error instanceof Error ? error.message : String(error);
750
- await this.persistence.updateRun(this.workflowRunId, {
751
- status: "FAILED",
752
- completedAt: /* @__PURE__ */ new Date()
753
- });
754
- this.emit("workflow:failed", {
755
- workflowRunId: this.workflowRunId,
756
- error: errorMessage
757
- });
758
- this.log("ERROR", `Workflow failed: ${errorMessage}`);
759
- throw error;
760
- }
761
- }
762
- /**
763
- * Execute a single stage
764
- */
765
- async executeStage(node, input, config, executionGroup, workflowContext) {
766
- const { stage } = node;
767
- const startTime = Date.now();
768
- try {
769
- const stageRecord = await this.persistence.upsertStage({
770
- workflowRunId: this.workflowRunId,
771
- stageId: stage.id,
772
- create: {
773
- workflowRunId: this.workflowRunId,
774
- stageId: stage.id,
775
- stageName: stage.name,
776
- stageNumber: executionGroup,
777
- executionGroup,
778
- status: "RUNNING",
779
- startedAt: /* @__PURE__ */ new Date(),
780
- config,
781
- inputData: input
782
- },
783
- update: {
784
- status: "RUNNING",
785
- startedAt: /* @__PURE__ */ new Date()
786
- }
787
- });
788
- this.emit("stage:started", {
789
- stageId: stage.id,
790
- stageName: stage.name,
791
- stageNumber: executionGroup
792
- });
793
- const isResuming = stageRecord.suspendedState !== null;
794
- if (isResuming) {
795
- this.log("INFO", `Resuming suspended stage: ${stage.name}`);
796
- } else {
797
- this.log("INFO", `Executing stage: ${stage.name}`);
798
- }
799
- const inputStr = JSON.stringify(input);
800
- logger2.debug(`Stage ${stage.name} input`, {
801
- stageId: stage.id,
802
- executionGroup,
803
- isResuming,
804
- input: inputStr.substring(0, 1e3) + (inputStr.length > 1e3 ? "..." : "")
805
- });
806
- const validatedInput = stage.inputSchema.parse(input);
807
- const logFn = (level, message, meta) => {
808
- this.log(level, message, meta);
809
- this.persistence.createLog({
810
- workflowStageId: stageRecord.id,
811
- workflowRunId: this.workflowRunId,
812
- level,
813
- message,
814
- metadata: meta
815
- }).catch((err) => logger2.error("Failed to persist log:", err));
816
- };
817
- const context = {
818
- workflowRunId: this.workflowRunId,
819
- stageId: stage.id,
820
- stageNumber: executionGroup,
821
- stageName: stage.name,
822
- input: validatedInput,
823
- config,
824
- onProgress: (update) => {
825
- this.emit("stage:progress", update);
826
- },
827
- onLog: logFn,
828
- log: logFn,
829
- storage: this.createStorageShim(),
830
- workflowContext,
831
- // If resuming from suspension, pass the suspended state
832
- resumeState: isResuming ? stageRecord.suspendedState : void 0
833
- };
834
- const result = await stage.execute(context);
835
- if (isSuspendedResult(result)) {
836
- const { state, pollConfig, metrics } = result;
837
- const stateStr = JSON.stringify(state);
838
- logger2.debug(`Stage ${stage.name} suspended`, {
839
- stageId: stage.id,
840
- nextPollAt: pollConfig.nextPollAt.toISOString(),
841
- pollInterval: pollConfig.pollInterval,
842
- maxWaitTime: pollConfig.maxWaitTime,
843
- state: stateStr.substring(0, 500) + (stateStr.length > 500 ? "..." : "")
844
- });
845
- await this.persistence.updateStage(stageRecord.id, {
846
- status: "SUSPENDED",
847
- suspendedState: state,
848
- nextPollAt: pollConfig.nextPollAt,
849
- pollInterval: pollConfig.pollInterval,
850
- maxWaitUntil: new Date(Date.now() + pollConfig.maxWaitTime),
851
- metrics
852
- });
853
- await this.persistence.updateRun(this.workflowRunId, {
854
- status: "SUSPENDED"
855
- });
856
- this.emit("stage:suspended", {
857
- stageId: stage.id,
858
- stageName: stage.name,
859
- resumeAt: pollConfig.nextPollAt
860
- });
861
- this.log(
862
- "INFO",
863
- `Stage suspended: ${stage.name}, next poll at ${pollConfig.nextPollAt.toISOString()}`
864
- );
865
- return "suspended";
866
- }
867
- const validatedOutput = stage.outputSchema.parse(result.output);
868
- const outputKey = await this.persistence.saveStageOutput(
869
- this.workflowRunId,
870
- this.workflowType,
871
- stage.id,
872
- validatedOutput
873
- );
874
- const endTime = Date.now();
875
- const duration = endTime - startTime;
876
- await this.persistence.updateStage(stageRecord.id, {
877
- status: "COMPLETED",
878
- completedAt: /* @__PURE__ */ new Date(),
879
- duration,
880
- outputData: { _artifactKey: outputKey },
881
- metrics: result.metrics,
882
- embeddingInfo: result.embeddings
883
- });
884
- this.emit("stage:completed", {
885
- stageId: stage.id,
886
- stageName: stage.name,
887
- duration
888
- });
889
- const outputStr = JSON.stringify(validatedOutput);
890
- logger2.debug(`Stage ${stage.name} output`, {
891
- stageId: stage.id,
892
- duration,
893
- output: outputStr.substring(0, 1e3) + (outputStr.length > 1e3 ? "..." : ""),
894
- metrics: result.metrics
895
- });
896
- this.log("INFO", `Stage completed: ${stage.name} in ${duration}ms`);
897
- return result;
898
- } catch (error) {
899
- const errorMessage = error instanceof Error ? error.message : String(error);
900
- const errorStack = error instanceof Error ? error.stack : void 0;
901
- logger2.error(`Stage ${stage.name} error`, {
902
- stageId: stage.id,
903
- error: errorMessage,
904
- stack: errorStack,
905
- duration: Date.now() - startTime
906
- });
907
- await this.persistence.updateStageByRunAndStageId(
908
- this.workflowRunId,
909
- stage.id,
910
- {
911
- status: "FAILED",
912
- completedAt: /* @__PURE__ */ new Date(),
913
- errorMessage
914
- }
915
- );
916
- this.emit("stage:failed", {
917
- stageId: stage.id,
918
- stageName: stage.name,
919
- error: errorMessage
920
- });
921
- this.log("ERROR", `Stage failed: ${stage.name} - ${errorMessage}`);
922
- throw error;
923
- }
924
- }
925
- /**
926
- * Load resume state from database
927
- */
928
- async loadResumeState() {
929
- const suspendedStage = await this.persistence.getFirstSuspendedStageReadyToResume(
930
- this.workflowRunId
931
- );
932
- logger2.debug(
933
- `loadResumeState - found suspended stage: ${suspendedStage?.stageId} ${suspendedStage?.status} ${suspendedStage?.nextPollAt} ${suspendedStage?.suspendedState ? "(has suspendedState)" : "(no suspendedState)"}`
934
- );
935
- if (suspendedStage) {
936
- logger2.debug(
937
- `Resuming from suspended stage ${suspendedStage.stageId}, group ${suspendedStage.executionGroup}`
938
- );
939
- let lastOutput = suspendedStage.inputData;
940
- if (!lastOutput) {
941
- if (suspendedStage.executionGroup === 1) {
942
- logger2.warn(
943
- `Suspended stage ${suspendedStage.stageId} has no inputData. Falling back to workflow run input.`
944
- );
945
- const run = await this.persistence.getRun(this.workflowRunId);
946
- lastOutput = run?.input;
947
- } else {
948
- logger2.warn(
949
- `Suspended stage ${suspendedStage.stageId} (group ${suspendedStage.executionGroup}) has no inputData. Loading previous stage output.`
950
- );
951
- const previousCompleted = await this.persistence.getLastCompletedStageBefore(
952
- this.workflowRunId,
953
- suspendedStage.executionGroup
954
- );
955
- if (previousCompleted) {
956
- const outputData2 = previousCompleted.outputData;
957
- if (outputData2?._artifactKey) {
958
- lastOutput = await this.persistence.loadArtifact(
959
- this.workflowRunId,
960
- outputData2._artifactKey
961
- );
962
- logger2.debug(
963
- `Loaded previous stage output from artifact: ${outputData2._artifactKey}`
964
- );
965
- } else if (outputData2) {
966
- lastOutput = outputData2;
967
- logger2.debug(`Using previous stage outputData directly`);
968
- }
969
- }
970
- if (!lastOutput) {
971
- throw new Error(
972
- `Cannot resume suspended stage ${suspendedStage.stageId}: no inputData stored and no previous stage output found`
973
- );
974
- }
975
- }
976
- }
977
- return {
978
- lastCompletedGroup: suspendedStage.executionGroup - 1,
979
- lastOutput
980
- };
981
- }
982
- logger2.debug(`No suspended stage found, looking for last completed stage`);
983
- const failedStage = await this.persistence.getFirstFailedStage(
984
- this.workflowRunId
985
- );
986
- if (failedStage) {
987
- logger2.debug(
988
- `Found failed stage: ${failedStage.stageId} in group ${failedStage.executionGroup}`
989
- );
990
- const lastCompletedBeforeFailed = await this.persistence.getLastCompletedStageBefore(
991
- this.workflowRunId,
992
- failedStage.executionGroup
993
- );
994
- if (lastCompletedBeforeFailed) {
995
- logger2.debug(
996
- `Found last completed stage before failure: ${lastCompletedBeforeFailed.stageId}`
997
- );
998
- const outputData2 = lastCompletedBeforeFailed.outputData;
999
- let output2;
1000
- if (outputData2?._artifactKey) {
1001
- output2 = await this.persistence.loadArtifact(
1002
- this.workflowRunId,
1003
- outputData2._artifactKey
1004
- );
1005
- } else if (outputData2) {
1006
- output2 = outputData2;
1007
- } else {
1008
- throw new Error(
1009
- `No output data found for stage ${lastCompletedBeforeFailed.stageId}`
1010
- );
1011
- }
1012
- await this.persistence.deleteStage(failedStage.id);
1013
- logger2.debug(
1014
- `Deleted failed stage ${failedStage.stageId} for re-execution`
1015
- );
1016
- return {
1017
- lastCompletedGroup: lastCompletedBeforeFailed.executionGroup,
1018
- lastOutput: output2
1019
- };
1020
- }
1021
- }
1022
- const stages = await this.persistence.getStagesByRun(this.workflowRunId, {
1023
- status: "COMPLETED",
1024
- orderBy: "desc"
1025
- });
1026
- if (stages.length === 0) {
1027
- return null;
1028
- }
1029
- const lastStage = stages[0];
1030
- logger2.debug(`Found last completed stage: ${lastStage.stageId}`);
1031
- const outputData = lastStage.outputData;
1032
- let output;
1033
- if (outputData?._artifactKey) {
1034
- output = await this.persistence.loadArtifact(
1035
- this.workflowRunId,
1036
- outputData._artifactKey
1037
- );
1038
- } else if (outputData) {
1039
- output = outputData;
1040
- } else {
1041
- throw new Error(`No output data found for stage ${lastStage.stageId}`);
1042
- }
1043
- return {
1044
- lastCompletedGroup: lastStage.executionGroup,
1045
- lastOutput: output
1046
- };
1047
- }
1048
- /**
1049
- * Load workflow context from all completed stages
1050
- * This rebuilds the workflowContext object so resumed stages can access previous outputs
1051
- */
1052
- async loadWorkflowContext() {
1053
- const completedStages = await this.persistence.getStagesByRun(
1054
- this.workflowRunId,
1055
- {
1056
- status: "COMPLETED",
1057
- orderBy: "asc"
1058
- }
1059
- );
1060
- const workflowContext = {};
1061
- for (const stage of completedStages) {
1062
- const outputData = stage.outputData;
1063
- let output;
1064
- if (outputData?._artifactKey) {
1065
- output = await this.persistence.loadArtifact(
1066
- this.workflowRunId,
1067
- outputData._artifactKey
1068
- );
1069
- } else if (outputData) {
1070
- output = outputData;
1071
- } else {
1072
- logger2.warn(
1073
- `No output data found for completed stage ${stage.stageId}`
1074
- );
1075
- continue;
1076
- }
1077
- workflowContext[stage.stageId] = output;
1078
- logger2.debug(
1079
- `Loaded output for stage ${stage.stageId} into workflowContext`
1080
- );
1081
- }
1082
- logger2.debug(
1083
- `Rebuilt workflowContext with ${Object.keys(workflowContext).length} stage outputs`
1084
- );
1085
- return workflowContext;
1086
- }
1087
- /**
1088
- * Load state for rerunning from a specific stage.
1089
- * Requires that previous stages have already been executed and their outputs persisted.
1090
- *
1091
- * @param stageId - The stage ID to start execution from
1092
- * @returns The execution group, input data, and workflow context
1093
- */
1094
- async loadFromStageState(stageId) {
1095
- const stage = this.workflow.getStage(stageId);
1096
- if (!stage) {
1097
- throw new Error(
1098
- `Stage "${stageId}" not found in workflow "${this.workflow.id}"`
1099
- );
1100
- }
1101
- const executionPlan = this.workflow.getExecutionPlan();
1102
- let executionGroup = -1;
1103
- for (const group of executionPlan) {
1104
- const foundInGroup = group.find((node) => node.stage.id === stageId);
1105
- if (foundInGroup) {
1106
- executionGroup = foundInGroup.executionGroup;
1107
- break;
1108
- }
1109
- }
1110
- if (executionGroup === -1) {
1111
- throw new Error(
1112
- `Stage "${stageId}" not found in execution plan for workflow "${this.workflow.id}"`
1113
- );
1114
- }
1115
- logger2.debug(
1116
- `loadFromStageState: stage "${stageId}" is in execution group ${executionGroup}`
1117
- );
1118
- let input;
1119
- if (executionGroup === 1) {
1120
- const run = await this.persistence.getRun(this.workflowRunId);
1121
- if (!run) {
1122
- throw new Error(`WorkflowRun "${this.workflowRunId}" not found`);
1123
- }
1124
- input = run.input;
1125
- logger2.debug(`Using workflow input for first group stage`);
1126
- } else {
1127
- const previousCompleted = await this.persistence.getLastCompletedStageBefore(
1128
- this.workflowRunId,
1129
- executionGroup
1130
- );
1131
- if (!previousCompleted) {
1132
- throw new Error(
1133
- `Cannot rerun from stage "${stageId}": no completed stages found before execution group ${executionGroup}. You must run the workflow from the beginning first.`
1134
- );
1135
- }
1136
- const outputData = previousCompleted.outputData;
1137
- if (outputData?._artifactKey) {
1138
- input = await this.persistence.loadArtifact(
1139
- this.workflowRunId,
1140
- outputData._artifactKey
1141
- );
1142
- logger2.debug(`Loaded input from artifact: ${outputData._artifactKey}`);
1143
- } else if (outputData) {
1144
- input = outputData;
1145
- logger2.debug(
1146
- `Using outputData directly from stage ${previousCompleted.stageId}`
1147
- );
1148
- } else {
1149
- throw new Error(
1150
- `Cannot rerun from stage "${stageId}": no output data found for previous stage "${previousCompleted.stageId}"`
1151
- );
1152
- }
1153
- }
1154
- const completedStages = await this.persistence.getStagesByRun(
1155
- this.workflowRunId,
1156
- {
1157
- status: "COMPLETED",
1158
- orderBy: "asc"
1159
- }
1160
- );
1161
- const workflowContext = {};
1162
- for (const completedStage of completedStages) {
1163
- if (completedStage.executionGroup >= executionGroup) {
1164
- continue;
1165
- }
1166
- const outputData = completedStage.outputData;
1167
- let output;
1168
- if (outputData?._artifactKey) {
1169
- output = await this.persistence.loadArtifact(
1170
- this.workflowRunId,
1171
- outputData._artifactKey
1172
- );
1173
- } else if (outputData) {
1174
- output = outputData;
1175
- } else {
1176
- logger2.warn(
1177
- `No output data found for completed stage ${completedStage.stageId}`
1178
- );
1179
- continue;
1180
- }
1181
- workflowContext[completedStage.stageId] = output;
1182
- logger2.debug(
1183
- `Loaded output for stage ${completedStage.stageId} into workflowContext`
1184
- );
1185
- }
1186
- const stagesToDelete = await this.persistence.getStagesByRun(
1187
- this.workflowRunId,
1188
- {}
1189
- );
1190
- for (const stageToDelete of stagesToDelete) {
1191
- if (stageToDelete.executionGroup >= executionGroup) {
1192
- await this.persistence.deleteStage(stageToDelete.id);
1193
- logger2.debug(
1194
- `Deleted stage ${stageToDelete.stageId} (group ${stageToDelete.executionGroup}) for re-execution`
1195
- );
1196
- }
1197
- }
1198
- return {
1199
- executionGroup,
1200
- input,
1201
- workflowContext
1202
- };
1203
- }
1204
- /**
1205
- * Create a minimal storage shim for context.storage (for API compatibility).
1206
- * Stage implementations should not rely on this - it may be removed in future.
1207
- */
1208
- createStorageShim() {
1209
- const persistence = this.persistence;
1210
- const workflowRunId = this.workflowRunId;
1211
- const workflowType = this.workflowType;
1212
- return {
1213
- async save(key, data) {
1214
- await persistence.saveArtifact({
1215
- workflowRunId,
1216
- key,
1217
- type: "ARTIFACT",
1218
- data,
1219
- size: Buffer.byteLength(JSON.stringify(data), "utf8")
1220
- });
1221
- },
1222
- async load(key) {
1223
- return persistence.loadArtifact(workflowRunId, key);
1224
- },
1225
- async exists(key) {
1226
- return persistence.hasArtifact(workflowRunId, key);
1227
- },
1228
- async delete(key) {
1229
- return persistence.deleteArtifact(workflowRunId, key);
1230
- },
1231
- getStageKey(stageId, suffix) {
1232
- const base = `workflow-v2/${workflowType}/${workflowRunId}/${stageId}`;
1233
- return suffix ? `${base}/${suffix}` : `${base}/output.json`;
1234
- }
1235
- };
1236
- }
1237
- /**
1238
- * Get aggregated statistics for the workflow run
1239
- */
1240
- async getAggregatedStats() {
1241
- const stats = await this.aiLogger.getStats(
1242
- `workflow.${this.workflowRunId}`
1243
- );
1244
- return {
1245
- totalCost: stats.totalCost,
1246
- totalTokens: stats.totalInputTokens + stats.totalOutputTokens
1247
- };
1248
- }
1249
- /**
1250
- * Log a message with automatic database persistence
1251
- */
1252
- log(level, message, meta) {
1253
- this.emit("log", { level, message, meta });
1254
- this.persistence.createLog({
1255
- workflowRunId: this.workflowRunId,
1256
- level,
1257
- message,
1258
- metadata: meta
1259
- }).catch((err) => logger2.error("Failed to persist log:", err));
1260
- }
1261
- };
1262
-
1263
- // src/core/stage-executor.ts
1264
- var logger3 = createLogger("StageExecutor");
1265
- var StageExecutor = class {
1266
- constructor(registry, persistence, workerId) {
1267
- this.registry = registry;
1268
- this.persistence = persistence;
1269
- this.workerId = workerId || `stage-executor-${process.pid}`;
1270
- }
1271
- workerId;
1272
- /**
1273
- * Execute a single stage
1274
- */
1275
- async execute(request) {
1276
- const { workflowRunId, workflowId, stageId, config } = request;
1277
- const startTime = Date.now();
1278
- logger3.debug(`Executing stage ${stageId} for workflow ${workflowRunId}`);
1279
- const workflow = this.registry.getWorkflow(workflowId);
1280
- if (!workflow) {
1281
- throw new Error(`Workflow ${workflowId} not found in registry`);
1282
- }
1283
- const stageDef = workflow.getStage(stageId);
1284
- if (!stageDef) {
1285
- throw new Error(`Stage ${stageId} not found in workflow ${workflowId}`);
1286
- }
1287
- const workflowRun = await this.persistence.getRun(workflowRunId);
1288
- if (!workflowRun) {
1289
- throw new Error(`WorkflowRun ${workflowRunId} not found`);
1290
- }
1291
- const workflowContext = await this.loadWorkflowContext(
1292
- workflowRunId,
1293
- workflowRun.workflowType
1294
- );
1295
- const stageRecord = await this.persistence.upsertStage({
1296
- workflowRunId,
1297
- stageId,
1298
- create: {
1299
- workflowRunId,
1300
- stageId,
1301
- stageName: stageDef.name,
1302
- stageNumber: this.getStageNumber(workflow, stageId),
1303
- executionGroup: this.getExecutionGroup(workflow, stageId),
1304
- status: "RUNNING",
1305
- startedAt: /* @__PURE__ */ new Date(),
1306
- config,
1307
- inputData: void 0
1308
- // Will be set later? Or needs to be passed?
1309
- // Note: original local code didn't set inputData in create? Wait, checking local code...
1310
- },
1311
- update: {
1312
- status: "RUNNING",
1313
- startedAt: /* @__PURE__ */ new Date()
1314
- // errorMessage: null, // Persistence interface might not support partial update of this field easily if not explicit?
1315
- // But upsertStage uses UpdateStageInput.
1316
- }
1317
- });
1318
- const input = await this.resolveStageInput(
1319
- workflow,
1320
- stageId,
1321
- workflowRun,
1322
- workflowContext
1323
- );
1324
- workflowEventBus.emitWorkflowEvent(workflowRunId, "stage:started", {
1325
- stageId,
1326
- stageName: stageDef.name,
1327
- stageNumber: stageRecord.stageNumber
1328
- });
1329
- try {
1330
- const validatedInput = stageDef.inputSchema.parse(input);
1331
- let stageConfig = config[stageId] || {};
1332
- try {
1333
- if (stageDef.configSchema) {
1334
- stageConfig = stageDef.configSchema.parse(stageConfig);
1335
- }
1336
- } catch (err) {
1337
- logger3.warn(
1338
- `Config parsing failed for ${stageId}, falling back to raw config`
1339
- );
1340
- }
1341
- const logFn = (level, message, meta) => this.log(workflowRunId, stageRecord.id, level, message, meta);
1342
- const context = {
1343
- workflowRunId,
1344
- stageId,
1345
- stageNumber: stageRecord.stageNumber,
1346
- stageName: stageDef.name,
1347
- stageRecordId: stageRecord.id,
1348
- input: validatedInput,
1349
- config: stageConfig,
1350
- resumeState: stageRecord.suspendedState,
1351
- onProgress: (update) => {
1352
- workflowEventBus.emitWorkflowEvent(
1353
- workflowRunId,
1354
- "stage:progress",
1355
- update
1356
- );
1357
- },
1358
- onLog: logFn,
1359
- log: logFn,
1360
- storage: this.createStorageShim(
1361
- workflowRunId,
1362
- workflowRun.workflowType
1363
- ),
1364
- workflowContext
1365
- };
1366
- const result = await stageDef.execute(context);
1367
- if (isSuspendedResult(result)) {
1368
- return await this.handleSuspended(stageRecord.id, result, startTime);
1369
- } else {
1370
- return await this.handleCompleted(
1371
- workflowRunId,
1372
- workflowRun.workflowType,
1373
- stageRecord.id,
1374
- stageId,
1375
- result,
1376
- startTime
1377
- );
1378
- }
1379
- } catch (error) {
1380
- return await this.handleFailed(stageRecord.id, stageId, error, startTime);
1381
- }
1382
- }
1383
- // -- Handlers --
1384
- async handleCompleted(workflowRunId, workflowType, stageRecordId, stageId, result, startTime) {
1385
- const duration = Date.now() - startTime;
1386
- const outputKey = await this.persistence.saveStageOutput(
1387
- workflowRunId,
1388
- workflowType,
1389
- stageId,
1390
- result.output
1391
- );
1392
- await this.persistence.updateStage(stageRecordId, {
1393
- status: "COMPLETED",
1394
- completedAt: /* @__PURE__ */ new Date(),
1395
- duration,
1396
- outputData: { _artifactKey: outputKey },
1397
- metrics: result.metrics,
1398
- embeddingInfo: result.embeddings
1399
- });
1400
- return {
1401
- type: "completed",
1402
- output: result.output,
1403
- metrics: result.metrics
1404
- };
1405
- }
1406
- async handleSuspended(stageRecordId, result, startTime) {
1407
- const { state, pollConfig, metrics } = result;
1408
- const nextPollAt = new Date(
1409
- pollConfig.nextPollAt || Date.now() + (pollConfig.pollInterval || 6e4)
1410
- );
1411
- await this.persistence.updateStage(stageRecordId, {
1412
- status: "SUSPENDED",
1413
- suspendedState: state,
1414
- nextPollAt,
1415
- pollInterval: pollConfig.pollInterval,
1416
- maxWaitUntil: pollConfig.maxWaitTime ? new Date(Date.now() + pollConfig.maxWaitTime) : void 0,
1417
- metrics
1418
- });
1419
- return {
1420
- type: "suspended",
1421
- suspendedState: state,
1422
- nextPollAt,
1423
- metrics
1424
- };
1425
- }
1426
- async handleFailed(stageRecordId, stageId, error, startTime) {
1427
- const errorMessage = error instanceof Error ? error.message : String(error);
1428
- const duration = Date.now() - startTime;
1429
- logger3.error(`Stage ${stageId} failed:`, error);
1430
- await this.persistence.updateStage(stageRecordId, {
1431
- status: "FAILED",
1432
- completedAt: /* @__PURE__ */ new Date(),
1433
- duration
1434
- // Note: updateStage input might not support duration? Check persistence interface. Assuming it does as StageRecord has it.
1435
- // errorMessage: errorMessage // Check if UpdateStageInput supports errorMessage.
1436
- // If not, use updateStageByRunAndStageId pattern or check interface.
1437
- });
1438
- await this.log(
1439
- "",
1440
- stageRecordId,
1441
- "ERROR",
1442
- errorMessage
1443
- );
1444
- return {
1445
- type: "failed",
1446
- error: errorMessage
1447
- };
1448
- }
1449
- // -- Helpers --
1450
- async loadWorkflowContext(workflowRunId, workflowType) {
1451
- const completedStages = await this.persistence.getStagesByRun(
1452
- workflowRunId,
1453
- {
1454
- status: "COMPLETED",
1455
- orderBy: "asc"
1456
- }
1457
- );
1458
- const context = {};
1459
- for (const stage of completedStages) {
1460
- const outputData = stage.outputData;
1461
- if (outputData?._artifactKey) {
1462
- context[stage.stageId] = await this.persistence.loadArtifact(
1463
- workflowRunId,
1464
- outputData._artifactKey
1465
- );
1466
- } else if (outputData && typeof outputData === "object") {
1467
- context[stage.stageId] = outputData;
1468
- }
1469
- }
1470
- return context;
1471
- }
1472
- /**
1473
- * Create a minimal storage shim for context.storage (for API compatibility).
1474
- * Stage implementations should not rely on this - it may be removed in future.
1475
- */
1476
- createStorageShim(workflowRunId, workflowType) {
1477
- const persistence = this.persistence;
1478
- return {
1479
- async save(key, data) {
1480
- await persistence.saveArtifact({
1481
- workflowRunId,
1482
- key,
1483
- type: "ARTIFACT",
1484
- data,
1485
- size: Buffer.byteLength(JSON.stringify(data), "utf8")
1486
- });
1487
- },
1488
- async load(key) {
1489
- return persistence.loadArtifact(workflowRunId, key);
1490
- },
1491
- async exists(key) {
1492
- return persistence.hasArtifact(workflowRunId, key);
1493
- },
1494
- async delete(key) {
1495
- return persistence.deleteArtifact(workflowRunId, key);
1496
- },
1497
- getStageKey(stageId, suffix) {
1498
- const base = `workflow-v2/${workflowType}/${workflowRunId}/${stageId}`;
1499
- return suffix ? `${base}/${suffix}` : `${base}/output.json`;
1500
- }
1501
- };
1502
- }
1503
- async resolveStageInput(workflow, stageId, workflowRun, workflowContext) {
1504
- const stageDef = workflow.getStage(stageId);
1505
- if (!stageDef) return {};
1506
- const groupIndex = workflow.getExecutionGroupIndex(stageId);
1507
- if (groupIndex === 0) {
1508
- return workflowRun.input;
1509
- }
1510
- const prevStageId = workflow.getPreviousStageId(stageId);
1511
- if (prevStageId) {
1512
- return workflowContext[prevStageId];
1513
- }
1514
- return workflowRun.input;
1515
- }
1516
- getStageNumber(workflow, stageId) {
1517
- return workflow.getStageIndex(stageId) + 1;
1518
- }
1519
- getExecutionGroup(workflow, stageId) {
1520
- return workflow.getExecutionGroupIndex(stageId);
1521
- }
1522
- async log(workflowRunId, stageRecordId, level, message, meta) {
1523
- logger3.debug(`[${level}] ${message}`);
1524
- await this.persistence.createLog({
1525
- workflowRunId,
1526
- workflowStageId: stageRecordId,
1527
- level,
1528
- message,
1529
- metadata: meta
1530
- }).catch((err) => logger3.error("Failed to write log:", err));
1531
- }
1532
- };
1533
-
1534
- // src/core/storage-providers/memory-storage.ts
1535
- var InMemoryStageStorage = class _InMemoryStageStorage {
1536
- constructor(workflowRunId, workflowType) {
1537
- this.workflowRunId = workflowRunId;
1538
- this.workflowType = workflowType;
1539
- if (!_InMemoryStageStorage.globalStorage.has(workflowRunId)) {
1540
- _InMemoryStageStorage.globalStorage.set(workflowRunId, /* @__PURE__ */ new Map());
1541
- }
1542
- const storage = _InMemoryStageStorage.globalStorage.get(workflowRunId);
1543
- if (!storage) {
1544
- throw new Error(
1545
- `Failed to initialize storage for workflow run ${workflowRunId}`
1546
- );
1547
- }
1548
- this.storage = storage;
1549
- }
1550
- providerType = "memory";
1551
- // Static storage for all workflow runs (for testing across instances)
1552
- static globalStorage = /* @__PURE__ */ new Map();
1553
- storage;
1554
- /**
1555
- * Generate storage key with consistent pattern:
1556
- * workflow-v2/{type}/{runId}/{stageId}/{suffix|output.json}
1557
- */
1558
- getStageKey(stageId, suffix) {
1559
- const base = `workflow-v2/${this.workflowType}/${this.workflowRunId}/${stageId}`;
1560
- return suffix ? `${base}/${suffix}` : `${base}/output.json`;
1561
- }
1562
- /**
1563
- * Save data to memory (deep clone to prevent mutations)
1564
- */
1565
- async save(key, data) {
1566
- this.storage.set(key, JSON.parse(JSON.stringify(data)));
1567
- }
1568
- /**
1569
- * Load data from memory (deep clone to prevent mutations)
1570
- */
1571
- async load(key) {
1572
- if (!this.storage.has(key)) {
1573
- throw new Error(`Artifact not found: ${key}`);
1574
- }
1575
- return JSON.parse(JSON.stringify(this.storage.get(key)));
1576
- }
1577
- /**
1578
- * Check if key exists in memory
1579
- */
1580
- async exists(key) {
1581
- return this.storage.has(key);
1582
- }
1583
- /**
1584
- * Delete data from memory
1585
- */
1586
- async delete(key) {
1587
- this.storage.delete(key);
1588
- }
1589
- /**
1590
- * Save stage output with standard key
1591
- */
1592
- async saveStageOutput(stageId, output) {
1593
- const key = this.getStageKey(stageId);
1594
- await this.save(key, output);
1595
- return key;
1596
- }
1597
- /**
1598
- * Load stage output with standard key
1599
- */
1600
- async loadStageOutput(stageId) {
1601
- const key = this.getStageKey(stageId);
1602
- return await this.load(key);
1603
- }
1604
- /**
1605
- * Save arbitrary artifact for a stage
1606
- */
1607
- async saveArtifact(stageId, artifactName, data) {
1608
- const key = this.getStageKey(stageId, `artifacts/${artifactName}.json`);
1609
- await this.save(key, data);
1610
- return key;
1611
- }
1612
- /**
1613
- * Load arbitrary artifact for a stage
1614
- */
1615
- async loadArtifact(stageId, artifactName) {
1616
- const key = this.getStageKey(stageId, `artifacts/${artifactName}.json`);
1617
- return await this.load(key);
1618
- }
1619
- /**
1620
- * List all artifacts for a workflow run
1621
- */
1622
- async listAllArtifacts() {
1623
- const artifacts = [];
1624
- for (const key of this.storage.keys()) {
1625
- const keyParts = key.split("/");
1626
- const stageId = keyParts.length >= 4 ? keyParts[3] : "unknown";
1627
- const name = keyParts[keyParts.length - 1] || "unknown";
1628
- artifacts.push({
1629
- key,
1630
- stageId,
1631
- name
1632
- });
1633
- }
1634
- return artifacts;
1635
- }
1636
- /**
1637
- * Testing helper: Clear all storage or specific workflow run
1638
- */
1639
- static clear(workflowRunId) {
1640
- if (workflowRunId) {
1641
- _InMemoryStageStorage.globalStorage.delete(workflowRunId);
1642
- } else {
1643
- _InMemoryStageStorage.globalStorage.clear();
1644
- }
1645
- }
1646
- /**
1647
- * Testing helper: Get all data for a workflow run
1648
- */
1649
- static getAll(workflowRunId) {
1650
- return _InMemoryStageStorage.globalStorage.get(workflowRunId) || /* @__PURE__ */ new Map();
1651
- }
1652
- };
1653
-
1654
- // src/core/storage-providers/prisma-storage.ts
1655
- var PrismaStageStorage = class {
1656
- constructor(prisma, workflowRunId, workflowType) {
1657
- this.prisma = prisma;
1658
- this.workflowRunId = workflowRunId;
1659
- this.workflowType = workflowType;
1660
- }
1661
- providerType = "prisma";
1662
- /**
1663
- * Generate storage key with consistent pattern:
1664
- * workflow-v2/{type}/{runId}/{stageId}/{suffix|output.json}
1665
- */
1666
- getStageKey(stageId, suffix) {
1667
- const base = `workflow-v2/${this.workflowType}/${this.workflowRunId}/${stageId}`;
1668
- return suffix ? `${base}/${suffix}` : `${base}/output.json`;
1669
- }
1670
- /**
1671
- * Save data as JSON to database
1672
- */
1673
- async save(key, data) {
1674
- const json = JSON.stringify(data);
1675
- const size = Buffer.byteLength(json, "utf8");
1676
- const type = key.includes("/artifacts/") ? "ARTIFACT" : "STAGE_OUTPUT";
1677
- const keyParts = key.split("/");
1678
- const stageId = keyParts.length >= 4 ? keyParts[3] : void 0;
1679
- let workflowStageId = null;
1680
- if (stageId) {
1681
- const stage = await this.prisma.workflowStage.findUnique({
1682
- where: {
1683
- workflowRunId_stageId: {
1684
- workflowRunId: this.workflowRunId,
1685
- stageId
1686
- }
1687
- },
1688
- select: { id: true }
1689
- });
1690
- workflowStageId = stage?.id ?? null;
1691
- }
1692
- await this.prisma.workflowArtifact.upsert({
1693
- where: {
1694
- workflowRunId_key: {
1695
- workflowRunId: this.workflowRunId,
1696
- key
1697
- }
1698
- },
1699
- update: {
1700
- data,
1701
- size,
1702
- type,
1703
- workflowStageId
1704
- },
1705
- create: {
1706
- workflowRunId: this.workflowRunId,
1707
- workflowStageId,
1708
- key,
1709
- type,
1710
- data,
1711
- size
1712
- }
1713
- });
1714
- }
1715
- /**
1716
- * Load and parse JSON from database
1717
- */
1718
- async load(key) {
1719
- const artifact = await this.prisma.workflowArtifact.findUnique({
1720
- where: {
1721
- workflowRunId_key: {
1722
- workflowRunId: this.workflowRunId,
1723
- key
1724
- }
1725
- }
1726
- });
1727
- if (!artifact) {
1728
- throw new Error(`Artifact not found: ${key}`);
1729
- }
1730
- return artifact.data;
1731
- }
1732
- /**
1733
- * Check if artifact exists in database
1734
- */
1735
- async exists(key) {
1736
- const artifact = await this.prisma.workflowArtifact.findUnique({
1737
- where: {
1738
- workflowRunId_key: {
1739
- workflowRunId: this.workflowRunId,
1740
- key
1741
- }
1742
- },
1743
- select: { id: true }
1744
- });
1745
- return artifact !== null;
1746
- }
1747
- /**
1748
- * Delete artifact from database
1749
- */
1750
- async delete(key) {
1751
- await this.prisma.workflowArtifact.delete({
1752
- where: {
1753
- workflowRunId_key: {
1754
- workflowRunId: this.workflowRunId,
1755
- key
1756
- }
1757
- }
1758
- });
1759
- }
1760
- /**
1761
- * Save stage output with standard key
1762
- */
1763
- async saveStageOutput(stageId, output) {
1764
- const key = this.getStageKey(stageId);
1765
- await this.save(key, output);
1766
- return key;
1767
- }
1768
- /**
1769
- * Load stage output with standard key
1770
- */
1771
- async loadStageOutput(stageId) {
1772
- const key = this.getStageKey(stageId);
1773
- return await this.load(key);
1774
- }
1775
- /**
1776
- * Save arbitrary artifact for a stage
1777
- */
1778
- async saveArtifact(stageId, artifactName, data) {
1779
- const key = this.getStageKey(stageId, `artifacts/${artifactName}.json`);
1780
- await this.save(key, data);
1781
- return key;
1782
- }
1783
- /**
1784
- * Load arbitrary artifact for a stage
1785
- */
1786
- async loadArtifact(stageId, artifactName) {
1787
- const key = this.getStageKey(stageId, `artifacts/${artifactName}.json`);
1788
- return await this.load(key);
1789
- }
1790
- /**
1791
- * List all artifacts for a workflow run (for export)
1792
- */
1793
- async listAllArtifacts() {
1794
- const artifacts = await this.prisma.workflowArtifact.findMany({
1795
- where: {
1796
- workflowRunId: this.workflowRunId
1797
- },
1798
- select: {
1799
- key: true,
1800
- workflowStageId: true
1801
- }
1802
- });
1803
- return artifacts.map(
1804
- (artifact) => {
1805
- const keyParts = artifact.key.split("/");
1806
- const stageId = keyParts.length >= 4 ? keyParts[3] : "unknown";
1807
- const name = keyParts[keyParts.length - 1] || "unknown";
1808
- return {
1809
- key: artifact.key,
1810
- stageId,
1811
- name
1812
- };
1813
- }
1814
- );
1815
- }
1816
- };
1817
-
1818
- // src/core/storage-factory.ts
1819
- function createStorage(options) {
1820
- const { provider, workflowRunId, workflowType, prisma } = options;
1821
- switch (provider) {
1822
- case "prisma":
1823
- if (!prisma) {
1824
- throw new Error(
1825
- 'Prisma storage requires a prisma client. Pass it via options.prisma or use provider: "memory" for testing.'
1826
- );
1827
- }
1828
- return new PrismaStageStorage(prisma, workflowRunId, workflowType);
1829
- case "memory":
1830
- return new InMemoryStageStorage(workflowRunId, workflowType);
1831
- default:
1832
- throw new Error(`Unknown storage provider: ${provider}`);
1833
- }
353
+ var AIConfigSchema = z.object({
354
+ /** The model to use for AI operations */
355
+ model: ModelKey.default("gemini-2.5-flash"),
356
+ /** Temperature for AI generations (0-2) */
357
+ temperature: z.number().min(0).max(2).default(0.7),
358
+ /** Maximum tokens to generate (undefined = model default) */
359
+ maxTokens: z.number().positive().optional()
360
+ });
361
+ var ConcurrencyConfigSchema = z.object({
362
+ /** Maximum concurrent operations (for parallel processing) */
363
+ concurrency: z.number().positive().default(5),
364
+ /** Delay between operations in milliseconds (rate limiting) */
365
+ delayMs: z.number().nonnegative().default(0),
366
+ /** Maximum retries on failure */
367
+ maxRetries: z.number().nonnegative().default(3)
368
+ });
369
+ var FeatureFlagsConfigSchema = z.object({
370
+ /** Feature flags for conditional stage behavior */
371
+ featureFlags: z.record(z.string(), z.boolean()).default({})
372
+ });
373
+ var DebugConfigSchema = z.object({
374
+ /** Enable verbose logging */
375
+ verbose: z.boolean().default(false),
376
+ /** Dry run mode (no side effects) */
377
+ dryRun: z.boolean().default(false)
378
+ });
379
+ function withAIConfig(schema) {
380
+ return schema.merge(AIConfigSchema);
1834
381
  }
1835
- function getDefaultStorageProvider() {
1836
- const provider = process.env.WORKFLOW_STORAGE_PROVIDER;
1837
- if (provider && ["prisma", "memory"].includes(provider)) {
1838
- return provider;
1839
- }
1840
- return "prisma";
382
+ function withConcurrency(schema) {
383
+ return schema.merge(ConcurrencyConfigSchema);
1841
384
  }
1842
- var logger4 = createLogger("Runtime");
1843
- var WorkflowRuntime = class {
1844
- isPolling = false;
1845
- isProcessingJobs = false;
1846
- isRunning = false;
1847
- pollIntervalMs;
1848
- jobPollIntervalMs;
1849
- staleJobThresholdMs;
1850
- workerId;
1851
- persistence;
1852
- jobQueue;
1853
- registry;
1854
- aiCallLogger;
1855
- getWorkflowPriority;
1856
- pollTimer = null;
1857
- stageExecutor;
1858
- jobsProcessed = 0;
1859
- constructor(config) {
1860
- this.pollIntervalMs = config.pollIntervalMs ?? 1e4;
1861
- this.jobPollIntervalMs = config.jobPollIntervalMs ?? 1e3;
1862
- this.staleJobThresholdMs = config.staleJobThresholdMs ?? 6e4;
1863
- this.workerId = config.workerId ?? `worker-${process.pid}-${os.hostname()}`;
1864
- this.persistence = config.persistence;
1865
- this.jobQueue = config.jobQueue;
1866
- this.registry = config.registry;
1867
- this.aiCallLogger = config.aiCallLogger;
1868
- this.getWorkflowPriority = config.getWorkflowPriority;
1869
- this.stageExecutor = new StageExecutor(
1870
- this.registry,
1871
- this.persistence,
1872
- this.workerId
1873
- );
1874
- }
1875
- // ==========================================================================
1876
- // AI Helper Factory
1877
- // ==========================================================================
1878
- /**
1879
- * Create an AI helper bound to this runtime's logger
1880
- * @param topic - Topic for logging (e.g., "workflow.abc123.stage.extraction")
1881
- * @param logContext - Optional log context for persistence logging in batch operations
1882
- */
1883
- createAIHelper(topic, logContext) {
1884
- if (!this.aiCallLogger) {
1885
- throw new Error(
1886
- "[Runtime] AICallLogger not configured. Pass aiCallLogger in config."
1887
- );
1888
- }
1889
- return createAIHelper(topic, this.aiCallLogger, logContext);
1890
- }
1891
- /**
1892
- * Create a LogContext for a workflow stage (for use with createAIHelper)
1893
- * This enables batch operations to log to the workflow persistence.
1894
- */
1895
- createLogContext(workflowRunId, stageRecordId) {
1896
- const persistence = this.persistence;
1897
- return {
1898
- workflowRunId,
1899
- stageRecordId,
1900
- createLog: (data) => persistence.createLog(data)
1901
- };
1902
- }
1903
- // ==========================================================================
1904
- // Lifecycle
1905
- // ==========================================================================
1906
- /**
1907
- * Start the runtime as a full worker (processes jobs + polls)
1908
- */
1909
- async start() {
1910
- if (this.isRunning) {
1911
- logger4.debug("Already running");
1912
- return;
1913
- }
1914
- logger4.info(`Starting worker ${this.workerId}`);
1915
- logger4.debug(
1916
- `Poll interval: ${this.pollIntervalMs}ms, Job poll: ${this.jobPollIntervalMs}ms`
1917
- );
1918
- this.isRunning = true;
1919
- this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
1920
- this.poll();
1921
- this.processJobs();
1922
- process.on("SIGTERM", () => this.stop());
1923
- process.on("SIGINT", () => this.stop());
1924
- }
1925
- /**
1926
- * Stop the runtime
1927
- */
1928
- stop() {
1929
- logger4.info(`Stopping worker ${this.workerId}...`);
1930
- this.isRunning = false;
1931
- if (this.pollTimer) {
1932
- clearInterval(this.pollTimer);
1933
- this.pollTimer = null;
1934
- }
1935
- logger4.info(`Stopped. Processed ${this.jobsProcessed} jobs.`);
1936
- }
1937
- // ==========================================================================
1938
- // Create Run - The main API for starting workflows
1939
- // ==========================================================================
1940
- /**
1941
- * Create a new workflow run with validation.
1942
- * The runtime will pick it up on the next poll cycle and start execution.
1943
- */
1944
- async createRun(options) {
1945
- const { workflowId, input, config = {}, priority, metadata } = options;
1946
- const workflow = this.registry.getWorkflow(workflowId);
1947
- if (!workflow) {
1948
- throw new Error(`Workflow ${workflowId} not found in registry`);
1949
- }
1950
- try {
1951
- workflow.inputSchema.parse(input);
1952
- } catch (error) {
1953
- throw new Error(`Invalid workflow input: ${error}`);
1954
- }
1955
- const defaultConfig = workflow.getDefaultConfig?.() ?? {};
1956
- const mergedConfig = { ...defaultConfig, ...config };
1957
- const configValidation = workflow.validateConfig(mergedConfig);
1958
- if (!configValidation.valid) {
1959
- const errors = configValidation.errors.map((e) => `${e.stageId}: ${e.error}`).join(", ");
1960
- throw new Error(`Invalid workflow config: ${errors}`);
1961
- }
1962
- const effectivePriority = priority ?? this.getWorkflowPriority?.(workflowId) ?? 5;
1963
- const workflowRun = await this.persistence.createRun({
1964
- workflowId,
1965
- workflowName: workflow.name,
1966
- workflowType: workflowId,
1967
- input,
1968
- config: mergedConfig,
1969
- priority: effectivePriority,
1970
- metadata
1971
- });
1972
- logger4.debug(`Created WorkflowRun ${workflowRun.id} for ${workflowId}`);
1973
- return {
1974
- workflowRunId: workflowRun.id
1975
- };
1976
- }
1977
- // ==========================================================================
1978
- // Job Processing Loop
1979
- // ==========================================================================
1980
- /**
1981
- * Process jobs from the queue
1982
- */
1983
- async processJobs() {
1984
- await this.jobQueue.releaseStaleJobs(this.staleJobThresholdMs);
1985
- let lastStaleCheck = Date.now();
1986
- let lastLogTime = Date.now();
1987
- while (this.isRunning) {
1988
- try {
1989
- const now = Date.now();
1990
- if (now - lastStaleCheck > this.staleJobThresholdMs) {
1991
- await this.jobQueue.releaseStaleJobs(this.staleJobThresholdMs);
1992
- lastStaleCheck = now;
1993
- }
1994
- if (now - lastLogTime > 1e4) {
1995
- logger4.debug(
1996
- `Worker ${this.workerId}: processed ${this.jobsProcessed} jobs`
1997
- );
1998
- lastLogTime = now;
1999
- }
2000
- const job = await this.jobQueue.dequeue();
2001
- if (!job) {
2002
- await new Promise((r) => setTimeout(r, this.jobPollIntervalMs));
2003
- continue;
2004
- }
2005
- const { jobId, workflowRunId, stageId, payload } = job;
2006
- const config = payload.config || {};
2007
- logger4.debug(
2008
- `Processing stage ${stageId} for workflow ${workflowRunId}`
2009
- );
2010
- const workflowId = await this.getWorkflowId(workflowRunId);
2011
- if (!workflowId) {
2012
- await this.jobQueue.fail(jobId, "WorkflowRun not found", false);
2013
- continue;
2014
- }
2015
- const result = await this.stageExecutor.execute({
2016
- workflowRunId,
2017
- stageId,
2018
- workflowId,
2019
- config
2020
- });
2021
- this.jobsProcessed++;
2022
- if (result.type === "completed") {
2023
- logger4.debug(`Job completed`, {
2024
- jobId,
2025
- workflowRunId,
2026
- stageId
2027
- });
2028
- await this.jobQueue.complete(jobId);
2029
- await this.transitionWorkflow(workflowRunId);
2030
- } else if (result.type === "suspended") {
2031
- const nextPollAt = result.nextPollAt || new Date(Date.now() + 6e4);
2032
- logger4.debug(`Job suspended`, {
2033
- jobId,
2034
- workflowRunId,
2035
- stageId,
2036
- nextPollAt: nextPollAt.toISOString()
2037
- });
2038
- await this.jobQueue.suspend(jobId, nextPollAt);
2039
- } else if (result.type === "failed") {
2040
- const canRetry = job.attempt < 3;
2041
- logger4.debug(`Job failed`, {
2042
- jobId,
2043
- workflowRunId,
2044
- stageId,
2045
- error: result.error,
2046
- attempt: job.attempt,
2047
- canRetry
2048
- });
2049
- await this.jobQueue.fail(
2050
- jobId,
2051
- result.error || "Unknown error",
2052
- canRetry
2053
- );
2054
- if (!canRetry) {
2055
- await this.persistence.updateRun(workflowRunId, {
2056
- status: "FAILED"
2057
- });
2058
- }
2059
- }
2060
- } catch (error) {
2061
- logger4.error("Error in job loop:", error);
2062
- await new Promise((r) => setTimeout(r, 5e3));
2063
- }
2064
- }
2065
- }
2066
- async getWorkflowId(runId) {
2067
- const run = await this.persistence.getRun(runId);
2068
- return run?.workflowId ?? null;
2069
- }
2070
- // ==========================================================================
2071
- // Polling - Orchestration
2072
- // ==========================================================================
2073
- /**
2074
- * Poll for pending workflows and suspended stages
2075
- */
2076
- async poll() {
2077
- if (this.isPolling) return;
2078
- this.isPolling = true;
2079
- try {
2080
- await this.pollPendingWorkflows();
2081
- await this.pollSuspendedStages();
2082
- } finally {
2083
- this.isPolling = false;
2084
- }
2085
- }
2086
- /**
2087
- * Poll for pending workflows and enqueue their first stage.
2088
- * Uses claimNextPendingRun() for zero-contention claiming with FOR UPDATE SKIP LOCKED.
2089
- */
2090
- async pollPendingWorkflows() {
2091
- let claimedCount = 0;
2092
- while (true) {
2093
- const run = await this.persistence.claimNextPendingRun();
2094
- if (!run) {
2095
- break;
2096
- }
2097
- claimedCount++;
2098
- logger4.debug(`Claimed workflow ${run.id}`);
2099
- try {
2100
- const workflow = this.registry.getWorkflow(run.workflowId);
2101
- if (!workflow) {
2102
- await this.persistence.updateRun(run.id, { status: "FAILED" });
2103
- continue;
2104
- }
2105
- const firstStages = workflow.getStagesInExecutionGroup(1);
2106
- if (firstStages.length === 0) {
2107
- await this.persistence.updateRun(run.id, { status: "FAILED" });
2108
- continue;
2109
- }
2110
- await this.enqueueExecutionGroup(run, workflow, 1);
2111
- logger4.debug(`Started workflow ${run.id}`);
2112
- } catch (error) {
2113
- logger4.error(`Error starting workflow ${run.id}:`, error);
2114
- await this.persistence.updateRun(run.id, { status: "FAILED" });
2115
- }
2116
- }
2117
- if (claimedCount > 0) {
2118
- logger4.debug(`Processed ${claimedCount} pending workflow(s)`);
2119
- }
2120
- }
2121
- /**
2122
- * Poll suspended stages and resume if ready (public for manual triggering)
2123
- */
2124
- async pollSuspendedStages() {
2125
- const suspendedStages = await this.persistence.getSuspendedStages(
2126
- /* @__PURE__ */ new Date()
2127
- );
2128
- if (suspendedStages.length === 0) return;
2129
- logger4.debug(`Found ${suspendedStages.length} suspended stages`);
2130
- for (const stageRecord of suspendedStages) {
2131
- try {
2132
- const workflowRun = await this.persistence.getRun(
2133
- stageRecord.workflowRunId
2134
- );
2135
- if (!workflowRun) continue;
2136
- await this.checkAndResume({ ...stageRecord, workflowRun });
2137
- } catch (error) {
2138
- const errorMessage = error instanceof Error ? error.message : String(error);
2139
- logger4.error(`Error checking stage ${stageRecord.stageId}:`, error);
2140
- const isTransientError = errorMessage.includes("fetch failed") || errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("network") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("socket hang up");
2141
- if (isTransientError) {
2142
- logger4.debug(
2143
- `Transient error for stage ${stageRecord.stageId}, will retry on next poll`
2144
- );
2145
- const nextPollAt = new Date(Date.now() + this.pollIntervalMs);
2146
- await this.persistence.updateStage(stageRecord.id, { nextPollAt }).catch((err) => logger4.error("Failed to update stage:", err));
2147
- } else {
2148
- await this.markStageFailed(
2149
- stageRecord.id,
2150
- `Runtime error: ${errorMessage}`
2151
- ).catch((err) => logger4.error("Failed to mark stage failed:", err));
2152
- await this.persistence.updateRun(stageRecord.workflowRunId, { status: "FAILED" }).catch((err) => logger4.error("Failed to update run:", err));
2153
- }
2154
- }
2155
- }
2156
- }
2157
- /**
2158
- * Transition a workflow to its next state (public for external calls)
2159
- */
2160
- async transitionWorkflow(workflowRunId) {
2161
- const workflowRun = await this.persistence.getRun(workflowRunId);
2162
- if (!workflowRun) return;
2163
- if (["FAILED", "CANCELLED", "COMPLETED"].includes(workflowRun.status))
2164
- return;
2165
- const workflow = this.registry.getWorkflow(workflowRun.workflowId);
2166
- if (!workflow) return;
2167
- const stages = await this.persistence.getStagesByRun(workflowRunId);
2168
- if (stages.length === 0) {
2169
- await this.enqueueExecutionGroup(workflowRun, workflow, 1);
2170
- return;
2171
- }
2172
- const activeStage = stages.find(
2173
- (s) => ["RUNNING", "PENDING", "SUSPENDED"].includes(s.status)
2174
- );
2175
- if (activeStage) return;
2176
- const maxGroup = Math.max(...stages.map((s) => s.executionGroup));
2177
- const nextStages = workflow.getStagesInExecutionGroup(maxGroup + 1);
2178
- if (nextStages.length > 0) {
2179
- await this.enqueueExecutionGroup(workflowRun, workflow, maxGroup + 1);
2180
- } else {
2181
- await this.completeWorkflow(workflowRun);
2182
- }
2183
- }
2184
- // ==========================================================================
2185
- // Private helpers
2186
- // ==========================================================================
2187
- async checkAndResume(stageRecord) {
2188
- const { workflowRun } = stageRecord;
2189
- const workflow = this.registry.getWorkflow(workflowRun.workflowId);
2190
- if (!workflow) {
2191
- await this.markStageFailed(
2192
- stageRecord.id,
2193
- "Workflow definition not found"
2194
- );
2195
- return;
2196
- }
2197
- const stageDef = workflow.getStage(stageRecord.stageId);
2198
- if (!stageDef || !stageDef.checkCompletion) {
2199
- await this.markStageFailed(
2200
- stageRecord.id,
2201
- "Stage does not support batch completion"
2202
- );
2203
- return;
2204
- }
2205
- const storage = this.createStorageShim(
2206
- workflowRun.id,
2207
- workflowRun.workflowType
2208
- );
2209
- const logFn = async (level, message) => {
2210
- await this.persistence.createLog({
2211
- workflowRunId: workflowRun.id,
2212
- workflowStageId: stageRecord.id,
2213
- level,
2214
- message: `[Runtime] ${message}`
2215
- }).catch((err) => logger4.error("Failed to create log:", err));
2216
- };
2217
- const completionResult = await stageDef.checkCompletion(
2218
- stageRecord.suspendedState,
2219
- {
2220
- workflowRunId: workflowRun.id,
2221
- stageId: stageRecord.stageId,
2222
- stageRecordId: stageRecord.id,
2223
- // For LogContext in AIHelper
2224
- config: stageRecord.config || {},
2225
- log: logFn,
2226
- onLog: logFn,
2227
- storage
2228
- }
2229
- );
2230
- logger4.debug("Stage completion result", completionResult);
2231
- if (completionResult.ready) {
2232
- if (completionResult.output !== void 0) {
2233
- const validatedOutput = stageDef.outputSchema.parse(
2234
- completionResult.output
2235
- );
2236
- const outputKey = await this.persistence.saveStageOutput(
2237
- workflowRun.id,
2238
- workflowRun.workflowType,
2239
- stageRecord.stageId,
2240
- validatedOutput
2241
- );
2242
- await this.persistence.updateStage(stageRecord.id, {
2243
- status: "COMPLETED",
2244
- completedAt: /* @__PURE__ */ new Date(),
2245
- duration: Date.now() - new Date(stageRecord.startedAt).getTime(),
2246
- outputData: { _artifactKey: outputKey },
2247
- metrics: completionResult.metrics,
2248
- embeddingInfo: completionResult.embeddings
2249
- });
2250
- } else {
2251
- await this.persistence.updateStage(stageRecord.id, {
2252
- nextPollAt: null
2253
- });
2254
- }
2255
- await this.resumeWorkflow(workflowRun, workflow);
2256
- } else if (completionResult.error) {
2257
- await this.markStageFailed(stageRecord.id, completionResult.error);
2258
- await this.persistence.updateRun(workflowRun.id, { status: "FAILED" });
2259
- workflowEventBus.emitWorkflowEvent(workflowRun.id, "workflow:failed", {
2260
- workflowRunId: workflowRun.id,
2261
- error: completionResult.error
2262
- });
2263
- } else {
2264
- const nextPollAt = new Date(
2265
- Date.now() + (completionResult.nextCheckIn || this.pollIntervalMs)
2266
- );
2267
- await this.persistence.updateStage(stageRecord.id, { nextPollAt });
2268
- }
2269
- }
2270
- async resumeWorkflow(workflowRun, workflow) {
2271
- try {
2272
- await this.persistence.updateRun(workflowRun.id, { status: "RUNNING" });
2273
- const executor = new WorkflowExecutor(
2274
- workflow,
2275
- workflowRun.id,
2276
- workflowRun.workflowType,
2277
- {
2278
- persistence: this.persistence,
2279
- aiLogger: this.aiCallLogger
2280
- }
2281
- );
2282
- await executor.execute(workflowRun.input, workflowRun.config || {}, {
2283
- resume: true
2284
- });
2285
- } catch (error) {
2286
- await this.persistence.updateRun(workflowRun.id, { status: "FAILED" });
2287
- workflowEventBus.emitWorkflowEvent(workflowRun.id, "workflow:failed", {
2288
- workflowRunId: workflowRun.id,
2289
- error: error instanceof Error ? error.message : String(error)
2290
- });
2291
- throw error;
2292
- }
2293
- }
2294
- /**
2295
- * Create a minimal storage shim for context.storage (for API compatibility).
2296
- */
2297
- createStorageShim(workflowRunId, workflowType) {
2298
- const persistence = this.persistence;
2299
- return {
2300
- async save(key, data) {
2301
- await persistence.saveArtifact({
2302
- workflowRunId,
2303
- key,
2304
- type: "ARTIFACT",
2305
- data,
2306
- size: Buffer.byteLength(JSON.stringify(data), "utf8")
2307
- });
2308
- },
2309
- async load(key) {
2310
- return persistence.loadArtifact(workflowRunId, key);
2311
- },
2312
- async exists(key) {
2313
- return persistence.hasArtifact(workflowRunId, key);
2314
- },
2315
- async delete(key) {
2316
- return persistence.deleteArtifact(workflowRunId, key);
2317
- },
2318
- getStageKey(stageId, suffix) {
2319
- const base = `workflow-v2/${workflowType}/${workflowRunId}/${stageId}`;
2320
- return suffix ? `${base}/${suffix}` : `${base}/output.json`;
2321
- }
2322
- };
2323
- }
2324
- async markStageFailed(stageId, errorMessage) {
2325
- await this.persistence.updateStage(stageId, {
2326
- status: "FAILED",
2327
- completedAt: /* @__PURE__ */ new Date(),
2328
- errorMessage
2329
- });
2330
- }
2331
- async enqueueExecutionGroup(workflowRun, workflow, groupIndex) {
2332
- const stages = workflow.getStagesInExecutionGroup(groupIndex);
2333
- if (stages.length === 0) return;
2334
- for (const stage of stages) {
2335
- await this.persistence.createStage({
2336
- workflowRunId: workflowRun.id,
2337
- stageId: stage.id,
2338
- stageName: stage.name,
2339
- stageNumber: workflow.getStageIndex(stage.id) + 1,
2340
- executionGroup: groupIndex,
2341
- status: "PENDING",
2342
- config: workflowRun.config?.[stage.id] || {}
2343
- });
2344
- }
2345
- await this.jobQueue.enqueueParallel(
2346
- stages.map((stage) => ({
2347
- workflowRunId: workflowRun.id,
2348
- stageId: stage.id,
2349
- priority: workflowRun.priority,
2350
- payload: { config: workflowRun.config || {} }
2351
- }))
2352
- );
2353
- }
2354
- async completeWorkflow(workflowRun) {
2355
- const stages = await this.persistence.getStagesByRun(workflowRun.id);
2356
- let totalCost = 0, totalTokens = 0;
2357
- for (const stage of stages) {
2358
- const metrics = stage.metrics;
2359
- if (metrics) {
2360
- totalCost += metrics.cost || 0;
2361
- totalTokens += (metrics.inputTokens || 0) + (metrics.outputTokens || 0);
2362
- }
2363
- }
2364
- await this.persistence.updateRun(workflowRun.id, {
2365
- status: "COMPLETED",
2366
- completedAt: /* @__PURE__ */ new Date(),
2367
- duration: Date.now() - new Date(workflowRun.createdAt).getTime(),
2368
- totalCost,
2369
- totalTokens
2370
- });
2371
- workflowEventBus.emitWorkflowEvent(workflowRun.id, "workflow:completed", {
2372
- workflowRunId: workflowRun.id,
2373
- output: workflowRun.output || {},
2374
- duration: Date.now() - new Date(workflowRun.createdAt).getTime(),
2375
- totalCost,
2376
- totalTokens
2377
- });
2378
- logger4.debug(`Workflow ${workflowRun.id} completed`);
2379
- }
2380
- };
2381
- function createWorkflowRuntime(config) {
2382
- return new WorkflowRuntime(config);
385
+ function withFeatureFlags(schema) {
386
+ return schema.merge(FeatureFlagsConfigSchema);
387
+ }
388
+ function withStandardConfig(schema) {
389
+ return schema.merge(AIConfigSchema).merge(ConcurrencyConfigSchema).merge(FeatureFlagsConfigSchema);
2383
390
  }
391
+ z.object({});
392
+ z.object({
393
+ model: ModelKey.default("gemini-2.5-flash"),
394
+ temperature: z.number().min(0).max(2).default(0.7)
395
+ });
2384
396
 
2385
- export { StageExecutor, Workflow, WorkflowBuilder, WorkflowExecutor, WorkflowRuntime, createStorage, createWorkflowRuntime, getDefaultStorageProvider, workflowEventBus };
397
+ export { AIConfigSchema, ConcurrencyConfigSchema, DebugConfigSchema, FeatureFlagsConfigSchema, Workflow, WorkflowBuilder, withAIConfig, withConcurrency, withFeatureFlags, withStandardConfig };
2386
398
  //# sourceMappingURL=index.js.map
2387
399
  //# sourceMappingURL=index.js.map