@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.
Files changed (79) hide show
  1. package/dist/cache/AdapterMetadataCache.d.ts +22 -0
  2. package/dist/cache/AdapterMetadataCache.d.ts.map +1 -0
  3. package/dist/cache/AdapterMetadataCache.js +66 -0
  4. package/dist/cache/AdapterMetadataCache.js.map +1 -0
  5. package/dist/cache/WorkflowParseCache.d.ts +26 -0
  6. package/dist/cache/WorkflowParseCache.d.ts.map +1 -0
  7. package/dist/cache/WorkflowParseCache.js +91 -0
  8. package/dist/cache/WorkflowParseCache.js.map +1 -0
  9. package/dist/cache/index.d.ts +3 -0
  10. package/dist/cache/index.d.ts.map +1 -0
  11. package/dist/cache/index.js +3 -0
  12. package/dist/cache/index.js.map +1 -0
  13. package/dist/core/EngineConfig.d.ts.map +1 -1
  14. package/dist/core/EngineConfig.js +8 -2
  15. package/dist/core/EngineConfig.js.map +1 -1
  16. package/dist/core/OrbytEngine.d.ts +104 -1
  17. package/dist/core/OrbytEngine.d.ts.map +1 -1
  18. package/dist/core/OrbytEngine.js +454 -95
  19. package/dist/core/OrbytEngine.js.map +1 -1
  20. package/dist/execution/StepExecutor.d.ts +8 -0
  21. package/dist/execution/StepExecutor.d.ts.map +1 -1
  22. package/dist/execution/StepExecutor.js +32 -3
  23. package/dist/execution/StepExecutor.js.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/logging/EngineLogger.js +4 -4
  29. package/dist/logging/EngineLogger.js.map +1 -1
  30. package/dist/parser/WorkflowParser.d.ts.map +1 -1
  31. package/dist/parser/WorkflowParser.js +2 -0
  32. package/dist/parser/WorkflowParser.js.map +1 -1
  33. package/dist/runtime/RuntimeArtifactStore.d.ts +27 -0
  34. package/dist/runtime/RuntimeArtifactStore.d.ts.map +1 -0
  35. package/dist/runtime/RuntimeArtifactStore.js +96 -0
  36. package/dist/runtime/RuntimeArtifactStore.js.map +1 -0
  37. package/dist/runtime/index.d.ts +2 -0
  38. package/dist/runtime/index.d.ts.map +1 -0
  39. package/dist/runtime/index.js +2 -0
  40. package/dist/runtime/index.js.map +1 -0
  41. package/dist/scheduling/Scheduler.d.ts.map +1 -1
  42. package/dist/scheduling/Scheduler.js +6 -4
  43. package/dist/scheduling/Scheduler.js.map +1 -1
  44. package/dist/security/ResourceValidator.d.ts +20 -0
  45. package/dist/security/ResourceValidator.d.ts.map +1 -0
  46. package/dist/security/ResourceValidator.js +107 -0
  47. package/dist/security/ResourceValidator.js.map +1 -0
  48. package/dist/security/index.d.ts +1 -0
  49. package/dist/security/index.d.ts.map +1 -1
  50. package/dist/security/index.js +1 -0
  51. package/dist/security/index.js.map +1 -1
  52. package/dist/storage/ExecutionStore.d.ts +2 -2
  53. package/dist/storage/ExecutionStore.d.ts.map +1 -1
  54. package/dist/storage/ExecutionStore.js +16 -25
  55. package/dist/storage/ExecutionStore.js.map +1 -1
  56. package/dist/storage/FileStorageAdapter.d.ts +23 -0
  57. package/dist/storage/FileStorageAdapter.d.ts.map +1 -0
  58. package/dist/storage/FileStorageAdapter.js +66 -0
  59. package/dist/storage/FileStorageAdapter.js.map +1 -0
  60. package/dist/storage/ScheduleStore.d.ts +2 -2
  61. package/dist/storage/ScheduleStore.d.ts.map +1 -1
  62. package/dist/storage/ScheduleStore.js +19 -27
  63. package/dist/storage/ScheduleStore.js.map +1 -1
  64. package/dist/storage/StorageAdapter.d.ts +22 -0
  65. package/dist/storage/StorageAdapter.d.ts.map +1 -0
  66. package/dist/storage/StorageAdapter.js +2 -0
  67. package/dist/storage/StorageAdapter.js.map +1 -0
  68. package/dist/storage/WorkflowStore.d.ts +2 -1
  69. package/dist/storage/WorkflowStore.d.ts.map +1 -1
  70. package/dist/storage/WorkflowStore.js +24 -27
  71. package/dist/storage/WorkflowStore.js.map +1 -1
  72. package/dist/storage/index.d.ts +2 -0
  73. package/dist/storage/index.d.ts.map +1 -1
  74. package/dist/storage/index.js +2 -0
  75. package/dist/storage/index.js.map +1 -1
  76. package/dist/types/core-types.d.ts +89 -0
  77. package/dist/types/core-types.d.ts.map +1 -1
  78. package/dist/types/core-types.js.map +1 -1
  79. package/package.json +1 -1
@@ -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(process.cwd(), '.orbyt');
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
- // Accept string (YAML/JSON), file path, or object. Always validate/parse via loader.
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 = await WorkflowLoader.fromObject(workflow);
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
- // Handle dry-run mode
459
- if (options.dryRun || this.config.mode === 'dry-run') {
460
- const dryResult = await this.dryRun(parsedWorkflow, options);
461
- LoggerManager.clearWorkflowContext();
462
- return dryResult;
463
- }
464
- // ============================================================================
465
- // INTELLIGENCE LAYERS (Foundation - doesn't change execution yet)
466
- // ============================================================================
467
- // 1. Intent Layer: Understand what the workflow is trying to do
468
- const classifiedIntent = IntentAnalyzer.classify(parsedWorkflow);
469
- this.log('debug', `Workflow intent: ${classifiedIntent.intent}`, {
470
- confidence: classifiedIntent.confidence,
471
- patterns: classifiedIntent.patterns,
472
- reasoning: classifiedIntent.reasoning,
473
- });
474
- // Log intent-based recommendations (for future optimization)
475
- const recommendations = IntentAnalyzer.getRecommendations(classifiedIntent.intent);
476
- if (recommendations.optimizations?.length) {
477
- this.log('debug', 'Intent-based optimizations available', {
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
- tips: recommendations.optimizations,
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
- // 2. Execution Strategy Layer: Decide HOW to run safely
483
- const strategyContext = {
484
- workflow: parsedWorkflow,
485
- intent: classifiedIntent.intent,
486
- resourceLimits: {
487
- maxConcurrentSteps: this.config.maxConcurrentSteps || 10,
488
- maxMemory: 0,
489
- timeout: sanitizedOptions.timeout || this.config.defaultTimeout || 300000,
490
- },
491
- };
492
- const executionStrategy = ExecutionStrategyResolver.resolve(strategyContext);
493
- this.log('debug', `Execution strategy: ${executionStrategy.strategy}`, {
494
- reason: executionStrategy.reason,
495
- adjustments: executionStrategy.adjustments,
496
- });
497
- // Enrich the active workflow context with the resolved execution strategy
498
- // so all subsequent logs carry it without callers needing to pass it manually.
499
- const strategyName = executionStrategy.strategy;
500
- const mappedStrategy = strategyName === 'parallel' ? 'parallel'
501
- : strategyName === 'mixed' ? 'mixed'
502
- : 'sequential'; // 'sequential' | 'conservative' | any unknown → sequential
503
- LoggerManager.patchWorkflowContext({ executionStrategy: mappedStrategy });
504
- // 3. Safety Guard: Check if safe to execute
505
- const safetyCheck = ExecutionStrategyGuard.isSafeToExecute(strategyContext);
506
- if (!safetyCheck.safe) {
507
- this.log('warn', `Execution safety check failed: ${safetyCheck.reason}`);
508
- // Foundation: Log only, don't block execution
509
- // Future: Can block or delay execution based on policy
510
- }
511
- // ============================================================================
512
- // END INTELLIGENCE LAYERS
513
- // ============================================================================
514
- // Build execution options
515
- const execOptions = {
516
- timeout: sanitizedOptions.timeout || this.config.defaultTimeout,
517
- env: sanitizedOptions.env,
518
- inputs: sanitizedOptions.variables,
519
- secrets: sanitizedOptions.secrets,
520
- context: {
521
- ...sanitizedOptions.context,
522
- _internal: internalContext, // Inject internal context (engine-only)
523
- },
524
- continueOnError: sanitizedOptions.continueOnError,
525
- triggeredBy: sanitizedOptions.triggeredBy || 'manual',
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
- this.log('info', `Running workflow: ${parsedWorkflow.name || 'unnamed'}`);
528
- // Persist workflow definition for replay before executing
529
- this.workflowStore.save(parsedWorkflow);
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
- this.adapterRegistry.register(adapter);
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