@orbytautomation/engine 0.7.0 → 0.8.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/dist/cache/AdapterMetadataCache.d.ts +22 -0
- package/dist/cache/AdapterMetadataCache.d.ts.map +1 -0
- package/dist/cache/AdapterMetadataCache.js +66 -0
- package/dist/cache/AdapterMetadataCache.js.map +1 -0
- package/dist/cache/WorkflowParseCache.d.ts +26 -0
- package/dist/cache/WorkflowParseCache.d.ts.map +1 -0
- package/dist/cache/WorkflowParseCache.js +91 -0
- package/dist/cache/WorkflowParseCache.js.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +3 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/core/EngineConfig.d.ts.map +1 -1
- package/dist/core/EngineConfig.js +8 -2
- package/dist/core/EngineConfig.js.map +1 -1
- package/dist/core/OrbytEngine.d.ts +104 -1
- package/dist/core/OrbytEngine.d.ts.map +1 -1
- package/dist/core/OrbytEngine.js +454 -95
- package/dist/core/OrbytEngine.js.map +1 -1
- package/dist/execution/StepExecutor.d.ts +8 -0
- package/dist/execution/StepExecutor.d.ts.map +1 -1
- package/dist/execution/StepExecutor.js +32 -3
- package/dist/execution/StepExecutor.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/EngineLogger.js +4 -4
- package/dist/logging/EngineLogger.js.map +1 -1
- package/dist/parser/WorkflowParser.d.ts.map +1 -1
- package/dist/parser/WorkflowParser.js +2 -0
- package/dist/parser/WorkflowParser.js.map +1 -1
- package/dist/runtime/RuntimeArtifactStore.d.ts +27 -0
- package/dist/runtime/RuntimeArtifactStore.d.ts.map +1 -0
- package/dist/runtime/RuntimeArtifactStore.js +96 -0
- package/dist/runtime/RuntimeArtifactStore.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/scheduling/Scheduler.d.ts.map +1 -1
- package/dist/scheduling/Scheduler.js +6 -4
- package/dist/scheduling/Scheduler.js.map +1 -1
- package/dist/security/ResourceValidator.d.ts +20 -0
- package/dist/security/ResourceValidator.d.ts.map +1 -0
- package/dist/security/ResourceValidator.js +107 -0
- package/dist/security/ResourceValidator.js.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1 -0
- package/dist/security/index.js.map +1 -1
- package/dist/storage/ExecutionStore.d.ts +2 -2
- package/dist/storage/ExecutionStore.d.ts.map +1 -1
- package/dist/storage/ExecutionStore.js +16 -25
- package/dist/storage/ExecutionStore.js.map +1 -1
- package/dist/storage/FileStorageAdapter.d.ts +23 -0
- package/dist/storage/FileStorageAdapter.d.ts.map +1 -0
- package/dist/storage/FileStorageAdapter.js +66 -0
- package/dist/storage/FileStorageAdapter.js.map +1 -0
- package/dist/storage/ScheduleStore.d.ts +2 -2
- package/dist/storage/ScheduleStore.d.ts.map +1 -1
- package/dist/storage/ScheduleStore.js +19 -27
- package/dist/storage/ScheduleStore.js.map +1 -1
- package/dist/storage/StorageAdapter.d.ts +22 -0
- package/dist/storage/StorageAdapter.d.ts.map +1 -0
- package/dist/storage/StorageAdapter.js +2 -0
- package/dist/storage/StorageAdapter.js.map +1 -0
- package/dist/storage/WorkflowStore.d.ts +2 -1
- package/dist/storage/WorkflowStore.d.ts.map +1 -1
- package/dist/storage/WorkflowStore.js +24 -27
- package/dist/storage/WorkflowStore.js.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/types/core-types.d.ts +89 -0
- package/dist/types/core-types.d.ts.map +1 -1
- package/dist/types/core-types.js.map +1 -1
- package/package.json +1 -1
package/dist/core/OrbytEngine.js
CHANGED
|
@@ -82,13 +82,16 @@ import { CLIAdapter, ShellAdapter, HTTPAdapter, FSAdapter } from '../adapters/bu
|
|
|
82
82
|
import { InternalContextBuilder } from '../execution/InternalExecutionContext.js';
|
|
83
83
|
import { IntentAnalyzer } from '../execution/IntentAnalyzer.js';
|
|
84
84
|
import { ExecutionStrategyResolver, ExecutionStrategyGuard } from '../execution/ExecutionStrategyResolver.js';
|
|
85
|
-
import { EngineEventType } from '../types/core-types.js';
|
|
85
|
+
import { EngineEventType, } from '../types/core-types.js';
|
|
86
86
|
import { ExplanationGenerator, ExplanationLogger } from '../explanation/index.js';
|
|
87
|
-
import { existsSync } from 'node:fs';
|
|
87
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
88
88
|
import { join } from 'node:path';
|
|
89
|
+
import { homedir } from 'node:os';
|
|
89
90
|
import { ExecutionStore } from '../storage/ExecutionStore.js';
|
|
90
91
|
import { WorkflowStore } from '../storage/WorkflowStore.js';
|
|
91
92
|
import { ScheduleStore } from '../storage/ScheduleStore.js';
|
|
93
|
+
import { WorkflowParseCache, AdapterMetadataCache } from '../cache/index.js';
|
|
94
|
+
import { RuntimeArtifactStore } from '../runtime/index.js';
|
|
92
95
|
/**
|
|
93
96
|
* OrbytEngine - Main public API
|
|
94
97
|
*
|
|
@@ -213,6 +216,7 @@ import { ScheduleStore } from '../storage/ScheduleStore.js';
|
|
|
213
216
|
* ```
|
|
214
217
|
*/
|
|
215
218
|
export class OrbytEngine {
|
|
219
|
+
static SUPPORTED_WORKFLOW_MAJOR = 1;
|
|
216
220
|
config;
|
|
217
221
|
executionEngine;
|
|
218
222
|
stepExecutor;
|
|
@@ -227,6 +231,9 @@ export class OrbytEngine {
|
|
|
227
231
|
executionStore;
|
|
228
232
|
workflowStore;
|
|
229
233
|
scheduleStore;
|
|
234
|
+
workflowParseCache;
|
|
235
|
+
adapterMetadataCache;
|
|
236
|
+
runtimeArtifactStore;
|
|
230
237
|
constructor(config = {}) {
|
|
231
238
|
// Validate and apply defaults
|
|
232
239
|
validateConfig(config);
|
|
@@ -259,6 +266,9 @@ export class OrbytEngine {
|
|
|
259
266
|
this.adapterRegistry = new AdapterRegistry();
|
|
260
267
|
// Initialize executors
|
|
261
268
|
this.stepExecutor = new StepExecutor();
|
|
269
|
+
// Share OrbytEngine's AdapterRegistry with StepExecutor so there is a
|
|
270
|
+
// single registry instance — preventing duplicate INFO logs on registration.
|
|
271
|
+
this.stepExecutor.setAdapterRegistry(this.adapterRegistry);
|
|
262
272
|
this.workflowExecutor = new WorkflowExecutor(this.stepExecutor);
|
|
263
273
|
// Initialize execution engine
|
|
264
274
|
this.executionEngine = new ExecutionEngine({
|
|
@@ -271,11 +281,17 @@ export class OrbytEngine {
|
|
|
271
281
|
});
|
|
272
282
|
// Wire components together
|
|
273
283
|
this.setupComponents();
|
|
284
|
+
// Ensure infrastructure directories exist before any persistence/caching.
|
|
285
|
+
this.bootstrapRuntimeDirectories();
|
|
274
286
|
// Initialise persistent stores (non-fatal — must never block engine startup)
|
|
275
|
-
const storeRoot = this.config.stateDir ?? join(
|
|
287
|
+
const storeRoot = this.config.stateDir ?? join(homedir(), '.orbyt');
|
|
276
288
|
this.executionStore = new ExecutionStore(join(storeRoot, 'executions'));
|
|
277
289
|
this.workflowStore = new WorkflowStore(join(storeRoot, 'workflows'));
|
|
278
290
|
this.scheduleStore = new ScheduleStore(join(storeRoot, 'schedules'));
|
|
291
|
+
this.workflowParseCache = new WorkflowParseCache(join(this.config.cacheDir, 'workflows'));
|
|
292
|
+
this.adapterMetadataCache = new AdapterMetadataCache(join(this.config.cacheDir, 'adapters'));
|
|
293
|
+
this.runtimeArtifactStore = new RuntimeArtifactStore(this.config.runtimeDir);
|
|
294
|
+
this.runtimeArtifactStore.ensureDirs();
|
|
279
295
|
this.workflowExecutor.setStateDir(join(storeRoot, 'executions'));
|
|
280
296
|
// Register built-in adapters
|
|
281
297
|
this.registerBuiltinAdapters();
|
|
@@ -334,6 +350,41 @@ export class OrbytEngine {
|
|
|
334
350
|
this.stepExecutor.setTimeoutManager(this.config.timeoutManager);
|
|
335
351
|
}
|
|
336
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Create all required engine directories under ~/.orbyt (or custom overrides).
|
|
355
|
+
* This keeps later store/cache writes simple and predictable.
|
|
356
|
+
*/
|
|
357
|
+
bootstrapRuntimeDirectories() {
|
|
358
|
+
const orbytHome = join(homedir(), '.orbyt');
|
|
359
|
+
const requiredDirs = [
|
|
360
|
+
orbytHome,
|
|
361
|
+
this.config.stateDir,
|
|
362
|
+
join(this.config.stateDir, 'executions'),
|
|
363
|
+
join(this.config.stateDir, 'workflows'),
|
|
364
|
+
join(this.config.stateDir, 'schedules'),
|
|
365
|
+
this.config.logDir,
|
|
366
|
+
this.config.cacheDir,
|
|
367
|
+
join(this.config.cacheDir, 'workflows'),
|
|
368
|
+
join(this.config.cacheDir, 'adapters'),
|
|
369
|
+
this.config.runtimeDir,
|
|
370
|
+
join(this.config.runtimeDir, 'dag'),
|
|
371
|
+
join(this.config.runtimeDir, 'context'),
|
|
372
|
+
join(this.config.runtimeDir, 'locks'),
|
|
373
|
+
join(orbytHome, 'plugins'),
|
|
374
|
+
join(orbytHome, 'metrics'),
|
|
375
|
+
join(orbytHome, 'config'),
|
|
376
|
+
join(orbytHome, 'tmp'),
|
|
377
|
+
join(orbytHome, 'cloud-sync'),
|
|
378
|
+
];
|
|
379
|
+
for (const dir of requiredDirs) {
|
|
380
|
+
try {
|
|
381
|
+
mkdirSync(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Non-fatal bootstrap: individual stores also guard their own writes.
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
337
388
|
/**
|
|
338
389
|
* Register built-in adapters
|
|
339
390
|
* These are the core adapters shipped with Orbyt
|
|
@@ -423,10 +474,132 @@ export class OrbytEngine {
|
|
|
423
474
|
if (!this.isStarted && this.config.enableScheduler) {
|
|
424
475
|
await this.start();
|
|
425
476
|
}
|
|
426
|
-
|
|
477
|
+
const parsedWorkflow = await this.resolveWorkflowInput(workflow);
|
|
478
|
+
return this.executeParsedWorkflow(parsedWorkflow, options);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Execute multiple workflows using explicit execution mode.
|
|
482
|
+
*
|
|
483
|
+
* Flow:
|
|
484
|
+
* 1. Preload+validate all workflows first
|
|
485
|
+
* 2. Execute according to mode (sequential | parallel | mixed)
|
|
486
|
+
*/
|
|
487
|
+
async runMany(workflows, options = {}) {
|
|
488
|
+
if (!Array.isArray(workflows) || workflows.length === 0) {
|
|
489
|
+
throw new Error('runMany requires at least one workflow input');
|
|
490
|
+
}
|
|
491
|
+
if (!this.isStarted && this.config.enableScheduler) {
|
|
492
|
+
await this.start();
|
|
493
|
+
}
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
const failFast = options.failFast === true;
|
|
496
|
+
const loaded = await this.preloadWorkflows(workflows);
|
|
497
|
+
const mode = this.resolveBatchExecutionMode(loaded, options.executionMode);
|
|
498
|
+
const maxParallel = Math.max(1, options.maxParallelWorkflows || this.config.maxConcurrentWorkflows || 1);
|
|
499
|
+
const inferredWaveSize = loaded.some((item) => item.workflow.strategy?.maxParallel)
|
|
500
|
+
? Math.max(...loaded.map((item) => item.workflow.strategy?.maxParallel || 1))
|
|
501
|
+
: 2;
|
|
502
|
+
const waveSize = Math.max(1, options.mixedBatchSize || inferredWaveSize);
|
|
503
|
+
const results = [];
|
|
504
|
+
if (mode === 'sequential') {
|
|
505
|
+
for (const item of loaded) {
|
|
506
|
+
const single = await this.executeLoadedItem(item, options, false);
|
|
507
|
+
results.push(single);
|
|
508
|
+
if (failFast && single.status === 'failed')
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (mode === 'parallel') {
|
|
513
|
+
const parallelResults = await this.mapWithConcurrency(loaded, maxParallel, (item) => this.executeLoadedItem(item, options, true));
|
|
514
|
+
results.push(...parallelResults);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
for (let i = 0; i < loaded.length; i += waveSize) {
|
|
518
|
+
const wave = loaded.slice(i, i + waveSize);
|
|
519
|
+
const waveResults = await this.mapWithConcurrency(wave, Math.min(maxParallel, wave.length), (item) => this.executeLoadedItem(item, options, true));
|
|
520
|
+
results.push(...waveResults);
|
|
521
|
+
if (failFast && waveResults.some((r) => r.status === 'failed'))
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const successfulWorkflows = results.filter((r) => r.status === 'success').length;
|
|
526
|
+
const failedWorkflows = results.length - successfulWorkflows;
|
|
527
|
+
return {
|
|
528
|
+
mode,
|
|
529
|
+
totalWorkflows: results.length,
|
|
530
|
+
successfulWorkflows,
|
|
531
|
+
failedWorkflows,
|
|
532
|
+
durationMs: Date.now() - startedAt,
|
|
533
|
+
results,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Preload and validate all workflow inputs before execution starts.
|
|
538
|
+
*/
|
|
539
|
+
async preloadWorkflows(workflows) {
|
|
540
|
+
const loaded = [];
|
|
541
|
+
const loadErrors = [];
|
|
542
|
+
for (let i = 0; i < workflows.length; i++) {
|
|
543
|
+
const source = workflows[i];
|
|
544
|
+
const sourceLabel = typeof source === 'string' ? source : `workflow#${i + 1}`;
|
|
545
|
+
try {
|
|
546
|
+
const parsed = await this.resolveWorkflowInput(source);
|
|
547
|
+
loaded.push({
|
|
548
|
+
source: sourceLabel,
|
|
549
|
+
workflow: parsed,
|
|
550
|
+
declaredMode: this.extractDeclaredExecutionMode(parsed),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
555
|
+
loadErrors.push(`[${sourceLabel}] ${message}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (loadErrors.length > 0) {
|
|
559
|
+
throw new Error(`WORKFLOW_PRELOAD_FAILED:\n${loadErrors.join('\n')}`);
|
|
560
|
+
}
|
|
561
|
+
return loaded;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Resolve batch execution mode with precedence:
|
|
565
|
+
* 1) Explicit API/CLI option
|
|
566
|
+
* 2) Declared workflow strategy modes from YAML
|
|
567
|
+
* 3) Default sequential
|
|
568
|
+
*/
|
|
569
|
+
resolveBatchExecutionMode(loaded, explicitMode) {
|
|
570
|
+
if (explicitMode)
|
|
571
|
+
return explicitMode;
|
|
572
|
+
const declared = loaded
|
|
573
|
+
.map((item) => item.declaredMode)
|
|
574
|
+
.filter((mode) => mode !== undefined);
|
|
575
|
+
if (declared.length === 0)
|
|
576
|
+
return 'sequential';
|
|
577
|
+
const unique = Array.from(new Set(declared));
|
|
578
|
+
if (unique.length === 1)
|
|
579
|
+
return unique[0];
|
|
580
|
+
// Different workflow-level declarations across batch: use mixed orchestration.
|
|
581
|
+
return 'mixed';
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Extract multi-workflow orchestration intent from parsed workflow schema.
|
|
585
|
+
* Accepts strategy.type values in {'sequential','parallel','mixed'}.
|
|
586
|
+
*/
|
|
587
|
+
extractDeclaredExecutionMode(workflow) {
|
|
588
|
+
const raw = workflow.strategy?.type;
|
|
589
|
+
if (!raw)
|
|
590
|
+
return undefined;
|
|
591
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
592
|
+
if (normalized === 'sequential' || normalized === 'parallel' || normalized === 'mixed') {
|
|
593
|
+
return normalized;
|
|
594
|
+
}
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Resolve any accepted workflow input to a parsed workflow and run preflight checks.
|
|
599
|
+
*/
|
|
600
|
+
async resolveWorkflowInput(workflow) {
|
|
427
601
|
let parsedWorkflow;
|
|
428
602
|
if (typeof workflow === 'string') {
|
|
429
|
-
// Try file path first, then YAML/JSON content
|
|
430
603
|
if (WorkflowLoader.looksLikeFilePath(workflow) && existsSync(workflow)) {
|
|
431
604
|
parsedWorkflow = await WorkflowLoader.fromFile(workflow);
|
|
432
605
|
}
|
|
@@ -440,11 +613,63 @@ export class OrbytEngine {
|
|
|
440
613
|
}
|
|
441
614
|
}
|
|
442
615
|
else if (typeof workflow === 'object' && workflow !== null) {
|
|
443
|
-
parsedWorkflow =
|
|
616
|
+
parsedWorkflow = this.isParsedWorkflowInput(workflow)
|
|
617
|
+
? workflow
|
|
618
|
+
: await WorkflowLoader.fromObject(workflow);
|
|
444
619
|
}
|
|
445
620
|
else {
|
|
446
621
|
throw new Error('Invalid workflow input: must be file path, YAML/JSON string, or object');
|
|
447
622
|
}
|
|
623
|
+
this.assertWorkflowVersionSupported(parsedWorkflow);
|
|
624
|
+
this.assertAdapterCapabilities(parsedWorkflow);
|
|
625
|
+
return parsedWorkflow;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Execute a loaded workflow item and capture per-item result envelope.
|
|
629
|
+
*/
|
|
630
|
+
async executeLoadedItem(item, options, isolatedRuntime) {
|
|
631
|
+
const startedAt = Date.now();
|
|
632
|
+
try {
|
|
633
|
+
const result = await this.executeParsedWorkflow(item.workflow, this.asWorkflowRunOptions(options), isolatedRuntime);
|
|
634
|
+
return {
|
|
635
|
+
source: item.source,
|
|
636
|
+
workflowName: result.workflowName,
|
|
637
|
+
status: 'success',
|
|
638
|
+
result,
|
|
639
|
+
durationMs: Date.now() - startedAt,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return {
|
|
644
|
+
source: item.source,
|
|
645
|
+
workflowName: item.workflow.name || item.workflow.metadata?.name,
|
|
646
|
+
status: 'failed',
|
|
647
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
648
|
+
durationMs: Date.now() - startedAt,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Strip batch-only options to build a single-workflow run options object.
|
|
654
|
+
*/
|
|
655
|
+
asWorkflowRunOptions(options) {
|
|
656
|
+
return {
|
|
657
|
+
variables: options.variables,
|
|
658
|
+
env: options.env,
|
|
659
|
+
secrets: options.secrets,
|
|
660
|
+
context: options.context,
|
|
661
|
+
timeout: options.timeout,
|
|
662
|
+
continueOnError: options.continueOnError,
|
|
663
|
+
dryRun: options.dryRun,
|
|
664
|
+
triggeredBy: options.triggeredBy,
|
|
665
|
+
_ownershipContext: options._ownershipContext,
|
|
666
|
+
_permissionPolicy: options._permissionPolicy,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Core single-workflow execution logic used by run() and runMany().
|
|
671
|
+
*/
|
|
672
|
+
async executeParsedWorkflow(parsedWorkflow, options, isolatedRuntime = false) {
|
|
448
673
|
// Inject internal fields after validation
|
|
449
674
|
const sanitizedOptions = options;
|
|
450
675
|
const internalContext = InternalContextBuilder.build(this.version, sanitizedOptions._ownershipContext);
|
|
@@ -455,97 +680,143 @@ export class OrbytEngine {
|
|
|
455
680
|
subscriptionTier: internalContext._ownership.subscriptionTier,
|
|
456
681
|
isBillable: internalContext._billing.isBillable,
|
|
457
682
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
683
|
+
try {
|
|
684
|
+
// Handle dry-run mode
|
|
685
|
+
if (options.dryRun || this.config.mode === 'dry-run') {
|
|
686
|
+
return await this.dryRun(parsedWorkflow, options);
|
|
687
|
+
}
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// INTELLIGENCE LAYERS (Foundation - doesn't change execution yet)
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// 1. Intent Layer: Understand what the workflow is trying to do
|
|
692
|
+
const classifiedIntent = IntentAnalyzer.classify(parsedWorkflow);
|
|
693
|
+
this.log('debug', `Workflow intent: ${classifiedIntent.intent}`, {
|
|
694
|
+
confidence: classifiedIntent.confidence,
|
|
695
|
+
patterns: classifiedIntent.patterns,
|
|
696
|
+
reasoning: classifiedIntent.reasoning,
|
|
697
|
+
});
|
|
698
|
+
// Log intent-based recommendations (for future optimization)
|
|
699
|
+
const recommendations = IntentAnalyzer.getRecommendations(classifiedIntent.intent);
|
|
700
|
+
if (recommendations.optimizations?.length) {
|
|
701
|
+
this.log('debug', 'Intent-based optimizations available', {
|
|
702
|
+
intent: classifiedIntent.intent,
|
|
703
|
+
tips: recommendations.optimizations,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
// 2. Execution Strategy Layer: Decide HOW to run safely
|
|
707
|
+
const strategyContext = {
|
|
708
|
+
workflow: parsedWorkflow,
|
|
478
709
|
intent: classifiedIntent.intent,
|
|
479
|
-
|
|
710
|
+
resourceLimits: {
|
|
711
|
+
maxConcurrentSteps: this.config.maxConcurrentSteps || 10,
|
|
712
|
+
maxMemory: 0,
|
|
713
|
+
timeout: sanitizedOptions.timeout || this.config.defaultTimeout || 300000,
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
const executionStrategy = ExecutionStrategyResolver.resolve(strategyContext);
|
|
717
|
+
this.log('debug', `Execution strategy: ${executionStrategy.strategy}`, {
|
|
718
|
+
reason: executionStrategy.reason,
|
|
719
|
+
adjustments: executionStrategy.adjustments,
|
|
720
|
+
});
|
|
721
|
+
const strategyName = executionStrategy.strategy;
|
|
722
|
+
const mappedStrategy = strategyName === 'parallel' ? 'parallel'
|
|
723
|
+
: strategyName === 'mixed' ? 'mixed'
|
|
724
|
+
: 'sequential';
|
|
725
|
+
LoggerManager.patchWorkflowContext({ executionStrategy: mappedStrategy });
|
|
726
|
+
// 3. Safety Guard: Check if safe to execute
|
|
727
|
+
const safetyCheck = ExecutionStrategyGuard.isSafeToExecute(strategyContext);
|
|
728
|
+
if (!safetyCheck.safe) {
|
|
729
|
+
this.log('warn', `Execution safety check failed: ${safetyCheck.reason}`);
|
|
730
|
+
}
|
|
731
|
+
// ============================================================================
|
|
732
|
+
// END INTELLIGENCE LAYERS
|
|
733
|
+
// ============================================================================
|
|
734
|
+
const execOptions = {
|
|
735
|
+
timeout: sanitizedOptions.timeout || this.config.defaultTimeout,
|
|
736
|
+
env: sanitizedOptions.env,
|
|
737
|
+
inputs: sanitizedOptions.variables,
|
|
738
|
+
secrets: sanitizedOptions.secrets,
|
|
739
|
+
context: {
|
|
740
|
+
...sanitizedOptions.context,
|
|
741
|
+
_internal: internalContext,
|
|
742
|
+
},
|
|
743
|
+
continueOnError: sanitizedOptions.continueOnError,
|
|
744
|
+
triggeredBy: sanitizedOptions.triggeredBy || 'manual',
|
|
745
|
+
};
|
|
746
|
+
this.log('info', `Running workflow: ${parsedWorkflow.name || 'unnamed'}`);
|
|
747
|
+
this.workflowStore.save(parsedWorkflow);
|
|
748
|
+
// Parallel/mixed mode can execute multiple workflows concurrently via runMany().
|
|
749
|
+
// In that case, use per-execution runtime instances to prevent shared mutable
|
|
750
|
+
// state collisions (executionId/context/step runtime).
|
|
751
|
+
const runtime = isolatedRuntime
|
|
752
|
+
? this.createIsolatedExecutionRuntime()
|
|
753
|
+
: {
|
|
754
|
+
stepExecutor: this.stepExecutor,
|
|
755
|
+
workflowExecutor: this.workflowExecutor,
|
|
756
|
+
};
|
|
757
|
+
const result = await this.measureWorkflowExecution(parsedWorkflow.name || 'unnamed', () => runtime.workflowExecutor.execute(parsedWorkflow, execOptions));
|
|
758
|
+
internalContext._usage.stepCount = result.metadata.totalSteps;
|
|
759
|
+
internalContext._usage.durationSeconds = result.duration / 1000;
|
|
760
|
+
internalContext._usage.weightedStepCount = result.metadata.totalSteps;
|
|
761
|
+
this.log('info', `Workflow completed: ${result.status}`, {
|
|
762
|
+
durationMs: result.duration,
|
|
763
|
+
steps: result.metadata.totalSteps,
|
|
764
|
+
billable: internalContext._billing.isBillable,
|
|
765
|
+
automationCount: internalContext._usage.automationCount,
|
|
766
|
+
stepCount: internalContext._usage.stepCount,
|
|
480
767
|
});
|
|
768
|
+
await this.onWorkflowBillingComplete(internalContext, result);
|
|
769
|
+
return result;
|
|
481
770
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
771
|
+
finally {
|
|
772
|
+
LoggerManager.clearWorkflowContext();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Create isolated per-workflow execution runtime.
|
|
777
|
+
*
|
|
778
|
+
* This is used by runMany parallel/mixed modes so each workflow run has its
|
|
779
|
+
* own mutable executor state while preserving shared adapter capabilities.
|
|
780
|
+
*/
|
|
781
|
+
createIsolatedExecutionRuntime() {
|
|
782
|
+
const stepExecutor = new StepExecutor();
|
|
783
|
+
const localRegistry = new AdapterRegistry();
|
|
784
|
+
for (const adapter of this.adapterRegistry.getAll()) {
|
|
785
|
+
localRegistry.register(adapter);
|
|
786
|
+
}
|
|
787
|
+
stepExecutor.setAdapterRegistry(localRegistry);
|
|
788
|
+
stepExecutor.setEventBus(this.eventBus);
|
|
789
|
+
stepExecutor.setHookManager(this.hookManager);
|
|
790
|
+
if (this.config.retryPolicy) {
|
|
791
|
+
stepExecutor.setRetryPolicy(this.config.retryPolicy);
|
|
792
|
+
}
|
|
793
|
+
if (this.config.timeoutManager) {
|
|
794
|
+
stepExecutor.setTimeoutManager(this.config.timeoutManager);
|
|
795
|
+
}
|
|
796
|
+
const workflowExecutor = new WorkflowExecutor(stepExecutor);
|
|
797
|
+
workflowExecutor.setEventBus(this.eventBus);
|
|
798
|
+
workflowExecutor.setHookManager(this.hookManager);
|
|
799
|
+
workflowExecutor.setStateDir(join(this.config.stateDir, 'executions'));
|
|
800
|
+
return { stepExecutor, workflowExecutor };
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Concurrency-limited mapper preserving input order.
|
|
804
|
+
*/
|
|
805
|
+
async mapWithConcurrency(items, concurrency, mapper) {
|
|
806
|
+
const results = new Array(items.length);
|
|
807
|
+
let nextIndex = 0;
|
|
808
|
+
const worker = async () => {
|
|
809
|
+
while (true) {
|
|
810
|
+
const current = nextIndex;
|
|
811
|
+
nextIndex += 1;
|
|
812
|
+
if (current >= items.length)
|
|
813
|
+
return;
|
|
814
|
+
results[current] = await mapper(items[current], current);
|
|
815
|
+
}
|
|
526
816
|
};
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// Execute workflow with performance measurement
|
|
531
|
-
const result = await this.measureWorkflowExecution(parsedWorkflow.name || 'unnamed', () => this.workflowExecutor.execute(parsedWorkflow, execOptions));
|
|
532
|
-
// Update usage counters after execution
|
|
533
|
-
internalContext._usage.stepCount = result.metadata.totalSteps;
|
|
534
|
-
internalContext._usage.durationSeconds = result.duration / 1000;
|
|
535
|
-
// Calculate weighted step count (future: based on actual step weights)
|
|
536
|
-
internalContext._usage.weightedStepCount = result.metadata.totalSteps;
|
|
537
|
-
this.log('info', `Workflow completed: ${result.status}`, {
|
|
538
|
-
duration: result.duration,
|
|
539
|
-
steps: result.metadata.totalSteps,
|
|
540
|
-
billable: internalContext._billing.isBillable,
|
|
541
|
-
automationCount: internalContext._usage.automationCount,
|
|
542
|
-
stepCount: internalContext._usage.stepCount,
|
|
543
|
-
});
|
|
544
|
-
// Call billing lifecycle hook
|
|
545
|
-
await this.onWorkflowBillingComplete(internalContext, result);
|
|
546
|
-
// Clear workflow context so it doesn't bleed into subsequent executions
|
|
547
|
-
LoggerManager.clearWorkflowContext();
|
|
548
|
-
return result;
|
|
817
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
818
|
+
await Promise.all(workers);
|
|
819
|
+
return results;
|
|
549
820
|
}
|
|
550
821
|
/**
|
|
551
822
|
* Billing lifecycle hook - called after workflow completes
|
|
@@ -592,7 +863,8 @@ export class OrbytEngine {
|
|
|
592
863
|
*/
|
|
593
864
|
async validate(workflow, options) {
|
|
594
865
|
// Use WorkflowLoader.validate to check workflow validity, passing logger if present
|
|
595
|
-
await WorkflowLoader.validate(workflow, options?.logger);
|
|
866
|
+
const parsed = await WorkflowLoader.validate(workflow, options?.logger);
|
|
867
|
+
this.assertWorkflowVersionSupported(parsed);
|
|
596
868
|
// Context was set in WorkflowLoader — clear it now that validation is done
|
|
597
869
|
LoggerManager.clearWorkflowContext();
|
|
598
870
|
return true;
|
|
@@ -621,6 +893,7 @@ export class OrbytEngine {
|
|
|
621
893
|
else {
|
|
622
894
|
parsedWorkflow = workflow;
|
|
623
895
|
}
|
|
896
|
+
this.assertWorkflowVersionSupported(parsedWorkflow);
|
|
624
897
|
// Generate explanation
|
|
625
898
|
const explanation = ExplanationGenerator.generate(parsedWorkflow);
|
|
626
899
|
// Log explanation for CLI visibility
|
|
@@ -680,7 +953,7 @@ export class OrbytEngine {
|
|
|
680
953
|
* @param adapter - Adapter to register
|
|
681
954
|
*/
|
|
682
955
|
registerAdapter(adapter) {
|
|
683
|
-
|
|
956
|
+
// adapterRegistry is shared with stepExecutor — one registration, one log.
|
|
684
957
|
this.stepExecutor.registerModernAdapter(adapter);
|
|
685
958
|
this.log('debug', `Registered adapter: ${adapter.name}`);
|
|
686
959
|
}
|
|
@@ -755,6 +1028,36 @@ export class OrbytEngine {
|
|
|
755
1028
|
getScheduleStore() {
|
|
756
1029
|
return this.scheduleStore;
|
|
757
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get parsed workflow cache for diagnostics and tooling integration.
|
|
1033
|
+
*/
|
|
1034
|
+
getWorkflowParseCache() {
|
|
1035
|
+
return this.workflowParseCache;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get adapter metadata cache for diagnostics and tooling integration.
|
|
1039
|
+
*/
|
|
1040
|
+
getAdapterMetadataCache() {
|
|
1041
|
+
return this.adapterMetadataCache;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Get runtime artifact store (dag/context/locks) for diagnostics and tooling.
|
|
1045
|
+
*/
|
|
1046
|
+
getRuntimeArtifactStore() {
|
|
1047
|
+
return this.runtimeArtifactStore;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get adapter registry statistics for diagnostics and CLI health checks.
|
|
1051
|
+
*/
|
|
1052
|
+
getAdapterStats() {
|
|
1053
|
+
return this.adapterRegistry.getStats();
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get engine runtime version.
|
|
1057
|
+
*/
|
|
1058
|
+
getVersion() {
|
|
1059
|
+
return this.version;
|
|
1060
|
+
}
|
|
758
1061
|
/**
|
|
759
1062
|
* Internal logging method
|
|
760
1063
|
*
|
|
@@ -800,5 +1103,61 @@ export class OrbytEngine {
|
|
|
800
1103
|
// - error: workflow takes longer than 5 minutes
|
|
801
1104
|
return this.logger.measureExecution(`Workflow "${workflowName}"`, fn, { warn: 30000, error: 300000 });
|
|
802
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Enforce runtime compatibility for workflow DSL version.
|
|
1108
|
+
*/
|
|
1109
|
+
assertWorkflowVersionSupported(workflow) {
|
|
1110
|
+
const raw = String(workflow.version || '').trim();
|
|
1111
|
+
const match = raw.match(/^v?(\d+)(?:\.\d+){0,2}$/);
|
|
1112
|
+
if (!match) {
|
|
1113
|
+
throw new Error(`UNSUPPORTED_WORKFLOW_VERSION: Invalid workflow version format "${raw}". ` +
|
|
1114
|
+
'Expected semantic format like "1.0" or "1.0.0".');
|
|
1115
|
+
}
|
|
1116
|
+
const major = parseInt(match[1], 10);
|
|
1117
|
+
if (major !== OrbytEngine.SUPPORTED_WORKFLOW_MAJOR) {
|
|
1118
|
+
throw new Error(`UNSUPPORTED_WORKFLOW_VERSION: Workflow version ${raw} is not supported. ` +
|
|
1119
|
+
`Supported versions: ${OrbytEngine.SUPPORTED_WORKFLOW_MAJOR}.x`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Preflight adapter/action capability checks before execution.
|
|
1124
|
+
*/
|
|
1125
|
+
assertAdapterCapabilities(workflow) {
|
|
1126
|
+
const failures = [];
|
|
1127
|
+
for (const step of workflow.steps) {
|
|
1128
|
+
const action = String(step.action || '').trim();
|
|
1129
|
+
const namespace = action.split('.')[0];
|
|
1130
|
+
if (!action || !namespace) {
|
|
1131
|
+
failures.push(`step "${step.id}": invalid action "${action}" (expected namespace.action)`);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
const adapter = this.adapterRegistry.get(namespace);
|
|
1135
|
+
if (!adapter) {
|
|
1136
|
+
failures.push(`step "${step.id}": adapter "${namespace}" is not registered (action: ${action})`);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (!adapter.supports(action)) {
|
|
1140
|
+
failures.push(`step "${step.id}": action "${action}" is not supported by adapter "${namespace}"`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (failures.length > 0) {
|
|
1144
|
+
throw new Error(`ADAPTER_ACTION_NOT_FOUND:\n${failures.join('\n')}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Detect already-parsed workflow objects passed by trusted callers (CLI/API).
|
|
1149
|
+
*/
|
|
1150
|
+
isParsedWorkflowInput(value) {
|
|
1151
|
+
if (!value || typeof value !== 'object')
|
|
1152
|
+
return false;
|
|
1153
|
+
const candidate = value;
|
|
1154
|
+
return (typeof candidate.version === 'string' &&
|
|
1155
|
+
typeof candidate.kind === 'string' &&
|
|
1156
|
+
Array.isArray(candidate.steps) &&
|
|
1157
|
+
candidate.steps.every((step) => step &&
|
|
1158
|
+
typeof step.id === 'string' &&
|
|
1159
|
+
typeof step.action === 'string' &&
|
|
1160
|
+
typeof step.adapter === 'string'));
|
|
1161
|
+
}
|
|
803
1162
|
}
|
|
804
1163
|
//# sourceMappingURL=OrbytEngine.js.map
|