foliko 2.0.22 → 2.0.23
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/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +2 -5
- package/plugins/core/workflow/context.js +941 -941
- package/plugins/core/workflow/engine.js +66 -66
- package/plugins/core/workflow/js-runner.js +318 -318
- package/plugins/core/workflow/json-runner.js +323 -323
- package/plugins/core/workflow/stages/choice.js +74 -74
- package/plugins/core/workflow/stages/each.js +123 -123
- package/plugins/core/workflow/stages/parallel.js +69 -69
- package/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +188 -221
- package/src/agent/sub.js +28 -25
- package/src/agent/tool-loop.js +648 -0
- package/src/llm/provider.js +12 -28
- package/src/plugin/base.js +17 -14
- package/src/plugin/manager.js +19 -0
- package/tests/core/chat-tool.test.js +187 -187
- package/tests/core/disable-thinking.test.js +64 -0
- package/tests/core/reasoning-content.test.js +129 -0
- package/tests/core/strip-stale-tool-calls.test.js +154 -0
- package/tests/core/tool-loop.test.js +208 -0
|
@@ -1,323 +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 };
|
|
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 };
|