foliko 2.0.5 → 2.0.7

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 (93) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +280 -24
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +44 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/scheduler/index.js +1 -0
  15. package/plugins/core/skill-manager/PROMPT.md +6 -0
  16. package/plugins/core/skill-manager/index.js +402 -115
  17. package/plugins/core/sub-agent/PROMPT.md +10 -0
  18. package/plugins/core/sub-agent/index.js +36 -3
  19. package/plugins/core/think/index.js +1 -0
  20. package/plugins/core/workflow/context.js +941 -0
  21. package/plugins/core/workflow/engine.js +66 -0
  22. package/plugins/core/workflow/examples/01-basic.js +42 -0
  23. package/plugins/core/workflow/examples/01-basic.json +30 -0
  24. package/plugins/core/workflow/examples/02-choice.js +75 -0
  25. package/plugins/core/workflow/examples/02-choice.json +59 -0
  26. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  27. package/plugins/core/workflow/examples/03-each.json +41 -0
  28. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  29. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  30. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  31. package/plugins/core/workflow/examples/05-each.js +65 -0
  32. package/plugins/core/workflow/examples/06-script.js +82 -0
  33. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  34. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  35. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  36. package/plugins/core/workflow/examples/10-logger.js +34 -0
  37. package/plugins/core/workflow/examples/11-storage.js +68 -0
  38. package/plugins/core/workflow/examples/simple.js +77 -0
  39. package/plugins/core/workflow/examples/simple.json +75 -0
  40. package/plugins/core/workflow/index.js +204 -78
  41. package/plugins/core/workflow/js-runner.js +318 -0
  42. package/plugins/core/workflow/json-runner.js +323 -0
  43. package/plugins/core/workflow/stages/action.js +211 -0
  44. package/plugins/core/workflow/stages/choice.js +74 -0
  45. package/plugins/core/workflow/stages/delay.js +73 -0
  46. package/plugins/core/workflow/stages/each.js +123 -0
  47. package/plugins/core/workflow/stages/parallel.js +69 -0
  48. package/plugins/core/workflow/stages/try.js +142 -0
  49. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  50. package/plugins/executors/data-splitter/index.js +8 -6
  51. package/plugins/executors/extension/extension-registry.js +145 -0
  52. package/plugins/executors/extension/index.js +405 -437
  53. package/plugins/executors/extension/prompt-builder.js +359 -0
  54. package/plugins/executors/extension/skill-helper.js +143 -0
  55. package/plugins/messaging/feishu/index.js +5 -3
  56. package/plugins/messaging/qq/index.js +6 -4
  57. package/plugins/messaging/telegram/index.js +6 -3
  58. package/plugins/messaging/weixin/index.js +5 -3
  59. package/plugins/tools/PROMPT.md +26 -0
  60. package/plugins/tools/index.js +6 -5
  61. package/sandbox/check-context.js +5 -0
  62. package/sandbox/test-context.js +27 -0
  63. package/sandbox/test-fixes.js +40 -0
  64. package/sandbox/test-hello-js.js +46 -0
  65. package/skills/foliko/AGENTS.md +196 -43
  66. package/skills/foliko/SKILL.md +157 -28
  67. package/skills/mcp/SKILL.md +77 -118
  68. package/skills/plugins/SKILL.md +89 -3
  69. package/skills/python/SKILL.md +57 -39
  70. package/skills/skill-guide/SKILL.md +42 -34
  71. package/skills/workflows/SKILL.md +753 -436
  72. package/src/agent/chat.js +56 -28
  73. package/src/agent/main.js +39 -14
  74. package/src/agent/prompt-registry.js +56 -16
  75. package/src/agent/prompts/PROMPT.md +3 -0
  76. package/src/agent/sub.js +1 -1
  77. package/src/cli/ui/chat-ui-old.js +5 -2
  78. package/src/cli/ui/chat-ui.js +9 -5
  79. package/src/common/constants.js +12 -0
  80. package/src/common/error-capture.js +91 -0
  81. package/src/common/json-safe.js +20 -0
  82. package/src/common/logger.js +2 -2
  83. package/src/context/compressor.js +6 -2
  84. package/src/executors/mcp-executor.js +105 -125
  85. package/src/framework/framework.js +78 -12
  86. package/src/index.js +4 -0
  87. package/src/plugin/base.js +908 -5
  88. package/src/plugin/manager.js +124 -9
  89. package/src/tool/schema.js +32 -9
  90. package/src/utils/sandbox.js +1 -1
  91. package/website/index.html +821 -0
  92. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  93. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
@@ -0,0 +1,318 @@
1
+ /**
2
+ * JS Workflow Runner - Total.js Style (Secondary)
3
+ * Executes workflows defined in JavaScript format using $.push(), $.next(), $.send() API
4
+ */
5
+
6
+ const { runScriptSafely } = require('../../../src/utils/sandbox');
7
+
8
+ class JsRunner {
9
+ constructor(framework) {
10
+ this.framework = framework;
11
+ this._log = require('../../../src/common/logger').logger.child('JsRunner');
12
+ }
13
+
14
+ /**
15
+ * Execute a JS workflow
16
+ * @param {string|Function} workflowCode - Workflow code or exported function
17
+ * @param {Object} input - Input data
18
+ * @param {string} sessionId - Session ID for tool execution
19
+ * @returns {Promise<Object>} execution result
20
+ */
21
+ async execute(workflowCode, input = {}, sessionId = null) {
22
+ this._log.debug('Executing JS workflow');
23
+
24
+ const { FlowContext } = require('./context');
25
+ const context = new FlowContext(input, sessionId);
26
+ context.setFramework(this.framework);
27
+
28
+ // Create stage executor and attach to context
29
+ const stageExecutor = new JsStageExecutor(this.framework);
30
+ context._stageExecutor = stageExecutor;
31
+
32
+ try {
33
+ // Get the workflow function
34
+ let workflowFn;
35
+ if (typeof workflowCode === 'function') {
36
+ workflowFn = workflowCode;
37
+ } else if (typeof workflowCode === 'string') {
38
+ workflowFn = this._parseWorkflow(workflowCode);
39
+ } else {
40
+ throw new Error('Workflow must be a function or string');
41
+ }
42
+
43
+ // Create the $ object for Total.js style API
44
+ const $ = this._createFlowApi(context, stageExecutor);
45
+
46
+ // Execute the workflow function
47
+ const flowPromise = workflowFn($);
48
+
49
+ // If it returns a promise, wait for it
50
+ if (flowPromise && typeof flowPromise.then === 'function') {
51
+ await flowPromise;
52
+ }
53
+
54
+ // Execute stages until destroyed or no more pending next
55
+ await this._runStages(context, stageExecutor);
56
+
57
+ // Build output
58
+ const output = {};
59
+ for (const [key, value] of context._results) {
60
+ output[key] = value;
61
+ }
62
+
63
+ return {
64
+ success: !context._destroyed,
65
+ output,
66
+ result: context._sentData || output,
67
+ };
68
+ } catch (err) {
69
+ this._log.error(`JS Workflow execution error: ${err.message}`);
70
+ return {
71
+ success: false,
72
+ error: err.message,
73
+ };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Parse workflow code string into a function
79
+ * Supports:
80
+ * FLOW(function($) { ... }) - Direct function
81
+ * module.exports = function($) { ... } - CommonJS export
82
+ * module.exports.default = function($) { ... } - Default export
83
+ */
84
+ _parseWorkflow(code) {
85
+ const sandboxContext = {
86
+ console: { log() {}, error() {}, warn() {}, info() {} },
87
+ Math,
88
+ JSON,
89
+ Promise,
90
+ Array,
91
+ Object,
92
+ Date,
93
+ RegExp,
94
+ Error,
95
+ Symbol,
96
+ Map,
97
+ Set,
98
+ module: { exports: {} },
99
+ exports: {},
100
+ FlowContext: require('./context').FlowContext,
101
+ };
102
+
103
+ try {
104
+ const ctx = require('vm').createContext(sandboxContext);
105
+
106
+ // Check which format the code uses
107
+ const isModuleExport = code.includes('module.exports');
108
+ let fn;
109
+
110
+ if (isModuleExport) {
111
+ // CommonJS export format: module.exports = function($) { ... }
112
+ const wrapped = `
113
+ (function(module, exports, FlowContext) {
114
+ ${code}
115
+ return module.exports;
116
+ })
117
+ `;
118
+ const factory = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
119
+ fn = factory(ctx.module, ctx.exports, require('./context').FlowContext);
120
+ } else if (code.includes('FLOW(')) {
121
+ // FLOW wrapper format: FLOW(function($) { ... })
122
+ const wrapped = `(function(FlowContext) { return (${code}) })`;
123
+ fn = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
124
+ fn = fn(require('./context').FlowContext);
125
+ } else {
126
+ // Assume it's a function directly
127
+ const wrapped = `(function(FlowContext) { return (${code}) })`;
128
+ fn = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
129
+ fn = fn(require('./context').FlowContext);
130
+ }
131
+
132
+ // Handle module.exports.default
133
+ if (fn && fn.default) {
134
+ fn = fn.default;
135
+ }
136
+
137
+ return fn;
138
+ } catch (err) {
139
+ throw new Error(`Failed to parse workflow: ${err.message}`);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Create the $ API object for Total.js style
145
+ */
146
+ _createFlowApi(context, stageExecutor) {
147
+ const $ = {
148
+ // Register a stage
149
+ push: (name, fn) => {
150
+ context.push(name, fn);
151
+ return $;
152
+ },
153
+
154
+ // Go to next stage (sync) - 支持直接传参
155
+ next: (name, ...args) => {
156
+ context.next(name, ...args);
157
+ return $;
158
+ },
159
+
160
+ // Go to next stage (async helper)
161
+ next2: (name, ...args) => {
162
+ return () => {
163
+ context.next(name, ...args);
164
+ };
165
+ },
166
+
167
+ // Send data to next stage
168
+ send: (data) => {
169
+ context.send(data);
170
+ return $;
171
+ },
172
+
173
+ // Input from previous stage (返回 sentData �?workflow input)
174
+ get input() {
175
+ return context._sentData !== undefined ? context._sentData : context.input;
176
+ },
177
+
178
+ // Flow-level persistent data
179
+ data: context.data,
180
+
181
+ // Built-in delay
182
+ wait: (ms) => context.wait(ms),
183
+
184
+ // $.tool - Tool accessor with list(), execute(), register(), get()
185
+ get tool() { return context.tool; },
186
+
187
+ // $.extension - Extension accessor with list(), register(), remove(), execute(), get()
188
+ get extension() { return context.extension; },
189
+
190
+ // $.workflow - Workflow accessor with list(), execute(), get()
191
+ get workflow() { return context.workflow; },
192
+
193
+ // $.agent - Agent accessor with create(), chat(), list()
194
+ get agent() { return context.agent; },
195
+
196
+ // $.prompt - Prompt accessor with send(), chat()
197
+ get prompt() { return context.prompt; },
198
+
199
+ // $.skill - Skill accessor with list(), execute(), get()
200
+ get skill() { return context.skill; },
201
+
202
+ // Execute script
203
+ script: (code) => context.script(code),
204
+
205
+ // End workflow
206
+ destroy: () => context.destroy(),
207
+
208
+ // Current stage info
209
+ get current() { return context.current; },
210
+ get prev() { return context.prev; },
211
+
212
+ // Stage registry
213
+ _stages: context._stages,
214
+ };
215
+
216
+ return $;
217
+ }
218
+
219
+ /**
220
+ * Run stages sequentially based on $.next() calls
221
+ */
222
+ async _runStages(context, stageExecutor) {
223
+ // Create $ API object for this run
224
+ const $ = this._createFlowApi(context, stageExecutor);
225
+
226
+ while (!context._destroyed && context._pendingNext) {
227
+ const nextName = context._pendingNext;
228
+
229
+ // Get args that were passed via $.next(name, ...args)
230
+ const args = context._sentData;
231
+ context._sentData = null;
232
+ context._pendingNext = null;
233
+
234
+ const fn = context._stages?.get(nextName);
235
+ if (!fn) {
236
+ this._log.warn(`Stage "${nextName}" not found`);
237
+ break;
238
+ }
239
+
240
+ context.prev = context.current;
241
+ context.current = nextName;
242
+
243
+ this._log.debug(`Executing JS stage: ${nextName}`);
244
+
245
+ try {
246
+ // Call fn with $ as first arg, then unpack args
247
+ // Supports: fn($) or fn($, a, b, c)
248
+ const fnArgs = args !== undefined
249
+ ? (Array.isArray(args) ? [$, ...args] : [$, args])
250
+ : [$];
251
+
252
+ const result = fn(...fnArgs);
253
+ if (result && typeof result.then === 'function') {
254
+ await result;
255
+ }
256
+ } catch (err) {
257
+ this._log.error(`Stage "${nextName}" error: ${err.message}`);
258
+ context.destroy();
259
+ throw err;
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * JS Stage Executor - handles stage execution in JS workflows
267
+ */
268
+ class JsStageExecutor {
269
+ constructor(framework) {
270
+ this.framework = framework;
271
+ this._log = require('../../../src/common/logger').logger.child('JsStageExecutor');
272
+ }
273
+
274
+ /**
275
+ * Execute a stage from JSON config within a JS workflow
276
+ */
277
+ async executeStage(config, context) {
278
+ const { ActionStage } = require('./stages/action');
279
+ const { ChoiceStage } = require('./stages/choice');
280
+ const { EachStage } = require('./stages/each');
281
+ const { ParallelStage } = require('./stages/parallel');
282
+ const type = this._getStageType(config);
283
+
284
+ switch (type) {
285
+ case 'action': {
286
+ const stage = new ActionStage(config);
287
+ return await stage.execute(context);
288
+ }
289
+ case 'choice': {
290
+ const stage = new ChoiceStage(config);
291
+ const nextStage = await stage.execute(context);
292
+ if (nextStage) {
293
+ context._pendingNext = nextStage;
294
+ }
295
+ return { nextStage };
296
+ }
297
+ case 'each': {
298
+ const stage = new EachStage(config);
299
+ return await stage.execute(context);
300
+ }
301
+ case 'parallel': {
302
+ const stage = new ParallelStage(config);
303
+ return await stage.execute(context);
304
+ }
305
+ default:
306
+ throw new Error(`Unknown stage type: ${type}`);
307
+ }
308
+ }
309
+
310
+ _getStageType(config) {
311
+ if (config.choice || config.when || config.default) return 'choice';
312
+ if (config.each || config.over) return 'each';
313
+ if (config.parallel) return 'parallel';
314
+ return 'action';
315
+ }
316
+ }
317
+
318
+ module.exports = { JsRunner };
@@ -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 };