@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.
- package/README.md +270 -513
- package/dist/chunk-HL3OJG7W.js +1033 -0
- package/dist/chunk-HL3OJG7W.js.map +1 -0
- package/dist/{chunk-7IITBLFY.js → chunk-NYKMT46J.js} +268 -25
- package/dist/chunk-NYKMT46J.js.map +1 -0
- package/dist/chunk-SPXBCZLB.js +17 -0
- package/dist/chunk-SPXBCZLB.js.map +1 -0
- package/dist/{client-5vz5Vv4A.d.ts → client-D4PoxADF.d.ts} +3 -143
- package/dist/client.d.ts +3 -2
- package/dist/{index-DmR3E8D7.d.ts → index-DAzCfO1R.d.ts} +20 -1
- package/dist/index.d.ts +234 -601
- package/dist/index.js +46 -2034
- package/dist/index.js.map +1 -1
- package/dist/{interface-Cv22wvLG.d.ts → interface-MMqhfQQK.d.ts} +69 -2
- package/dist/kernel/index.d.ts +26 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/index.js.map +1 -0
- package/dist/kernel/testing/index.d.ts +44 -0
- package/dist/kernel/testing/index.js +85 -0
- package/dist/kernel/testing/index.js.map +1 -0
- package/dist/persistence/index.d.ts +2 -2
- package/dist/persistence/index.js +2 -1
- package/dist/persistence/prisma/index.d.ts +2 -2
- package/dist/persistence/prisma/index.js +2 -1
- package/dist/plugins-BCnDUwIc.d.ts +415 -0
- package/dist/ports-tU3rzPXJ.d.ts +245 -0
- package/dist/stage-BPw7m9Wx.d.ts +144 -0
- package/dist/testing/index.d.ts +23 -1
- package/dist/testing/index.js +156 -13
- package/dist/testing/index.js.map +1 -1
- package/package.json +11 -1
- package/skills/workflow-engine/SKILL.md +234 -348
- package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
- package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
- package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
- package/skills/workflow-engine/references/08-common-patterns.md +118 -431
- package/dist/chunk-7IITBLFY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
5
|
-
import
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
var
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
1836
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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 {
|
|
397
|
+
export { AIConfigSchema, ConcurrencyConfigSchema, DebugConfigSchema, FeatureFlagsConfigSchema, Workflow, WorkflowBuilder, withAIConfig, withConcurrency, withFeatureFlags, withStandardConfig };
|
|
2386
398
|
//# sourceMappingURL=index.js.map
|
|
2387
399
|
//# sourceMappingURL=index.js.map
|