@pikku/core 0.11.1 → 0.11.2

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 (143) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/errors/error-handler.js +3 -3
  3. package/dist/function/function-runner.d.ts +4 -3
  4. package/dist/function/function-runner.js +60 -14
  5. package/dist/function/functions.types.d.ts +84 -5
  6. package/dist/function/functions.types.js +41 -1
  7. package/dist/index.d.ts +6 -2
  8. package/dist/index.js +5 -2
  9. package/dist/middleware/auth-apikey.d.ts +1 -1
  10. package/dist/middleware/auth-bearer.d.ts +1 -1
  11. package/dist/middleware/auth-cookie.d.ts +1 -1
  12. package/dist/middleware/timeout.d.ts +1 -1
  13. package/dist/middleware-runner.d.ts +3 -14
  14. package/dist/middleware-runner.js +8 -8
  15. package/dist/permissions.d.ts +3 -86
  16. package/dist/permissions.js +11 -67
  17. package/dist/pikku-state.d.ts +32 -104
  18. package/dist/pikku-state.js +140 -66
  19. package/dist/schema.d.ts +8 -6
  20. package/dist/schema.js +25 -13
  21. package/dist/services/workflow-service.d.ts +38 -0
  22. package/dist/services/workflow-service.js +1 -0
  23. package/dist/types/core.types.d.ts +12 -4
  24. package/dist/types/core.types.js +1 -1
  25. package/dist/types/state.types.d.ts +135 -0
  26. package/dist/types/state.types.js +1 -0
  27. package/dist/utils.d.ts +8 -0
  28. package/dist/utils.js +45 -0
  29. package/dist/wirings/channel/channel-handler.js +1 -1
  30. package/dist/wirings/channel/channel-runner.js +5 -5
  31. package/dist/wirings/channel/log-channels.js +1 -1
  32. package/dist/wirings/channel/serverless/serverless-channel-runner.js +2 -2
  33. package/dist/wirings/cli/channel/cli-channel-runner.js +3 -2
  34. package/dist/wirings/cli/cli-runner.js +10 -10
  35. package/dist/wirings/cli/cli.types.d.ts +1 -0
  36. package/dist/wirings/forge-node/forge-node.types.d.ts +120 -0
  37. package/dist/wirings/forge-node/forge-node.types.js +38 -0
  38. package/dist/wirings/forge-node/index.d.ts +1 -0
  39. package/dist/wirings/forge-node/index.js +1 -0
  40. package/dist/wirings/http/http-runner.d.ts +2 -2
  41. package/dist/wirings/http/http-runner.js +21 -12
  42. package/dist/wirings/http/http.types.d.ts +14 -1
  43. package/dist/wirings/http/log-http-routes.js +1 -1
  44. package/dist/wirings/http/routers/path-to-regex.js +2 -2
  45. package/dist/wirings/mcp/mcp-runner.js +21 -21
  46. package/dist/wirings/queue/queue-runner.js +6 -6
  47. package/dist/wirings/rpc/index.d.ts +1 -1
  48. package/dist/wirings/rpc/index.js +1 -1
  49. package/dist/wirings/rpc/rpc-runner.d.ts +7 -8
  50. package/dist/wirings/rpc/rpc-runner.js +62 -14
  51. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  52. package/dist/wirings/scheduler/log-schedulers.js +1 -1
  53. package/dist/wirings/scheduler/scheduler-runner.js +5 -5
  54. package/dist/wirings/trigger/index.d.ts +2 -0
  55. package/dist/wirings/trigger/index.js +2 -0
  56. package/dist/wirings/trigger/trigger-runner.d.ts +29 -0
  57. package/dist/wirings/trigger/trigger-runner.js +57 -0
  58. package/dist/wirings/trigger/trigger.types.d.ts +42 -0
  59. package/dist/wirings/trigger/trigger.types.js +1 -0
  60. package/dist/wirings/workflow/dsl/index.d.ts +5 -0
  61. package/dist/wirings/workflow/dsl/index.js +4 -0
  62. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +286 -0
  63. package/dist/wirings/workflow/dsl/workflow-dsl.types.js +5 -0
  64. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +5 -0
  65. package/dist/wirings/workflow/dsl/workflow-runner.js +22 -0
  66. package/dist/wirings/workflow/graph/graph-node.d.ts +122 -0
  67. package/dist/wirings/workflow/graph/graph-node.js +58 -0
  68. package/dist/wirings/workflow/graph/graph-runner.d.ts +35 -0
  69. package/dist/wirings/workflow/graph/graph-runner.js +452 -0
  70. package/dist/wirings/workflow/graph/index.d.ts +3 -0
  71. package/dist/wirings/workflow/graph/index.js +3 -0
  72. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +95 -0
  73. package/dist/wirings/workflow/graph/workflow-graph.types.js +15 -0
  74. package/dist/wirings/workflow/index.d.ts +7 -12
  75. package/dist/wirings/workflow/index.js +6 -11
  76. package/dist/wirings/workflow/pikku-workflow-service.d.ts +96 -6
  77. package/dist/wirings/workflow/pikku-workflow-service.js +230 -103
  78. package/dist/wirings/workflow/wire-workflow.d.ts +42 -0
  79. package/dist/wirings/workflow/wire-workflow.js +53 -0
  80. package/dist/wirings/workflow/workflow-utils.d.ts +23 -0
  81. package/dist/wirings/workflow/workflow-utils.js +66 -0
  82. package/dist/wirings/workflow/workflow.types.d.ts +133 -211
  83. package/package.json +10 -1
  84. package/src/errors/error-handler.ts +3 -3
  85. package/src/function/function-runner.test.ts +1 -1
  86. package/src/function/function-runner.ts +86 -16
  87. package/src/function/functions.types.ts +126 -4
  88. package/src/index.ts +10 -5
  89. package/src/middleware-runner.ts +16 -9
  90. package/src/permissions.test.ts +28 -86
  91. package/src/permissions.ts +21 -80
  92. package/src/pikku-state.ts +156 -201
  93. package/src/schema.ts +42 -13
  94. package/src/services/workflow-service.ts +74 -0
  95. package/src/types/core.types.ts +12 -3
  96. package/src/types/state.types.ts +195 -0
  97. package/src/utils.ts +55 -0
  98. package/src/wirings/channel/channel-handler.ts +1 -1
  99. package/src/wirings/channel/channel-runner.ts +5 -5
  100. package/src/wirings/channel/local/local-channel-runner.test.ts +3 -1
  101. package/src/wirings/channel/log-channels.ts +1 -1
  102. package/src/wirings/channel/serverless/serverless-channel-runner.ts +2 -2
  103. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -3
  104. package/src/wirings/cli/cli-runner.test.ts +19 -19
  105. package/src/wirings/cli/cli-runner.ts +10 -9
  106. package/src/wirings/cli/cli.types.ts +1 -0
  107. package/src/wirings/forge-node/forge-node.types.ts +135 -0
  108. package/src/wirings/forge-node/index.ts +1 -0
  109. package/src/wirings/http/http-runner.test.ts +2 -2
  110. package/src/wirings/http/http-runner.ts +30 -13
  111. package/src/wirings/http/http.types.ts +14 -0
  112. package/src/wirings/http/log-http-routes.ts +1 -1
  113. package/src/wirings/http/routers/path-to-regex.test.ts +30 -19
  114. package/src/wirings/http/routers/path-to-regex.ts +2 -2
  115. package/src/wirings/mcp/mcp-runner.ts +21 -21
  116. package/src/wirings/queue/queue-runner.test.ts +19 -19
  117. package/src/wirings/queue/queue-runner.ts +6 -6
  118. package/src/wirings/rpc/index.ts +1 -1
  119. package/src/wirings/rpc/rpc-runner.ts +88 -22
  120. package/src/wirings/rpc/rpc-types.ts +8 -0
  121. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  122. package/src/wirings/scheduler/scheduler-runner.test.ts +27 -27
  123. package/src/wirings/scheduler/scheduler-runner.ts +5 -5
  124. package/src/wirings/trigger/index.ts +2 -0
  125. package/src/wirings/trigger/trigger-runner.ts +96 -0
  126. package/src/wirings/trigger/trigger.types.ts +56 -0
  127. package/src/wirings/workflow/dsl/index.ts +30 -0
  128. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +312 -0
  129. package/src/wirings/workflow/dsl/workflow-runner.ts +27 -0
  130. package/src/wirings/workflow/graph/graph-node.ts +227 -0
  131. package/src/wirings/workflow/graph/graph-runner.ts +694 -0
  132. package/src/wirings/workflow/graph/index.ts +3 -0
  133. package/src/wirings/workflow/graph/workflow-graph.types.ts +126 -0
  134. package/src/wirings/workflow/index.ts +53 -26
  135. package/src/wirings/workflow/pikku-workflow-service.ts +333 -117
  136. package/src/wirings/workflow/wire-workflow.ts +94 -0
  137. package/src/wirings/workflow/workflow-utils.ts +120 -0
  138. package/src/wirings/workflow/workflow.types.ts +166 -277
  139. package/tsconfig.tsbuildinfo +1 -1
  140. package/dist/wirings/workflow/workflow-runner.d.ts +0 -34
  141. package/dist/wirings/workflow/workflow-runner.js +0 -65
  142. package/src/function/function-runner.ts.bak +0 -188
  143. package/src/wirings/workflow/workflow-runner.ts +0 -72
@@ -9,21 +9,61 @@ import {
9
9
  SerializedError,
10
10
  } from '../../types/core.types.js'
11
11
  import { QueueService } from '../queue/queue.types.js'
12
- import {
13
- WorkflowAsyncException,
14
- WorkflowCancelledException,
15
- WorkflowNotFoundError,
16
- WorkflowRunNotFound,
17
- } from './workflow-runner.js'
18
12
  import type {
19
13
  PikkuWorkflowWire,
20
14
  StepState,
21
15
  WorkflowRun,
22
16
  WorkflowStatus,
23
- WorkflowService,
24
17
  WorkflowServiceConfig,
25
18
  WorkflowStepOptions,
26
19
  } from './workflow.types.js'
20
+ import { executeGraphStep, runWorkflowGraph } from './graph/graph-runner.js'
21
+ import { WorkflowService } from '../../services/workflow-service.js'
22
+ import { PikkuError } from '../../errors/error-handler.js'
23
+
24
+ /**
25
+ * Exception thrown when workflow needs to pause for async step
26
+ */
27
+ export class WorkflowAsyncException extends Error {
28
+ constructor(
29
+ public readonly runId: string,
30
+ public readonly stepName: string
31
+ ) {
32
+ super(`Workflow paused at step: ${stepName}`)
33
+ this.name = 'WorkflowAsyncException'
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Exception thrown when workflow is cancelled
39
+ */
40
+ export class WorkflowCancelledException extends Error {
41
+ constructor(
42
+ public readonly runId: string,
43
+ public readonly reason?: string
44
+ ) {
45
+ super(reason || 'Workflow cancelled')
46
+ this.name = 'WorkflowCancelledException'
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Error class for workflow not found
52
+ */
53
+ export class WorkflowNotFoundError extends PikkuError {
54
+ constructor(name: string) {
55
+ super(`Workflow not found: ${name}`)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Error class for workflow not found
61
+ */
62
+ export class WorkflowRunNotFound extends PikkuError {
63
+ constructor(runId: string) {
64
+ super(`Workflow run not found: ${runId}`)
65
+ }
66
+ }
27
67
 
28
68
  export class WorkflowServiceNotInitialized extends Error {}
29
69
  export class WorkflowStepNameNotString extends Error {
@@ -41,9 +81,31 @@ export abstract class PikkuWorkflowService implements WorkflowService {
41
81
  private config: WorkflowServiceConfig | undefined
42
82
  private singletonServices: CoreSingletonServices | undefined
43
83
  private createWireServices: CreateWireServices | undefined
84
+ private inlineRuns = new Set<string>()
44
85
 
45
86
  constructor() {}
46
87
 
88
+ /**
89
+ * Check if a run is executing inline (without queues)
90
+ */
91
+ protected isInline(runId: string): boolean {
92
+ return this.inlineRuns.has(runId)
93
+ }
94
+
95
+ /**
96
+ * Register a run as inline (for graph-runner to use)
97
+ */
98
+ public registerInlineRun(runId: string): void {
99
+ this.inlineRuns.add(runId)
100
+ }
101
+
102
+ /**
103
+ * Unregister a run from inline tracking
104
+ */
105
+ public unregisterInlineRun(runId: string): void {
106
+ this.inlineRuns.delete(runId)
107
+ }
108
+
47
109
  public setServices(
48
110
  singletonServices: CoreSingletonServices,
49
111
  createWireServices: CreateWireServices,
@@ -58,7 +120,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
58
120
  workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
59
121
  stepWorkerQueueName:
60
122
  workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
61
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowStepSleeper',
123
+ sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
62
124
  }
63
125
  }
64
126
 
@@ -68,7 +130,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
68
130
  * @param input - Input data for the workflow
69
131
  * @returns Run ID
70
132
  */
71
- abstract createRun(workflowName: string, input: any): Promise<string>
133
+ abstract createRun(
134
+ workflowName: string,
135
+ input: any,
136
+ inline?: boolean
137
+ ): Promise<string>
72
138
 
73
139
  /**
74
140
  * Get a workflow run by ID
@@ -193,6 +259,68 @@ export abstract class PikkuWorkflowService implements WorkflowService {
193
259
  */
194
260
  abstract close(): Promise<void>
195
261
 
262
+ // ============================================================================
263
+ // Workflow Graph Methods
264
+ // ============================================================================
265
+
266
+ /**
267
+ * Get completed graph state (lightweight - no results)
268
+ * @param runId - Run ID
269
+ * @returns Completed node IDs and their branch keys
270
+ */
271
+ abstract getCompletedGraphState(runId: string): Promise<{
272
+ completedNodeIds: string[]
273
+ branchKeys: Record<string, string>
274
+ }>
275
+
276
+ /**
277
+ * Filter candidate nodes to only those without existing steps
278
+ * @param runId - Run ID
279
+ * @param nodeIds - Candidate node IDs to check
280
+ * @returns Node IDs that don't have a step yet
281
+ */
282
+ abstract getNodesWithoutSteps(
283
+ runId: string,
284
+ nodeIds: string[]
285
+ ): Promise<string[]>
286
+
287
+ /**
288
+ * Get results for specific nodes
289
+ * @param runId - Run ID
290
+ * @param nodeIds - Node IDs to fetch results for
291
+ * @returns Map of nodeId to result
292
+ */
293
+ abstract getNodeResults(
294
+ runId: string,
295
+ nodeIds: string[]
296
+ ): Promise<Record<string, any>>
297
+
298
+ /**
299
+ * Set the branch key for a graph node step
300
+ * @param stepId - Step ID
301
+ * @param branchKey - Branch key selected by graph.branch()
302
+ */
303
+ abstract setBranchTaken(stepId: string, branchKey: string): Promise<void>
304
+
305
+ /**
306
+ * Update a state variable in the workflow run's state
307
+ * @param runId - Run ID
308
+ * @param name - Variable name
309
+ * @param value - Value to store
310
+ */
311
+ abstract updateRunState(
312
+ runId: string,
313
+ name: string,
314
+ value: unknown
315
+ ): Promise<void>
316
+
317
+ /**
318
+ * Get the entire state object for a workflow run
319
+ * @param runId - Run ID
320
+ * @returns The state object with all variables
321
+ */
322
+ abstract getRunState(runId: string): Promise<Record<string, unknown>>
323
+
196
324
  // ============================================================================
197
325
  // Workflow Lifecycle Methods
198
326
  // ============================================================================
@@ -211,7 +339,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
211
339
  * Sets the step result to null and resumes the workflow
212
340
  * @param data - Sleeper input data
213
341
  */
214
- public async executeWorkflowSleep(
342
+ public async executeWorkflowSleepCompleted(
215
343
  runId: string,
216
344
  stepId: string
217
345
  ): Promise<void> {
@@ -238,13 +366,39 @@ export abstract class PikkuWorkflowService implements WorkflowService {
238
366
 
239
367
  /**
240
368
  * Start a new workflow run
369
+ * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
370
+ * @param options.inline - If true, execute workflow directly without queue service
371
+ * @param options.startNode - Starting node ID for graph workflows (from wire config)
241
372
  */
242
373
  public async startWorkflow<I>(
243
374
  name: string,
244
375
  input: I,
245
- rpcService: any
376
+ rpcService: any,
377
+ options?: { inline?: boolean; startNode?: string }
246
378
  ): Promise<{ runId: string }> {
247
- const registrations = pikkuState('workflows', 'registrations')
379
+ // Check meta to determine workflow type
380
+ const meta = pikkuState(null, 'workflows', 'meta')
381
+ const workflowMeta = meta[name]
382
+
383
+ if (!workflowMeta) {
384
+ throw new WorkflowNotFoundError(name)
385
+ }
386
+
387
+ // Check if this is a graph workflow (source === 'graph')
388
+ if (workflowMeta.source === 'graph') {
389
+ // Graph workflows use the graph scheduler
390
+ return runWorkflowGraph(
391
+ this,
392
+ name,
393
+ input,
394
+ rpcService,
395
+ options?.inline,
396
+ options?.startNode
397
+ )
398
+ }
399
+
400
+ // DSL workflow - check registration exists
401
+ const registrations = pikkuState(null, 'workflows', 'registrations')
248
402
  const workflow = registrations.get(name)
249
403
 
250
404
  if (!workflow) {
@@ -252,19 +406,29 @@ export abstract class PikkuWorkflowService implements WorkflowService {
252
406
  }
253
407
 
254
408
  // Create workflow run in state
255
- const runId = await this.createRun(name, input)
409
+ const runId = await this.createRun(name, input, options?.inline)
256
410
 
257
- // If queue service is available, use remote execution (queue-based)
258
- // Otherwise, execute directly (inline/synchronous)
259
- if (this.singletonServices?.queueService) {
260
- // Queue orchestrator to start the workflow
261
- await this.resumeWorkflow(runId)
262
- } else {
263
- // No queue service - execute directly via runWorkflowJob
264
- await this.runWorkflowJob(runId, rpcService)
411
+ // Register inline run for fast lookup
412
+ if (options?.inline) {
413
+ this.inlineRuns.add(runId)
265
414
  }
266
415
 
267
- return { runId }
416
+ try {
417
+ // If inline mode or no queue service, execute directly
418
+ // Otherwise, use remote execution (queue-based)
419
+ if (options?.inline || !this.singletonServices?.queueService) {
420
+ await this.runWorkflowJob(runId, rpcService)
421
+ } else {
422
+ await this.resumeWorkflow(runId)
423
+ }
424
+
425
+ return { runId }
426
+ } finally {
427
+ // Clean up inline tracking
428
+ if (options?.inline) {
429
+ this.inlineRuns.delete(runId)
430
+ }
431
+ }
268
432
  }
269
433
 
270
434
  public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
@@ -273,14 +437,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
273
437
  throw new WorkflowRunNotFound(runId)
274
438
  }
275
439
 
276
- const registrations = pikkuState('workflows', 'registrations')
440
+ const registrations = pikkuState(null, 'workflows', 'registrations')
277
441
  const workflow = registrations.get(run.workflow)
278
442
  if (!workflow) {
279
443
  throw new WorkflowNotFoundError(run.workflow)
280
444
  }
281
445
 
282
446
  await this.withRunLock(runId, async () => {
283
- const meta = pikkuState('workflows', 'meta')
447
+ const meta = pikkuState(null, 'workflows', 'meta')
284
448
  const workflowMeta = meta[run.workflow]
285
449
 
286
450
  const workflowWire = this.createWorkflowWire(
@@ -292,7 +456,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
292
456
  try {
293
457
  const result = await runPikkuFunc(
294
458
  'workflow',
295
- workflowMeta.workflowName,
459
+ workflowMeta.name,
296
460
  workflowMeta.pikkuFuncName,
297
461
  {
298
462
  singletonServices: this.singletonServices!,
@@ -311,7 +475,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
311
475
 
312
476
  // Check if it's a WorkflowCancelledException
313
477
  if (error instanceof WorkflowCancelledException) {
314
- // Workflow was cancelled - status already updated, just rethrow
478
+ // Workflow was cancelled - update status and rethrow
479
+ await this.updateRunStatus(runId, 'cancelled', undefined, {
480
+ message: error.message || 'Workflow cancelled',
481
+ stack: '',
482
+ code: 'WORKFLOW_CANCELLED',
483
+ })
315
484
  throw error
316
485
  }
317
486
 
@@ -364,24 +533,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
364
533
  }
365
534
 
366
535
  try {
367
- // Execute RPC with workflow step context
368
- const result = await rpcService.rpcWithWire(rpcName, data, {
369
- workflowStep: {
536
+ let result: any
537
+
538
+ // Check if this is a graph node step (step name starts with 'node:')
539
+ if (stepName.startsWith('node:')) {
540
+ // Get the graph name from the workflow run
541
+ const run = await this.getRun(runId)
542
+ if (!run) {
543
+ throw new Error(`Workflow run not found: ${runId}`)
544
+ }
545
+ const graphName = run.workflow
546
+
547
+ // Execute as graph step with graph wire context
548
+ result = await executeGraphStep(
549
+ this,
550
+ rpcService,
370
551
  runId,
371
- stepId: stepState.stepId,
372
- attemptCount: stepState.attemptCount,
373
- },
374
- })
552
+ stepState.stepId,
553
+ stepName,
554
+ rpcName,
555
+ data,
556
+ graphName
557
+ )
558
+ } else {
559
+ // Execute RPC with workflow step context (regular workflow)
560
+ result = await rpcService.rpcWithWire(rpcName, data, {
561
+ workflowStep: {
562
+ runId,
563
+ stepId: stepState.stepId,
564
+ attemptCount: stepState.attemptCount,
565
+ },
566
+ })
567
+ }
375
568
 
376
569
  // Store result and mark succeeded
377
570
  await this.setStepResult(stepState.stepId, result)
378
571
 
379
572
  // Resume orchestrator to continue workflow
380
- try {
381
- await this.resumeWorkflow(runId)
382
- } catch (resumeError: any) {
383
- throw resumeError
384
- }
573
+ await this.resumeWorkflow(runId)
385
574
  } catch (error: any) {
386
575
  // Store error and mark failed
387
576
  await this.setStepError(stepState.stepId, error)
@@ -451,7 +640,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
451
640
  let stepState: StepState
452
641
  try {
453
642
  stepState = await this.getStepState(runId, stepName)
454
- } catch (error: any) {
643
+ } catch {
455
644
  // Step doesn't exist - create it
456
645
  stepState = await this.insertStepState(
457
646
  runId,
@@ -488,8 +677,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
488
677
  // Step is pending - schedule it
489
678
  await this.setStepScheduled(stepState.stepId)
490
679
 
491
- // Enqueue step worker
492
- if (this.singletonServices!.queueService) {
680
+ // Enqueue step worker (unless inline mode)
681
+ if (!this.isInline(runId) && this.singletonServices!.queueService) {
493
682
  // Map step retry options to queue job options
494
683
  const retries = stepOptions?.retries ?? 0
495
684
  const retryDelay = stepOptions?.retryDelay
@@ -514,45 +703,52 @@ export abstract class PikkuWorkflowService implements WorkflowService {
514
703
  : undefined,
515
704
  }
516
705
  )
706
+ // Pause workflow - step will callback when done
707
+ throw new WorkflowAsyncException(runId, stepName)
517
708
  } else {
518
- // No queue service - execute locally
709
+ // Inline or no queue service - execute locally with retry loop
519
710
  const retries = stepOptions?.retries ?? 0
520
711
  const retryDelay = stepOptions?.retryDelay
521
- const attemptCount = stepState.attemptCount
522
-
523
- try {
524
- const result = await rpcService.rpcWithWire(rpcName, data, {
525
- workflowStep: {
526
- runId,
527
- stepId: stepState.stepId,
528
- attemptCount: stepState.attemptCount,
529
- },
530
- })
531
- await this.setStepResult(stepState.stepId, result)
532
- return result
533
- } catch (error: any) {
534
- // Record the error (marks step as failed)
535
- await this.setStepError(stepState.stepId, error)
712
+ let currentStepState = stepState
536
713
 
537
- // Check if we should retry
538
- if (attemptCount < retries) {
539
- // Create a new pending retry attempt (copies metadata from failed step)
540
- await this.createRetryAttempt(stepState.stepId, 'pending')
541
-
542
- // Schedule orchestrator to retry after delay
543
- await this.scheduleOrchestratorRetry(runId, retryDelay)
544
-
545
- // Pause workflow - orchestrator will replay and pick up new attempt
546
- throw new WorkflowAsyncException(runId, stepName)
714
+ while (true) {
715
+ try {
716
+ await this.setStepRunning(currentStepState.stepId)
717
+ const result = await rpcService.rpcWithWire(rpcName, data, {
718
+ workflowStep: {
719
+ runId,
720
+ stepId: currentStepState.stepId,
721
+ attemptCount: currentStepState.attemptCount,
722
+ },
723
+ })
724
+ await this.setStepResult(currentStepState.stepId, result)
725
+ return result
726
+ } catch (error: any) {
727
+ // Record the error (marks step as failed)
728
+ await this.setStepError(currentStepState.stepId, error)
729
+
730
+ // Check if we should retry
731
+ if (currentStepState.attemptCount < retries) {
732
+ // Create a new pending retry attempt
733
+ currentStepState = await this.createRetryAttempt(
734
+ currentStepState.stepId,
735
+ 'pending'
736
+ )
737
+
738
+ // Wait for retry delay if specified
739
+ if (retryDelay) {
740
+ await new Promise((resolve) =>
741
+ setTimeout(resolve, getDurationInMilliseconds(retryDelay))
742
+ )
743
+ }
744
+ // Continue loop to retry
745
+ } else {
746
+ // No more retries, fail the workflow
747
+ throw error
748
+ }
547
749
  }
548
-
549
- // No more retries, fail the workflow
550
- throw error
551
750
  }
552
751
  }
553
-
554
- // Pause workflow - step will callback when done
555
- throw new WorkflowAsyncException(runId, stepName)
556
752
  }
557
753
 
558
754
  private async inlineStep(
@@ -565,7 +761,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
565
761
  let stepState: StepState
566
762
  try {
567
763
  stepState = await this.getStepState(runId, stepName)
568
- } catch (error: any) {
764
+ } catch {
569
765
  // Step doesn't exist - create it (inline, no RPC)
570
766
  stepState = await this.insertStepState(
571
767
  runId,
@@ -584,30 +780,68 @@ export abstract class PikkuWorkflowService implements WorkflowService {
584
780
  // Execute inline function
585
781
  const retries = stepOptions?.retries ?? this.getConfig().retries
586
782
  const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay
587
- const attemptCount = stepState.attemptCount
783
+ let currentStepState = stepState
588
784
 
589
- try {
590
- const result = await fn()
591
- await this.setStepResult(stepState.stepId, result)
592
- return result
593
- } catch (error: any) {
594
- // Record the error (marks step as failed)
595
- await this.setStepError(stepState.stepId, error)
785
+ // Check if we're running inline (in-memory) or remote (queue-based)
786
+ if (this.isInline(runId)) {
787
+ // Inline mode - execute with retry loop
788
+ while (true) {
789
+ try {
790
+ await this.setStepRunning(currentStepState.stepId)
791
+ const result = await fn()
792
+ await this.setStepResult(currentStepState.stepId, result)
793
+ return result
794
+ } catch (error: any) {
795
+ // Record the error (marks step as failed)
796
+ await this.setStepError(currentStepState.stepId, error)
797
+
798
+ // Check if we should retry
799
+ if (currentStepState.attemptCount < retries) {
800
+ // Create a new pending retry attempt
801
+ currentStepState = await this.createRetryAttempt(
802
+ currentStepState.stepId,
803
+ 'pending'
804
+ )
805
+
806
+ // Wait for retry delay if specified
807
+ if (retryDelay) {
808
+ await new Promise((resolve) =>
809
+ setTimeout(resolve, getDurationInMilliseconds(retryDelay))
810
+ )
811
+ }
812
+ // Continue loop to retry
813
+ } else {
814
+ // No more retries, fail the workflow
815
+ throw error
816
+ }
817
+ }
818
+ }
819
+ } else {
820
+ // Remote mode - use queue-based retry
821
+ try {
822
+ await this.setStepRunning(currentStepState.stepId)
823
+ const result = await fn()
824
+ await this.setStepResult(currentStepState.stepId, result)
825
+ return result
826
+ } catch (error: any) {
827
+ // Record the error (marks step as failed)
828
+ await this.setStepError(currentStepState.stepId, error)
829
+
830
+ // Check if we should retry
831
+ if (currentStepState.attemptCount < retries) {
832
+ // Create a new pending retry attempt (copies metadata from failed step)
833
+ await this.createRetryAttempt(currentStepState.stepId, 'pending')
596
834
 
597
- // Check if we should retry
598
- if (attemptCount < retries) {
599
- // Create a new pending retry attempt (copies metadata from failed step)
600
- await this.createRetryAttempt(stepState.stepId, 'pending')
835
+ // Schedule orchestrator to retry after delay
836
+ await this.scheduleOrchestratorRetry(runId, retryDelay)
601
837
 
602
- // Schedule orchestrator to retry after delay
603
- await this.scheduleOrchestratorRetry(runId, retryDelay)
838
+ // Pause workflow - orchestrator will replay and pick up new attempt
839
+ throw new WorkflowAsyncException(runId, stepName)
840
+ }
604
841
 
605
- // Pause workflow - orchestrator will replay and pick up new attempt
606
- throw new WorkflowAsyncException(runId, stepName)
842
+ // No more retries, fail the workflow
843
+ throw error
607
844
  }
608
-
609
- // No more retries, fail the workflow
610
- throw error
611
845
  }
612
846
  }
613
847
 
@@ -616,7 +850,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
616
850
  let stepState: StepState
617
851
  try {
618
852
  stepState = await this.getStepState(runId, stepName)
619
- } catch (error: any) {
853
+ } catch {
620
854
  // Step doesn't exist - create it (sleep step, no RPC)
621
855
  stepState = await this.insertStepState(
622
856
  runId,
@@ -640,9 +874,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
640
874
  // Step is pending - schedule it
641
875
  await this.setStepScheduled(stepState.stepId)
642
876
 
643
- // Check if we have queue service (remote mode) or inline mode
644
- if (this.singletonServices!.schedulerService) {
645
- // Remote mode - enqueue sleep worker with delay
877
+ // Check if inline mode or no scheduler service
878
+ if (!this.isInline(runId) && this.singletonServices!.schedulerService) {
879
+ // Remote mode - schedule sleep via scheduler service
646
880
  await this.singletonServices!.schedulerService.scheduleRPC(
647
881
  duration,
648
882
  this.getConfig().sleeperRPCName,
@@ -651,6 +885,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
651
885
  stepId: stepState.stepId,
652
886
  }
653
887
  )
888
+ // Pause workflow - sleep will callback when done
889
+ throw new WorkflowAsyncException(runId, stepName)
654
890
  } else {
655
891
  // Inline mode - use setTimeout with actual duration
656
892
  await new Promise((resolve) =>
@@ -659,21 +895,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
659
895
  await this.setStepResult(stepState.stepId, null)
660
896
  return
661
897
  }
662
-
663
- // Pause workflow - sleep will callback when done
664
- throw new WorkflowAsyncException(runId, stepName)
665
- }
666
-
667
- private async cancelWorkflow(runId: string, reason?: string) {
668
- // Update workflow run status to cancelled
669
- await this.updateRunStatus(runId, 'cancelled', undefined, {
670
- message: reason || 'Workflow cancelled by user',
671
- stack: '',
672
- code: 'WORKFLOW_CANCELLED',
673
- })
674
-
675
- // Throw cancellation exception to stop workflow execution
676
- throw new WorkflowCancelledException(runId, reason)
677
898
  }
678
899
 
679
900
  private createWorkflowWire(
@@ -722,11 +943,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
722
943
  getDurationInMilliseconds(duration)
723
944
  )
724
945
  },
725
-
726
- // Implement workflow.cancel()
727
- cancel: async (reason?: string): Promise<void> => {
728
- await this.cancelWorkflow(runId, reason)
729
- },
730
946
  }
731
947
  return workflowWire
732
948
  }