@orbytautomation/engine 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -108
- package/dist/core/EngineConfig.d.ts +1 -1
- package/dist/core/EngineConfig.d.ts.map +1 -1
- package/dist/core/EngineConfig.js +55 -0
- package/dist/core/EngineConfig.js.map +1 -1
- package/dist/core/OrbytEngine.d.ts +27 -1
- package/dist/core/OrbytEngine.d.ts.map +1 -1
- package/dist/core/OrbytEngine.js +471 -5
- package/dist/core/OrbytEngine.js.map +1 -1
- package/dist/distributed/DistributedStepWorker.d.ts +40 -0
- package/dist/distributed/DistributedStepWorker.d.ts.map +1 -0
- package/dist/distributed/DistributedStepWorker.js +96 -0
- package/dist/distributed/DistributedStepWorker.js.map +1 -0
- package/dist/distributed/DistributedWorkflowOrchestrator.d.ts +51 -0
- package/dist/distributed/DistributedWorkflowOrchestrator.d.ts.map +1 -0
- package/dist/distributed/DistributedWorkflowOrchestrator.js +430 -0
- package/dist/distributed/DistributedWorkflowOrchestrator.js.map +1 -0
- package/dist/distributed/FileDistributedJobQueue.d.ts +29 -0
- package/dist/distributed/FileDistributedJobQueue.d.ts.map +1 -0
- package/dist/distributed/FileDistributedJobQueue.js +170 -0
- package/dist/distributed/FileDistributedJobQueue.js.map +1 -0
- package/dist/distributed/InMemoryDistributedJobQueue.d.ts +26 -0
- package/dist/distributed/InMemoryDistributedJobQueue.d.ts.map +1 -0
- package/dist/distributed/InMemoryDistributedJobQueue.js +130 -0
- package/dist/distributed/InMemoryDistributedJobQueue.js.map +1 -0
- package/dist/distributed/index.d.ts +5 -0
- package/dist/distributed/index.d.ts.map +1 -0
- package/dist/distributed/index.js +5 -0
- package/dist/distributed/index.js.map +1 -0
- package/dist/errors/FieldRegistry.d.ts +6 -2
- package/dist/errors/FieldRegistry.d.ts.map +1 -1
- package/dist/errors/FieldRegistry.js +11 -0
- package/dist/errors/FieldRegistry.js.map +1 -1
- package/dist/execution/ExecutionEngine.d.ts.map +1 -1
- package/dist/execution/ExecutionEngine.js +2 -1
- package/dist/execution/ExecutionEngine.js.map +1 -1
- package/dist/execution/InternalExecutionContext.d.ts.map +1 -1
- package/dist/execution/InternalExecutionContext.js +3 -1
- package/dist/execution/InternalExecutionContext.js.map +1 -1
- package/dist/execution/WorkflowExecutor.d.ts +5 -0
- package/dist/execution/WorkflowExecutor.d.ts.map +1 -1
- package/dist/execution/WorkflowExecutor.js +195 -7
- package/dist/execution/WorkflowExecutor.js.map +1 -1
- package/dist/explanation/ExplanationGenerator.d.ts.map +1 -1
- package/dist/explanation/ExplanationGenerator.js +6 -0
- package/dist/explanation/ExplanationGenerator.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser/SchemaValidator.d.ts +13 -0
- package/dist/parser/SchemaValidator.d.ts.map +1 -1
- package/dist/parser/SchemaValidator.js +175 -1
- package/dist/parser/SchemaValidator.js.map +1 -1
- package/dist/parser/WorkflowParser.d.ts +5 -0
- package/dist/parser/WorkflowParser.d.ts.map +1 -1
- package/dist/parser/WorkflowParser.js +20 -0
- package/dist/parser/WorkflowParser.js.map +1 -1
- package/dist/scheduling/JobScheduler.d.ts +12 -0
- package/dist/scheduling/JobScheduler.d.ts.map +1 -1
- package/dist/scheduling/JobScheduler.js +136 -20
- package/dist/scheduling/JobScheduler.js.map +1 -1
- package/dist/scheduling/Scheduler.d.ts +3 -0
- package/dist/scheduling/Scheduler.d.ts.map +1 -1
- package/dist/scheduling/Scheduler.js +3 -0
- package/dist/scheduling/Scheduler.js.map +1 -1
- package/dist/scheduling/workers/workflow-worker.js +59 -3
- package/dist/scheduling/workers/workflow-worker.js.map +1 -1
- package/dist/storage/CheckpointStore.d.ts +59 -0
- package/dist/storage/CheckpointStore.d.ts.map +1 -0
- package/dist/storage/CheckpointStore.js +62 -0
- package/dist/storage/CheckpointStore.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/testing/integration/distributed/distributed-smoke.d.ts +3 -0
- package/dist/testing/integration/distributed/distributed-smoke.d.ts.map +1 -0
- package/dist/testing/integration/distributed/distributed-smoke.js +80 -0
- package/dist/testing/integration/distributed/distributed-smoke.js.map +1 -0
- package/dist/types/core-types.d.ts +278 -1
- package/dist/types/core-types.d.ts.map +1 -1
- package/dist/types/core-types.js.map +1 -1
- package/dist/usage/FileSpoolUsageCollector.d.ts +74 -0
- package/dist/usage/FileSpoolUsageCollector.d.ts.map +1 -0
- package/dist/usage/FileSpoolUsageCollector.js +225 -0
- package/dist/usage/FileSpoolUsageCollector.js.map +1 -0
- package/dist/usage/NoOpUsageCollector.d.ts +35 -0
- package/dist/usage/NoOpUsageCollector.d.ts.map +1 -0
- package/dist/usage/NoOpUsageCollector.js +40 -0
- package/dist/usage/NoOpUsageCollector.js.map +1 -0
- package/dist/usage/UsageEventFactory.d.ts +80 -0
- package/dist/usage/UsageEventFactory.d.ts.map +1 -0
- package/dist/usage/UsageEventFactory.js +117 -0
- package/dist/usage/UsageEventFactory.js.map +1 -0
- package/dist/usage/index.d.ts +11 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +11 -0
- package/dist/usage/index.js.map +1 -0
- package/package.json +7 -3
package/dist/core/OrbytEngine.js
CHANGED
|
@@ -82,9 +82,9 @@ 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
|
|
85
|
+
import { EngineEventType } from '../types/core-types.js';
|
|
86
86
|
import { ExplanationGenerator, ExplanationLogger } from '../explanation/index.js';
|
|
87
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
87
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } 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';
|
|
@@ -92,6 +92,11 @@ import { WorkflowStore } from '../storage/WorkflowStore.js';
|
|
|
92
92
|
import { ScheduleStore } from '../storage/ScheduleStore.js';
|
|
93
93
|
import { WorkflowParseCache, AdapterMetadataCache } from '../cache/index.js';
|
|
94
94
|
import { RuntimeArtifactStore } from '../runtime/index.js';
|
|
95
|
+
import { NoOpUsageCollector } from '../usage/NoOpUsageCollector.js';
|
|
96
|
+
import { FileSpoolUsageCollector, HttpUsageBatchTransport } from '../usage/FileSpoolUsageCollector.js';
|
|
97
|
+
import { DistributedWorkflowOrchestrator, FileDistributedJobQueue, InMemoryDistributedJobQueue } from '../distributed/index.js';
|
|
98
|
+
import { CheckpointStore } from '../storage/CheckpointStore.js';
|
|
99
|
+
import { createAdapterCallEvent, createStepExecuteEvent, createTriggerFireEvent, createWorkflowRunEvent, } from '../usage/UsageEventFactory.js';
|
|
95
100
|
/**
|
|
96
101
|
* OrbytEngine - Main public API
|
|
97
102
|
*
|
|
@@ -234,6 +239,7 @@ export class OrbytEngine {
|
|
|
234
239
|
workflowParseCache;
|
|
235
240
|
adapterMetadataCache;
|
|
236
241
|
runtimeArtifactStore;
|
|
242
|
+
usageCollector;
|
|
237
243
|
constructor(config = {}) {
|
|
238
244
|
// Validate and apply defaults
|
|
239
245
|
validateConfig(config);
|
|
@@ -275,14 +281,53 @@ export class OrbytEngine {
|
|
|
275
281
|
maxConcurrentExecutions: this.config.maxConcurrentWorkflows,
|
|
276
282
|
defaultTimeout: this.config.defaultTimeout,
|
|
277
283
|
enableScheduler: this.config.enableScheduler,
|
|
284
|
+
scheduler: this.config.scheduler,
|
|
278
285
|
queue: this.config.queue,
|
|
279
286
|
retryPolicy: this.config.retryPolicy,
|
|
280
287
|
timeoutManager: this.config.timeoutManager,
|
|
281
288
|
});
|
|
282
289
|
// Wire components together
|
|
283
290
|
this.setupComponents();
|
|
291
|
+
// Initialize usage collector.
|
|
292
|
+
// Priority:
|
|
293
|
+
// 1) user-provided collector
|
|
294
|
+
// 2) built-in durable file spool collector (default and production-safe)
|
|
295
|
+
// 3) no-op collector (testing-only escape hatch)
|
|
296
|
+
if (this.config.usageCollector) {
|
|
297
|
+
this.usageCollector = this.config.usageCollector;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const usageSpool = this.config.usageSpool;
|
|
301
|
+
const spoolEnabled = usageSpool?.enabled ?? true;
|
|
302
|
+
const spoolBaseDir = usageSpool?.baseDir ?? join(homedir(), '.orbyt', 'usage');
|
|
303
|
+
const allowNoOpForTesting = process.env.NODE_ENV === 'test' ||
|
|
304
|
+
process.env.ORBYT_ALLOW_NOOP_USAGE_COLLECTOR === '1';
|
|
305
|
+
if (spoolEnabled || !allowNoOpForTesting) {
|
|
306
|
+
if (!spoolEnabled && !allowNoOpForTesting) {
|
|
307
|
+
this.log('warn', 'usageSpool.enabled=false ignored outside test mode; using durable spool collector');
|
|
308
|
+
}
|
|
309
|
+
const transport = usageSpool?.billingEndpoint
|
|
310
|
+
? new HttpUsageBatchTransport({
|
|
311
|
+
endpoint: usageSpool.billingEndpoint,
|
|
312
|
+
apiKey: usageSpool.billingApiKey,
|
|
313
|
+
timeoutMs: usageSpool.requestTimeoutMs,
|
|
314
|
+
})
|
|
315
|
+
: undefined;
|
|
316
|
+
this.usageCollector = new FileSpoolUsageCollector({
|
|
317
|
+
baseDir: spoolBaseDir,
|
|
318
|
+
batchSize: usageSpool?.batchSize,
|
|
319
|
+
flushIntervalMs: usageSpool?.flushIntervalMs,
|
|
320
|
+
maxRetryAttempts: usageSpool?.maxRetryAttempts,
|
|
321
|
+
transport,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this.usageCollector = new NoOpUsageCollector();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
284
328
|
// Ensure infrastructure directories exist before any persistence/caching.
|
|
285
329
|
this.bootstrapRuntimeDirectories();
|
|
330
|
+
this.ensureFirstRunConfigFile();
|
|
286
331
|
// Initialise persistent stores (non-fatal — must never block engine startup)
|
|
287
332
|
const storeRoot = this.config.stateDir ?? join(homedir(), '.orbyt');
|
|
288
333
|
this.executionStore = new ExecutionStore(join(storeRoot, 'executions'));
|
|
@@ -360,6 +405,7 @@ export class OrbytEngine {
|
|
|
360
405
|
orbytHome,
|
|
361
406
|
this.config.stateDir,
|
|
362
407
|
join(this.config.stateDir, 'executions'),
|
|
408
|
+
join(this.config.stateDir, 'checkpoints'),
|
|
363
409
|
join(this.config.stateDir, 'workflows'),
|
|
364
410
|
join(this.config.stateDir, 'schedules'),
|
|
365
411
|
this.config.logDir,
|
|
@@ -373,6 +419,11 @@ export class OrbytEngine {
|
|
|
373
419
|
join(orbytHome, 'plugins'),
|
|
374
420
|
join(orbytHome, 'metrics'),
|
|
375
421
|
join(orbytHome, 'config'),
|
|
422
|
+
join(orbytHome, 'usage'),
|
|
423
|
+
join(orbytHome, 'usage', 'events'),
|
|
424
|
+
join(orbytHome, 'usage', 'pending'),
|
|
425
|
+
join(orbytHome, 'usage', 'sent'),
|
|
426
|
+
join(orbytHome, 'usage', 'failed'),
|
|
376
427
|
join(orbytHome, 'tmp'),
|
|
377
428
|
join(orbytHome, 'cloud-sync'),
|
|
378
429
|
];
|
|
@@ -385,6 +436,54 @@ export class OrbytEngine {
|
|
|
385
436
|
}
|
|
386
437
|
}
|
|
387
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Ensure a first-run config file exists at ~/.orbyt/config/config.json.
|
|
441
|
+
*
|
|
442
|
+
* This file is created once and then preserved as the local runtime config
|
|
443
|
+
* snapshot for visibility and tooling introspection.
|
|
444
|
+
*/
|
|
445
|
+
ensureFirstRunConfigFile() {
|
|
446
|
+
const configDir = join(homedir(), '.orbyt', 'config');
|
|
447
|
+
const configPath = join(configDir, 'config.json');
|
|
448
|
+
if (existsSync(configPath)) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
mkdirSync(configDir, { recursive: true });
|
|
453
|
+
const payload = {
|
|
454
|
+
version: 1,
|
|
455
|
+
createdAt: new Date().toISOString(),
|
|
456
|
+
source: 'orbyt-engine',
|
|
457
|
+
engine: {
|
|
458
|
+
version: this.version,
|
|
459
|
+
mode: this.config.mode,
|
|
460
|
+
logLevel: this.config.logLevel,
|
|
461
|
+
maxConcurrentWorkflows: this.config.maxConcurrentWorkflows,
|
|
462
|
+
maxConcurrentSteps: this.config.maxConcurrentSteps,
|
|
463
|
+
defaultTimeout: this.config.defaultTimeout,
|
|
464
|
+
enableScheduler: this.config.enableScheduler,
|
|
465
|
+
enableMetrics: this.config.enableMetrics,
|
|
466
|
+
enableEvents: this.config.enableEvents,
|
|
467
|
+
sandboxMode: this.config.sandboxMode,
|
|
468
|
+
},
|
|
469
|
+
paths: {
|
|
470
|
+
stateDir: this.config.stateDir,
|
|
471
|
+
logDir: this.config.logDir,
|
|
472
|
+
cacheDir: this.config.cacheDir,
|
|
473
|
+
runtimeDir: this.config.runtimeDir,
|
|
474
|
+
workingDirectory: this.config.workingDirectory,
|
|
475
|
+
},
|
|
476
|
+
usageSpool: this.config.usageSpool,
|
|
477
|
+
};
|
|
478
|
+
writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
479
|
+
this.log('debug', 'Created first-run engine config file', { configPath });
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
this.log('warn', 'Failed to create first-run engine config file', {
|
|
483
|
+
error: error instanceof Error ? error.message : String(error),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
388
487
|
/**
|
|
389
488
|
* Register built-in adapters
|
|
390
489
|
* These are the core adapters shipped with Orbyt
|
|
@@ -424,6 +523,14 @@ export class OrbytEngine {
|
|
|
424
523
|
// Emit engine.stopped event
|
|
425
524
|
await this.eventBus.emit(createEvent(EngineEventType.ENGINE_STOPPED, { timestamp: Date.now() }, {}));
|
|
426
525
|
this.log('info', 'Orbyt Engine stopped');
|
|
526
|
+
// Best-effort collector draining/cleanup on shutdown.
|
|
527
|
+
try {
|
|
528
|
+
await this.usageCollector.flush?.();
|
|
529
|
+
await this.usageCollector.close?.();
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Non-fatal on shutdown
|
|
533
|
+
}
|
|
427
534
|
}
|
|
428
535
|
/**
|
|
429
536
|
* Run a workflow
|
|
@@ -662,6 +769,8 @@ export class OrbytEngine {
|
|
|
662
769
|
continueOnError: options.continueOnError,
|
|
663
770
|
dryRun: options.dryRun,
|
|
664
771
|
triggeredBy: options.triggeredBy,
|
|
772
|
+
resumeFromRunId: options.resumeFromRunId,
|
|
773
|
+
resumePolicy: options.resumePolicy,
|
|
665
774
|
_ownershipContext: options._ownershipContext,
|
|
666
775
|
_permissionPolicy: options._permissionPolicy,
|
|
667
776
|
};
|
|
@@ -749,7 +858,34 @@ export class OrbytEngine {
|
|
|
749
858
|
},
|
|
750
859
|
continueOnError: sanitizedOptions.continueOnError,
|
|
751
860
|
triggeredBy: sanitizedOptions.triggeredBy || 'manual',
|
|
861
|
+
resumeFromRunId: sanitizedOptions.resumeFromRunId,
|
|
862
|
+
resumePolicy: sanitizedOptions.resumePolicy,
|
|
752
863
|
};
|
|
864
|
+
if (execOptions.triggeredBy && execOptions.triggeredBy !== 'manual') {
|
|
865
|
+
internalContext._usage.triggerFireCount += 1;
|
|
866
|
+
this.recordUsageEvent(createTriggerFireEvent({
|
|
867
|
+
executionId: internalContext._identity.executionId,
|
|
868
|
+
workflowId: parsedWorkflow.name || parsedWorkflow.metadata?.name,
|
|
869
|
+
userId: internalContext._ownership.userId,
|
|
870
|
+
workspaceId: internalContext._ownership.workspaceId,
|
|
871
|
+
pricingTier: internalContext._billing.pricingTierResolved,
|
|
872
|
+
billable: internalContext._billing.isBillable,
|
|
873
|
+
metadata: {
|
|
874
|
+
success: true,
|
|
875
|
+
triggeredBy: execOptions.triggeredBy,
|
|
876
|
+
},
|
|
877
|
+
}));
|
|
878
|
+
}
|
|
879
|
+
internalContext._usage.automationCount += 1;
|
|
880
|
+
this.recordUsageEvent(createWorkflowRunEvent({
|
|
881
|
+
executionId: internalContext._identity.executionId,
|
|
882
|
+
workflowId: parsedWorkflow.name || parsedWorkflow.metadata?.name,
|
|
883
|
+
userId: internalContext._ownership.userId,
|
|
884
|
+
workspaceId: internalContext._ownership.workspaceId,
|
|
885
|
+
executionMode: isolatedRuntime ? 'parallel' : 'single',
|
|
886
|
+
pricingTier: internalContext._billing.pricingTierResolved,
|
|
887
|
+
billable: internalContext._billing.isBillable,
|
|
888
|
+
}));
|
|
753
889
|
this.log('info', `Running workflow: ${parsedWorkflow.name || 'unnamed'}`);
|
|
754
890
|
this.workflowStore.save(parsedWorkflow);
|
|
755
891
|
// Parallel/mixed mode can execute multiple workflows concurrently via runMany().
|
|
@@ -761,10 +897,11 @@ export class OrbytEngine {
|
|
|
761
897
|
stepExecutor: this.stepExecutor,
|
|
762
898
|
workflowExecutor: this.workflowExecutor,
|
|
763
899
|
};
|
|
764
|
-
const
|
|
765
|
-
|
|
900
|
+
const useDistributedRuntime = this.config.mode === 'distributed';
|
|
901
|
+
const result = await this.measureWorkflowExecution(parsedWorkflow.name || 'unnamed', () => useDistributedRuntime
|
|
902
|
+
? this.executeDistributedWorkflow(parsedWorkflow, execOptions, runtime.stepExecutor)
|
|
903
|
+
: runtime.workflowExecutor.execute(parsedWorkflow, execOptions));
|
|
766
904
|
internalContext._usage.durationSeconds = result.duration / 1000;
|
|
767
|
-
internalContext._usage.weightedStepCount = result.metadata.totalSteps;
|
|
768
905
|
this.log('info', `Workflow completed: ${result.status}`, {
|
|
769
906
|
durationMs: result.duration,
|
|
770
907
|
steps: result.metadata.totalSteps,
|
|
@@ -772,6 +909,54 @@ export class OrbytEngine {
|
|
|
772
909
|
automationCount: internalContext._usage.automationCount,
|
|
773
910
|
stepCount: internalContext._usage.stepCount,
|
|
774
911
|
});
|
|
912
|
+
const workflowId = parsedWorkflow.name || parsedWorkflow.metadata?.name;
|
|
913
|
+
for (const [stepId, stepResult] of result.stepResults.entries()) {
|
|
914
|
+
const stepDefinition = parsedWorkflow.steps.find((step) => step.id === stepId);
|
|
915
|
+
if (!stepDefinition)
|
|
916
|
+
continue;
|
|
917
|
+
const success = stepResult.status === 'success';
|
|
918
|
+
const errorMessage = stepResult.error?.message;
|
|
919
|
+
const executed = stepResult.status !== 'skipped';
|
|
920
|
+
if (executed) {
|
|
921
|
+
internalContext._usage.stepCount += 1;
|
|
922
|
+
internalContext._usage.weightedStepCount += 1;
|
|
923
|
+
}
|
|
924
|
+
if (executed) {
|
|
925
|
+
this.recordUsageEvent(createStepExecuteEvent({
|
|
926
|
+
executionId: internalContext._identity.executionId,
|
|
927
|
+
stepId,
|
|
928
|
+
workflowId,
|
|
929
|
+
userId: internalContext._ownership.userId,
|
|
930
|
+
workspaceId: internalContext._ownership.workspaceId,
|
|
931
|
+
adapterType: stepDefinition.adapter,
|
|
932
|
+
adapterName: stepDefinition.action,
|
|
933
|
+
durationMs: stepResult.duration,
|
|
934
|
+
success,
|
|
935
|
+
retries: Math.max(0, stepResult.attempts - 1),
|
|
936
|
+
error: errorMessage,
|
|
937
|
+
pricingTier: internalContext._billing.pricingTierResolved,
|
|
938
|
+
billable: internalContext._billing.isBillable,
|
|
939
|
+
}));
|
|
940
|
+
}
|
|
941
|
+
if (executed) {
|
|
942
|
+
internalContext._usage.adapterCallCount += 1;
|
|
943
|
+
this.recordUsageEvent(createAdapterCallEvent({
|
|
944
|
+
executionId: internalContext._identity.executionId,
|
|
945
|
+
stepId,
|
|
946
|
+
adapterType: stepDefinition.adapter,
|
|
947
|
+
adapterName: stepDefinition.action,
|
|
948
|
+
workflowId,
|
|
949
|
+
userId: internalContext._ownership.userId,
|
|
950
|
+
workspaceId: internalContext._ownership.workspaceId,
|
|
951
|
+
durationMs: stepResult.duration,
|
|
952
|
+
success,
|
|
953
|
+
retries: Math.max(0, stepResult.attempts - 1),
|
|
954
|
+
error: errorMessage,
|
|
955
|
+
pricingTier: internalContext._billing.pricingTierResolved,
|
|
956
|
+
billable: internalContext._billing.isBillable,
|
|
957
|
+
}));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
775
960
|
await this.onWorkflowBillingComplete(internalContext, result);
|
|
776
961
|
return result;
|
|
777
962
|
});
|
|
@@ -779,6 +964,42 @@ export class OrbytEngine {
|
|
|
779
964
|
LoggerManager.clearWorkflowContext();
|
|
780
965
|
return result;
|
|
781
966
|
}
|
|
967
|
+
async executeDistributedWorkflow(workflow, execOptions, stepExecutor) {
|
|
968
|
+
const distributed = this.config.distributed;
|
|
969
|
+
const queueBackend = distributed?.queueBackend ?? 'memory';
|
|
970
|
+
const queueStateDir = distributed?.fileQueueStateDir ?? join(homedir(), '.orbyt', 'distributed-queue');
|
|
971
|
+
const workerCount = distributed?.workerCount
|
|
972
|
+
?? this.config.scheduler?.job?.workerCount
|
|
973
|
+
?? Math.max(1, Math.min(8, this.config.maxConcurrentSteps || 4));
|
|
974
|
+
const queue = distributed?.jobQueue
|
|
975
|
+
?? (queueBackend === 'file'
|
|
976
|
+
? new FileDistributedJobQueue({
|
|
977
|
+
stateDir: queueStateDir,
|
|
978
|
+
leaseMs: distributed?.leaseMs,
|
|
979
|
+
})
|
|
980
|
+
: new InMemoryDistributedJobQueue({
|
|
981
|
+
leaseMs: distributed?.leaseMs,
|
|
982
|
+
}));
|
|
983
|
+
const orchestrator = new DistributedWorkflowOrchestrator({
|
|
984
|
+
queue,
|
|
985
|
+
stepExecutor,
|
|
986
|
+
workerCount,
|
|
987
|
+
pollIntervalMs: distributed?.pollIntervalMs,
|
|
988
|
+
eventBus: this.eventBus,
|
|
989
|
+
hookManager: this.hookManager,
|
|
990
|
+
executionStore: this.executionStore,
|
|
991
|
+
checkpointStore: new CheckpointStore(join(this.config.stateDir, 'checkpoints')),
|
|
992
|
+
leaseExtensionMs: distributed?.leaseExtensionMs,
|
|
993
|
+
});
|
|
994
|
+
this.log('info', '[Distributed] Executing workflow via scheduler->queue->workers runtime', {
|
|
995
|
+
workerCount,
|
|
996
|
+
queueBackend: distributed?.jobQueue
|
|
997
|
+
? 'custom'
|
|
998
|
+
: queueBackend,
|
|
999
|
+
workflowId: workflow.name || workflow.metadata?.name,
|
|
1000
|
+
});
|
|
1001
|
+
return orchestrator.execute(workflow, execOptions);
|
|
1002
|
+
}
|
|
782
1003
|
/**
|
|
783
1004
|
* Create isolated per-workflow execution runtime.
|
|
784
1005
|
*
|
|
@@ -861,6 +1082,31 @@ export class OrbytEngine {
|
|
|
861
1082
|
// });
|
|
862
1083
|
// }
|
|
863
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Safe usage event recording
|
|
1087
|
+
*
|
|
1088
|
+
* Records a usage event through the configured collector.
|
|
1089
|
+
* Failures are logged but never propagate (non-fatal).
|
|
1090
|
+
* Called asynchronously and does not block execution.
|
|
1091
|
+
*
|
|
1092
|
+
* @param event - Usage event to record
|
|
1093
|
+
*/
|
|
1094
|
+
recordUsageEvent(event) {
|
|
1095
|
+
// Fire-and-forget: record usage asynchronously without blocking
|
|
1096
|
+
process.nextTick(async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
await this.usageCollector.record(event);
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
// Log but don't propagate - usage tracking must never fail execution
|
|
1102
|
+
this.log('debug', 'Usage collection failed (non-fatal)', {
|
|
1103
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1104
|
+
eventType: event.type,
|
|
1105
|
+
executionId: event.executionId,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
864
1110
|
/**
|
|
865
1111
|
* Validate a workflow without executing it
|
|
866
1112
|
*
|
|
@@ -1065,6 +1311,226 @@ export class OrbytEngine {
|
|
|
1065
1311
|
getVersion() {
|
|
1066
1312
|
return this.version;
|
|
1067
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Query usage events from the local spool archive and return aggregated metrics.
|
|
1316
|
+
*/
|
|
1317
|
+
async getUsage(options = {}) {
|
|
1318
|
+
const now = Date.now();
|
|
1319
|
+
const from = options.from ?? now - 30 * 24 * 60 * 60 * 1000;
|
|
1320
|
+
const to = options.to ?? now;
|
|
1321
|
+
const groupBy = options.groupBy ?? 'none';
|
|
1322
|
+
if (from > to) {
|
|
1323
|
+
throw new Error(`Invalid usage query range: from (${from}) must be <= to (${to})`);
|
|
1324
|
+
}
|
|
1325
|
+
const usageBaseDir = this.config.usageSpool?.baseDir ?? join(homedir(), '.orbyt', 'usage');
|
|
1326
|
+
const eventsDir = join(usageBaseDir, 'events');
|
|
1327
|
+
const filteredEvents = [];
|
|
1328
|
+
if (existsSync(eventsDir)) {
|
|
1329
|
+
const files = readdirSync(eventsDir)
|
|
1330
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
1331
|
+
.sort();
|
|
1332
|
+
for (const file of files) {
|
|
1333
|
+
const filePath = join(eventsDir, file);
|
|
1334
|
+
let content = '';
|
|
1335
|
+
try {
|
|
1336
|
+
content = readFileSync(filePath, 'utf8');
|
|
1337
|
+
}
|
|
1338
|
+
catch {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (!content.trim()) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const lines = content.split('\n');
|
|
1345
|
+
for (const line of lines) {
|
|
1346
|
+
const trimmed = line.trim();
|
|
1347
|
+
if (!trimmed) {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
let event;
|
|
1351
|
+
try {
|
|
1352
|
+
event = JSON.parse(trimmed);
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (!this.matchesUsageQuery(event, options, from, to)) {
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
filteredEvents.push(event);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const byType = {};
|
|
1365
|
+
const byProduct = {};
|
|
1366
|
+
const byAdapter = {};
|
|
1367
|
+
const byWorkflow = {};
|
|
1368
|
+
const byWorkspace = {};
|
|
1369
|
+
const byUser = {};
|
|
1370
|
+
const byTrigger = {};
|
|
1371
|
+
const groupedMap = new Map();
|
|
1372
|
+
let billableEvents = 0;
|
|
1373
|
+
let successEvents = 0;
|
|
1374
|
+
let failureEvents = 0;
|
|
1375
|
+
let totalDurationMs = 0;
|
|
1376
|
+
for (const event of filteredEvents) {
|
|
1377
|
+
byType[event.type] = (byType[event.type] ?? 0) + 1;
|
|
1378
|
+
byProduct[event.product] = (byProduct[event.product] ?? 0) + 1;
|
|
1379
|
+
const adapterKey = event.adapterName || event.adapterType || 'unknown';
|
|
1380
|
+
byAdapter[adapterKey] = (byAdapter[adapterKey] ?? 0) + 1;
|
|
1381
|
+
const workflowKey = event.workflowId || 'unknown';
|
|
1382
|
+
byWorkflow[workflowKey] = (byWorkflow[workflowKey] ?? 0) + 1;
|
|
1383
|
+
const workspaceKey = event.workspaceId || 'unknown';
|
|
1384
|
+
byWorkspace[workspaceKey] = (byWorkspace[workspaceKey] ?? 0) + 1;
|
|
1385
|
+
const userKey = event.userId || 'unknown';
|
|
1386
|
+
byUser[userKey] = (byUser[userKey] ?? 0) + 1;
|
|
1387
|
+
const triggerKey = this.resolveTriggerKey(event);
|
|
1388
|
+
byTrigger[triggerKey] = (byTrigger[triggerKey] ?? 0) + 1;
|
|
1389
|
+
if (event.billable === true) {
|
|
1390
|
+
billableEvents += 1;
|
|
1391
|
+
}
|
|
1392
|
+
if (event.metadata?.success === true) {
|
|
1393
|
+
successEvents += 1;
|
|
1394
|
+
}
|
|
1395
|
+
else if (event.metadata?.success === false) {
|
|
1396
|
+
failureEvents += 1;
|
|
1397
|
+
}
|
|
1398
|
+
const durationMs = typeof event.metadata?.durationMs === 'number'
|
|
1399
|
+
? Math.max(0, event.metadata.durationMs)
|
|
1400
|
+
: 0;
|
|
1401
|
+
totalDurationMs += durationMs;
|
|
1402
|
+
if (groupBy !== 'none') {
|
|
1403
|
+
const key = this.resolveUsageGroupKey(event, groupBy);
|
|
1404
|
+
const existing = groupedMap.get(key) ?? {
|
|
1405
|
+
key,
|
|
1406
|
+
eventCount: 0,
|
|
1407
|
+
billableCount: 0,
|
|
1408
|
+
successCount: 0,
|
|
1409
|
+
failureCount: 0,
|
|
1410
|
+
totalDurationMs: 0,
|
|
1411
|
+
};
|
|
1412
|
+
existing.eventCount += 1;
|
|
1413
|
+
if (event.billable === true) {
|
|
1414
|
+
existing.billableCount += 1;
|
|
1415
|
+
}
|
|
1416
|
+
if (event.metadata?.success === true) {
|
|
1417
|
+
existing.successCount += 1;
|
|
1418
|
+
}
|
|
1419
|
+
else if (event.metadata?.success === false) {
|
|
1420
|
+
existing.failureCount += 1;
|
|
1421
|
+
}
|
|
1422
|
+
existing.totalDurationMs += durationMs;
|
|
1423
|
+
groupedMap.set(key, existing);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const normalizedLimit = typeof options.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
|
|
1427
|
+
? Math.floor(options.limit)
|
|
1428
|
+
: undefined;
|
|
1429
|
+
const outputEvents = options.includeEvents
|
|
1430
|
+
? (normalizedLimit ? filteredEvents.slice(-normalizedLimit) : filteredEvents)
|
|
1431
|
+
: undefined;
|
|
1432
|
+
return {
|
|
1433
|
+
query: {
|
|
1434
|
+
from,
|
|
1435
|
+
to,
|
|
1436
|
+
groupBy,
|
|
1437
|
+
limit: normalizedLimit,
|
|
1438
|
+
},
|
|
1439
|
+
totalEvents: filteredEvents.length,
|
|
1440
|
+
billableEvents,
|
|
1441
|
+
successEvents,
|
|
1442
|
+
failureEvents,
|
|
1443
|
+
totalDurationMs,
|
|
1444
|
+
byType,
|
|
1445
|
+
byProduct,
|
|
1446
|
+
byAdapter,
|
|
1447
|
+
byWorkflow,
|
|
1448
|
+
byWorkspace,
|
|
1449
|
+
byUser,
|
|
1450
|
+
byTrigger,
|
|
1451
|
+
grouped: Array.from(groupedMap.values()).sort((a, b) => a.key.localeCompare(b.key)),
|
|
1452
|
+
events: outputEvents?.map((event) => ({
|
|
1453
|
+
id: event.id,
|
|
1454
|
+
type: event.type,
|
|
1455
|
+
timestamp: event.timestamp,
|
|
1456
|
+
product: event.product,
|
|
1457
|
+
executionId: event.executionId,
|
|
1458
|
+
workflowId: event.workflowId,
|
|
1459
|
+
stepId: event.stepId,
|
|
1460
|
+
adapterName: event.adapterName,
|
|
1461
|
+
adapterType: event.adapterType,
|
|
1462
|
+
billable: event.billable,
|
|
1463
|
+
metadata: event.metadata,
|
|
1464
|
+
})),
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
matchesUsageQuery(event, options, from, to) {
|
|
1468
|
+
if (!event || typeof event !== 'object')
|
|
1469
|
+
return false;
|
|
1470
|
+
if (typeof event.timestamp !== 'number')
|
|
1471
|
+
return false;
|
|
1472
|
+
if (event.timestamp < from || event.timestamp > to)
|
|
1473
|
+
return false;
|
|
1474
|
+
if (options.userId && event.userId !== options.userId)
|
|
1475
|
+
return false;
|
|
1476
|
+
if (options.workspaceId && event.workspaceId !== options.workspaceId)
|
|
1477
|
+
return false;
|
|
1478
|
+
if (options.product && event.product !== options.product)
|
|
1479
|
+
return false;
|
|
1480
|
+
if (options.eventType && event.type !== options.eventType)
|
|
1481
|
+
return false;
|
|
1482
|
+
if (options.adapterName && event.adapterName !== options.adapterName)
|
|
1483
|
+
return false;
|
|
1484
|
+
if (options.adapterType && event.adapterType !== options.adapterType)
|
|
1485
|
+
return false;
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
resolveUsageGroupKey(event, groupBy) {
|
|
1489
|
+
if (groupBy === 'workflow') {
|
|
1490
|
+
return event.workflowId || 'unknown';
|
|
1491
|
+
}
|
|
1492
|
+
if (groupBy === 'adapter') {
|
|
1493
|
+
return event.adapterName || event.adapterType || 'unknown';
|
|
1494
|
+
}
|
|
1495
|
+
if (groupBy === 'trigger') {
|
|
1496
|
+
return this.resolveTriggerKey(event);
|
|
1497
|
+
}
|
|
1498
|
+
if (groupBy === 'product') {
|
|
1499
|
+
return event.product || 'unknown';
|
|
1500
|
+
}
|
|
1501
|
+
if (groupBy === 'workspace') {
|
|
1502
|
+
return event.workspaceId || 'unknown';
|
|
1503
|
+
}
|
|
1504
|
+
if (groupBy === 'user') {
|
|
1505
|
+
return event.userId || 'unknown';
|
|
1506
|
+
}
|
|
1507
|
+
if (groupBy === 'type') {
|
|
1508
|
+
return event.type || 'unknown';
|
|
1509
|
+
}
|
|
1510
|
+
const date = new Date(event.timestamp);
|
|
1511
|
+
if (groupBy === 'hourly') {
|
|
1512
|
+
return `${date.toISOString().slice(0, 13)}:00:00Z`;
|
|
1513
|
+
}
|
|
1514
|
+
if (groupBy === 'daily') {
|
|
1515
|
+
return date.toISOString().slice(0, 10);
|
|
1516
|
+
}
|
|
1517
|
+
if (groupBy === 'weekly') {
|
|
1518
|
+
const day = date.getUTCDay();
|
|
1519
|
+
const shift = day === 0 ? -6 : 1 - day;
|
|
1520
|
+
const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + shift));
|
|
1521
|
+
return start.toISOString().slice(0, 10);
|
|
1522
|
+
}
|
|
1523
|
+
return 'all';
|
|
1524
|
+
}
|
|
1525
|
+
resolveTriggerKey(event) {
|
|
1526
|
+
if (event.type === 'usage.trigger.fire') {
|
|
1527
|
+
const triggerType = typeof event.metadata?.triggerType === 'string'
|
|
1528
|
+
? event.metadata.triggerType
|
|
1529
|
+
: undefined;
|
|
1530
|
+
return triggerType || 'trigger.fire';
|
|
1531
|
+
}
|
|
1532
|
+
return 'non-trigger';
|
|
1533
|
+
}
|
|
1068
1534
|
/**
|
|
1069
1535
|
* Internal logging method
|
|
1070
1536
|
*
|