@orbytautomation/engine 0.7.1 → 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 +2 -0
- 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 +452 -94
- 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,14 +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
89
|
import { homedir } from 'node:os';
|
|
90
90
|
import { ExecutionStore } from '../storage/ExecutionStore.js';
|
|
91
91
|
import { WorkflowStore } from '../storage/WorkflowStore.js';
|
|
92
92
|
import { ScheduleStore } from '../storage/ScheduleStore.js';
|
|
93
|
+
import { WorkflowParseCache, AdapterMetadataCache } from '../cache/index.js';
|
|
94
|
+
import { RuntimeArtifactStore } from '../runtime/index.js';
|
|
93
95
|
/**
|
|
94
96
|
* OrbytEngine - Main public API
|
|
95
97
|
*
|
|
@@ -214,6 +216,7 @@ import { ScheduleStore } from '../storage/ScheduleStore.js';
|
|
|
214
216
|
* ```
|
|
215
217
|
*/
|
|
216
218
|
export class OrbytEngine {
|
|
219
|
+
static SUPPORTED_WORKFLOW_MAJOR = 1;
|
|
217
220
|
config;
|
|
218
221
|
executionEngine;
|
|
219
222
|
stepExecutor;
|
|
@@ -228,6 +231,9 @@ export class OrbytEngine {
|
|
|
228
231
|
executionStore;
|
|
229
232
|
workflowStore;
|
|
230
233
|
scheduleStore;
|
|
234
|
+
workflowParseCache;
|
|
235
|
+
adapterMetadataCache;
|
|
236
|
+
runtimeArtifactStore;
|
|
231
237
|
constructor(config = {}) {
|
|
232
238
|
// Validate and apply defaults
|
|
233
239
|
validateConfig(config);
|
|
@@ -260,6 +266,9 @@ export class OrbytEngine {
|
|
|
260
266
|
this.adapterRegistry = new AdapterRegistry();
|
|
261
267
|
// Initialize executors
|
|
262
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);
|
|
263
272
|
this.workflowExecutor = new WorkflowExecutor(this.stepExecutor);
|
|
264
273
|
// Initialize execution engine
|
|
265
274
|
this.executionEngine = new ExecutionEngine({
|
|
@@ -272,11 +281,17 @@ export class OrbytEngine {
|
|
|
272
281
|
});
|
|
273
282
|
// Wire components together
|
|
274
283
|
this.setupComponents();
|
|
284
|
+
// Ensure infrastructure directories exist before any persistence/caching.
|
|
285
|
+
this.bootstrapRuntimeDirectories();
|
|
275
286
|
// Initialise persistent stores (non-fatal — must never block engine startup)
|
|
276
287
|
const storeRoot = this.config.stateDir ?? join(homedir(), '.orbyt');
|
|
277
288
|
this.executionStore = new ExecutionStore(join(storeRoot, 'executions'));
|
|
278
289
|
this.workflowStore = new WorkflowStore(join(storeRoot, 'workflows'));
|
|
279
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();
|
|
280
295
|
this.workflowExecutor.setStateDir(join(storeRoot, 'executions'));
|
|
281
296
|
// Register built-in adapters
|
|
282
297
|
this.registerBuiltinAdapters();
|
|
@@ -335,6 +350,41 @@ export class OrbytEngine {
|
|
|
335
350
|
this.stepExecutor.setTimeoutManager(this.config.timeoutManager);
|
|
336
351
|
}
|
|
337
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
|
+
}
|
|
338
388
|
/**
|
|
339
389
|
* Register built-in adapters
|
|
340
390
|
* These are the core adapters shipped with Orbyt
|
|
@@ -424,10 +474,132 @@ export class OrbytEngine {
|
|
|
424
474
|
if (!this.isStarted && this.config.enableScheduler) {
|
|
425
475
|
await this.start();
|
|
426
476
|
}
|
|
427
|
-
|
|
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) {
|
|
428
601
|
let parsedWorkflow;
|
|
429
602
|
if (typeof workflow === 'string') {
|
|
430
|
-
// Try file path first, then YAML/JSON content
|
|
431
603
|
if (WorkflowLoader.looksLikeFilePath(workflow) && existsSync(workflow)) {
|
|
432
604
|
parsedWorkflow = await WorkflowLoader.fromFile(workflow);
|
|
433
605
|
}
|
|
@@ -441,11 +613,63 @@ export class OrbytEngine {
|
|
|
441
613
|
}
|
|
442
614
|
}
|
|
443
615
|
else if (typeof workflow === 'object' && workflow !== null) {
|
|
444
|
-
parsedWorkflow =
|
|
616
|
+
parsedWorkflow = this.isParsedWorkflowInput(workflow)
|
|
617
|
+
? workflow
|
|
618
|
+
: await WorkflowLoader.fromObject(workflow);
|
|
445
619
|
}
|
|
446
620
|
else {
|
|
447
621
|
throw new Error('Invalid workflow input: must be file path, YAML/JSON string, or object');
|
|
448
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) {
|
|
449
673
|
// Inject internal fields after validation
|
|
450
674
|
const sanitizedOptions = options;
|
|
451
675
|
const internalContext = InternalContextBuilder.build(this.version, sanitizedOptions._ownershipContext);
|
|
@@ -456,97 +680,143 @@ export class OrbytEngine {
|
|
|
456
680
|
subscriptionTier: internalContext._ownership.subscriptionTier,
|
|
457
681
|
isBillable: internalContext._billing.isBillable,
|
|
458
682
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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,
|
|
479
709
|
intent: classifiedIntent.intent,
|
|
480
|
-
|
|
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,
|
|
481
767
|
});
|
|
768
|
+
await this.onWorkflowBillingComplete(internalContext, result);
|
|
769
|
+
return result;
|
|
482
770
|
}
|
|
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
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
816
|
};
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
// Execute workflow with performance measurement
|
|
532
|
-
const result = await this.measureWorkflowExecution(parsedWorkflow.name || 'unnamed', () => this.workflowExecutor.execute(parsedWorkflow, execOptions));
|
|
533
|
-
// Update usage counters after execution
|
|
534
|
-
internalContext._usage.stepCount = result.metadata.totalSteps;
|
|
535
|
-
internalContext._usage.durationSeconds = result.duration / 1000;
|
|
536
|
-
// Calculate weighted step count (future: based on actual step weights)
|
|
537
|
-
internalContext._usage.weightedStepCount = result.metadata.totalSteps;
|
|
538
|
-
this.log('info', `Workflow completed: ${result.status}`, {
|
|
539
|
-
duration: result.duration,
|
|
540
|
-
steps: result.metadata.totalSteps,
|
|
541
|
-
billable: internalContext._billing.isBillable,
|
|
542
|
-
automationCount: internalContext._usage.automationCount,
|
|
543
|
-
stepCount: internalContext._usage.stepCount,
|
|
544
|
-
});
|
|
545
|
-
// Call billing lifecycle hook
|
|
546
|
-
await this.onWorkflowBillingComplete(internalContext, result);
|
|
547
|
-
// Clear workflow context so it doesn't bleed into subsequent executions
|
|
548
|
-
LoggerManager.clearWorkflowContext();
|
|
549
|
-
return result;
|
|
817
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
818
|
+
await Promise.all(workers);
|
|
819
|
+
return results;
|
|
550
820
|
}
|
|
551
821
|
/**
|
|
552
822
|
* Billing lifecycle hook - called after workflow completes
|
|
@@ -593,7 +863,8 @@ export class OrbytEngine {
|
|
|
593
863
|
*/
|
|
594
864
|
async validate(workflow, options) {
|
|
595
865
|
// Use WorkflowLoader.validate to check workflow validity, passing logger if present
|
|
596
|
-
await WorkflowLoader.validate(workflow, options?.logger);
|
|
866
|
+
const parsed = await WorkflowLoader.validate(workflow, options?.logger);
|
|
867
|
+
this.assertWorkflowVersionSupported(parsed);
|
|
597
868
|
// Context was set in WorkflowLoader — clear it now that validation is done
|
|
598
869
|
LoggerManager.clearWorkflowContext();
|
|
599
870
|
return true;
|
|
@@ -622,6 +893,7 @@ export class OrbytEngine {
|
|
|
622
893
|
else {
|
|
623
894
|
parsedWorkflow = workflow;
|
|
624
895
|
}
|
|
896
|
+
this.assertWorkflowVersionSupported(parsedWorkflow);
|
|
625
897
|
// Generate explanation
|
|
626
898
|
const explanation = ExplanationGenerator.generate(parsedWorkflow);
|
|
627
899
|
// Log explanation for CLI visibility
|
|
@@ -681,7 +953,7 @@ export class OrbytEngine {
|
|
|
681
953
|
* @param adapter - Adapter to register
|
|
682
954
|
*/
|
|
683
955
|
registerAdapter(adapter) {
|
|
684
|
-
|
|
956
|
+
// adapterRegistry is shared with stepExecutor — one registration, one log.
|
|
685
957
|
this.stepExecutor.registerModernAdapter(adapter);
|
|
686
958
|
this.log('debug', `Registered adapter: ${adapter.name}`);
|
|
687
959
|
}
|
|
@@ -756,6 +1028,36 @@ export class OrbytEngine {
|
|
|
756
1028
|
getScheduleStore() {
|
|
757
1029
|
return this.scheduleStore;
|
|
758
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
|
+
}
|
|
759
1061
|
/**
|
|
760
1062
|
* Internal logging method
|
|
761
1063
|
*
|
|
@@ -801,5 +1103,61 @@ export class OrbytEngine {
|
|
|
801
1103
|
// - error: workflow takes longer than 5 minutes
|
|
802
1104
|
return this.logger.measureExecution(`Workflow "${workflowName}"`, fn, { warn: 30000, error: 300000 });
|
|
803
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
|
+
}
|
|
804
1162
|
}
|
|
805
1163
|
//# sourceMappingURL=OrbytEngine.js.map
|