@pikku/core 0.12.19 → 0.12.21

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 (146) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/function-runner.js +53 -3
  6. package/dist/function/functions.types.d.ts +3 -2
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/content-service.d.ts +66 -37
  21. package/dist/services/in-memory-queue-service.js +1 -1
  22. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  23. package/dist/services/in-memory-workflow-service.js +31 -13
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/local-content.d.ts +10 -12
  26. package/dist/services/local-content.js +54 -38
  27. package/dist/services/user-session-service.d.ts +1 -1
  28. package/dist/testing/service-tests.js +24 -0
  29. package/dist/types/core.types.d.ts +4 -0
  30. package/dist/types/state.types.d.ts +2 -18
  31. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  32. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  33. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  34. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  35. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  36. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  37. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  38. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  39. package/dist/wirings/channel/channel-handler.js +1 -1
  40. package/dist/wirings/channel/channel-store.d.ts +13 -0
  41. package/dist/wirings/channel/channel.types.d.ts +3 -0
  42. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  43. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  44. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  45. package/dist/wirings/cli/cli-runner.js +24 -5
  46. package/dist/wirings/cli/command-parser.js +19 -4
  47. package/dist/wirings/http/http-runner.js +72 -36
  48. package/dist/wirings/http/http.types.d.ts +1 -0
  49. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  50. package/dist/wirings/http/web-request.js +32 -0
  51. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  52. package/dist/wirings/rpc/rpc-runner.js +45 -11
  53. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  54. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  55. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  56. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  57. package/dist/wirings/workflow/index.d.ts +1 -1
  58. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  59. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  60. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  61. package/package.json +1 -1
  62. package/run-tests.sh +4 -1
  63. package/src/dev/hot-reload.test.ts +62 -11
  64. package/src/dev/hot-reload.ts +28 -7
  65. package/src/errors/error-handler.ts +8 -2
  66. package/src/errors/error.test.ts +2 -0
  67. package/src/function/function-runner.test.ts +500 -10
  68. package/src/function/function-runner.ts +68 -3
  69. package/src/function/functions.types.ts +3 -2
  70. package/src/index.ts +22 -3
  71. package/src/middleware/index.ts +11 -2
  72. package/src/middleware/telemetry.ts +2 -2
  73. package/src/middleware-runner.test.ts +16 -16
  74. package/src/middleware-runner.ts +42 -30
  75. package/src/permissions.test.ts +27 -24
  76. package/src/permissions.ts +41 -25
  77. package/src/pikku-request.test.ts +35 -0
  78. package/src/pikku-request.ts +1 -1
  79. package/src/pikku-state.ts +2 -3
  80. package/src/run-tests-script.test.ts +18 -0
  81. package/src/services/ai-agent-runner-service.ts +1 -0
  82. package/src/services/content-service.ts +79 -51
  83. package/src/services/in-memory-queue-service.ts +1 -1
  84. package/src/services/in-memory-session-store.ts +1 -2
  85. package/src/services/in-memory-workflow-service.test.ts +33 -11
  86. package/src/services/in-memory-workflow-service.ts +49 -13
  87. package/src/services/index.ts +10 -1
  88. package/src/services/local-content.test.ts +54 -0
  89. package/src/services/local-content.ts +80 -53
  90. package/src/services/typed-credential-service.ts +3 -3
  91. package/src/services/typed-secret-service.ts +3 -3
  92. package/src/services/typed-variables-service.ts +3 -3
  93. package/src/services/user-session-service.ts +4 -4
  94. package/src/testing/service-tests.ts +30 -0
  95. package/src/types/core.types.ts +6 -2
  96. package/src/types/state.types.ts +2 -13
  97. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  98. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  99. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  100. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  101. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  102. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  103. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  104. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  105. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  106. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  107. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  108. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  109. package/src/wirings/channel/channel-handler.test.ts +272 -0
  110. package/src/wirings/channel/channel-handler.ts +1 -1
  111. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  112. package/src/wirings/channel/channel-store.ts +19 -0
  113. package/src/wirings/channel/channel.types.ts +4 -0
  114. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  115. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  116. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  117. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  118. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  119. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  120. package/src/wirings/cli/cli-runner.test.ts +255 -2
  121. package/src/wirings/cli/cli-runner.ts +31 -10
  122. package/src/wirings/cli/cli.types.ts +4 -8
  123. package/src/wirings/cli/command-parser.test.ts +83 -0
  124. package/src/wirings/cli/command-parser.ts +23 -4
  125. package/src/wirings/http/http-runner.test.ts +296 -1
  126. package/src/wirings/http/http-runner.ts +87 -57
  127. package/src/wirings/http/http.types.ts +11 -26
  128. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  129. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  130. package/src/wirings/http/web-request.test.ts +115 -0
  131. package/src/wirings/http/web-request.ts +43 -5
  132. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  133. package/src/wirings/queue/queue.types.ts +2 -5
  134. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  135. package/src/wirings/rpc/rpc-runner.ts +60 -21
  136. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  137. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  138. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  139. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  140. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  141. package/src/wirings/workflow/index.ts +1 -0
  142. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  143. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  144. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  145. package/src/wirings/workflow/workflow.types.ts +68 -5
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -51,6 +51,7 @@ import type {
51
51
  StepStatus,
52
52
  WorkflowPlannedStep,
53
53
  WorkflowRun,
54
+ WorkflowRunMirror,
54
55
  WorkflowRunStatus,
55
56
  WorkflowRunWire,
56
57
  WorkflowStatus,
@@ -131,6 +132,18 @@ addError(WorkflowRunNotFoundError, {
131
132
  message: 'Workflow run not found.',
132
133
  })
133
134
 
135
+ export class WorkflowRunFailedError extends PikkuError {
136
+ public payload: { message?: string }
137
+ constructor(message?: string) {
138
+ super(`Workflow run failed: ${message ?? 'unknown'}`)
139
+ this.payload = { message }
140
+ }
141
+ }
142
+ addError(WorkflowRunFailedError, {
143
+ status: 422,
144
+ message: 'Workflow run failed.',
145
+ })
146
+
134
147
  export class WorkflowServiceNotInitialized extends Error {}
135
148
  export class WorkflowStepNameNotString extends Error {
136
149
  constructor(stepName: any) {
@@ -157,7 +170,43 @@ export abstract class PikkuWorkflowService implements WorkflowService {
157
170
  return getSingletonServices()?.logger
158
171
  }
159
172
 
160
- constructor() {
173
+ protected mirror?: WorkflowRunMirror
174
+
175
+ constructor(
176
+ options: { wireQueues?: boolean; mirror?: WorkflowRunMirror } = {}
177
+ ) {
178
+ const wireQueues = options.wireQueues ?? true
179
+ this.mirror = options.mirror
180
+ if (wireQueues) {
181
+ this.wireQueueWorkers()
182
+ }
183
+ }
184
+
185
+ private async safeMirror(fn: () => Promise<void>): Promise<void> {
186
+ if (!this.mirror) return
187
+ try {
188
+ await fn()
189
+ } catch (err: any) {
190
+ try {
191
+ this.logger?.warn?.(
192
+ `[pikku] WorkflowRunMirror write failed: ${err?.message ?? err}`
193
+ )
194
+ } catch {
195
+ // logger unavailable (e.g. singleton services not initialized) — swallow
196
+ }
197
+ }
198
+ }
199
+
200
+ public rewireQueueWorkers(): void {
201
+ this.wireQueueWorkers()
202
+ }
203
+
204
+ /**
205
+ * Wire the queue-based orchestrator/step/sleeper workers.
206
+ * Subclasses that orchestrate without queues (e.g. Durable Objects) should
207
+ * pass `wireQueues: false` to the base constructor and skip this entirely.
208
+ */
209
+ protected wireQueueWorkers(): void {
161
210
  const functions = pikkuState(null, 'function', 'functions')
162
211
  const functionsMeta = pikkuState(null, 'function', 'meta')
163
212
 
@@ -237,6 +286,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
237
286
  func: pikkuWorkflowSleeperFunc,
238
287
  })
239
288
  }
289
+ if (!functionsMeta.pikkuWorkflowSleeper) {
290
+ functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper')
291
+ }
240
292
  }
241
293
 
242
294
  /**
@@ -268,7 +320,40 @@ export abstract class PikkuWorkflowService implements WorkflowService {
268
320
  }
269
321
  }
270
322
 
271
- abstract createRun(
323
+ public async createRun(
324
+ workflowName: string,
325
+ input: any,
326
+ inline: boolean,
327
+ graphHash: string,
328
+ wire: WorkflowRunWire,
329
+ options?: {
330
+ deterministic?: boolean
331
+ plannedSteps?: WorkflowPlannedStep[]
332
+ }
333
+ ): Promise<string> {
334
+ const runId = await this.createRunImpl(
335
+ workflowName,
336
+ input,
337
+ inline,
338
+ graphHash,
339
+ wire,
340
+ options
341
+ )
342
+ await this.safeMirror(() =>
343
+ this.mirror!.createRun(
344
+ runId,
345
+ workflowName,
346
+ input,
347
+ inline,
348
+ graphHash,
349
+ wire,
350
+ options
351
+ )
352
+ )
353
+ return runId
354
+ }
355
+
356
+ protected abstract createRunImpl(
272
357
  workflowName: string,
273
358
  input: any,
274
359
  inline: boolean,
@@ -352,7 +437,19 @@ export abstract class PikkuWorkflowService implements WorkflowService {
352
437
  * @param id - Run ID
353
438
  * @param status - New status
354
439
  */
355
- abstract updateRunStatus(
440
+ public async updateRunStatus(
441
+ id: string,
442
+ status: WorkflowStatus,
443
+ output?: any,
444
+ error?: SerializedError
445
+ ): Promise<void> {
446
+ await this.updateRunStatusImpl(id, status, output, error)
447
+ await this.safeMirror(() =>
448
+ this.mirror!.updateRunStatus(id, status, output, error)
449
+ )
450
+ }
451
+
452
+ protected abstract updateRunStatusImpl(
356
453
  id: string,
357
454
  status: WorkflowStatus,
358
455
  output?: any,
@@ -369,7 +466,27 @@ export abstract class PikkuWorkflowService implements WorkflowService {
369
466
  * @param stepOptions - Step options (retries, retryDelay)
370
467
  * @returns Step state with generated stepId
371
468
  */
372
- abstract insertStepState(
469
+ public async insertStepState(
470
+ runId: string,
471
+ stepName: string,
472
+ rpcName: string | null,
473
+ data: any,
474
+ stepOptions?: WorkflowStepOptions
475
+ ): Promise<StepState> {
476
+ const step = await this.insertStepStateImpl(
477
+ runId,
478
+ stepName,
479
+ rpcName,
480
+ data,
481
+ stepOptions
482
+ )
483
+ await this.safeMirror(() =>
484
+ this.mirror!.insertStepState(runId, { ...step, stepName, rpcName, data })
485
+ )
486
+ return step
487
+ }
488
+
489
+ protected abstract insertStepStateImpl(
373
490
  runId: string,
374
491
  stepName: string,
375
492
  rpcName: string | null,
@@ -390,14 +507,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
390
507
  * Updates both workflow_step and workflow_step_history
391
508
  * @param stepId - Step ID
392
509
  */
393
- abstract setStepRunning(stepId: string): Promise<void>
510
+ public async setStepRunning(stepId: string): Promise<void> {
511
+ await this.setStepRunningImpl(stepId)
512
+ await this.safeMirror(() => this.mirror!.setStepRunning(stepId))
513
+ }
514
+
515
+ protected abstract setStepRunningImpl(stepId: string): Promise<void>
394
516
 
395
517
  /**
396
518
  * Mark step as scheduled (queued for execution)
397
519
  * Updates both workflow_step and workflow_step_history
398
520
  * @param stepId - Step ID
399
521
  */
400
- abstract setStepScheduled(stepId: string): Promise<void>
522
+ public async setStepScheduled(stepId: string): Promise<void> {
523
+ await this.setStepScheduledImpl(stepId)
524
+ await this.safeMirror(() => this.mirror!.setStepScheduled(stepId))
525
+ }
526
+
527
+ protected abstract setStepScheduledImpl(stepId: string): Promise<void>
401
528
 
402
529
  /**
403
530
  * Store step result and mark as succeeded
@@ -405,14 +532,35 @@ export abstract class PikkuWorkflowService implements WorkflowService {
405
532
  * @param stepId - Step ID
406
533
  * @param result - Step result
407
534
  */
408
- abstract setStepResult(stepId: string, result: any): Promise<void>
535
+ public async setStepResult(stepId: string, result: any): Promise<void> {
536
+ await this.setStepResultImpl(stepId, result)
537
+ await this.safeMirror(() => this.mirror!.setStepResult(stepId, result))
538
+ }
539
+
540
+ protected abstract setStepResultImpl(
541
+ stepId: string,
542
+ result: any
543
+ ): Promise<void>
409
544
 
410
545
  /**
411
546
  * Set the child workflow run ID on a step
412
547
  * @param stepId - Step ID
413
548
  * @param childRunId - Child workflow run ID
414
549
  */
415
- abstract setStepChildRunId(stepId: string, childRunId: string): Promise<void>
550
+ public async setStepChildRunId(
551
+ stepId: string,
552
+ childRunId: string
553
+ ): Promise<void> {
554
+ await this.setStepChildRunIdImpl(stepId, childRunId)
555
+ await this.safeMirror(() =>
556
+ this.mirror!.setStepChildRunId(stepId, childRunId)
557
+ )
558
+ }
559
+
560
+ protected abstract setStepChildRunIdImpl(
561
+ stepId: string,
562
+ childRunId: string
563
+ ): Promise<void>
416
564
 
417
565
  /**
418
566
  * Store step error and mark as failed
@@ -420,7 +568,20 @@ export abstract class PikkuWorkflowService implements WorkflowService {
420
568
  * @param stepId - Step ID
421
569
  * @param error - Error object
422
570
  */
423
- abstract setStepError(stepId: string, error: Error): Promise<void>
571
+ public async setStepError(stepId: string, error: Error): Promise<void> {
572
+ await this.setStepErrorImpl(stepId, error)
573
+ const serialized: SerializedError = {
574
+ message: error.message,
575
+ stack: error.stack,
576
+ code: (error as any).code,
577
+ }
578
+ await this.safeMirror(() => this.mirror!.setStepError(stepId, serialized))
579
+ }
580
+
581
+ protected abstract setStepErrorImpl(
582
+ stepId: string,
583
+ error: Error
584
+ ): Promise<void>
424
585
 
425
586
  /**
426
587
  * Create a new retry attempt for a failed step
@@ -430,7 +591,22 @@ export abstract class PikkuWorkflowService implements WorkflowService {
430
591
  * @param failedStepId - Failed step ID to copy from
431
592
  * @returns New step state for the retry attempt
432
593
  */
433
- abstract createRetryAttempt(
594
+ public async createRetryAttempt(
595
+ failedStepId: string,
596
+ status: 'pending' | 'running'
597
+ ): Promise<StepState> {
598
+ const newStep = await this.createRetryAttemptImpl(failedStepId, status)
599
+ const stepName = (newStep as any).stepName ?? ''
600
+ await this.safeMirror(() =>
601
+ this.mirror!.createRetryAttempt(failedStepId, {
602
+ ...newStep,
603
+ stepName,
604
+ })
605
+ )
606
+ return newStep
607
+ }
608
+
609
+ protected abstract createRetryAttemptImpl(
434
610
  failedStepId: string,
435
611
  status: 'pending' | 'running'
436
612
  ): Promise<StepState>
@@ -503,7 +679,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
503
679
  * @param stepId - Step ID
504
680
  * @param branchKey - Branch key selected by graph.branch()
505
681
  */
506
- abstract setBranchTaken(stepId: string, branchKey: string): Promise<void>
682
+ public async setBranchTaken(
683
+ stepId: string,
684
+ branchKey: string
685
+ ): Promise<void> {
686
+ await this.setBranchTakenImpl(stepId, branchKey)
687
+ await this.safeMirror(() => this.mirror!.setBranchTaken(stepId, branchKey))
688
+ }
689
+
690
+ protected abstract setBranchTakenImpl(
691
+ stepId: string,
692
+ branchKey: string
693
+ ): Promise<void>
507
694
 
508
695
  /**
509
696
  * Update a state variable in the workflow run's state
@@ -511,7 +698,16 @@ export abstract class PikkuWorkflowService implements WorkflowService {
511
698
  * @param name - Variable name
512
699
  * @param value - Value to store
513
700
  */
514
- abstract updateRunState(
701
+ public async updateRunState(
702
+ runId: string,
703
+ name: string,
704
+ value: unknown
705
+ ): Promise<void> {
706
+ await this.updateRunStateImpl(runId, name, value)
707
+ await this.safeMirror(() => this.mirror!.updateRunState(runId, name, value))
708
+ }
709
+
710
+ protected abstract updateRunStateImpl(
515
711
  runId: string,
516
712
  name: string,
517
713
  value: unknown
@@ -524,7 +720,20 @@ export abstract class PikkuWorkflowService implements WorkflowService {
524
720
  */
525
721
  abstract getRunState(runId: string): Promise<Record<string, unknown>>
526
722
 
527
- abstract upsertWorkflowVersion(
723
+ public async upsertWorkflowVersion(
724
+ name: string,
725
+ graphHash: string,
726
+ graph: any,
727
+ source: string,
728
+ status?: WorkflowVersionStatus
729
+ ): Promise<void> {
730
+ await this.upsertWorkflowVersionImpl(name, graphHash, graph, source, status)
731
+ await this.safeMirror(() =>
732
+ this.mirror!.upsertWorkflowVersion(name, graphHash, graph, source, status)
733
+ )
734
+ }
735
+
736
+ protected abstract upsertWorkflowVersionImpl(
528
737
  name: string,
529
738
  graphHash: string,
530
739
  graph: any,
@@ -532,7 +741,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
532
741
  status?: WorkflowVersionStatus
533
742
  ): Promise<void>
534
743
 
535
- abstract updateWorkflowVersionStatus(
744
+ public async updateWorkflowVersionStatus(
745
+ name: string,
746
+ graphHash: string,
747
+ status: WorkflowVersionStatus
748
+ ): Promise<void> {
749
+ await this.updateWorkflowVersionStatusImpl(name, graphHash, status)
750
+ await this.safeMirror(() =>
751
+ this.mirror!.updateWorkflowVersionStatus(name, graphHash, status)
752
+ )
753
+ }
754
+
755
+ protected abstract updateWorkflowVersionStatusImpl(
536
756
  name: string,
537
757
  graphHash: string,
538
758
  status: WorkflowVersionStatus
@@ -573,9 +793,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
573
793
  runId: string,
574
794
  stepName: string,
575
795
  rpcName: string,
576
- data: any
796
+ data: any,
797
+ stepOptions?: WorkflowStepOptions
577
798
  ): Promise<void> {
578
799
  const queueService = this.verifyQueueService()
800
+ const retries = stepOptions?.retries ?? 0
801
+ const retryDelay = stepOptions?.retryDelay
579
802
  await queueService.add(
580
803
  this.getStepWorkerQueueName(rpcName),
581
804
  JSON.parse(
@@ -585,7 +808,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
585
808
  rpcName,
586
809
  data,
587
810
  })
588
- )
811
+ ),
812
+ retries > 0 || retryDelay
813
+ ? {
814
+ attempts: retries + 1,
815
+ backoff:
816
+ typeof retryDelay === 'number'
817
+ ? { type: 'fixed', delay: retryDelay }
818
+ : retryDelay === 'exponential'
819
+ ? 'exponential'
820
+ : undefined,
821
+ }
822
+ : undefined
589
823
  )
590
824
  }
591
825
 
@@ -624,6 +858,81 @@ export abstract class PikkuWorkflowService implements WorkflowService {
624
858
  )
625
859
  }
626
860
 
861
+ /**
862
+ * Dispatch a workflow step to be executed asynchronously.
863
+ *
864
+ * Default implementation enqueues a step worker job via the queue service.
865
+ * Subclasses with non-queue transports (e.g. Durable Objects) override this
866
+ * to dispatch via their own mechanism (RPC to a step worker, etc.).
867
+ *
868
+ * On return, the workflow is paused via `WorkflowAsyncException` thrown by
869
+ * the caller; the step transport is responsible for calling back into the
870
+ * orchestrator (via `resumeWorkflow` or equivalent) when the step completes.
871
+ *
872
+ * @returns true if dispatch was async (caller should pause), false to fall
873
+ * through to the inline execution path.
874
+ */
875
+ protected async dispatchStep(
876
+ runId: string,
877
+ stepName: string,
878
+ rpcName: string,
879
+ data: unknown,
880
+ stepOptions?: WorkflowStepOptions
881
+ ): Promise<boolean> {
882
+ if (this.isInline(runId) || !getSingletonServices()?.queueService) {
883
+ return false
884
+ }
885
+ const retries = stepOptions?.retries ?? 0
886
+ const retryDelay = stepOptions?.retryDelay
887
+ await getSingletonServices()!.queueService!.add(
888
+ this.getStepWorkerQueueName(rpcName),
889
+ JSON.parse(
890
+ JSON.stringify({
891
+ runId,
892
+ stepName,
893
+ rpcName,
894
+ data,
895
+ })
896
+ ),
897
+ {
898
+ attempts: retries + 1,
899
+ backoff:
900
+ typeof retryDelay === 'number'
901
+ ? { type: 'fixed', delay: retryDelay }
902
+ : retryDelay === 'exponential'
903
+ ? 'exponential'
904
+ : undefined,
905
+ }
906
+ )
907
+ return true
908
+ }
909
+
910
+ /**
911
+ * Schedule a workflow sleep wakeup at the given duration.
912
+ *
913
+ * Default implementation uses the scheduler service to enqueue a delayed
914
+ * sleeper RPC. Subclasses with native timer primitives (e.g. Durable Object
915
+ * alarms) override this to schedule directly without going through queues.
916
+ *
917
+ * @returns true if the wakeup was scheduled remotely (caller should pause),
918
+ * false to fall through to inline `setTimeout` behavior.
919
+ */
920
+ protected async scheduleSleep(
921
+ runId: string,
922
+ stepId: string,
923
+ duration: number | string
924
+ ): Promise<boolean> {
925
+ if (this.isInline(runId) || !getSingletonServices()?.schedulerService) {
926
+ return false
927
+ }
928
+ await getSingletonServices()!.schedulerService!.scheduleRPC(
929
+ duration,
930
+ this.getConfig().sleeperRPCName,
931
+ { runId, stepId }
932
+ )
933
+ return true
934
+ }
935
+
627
936
  /**
628
937
  * Start a new workflow run
629
938
  * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
@@ -713,10 +1022,16 @@ export abstract class PikkuWorkflowService implements WorkflowService {
713
1022
  error.name !== 'WorkflowCancelledException' &&
714
1023
  error.name !== 'WorkflowSuspendedException'
715
1024
  ) {
1025
+ await this.updateRunStatus(runId, 'failed', undefined, {
1026
+ name: error.name,
1027
+ message: error.message,
1028
+ stack: error.stack,
1029
+ })
716
1030
  getSingletonServices()!.logger.error(
717
1031
  `Workflow ${name} (run ${runId}) failed:`,
718
1032
  error
719
1033
  )
1034
+ throw error
720
1035
  }
721
1036
  } finally {
722
1037
  this.inlineRuns.delete(runId)
@@ -749,7 +1064,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
749
1064
  }
750
1065
  if (WORKFLOW_END_STATES.has(run.status)) {
751
1066
  if (run.status === 'failed') {
752
- throw new Error(run.error?.message || 'Workflow failed')
1067
+ throw new WorkflowRunFailedError(run.error?.message)
753
1068
  }
754
1069
  if (run.status === 'cancelled') {
755
1070
  throw new Error('Workflow was cancelled')
@@ -1194,38 +1509,21 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1194
1509
  // Step is pending - schedule it
1195
1510
  await this.setStepScheduled(stepState.stepId)
1196
1511
 
1197
- // Enqueue step worker (unless inline mode)
1198
- if (!this.isInline(runId) && getSingletonServices()!.queueService) {
1199
- // Map step retry options to queue job options
1200
- const retries = stepOptions?.retries ?? 0
1201
- const retryDelay = stepOptions?.retryDelay
1202
-
1203
- await getSingletonServices()!.queueService!.add(
1204
- this.getStepWorkerQueueName(rpcName),
1205
- JSON.parse(
1206
- JSON.stringify({
1207
- runId,
1208
- stepName,
1209
- rpcName,
1210
- data,
1211
- })
1212
- ),
1213
- {
1214
- // attempts includes initial attempt, retries doesn't
1215
- attempts: retries + 1,
1216
- // Map retry delay to backoff
1217
- backoff:
1218
- typeof retryDelay === 'number'
1219
- ? { type: 'fixed', delay: retryDelay }
1220
- : retryDelay === 'exponential'
1221
- ? 'exponential'
1222
- : undefined,
1223
- }
1224
- )
1225
- // Pause workflow - step will callback when done
1512
+ // Hand off to subclass-overridable transport. Default behavior enqueues
1513
+ // via the queue service; DO-style subclasses RPC to a step worker.
1514
+ const dispatched = await this.dispatchStep(
1515
+ runId,
1516
+ stepName,
1517
+ rpcName,
1518
+ data,
1519
+ stepOptions
1520
+ )
1521
+ if (dispatched) {
1226
1522
  throw new WorkflowAsyncException(runId, stepName)
1227
- } else {
1228
- // Inline or no queue service - execute locally with retry loop
1523
+ }
1524
+
1525
+ {
1526
+ // Inline (no transport available) - execute locally with retry loop
1229
1527
  const retries = stepOptions?.retries ?? 0
1230
1528
  const retryDelay = stepOptions?.retryDelay
1231
1529
  let currentStepState = stepState
@@ -1436,27 +1734,23 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1436
1734
  // Step is pending - schedule it
1437
1735
  await this.setStepScheduled(stepState.stepId)
1438
1736
 
1439
- // Check if inline mode or no scheduler service
1440
- if (!this.isInline(runId) && getSingletonServices()!.schedulerService) {
1441
- // Remote mode - schedule sleep via scheduler service
1442
- await getSingletonServices()!.schedulerService!.scheduleRPC(
1443
- duration,
1444
- this.getConfig().sleeperRPCName,
1445
- {
1446
- runId,
1447
- stepId: stepState.stepId,
1448
- }
1449
- )
1450
- // Pause workflow - sleep will callback when done
1737
+ // Hand off to subclass-overridable transport. Default behavior schedules
1738
+ // a delayed sleeper RPC via the scheduler service; DO-style subclasses
1739
+ // override to use native timer primitives (e.g. setAlarm).
1740
+ const scheduled = await this.scheduleSleep(
1741
+ runId,
1742
+ stepState.stepId,
1743
+ duration
1744
+ )
1745
+ if (scheduled) {
1451
1746
  throw new WorkflowAsyncException(runId, stepName)
1452
- } else {
1453
- // Inline mode - use setTimeout with actual duration
1454
- await new Promise((resolve) =>
1455
- setTimeout(resolve, getDurationInMilliseconds(duration))
1456
- )
1457
- await this.setStepResult(stepState.stepId, null)
1458
- return
1459
1747
  }
1748
+
1749
+ // Inline mode - use setTimeout with actual duration
1750
+ await new Promise((resolve) =>
1751
+ setTimeout(resolve, getDurationInMilliseconds(duration))
1752
+ )
1753
+ await this.setStepResult(stepState.stepId, null)
1460
1754
  }
1461
1755
 
1462
1756
  private getSuspendStepName(reason: string): string {
@@ -1592,12 +1886,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1592
1886
  * Get the orchestrator queue name for a specific workflow.
1593
1887
  * Checks queue meta for a per-workflow queue first (e.g. wf-orchestrator-{name}),
1594
1888
  * falls back to the shared orchestrator queue.
1889
+ *
1890
+ * Reads from `queue.meta` (always populated globally) rather than
1891
+ * `queue.registrations` (only populated for queues this unit consumes).
1892
+ * In a per-unit deploy the orchestrator unit doesn't consume per-step
1893
+ * queues — but it produces to them — so registrations would miss them.
1595
1894
  */
1596
1895
  protected getOrchestratorQueueName(workflowName?: string): string {
1597
1896
  if (workflowName) {
1598
1897
  const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
1599
- const registrations = pikkuState(null, 'queue', 'registrations')
1600
- if (registrations.has(perWorkflow)) {
1898
+ const meta = pikkuState(null, 'queue', 'meta')
1899
+ if (meta[perWorkflow]) {
1601
1900
  return perWorkflow
1602
1901
  }
1603
1902
  }
@@ -1607,8 +1906,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1607
1906
  protected getStepWorkerQueueName(rpcName?: string): string {
1608
1907
  if (rpcName) {
1609
1908
  const perStep = `wf-step-${toKebab(rpcName)}`
1610
- const registrations = pikkuState(null, 'queue', 'registrations')
1611
- if (registrations.has(perStep)) {
1909
+ const meta = pikkuState(null, 'queue', 'meta')
1910
+ if (meta[perStep]) {
1612
1911
  return perStep
1613
1912
  }
1614
1913
  }