@tasker-systems/tasker 0.1.4 → 0.1.5

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.
@@ -208,7 +208,7 @@ if (process.env.TASKER_ENV !== "production") {
208
208
  }
209
209
  var log = pino(loggerOptions);
210
210
  var EventPoller = class {
211
- runtime;
211
+ module;
212
212
  config;
213
213
  emitter;
214
214
  state = "stopped";
@@ -221,12 +221,12 @@ var EventPoller = class {
221
221
  /**
222
222
  * Create a new EventPoller.
223
223
  *
224
- * @param runtime - The FFI runtime for polling events
224
+ * @param module - The napi-rs module for polling events
225
225
  * @param emitter - The event emitter to dispatch events to (required, no fallback)
226
226
  * @param config - Optional configuration for polling behavior
227
227
  */
228
- constructor(runtime, emitter, config = {}) {
229
- this.runtime = runtime;
228
+ constructor(module, emitter, config = {}) {
229
+ this.module = module;
230
230
  this.emitter = emitter;
231
231
  this.config = {
232
232
  pollIntervalMs: config.pollIntervalMs ?? 10,
@@ -293,13 +293,6 @@ var EventPoller = class {
293
293
  log.debug({ component: "event-poller" }, "Already running, returning early");
294
294
  return;
295
295
  }
296
- log.debug(
297
- { component: "event-poller", runtimeLoaded: this.runtime.isLoaded },
298
- "Checking runtime.isLoaded"
299
- );
300
- if (!this.runtime.isLoaded) {
301
- throw new Error("Runtime not loaded. Call runtime.load() first.");
302
- }
303
296
  this.state = "running";
304
297
  this.pollCount = 0;
305
298
  this.cycleCount = 0;
@@ -360,17 +353,17 @@ var EventPoller = class {
360
353
  try {
361
354
  let eventsProcessed = 0;
362
355
  for (let i = 0; i < this.config.maxEventsPerCycle; i++) {
363
- const event = this.runtime.pollStepEvents();
356
+ const event = this.module.pollStepEvents();
364
357
  if (event === null) {
365
358
  break;
366
359
  }
367
360
  eventsProcessed++;
368
- const handlerCallable = event.step_definition.handler.callable;
361
+ const handlerCallable = event.stepDefinition.handlerCallable;
369
362
  log.info(
370
363
  {
371
364
  component: "event-poller",
372
365
  operation: "event_received",
373
- stepUuid: event.step_uuid,
366
+ stepUuid: event.stepUuid,
374
367
  handlerCallable,
375
368
  eventIndex: i
376
369
  },
@@ -382,7 +375,7 @@ var EventPoller = class {
382
375
  this.checkStarvation();
383
376
  }
384
377
  if (this.pollCount % this.config.cleanupInterval === 0) {
385
- this.runtime.cleanupTimeouts();
378
+ this.module.cleanupTimeouts();
386
379
  }
387
380
  if (this.pollCount % this.config.metricsInterval === 0) {
388
381
  this.emitMetrics();
@@ -403,12 +396,12 @@ var EventPoller = class {
403
396
  * Handle a step event
404
397
  */
405
398
  handleStepEvent(event) {
406
- const handlerCallable = event.step_definition.handler.callable;
399
+ const handlerCallable = event.stepDefinition.handlerCallable;
407
400
  log.debug(
408
401
  {
409
402
  component: "event-poller",
410
403
  operation: "handle_step_event",
411
- stepUuid: event.step_uuid,
404
+ stepUuid: event.stepUuid,
412
405
  handlerCallable,
413
406
  hasCallback: !!this.stepEventCallback
414
407
  },
@@ -419,7 +412,7 @@ var EventPoller = class {
419
412
  {
420
413
  component: "event-poller",
421
414
  emitterInstanceId: this.emitter.getInstanceId(),
422
- stepUuid: event.step_uuid,
415
+ stepUuid: event.stepUuid,
423
416
  listenerCount: listenerCountBefore,
424
417
  eventName: StepEventNames.STEP_EXECUTION_RECEIVED
425
418
  },
@@ -433,7 +426,7 @@ var EventPoller = class {
433
426
  log.info(
434
427
  {
435
428
  component: "event-poller",
436
- stepUuid: event.step_uuid,
429
+ stepUuid: event.stepUuid,
437
430
  emitResult,
438
431
  listenerCountAfter: this.emitter.listenerCount(StepEventNames.STEP_EXECUTION_RECEIVED),
439
432
  eventName: StepEventNames.STEP_EXECUTION_RECEIVED
@@ -444,7 +437,7 @@ var EventPoller = class {
444
437
  log.error(
445
438
  {
446
439
  component: "event-poller",
447
- stepUuid: event.step_uuid,
440
+ stepUuid: event.stepUuid,
448
441
  error: emitError instanceof Error ? emitError.message : String(emitError),
449
442
  stack: emitError instanceof Error ? emitError.stack : void 0
450
443
  },
@@ -453,7 +446,7 @@ var EventPoller = class {
453
446
  }
454
447
  if (this.stepEventCallback) {
455
448
  log.debug(
456
- { component: "event-poller", stepUuid: event.step_uuid },
449
+ { component: "event-poller", stepUuid: event.stepUuid },
457
450
  "Invoking step event callback"
458
451
  );
459
452
  this.stepEventCallback(event).catch((error) => {
@@ -461,7 +454,7 @@ var EventPoller = class {
461
454
  });
462
455
  } else {
463
456
  log.warn(
464
- { component: "event-poller", stepUuid: event.step_uuid },
457
+ { component: "event-poller", stepUuid: event.stepUuid },
465
458
  "No step event callback registered!"
466
459
  );
467
460
  }
@@ -471,9 +464,9 @@ var EventPoller = class {
471
464
  */
472
465
  checkStarvation() {
473
466
  try {
474
- this.runtime.checkStarvationWarnings();
475
- const metrics = this.runtime.getFfiDispatchMetrics();
476
- if (metrics.starvation_detected) {
467
+ this.module.checkStarvationWarnings();
468
+ const metrics = this.module.getFfiDispatchMetrics();
469
+ if (metrics.starvationDetected) {
477
470
  this.emitter.emitStarvationDetected(metrics);
478
471
  }
479
472
  } catch (error) {
@@ -485,7 +478,7 @@ var EventPoller = class {
485
478
  */
486
479
  emitMetrics() {
487
480
  try {
488
- const metrics = this.runtime.getFfiDispatchMetrics();
481
+ const metrics = this.module.getFfiDispatchMetrics();
489
482
  this.emitter.emitMetricsUpdated(metrics);
490
483
  if (this.metricsCallback) {
491
484
  this.metricsCallback(metrics);
@@ -510,23 +503,8 @@ var EventPoller = class {
510
503
  }
511
504
  }
512
505
  };
513
- function createEventPoller(runtime, emitter, config) {
514
- return new EventPoller(runtime, emitter, config);
515
- }
516
- function getLoggingRuntime() {
517
- return null;
518
- }
519
- function toFfiFields(fields) {
520
- if (!fields) {
521
- return {};
522
- }
523
- const result = {};
524
- for (const [key, value] of Object.entries(fields)) {
525
- if (value !== void 0) {
526
- result[key] = value;
527
- }
528
- }
529
- return result;
506
+ function createEventPoller(module, emitter, config) {
507
+ return new EventPoller(module, emitter, config);
530
508
  }
531
509
  function fallbackLog(level, message, fields) {
532
510
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -534,52 +512,28 @@ function fallbackLog(level, message, fields) {
534
512
  console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}${fieldsStr}`);
535
513
  }
536
514
  function logError(message, fields) {
537
- const runtime = getLoggingRuntime();
538
- if (!runtime) {
515
+ {
539
516
  fallbackLog("error", message, fields);
540
517
  return;
541
518
  }
542
- try {
543
- runtime.logError(message, toFfiFields(fields));
544
- } catch {
545
- fallbackLog("error", message, fields);
546
- }
547
519
  }
548
520
  function logWarn(message, fields) {
549
- const runtime = getLoggingRuntime();
550
- if (!runtime) {
521
+ {
551
522
  fallbackLog("warn", message, fields);
552
523
  return;
553
524
  }
554
- try {
555
- runtime.logWarn(message, toFfiFields(fields));
556
- } catch {
557
- fallbackLog("warn", message, fields);
558
- }
559
525
  }
560
526
  function logInfo(message, fields) {
561
- const runtime = getLoggingRuntime();
562
- if (!runtime) {
527
+ {
563
528
  fallbackLog("info", message, fields);
564
529
  return;
565
530
  }
566
- try {
567
- runtime.logInfo(message, toFfiFields(fields));
568
- } catch {
569
- fallbackLog("info", message, fields);
570
- }
571
531
  }
572
532
  function logDebug(message, fields) {
573
- const runtime = getLoggingRuntime();
574
- if (!runtime) {
533
+ {
575
534
  fallbackLog("debug", message, fields);
576
535
  return;
577
536
  }
578
- try {
579
- runtime.logDebug(message, toFfiFields(fields));
580
- } catch {
581
- fallbackLog("debug", message, fields);
582
- }
583
537
  }
584
538
 
585
539
  // src/types/step-context.ts
@@ -598,9 +552,9 @@ var StepContext = class _StepContext {
598
552
  inputData;
599
553
  /** Results from dependent steps */
600
554
  dependencyResults;
601
- /** Handler-specific configuration (from step_definition.handler.initialization) */
555
+ /** Handler-specific configuration (from stepDefinition.handlerInitialization) */
602
556
  stepConfig;
603
- /** Step-specific inputs (from workflow_step.inputs, used for batch cursor config) */
557
+ /** Step-specific inputs (from workflowStep.inputs, used for batch cursor config) */
604
558
  stepInputs;
605
559
  /** Current retry attempt number */
606
560
  retryCount;
@@ -622,16 +576,15 @@ var StepContext = class _StepContext {
622
576
  /**
623
577
  * Create a StepContext from an FFI event.
624
578
  *
625
- * Extracts input data, dependency results, and configuration from
626
- * the task_sequence_step payload.
579
+ * TAS-290: All field access uses camelCase (napi-rs auto-converts).
627
580
  *
628
581
  * The FFI data structure mirrors the Ruby TaskSequenceStepWrapper:
629
582
  * - task.context -> inputData (task context with user inputs)
630
- * - dependency_results -> results from parent steps
631
- * - step_definition.handler.initialization -> stepConfig
632
- * - workflow_step.attempts -> retryCount
633
- * - workflow_step.max_attempts -> maxRetries
634
- * - workflow_step.inputs -> stepInputs
583
+ * - dependencyResults -> results from parent steps
584
+ * - stepDefinition.handlerInitialization -> stepConfig
585
+ * - workflowStep.attempts -> retryCount
586
+ * - workflowStep.maxAttempts -> maxRetries
587
+ * - workflowStep.inputs -> stepInputs
635
588
  *
636
589
  * @param event - The FFI step event
637
590
  * @param handlerName - Name of the handler to execute
@@ -640,19 +593,18 @@ var StepContext = class _StepContext {
640
593
  static fromFfiEvent(event, handlerName) {
641
594
  const task = event.task ?? {};
642
595
  const inputData = task.context ?? {};
643
- const dependencyResults = event.dependency_results ?? {};
644
- const stepDefinition = event.step_definition ?? {};
645
- const handlerConfig = stepDefinition.handler ?? {};
646
- const stepConfig = handlerConfig.initialization ?? {};
647
- const workflowStep = event.workflow_step ?? {};
596
+ const dependencyResults = event.dependencyResults ?? {};
597
+ const stepDefinition = event.stepDefinition ?? {};
598
+ const stepConfig = stepDefinition.handlerInitialization ?? {};
599
+ const workflowStep = event.workflowStep ?? {};
648
600
  const retryCount = workflowStep.attempts ?? 0;
649
- const maxRetries = workflowStep.max_attempts ?? 3;
601
+ const maxRetries = workflowStep.maxAttempts ?? 3;
650
602
  const stepInputs = workflowStep.inputs ?? {};
651
603
  return new _StepContext({
652
604
  event,
653
- taskUuid: event.task_uuid,
654
- stepUuid: event.step_uuid,
655
- correlationId: event.correlation_id,
605
+ taskUuid: event.taskUuid,
606
+ stepUuid: event.stepUuid,
607
+ correlationId: event.correlationId,
656
608
  handlerName,
657
609
  inputData,
658
610
  dependencyResults,
@@ -782,10 +734,14 @@ var StepContext = class _StepContext {
782
734
  /**
783
735
  * Get the raw checkpoint data from the workflow step.
784
736
  *
737
+ * Note: Checkpoint data from the database uses snake_case keys
738
+ * (cursor, items_processed, accumulated_results) because it's stored
739
+ * as a serde_json::Value that passes through napi-rs as-is.
740
+ *
785
741
  * @returns The checkpoint data object or null if not set
786
742
  */
787
743
  get checkpoint() {
788
- const workflowStep = this.event.workflow_step ?? {};
744
+ const workflowStep = this.event.workflowStep ?? {};
789
745
  return workflowStep.checkpoint ?? null;
790
746
  }
791
747
  /**
@@ -885,6 +841,130 @@ var StepContext = class _StepContext {
885
841
  }
886
842
  };
887
843
 
844
+ // src/types/step-handler-result.ts
845
+ var StepHandlerResult = class _StepHandlerResult {
846
+ /** Whether the handler executed successfully */
847
+ success;
848
+ /** Handler output data (success case) */
849
+ result;
850
+ /** Error message (failure case) */
851
+ errorMessage;
852
+ /** Error type/category for classification */
853
+ errorType;
854
+ /** Optional application-specific error code */
855
+ errorCode;
856
+ /** Whether the error is retryable */
857
+ retryable;
858
+ /** Additional execution metadata */
859
+ metadata;
860
+ /** Orchestration metadata for workflow coordination hints (e.g., backoff, headers) */
861
+ orchestrationMetadata;
862
+ constructor(params) {
863
+ this.success = params.success;
864
+ this.result = params.result ?? null;
865
+ this.errorMessage = params.errorMessage ?? null;
866
+ this.errorType = params.errorType ?? null;
867
+ this.errorCode = params.errorCode ?? null;
868
+ this.retryable = params.retryable ?? true;
869
+ this.metadata = params.metadata ?? {};
870
+ this.orchestrationMetadata = params.orchestrationMetadata ?? null;
871
+ }
872
+ /**
873
+ * Create a successful handler result.
874
+ *
875
+ * This is the primary factory method for creating success results.
876
+ * Aligned with Ruby and Python worker APIs.
877
+ *
878
+ * @param result - The handler output data
879
+ * @param metadata - Optional additional metadata
880
+ * @returns A StepHandlerResult indicating success
881
+ *
882
+ * @example
883
+ * ```typescript
884
+ * return StepHandlerResult.success(
885
+ * { processed: 100, skipped: 5 }
886
+ * );
887
+ * ```
888
+ */
889
+ static success(result, metadata) {
890
+ return new _StepHandlerResult({
891
+ success: true,
892
+ result,
893
+ metadata: metadata ?? {}
894
+ });
895
+ }
896
+ /**
897
+ * Create a failure handler result.
898
+ *
899
+ * @param message - Human-readable error message
900
+ * @param errorType - Error type/category for classification. Use ErrorType enum.
901
+ * @param retryable - Whether the error is retryable (default: true)
902
+ * @param metadata - Optional additional metadata
903
+ * @param errorCode - Optional application-specific error code
904
+ * @returns A StepHandlerResult indicating failure
905
+ *
906
+ * @example
907
+ * ```typescript
908
+ * return StepHandlerResult.failure(
909
+ * 'Invalid input format',
910
+ * ErrorType.VALIDATION_ERROR,
911
+ * false
912
+ * );
913
+ * ```
914
+ *
915
+ * @example With error code
916
+ * ```typescript
917
+ * return StepHandlerResult.failure(
918
+ * 'Gateway timeout',
919
+ * ErrorType.TIMEOUT,
920
+ * true,
921
+ * { duration_ms: 30000 },
922
+ * 'GATEWAY_TIMEOUT'
923
+ * );
924
+ * ```
925
+ */
926
+ static failure(message, errorType = "handler_error" /* HANDLER_ERROR */, retryable = true, metadata, errorCode) {
927
+ return new _StepHandlerResult({
928
+ success: false,
929
+ errorMessage: message,
930
+ // ErrorType enum values are already strings, so this works directly
931
+ errorType,
932
+ errorCode: errorCode ?? null,
933
+ retryable,
934
+ metadata: metadata ?? {}
935
+ });
936
+ }
937
+ /**
938
+ * Check if this result indicates success.
939
+ */
940
+ isSuccess() {
941
+ return this.success;
942
+ }
943
+ /**
944
+ * Check if this result indicates failure.
945
+ */
946
+ isFailure() {
947
+ return !this.success;
948
+ }
949
+ /**
950
+ * Convert to JSON for serialization.
951
+ *
952
+ * Uses snake_case keys to match the Rust FFI contract.
953
+ */
954
+ toJSON() {
955
+ return {
956
+ success: this.success,
957
+ result: this.result,
958
+ error_message: this.errorMessage,
959
+ error_type: this.errorType,
960
+ error_code: this.errorCode,
961
+ retryable: this.retryable,
962
+ metadata: this.metadata,
963
+ orchestration_metadata: this.orchestrationMetadata
964
+ };
965
+ }
966
+ };
967
+
888
968
  // src/subscriber/step-execution-subscriber.ts
889
969
  var loggerOptions2 = {
890
970
  name: "step-subscriber",
@@ -900,7 +980,7 @@ var pinoLog = pino(loggerOptions2);
900
980
  var StepExecutionSubscriber = class {
901
981
  emitter;
902
982
  registry;
903
- runtime;
983
+ module;
904
984
  workerId;
905
985
  maxConcurrent;
906
986
  handlerTimeoutMs;
@@ -913,13 +993,13 @@ var StepExecutionSubscriber = class {
913
993
  *
914
994
  * @param emitter - The event emitter to subscribe to (required, no fallback)
915
995
  * @param registry - The handler registry for resolving step handlers
916
- * @param runtime - The FFI runtime for submitting results (required, no fallback)
996
+ * @param module - The napi-rs module for submitting results (required, no fallback)
917
997
  * @param config - Optional configuration for execution behavior
918
998
  */
919
- constructor(emitter, registry, runtime, config = {}) {
999
+ constructor(emitter, registry, module, config = {}) {
920
1000
  this.emitter = emitter;
921
1001
  this.registry = registry;
922
- this.runtime = runtime;
1002
+ this.module = module;
923
1003
  this.workerId = config.workerId ?? `typescript-worker-${process.pid}`;
924
1004
  this.maxConcurrent = config.maxConcurrent ?? 10;
925
1005
  this.handlerTimeoutMs = config.handlerTimeoutMs ?? 3e5;
@@ -956,8 +1036,8 @@ var StepExecutionSubscriber = class {
956
1036
  pinoLog.info(
957
1037
  {
958
1038
  component: "subscriber",
959
- eventId: payload.event.event_id,
960
- stepUuid: payload.event.step_uuid
1039
+ eventId: payload.event.eventId,
1040
+ stepUuid: payload.event.stepUuid
961
1041
  },
962
1042
  "Received step event in subscriber callback!"
963
1043
  );
@@ -1057,7 +1137,7 @@ var StepExecutionSubscriber = class {
1057
1137
  pinoLog.info(
1058
1138
  {
1059
1139
  component: "subscriber",
1060
- eventId: event.event_id,
1140
+ eventId: event.eventId,
1061
1141
  running: this.running,
1062
1142
  activeHandlers: this.activeHandlers,
1063
1143
  maxConcurrent: this.maxConcurrent
@@ -1066,7 +1146,7 @@ var StepExecutionSubscriber = class {
1066
1146
  );
1067
1147
  if (!this.running) {
1068
1148
  pinoLog.warn(
1069
- { component: "subscriber", eventId: event.event_id },
1149
+ { component: "subscriber", eventId: event.eventId },
1070
1150
  "Received event while stopped, ignoring"
1071
1151
  );
1072
1152
  return;
@@ -1083,14 +1163,14 @@ var StepExecutionSubscriber = class {
1083
1163
  return;
1084
1164
  }
1085
1165
  pinoLog.info(
1086
- { component: "subscriber", eventId: event.event_id },
1166
+ { component: "subscriber", eventId: event.eventId },
1087
1167
  "About to call processEvent()"
1088
1168
  );
1089
1169
  this.processEvent(event).catch((error) => {
1090
1170
  pinoLog.error(
1091
1171
  {
1092
1172
  component: "subscriber",
1093
- eventId: event.event_id,
1173
+ eventId: event.eventId,
1094
1174
  error: error instanceof Error ? error.message : String(error),
1095
1175
  stack: error instanceof Error ? error.stack : void 0
1096
1176
  },
@@ -1100,37 +1180,65 @@ var StepExecutionSubscriber = class {
1100
1180
  }
1101
1181
  /**
1102
1182
  * Process a step execution event.
1183
+ *
1184
+ * All paths produce a StepHandlerResult — the handler's result or a
1185
+ * system-level failure. This mirrors Python's pattern where system errors
1186
+ * (handler not found, timeout, uncaught exception) become
1187
+ * StepHandlerResult.failure() with appropriate error_type and retryable.
1103
1188
  */
1104
1189
  async processEvent(event) {
1105
- pinoLog.info({ component: "subscriber", eventId: event.event_id }, "processEvent() starting");
1190
+ pinoLog.info({ component: "subscriber", eventId: event.eventId }, "processEvent() starting");
1106
1191
  this.activeHandlers++;
1107
1192
  const startTime = Date.now();
1193
+ const { handlerResult, handlerName, handlerWasInvoked } = await this.resolveAndExecuteHandler(event);
1194
+ const executionTimeMs = Date.now() - startTime;
1195
+ await this.submitResult(event, handlerResult, executionTimeMs);
1196
+ this.emitCompletionEvents(
1197
+ event,
1198
+ handlerResult,
1199
+ handlerName,
1200
+ executionTimeMs,
1201
+ handlerWasInvoked
1202
+ );
1203
+ this.activeHandlers--;
1204
+ }
1205
+ /**
1206
+ * Resolve handler from registry and execute it, returning a StepHandlerResult.
1207
+ *
1208
+ * System-level failures (no handler name, handler not found, uncaught exception)
1209
+ * are converted to StepHandlerResult.failure() with appropriate error_type and
1210
+ * retryable — the caller always gets a result, never an exception.
1211
+ */
1212
+ async resolveAndExecuteHandler(event) {
1213
+ let handlerName = null;
1108
1214
  try {
1109
- const handlerName = this.extractHandlerName(event);
1215
+ handlerName = this.extractHandlerName(event);
1110
1216
  pinoLog.info(
1111
- { component: "subscriber", eventId: event.event_id, handlerName },
1217
+ { component: "subscriber", eventId: event.eventId, handlerName },
1112
1218
  "Extracted handler name"
1113
1219
  );
1114
1220
  if (!handlerName) {
1115
1221
  pinoLog.error(
1116
- { component: "subscriber", eventId: event.event_id },
1222
+ { component: "subscriber", eventId: event.eventId },
1117
1223
  "No handler name found!"
1118
1224
  );
1119
- await this.submitErrorResult(event, "No handler name found in step definition", startTime);
1120
- return;
1225
+ return {
1226
+ handlerResult: StepHandlerResult.failure(
1227
+ "No handler name found in step definition",
1228
+ "permanent_error" /* PERMANENT_ERROR */,
1229
+ false
1230
+ ),
1231
+ handlerName,
1232
+ handlerWasInvoked: false
1233
+ };
1121
1234
  }
1122
1235
  pinoLog.info(
1123
- {
1124
- component: "subscriber",
1125
- eventId: event.event_id,
1126
- stepUuid: event.step_uuid,
1127
- handlerName
1128
- },
1236
+ { component: "subscriber", eventId: event.eventId, stepUuid: event.stepUuid, handlerName },
1129
1237
  "Processing step event"
1130
1238
  );
1131
1239
  this.emitter.emit(StepEventNames.STEP_EXECUTION_STARTED, {
1132
- eventId: event.event_id,
1133
- stepUuid: event.step_uuid,
1240
+ eventId: event.eventId,
1241
+ stepUuid: event.stepUuid,
1134
1242
  handlerName,
1135
1243
  timestamp: /* @__PURE__ */ new Date()
1136
1244
  });
@@ -1142,64 +1250,80 @@ var StepExecutionSubscriber = class {
1142
1250
  );
1143
1251
  if (!handler) {
1144
1252
  pinoLog.error({ component: "subscriber", handlerName }, "Handler not found in registry!");
1145
- await this.submitErrorResult(event, `Handler not found: ${handlerName}`, startTime);
1146
- return;
1147
- }
1148
- pinoLog.info({ component: "subscriber", handlerName }, "Creating StepContext from FFI event");
1149
- const context = StepContext.fromFfiEvent(event, handlerName);
1150
- pinoLog.info(
1151
- { component: "subscriber", handlerName },
1152
- "StepContext created, executing handler"
1153
- );
1154
- const result = await this.executeWithTimeout(
1155
- () => handler.call(context),
1156
- this.handlerTimeoutMs
1157
- );
1158
- pinoLog.info(
1159
- { component: "subscriber", handlerName, success: result.success },
1160
- "Handler execution completed"
1161
- );
1162
- const executionTimeMs = Date.now() - startTime;
1163
- await this.submitResult(event, result, executionTimeMs);
1164
- if (result.success) {
1165
- this.emitter.emit(StepEventNames.STEP_EXECUTION_COMPLETED, {
1166
- eventId: event.event_id,
1167
- stepUuid: event.step_uuid,
1253
+ return {
1254
+ handlerResult: StepHandlerResult.failure(
1255
+ `Handler not found: ${handlerName}`,
1256
+ "handler_not_found",
1257
+ false
1258
+ ),
1168
1259
  handlerName,
1169
- executionTimeMs,
1170
- timestamp: /* @__PURE__ */ new Date()
1171
- });
1172
- } else {
1173
- this.emitter.emit(StepEventNames.STEP_EXECUTION_FAILED, {
1174
- eventId: event.event_id,
1175
- stepUuid: event.step_uuid,
1176
- handlerName,
1177
- error: result.errorMessage,
1178
- executionTimeMs,
1179
- timestamp: /* @__PURE__ */ new Date()
1180
- });
1260
+ handlerWasInvoked: false
1261
+ };
1181
1262
  }
1182
- this.processedCount++;
1263
+ return await this.invokeHandler(event, handler, handlerName);
1183
1264
  } catch (error) {
1184
- this.errorCount++;
1185
1265
  const errorMessage = error instanceof Error ? error.message : String(error);
1266
+ const errorTypeName = error instanceof Error ? error.constructor.name : "Error";
1186
1267
  logError("Handler execution failed", {
1187
1268
  component: "subscriber",
1188
- event_id: event.event_id,
1189
- step_uuid: event.step_uuid,
1269
+ event_id: event.eventId,
1270
+ step_uuid: event.stepUuid,
1190
1271
  error_message: errorMessage
1191
1272
  });
1192
- await this.submitErrorResult(event, errorMessage, startTime);
1193
- this.emitter.emit(StepEventNames.STEP_EXECUTION_FAILED, {
1194
- eventId: event.event_id,
1195
- stepUuid: event.step_uuid,
1196
- error: errorMessage,
1197
- executionTimeMs: Date.now() - startTime,
1198
- timestamp: /* @__PURE__ */ new Date()
1199
- });
1200
- } finally {
1201
- this.activeHandlers--;
1273
+ return {
1274
+ handlerResult: StepHandlerResult.failure(errorMessage, errorTypeName, true, {
1275
+ traceback: error instanceof Error ? error.stack : void 0
1276
+ }),
1277
+ handlerName,
1278
+ handlerWasInvoked: false
1279
+ };
1280
+ }
1281
+ }
1282
+ /**
1283
+ * Create context and invoke the handler, returning its result.
1284
+ *
1285
+ * handlerWasInvoked is true only if the handler returned (not threw).
1286
+ */
1287
+ async invokeHandler(event, handler, handlerName) {
1288
+ pinoLog.info({ component: "subscriber", handlerName }, "Creating StepContext from FFI event");
1289
+ const context = StepContext.fromFfiEvent(event, handlerName);
1290
+ pinoLog.info(
1291
+ { component: "subscriber", handlerName },
1292
+ "StepContext created, executing handler"
1293
+ );
1294
+ const handlerResult = await this.executeWithTimeout(
1295
+ () => handler.call(context),
1296
+ this.handlerTimeoutMs
1297
+ );
1298
+ pinoLog.info(
1299
+ { component: "subscriber", handlerName, success: handlerResult.success },
1300
+ "Handler execution completed"
1301
+ );
1302
+ return { handlerResult, handlerName, handlerWasInvoked: true };
1303
+ }
1304
+ /**
1305
+ * Update counters and emit observability events after step completion.
1306
+ *
1307
+ * A handler returning failure() is still "processed" — the handler ran and
1308
+ * gave a definitive answer. Only system-level errors (handler not found,
1309
+ * timeout, uncaught exception) count as "errors".
1310
+ */
1311
+ emitCompletionEvents(event, handlerResult, handlerName, executionTimeMs, handlerWasInvoked) {
1312
+ const name = handlerName ?? "unknown";
1313
+ if (handlerResult.success || handlerWasInvoked) {
1314
+ this.processedCount++;
1315
+ } else {
1316
+ this.errorCount++;
1202
1317
  }
1318
+ const eventName = handlerResult.success ? StepEventNames.STEP_EXECUTION_COMPLETED : StepEventNames.STEP_EXECUTION_FAILED;
1319
+ this.emitter.emit(eventName, {
1320
+ eventId: event.eventId,
1321
+ stepUuid: event.stepUuid,
1322
+ handlerName: name,
1323
+ ...handlerResult.success ? {} : { error: handlerResult.errorMessage },
1324
+ executionTimeMs,
1325
+ timestamp: /* @__PURE__ */ new Date()
1326
+ });
1203
1327
  }
1204
1328
  /**
1205
1329
  * Execute a function with a timeout.
@@ -1218,18 +1342,10 @@ var StepExecutionSubscriber = class {
1218
1342
  /**
1219
1343
  * Extract handler name from FFI event.
1220
1344
  *
1221
- * The handler name is in step_definition.handler.callable
1345
+ * TAS-290: With napi-rs, handler callable is flattened to stepDefinition.handlerCallable
1222
1346
  */
1223
1347
  extractHandlerName(event) {
1224
- const stepDefinition = event.step_definition;
1225
- if (!stepDefinition) {
1226
- return null;
1227
- }
1228
- const handler = stepDefinition.handler;
1229
- if (!handler) {
1230
- return null;
1231
- }
1232
- return handler.callable || null;
1348
+ return event.stepDefinition?.handlerCallable || null;
1233
1349
  }
1234
1350
  /**
1235
1351
  * Submit a handler result via FFI.
@@ -1238,23 +1354,13 @@ var StepExecutionSubscriber = class {
1238
1354
  * instead of the normal completion path.
1239
1355
  */
1240
1356
  async submitResult(event, result, executionTimeMs) {
1241
- pinoLog.info(
1242
- { component: "subscriber", eventId: event.event_id, runtimeLoaded: this.runtime.isLoaded },
1243
- "submitResult() called"
1244
- );
1245
- if (!this.runtime.isLoaded) {
1246
- pinoLog.error(
1247
- { component: "subscriber", eventId: event.event_id },
1248
- "Cannot submit result: runtime not loaded!"
1249
- );
1250
- return;
1251
- }
1357
+ pinoLog.info({ component: "subscriber", eventId: event.eventId }, "submitResult() called");
1252
1358
  if (result.metadata?.checkpoint_yield === true) {
1253
1359
  await this.submitCheckpointYield(event, result);
1254
1360
  return;
1255
1361
  }
1256
- const executionResult = this.buildExecutionResult(event, result, executionTimeMs);
1257
- await this.sendCompletionViaFfi(event, executionResult, result.success);
1362
+ const serdeResult = this.buildStepExecutionResult(event, result, executionTimeMs);
1363
+ await this.sendCompletionViaFfi(event, serdeResult, result.success);
1258
1364
  }
1259
1365
  /**
1260
1366
  * TAS-125: Submit a checkpoint yield via FFI.
@@ -1264,165 +1370,139 @@ var StepExecutionSubscriber = class {
1264
1370
  */
1265
1371
  async submitCheckpointYield(event, result) {
1266
1372
  pinoLog.info(
1267
- { component: "subscriber", eventId: event.event_id },
1373
+ { component: "subscriber", eventId: event.eventId },
1268
1374
  "submitCheckpointYield() called - handler yielded checkpoint"
1269
1375
  );
1270
1376
  const resultData = result.result ?? {};
1271
1377
  const checkpointData = {
1272
- step_uuid: event.step_uuid,
1378
+ stepUuid: event.stepUuid,
1273
1379
  cursor: resultData.cursor ?? 0,
1274
- items_processed: resultData.items_processed ?? 0
1380
+ itemsProcessed: resultData.items_processed ?? 0
1275
1381
  };
1276
1382
  const accumulatedResults = resultData.accumulated_results;
1277
1383
  if (accumulatedResults !== void 0) {
1278
- checkpointData.accumulated_results = accumulatedResults;
1384
+ checkpointData.accumulatedResults = accumulatedResults;
1279
1385
  }
1280
1386
  try {
1281
- const success = this.runtime.checkpointYieldStepEvent(event.event_id, checkpointData);
1387
+ const success = this.module.checkpointYieldStepEvent(event.eventId, checkpointData);
1282
1388
  if (success) {
1283
1389
  pinoLog.info(
1284
1390
  {
1285
1391
  component: "subscriber",
1286
- eventId: event.event_id,
1392
+ eventId: event.eventId,
1287
1393
  cursor: checkpointData.cursor,
1288
- itemsProcessed: checkpointData.items_processed
1394
+ itemsProcessed: checkpointData.itemsProcessed
1289
1395
  },
1290
1396
  "Checkpoint yield submitted successfully - step will be re-dispatched"
1291
1397
  );
1292
1398
  this.emitter.emit(StepEventNames.STEP_CHECKPOINT_YIELD_SENT, {
1293
- eventId: event.event_id,
1294
- stepUuid: event.step_uuid,
1399
+ eventId: event.eventId,
1400
+ stepUuid: event.stepUuid,
1295
1401
  cursor: checkpointData.cursor,
1296
- itemsProcessed: checkpointData.items_processed,
1402
+ itemsProcessed: checkpointData.itemsProcessed,
1297
1403
  timestamp: /* @__PURE__ */ new Date()
1298
1404
  });
1299
1405
  logInfo("Checkpoint yield submitted", {
1300
1406
  component: "subscriber",
1301
- event_id: event.event_id,
1302
- step_uuid: event.step_uuid,
1407
+ event_id: event.eventId,
1408
+ step_uuid: event.stepUuid,
1303
1409
  cursor: String(checkpointData.cursor),
1304
- items_processed: String(checkpointData.items_processed)
1410
+ items_processed: String(checkpointData.itemsProcessed)
1305
1411
  });
1306
1412
  } else {
1307
1413
  pinoLog.error(
1308
- { component: "subscriber", eventId: event.event_id },
1414
+ { component: "subscriber", eventId: event.eventId },
1309
1415
  "Checkpoint yield rejected by Rust - event may not be in pending map"
1310
1416
  );
1311
1417
  logError("Checkpoint yield rejected", {
1312
1418
  component: "subscriber",
1313
- event_id: event.event_id,
1314
- step_uuid: event.step_uuid
1419
+ event_id: event.eventId,
1420
+ step_uuid: event.stepUuid
1315
1421
  });
1316
1422
  }
1317
1423
  } catch (error) {
1318
1424
  pinoLog.error(
1319
1425
  {
1320
1426
  component: "subscriber",
1321
- eventId: event.event_id,
1427
+ eventId: event.eventId,
1322
1428
  error: error instanceof Error ? error.message : String(error)
1323
1429
  },
1324
1430
  "Checkpoint yield failed with error"
1325
1431
  );
1326
1432
  logError("Failed to submit checkpoint yield", {
1327
1433
  component: "subscriber",
1328
- event_id: event.event_id,
1434
+ event_id: event.eventId,
1329
1435
  error_message: error instanceof Error ? error.message : String(error)
1330
1436
  });
1331
1437
  }
1332
1438
  }
1333
1439
  /**
1334
- * Submit an error result via FFI (for handler resolution/execution failures).
1335
- */
1336
- async submitErrorResult(event, errorMessage, startTime) {
1337
- if (!this.runtime.isLoaded) {
1338
- logError("Cannot submit error result: runtime not available", {
1339
- component: "subscriber",
1340
- event_id: event.event_id
1341
- });
1342
- return;
1343
- }
1344
- const executionTimeMs = Date.now() - startTime;
1345
- const executionResult = this.buildErrorExecutionResult(event, errorMessage, executionTimeMs);
1346
- const accepted = await this.sendCompletionViaFfi(event, executionResult, false);
1347
- if (accepted) {
1348
- this.errorCount++;
1349
- }
1350
- }
1351
- /**
1352
- * Build a StepExecutionResult from a handler result.
1440
+ * Build a NapiStepExecutionResult from a StepHandlerResult.
1441
+ *
1442
+ * The subscriber's only job here is to WRAP the handler result with
1443
+ * execution metadata (timing, worker_id, step_uuid). All handler decisions
1444
+ * (success, retryable, errorType, errorCode, orchestrationMetadata) are
1445
+ * passed through faithfully — the subscriber does not re-interpret them.
1353
1446
  *
1354
- * IMPORTANT: metadata.retryable must be set for Rust's is_retryable() to work correctly.
1447
+ * This mirrors Python's _submit_result() which calls
1448
+ * StepExecutionResult.success_result() or .failure_result() passing
1449
+ * handler fields straight through.
1450
+ *
1451
+ * napi-rs #[napi(object)] maps Option<T> to `?: T`. With
1452
+ * exactOptionalPropertyTypes, optional props must be OMITTED (not null
1453
+ * or undefined) — hence the conditional spread pattern.
1355
1454
  */
1356
- buildExecutionResult(event, result, executionTimeMs) {
1357
- const executionResult = {
1358
- step_uuid: event.step_uuid,
1455
+ buildStepExecutionResult(event, result, executionTimeMs) {
1456
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1457
+ const metadata = {
1458
+ executionTimeMs,
1459
+ completedAt: now,
1460
+ ...this.workerId != null && { workerId: this.workerId },
1461
+ ...Object.keys(result.metadata).length > 0 && { custom: result.metadata },
1462
+ // Pass through handler's classification — subscriber doesn't interpret these
1463
+ ...result.retryable != null && { retryable: result.retryable },
1464
+ ...result.errorType != null && { errorType: result.errorType },
1465
+ ...result.errorCode != null && { errorCode: result.errorCode }
1466
+ };
1467
+ const napiResult = {
1468
+ stepUuid: event.stepUuid,
1359
1469
  success: result.success,
1360
1470
  result: result.result ?? {},
1361
- metadata: {
1362
- execution_time_ms: executionTimeMs,
1363
- worker_id: this.workerId,
1364
- handler_name: this.extractHandlerName(event) ?? "unknown",
1365
- attempt_number: event.workflow_step?.attempts ?? 1,
1366
- retryable: result.retryable ?? false,
1367
- ...result.metadata
1368
- },
1369
- status: result.success ? "completed" : "failed"
1471
+ status: result.success ? "completed" : "failed",
1472
+ metadata
1370
1473
  };
1371
1474
  if (!result.success) {
1372
- executionResult.error = {
1475
+ napiResult.error = {
1373
1476
  message: result.errorMessage ?? "Unknown error",
1374
- error_type: result.errorType ?? "handler_error",
1375
- retryable: result.retryable,
1376
- status_code: null,
1377
- backtrace: null
1477
+ ...result.errorType != null && { errorType: result.errorType },
1478
+ ...result.retryable != null && { retryable: result.retryable }
1378
1479
  };
1379
1480
  }
1380
- return executionResult;
1381
- }
1382
- /**
1383
- * Build an error StepExecutionResult for handler resolution/execution failures.
1384
- *
1385
- * IMPORTANT: metadata.retryable must be set for Rust's is_retryable() to work correctly.
1386
- */
1387
- buildErrorExecutionResult(event, errorMessage, executionTimeMs) {
1388
- return {
1389
- step_uuid: event.step_uuid,
1390
- success: false,
1391
- result: {},
1392
- metadata: {
1393
- execution_time_ms: executionTimeMs,
1394
- worker_id: this.workerId,
1395
- retryable: true
1396
- },
1397
- status: "error",
1398
- error: {
1399
- message: errorMessage,
1400
- error_type: "handler_error",
1401
- retryable: true,
1402
- status_code: null,
1403
- backtrace: null
1404
- }
1405
- };
1481
+ if (result.orchestrationMetadata != null) {
1482
+ napiResult.orchestrationMetadata = result.orchestrationMetadata;
1483
+ }
1484
+ return napiResult;
1406
1485
  }
1407
1486
  /**
1408
1487
  * Send a completion result to Rust via FFI and handle the response.
1409
1488
  *
1410
1489
  * @returns true if the completion was accepted by Rust, false otherwise
1411
1490
  */
1412
- async sendCompletionViaFfi(event, executionResult, isSuccess) {
1491
+ async sendCompletionViaFfi(event, napiResult, isSuccess) {
1413
1492
  pinoLog.info(
1414
1493
  {
1415
1494
  component: "subscriber",
1416
- eventId: event.event_id,
1417
- stepUuid: event.step_uuid,
1418
- resultJson: JSON.stringify(executionResult)
1495
+ eventId: event.eventId,
1496
+ stepUuid: event.stepUuid,
1497
+ success: napiResult.success,
1498
+ status: napiResult.status
1419
1499
  },
1420
- "About to call runtime.completeStepEvent()"
1500
+ "About to call module.completeStepEvent()"
1421
1501
  );
1422
1502
  try {
1423
- const ffiResult = this.runtime.completeStepEvent(event.event_id, executionResult);
1503
+ const ffiResult = this.module.completeStepEvent(event.eventId, napiResult);
1424
1504
  if (ffiResult) {
1425
- this.handleFfiSuccess(event, executionResult, isSuccess);
1505
+ this.handleFfiSuccess(event, isSuccess);
1426
1506
  return true;
1427
1507
  }
1428
1508
  this.handleFfiRejection(event);
@@ -1435,23 +1515,22 @@ var StepExecutionSubscriber = class {
1435
1515
  /**
1436
1516
  * Handle successful FFI completion submission.
1437
1517
  */
1438
- handleFfiSuccess(event, executionResult, isSuccess) {
1518
+ handleFfiSuccess(event, isSuccess) {
1439
1519
  pinoLog.info(
1440
- { component: "subscriber", eventId: event.event_id, success: isSuccess },
1520
+ { component: "subscriber", eventId: event.eventId, success: isSuccess },
1441
1521
  "completeStepEvent() returned TRUE - completion accepted by Rust"
1442
1522
  );
1443
1523
  this.emitter.emit(StepEventNames.STEP_COMPLETION_SENT, {
1444
- eventId: event.event_id,
1445
- stepUuid: event.step_uuid,
1524
+ eventId: event.eventId,
1525
+ stepUuid: event.stepUuid,
1446
1526
  success: isSuccess,
1447
1527
  timestamp: /* @__PURE__ */ new Date()
1448
1528
  });
1449
1529
  logDebug("Step result submitted", {
1450
1530
  component: "subscriber",
1451
- event_id: event.event_id,
1452
- step_uuid: event.step_uuid,
1453
- success: String(isSuccess),
1454
- execution_time_ms: String(executionResult.metadata.execution_time_ms)
1531
+ event_id: event.eventId,
1532
+ step_uuid: event.stepUuid,
1533
+ success: String(isSuccess)
1455
1534
  });
1456
1535
  }
1457
1536
  /**
@@ -1461,15 +1540,15 @@ var StepExecutionSubscriber = class {
1461
1540
  pinoLog.error(
1462
1541
  {
1463
1542
  component: "subscriber",
1464
- eventId: event.event_id,
1465
- stepUuid: event.step_uuid
1543
+ eventId: event.eventId,
1544
+ stepUuid: event.stepUuid
1466
1545
  },
1467
1546
  "completeStepEvent() returned FALSE - completion REJECTED by Rust! Event may not be in pending map."
1468
1547
  );
1469
1548
  logError("FFI completion rejected", {
1470
1549
  component: "subscriber",
1471
- event_id: event.event_id,
1472
- step_uuid: event.step_uuid
1550
+ event_id: event.eventId,
1551
+ step_uuid: event.stepUuid
1473
1552
  });
1474
1553
  }
1475
1554
  /**
@@ -1479,7 +1558,7 @@ var StepExecutionSubscriber = class {
1479
1558
  pinoLog.error(
1480
1559
  {
1481
1560
  component: "subscriber",
1482
- eventId: event.event_id,
1561
+ eventId: event.eventId,
1483
1562
  error: error instanceof Error ? error.message : String(error),
1484
1563
  stack: error instanceof Error ? error.stack : void 0
1485
1564
  },
@@ -1487,7 +1566,7 @@ var StepExecutionSubscriber = class {
1487
1566
  );
1488
1567
  logError("Failed to submit step result", {
1489
1568
  component: "subscriber",
1490
- event_id: event.event_id,
1569
+ event_id: event.eventId,
1491
1570
  error_message: error instanceof Error ? error.message : String(error)
1492
1571
  });
1493
1572
  }
@@ -1513,17 +1592,17 @@ var EventSystem = class {
1513
1592
  /**
1514
1593
  * Create a new EventSystem.
1515
1594
  *
1516
- * @param runtime - The FFI runtime for polling events and submitting results
1595
+ * @param module - The napi-rs module for polling events and submitting results
1517
1596
  * @param registry - The handler registry for resolving step handlers
1518
1597
  * @param config - Optional configuration for poller and subscriber
1519
1598
  */
1520
- constructor(runtime, registry, config = {}) {
1599
+ constructor(module, registry, config = {}) {
1521
1600
  this.emitter = new TaskerEventEmitter();
1522
- this.poller = new EventPoller(runtime, this.emitter, config.poller);
1601
+ this.poller = new EventPoller(module, this.emitter, config.poller);
1523
1602
  this.subscriber = new StepExecutionSubscriber(
1524
1603
  this.emitter,
1525
1604
  registry,
1526
- runtime,
1605
+ module,
1527
1606
  config.subscriber
1528
1607
  );
1529
1608
  }
@@ -1553,11 +1632,11 @@ var EventSystem = class {
1553
1632
  {
1554
1633
  component: "event-system",
1555
1634
  debugListener: true,
1556
- eventId: payload.event?.event_id,
1557
- stepUuid: payload.event?.step_uuid,
1635
+ eventId: payload.event?.eventId,
1636
+ stepUuid: payload.event?.stepUuid,
1558
1637
  eventName: StepEventNames.STEP_EXECUTION_RECEIVED
1559
1638
  },
1560
- `\u{1F50D} DEBUG LISTENER: Received ${StepEventNames.STEP_EXECUTION_RECEIVED} event!`
1639
+ `DEBUG LISTENER: Received ${StepEventNames.STEP_EXECUTION_RECEIVED} event!`
1561
1640
  );
1562
1641
  }
1563
1642
  );