foliko 2.0.6 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/docs/public-api.md +3 -3
  3. package/examples/workflow.js +62 -57
  4. package/package.json +2 -3
  5. package/plugins/core/scheduler/CHANGELOG.md +31 -0
  6. package/plugins/core/scheduler/index.js +576 -638
  7. package/plugins/core/workflow/context.js +941 -0
  8. package/plugins/core/workflow/engine.js +66 -0
  9. package/plugins/core/workflow/examples/01-basic.js +42 -0
  10. package/plugins/core/workflow/examples/01-basic.json +30 -0
  11. package/plugins/core/workflow/examples/02-choice.js +75 -0
  12. package/plugins/core/workflow/examples/02-choice.json +59 -0
  13. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  14. package/plugins/core/workflow/examples/03-each.json +41 -0
  15. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  16. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  17. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  18. package/plugins/core/workflow/examples/05-each.js +65 -0
  19. package/plugins/core/workflow/examples/06-script.js +82 -0
  20. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  21. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  22. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  23. package/plugins/core/workflow/examples/10-logger.js +34 -0
  24. package/plugins/core/workflow/examples/11-storage.js +68 -0
  25. package/plugins/core/workflow/examples/simple.js +77 -0
  26. package/plugins/core/workflow/examples/simple.json +75 -0
  27. package/plugins/core/workflow/index.js +124 -58
  28. package/plugins/core/workflow/js-runner.js +318 -0
  29. package/plugins/core/workflow/json-runner.js +323 -0
  30. package/plugins/core/workflow/stages/action.js +211 -0
  31. package/plugins/core/workflow/stages/choice.js +74 -0
  32. package/plugins/core/workflow/stages/delay.js +73 -0
  33. package/plugins/core/workflow/stages/each.js +123 -0
  34. package/plugins/core/workflow/stages/parallel.js +69 -0
  35. package/plugins/core/workflow/stages/try.js +142 -0
  36. package/plugins/executors/data-splitter/index.js +3 -2
  37. package/plugins/memory/index.js +10 -3
  38. package/sandbox/check-context.js +5 -0
  39. package/sandbox/test-context.js +27 -0
  40. package/sandbox/test-fixes.js +40 -0
  41. package/sandbox/test-hello-js.js +46 -0
  42. package/skills/workflows/SKILL.md +715 -613
  43. package/src/agent/chat.js +8 -1
  44. package/src/agent/main.js +5 -1
  45. package/src/cli/ui/chat-ui.js +4 -3
  46. package/src/common/json-safe.js +20 -0
  47. package/src/framework/framework.js +58 -2
  48. package/src/index.js +10 -0
  49. package/src/plugin/base.js +7 -4
  50. package/src/plugin/manager.js +95 -1
  51. package/src/utils/sandbox.js +1 -1
  52. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  53. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
@@ -0,0 +1,323 @@
1
+ /**
2
+ * JSON Workflow Runner - Primary v2 format
3
+ * Executes workflows defined in JSON format
4
+ */
5
+
6
+ const { ActionStage } = require('./stages/action');
7
+ const { ChoiceStage } = require('./stages/choice');
8
+ const { EachStage } = require('./stages/each');
9
+ const { ParallelStage } = require('./stages/parallel');
10
+ const { TryStage } = require('./stages/try');
11
+ const { DelayStage } = require('./stages/delay');
12
+ const { evaluateInSandbox } = require('../../../src/utils/sandbox');
13
+
14
+ class JsonRunner {
15
+ constructor(framework) {
16
+ this.framework = framework;
17
+ this._log = require('../../../src/common/logger').logger.child('JsonRunner');
18
+ }
19
+
20
+ /**
21
+ * Execute a JSON workflow
22
+ * @param {Object} workflow - Workflow definition with flow, version, stages
23
+ * @param {Object} input - Input data
24
+ * @param {string} sessionId - Session ID for tool execution
25
+ * @returns {Promise<Object>} execution result
26
+ */
27
+ async execute(workflow, input = {}, sessionId = null) {
28
+ this._log.debug(`Executing JSON workflow: ${workflow.flow || 'unnamed'}`);
29
+
30
+ const { FlowContext } = require('./context');
31
+ const context = new FlowContext(input, sessionId);
32
+ context.setFramework(this.framework);
33
+
34
+ // Create stage executor and attach to context
35
+ const stageExecutor = new StageExecutor(this.framework);
36
+ context._stageExecutor = stageExecutor;
37
+
38
+ try {
39
+ // Execute stages sequentially
40
+ let stageIndex = 0;
41
+ const stages = workflow.stages || [];
42
+
43
+ // Track choice context for branch skipping
44
+ // When a choice selects a branch, we identify all sibling stages and skip non-selected ones
45
+ let choiceContext = null; // { choiceIndex, branches[], skipStageNames[] }
46
+
47
+ while (stageIndex < stages.length && !context._destroyed) {
48
+ const stageConfig = stages[stageIndex];
49
+ context.prev = context.current;
50
+ context.current = stageConfig.name || `stage_${stageIndex}`;
51
+
52
+ // Check if this stage should be skipped (it's a non-selected sibling of a choice)
53
+ // A "sibling" here means: any stage at the top level of `stages` whose name
54
+ // matches one of the choice's branch targets (do) or the default.
55
+ //
56
+ // Lifecycle:
57
+ // 1. Choice sets choiceContext = { targetStageName, skipBranchNames, choiceIndex,
58
+ // targetExecuted=false } and jumps to the chosen branch.
59
+ // 2. While inside the branch's stage range: skip any stage whose name is in
60
+ // skipBranchNames (siblings), then mark targetExecuted when the chosen
61
+ // branch's own stage runs.
62
+ // 3. Once we've consumed a full stage past the target (i.e. we're leaving
63
+ // the branch's "slot"), clear context so later choices/stages behave
64
+ // normally.
65
+ if (choiceContext) {
66
+ const stageName = stageConfig.name;
67
+ if (choiceContext.skipBranchNames.includes(stageName)) {
68
+ this._log.debug(`Skipping branch "${stageName}" (not selected by choice at index ${choiceContext.choiceIndex})`);
69
+ stageIndex++;
70
+ continue;
71
+ }
72
+ if (choiceContext.targetExecuted) {
73
+ // The chosen branch has already run; any further sibling stage means
74
+ // we're moving past the choice group - clear the context.
75
+ choiceContext = null;
76
+ } else {
77
+ // This is the chosen branch's stage (or some other non-sibling stage
78
+ // before the target — treat as target).
79
+ if (stageName === choiceContext.targetStageName) {
80
+ choiceContext.targetExecuted = true;
81
+ this._log.debug(`Choice branch "${stageName}" is now executing`);
82
+ } else {
83
+ // Defensive: if something doesn't match the expected target, clear
84
+ // the context rather than risk skipping real work.
85
+ choiceContext = null;
86
+ }
87
+ }
88
+ }
89
+
90
+ this._log.debug(`Executing stage: ${context.current}`);
91
+
92
+ // Execute the stage
93
+ const result = await stageExecutor.executeStage(stageConfig, context);
94
+
95
+ // Check if stage set a pending next (for choice/each stages)
96
+ if (context._pendingNext) {
97
+ const nextStageName = context._pendingNext;
98
+ context._pendingNext = null;
99
+
100
+ // Find the next stage index by name
101
+ const nextIndex = stages.findIndex(
102
+ (s) => (s.name || `stage_${stages.indexOf(s)}`) === nextStageName
103
+ );
104
+ if (nextIndex >= 0) {
105
+ // Check if this jump is from a choice to one of its branches
106
+ const currentStage = stages[stageIndex];
107
+ if (currentStage && (currentStage.choice || currentStage.when)) {
108
+ // Collect ALL branch names (do targets) and default - these are all siblings
109
+ const allBranchNames = [];
110
+ if (currentStage.default) allBranchNames.push(currentStage.default);
111
+ for (const b of currentStage.choice || []) {
112
+ if (b.do) allBranchNames.push(b.do);
113
+ }
114
+ // Skip all branches except the selected one
115
+ const skipBranchNames = allBranchNames.filter(name => name !== nextStageName);
116
+ if (skipBranchNames.length > 0) {
117
+ choiceContext = {
118
+ choiceIndex: stageIndex,
119
+ targetStageName: nextStageName,
120
+ skipBranchNames,
121
+ targetExecuted: false,
122
+ };
123
+ this._log.debug(`Choice selected "${nextStageName}", skipping branches: ${skipBranchNames.join(', ')}`);
124
+ }
125
+ }
126
+
127
+ stageIndex = nextIndex;
128
+ continue;
129
+ } else {
130
+ this._log.warn(`Next stage "${nextStageName}" not found`);
131
+ }
132
+ }
133
+
134
+ stageIndex++;
135
+ }
136
+
137
+ // Build output from all stage results
138
+ const output = {};
139
+ for (const [key, value] of context._results) {
140
+ output[key] = value;
141
+ }
142
+
143
+ return {
144
+ success: true,
145
+ flow: workflow.flow,
146
+ stageCount: stages.length,
147
+ output,
148
+ result: context._sentData || output,
149
+ };
150
+ } catch (err) {
151
+ this._log.error(`Workflow execution error: ${err.message}`);
152
+ return {
153
+ success: false,
154
+ error: err.message,
155
+ flow: workflow.flow,
156
+ };
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Stage Executor - executes individual stages based on type
163
+ */
164
+ class StageExecutor {
165
+ constructor(framework) {
166
+ this.framework = framework;
167
+ this._log = require('../../../src/common/logger').logger.child('StageExecutor');
168
+ }
169
+
170
+ /**
171
+ * Execute a single stage
172
+ * @param {Object} config - Stage configuration
173
+ * @param {FlowContext} context - Flow context
174
+ * @returns {Promise<any>} stage result
175
+ */
176
+ async executeStage(config, context) {
177
+ const type = this._getStageType(config);
178
+
179
+ switch (type) {
180
+ case 'action':
181
+ return await this._executeAction(config, context);
182
+ case 'choice':
183
+ return await this._executeChoice(config, context);
184
+ case 'each':
185
+ return await this._executeEach(config, context);
186
+ case 'parallel':
187
+ return await this._executeParallel(config, context);
188
+ case 'try':
189
+ return await this._executeTry(config, context);
190
+ case 'delay':
191
+ return await this._executeDelay(config, context);
192
+ case 'stage':
193
+ return await this._executeNested(config, context);
194
+ default:
195
+ // Default to action (tool or script)
196
+ return await this._executeAction(config, context);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Determine stage type from configuration
202
+ */
203
+ _getStageType(config) {
204
+ if (config.try && typeof config.try === 'object') return 'try';
205
+ if (config.delay !== undefined || config.wait !== undefined) return 'delay';
206
+ if (config.choice || config.when || config.default) return 'choice';
207
+ if (config.each || config.over) return 'each';
208
+ if (config.parallel) return 'parallel';
209
+ if (config.stage) return 'stage';
210
+ if (config.tool || config.action || config.script) return 'action';
211
+ return 'action'; // Default
212
+ }
213
+
214
+ /**
215
+ * Execute action stage
216
+ */
217
+ async _executeAction(config, context) {
218
+ const stage = new ActionStage(config);
219
+ return await stage.execute(context);
220
+ }
221
+
222
+ /**
223
+ * Execute choice stage - may set context._pendingNext
224
+ */
225
+ async _executeChoice(config, context) {
226
+ const stage = new ChoiceStage(config);
227
+ const nextStage = await stage.execute(context);
228
+ if (nextStage) {
229
+ context._pendingNext = nextStage;
230
+ }
231
+ return { nextStage };
232
+ }
233
+
234
+ /**
235
+ * Execute each stage
236
+ */
237
+ async _executeEach(config, context) {
238
+ const stage = new EachStage(config);
239
+ return await stage.execute(context);
240
+ }
241
+
242
+ /**
243
+ * Execute parallel stage
244
+ */
245
+ async _executeParallel(config, context) {
246
+ const stage = new ParallelStage(config);
247
+ return await stage.execute(context);
248
+ }
249
+
250
+ /**
251
+ * Execute try-catch stage.
252
+ * Catches errors thrown by any child stage (action / script / tool) so a single
253
+ * failing stage doesn't tear down the whole workflow. Resolves with a
254
+ * `{ success, result | error, caught? }` object.
255
+ */
256
+ async _executeTry(config, context) {
257
+ const stage = new TryStage(config);
258
+ const result = await stage.execute(context);
259
+ if (config.name) {
260
+ context.setResult(config.name, result);
261
+ }
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * Execute delay/wait stage. Pure synchronous pause; the result is the
267
+ * resolved `data` payload (often `null`).
268
+ */
269
+ async _executeDelay(config, context) {
270
+ const stage = new DelayStage(config);
271
+ const result = await stage.execute(context);
272
+ if (config.name) {
273
+ context.setResult(config.name, result);
274
+ }
275
+ return result;
276
+ }
277
+
278
+ /**
279
+ * Execute nested stage (sub-workflow)
280
+ */
281
+ async _executeNested(config, context) {
282
+ if (!config.stage) {
283
+ throw new Error(`Nested stage "${config.name}" requires a stage definition`);
284
+ }
285
+
286
+ const { runScriptSafely } = require('../../../src/utils/sandbox');
287
+
288
+ // If stage is a script
289
+ if (typeof config.stage === 'string') {
290
+ // It's a reference to another stage or inline script
291
+ // For now, treat as inline script
292
+ const scriptContext = {
293
+ input: context.input,
294
+ data: context.data,
295
+ variables: Object.fromEntries(context._results),
296
+ previousResult: context._sentData,
297
+ console: { log: (...args) => this._log.debug('[Script]', ...args) },
298
+ };
299
+ return await runScriptSafely(config.stage, scriptContext, { timeout: 10000 });
300
+ }
301
+
302
+ // If stage is an object with stages array, execute sub-workflow
303
+ if (Array.isArray(config.stage.stages)) {
304
+ const subContext = new (require('./context').FlowContext)(context.input, context._sessionId);
305
+ subContext.setFramework(this.framework);
306
+ subContext._stageExecutor = this;
307
+
308
+ // Copy relevant data
309
+ subContext.data = { ...context.data };
310
+
311
+ for (const subStage of config.stage.stages) {
312
+ if (subContext._destroyed) break;
313
+ await this.executeStage(subStage, subContext);
314
+ }
315
+
316
+ return Object.fromEntries(subContext._results);
317
+ }
318
+
319
+ throw new Error(`Invalid nested stage definition for "${config.name}"`);
320
+ }
321
+ }
322
+
323
+ module.exports = { JsonRunner, StageExecutor };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * action stage executor
3
+ * Executes a tool, script, or chain-style call (extension, skill, workflow, agent, prompt)
4
+ */
5
+
6
+ const { runScriptSafely } = require('../../../../src/utils/sandbox');
7
+
8
+ class ActionStage {
9
+ constructor(config) {
10
+ this.name = config.name;
11
+ this.tool = config.tool;
12
+ this.args = config.args || {};
13
+ this.script = config.script;
14
+ this.action = config.action; // alias for tool
15
+
16
+ // Chain-style call types for JSON format
17
+ this.extension = config.extension; // extension name
18
+ this.skill = config.skill; // skill name
19
+ this.workflow = config.workflow; // workflow name
20
+ this.agent = config.agent; // agent action
21
+ this.prompt = config.prompt; // prompt action
22
+ this.command = config.command; // command for skill
23
+ }
24
+
25
+ /**
26
+ * Execute the action stage
27
+ * @param {FlowContext} context
28
+ * @returns {Promise<any>} result
29
+ */
30
+ async execute(context) {
31
+ const log = context._log;
32
+
33
+ // Handle chain-style calls in JSON format
34
+ if (this.extension) {
35
+ return await this._executeExtension(context, log);
36
+ }
37
+ if (this.skill) {
38
+ return await this._executeSkill(context, log);
39
+ }
40
+ if (this.workflow) {
41
+ return await this._executeWorkflow(context, log);
42
+ }
43
+ if (this.agent) {
44
+ return await this._executeAgent(context, log);
45
+ }
46
+ if (this.prompt) {
47
+ return await this._executePrompt(context, log);
48
+ }
49
+
50
+ // Handle script execution first - "action": "script" with inline script code
51
+ if (this.script) {
52
+ log.debug(`Executing script in stage: ${this.name}`);
53
+
54
+ // 解析 script 模板字符串(支持 {{stageName}} 和 {{input.xxx}} 等)
55
+ const resolvedScript = context.resolveValue(this.script, { raw: true });
56
+ if (resolvedScript !== this.script) {
57
+ log.debug(`Script template resolved: ${this.script} -> ${resolvedScript}`);
58
+ }
59
+
60
+ // 暴露 $ 链式 API(与 JS 工作流格式保持一致)
61
+ const scriptContext = {
62
+ $: context,
63
+ input: context.input,
64
+ data: context.data,
65
+ variables: Object.fromEntries(context._results),
66
+ previousResult: context._sentData,
67
+ console: {
68
+ log: (...args) => {
69
+ log.debug('[Script]', ...args);
70
+ },
71
+ },
72
+ };
73
+
74
+ const result = await runScriptSafely(resolvedScript, scriptContext, { timeout: 10000 });
75
+
76
+ // Store result
77
+ if (this.name) {
78
+ context.setResult(this.name, result);
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ // Tool execution: if tool or action (non-"script") is specified
85
+ if (this.tool || (this.action && this.action !== 'script')) {
86
+ const toolName = this.tool || this.action;
87
+ log.debug(`Executing tool: ${toolName}`);
88
+
89
+ // Resolve args with variable interpolation
90
+ const resolvedArgs = context.resolveArgs(this.args);
91
+
92
+ // Execute tool
93
+ let result;
94
+ let sessionId = context._sessionId;
95
+ if (!sessionId && context._framework?.getExecutionContext) {
96
+ const ctx = context._framework.getExecutionContext();
97
+ sessionId = ctx?.sessionId;
98
+ }
99
+
100
+ if (sessionId && context._framework?.runWithContext) {
101
+ result = await context._framework.runWithContext(
102
+ { sessionId },
103
+ async () => await context._framework.executeTool(toolName, resolvedArgs)
104
+ );
105
+ } else {
106
+ result = await context._framework.executeTool(toolName, resolvedArgs);
107
+ }
108
+
109
+ // Store result
110
+ if (this.name) {
111
+ context.setResult(this.name, result);
112
+ }
113
+
114
+ return result;
115
+ }
116
+
117
+ throw new Error(`Action stage "${this.name}" has neither tool nor script`);
118
+ }
119
+
120
+ /**
121
+ * Execute extension call (JSON format: { extension: "name", tool: "toolName", args: {...} })
122
+ */
123
+ async _executeExtension(context, log) {
124
+ const toolName = this.tool || this.command;
125
+ if (!toolName) {
126
+ throw new Error(`Extension stage "${this.name}" requires a tool name`);
127
+ }
128
+
129
+ log.debug(`Executing extension: ${this.extension}/${toolName}`);
130
+ const result = await context.extension.execute(this.extension, toolName, this.args);
131
+
132
+ if (this.name) {
133
+ context.setResult(this.name, result);
134
+ }
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Execute skill call (JSON format: { skill: "name", command: "cmd", args: {...} })
140
+ */
141
+ async _executeSkill(context, log) {
142
+ const command = this.command || this.tool;
143
+ if (!command) {
144
+ throw new Error(`Skill stage "${this.name}" requires a command`);
145
+ }
146
+
147
+ log.debug(`Executing skill: ${this.skill}/${command}`);
148
+ const result = await context.skill.execute(this.skill, command, this.args);
149
+
150
+ if (this.name) {
151
+ context.setResult(this.name, result);
152
+ }
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Execute workflow call (JSON format: { workflow: "name", args: { input: {...} } })
158
+ */
159
+ async _executeWorkflow(context, log) {
160
+ log.debug(`Executing workflow: ${this.workflow}`);
161
+ const result = await context.workflow.execute(this.workflow, this.args.input || this.args);
162
+
163
+ if (this.name) {
164
+ context.setResult(this.name, result);
165
+ }
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * Execute agent call (JSON format: { agent: "action", args: {...} })
171
+ */
172
+ async _executeAgent(context, log) {
173
+ log.debug(`Executing agent: ${this.agent}`);
174
+ let result;
175
+
176
+ if (this.agent === 'create') {
177
+ const agent = await context.agent.create(this.args);
178
+ result = { success: true, agentId: agent.id };
179
+ } else if (this.agent === 'chat') {
180
+ result = await context.agent.chat(this.args);
181
+ } else {
182
+ throw new Error(`Unknown agent action: ${this.agent}`);
183
+ }
184
+
185
+ if (this.name) {
186
+ context.setResult(this.name, result);
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * Execute prompt call (JSON format: { prompt: "action", args: {...} })
193
+ */
194
+ async _executePrompt(context, log) {
195
+ log.debug(`Executing prompt: ${this.prompt}`);
196
+ let result;
197
+
198
+ if (this.prompt === 'send' || this.prompt === 'chat') {
199
+ result = await context.prompt.send(this.args);
200
+ } else {
201
+ throw new Error(`Unknown prompt action: ${this.prompt}`);
202
+ }
203
+
204
+ if (this.name) {
205
+ context.setResult(this.name, result);
206
+ }
207
+ return result;
208
+ }
209
+ }
210
+
211
+ module.exports = { ActionStage };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * choice stage executor
3
+ * Conditional branching (if/else/switch)
4
+ */
5
+
6
+ const { evaluateInSandbox } = require('../../../../src/utils/sandbox');
7
+
8
+ class ChoiceStage {
9
+ constructor(config) {
10
+ this.name = config.name;
11
+ this.choice = config.choice || []; // Array of { when, do } or { case, do }
12
+ this.default = config.default; // Default stage name
13
+ this._log = require('../../../../src/common/logger').logger.child('ChoiceStage');
14
+ }
15
+
16
+ /**
17
+ * Execute the choice stage
18
+ * @param {FlowContext} context
19
+ * @returns {Promise<string|null>} next stage name
20
+ */
21
+ async execute(context) {
22
+ this._log.debug(`Evaluating choice: ${this.name}`);
23
+
24
+ // Try each when/case condition
25
+ for (const branch of this.choice) {
26
+ const condition = branch.when || branch.case;
27
+ if (!condition) continue;
28
+
29
+ try {
30
+ // Resolve condition with variable interpolation
31
+ const resolvedCondition = context.resolveValue(condition);
32
+
33
+ // Evaluate condition
34
+ let matched = false;
35
+ if (typeof resolvedCondition === 'boolean') {
36
+ matched = resolvedCondition;
37
+ } else if (typeof resolvedCondition === 'string') {
38
+ // Use sandbox to evaluate the condition expression
39
+ // Extract stage results to top level so they can be accessed directly
40
+ // e.g., "init.value > 100" can access init from stage results
41
+ // Also spread context.data so each-loop variables (e.g. "fruit") work
42
+ // in choice conditions.
43
+ const stageResults = Object.fromEntries(context._results);
44
+ const evalContext = {
45
+ ...stageResults, // Spread stage results to top level
46
+ ...context.data, // Spread data so loop variables are reachable
47
+ input: context.input,
48
+ data: context.data,
49
+ previousResult: context._sentData,
50
+ };
51
+ matched = evaluateInSandbox(resolvedCondition, evalContext, { timeout: 5000 });
52
+ }
53
+
54
+ if (matched) {
55
+ this._log.debug(`Choice matched: ${condition} -> ${branch.do}`);
56
+ return branch.do; // Return next stage name
57
+ }
58
+ } catch (err) {
59
+ this._log.warn(`Condition evaluation error for "${condition}": ${err.message}`);
60
+ }
61
+ }
62
+
63
+ // No condition matched, use default
64
+ if (this.default) {
65
+ this._log.debug(`No choice matched, using default: ${this.default}`);
66
+ return this.default;
67
+ }
68
+
69
+ this._log.debug(`No choice matched and no default for: ${this.name}`);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ module.exports = { ChoiceStage };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * delay stage executor
3
+ *
4
+ * Pauses the workflow for a given number of milliseconds.
5
+ *
6
+ * Configuration:
7
+ * {
8
+ * "name": "wait-step", // optional
9
+ * "ms": 1000, // milliseconds to wait (alias: delay, wait)
10
+ * "result": { ... } // optional value to record as the stage's output
11
+ * }
12
+ *
13
+ * Useful for rate-limiting, pacing retries, or signaling that an
14
+ * external dependency is allowed time to settle.
15
+ */
16
+
17
+ class DelayStage {
18
+ constructor(config) {
19
+ this.name = config.name;
20
+ // Accept several common field names so declarative authoring stays ergonomic.
21
+ this.msRaw = config.ms ?? config.delay ?? config.wait ?? 0;
22
+ this.result = config.result; // optional value to surface as the stage output
23
+ this._log = require('../../../../src/common/logger').logger.child('DelayStage');
24
+ }
25
+
26
+ /**
27
+ * Get the resolved delay in ms.
28
+ * Supports string templates (e.g. "{{poll-interval}}") so the wait time
29
+ * can be derived from prior stage output.
30
+ */
31
+ _resolveMs(context) {
32
+ const resolveOptions = { raw: true };
33
+ if (typeof this.msRaw === 'string') {
34
+ const resolved = context.resolveValue(this.msRaw, resolveOptions);
35
+ const num = Number(resolved);
36
+ if (Number.isFinite(num) && num >= 0) {
37
+ return num;
38
+ }
39
+ return 0;
40
+ }
41
+ const num = Number(this.msRaw);
42
+ return Number.isFinite(num) && num >= 0 ? num : 0;
43
+ }
44
+
45
+ async execute(context) {
46
+ const ms = this._resolveMs(context);
47
+ this._log.debug(`Delay stage "${this.name || '(unnamed)'}" waiting ${ms}ms`);
48
+
49
+ // Hard cap: avoid accidental multi-hour sleeps via bad input.
50
+ const MAX_DELAY_MS = 10 * 60 * 1000; // 10 minutes
51
+ const safeMs = Math.min(ms, MAX_DELAY_MS);
52
+
53
+ await new Promise((resolve) => setTimeout(resolve, safeMs));
54
+
55
+ // Optional result payload (used when the stage is named and downstream
56
+ // stages need to reference the resolution time, e.g. "{{wait.done}}").
57
+ const payload = typeof this.result === 'string'
58
+ ? context.resolveValue(this.result, { raw: true })
59
+ : this.result;
60
+
61
+ const output = payload !== undefined
62
+ ? payload
63
+ : { waited: safeMs, completedAt: new Date().toISOString() };
64
+
65
+ if (this.name) {
66
+ context.setResult(this.name, output);
67
+ }
68
+
69
+ return output;
70
+ }
71
+ }
72
+
73
+ module.exports = { DelayStage };