@olib-ai/owl-browser-sdk 2.0.7 → 2.1.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 +14 -104
- package/dist/flow/conditions.d.ts +2 -2
- package/dist/flow/conditions.d.ts.map +1 -1
- package/dist/flow/conditions.js +25 -5
- package/dist/flow/conditions.js.map +1 -1
- package/dist/flow/executor.d.ts +44 -2
- package/dist/flow/executor.d.ts.map +1 -1
- package/dist/flow/executor.js +441 -51
- package/dist/flow/executor.js.map +1 -1
- package/dist/flow/expectations.d.ts.map +1 -1
- package/dist/flow/expectations.js +42 -4
- package/dist/flow/expectations.js.map +1 -1
- package/dist/flow/extraction.d.ts +32 -0
- package/dist/flow/extraction.d.ts.map +1 -0
- package/dist/flow/extraction.js +56 -0
- package/dist/flow/extraction.js.map +1 -0
- package/dist/flow/index.d.ts +2 -0
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +1 -0
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/notifications.d.ts +97 -0
- package/dist/flow/notifications.d.ts.map +1 -0
- package/dist/flow/notifications.js +249 -0
- package/dist/flow/notifications.js.map +1 -0
- package/dist/flow/variables.d.ts +7 -1
- package/dist/flow/variables.d.ts.map +1 -1
- package/dist/flow/variables.js +90 -33
- package/dist/flow/variables.js.map +1 -1
- package/dist/types.d.ts +100 -2
- package/dist/types.d.ts.map +1 -1
- package/openapi.json +295 -45
- package/package.json +1 -1
package/dist/flow/executor.js
CHANGED
|
@@ -6,9 +6,64 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import { ConditionOperator } from '../types.js';
|
|
9
|
-
import { resolveVariables } from './variables.js';
|
|
9
|
+
import { resolveValue, resolveVariables } from './variables.js';
|
|
10
10
|
import { checkExpectation } from './expectations.js';
|
|
11
11
|
import { evaluateCondition } from './conditions.js';
|
|
12
|
+
import { applyExtract } from './extraction.js';
|
|
13
|
+
import { makeEvent } from './notifications.js';
|
|
14
|
+
class TimeoutAbort extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('step timeout');
|
|
17
|
+
this.name = 'TimeoutAbort';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
if (ms <= 0)
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
}
|
|
25
|
+
function retryDelayMs(attempt, baseMs, maxMs) {
|
|
26
|
+
if (baseMs <= 0)
|
|
27
|
+
return 0;
|
|
28
|
+
let delay = baseMs * Math.pow(2, attempt);
|
|
29
|
+
if (maxMs > 0 && delay > maxMs)
|
|
30
|
+
delay = maxMs;
|
|
31
|
+
return delay;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve any ${prev}/${vars}/${params} references inside an expectation
|
|
35
|
+
* payload. Returns a new StepExpectation with string fields run through
|
|
36
|
+
* the resolver; numeric/boolean fields are passed through unchanged.
|
|
37
|
+
*/
|
|
38
|
+
function resolveExpectation(expected, previousResult, vars_, params_) {
|
|
39
|
+
const resolved = { ...expected };
|
|
40
|
+
if (expected.equals !== undefined) {
|
|
41
|
+
resolved.equals = resolveValue(expected.equals, previousResult, vars_, params_);
|
|
42
|
+
}
|
|
43
|
+
if (expected.contains !== undefined) {
|
|
44
|
+
resolved.contains = resolveValue(expected.contains, previousResult, vars_, params_);
|
|
45
|
+
}
|
|
46
|
+
if (expected.matches !== undefined) {
|
|
47
|
+
resolved.matches = resolveValue(expected.matches, previousResult, vars_, params_);
|
|
48
|
+
}
|
|
49
|
+
if (expected.oneOf !== undefined) {
|
|
50
|
+
resolved.oneOf = resolveValue(expected.oneOf, previousResult, vars_, params_);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
async function runWithTimeout(fn, timeoutMs) {
|
|
55
|
+
let timer;
|
|
56
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
57
|
+
timer = setTimeout(() => reject(new TimeoutAbort()), timeoutMs);
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (timer)
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
12
67
|
/**
|
|
13
68
|
* Parameter aliases for flow JSON compatibility.
|
|
14
69
|
* Maps tool_name -> {alias_name -> canonical_name}
|
|
@@ -52,12 +107,6 @@ function applyParameterAliases(toolName, params) {
|
|
|
52
107
|
function generateStepId() {
|
|
53
108
|
return 'step_' + Math.random().toString(36).substring(2, 10);
|
|
54
109
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Sleep for a given number of milliseconds.
|
|
57
|
-
*/
|
|
58
|
-
function sleep(ms) {
|
|
59
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
60
|
-
}
|
|
61
110
|
/**
|
|
62
111
|
* Flow execution engine that runs a series of browser tool steps.
|
|
63
112
|
*
|
|
@@ -92,9 +141,31 @@ export class FlowExecutor {
|
|
|
92
141
|
_client;
|
|
93
142
|
_contextId;
|
|
94
143
|
_abortFlag = false;
|
|
95
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Flow-scoped variables captured by `step.capture = "name"`. Resolved via
|
|
146
|
+
* `${vars.NAME}` in any later step's params, regardless of how many
|
|
147
|
+
* steps separate the capture from the use.
|
|
148
|
+
*/
|
|
149
|
+
_vars = {};
|
|
150
|
+
/**
|
|
151
|
+
* Flow-scoped read-only parameters. Seeded from `flow.parameters`, may be
|
|
152
|
+
* overridden via setParams() (runner CLI). Resolved via `${params.NAME}`.
|
|
153
|
+
*/
|
|
154
|
+
_params = {};
|
|
155
|
+
_notifier;
|
|
156
|
+
_eventMetadata = {};
|
|
157
|
+
constructor(client, contextId, options) {
|
|
96
158
|
this._client = client;
|
|
97
159
|
this._contextId = contextId;
|
|
160
|
+
this._notifier = options?.notifier;
|
|
161
|
+
}
|
|
162
|
+
/** Attach metadata to every FlowEvent this executor emits. */
|
|
163
|
+
setEventMetadata(metadata) {
|
|
164
|
+
this._eventMetadata = { ...metadata };
|
|
165
|
+
}
|
|
166
|
+
/** Override flow parameters; merged with declared defaults at execute() time. */
|
|
167
|
+
setParams(params) {
|
|
168
|
+
Object.assign(this._params, params);
|
|
98
169
|
}
|
|
99
170
|
/**
|
|
100
171
|
* Signal to abort the current flow execution.
|
|
@@ -103,42 +174,140 @@ export class FlowExecutor {
|
|
|
103
174
|
this._abortFlag = true;
|
|
104
175
|
}
|
|
105
176
|
/**
|
|
106
|
-
* Reset the abort flag for a new execution.
|
|
177
|
+
* Reset the abort flag and clear flow-scoped vars for a new execution.
|
|
107
178
|
*/
|
|
108
179
|
reset() {
|
|
109
180
|
this._abortFlag = false;
|
|
181
|
+
this._vars = {};
|
|
182
|
+
this._params = {};
|
|
110
183
|
}
|
|
111
184
|
/**
|
|
112
185
|
* Execute a flow and return the results.
|
|
186
|
+
*
|
|
187
|
+
* Honours the flow's top-level `requiredEnv` and `timeoutSeconds`
|
|
188
|
+
* directly — embedders that drive `FlowExecutor` from their own code
|
|
189
|
+
* get the same gates the bundled runner provides, without having to
|
|
190
|
+
* reimplement them. When `requiredEnv` is unmet, the executor returns
|
|
191
|
+
* `success: true, skipped: true` and emits `flow.skipped` (no
|
|
192
|
+
* `flow.started` is emitted in that case so subscribers never see a
|
|
193
|
+
* started event without a terminal counterpart).
|
|
113
194
|
*/
|
|
114
195
|
async execute(flow) {
|
|
115
196
|
this._abortFlag = false;
|
|
197
|
+
this._vars = {};
|
|
198
|
+
// Seed parameter scope: flow defaults first, then any setParams() overrides.
|
|
199
|
+
const merged = { ...(flow.parameters ?? {}), ...this._params };
|
|
200
|
+
this._params = merged;
|
|
116
201
|
const startTime = performance.now();
|
|
117
202
|
const results = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
203
|
+
// Pre-flight: requiredEnv. If any declared env var is missing or
|
|
204
|
+
// empty, skip the flow with a clean reason instead of running it
|
|
205
|
+
// blind and failing mid-step. Emit `flow.skipped`; no `flow.started`
|
|
206
|
+
// first because there's no terminal event to pair with.
|
|
207
|
+
const missingEnv = (flow.requiredEnv ?? []).filter((n) => !process.env[n]);
|
|
208
|
+
if (missingEnv.length > 0) {
|
|
209
|
+
const reason = `missing required env vars: ${missingEnv.join(', ')}`;
|
|
210
|
+
const skipped = {
|
|
121
211
|
success: true,
|
|
122
212
|
steps: [],
|
|
123
213
|
totalDurationMs: 0,
|
|
214
|
+
error: reason,
|
|
215
|
+
skipped: true,
|
|
124
216
|
};
|
|
217
|
+
await this._emitSkipped(flow, reason);
|
|
218
|
+
return skipped;
|
|
219
|
+
}
|
|
220
|
+
await this._emitStarted(flow);
|
|
221
|
+
const enabledSteps = flow.steps.filter(s => s.enabled);
|
|
222
|
+
if (enabledSteps.length === 0) {
|
|
223
|
+
const empty = { success: true, steps: [], totalDurationMs: 0 };
|
|
224
|
+
await this._emitTerminal(flow, empty, startTime);
|
|
225
|
+
return empty;
|
|
125
226
|
}
|
|
227
|
+
// Wall-clock cap for the entire flow. When the flow declares
|
|
228
|
+
// `timeoutSeconds` we race the step-loop against a timer. The
|
|
229
|
+
// bundled runner has its own outer cap; embedders calling execute()
|
|
230
|
+
// directly would otherwise have no cap.
|
|
231
|
+
const timeoutS = flow.timeoutSeconds && flow.timeoutSeconds > 0
|
|
232
|
+
? flow.timeoutSeconds : undefined;
|
|
233
|
+
let final;
|
|
126
234
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
235
|
+
let success;
|
|
236
|
+
if (timeoutS !== undefined) {
|
|
237
|
+
const [ok] = await runWithTimeout(() => this._executeSteps(enabledSteps, results, undefined), timeoutS * 1000);
|
|
238
|
+
success = ok;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const [ok] = await this._executeSteps(enabledSteps, results, undefined);
|
|
242
|
+
success = ok;
|
|
243
|
+
}
|
|
244
|
+
final = {
|
|
129
245
|
success,
|
|
130
246
|
steps: results,
|
|
131
247
|
totalDurationMs: performance.now() - startTime,
|
|
132
248
|
};
|
|
133
249
|
}
|
|
134
250
|
catch (e) {
|
|
135
|
-
|
|
251
|
+
const isTimeout = e instanceof TimeoutAbort;
|
|
252
|
+
final = {
|
|
136
253
|
success: false,
|
|
137
254
|
steps: results,
|
|
138
255
|
totalDurationMs: performance.now() - startTime,
|
|
139
|
-
error:
|
|
256
|
+
error: isTimeout
|
|
257
|
+
? `flow timeout after ${timeoutS}s`
|
|
258
|
+
: (e instanceof Error ? e.message : String(e)),
|
|
140
259
|
};
|
|
141
260
|
}
|
|
261
|
+
await this._emitTerminal(flow, final, startTime);
|
|
262
|
+
return final;
|
|
263
|
+
}
|
|
264
|
+
// ------------------------------------------------------------------
|
|
265
|
+
// Notification emission helpers — no-op when no notifier attached.
|
|
266
|
+
// ------------------------------------------------------------------
|
|
267
|
+
async _emitStarted(flow) {
|
|
268
|
+
if (!this._notifier)
|
|
269
|
+
return;
|
|
270
|
+
await this._notifier.emit(makeEvent({
|
|
271
|
+
type: 'flow.started',
|
|
272
|
+
flowName: flow.name,
|
|
273
|
+
stepCount: flow.steps.length,
|
|
274
|
+
metadata: this._eventMetadata,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/** Emit `flow.skipped` when `requiredEnv` is unmet. */
|
|
278
|
+
async _emitSkipped(flow, reason) {
|
|
279
|
+
if (!this._notifier)
|
|
280
|
+
return;
|
|
281
|
+
await this._notifier.emit(makeEvent({
|
|
282
|
+
type: 'flow.skipped',
|
|
283
|
+
flowName: flow.name,
|
|
284
|
+
stepCount: flow.steps.length,
|
|
285
|
+
error: reason,
|
|
286
|
+
metadata: this._eventMetadata,
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
async _emitTerminal(flow, result, startTime) {
|
|
290
|
+
if (!this._notifier)
|
|
291
|
+
return;
|
|
292
|
+
let failedStep;
|
|
293
|
+
for (const r of result.steps) {
|
|
294
|
+
if (!r.success) {
|
|
295
|
+
failedStep = { index: r.stepIndex, tool: r.toolName, error: r.error };
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
await this._notifier.emit(makeEvent({
|
|
300
|
+
type: result.success ? 'flow.succeeded' : 'flow.failed',
|
|
301
|
+
flowName: flow.name,
|
|
302
|
+
durationMs: performance.now() - startTime,
|
|
303
|
+
success: result.success,
|
|
304
|
+
error: result.error,
|
|
305
|
+
failedStep,
|
|
306
|
+
stepCount: flow.steps.length,
|
|
307
|
+
executedSteps: result.steps.length,
|
|
308
|
+
metadata: this._eventMetadata,
|
|
309
|
+
result,
|
|
310
|
+
}));
|
|
142
311
|
}
|
|
143
312
|
async _executeSteps(steps, results, previousResult) {
|
|
144
313
|
let lastResult = previousResult;
|
|
@@ -147,6 +316,18 @@ export class FlowExecutor {
|
|
|
147
316
|
return [false, lastResult];
|
|
148
317
|
}
|
|
149
318
|
const step = steps[i];
|
|
319
|
+
if (step.type === 'for_each') {
|
|
320
|
+
// Iterate `step.params.source` and run `step.params.body` once per
|
|
321
|
+
// element. See _executeForEachStep for the full schema.
|
|
322
|
+
const [loopOk, loopLast] = await this._executeForEachStep(step, i, lastResult, results);
|
|
323
|
+
if (!loopOk && !step.optional) {
|
|
324
|
+
return [false, lastResult];
|
|
325
|
+
}
|
|
326
|
+
if (loopOk) {
|
|
327
|
+
lastResult = loopLast;
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
150
331
|
if (step.type === 'condition' && step.condition) {
|
|
151
332
|
const result = await this._executeConditionStep(step, i, lastResult, results);
|
|
152
333
|
if (!result.success) {
|
|
@@ -158,9 +339,22 @@ export class FlowExecutor {
|
|
|
158
339
|
const result = await this._executeToolStep(step, i, lastResult);
|
|
159
340
|
results.push(result);
|
|
160
341
|
if (!result.success) {
|
|
342
|
+
if (step.optional) {
|
|
343
|
+
// Best-effort step: record the failure but don't abort the flow
|
|
344
|
+
// and don't propagate the error as the new lastResult (we'd
|
|
345
|
+
// otherwise corrupt downstream variable resolution).
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
161
348
|
return [false, lastResult];
|
|
162
349
|
}
|
|
163
350
|
lastResult = result.result;
|
|
351
|
+
// Capture into flow-scoped vars so later steps can resolve
|
|
352
|
+
// ${vars.NAME} regardless of how many steps separate them. When
|
|
353
|
+
// `step.extract` is set, the captured value is the extracted form;
|
|
354
|
+
// raw stays on `${prev}` / `result.result`.
|
|
355
|
+
if (step.capture) {
|
|
356
|
+
this._vars[step.capture] = step.extract ? result.extracted : result.result;
|
|
357
|
+
}
|
|
164
358
|
}
|
|
165
359
|
// Small delay between steps
|
|
166
360
|
if (i < steps.length - 1) {
|
|
@@ -169,6 +363,69 @@ export class FlowExecutor {
|
|
|
169
363
|
}
|
|
170
364
|
return [true, lastResult];
|
|
171
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Execute a `for_each` step: iterate an array source, run a body per item.
|
|
368
|
+
*
|
|
369
|
+
* Schema (under step.params):
|
|
370
|
+
* source `${...}` ref or literal array
|
|
371
|
+
* body array of step JSON to run per item
|
|
372
|
+
* item_var capture name for the current item (default 'item')
|
|
373
|
+
* index_var capture name for the index (default 'index')
|
|
374
|
+
*/
|
|
375
|
+
async _executeForEachStep(step, stepIndex, previousResult, results) {
|
|
376
|
+
const sourceRaw = resolveValue(step.params['source'], previousResult, this._vars, this._params);
|
|
377
|
+
const bodyRaw = step.params['body'];
|
|
378
|
+
const itemVar = step.params['item_var'] || 'item';
|
|
379
|
+
const indexVar = step.params['index_var'] || 'index';
|
|
380
|
+
let items = [];
|
|
381
|
+
if (Array.isArray(sourceRaw)) {
|
|
382
|
+
items = sourceRaw;
|
|
383
|
+
}
|
|
384
|
+
else if (typeof sourceRaw === 'string') {
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(sourceRaw);
|
|
387
|
+
items = Array.isArray(parsed) ? parsed : [];
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
items = [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!Array.isArray(bodyRaw) || bodyRaw.length === 0) {
|
|
394
|
+
results.push({
|
|
395
|
+
stepIndex,
|
|
396
|
+
stepId: step.id,
|
|
397
|
+
toolName: 'for_each',
|
|
398
|
+
success: true,
|
|
399
|
+
result: { iterations: 0, items: 0, note: 'for_each body empty/invalid' },
|
|
400
|
+
durationMs: 0,
|
|
401
|
+
});
|
|
402
|
+
return [true, previousResult];
|
|
403
|
+
}
|
|
404
|
+
const bodySteps = bodyRaw.map((b) => FlowExecutor.parseStep(b));
|
|
405
|
+
let last = previousResult;
|
|
406
|
+
let iterations = 0;
|
|
407
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
408
|
+
if (this._abortFlag)
|
|
409
|
+
return [false, last];
|
|
410
|
+
this._vars[itemVar] = items[idx];
|
|
411
|
+
this._vars[indexVar] = idx;
|
|
412
|
+
const enabled = bodySteps.filter((s) => s.enabled);
|
|
413
|
+
const [iterOk, iterLast] = await this._executeSteps(enabled, results, items[idx]);
|
|
414
|
+
iterations++;
|
|
415
|
+
if (!iterOk)
|
|
416
|
+
return [false, iterLast];
|
|
417
|
+
last = iterLast;
|
|
418
|
+
}
|
|
419
|
+
results.push({
|
|
420
|
+
stepIndex,
|
|
421
|
+
stepId: step.id,
|
|
422
|
+
toolName: 'for_each',
|
|
423
|
+
success: true,
|
|
424
|
+
result: { iterations, items: items.length },
|
|
425
|
+
durationMs: 0,
|
|
426
|
+
});
|
|
427
|
+
return [true, last];
|
|
428
|
+
}
|
|
172
429
|
async _executeConditionStep(step, stepIndex, previousResult, results) {
|
|
173
430
|
const startTime = performance.now();
|
|
174
431
|
if (!step.condition) {
|
|
@@ -181,7 +438,15 @@ export class FlowExecutor {
|
|
|
181
438
|
durationMs: performance.now() - startTime,
|
|
182
439
|
};
|
|
183
440
|
}
|
|
184
|
-
|
|
441
|
+
// Resolve any ${prev}/${vars}/${params} references in the condition
|
|
442
|
+
// value before comparing. Without this, condition.value was used
|
|
443
|
+
// verbatim and any ${vars.X}-style condition silently always took the
|
|
444
|
+
// wrong branch.
|
|
445
|
+
const resolvedCondition = {
|
|
446
|
+
...step.condition,
|
|
447
|
+
value: resolveValue(step.condition.value, previousResult, this._vars, this._params),
|
|
448
|
+
};
|
|
449
|
+
const conditionResult = evaluateCondition(resolvedCondition, previousResult, results);
|
|
185
450
|
const branchTaken = conditionResult ? 'true' : 'false';
|
|
186
451
|
const conditionStepResult = {
|
|
187
452
|
stepIndex,
|
|
@@ -223,55 +488,119 @@ export class FlowExecutor {
|
|
|
223
488
|
}
|
|
224
489
|
async _executeToolStep(step, stepIndex, previousResult) {
|
|
225
490
|
const startTime = performance.now();
|
|
226
|
-
let params = resolveVariables(step.params, previousResult);
|
|
491
|
+
let params = resolveVariables(step.params, previousResult, this._vars, this._params);
|
|
227
492
|
params = applyParameterAliases(step.type, params);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
493
|
+
// Only inject context_id for tools that actually accept it. See the
|
|
494
|
+
// matching gating in the Python executor for the why.
|
|
495
|
+
const toolDef = this._client.getTool(step.type);
|
|
496
|
+
if (!toolDef || 'context_id' in toolDef.parameters) {
|
|
497
|
+
params['context_id'] = this._contextId;
|
|
498
|
+
}
|
|
499
|
+
const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
|
|
500
|
+
const backoffMs = step.retry?.backoffMs ?? 0;
|
|
501
|
+
const maxBackoffMs = step.retry?.maxBackoffMs ?? 0;
|
|
502
|
+
let lastError;
|
|
503
|
+
let lastExpectation;
|
|
504
|
+
let lastResult = undefined;
|
|
505
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
506
|
+
try {
|
|
507
|
+
if (typeof step.timeoutMs === 'number' && step.timeoutMs > 0) {
|
|
508
|
+
lastResult = await runWithTimeout(() => this._client.execute(step.type, params), step.timeoutMs);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
lastResult = await this._client.execute(step.type, params);
|
|
512
|
+
}
|
|
513
|
+
// Apply post-execution extraction (jsonpath or regex capture
|
|
514
|
+
// groups) BEFORE the expectation check so authors can `expected`
|
|
515
|
+
// against the extracted value via `field: "<group_name>"`.
|
|
516
|
+
const extracted = step.extract ? applyExtract(lastResult, step.extract) : undefined;
|
|
517
|
+
const extractFailed = step.extract !== undefined && extracted === undefined;
|
|
518
|
+
if (extractFailed) {
|
|
519
|
+
lastError = `Extract "${step.extract.mode}" yielded no match for pattern: ${step.extract.pattern}`;
|
|
520
|
+
if (attempt + 1 < maxAttempts) {
|
|
521
|
+
await sleep(retryDelayMs(attempt, backoffMs, maxBackoffMs));
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
235
524
|
return {
|
|
236
525
|
stepIndex,
|
|
237
526
|
stepId: step.id,
|
|
238
527
|
toolName: step.type,
|
|
239
528
|
success: false,
|
|
240
|
-
result,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
529
|
+
result: lastResult,
|
|
530
|
+
extracted,
|
|
531
|
+
error: lastError,
|
|
532
|
+
durationMs: performance.now() - startTime,
|
|
533
|
+
attemptsUsed: attempt + 1,
|
|
244
534
|
};
|
|
245
535
|
}
|
|
536
|
+
if (step.expected) {
|
|
537
|
+
// Resolve `${prev}/${vars}/${params}` references inside the
|
|
538
|
+
// expectation payload before checking. Without this,
|
|
539
|
+
// `expected: { equals: "${vars.X}" }` always failed verbatim.
|
|
540
|
+
const resolvedExpected = resolveExpectation(step.expected, lastResult, this._vars, this._params);
|
|
541
|
+
// Expectation runs against the extracted value when an extract
|
|
542
|
+
// block is present (authors typically expect-on the OTP / token
|
|
543
|
+
// they just pulled, not the wrapping payload).
|
|
544
|
+
const expectationTarget = step.extract ? extracted : lastResult;
|
|
545
|
+
lastExpectation = checkExpectation(expectationTarget, resolvedExpected);
|
|
546
|
+
if (!lastExpectation.passed) {
|
|
547
|
+
lastError = 'Expectation failed: ' + lastExpectation.message;
|
|
548
|
+
// Expectation failure is retryable.
|
|
549
|
+
if (attempt + 1 < maxAttempts) {
|
|
550
|
+
await sleep(retryDelayMs(attempt, backoffMs, maxBackoffMs));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
stepIndex,
|
|
555
|
+
stepId: step.id,
|
|
556
|
+
toolName: step.type,
|
|
557
|
+
success: false,
|
|
558
|
+
result: lastResult,
|
|
559
|
+
extracted,
|
|
560
|
+
error: lastError,
|
|
561
|
+
durationMs: performance.now() - startTime,
|
|
562
|
+
expectationResult: lastExpectation,
|
|
563
|
+
attemptsUsed: attempt + 1,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
246
567
|
return {
|
|
247
568
|
stepIndex,
|
|
248
569
|
stepId: step.id,
|
|
249
570
|
toolName: step.type,
|
|
250
571
|
success: true,
|
|
251
|
-
result,
|
|
252
|
-
|
|
253
|
-
|
|
572
|
+
result: lastResult,
|
|
573
|
+
extracted,
|
|
574
|
+
durationMs: performance.now() - startTime,
|
|
575
|
+
expectationResult: lastExpectation,
|
|
576
|
+
attemptsUsed: attempt + 1,
|
|
254
577
|
};
|
|
255
578
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
toolName: step.type,
|
|
270
|
-
success: false,
|
|
271
|
-
error: e instanceof Error ? e.message : String(e),
|
|
272
|
-
durationMs: performance.now() - startTime,
|
|
273
|
-
};
|
|
579
|
+
catch (e) {
|
|
580
|
+
if (e instanceof TimeoutAbort) {
|
|
581
|
+
lastError =
|
|
582
|
+
`Step timed out after ${step.timeoutMs}ms` +
|
|
583
|
+
` (attempt ${attempt + 1}/${maxAttempts})`;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (attempt + 1 < maxAttempts) {
|
|
590
|
+
await sleep(retryDelayMs(attempt, backoffMs, maxBackoffMs));
|
|
591
|
+
}
|
|
274
592
|
}
|
|
593
|
+
return {
|
|
594
|
+
stepIndex,
|
|
595
|
+
stepId: step.id,
|
|
596
|
+
toolName: step.type,
|
|
597
|
+
success: false,
|
|
598
|
+
result: lastResult,
|
|
599
|
+
error: lastError ?? 'step failed',
|
|
600
|
+
durationMs: performance.now() - startTime,
|
|
601
|
+
expectationResult: lastExpectation,
|
|
602
|
+
attemptsUsed: maxAttempts,
|
|
603
|
+
};
|
|
275
604
|
}
|
|
276
605
|
/**
|
|
277
606
|
* Load a flow from a JSON file.
|
|
@@ -287,10 +616,25 @@ export class FlowExecutor {
|
|
|
287
616
|
static parseFlow(data) {
|
|
288
617
|
const stepsData = (data['steps'] ?? []);
|
|
289
618
|
const steps = stepsData.map(s => FlowExecutor.parseStep(s));
|
|
619
|
+
const timeoutSecondsRaw = data['timeoutSeconds'] ?? data['timeout_seconds'];
|
|
620
|
+
const timeoutSeconds = typeof timeoutSecondsRaw === 'number' && Number.isFinite(timeoutSecondsRaw)
|
|
621
|
+
? timeoutSecondsRaw
|
|
622
|
+
: undefined;
|
|
623
|
+
const requiredEnvRaw = data['requiredEnv'] ?? data['required_env'];
|
|
624
|
+
const requiredEnv = Array.isArray(requiredEnvRaw)
|
|
625
|
+
? requiredEnvRaw
|
|
626
|
+
: undefined;
|
|
627
|
+
const parametersRaw = data['parameters'] ?? data['params'];
|
|
628
|
+
const parameters = parametersRaw && typeof parametersRaw === 'object' && !Array.isArray(parametersRaw)
|
|
629
|
+
? parametersRaw
|
|
630
|
+
: undefined;
|
|
290
631
|
return {
|
|
291
632
|
name: String(data['name'] ?? 'Unnamed Flow'),
|
|
292
633
|
description: data['description'],
|
|
293
634
|
steps,
|
|
635
|
+
timeoutSeconds,
|
|
636
|
+
requiredEnv,
|
|
637
|
+
parameters,
|
|
294
638
|
};
|
|
295
639
|
}
|
|
296
640
|
/**
|
|
@@ -299,11 +643,18 @@ export class FlowExecutor {
|
|
|
299
643
|
static parseStep(data) {
|
|
300
644
|
const stepId = String(data['id'] ?? generateStepId());
|
|
301
645
|
const stepType = String(data['type'] ?? '');
|
|
302
|
-
// Extract params (everything except metadata)
|
|
646
|
+
// Extract params (everything except metadata). Reserved step-level keys
|
|
647
|
+
// get stripped so they aren't sent to the http-server's strict
|
|
648
|
+
// tools_validate as unknown tool params.
|
|
303
649
|
const params = {};
|
|
304
650
|
const keysToRemove = new Set([
|
|
305
651
|
'type', 'selected', 'enabled', 'expected',
|
|
306
652
|
'condition', 'onTrue', 'onFalse', 'on_true', 'on_false', 'id',
|
|
653
|
+
// Lap-1.5 step-level extensions:
|
|
654
|
+
'retry', 'timeout', 'timeoutMs', 'timeout_ms',
|
|
655
|
+
'optional', 'tags', 'note',
|
|
656
|
+
// Lap-2 step-level extensions:
|
|
657
|
+
'capture', 'extract',
|
|
307
658
|
]);
|
|
308
659
|
// Only remove 'description' if this tool doesn't use it as a parameter
|
|
309
660
|
if (!TOOLS_WITH_DESCRIPTION_PARAM.has(stepType)) {
|
|
@@ -327,6 +678,7 @@ export class FlowExecutor {
|
|
|
327
678
|
notEmpty: (expData['notEmpty'] ?? expData['not_empty']),
|
|
328
679
|
field: expData['field'],
|
|
329
680
|
matches: expData['matches'],
|
|
681
|
+
oneOf: (expData['oneOf'] ?? expData['one_of']),
|
|
330
682
|
};
|
|
331
683
|
}
|
|
332
684
|
// Parse condition
|
|
@@ -353,6 +705,21 @@ export class FlowExecutor {
|
|
|
353
705
|
onFalse = onFalseData.map(s => FlowExecutor.parseStep(s));
|
|
354
706
|
}
|
|
355
707
|
const enabled = (data['enabled'] ?? data['selected'] ?? true);
|
|
708
|
+
// Step-level extensions: retry / timeout / optional / tags.
|
|
709
|
+
let retry;
|
|
710
|
+
if (data['retry'] && typeof data['retry'] === 'object') {
|
|
711
|
+
const r = data['retry'];
|
|
712
|
+
const maxAttempts = Number(r['maxAttempts'] ?? r['max_attempts'] ?? 1);
|
|
713
|
+
retry = {
|
|
714
|
+
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 1,
|
|
715
|
+
backoffMs: Number(r['backoffMs'] ?? r['backoff_ms'] ?? 200),
|
|
716
|
+
maxBackoffMs: Number(r['maxBackoffMs'] ?? r['max_backoff_ms'] ?? 5000),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const timeoutMsRaw = data['timeoutMs'] ?? data['timeout_ms'];
|
|
720
|
+
const timeoutMs = typeof timeoutMsRaw === 'number' && Number.isFinite(timeoutMsRaw)
|
|
721
|
+
? timeoutMsRaw
|
|
722
|
+
: undefined;
|
|
356
723
|
return {
|
|
357
724
|
id: stepId,
|
|
358
725
|
type: stepType,
|
|
@@ -363,7 +730,30 @@ export class FlowExecutor {
|
|
|
363
730
|
condition,
|
|
364
731
|
onTrue,
|
|
365
732
|
onFalse,
|
|
733
|
+
timeoutMs,
|
|
734
|
+
retry,
|
|
735
|
+
optional: Boolean(data['optional']),
|
|
736
|
+
tags: Array.isArray(data['tags']) ? data['tags'] : undefined,
|
|
737
|
+
capture: typeof data['capture'] === 'string' ? data['capture'] : undefined,
|
|
738
|
+
extract: parseExtract(data['extract']),
|
|
366
739
|
};
|
|
367
740
|
}
|
|
368
741
|
}
|
|
742
|
+
function parseExtract(raw) {
|
|
743
|
+
if (!raw || typeof raw !== 'object')
|
|
744
|
+
return undefined;
|
|
745
|
+
const r = raw;
|
|
746
|
+
const mode = r['mode'];
|
|
747
|
+
if (mode !== 'jsonpath' && mode !== 'regex')
|
|
748
|
+
return undefined;
|
|
749
|
+
const pattern = typeof r['pattern'] === 'string' ? r['pattern'] : '';
|
|
750
|
+
if (!pattern)
|
|
751
|
+
return undefined;
|
|
752
|
+
return {
|
|
753
|
+
mode,
|
|
754
|
+
pattern,
|
|
755
|
+
on: typeof r['on'] === 'string' ? r['on'] : undefined,
|
|
756
|
+
flags: typeof r['flags'] === 'string' ? r['flags'] : undefined,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
369
759
|
//# sourceMappingURL=executor.js.map
|