@xyne/workflow-sdk 1.0.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 +36 -0
- package/dist/agents/agent-step.d.ts +152 -0
- package/dist/agents/agent-step.d.ts.map +1 -0
- package/dist/agents/agent-step.js +403 -0
- package/dist/agents/agent-step.js.map +1 -0
- package/dist/agents/base-runtime.d.ts +38 -0
- package/dist/agents/base-runtime.d.ts.map +1 -0
- package/dist/agents/base-runtime.js +21 -0
- package/dist/agents/base-runtime.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +17 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/interceptor.d.ts +19 -0
- package/dist/agents/interceptor.d.ts.map +1 -0
- package/dist/agents/interceptor.js +21 -0
- package/dist/agents/interceptor.js.map +1 -0
- package/dist/agents/interceptors/approval-gate.d.ts +14 -0
- package/dist/agents/interceptors/approval-gate.d.ts.map +1 -0
- package/dist/agents/interceptors/approval-gate.js +19 -0
- package/dist/agents/interceptors/approval-gate.js.map +1 -0
- package/dist/agents/interceptors/tool-logger.d.ts +15 -0
- package/dist/agents/interceptors/tool-logger.d.ts.map +1 -0
- package/dist/agents/interceptors/tool-logger.js +23 -0
- package/dist/agents/interceptors/tool-logger.js.map +1 -0
- package/dist/agents/pi-mono-runtime.d.ts +88 -0
- package/dist/agents/pi-mono-runtime.d.ts.map +1 -0
- package/dist/agents/pi-mono-runtime.js +129 -0
- package/dist/agents/pi-mono-runtime.js.map +1 -0
- package/dist/agents/tool-registry.d.ts +25 -0
- package/dist/agents/tool-registry.d.ts.map +1 -0
- package/dist/agents/tool-registry.js +47 -0
- package/dist/agents/tool-registry.js.map +1 -0
- package/dist/agents/tool-types.d.ts +25 -0
- package/dist/agents/tool-types.d.ts.map +1 -0
- package/dist/agents/tool-types.js +9 -0
- package/dist/agents/tool-types.js.map +1 -0
- package/dist/agents/types.d.ts +91 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +12 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/builder/index.d.ts +30 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +32 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +128 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +10 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workflow-client.d.ts +15 -0
- package/dist/client/workflow-client.d.ts.map +1 -0
- package/dist/client/workflow-client.js +293 -0
- package/dist/client/workflow-client.js.map +1 -0
- package/dist/common/attachment.d.ts +31 -0
- package/dist/common/attachment.d.ts.map +1 -0
- package/dist/common/attachment.js +21 -0
- package/dist/common/attachment.js.map +1 -0
- package/dist/common/executable-check.d.ts +42 -0
- package/dist/common/executable-check.d.ts.map +1 -0
- package/dist/common/executable-check.js +115 -0
- package/dist/common/executable-check.js.map +1 -0
- package/dist/common/index.d.ts +21 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +19 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/resume-payload.d.ts +34 -0
- package/dist/common/resume-payload.d.ts.map +1 -0
- package/dist/common/resume-payload.js +12 -0
- package/dist/common/resume-payload.js.map +1 -0
- package/dist/engine/available-context.d.ts +29 -0
- package/dist/engine/available-context.d.ts.map +1 -0
- package/dist/engine/available-context.js +66 -0
- package/dist/engine/available-context.js.map +1 -0
- package/dist/engine/condition-evaluator.d.ts +13 -0
- package/dist/engine/condition-evaluator.d.ts.map +1 -0
- package/dist/engine/condition-evaluator.js +92 -0
- package/dist/engine/condition-evaluator.js.map +1 -0
- package/dist/engine/config-validator.d.ts +25 -0
- package/dist/engine/config-validator.d.ts.map +1 -0
- package/dist/engine/config-validator.js +316 -0
- package/dist/engine/config-validator.js.map +1 -0
- package/dist/engine/pause-step.d.ts +20 -0
- package/dist/engine/pause-step.d.ts.map +1 -0
- package/dist/engine/pause-step.js +24 -0
- package/dist/engine/pause-step.js.map +1 -0
- package/dist/engine/service-registry.d.ts +49 -0
- package/dist/engine/service-registry.d.ts.map +1 -0
- package/dist/engine/service-registry.js +76 -0
- package/dist/engine/service-registry.js.map +1 -0
- package/dist/engine/variable-resolver.d.ts +29 -0
- package/dist/engine/variable-resolver.d.ts.map +1 -0
- package/dist/engine/variable-resolver.js +130 -0
- package/dist/engine/variable-resolver.js.map +1 -0
- package/dist/engine/workflow-executor.d.ts +96 -0
- package/dist/engine/workflow-executor.d.ts.map +1 -0
- package/dist/engine/workflow-executor.js +837 -0
- package/dist/engine/workflow-executor.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/in-memory-adapter.d.ts +138 -0
- package/dist/persistence/in-memory-adapter.d.ts.map +1 -0
- package/dist/persistence/in-memory-adapter.js +315 -0
- package/dist/persistence/in-memory-adapter.js.map +1 -0
- package/dist/persistence/types.d.ts +214 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +8 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/router/types.d.ts +57 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +8 -0
- package/dist/router/types.js.map +1 -0
- package/dist/router/workflow-router.d.ts +26 -0
- package/dist/router/workflow-router.d.ts.map +1 -0
- package/dist/router/workflow-router.js +611 -0
- package/dist/router/workflow-router.js.map +1 -0
- package/dist/runtime/execution-event-bus.d.ts +38 -0
- package/dist/runtime/execution-event-bus.d.ts.map +1 -0
- package/dist/runtime/execution-event-bus.js +87 -0
- package/dist/runtime/execution-event-bus.js.map +1 -0
- package/dist/runtime/types.d.ts +129 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/workflow-runtime.d.ts +256 -0
- package/dist/runtime/workflow-runtime.d.ts.map +1 -0
- package/dist/runtime/workflow-runtime.js +699 -0
- package/dist/runtime/workflow-runtime.js.map +1 -0
- package/dist/steps/base-step.d.ts +204 -0
- package/dist/steps/base-step.d.ts.map +1 -0
- package/dist/steps/base-step.js +69 -0
- package/dist/steps/base-step.js.map +1 -0
- package/dist/steps/builtin/code.step.d.ts +95 -0
- package/dist/steps/builtin/code.step.d.ts.map +1 -0
- package/dist/steps/builtin/code.step.js +122 -0
- package/dist/steps/builtin/code.step.js.map +1 -0
- package/dist/steps/builtin/conditional.step.d.ts +146 -0
- package/dist/steps/builtin/conditional.step.d.ts.map +1 -0
- package/dist/steps/builtin/conditional.step.js +71 -0
- package/dist/steps/builtin/conditional.step.js.map +1 -0
- package/dist/steps/builtin/dedup.step.d.ts +65 -0
- package/dist/steps/builtin/dedup.step.d.ts.map +1 -0
- package/dist/steps/builtin/dedup.step.js +61 -0
- package/dist/steps/builtin/dedup.step.js.map +1 -0
- package/dist/steps/builtin/http-request.step.d.ts +928 -0
- package/dist/steps/builtin/http-request.step.d.ts.map +1 -0
- package/dist/steps/builtin/http-request.step.js +570 -0
- package/dist/steps/builtin/http-request.step.js.map +1 -0
- package/dist/steps/builtin/loop.step.d.ts +100 -0
- package/dist/steps/builtin/loop.step.d.ts.map +1 -0
- package/dist/steps/builtin/loop.step.js +79 -0
- package/dist/steps/builtin/loop.step.js.map +1 -0
- package/dist/steps/builtin/parallel.step.d.ts +208 -0
- package/dist/steps/builtin/parallel.step.d.ts.map +1 -0
- package/dist/steps/builtin/parallel.step.js +249 -0
- package/dist/steps/builtin/parallel.step.js.map +1 -0
- package/dist/steps/builtin/switch.step.d.ts +200 -0
- package/dist/steps/builtin/switch.step.d.ts.map +1 -0
- package/dist/steps/builtin/switch.step.js +92 -0
- package/dist/steps/builtin/switch.step.js.map +1 -0
- package/dist/steps/builtin/transform.step.d.ts +247 -0
- package/dist/steps/builtin/transform.step.d.ts.map +1 -0
- package/dist/steps/builtin/transform.step.js +135 -0
- package/dist/steps/builtin/transform.step.js.map +1 -0
- package/dist/steps/builtin/wait.step.d.ts +921 -0
- package/dist/steps/builtin/wait.step.d.ts.map +1 -0
- package/dist/steps/builtin/wait.step.js +211 -0
- package/dist/steps/builtin/wait.step.js.map +1 -0
- package/dist/steps/step-registry.d.ts +64 -0
- package/dist/steps/step-registry.d.ts.map +1 -0
- package/dist/steps/step-registry.js +102 -0
- package/dist/steps/step-registry.js.map +1 -0
- package/dist/storage/in-memory-adapter.d.ts +25 -0
- package/dist/storage/in-memory-adapter.d.ts.map +1 -0
- package/dist/storage/in-memory-adapter.js +41 -0
- package/dist/storage/in-memory-adapter.js.map +1 -0
- package/dist/storage/types.d.ts +53 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +13 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-context.d.ts +13 -0
- package/dist/testing/mock-context.d.ts.map +1 -0
- package/dist/testing/mock-context.js +21 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/testing/mock-step-context.d.ts +47 -0
- package/dist/testing/mock-step-context.d.ts.map +1 -0
- package/dist/testing/mock-step-context.js +59 -0
- package/dist/testing/mock-step-context.js.map +1 -0
- package/dist/triggers/base-trigger.d.ts +58 -0
- package/dist/triggers/base-trigger.d.ts.map +1 -0
- package/dist/triggers/base-trigger.js +37 -0
- package/dist/triggers/base-trigger.js.map +1 -0
- package/dist/triggers/builtin/default-cron-trigger.d.ts +44 -0
- package/dist/triggers/builtin/default-cron-trigger.d.ts.map +1 -0
- package/dist/triggers/builtin/default-cron-trigger.js +31 -0
- package/dist/triggers/builtin/default-cron-trigger.js.map +1 -0
- package/dist/triggers/builtin/default-event-trigger.d.ts +32 -0
- package/dist/triggers/builtin/default-event-trigger.d.ts.map +1 -0
- package/dist/triggers/builtin/default-event-trigger.js +21 -0
- package/dist/triggers/builtin/default-event-trigger.js.map +1 -0
- package/dist/triggers/builtin/default-manual-trigger.d.ts +119 -0
- package/dist/triggers/builtin/default-manual-trigger.d.ts.map +1 -0
- package/dist/triggers/builtin/default-manual-trigger.js +64 -0
- package/dist/triggers/builtin/default-manual-trigger.js.map +1 -0
- package/dist/triggers/builtin/default-webhook-trigger.d.ts +72 -0
- package/dist/triggers/builtin/default-webhook-trigger.d.ts.map +1 -0
- package/dist/triggers/builtin/default-webhook-trigger.js +91 -0
- package/dist/triggers/builtin/default-webhook-trigger.js.map +1 -0
- package/dist/triggers/cron-trigger.d.ts +52 -0
- package/dist/triggers/cron-trigger.d.ts.map +1 -0
- package/dist/triggers/cron-trigger.js +15 -0
- package/dist/triggers/cron-trigger.js.map +1 -0
- package/dist/triggers/event-trigger.d.ts +14 -0
- package/dist/triggers/event-trigger.d.ts.map +1 -0
- package/dist/triggers/event-trigger.js +13 -0
- package/dist/triggers/event-trigger.js.map +1 -0
- package/dist/triggers/manual-trigger.d.ts +11 -0
- package/dist/triggers/manual-trigger.d.ts.map +1 -0
- package/dist/triggers/manual-trigger.js +10 -0
- package/dist/triggers/manual-trigger.js.map +1 -0
- package/dist/triggers/trigger-registry.d.ts +48 -0
- package/dist/triggers/trigger-registry.d.ts.map +1 -0
- package/dist/triggers/trigger-registry.js +81 -0
- package/dist/triggers/trigger-registry.js.map +1 -0
- package/dist/triggers/webhook-trigger.d.ts +54 -0
- package/dist/triggers/webhook-trigger.d.ts.map +1 -0
- package/dist/triggers/webhook-trigger.js +13 -0
- package/dist/triggers/webhook-trigger.js.map +1 -0
- package/dist/types/attachment.d.ts +23 -0
- package/dist/types/attachment.d.ts.map +1 -0
- package/dist/types/attachment.js +2 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/categories.d.ts +14 -0
- package/dist/types/categories.d.ts.map +1 -0
- package/dist/types/categories.js +10 -0
- package/dist/types/categories.js.map +1 -0
- package/dist/types/context.d.ts +61 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/known-types.d.ts +34 -0
- package/dist/types/known-types.d.ts.map +1 -0
- package/dist/types/known-types.js +49 -0
- package/dist/types/known-types.js.map +1 -0
- package/dist/types/operators.d.ts +20 -0
- package/dist/types/operators.d.ts.map +1 -0
- package/dist/types/operators.js +27 -0
- package/dist/types/operators.js.map +1 -0
- package/dist/types/pause-path.d.ts +23 -0
- package/dist/types/pause-path.d.ts.map +1 -0
- package/dist/types/pause-path.js +2 -0
- package/dist/types/pause-path.js.map +1 -0
- package/dist/types/pause-state.d.ts +100 -0
- package/dist/types/pause-state.d.ts.map +1 -0
- package/dist/types/pause-state.js +15 -0
- package/dist/types/pause-state.js.map +1 -0
- package/dist/types/resume-payload.d.ts +34 -0
- package/dist/types/resume-payload.d.ts.map +1 -0
- package/dist/types/resume-payload.js +12 -0
- package/dist/types/resume-payload.js.map +1 -0
- package/dist/types/status.d.ts +27 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/status.js +41 -0
- package/dist/types/status.js.map +1 -0
- package/dist/types/step-events.d.ts +22 -0
- package/dist/types/step-events.d.ts.map +1 -0
- package/dist/types/step-events.js +8 -0
- package/dist/types/step-events.js.map +1 -0
- package/dist/types/step-types.d.ts +12 -0
- package/dist/types/step-types.d.ts.map +1 -0
- package/dist/types/step-types.js +2 -0
- package/dist/types/step-types.js.map +1 -0
- package/dist/types/trigger-types.d.ts +12 -0
- package/dist/types/trigger-types.d.ts.map +1 -0
- package/dist/types/trigger-types.js +2 -0
- package/dist/types/trigger-types.js.map +1 -0
- package/dist/types/validation.d.ts +19 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +11 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/types/workflow-config.d.ts +122 -0
- package/dist/types/workflow-config.d.ts.map +1 -0
- package/dist/types/workflow-config.js +71 -0
- package/dist/types/workflow-config.js.map +1 -0
- package/dist/util/executable-check.d.ts +42 -0
- package/dist/util/executable-check.d.ts.map +1 -0
- package/dist/util/executable-check.js +115 -0
- package/dist/util/executable-check.js.map +1 -0
- package/dist/util/schema-convert.d.ts +14 -0
- package/dist/util/schema-convert.d.ts.map +1 -0
- package/dist/util/schema-convert.js +16 -0
- package/dist/util/schema-convert.js.map +1 -0
- package/dist/util/variable-ref.d.ts +52 -0
- package/dist/util/variable-ref.d.ts.map +1 -0
- package/dist/util/variable-ref.js +89 -0
- package/dist/util/variable-ref.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { CONTROL_FLOW_STEP_TYPES } from '../types/known-types.js';
|
|
2
|
+
import { BaseActionStep, BaseControlFlowStep, StepKind, createPauseFunction, } from '../steps/base-step.js';
|
|
3
|
+
import { VariableResolver, stripNullForOptionalKeys } from './variable-resolver.js';
|
|
4
|
+
import { PauseStep } from './pause-step.js';
|
|
5
|
+
const noopLogger = {
|
|
6
|
+
info: () => { },
|
|
7
|
+
warn: () => { },
|
|
8
|
+
error: () => { },
|
|
9
|
+
};
|
|
10
|
+
// ─── WorkflowExecutor ───
|
|
11
|
+
/**
|
|
12
|
+
* Core runtime engine for executing workflows.
|
|
13
|
+
*
|
|
14
|
+
* Orchestrates: step execution, variable resolution, pause/resume
|
|
15
|
+
* (including inside branches), error handling (onError: continue),
|
|
16
|
+
* retry, timeout, credential resolution, and state persistence.
|
|
17
|
+
*
|
|
18
|
+
* Host provides: PersistenceAdapter, StepRegistry, TriggerRegistry,
|
|
19
|
+
* ServiceRegistry, and an optional logger.
|
|
20
|
+
*/
|
|
21
|
+
export class WorkflowExecutor {
|
|
22
|
+
persistence;
|
|
23
|
+
stepRegistry;
|
|
24
|
+
triggerRegistry;
|
|
25
|
+
services;
|
|
26
|
+
resolver;
|
|
27
|
+
log;
|
|
28
|
+
eventBus;
|
|
29
|
+
baseUrl;
|
|
30
|
+
constructor(persistence, stepRegistry, triggerRegistry, services, options) {
|
|
31
|
+
this.persistence = persistence;
|
|
32
|
+
this.stepRegistry = stepRegistry;
|
|
33
|
+
this.triggerRegistry = triggerRegistry;
|
|
34
|
+
this.services = services;
|
|
35
|
+
this.resolver = options?.variableResolver ?? new VariableResolver();
|
|
36
|
+
this.log = options?.logger ?? noopLogger;
|
|
37
|
+
this.eventBus = options?.eventBus;
|
|
38
|
+
this.baseUrl = options?.baseUrl ?? '';
|
|
39
|
+
}
|
|
40
|
+
/** Emit an execution stream event (no-op if no eventBus). */
|
|
41
|
+
emitEvent(executionId, event) {
|
|
42
|
+
this.eventBus?.emit(executionId, event);
|
|
43
|
+
}
|
|
44
|
+
// ─── Public API ───
|
|
45
|
+
/**
|
|
46
|
+
* Run or resume an execution by ID.
|
|
47
|
+
*
|
|
48
|
+
* Loads the execution from persistence, determines if it's a fresh start
|
|
49
|
+
* or a resume, then walks through the steps.
|
|
50
|
+
*/
|
|
51
|
+
async runExecution(executionId) {
|
|
52
|
+
const exec = await this.persistence.getExecution(executionId);
|
|
53
|
+
if (!exec) {
|
|
54
|
+
this.log.warn(`runExecution: execution ${executionId} not found`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const isResume = exec.status === 'EXTERNAL_WAIT';
|
|
58
|
+
const isFresh = exec.status === 'PENDING' || exec.status === 'SCHEDULED';
|
|
59
|
+
if (!isFresh && !isResume) {
|
|
60
|
+
this.log.warn(`runExecution: execution ${executionId} status=${exec.status} — skipping`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const workflow = await this.persistence.getWorkflow(exec.workflowId);
|
|
64
|
+
if (!workflow) {
|
|
65
|
+
await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
|
|
66
|
+
this.log.warn(`runExecution: workflow ${exec.workflowId} not found — CANCELLED`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const config = JSON.parse(workflow.config ?? '{}');
|
|
70
|
+
const state = await this.persistence.getExecutionState(executionId);
|
|
71
|
+
if (!state?.context) {
|
|
72
|
+
if (isResume) {
|
|
73
|
+
throw new Error(`runExecution: pause state missing for resume of ${executionId}`);
|
|
74
|
+
}
|
|
75
|
+
await this.persistence.updateExecutionStatus(executionId, 'FAILED');
|
|
76
|
+
this.log.error(`runExecution: state.context missing for ${executionId} — FAILED`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let context;
|
|
80
|
+
try {
|
|
81
|
+
context = JSON.parse(state.context);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
if (isResume) {
|
|
85
|
+
throw new Error(`runExecution: state.context unparseable for resume of ${executionId}`);
|
|
86
|
+
}
|
|
87
|
+
await this.persistence.updateExecutionStatus(executionId, 'FAILED');
|
|
88
|
+
this.log.error(`runExecution: state.context unparseable for ${executionId} — FAILED`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Hydrate trigger data if fresh run
|
|
92
|
+
if (!isResume && this.triggerRegistry.has(config.trigger.type)) {
|
|
93
|
+
const triggerImpl = this.triggerRegistry.get(config.trigger.type);
|
|
94
|
+
if (typeof triggerImpl.hydratePayload === 'function') {
|
|
95
|
+
try {
|
|
96
|
+
const triggerData = context.trigger;
|
|
97
|
+
const hydrated = await triggerImpl.hydratePayload(triggerData);
|
|
98
|
+
context.trigger = {
|
|
99
|
+
...context.trigger,
|
|
100
|
+
...hydrated,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.log.warn(`runExecution: hydratePayload failed for ${executionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Filter check
|
|
108
|
+
const filterConfig = (config.trigger.config ?? {});
|
|
109
|
+
if (!triggerImpl.matchFilters(filterConfig, context.trigger)) {
|
|
110
|
+
await this.persistence.updateExecutionStatus(executionId, 'SKIPPED');
|
|
111
|
+
await this.persistence.persistState(executionId, {
|
|
112
|
+
context: JSON.stringify(context),
|
|
113
|
+
});
|
|
114
|
+
this.log.info(`runExecution: filter mismatch for ${executionId} — SKIPPED`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Resume: hydrate step context from persisted step rows
|
|
119
|
+
let startIndex = 0;
|
|
120
|
+
let resumeAtIndex;
|
|
121
|
+
let branchResumePath;
|
|
122
|
+
if (isResume) {
|
|
123
|
+
const stepRows = await this.persistence.getStepRows(executionId);
|
|
124
|
+
for (const row of stepRows) {
|
|
125
|
+
const idx = parseStepIndex(row.stepName);
|
|
126
|
+
if (idx === null)
|
|
127
|
+
continue;
|
|
128
|
+
const stepConfig = config.steps[idx];
|
|
129
|
+
if (!stepConfig || !row.data)
|
|
130
|
+
continue;
|
|
131
|
+
const parsed = safeParseJson(row.data);
|
|
132
|
+
if (parsed) {
|
|
133
|
+
context.steps[stepConfig.id] = {
|
|
134
|
+
type: parsed.type ?? stepConfig.type,
|
|
135
|
+
...(parsed.input !== undefined ? { input: parsed.input } : {}),
|
|
136
|
+
output: parsed.output ?? {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (state.pauseType === 'review') {
|
|
141
|
+
// ─── Review-gate resume ───
|
|
142
|
+
const reviewedIdx = state.currentStepIndex;
|
|
143
|
+
const reviewedStep = config.steps[reviewedIdx];
|
|
144
|
+
const resumePayload = await this.persistence.getResumePayload(executionId);
|
|
145
|
+
const action = resumePayload?.action ?? 'approve';
|
|
146
|
+
if (action === 'abort') {
|
|
147
|
+
await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
|
|
148
|
+
this.emitEvent(executionId, { event: 'execution_cancelled', data: {} });
|
|
149
|
+
this.log.info(`run ${executionId} review-gate ABORTED`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (action === 'retry' && reviewedStep) {
|
|
153
|
+
// Re-execute the reviewed step fresh, with priorState + feedback overrides.
|
|
154
|
+
startIndex = reviewedIdx;
|
|
155
|
+
const priorRow = await this.persistence.getStep(executionId, `step_${String(reviewedIdx)}`);
|
|
156
|
+
const priorState = priorRow?.data
|
|
157
|
+
? safeParseJson(priorRow.data) ?? undefined
|
|
158
|
+
: undefined;
|
|
159
|
+
context.__meta = {
|
|
160
|
+
...context.__meta,
|
|
161
|
+
...(priorState ? { __priorStepState: priorState } : {}),
|
|
162
|
+
__configOverrides: extractConfigOverrides(resumePayload),
|
|
163
|
+
};
|
|
164
|
+
// Clear the entry so it re-executes (and downstream resolvers don't see stale output)
|
|
165
|
+
delete context.steps[reviewedStep.id];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// approve: advance past the reviewed step
|
|
169
|
+
startIndex = reviewedIdx + 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// ─── Step-level pause resume (existing PausePath logic) ───
|
|
174
|
+
const savedPausePath = state.pausePath
|
|
175
|
+
? safeParseJson(state.pausePath)
|
|
176
|
+
: null;
|
|
177
|
+
if (savedPausePath && savedPausePath.length > 0) {
|
|
178
|
+
const first = savedPausePath[0];
|
|
179
|
+
startIndex = first.stepIndex;
|
|
180
|
+
resumeAtIndex = first.stepIndex;
|
|
181
|
+
branchResumePath =
|
|
182
|
+
savedPausePath.length > 1 ? savedPausePath.slice(1) : undefined;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
startIndex = state.currentStepIndex;
|
|
186
|
+
resumeAtIndex = state.currentStepIndex;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// ─── Fresh-run path: check for rerunFromStep seed ───
|
|
192
|
+
const rerun = context.__meta?.rerunSource;
|
|
193
|
+
if (rerun) {
|
|
194
|
+
startIndex = rerun.fromStepIndex;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Run — preserve transient seed fields (priorState, configOverrides, rerunSource)
|
|
198
|
+
// that the resume/rerun paths populated above.
|
|
199
|
+
const prevMeta = context.__meta ?? {};
|
|
200
|
+
context.__meta = {
|
|
201
|
+
...prevMeta,
|
|
202
|
+
error: null,
|
|
203
|
+
chain: prevMeta.chain ?? [],
|
|
204
|
+
};
|
|
205
|
+
await this.persistence.updateExecutionStatus(executionId, 'RUNNING');
|
|
206
|
+
await this.persistence.persistState(executionId, {
|
|
207
|
+
context: JSON.stringify(context),
|
|
208
|
+
});
|
|
209
|
+
this.log.info(`run ${isResume ? 'RESUMED' : 'STARTED'} execId=${executionId} workflow=${workflow.id} startIndex=${startIndex}`);
|
|
210
|
+
await this.runSteps(executionId, context, config.steps, startIndex, resumeAtIndex, branchResumePath, config.settings);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Start a fresh execution for a workflow.
|
|
214
|
+
*
|
|
215
|
+
* Creates the execution record + initial state, then runs it.
|
|
216
|
+
* Returns the execution ID.
|
|
217
|
+
*/
|
|
218
|
+
async startExecution(workflowId, context) {
|
|
219
|
+
const executionId = await this.persistence.createExecution({
|
|
220
|
+
workflowId,
|
|
221
|
+
status: 'PENDING',
|
|
222
|
+
context: JSON.stringify(context),
|
|
223
|
+
});
|
|
224
|
+
await this.runExecution(executionId);
|
|
225
|
+
return executionId;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Create a new execution that reruns from `fromStepId`, using the source
|
|
229
|
+
* execution's cached outputs for all prior steps as the resolved context.
|
|
230
|
+
*
|
|
231
|
+
* The new execution starts at `fromStepIndex` (not 0). Steps before that
|
|
232
|
+
* are pre-populated in `context.steps` so variable resolution finds them.
|
|
233
|
+
* The step at `fromStepIndex` receives the source row's data as
|
|
234
|
+
* `ctx.priorState` (when `inheritStepState` is true, the default), enabling
|
|
235
|
+
* agents and other stateful steps to continue from where they left off.
|
|
236
|
+
*
|
|
237
|
+
* Returns the new execution ID. Caller is responsible for enqueueing it.
|
|
238
|
+
*/
|
|
239
|
+
async rerunFromStep(sourceExecutionId, fromStepId, options) {
|
|
240
|
+
const inheritStepState = options?.inheritStepState ?? true;
|
|
241
|
+
const source = await this.persistence.getExecution(sourceExecutionId);
|
|
242
|
+
if (!source) {
|
|
243
|
+
throw new Error(`Source execution ${sourceExecutionId} not found`);
|
|
244
|
+
}
|
|
245
|
+
const terminal = ['COMPLETED', 'FAILED', 'CANCELLED'];
|
|
246
|
+
if (!terminal.includes(source.status)) {
|
|
247
|
+
throw new Error(`Cannot rerun from non-terminal execution ${sourceExecutionId} (status: ${source.status})`);
|
|
248
|
+
}
|
|
249
|
+
const workflow = await this.persistence.getWorkflow(source.workflowId);
|
|
250
|
+
if (!workflow) {
|
|
251
|
+
throw new Error(`Workflow ${source.workflowId} not found`);
|
|
252
|
+
}
|
|
253
|
+
const config = JSON.parse(workflow.config ?? '{}');
|
|
254
|
+
const fromStepIndex = config.steps.findIndex((s) => s.id === fromStepId);
|
|
255
|
+
if (fromStepIndex < 0) {
|
|
256
|
+
throw new Error(`Step "${fromStepId}" not found in workflow ${source.workflowId}`);
|
|
257
|
+
}
|
|
258
|
+
const sourceState = await this.persistence.getExecutionState(sourceExecutionId);
|
|
259
|
+
if (!sourceState?.context) {
|
|
260
|
+
throw new Error(`Source execution ${sourceExecutionId} has no persisted context`);
|
|
261
|
+
}
|
|
262
|
+
const sourceContext = JSON.parse(sourceState.context);
|
|
263
|
+
// Seed the new context with the source's trigger + steps 0..N-1 outputs.
|
|
264
|
+
const seededSteps = {};
|
|
265
|
+
const stepRows = await this.persistence.getStepRows(sourceExecutionId);
|
|
266
|
+
const rowByName = new Map();
|
|
267
|
+
for (const row of stepRows) {
|
|
268
|
+
rowByName.set(row.stepName, row);
|
|
269
|
+
}
|
|
270
|
+
for (let i = 0; i < fromStepIndex; i++) {
|
|
271
|
+
const stepCfg = config.steps[i];
|
|
272
|
+
const row = rowByName.get(`step_${String(i)}`);
|
|
273
|
+
if (!row?.data)
|
|
274
|
+
continue;
|
|
275
|
+
const parsed = safeParseJson(row.data);
|
|
276
|
+
if (parsed) {
|
|
277
|
+
seededSteps[stepCfg.id] = {
|
|
278
|
+
type: parsed.type ?? stepCfg.type,
|
|
279
|
+
...(parsed.input !== undefined ? { input: parsed.input } : {}),
|
|
280
|
+
output: parsed.output ?? {},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Priorstate for the entry step (the one being re-executed)
|
|
285
|
+
let priorStepState;
|
|
286
|
+
if (inheritStepState) {
|
|
287
|
+
const entryRow = rowByName.get(`step_${String(fromStepIndex)}`);
|
|
288
|
+
if (entryRow?.data) {
|
|
289
|
+
priorStepState =
|
|
290
|
+
safeParseJson(entryRow.data) ?? undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const newContext = {
|
|
294
|
+
workflow: sourceContext.workflow,
|
|
295
|
+
trigger: sourceContext.trigger,
|
|
296
|
+
steps: seededSteps,
|
|
297
|
+
__meta: {
|
|
298
|
+
error: null,
|
|
299
|
+
chain: sourceContext.__meta?.chain ?? [],
|
|
300
|
+
rerunSource: {
|
|
301
|
+
executionId: sourceExecutionId,
|
|
302
|
+
fromStepIndex,
|
|
303
|
+
fromStepId,
|
|
304
|
+
},
|
|
305
|
+
...(priorStepState ? { __priorStepState: priorStepState } : {}),
|
|
306
|
+
...(options?.configOverrides
|
|
307
|
+
? { __configOverrides: options.configOverrides }
|
|
308
|
+
: {}),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const newExecutionId = await this.persistence.createExecution({
|
|
312
|
+
workflowId: source.workflowId,
|
|
313
|
+
status: 'PENDING',
|
|
314
|
+
context: JSON.stringify(newContext),
|
|
315
|
+
sourceExecutionId,
|
|
316
|
+
});
|
|
317
|
+
this.log.info(`rerun created exec=${newExecutionId} from=${sourceExecutionId} step=${fromStepId}`);
|
|
318
|
+
return newExecutionId;
|
|
319
|
+
}
|
|
320
|
+
// ─── Internal: run loop ───
|
|
321
|
+
async runSteps(executionId, context, steps, startIndex, resumeAtIndex, branchResumePath, settings) {
|
|
322
|
+
try {
|
|
323
|
+
const walkResult = await this.walkSteps(steps, context, executionId, startIndex, resumeAtIndex, branchResumePath, settings);
|
|
324
|
+
if (walkResult.kind === 'paused') {
|
|
325
|
+
this.log.info(`run ${executionId} PAUSED at path=${JSON.stringify(walkResult.pausePath)}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
await this.persistence.updateExecutionStatus(executionId, 'COMPLETED');
|
|
329
|
+
context.__meta = { ...context.__meta, error: null };
|
|
330
|
+
await this.persistence.persistState(executionId, {
|
|
331
|
+
context: JSON.stringify(context),
|
|
332
|
+
currentStepIndex: steps.length,
|
|
333
|
+
});
|
|
334
|
+
this.emitEvent(executionId, { event: 'execution_completed', data: { status: 'COMPLETED' } });
|
|
335
|
+
this.log.info(`run ${executionId} COMPLETED`);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
339
|
+
await this.persistence.updateExecutionStatus(executionId, 'FAILED');
|
|
340
|
+
context.__meta = { ...context.__meta, error: errMsg };
|
|
341
|
+
await this.persistence.persistState(executionId, {
|
|
342
|
+
context: JSON.stringify(context),
|
|
343
|
+
});
|
|
344
|
+
this.emitEvent(executionId, { event: 'execution_failed', data: { error: errMsg } });
|
|
345
|
+
this.log.error(`run ${executionId} FAILED: ${errMsg}`);
|
|
346
|
+
throw err;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async walkSteps(steps, context, executionId, startIndex = 0, resumeAtIndex, branchResumePath, settings) {
|
|
350
|
+
const onError = settings?.onError ?? 'stop';
|
|
351
|
+
const retry = settings?.retry;
|
|
352
|
+
for (let i = startIndex; i < steps.length; i++) {
|
|
353
|
+
const step = steps[i];
|
|
354
|
+
const stepName = `step_${String(i)}`;
|
|
355
|
+
const isResuming = resumeAtIndex === i;
|
|
356
|
+
// Only pass branchResumePath for the step being resumed
|
|
357
|
+
const stepBranchResumePath = isResuming ? branchResumePath : undefined;
|
|
358
|
+
if (!isResuming) {
|
|
359
|
+
const executorType = CONTROL_FLOW_STEP_TYPES.has(step.type)
|
|
360
|
+
? 'conditional'
|
|
361
|
+
: 'deterministic';
|
|
362
|
+
await this.persistence.upsertStep(executionId, stepName, {
|
|
363
|
+
status: 'RUNNING',
|
|
364
|
+
executorType,
|
|
365
|
+
});
|
|
366
|
+
this.emitEvent(executionId, {
|
|
367
|
+
event: 'step_started',
|
|
368
|
+
data: { stepName: step.id, stepType: step.type },
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Consume entry-step extras (priorState + configOverrides) on the first
|
|
372
|
+
// iteration only. These come from review-gate retry or rerunFromStep.
|
|
373
|
+
const isEntryStep = i === startIndex && !isResuming;
|
|
374
|
+
const entryPriorState = isEntryStep
|
|
375
|
+
? context.__meta?.__priorStepState
|
|
376
|
+
: undefined;
|
|
377
|
+
const entryConfigOverrides = isEntryStep
|
|
378
|
+
? context.__meta?.__configOverrides
|
|
379
|
+
: undefined;
|
|
380
|
+
if (isEntryStep && context.__meta) {
|
|
381
|
+
// Single-use — clear so subsequent steps don't see them.
|
|
382
|
+
delete context.__meta.__priorStepState;
|
|
383
|
+
delete context.__meta.__configOverrides;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await this.executeStepWithRetry(step, context, { executionId, stepName, isResuming, priorState: entryPriorState, configOverrides: entryConfigOverrides }, stepBranchResumePath, retry);
|
|
387
|
+
const ctxEntry = context.steps[step.id];
|
|
388
|
+
await this.persistence.upsertStep(executionId, stepName, {
|
|
389
|
+
status: 'COMPLETED',
|
|
390
|
+
executorType: CONTROL_FLOW_STEP_TYPES.has(step.type)
|
|
391
|
+
? 'conditional'
|
|
392
|
+
: 'deterministic',
|
|
393
|
+
data: JSON.stringify(ctxEntry ?? { output: null }),
|
|
394
|
+
});
|
|
395
|
+
this.emitEvent(executionId, {
|
|
396
|
+
event: 'step_completed',
|
|
397
|
+
data: {
|
|
398
|
+
stepName: step.id,
|
|
399
|
+
output: (ctxEntry?.output ?? {}),
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
// ─── Review gate: pause after step completes if onComplete === 'review' ───
|
|
403
|
+
if (step.onComplete === 'review') {
|
|
404
|
+
const reviewPauseState = {
|
|
405
|
+
reason: `Review output of "${step.id}"`,
|
|
406
|
+
category: 'review',
|
|
407
|
+
display: ctxEntry?.output
|
|
408
|
+
? [{ label: 'Output', value: ctxEntry.output }]
|
|
409
|
+
: undefined,
|
|
410
|
+
actions: [
|
|
411
|
+
{ key: 'approve', label: 'Approve', style: 'primary' },
|
|
412
|
+
{ key: 'retry', label: 'Retry with Feedback', style: 'default', submitsForm: true },
|
|
413
|
+
{ key: 'abort', label: 'Reject', style: 'danger', abort: true },
|
|
414
|
+
],
|
|
415
|
+
form: {
|
|
416
|
+
fields: [
|
|
417
|
+
{
|
|
418
|
+
type: 'textarea',
|
|
419
|
+
name: 'feedback',
|
|
420
|
+
label: 'Feedback',
|
|
421
|
+
placeholder: 'Optional instructions for retry...',
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
await this.persistence.persistState(executionId, {
|
|
427
|
+
context: JSON.stringify(context),
|
|
428
|
+
currentStepIndex: i,
|
|
429
|
+
pauseType: 'review',
|
|
430
|
+
});
|
|
431
|
+
await this.persistence.updateExecutionStatus(executionId, 'EXTERNAL_WAIT');
|
|
432
|
+
this.emitEvent(executionId, {
|
|
433
|
+
event: 'execution_paused',
|
|
434
|
+
data: { stepName: step.id, pauseState: reviewPauseState },
|
|
435
|
+
});
|
|
436
|
+
this.log.info(`run ${executionId} REVIEW-GATE paused at step=${step.id}`);
|
|
437
|
+
return { kind: 'paused', pausePath: [], externalRef: undefined };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
if (PauseStep.is(err)) {
|
|
442
|
+
const pe = err;
|
|
443
|
+
const fullPath = [
|
|
444
|
+
{ stepIndex: i },
|
|
445
|
+
...(pe._pausePath ?? []),
|
|
446
|
+
];
|
|
447
|
+
const ctxEntry = context.steps[step.id];
|
|
448
|
+
const base = ctxEntry ?? { type: step.type, output: {} };
|
|
449
|
+
const merged = pe.statePatch
|
|
450
|
+
? { ...base, ...pe.statePatch }
|
|
451
|
+
: base;
|
|
452
|
+
await this.persistence.upsertStep(executionId, stepName, {
|
|
453
|
+
status: 'EXTERNAL_WAIT',
|
|
454
|
+
executorType: CONTROL_FLOW_STEP_TYPES.has(step.type)
|
|
455
|
+
? 'conditional'
|
|
456
|
+
: 'deterministic',
|
|
457
|
+
data: JSON.stringify(merged),
|
|
458
|
+
});
|
|
459
|
+
await this.persistence.persistState(executionId, {
|
|
460
|
+
context: JSON.stringify(context),
|
|
461
|
+
currentStepIndex: i,
|
|
462
|
+
pausePath: JSON.stringify(fullPath),
|
|
463
|
+
});
|
|
464
|
+
await this.persistence.updateExecutionStatus(executionId, 'EXTERNAL_WAIT');
|
|
465
|
+
this.emitEvent(executionId, {
|
|
466
|
+
event: 'execution_paused',
|
|
467
|
+
data: {
|
|
468
|
+
stepName: step.id,
|
|
469
|
+
pauseState: merged['pauseState'] ?? { reason: pe.message },
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
return {
|
|
473
|
+
kind: 'paused',
|
|
474
|
+
pausePath: fullPath,
|
|
475
|
+
externalRef: pe.externalRef,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
479
|
+
const ctxEntry = context.steps[step.id];
|
|
480
|
+
if (onError === 'continue') {
|
|
481
|
+
context.steps[step.id] = {
|
|
482
|
+
type: step.type,
|
|
483
|
+
...(ctxEntry?.input !== undefined
|
|
484
|
+
? { input: ctxEntry.input }
|
|
485
|
+
: {}),
|
|
486
|
+
output: ctxEntry?.output ?? {},
|
|
487
|
+
error: errMsg,
|
|
488
|
+
};
|
|
489
|
+
await this.persistence.upsertStep(executionId, stepName, {
|
|
490
|
+
status: 'FAILED',
|
|
491
|
+
executorType: CONTROL_FLOW_STEP_TYPES.has(step.type)
|
|
492
|
+
? 'conditional'
|
|
493
|
+
: 'deterministic',
|
|
494
|
+
data: JSON.stringify({ ...(ctxEntry ?? {}), error: errMsg }),
|
|
495
|
+
});
|
|
496
|
+
this.emitEvent(executionId, {
|
|
497
|
+
event: 'step_failed',
|
|
498
|
+
data: { stepName: step.id, error: errMsg },
|
|
499
|
+
});
|
|
500
|
+
this.log.warn(`step ${step.id} failed but continuing: ${errMsg}`);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
await this.persistence.upsertStep(executionId, stepName, {
|
|
504
|
+
status: 'FAILED',
|
|
505
|
+
executorType: CONTROL_FLOW_STEP_TYPES.has(step.type)
|
|
506
|
+
? 'conditional'
|
|
507
|
+
: 'deterministic',
|
|
508
|
+
data: JSON.stringify({ ...(ctxEntry ?? {}), error: errMsg }),
|
|
509
|
+
});
|
|
510
|
+
this.emitEvent(executionId, {
|
|
511
|
+
event: 'step_failed',
|
|
512
|
+
data: { stepName: step.id, error: errMsg },
|
|
513
|
+
});
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return { kind: 'completed' };
|
|
518
|
+
}
|
|
519
|
+
async executeStepWithRetry(step, context, callCtx, branchResumePath, retry) {
|
|
520
|
+
if (!retry || callCtx.isResuming) {
|
|
521
|
+
await this.executeStep(step, context, callCtx, branchResumePath);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
let lastError;
|
|
525
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt++) {
|
|
526
|
+
try {
|
|
527
|
+
await this.executeStep(step, context, callCtx, branchResumePath);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
if (PauseStep.is(err))
|
|
532
|
+
throw err;
|
|
533
|
+
lastError = err;
|
|
534
|
+
this.log.warn(`step ${step.id} attempt ${String(attempt)}/${String(retry.maxAttempts)} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
535
|
+
if (attempt < retry.maxAttempts) {
|
|
536
|
+
await delay(retry.intervalMs);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw lastError;
|
|
541
|
+
}
|
|
542
|
+
async executeStep(step, context, callCtx, branchResumePath) {
|
|
543
|
+
const stepImpl = this.stepRegistry.get(step.type);
|
|
544
|
+
if (stepImpl.kind === StepKind.CONTROL) {
|
|
545
|
+
const controlImpl = stepImpl;
|
|
546
|
+
const parsed = controlImpl.configSchema.safeParse(step.config);
|
|
547
|
+
if (!parsed.success) {
|
|
548
|
+
throw new Error(`Step "${step.id}" (${step.type}) config validation failed:\n${formatZodErrors(parsed.error)}`);
|
|
549
|
+
}
|
|
550
|
+
this.log.info(`step ${callCtx.isResuming ? 'RESUME' : 'START'} id=${step.id} type=${step.type}`);
|
|
551
|
+
const t0 = Date.now();
|
|
552
|
+
const stepCtx = await this.buildStepContext(context, { ...callCtx, stepId: step.id }, stepImpl);
|
|
553
|
+
const output = await controlImpl.execute(parsed.data, {
|
|
554
|
+
...stepCtx,
|
|
555
|
+
walkBranch: (branchSteps, ctx, branchKey) => this.walkBranchSteps(branchSteps, ctx, callCtx.executionId, step.id, branchKey, branchResumePath),
|
|
556
|
+
});
|
|
557
|
+
context.steps[step.id] = { type: step.type, output };
|
|
558
|
+
this.log.info(`step OK id=${step.id} type=${step.type} elapsed=${String(Date.now() - t0)}ms`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Action step: merge configOverrides → resolve variables → validate → execute
|
|
562
|
+
const baseConfig = callCtx.configOverrides
|
|
563
|
+
? { ...step.config, ...callCtx.configOverrides }
|
|
564
|
+
: step.config;
|
|
565
|
+
const resolvedConfig = stripNullForOptionalKeys(this.resolver.resolve(baseConfig, context), stepImpl.configSchema);
|
|
566
|
+
const parsed = stepImpl.configSchema.safeParse(resolvedConfig);
|
|
567
|
+
if (!parsed.success) {
|
|
568
|
+
throw new Error(`Step "${step.id}" (${step.type}) config validation failed after variable resolution:\n${formatZodErrors(parsed.error)}`);
|
|
569
|
+
}
|
|
570
|
+
const resolvedInput = parsed.data;
|
|
571
|
+
const persistedInput = stepImpl.redactInput
|
|
572
|
+
? stepImpl.redactInput(resolvedInput)
|
|
573
|
+
: resolvedInput;
|
|
574
|
+
context.steps[step.id] = {
|
|
575
|
+
type: step.type,
|
|
576
|
+
input: persistedInput,
|
|
577
|
+
output: {},
|
|
578
|
+
};
|
|
579
|
+
const actionImpl = stepImpl;
|
|
580
|
+
const stepCtx = await this.buildStepContext(context, { ...callCtx, stepId: step.id }, stepImpl);
|
|
581
|
+
this.log.info(`step ${callCtx.isResuming ? 'RESUME' : 'START'} id=${step.id} type=${step.type}`);
|
|
582
|
+
const t0 = Date.now();
|
|
583
|
+
try {
|
|
584
|
+
let output;
|
|
585
|
+
if (callCtx.isResuming) {
|
|
586
|
+
output = await this.invokeResume(actionImpl, callCtx.executionId, callCtx.stepName, resolvedInput, stepCtx);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
output = await actionImpl.execute(resolvedInput, stepCtx);
|
|
590
|
+
}
|
|
591
|
+
context.steps[step.id] = {
|
|
592
|
+
type: step.type,
|
|
593
|
+
input: persistedInput,
|
|
594
|
+
output,
|
|
595
|
+
};
|
|
596
|
+
this.log.info(`step OK id=${step.id} type=${step.type} elapsed=${String(Date.now() - t0)}ms`);
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
if (!PauseStep.is(err)) {
|
|
600
|
+
this.log.error(`step FAIL id=${step.id} type=${step.type} elapsed=${String(Date.now() - t0)}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
601
|
+
}
|
|
602
|
+
throw err;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Walk steps inside a control-flow branch. Supports nested pause.
|
|
607
|
+
*
|
|
608
|
+
* Each sub-step is persisted as its own row (keyed by config `id`) and
|
|
609
|
+
* emits SSE events, mirroring the top-level `walkSteps` behavior.
|
|
610
|
+
*
|
|
611
|
+
* When a step pauses inside a branch, the PauseStep error is enriched
|
|
612
|
+
* with branch path information and re-thrown so the top-level walkSteps
|
|
613
|
+
* can persist the full PausePath.
|
|
614
|
+
*/
|
|
615
|
+
async walkBranchSteps(steps, context, executionId, controlStepId, branchKey, branchResumePath) {
|
|
616
|
+
const resolvedBranchKey = branchKey ?? 'default';
|
|
617
|
+
// Determine start position for resume within this branch
|
|
618
|
+
let startIndex = 0;
|
|
619
|
+
let innerResuming = false;
|
|
620
|
+
let deeperResumePath;
|
|
621
|
+
if (branchResumePath && branchResumePath.length > 0) {
|
|
622
|
+
const segment = branchResumePath[0];
|
|
623
|
+
if (segment.branch?.stepId === controlStepId &&
|
|
624
|
+
segment.branch.branchKey === resolvedBranchKey) {
|
|
625
|
+
startIndex = segment.stepIndex;
|
|
626
|
+
innerResuming = true;
|
|
627
|
+
deeperResumePath =
|
|
628
|
+
branchResumePath.length > 1
|
|
629
|
+
? branchResumePath.slice(1)
|
|
630
|
+
: undefined;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (let j = startIndex; j < steps.length; j++) {
|
|
634
|
+
const bStep = steps[j];
|
|
635
|
+
const isResuming = innerResuming && j === startIndex;
|
|
636
|
+
const executorType = CONTROL_FLOW_STEP_TYPES.has(bStep.type)
|
|
637
|
+
? 'conditional'
|
|
638
|
+
: 'deterministic';
|
|
639
|
+
// Persist RUNNING + emit event (skip for resuming steps)
|
|
640
|
+
if (!isResuming && executionId) {
|
|
641
|
+
await this.persistence.upsertStep(executionId, bStep.id, {
|
|
642
|
+
status: 'RUNNING',
|
|
643
|
+
executorType,
|
|
644
|
+
});
|
|
645
|
+
this.emitEvent(executionId, {
|
|
646
|
+
event: 'step_started',
|
|
647
|
+
data: { stepName: bStep.id, stepType: bStep.type },
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
await this.executeStep(bStep, context, {
|
|
652
|
+
executionId,
|
|
653
|
+
stepName: bStep.id,
|
|
654
|
+
isResuming,
|
|
655
|
+
}, isResuming ? deeperResumePath : undefined);
|
|
656
|
+
// Persist COMPLETED + emit event
|
|
657
|
+
if (executionId) {
|
|
658
|
+
const ctxEntry = context.steps[bStep.id];
|
|
659
|
+
await this.persistence.upsertStep(executionId, bStep.id, {
|
|
660
|
+
status: 'COMPLETED',
|
|
661
|
+
executorType,
|
|
662
|
+
data: JSON.stringify(ctxEntry ?? { output: null }),
|
|
663
|
+
});
|
|
664
|
+
this.emitEvent(executionId, {
|
|
665
|
+
event: 'step_completed',
|
|
666
|
+
data: {
|
|
667
|
+
stepName: bStep.id,
|
|
668
|
+
output: (ctxEntry?.output ?? {}),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
if (PauseStep.is(err)) {
|
|
675
|
+
const pe = err;
|
|
676
|
+
// Persist EXTERNAL_WAIT for the sub-step
|
|
677
|
+
if (executionId) {
|
|
678
|
+
const ctxEntry = context.steps[bStep.id];
|
|
679
|
+
const base = ctxEntry ?? { type: bStep.type, output: {} };
|
|
680
|
+
const merged = pe.statePatch
|
|
681
|
+
? { ...base, ...pe.statePatch }
|
|
682
|
+
: base;
|
|
683
|
+
await this.persistence.upsertStep(executionId, bStep.id, {
|
|
684
|
+
status: 'EXTERNAL_WAIT',
|
|
685
|
+
executorType,
|
|
686
|
+
data: JSON.stringify(merged),
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
pe._pausePath = [
|
|
690
|
+
{
|
|
691
|
+
stepIndex: j,
|
|
692
|
+
branch: {
|
|
693
|
+
stepId: controlStepId,
|
|
694
|
+
branchKey: resolvedBranchKey,
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
...(pe._pausePath ?? []),
|
|
698
|
+
];
|
|
699
|
+
throw pe;
|
|
700
|
+
}
|
|
701
|
+
// Persist FAILED + emit event
|
|
702
|
+
if (executionId) {
|
|
703
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
704
|
+
await this.persistence.upsertStep(executionId, bStep.id, {
|
|
705
|
+
status: 'FAILED',
|
|
706
|
+
executorType,
|
|
707
|
+
data: JSON.stringify({ error: errMsg }),
|
|
708
|
+
});
|
|
709
|
+
this.emitEvent(executionId, {
|
|
710
|
+
event: 'step_failed',
|
|
711
|
+
data: { stepName: bStep.id, error: errMsg },
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
throw err;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async invokeResume(actionImpl, executionId, stepName, resolvedInput, ctx) {
|
|
719
|
+
const row = await this.persistence.getStep(executionId, stepName);
|
|
720
|
+
let rowData = row?.data
|
|
721
|
+
? (safeParseJson(row.data) ?? {})
|
|
722
|
+
: {};
|
|
723
|
+
// Merge user's resume payload (approve/reject/form data) into rowData
|
|
724
|
+
const resumePayload = await this.persistence.getResumePayload(executionId);
|
|
725
|
+
if (resumePayload) {
|
|
726
|
+
rowData = { ...rowData, resumePayload };
|
|
727
|
+
}
|
|
728
|
+
if (typeof actionImpl.onResume === 'function') {
|
|
729
|
+
return actionImpl.onResume(rowData, resolvedInput, ctx);
|
|
730
|
+
}
|
|
731
|
+
const fallback = rowData['output'];
|
|
732
|
+
return fallback &&
|
|
733
|
+
typeof fallback === 'object' &&
|
|
734
|
+
!Array.isArray(fallback)
|
|
735
|
+
? fallback
|
|
736
|
+
: {};
|
|
737
|
+
}
|
|
738
|
+
async buildStepContext(context, callCtx, stepImpl) {
|
|
739
|
+
// Resolve credentials if step declares credentialSlots
|
|
740
|
+
let credentials = {};
|
|
741
|
+
if (stepImpl.credentialSlots && stepImpl.credentialSlots.length > 0) {
|
|
742
|
+
for (const slot of stepImpl.credentialSlots) {
|
|
743
|
+
const resolved = await this.persistence.resolveCredential(context.workflow.metadata, slot.name);
|
|
744
|
+
if (resolved) {
|
|
745
|
+
credentials[slot.name] = resolved;
|
|
746
|
+
}
|
|
747
|
+
else if (slot.required) {
|
|
748
|
+
throw new Error(`Required credential "${slot.name}" not found`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
workflow: context,
|
|
754
|
+
runtime: {
|
|
755
|
+
executionId: callCtx.executionId,
|
|
756
|
+
stepId: callCtx.stepId,
|
|
757
|
+
stepName: callCtx.stepName,
|
|
758
|
+
isResuming: callCtx.isResuming,
|
|
759
|
+
metadata: context.workflow.metadata,
|
|
760
|
+
baseUrl: this.baseUrl,
|
|
761
|
+
},
|
|
762
|
+
...(callCtx.priorState ? { priorState: callCtx.priorState } : {}),
|
|
763
|
+
services: this.services,
|
|
764
|
+
credentials,
|
|
765
|
+
staticData: {
|
|
766
|
+
get: (key) => this.persistence.getStaticData(context.workflow.id, key),
|
|
767
|
+
set: (key, value) => this.persistence.setStaticData(context.workflow.id, key, value),
|
|
768
|
+
delete: (key) => this.persistence.deleteStaticData(context.workflow.id, key),
|
|
769
|
+
},
|
|
770
|
+
pause: createPauseFunction(),
|
|
771
|
+
emit: (event) => {
|
|
772
|
+
const normalized = {
|
|
773
|
+
type: event.type,
|
|
774
|
+
data: JSON.stringify(event.data),
|
|
775
|
+
};
|
|
776
|
+
// Fire-and-forget — never blocks step execution
|
|
777
|
+
this.persistence
|
|
778
|
+
.appendStepEvent(callCtx.executionId, callCtx.stepName, normalized)
|
|
779
|
+
.catch((err) => {
|
|
780
|
+
this.log.warn(`Failed to persist step event: ${err instanceof Error ? err.message : String(err)}`);
|
|
781
|
+
});
|
|
782
|
+
// Also forward to the SSE event bus
|
|
783
|
+
this.emitEvent(callCtx.executionId, {
|
|
784
|
+
event: 'step_progress',
|
|
785
|
+
data: { stepName: callCtx.stepId, ...event.data },
|
|
786
|
+
});
|
|
787
|
+
},
|
|
788
|
+
log: this.log,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// ─── Helpers ───
|
|
793
|
+
function safeParseJson(raw) {
|
|
794
|
+
try {
|
|
795
|
+
return JSON.parse(raw);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function parseStepIndex(name) {
|
|
802
|
+
const m = /^step_(\d+)$/.exec(name);
|
|
803
|
+
if (!m?.[1])
|
|
804
|
+
return null;
|
|
805
|
+
const n = Number.parseInt(m[1], 10);
|
|
806
|
+
return Number.isInteger(n) ? n : null;
|
|
807
|
+
}
|
|
808
|
+
function formatZodErrors(error) {
|
|
809
|
+
return error.issues
|
|
810
|
+
.map((i) => ` ${i.path.join('.')}: ${i.message}`)
|
|
811
|
+
.join('\n');
|
|
812
|
+
}
|
|
813
|
+
function delay(ms) {
|
|
814
|
+
return new Promise((resolve) => {
|
|
815
|
+
setTimeout(resolve, ms);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Convert a review-gate retry's resume payload into config overrides
|
|
820
|
+
* the entry step will see. Surfaces `feedback` + `attachments` as top-level
|
|
821
|
+
* keys (so steps can read them without unwrapping), then merges the opaque
|
|
822
|
+
* `data` blob underneath.
|
|
823
|
+
*/
|
|
824
|
+
function extractConfigOverrides(payload) {
|
|
825
|
+
if (!payload)
|
|
826
|
+
return {};
|
|
827
|
+
const overrides = {};
|
|
828
|
+
if (payload.data) {
|
|
829
|
+
Object.assign(overrides, payload.data);
|
|
830
|
+
}
|
|
831
|
+
if (payload.feedback !== undefined)
|
|
832
|
+
overrides['feedback'] = payload.feedback;
|
|
833
|
+
if (payload.attachments !== undefined)
|
|
834
|
+
overrides['attachments'] = payload.attachments;
|
|
835
|
+
return overrides;
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=workflow-executor.js.map
|