@pikku/core 0.11.0 → 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 (184) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/errors/error-handler.js +3 -3
  3. package/dist/errors/errors.d.ts +1 -1
  4. package/dist/errors/errors.js +3 -3
  5. package/dist/function/function-runner.d.ts +8 -8
  6. package/dist/function/function-runner.js +88 -23
  7. package/dist/function/functions.types.d.ts +104 -33
  8. package/dist/function/functions.types.js +50 -7
  9. package/dist/handle-error.d.ts +1 -1
  10. package/dist/handle-error.js +1 -1
  11. package/dist/index.d.ts +6 -2
  12. package/dist/index.js +5 -2
  13. package/dist/middleware/auth-apikey.d.ts +1 -1
  14. package/dist/middleware/auth-apikey.js +3 -3
  15. package/dist/middleware/auth-bearer.d.ts +1 -1
  16. package/dist/middleware/auth-bearer.js +3 -3
  17. package/dist/middleware/auth-cookie.d.ts +1 -1
  18. package/dist/middleware/auth-cookie.js +5 -5
  19. package/dist/middleware/timeout.d.ts +1 -1
  20. package/dist/middleware/timeout.js +3 -2
  21. package/dist/middleware-runner.d.ts +6 -20
  22. package/dist/middleware-runner.js +19 -20
  23. package/dist/permissions.d.ts +6 -89
  24. package/dist/permissions.js +24 -81
  25. package/dist/pikku-state.d.ts +32 -104
  26. package/dist/pikku-state.js +140 -66
  27. package/dist/schema.d.ts +8 -6
  28. package/dist/schema.js +25 -13
  29. package/dist/services/user-session-service.d.ts +7 -3
  30. package/dist/services/user-session-service.js +8 -1
  31. package/dist/services/workflow-service.d.ts +38 -0
  32. package/dist/services/workflow-service.js +1 -0
  33. package/dist/types/core.types.d.ts +40 -41
  34. package/dist/types/core.types.js +4 -15
  35. package/dist/types/state.types.d.ts +135 -0
  36. package/dist/types/state.types.js +1 -0
  37. package/dist/utils.d.ts +9 -1
  38. package/dist/utils.js +48 -3
  39. package/dist/wirings/channel/channel-common.js +2 -3
  40. package/dist/wirings/channel/channel-handler.d.ts +3 -2
  41. package/dist/wirings/channel/channel-handler.js +7 -10
  42. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  43. package/dist/wirings/channel/channel-runner.js +5 -5
  44. package/dist/wirings/channel/channel.types.d.ts +10 -5
  45. package/dist/wirings/channel/local/local-channel-runner.d.ts +1 -1
  46. package/dist/wirings/channel/local/local-channel-runner.js +19 -26
  47. package/dist/wirings/channel/log-channels.js +1 -1
  48. package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +1 -1
  49. package/dist/wirings/channel/serverless/serverless-channel-runner.js +43 -48
  50. package/dist/wirings/cli/channel/cli-channel-runner.js +3 -2
  51. package/dist/wirings/cli/cli-runner.d.ts +5 -5
  52. package/dist/wirings/cli/cli-runner.js +20 -38
  53. package/dist/wirings/cli/cli.types.d.ts +16 -11
  54. package/dist/wirings/forge-node/forge-node.types.d.ts +120 -0
  55. package/dist/wirings/forge-node/forge-node.types.js +38 -0
  56. package/dist/wirings/forge-node/index.d.ts +1 -0
  57. package/dist/wirings/forge-node/index.js +1 -0
  58. package/dist/wirings/http/http-runner.d.ts +8 -8
  59. package/dist/wirings/http/http-runner.js +46 -51
  60. package/dist/wirings/http/http.types.d.ts +19 -11
  61. package/dist/wirings/http/log-http-routes.js +1 -1
  62. package/dist/wirings/http/routers/path-to-regex.js +2 -2
  63. package/dist/wirings/mcp/mcp-runner.d.ts +2 -2
  64. package/dist/wirings/mcp/mcp-runner.js +33 -40
  65. package/dist/wirings/mcp/mcp.types.d.ts +6 -0
  66. package/dist/wirings/queue/queue-runner.d.ts +3 -3
  67. package/dist/wirings/queue/queue-runner.js +13 -23
  68. package/dist/wirings/queue/queue.types.d.ts +4 -11
  69. package/dist/wirings/rpc/index.d.ts +1 -1
  70. package/dist/wirings/rpc/index.js +1 -1
  71. package/dist/wirings/rpc/rpc-runner.d.ts +17 -16
  72. package/dist/wirings/rpc/rpc-runner.js +98 -61
  73. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  74. package/dist/wirings/scheduler/log-schedulers.js +1 -1
  75. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -3
  76. package/dist/wirings/scheduler/scheduler-runner.js +18 -29
  77. package/dist/wirings/scheduler/scheduler.types.d.ts +3 -8
  78. package/dist/wirings/trigger/index.d.ts +2 -0
  79. package/dist/wirings/trigger/index.js +2 -0
  80. package/dist/wirings/trigger/trigger-runner.d.ts +29 -0
  81. package/dist/wirings/trigger/trigger-runner.js +57 -0
  82. package/dist/wirings/trigger/trigger.types.d.ts +42 -0
  83. package/dist/wirings/trigger/trigger.types.js +1 -0
  84. package/dist/wirings/workflow/dsl/index.d.ts +5 -0
  85. package/dist/wirings/workflow/dsl/index.js +4 -0
  86. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +286 -0
  87. package/dist/wirings/workflow/dsl/workflow-dsl.types.js +5 -0
  88. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +5 -0
  89. package/dist/wirings/workflow/dsl/workflow-runner.js +22 -0
  90. package/dist/wirings/workflow/graph/graph-node.d.ts +122 -0
  91. package/dist/wirings/workflow/graph/graph-node.js +58 -0
  92. package/dist/wirings/workflow/graph/graph-runner.d.ts +35 -0
  93. package/dist/wirings/workflow/graph/graph-runner.js +452 -0
  94. package/dist/wirings/workflow/graph/index.d.ts +3 -0
  95. package/dist/wirings/workflow/graph/index.js +3 -0
  96. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +95 -0
  97. package/dist/wirings/workflow/graph/workflow-graph.types.js +15 -0
  98. package/dist/wirings/workflow/index.d.ts +7 -3
  99. package/dist/wirings/workflow/index.js +6 -3
  100. package/dist/wirings/workflow/pikku-workflow-service.d.ts +108 -10
  101. package/dist/wirings/workflow/pikku-workflow-service.js +274 -165
  102. package/dist/wirings/workflow/wire-workflow.d.ts +42 -0
  103. package/dist/wirings/workflow/wire-workflow.js +53 -0
  104. package/dist/wirings/workflow/workflow-utils.d.ts +23 -0
  105. package/dist/wirings/workflow/workflow-utils.js +66 -0
  106. package/dist/wirings/workflow/workflow.types.d.ts +135 -128
  107. package/package.json +11 -2
  108. package/src/errors/error-handler.ts +3 -3
  109. package/src/errors/errors.ts +3 -3
  110. package/src/factory-functions.test.ts +9 -36
  111. package/src/function/function-runner.test.ts +58 -69
  112. package/src/function/function-runner.ts +130 -37
  113. package/src/function/functions.types.ts +193 -54
  114. package/src/handle-error.ts +1 -1
  115. package/src/index.ts +10 -5
  116. package/src/middleware/auth-apikey.test.ts +360 -0
  117. package/src/middleware/auth-apikey.ts +3 -7
  118. package/src/middleware/auth-bearer.test.ts +447 -0
  119. package/src/middleware/auth-bearer.ts +27 -33
  120. package/src/middleware/auth-cookie.test.ts +525 -0
  121. package/src/middleware/auth-cookie.ts +6 -6
  122. package/src/middleware/timeout.ts +4 -2
  123. package/src/middleware-runner.test.ts +58 -127
  124. package/src/middleware-runner.ts +29 -25
  125. package/src/permissions.test.ts +68 -130
  126. package/src/permissions.ts +41 -100
  127. package/src/pikku-state.ts +156 -201
  128. package/src/schema.ts +42 -13
  129. package/src/services/user-session-service.ts +14 -4
  130. package/src/services/workflow-service.ts +74 -0
  131. package/src/types/core.types.ts +79 -58
  132. package/src/types/state.types.ts +195 -0
  133. package/src/utils.ts +59 -4
  134. package/src/wirings/channel/channel-common.ts +4 -7
  135. package/src/wirings/channel/channel-handler.ts +17 -23
  136. package/src/wirings/channel/channel-runner.ts +7 -7
  137. package/src/wirings/channel/channel.types.ts +14 -11
  138. package/src/wirings/channel/local/local-channel-runner.test.ts +8 -4
  139. package/src/wirings/channel/local/local-channel-runner.ts +23 -38
  140. package/src/wirings/channel/log-channels.ts +1 -1
  141. package/src/wirings/channel/serverless/serverless-channel-runner.ts +53 -79
  142. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -3
  143. package/src/wirings/cli/cli-runner.test.ts +26 -26
  144. package/src/wirings/cli/cli-runner.ts +34 -71
  145. package/src/wirings/cli/cli.types.ts +23 -16
  146. package/src/wirings/forge-node/forge-node.types.ts +135 -0
  147. package/src/wirings/forge-node/index.ts +1 -0
  148. package/src/wirings/http/http-runner.test.ts +10 -10
  149. package/src/wirings/http/http-runner.ts +56 -66
  150. package/src/wirings/http/http.types.ts +19 -14
  151. package/src/wirings/http/log-http-routes.ts +1 -1
  152. package/src/wirings/http/routers/path-to-regex.test.ts +30 -19
  153. package/src/wirings/http/routers/path-to-regex.ts +2 -2
  154. package/src/wirings/mcp/mcp-runner.ts +44 -69
  155. package/src/wirings/mcp/mcp.types.ts +6 -0
  156. package/src/wirings/queue/queue-runner.test.ts +641 -0
  157. package/src/wirings/queue/queue-runner.ts +16 -38
  158. package/src/wirings/queue/queue.types.ts +4 -11
  159. package/src/wirings/rpc/index.ts +1 -1
  160. package/src/wirings/rpc/rpc-runner.ts +129 -77
  161. package/src/wirings/rpc/rpc-types.ts +8 -0
  162. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  163. package/src/wirings/scheduler/scheduler-runner.test.ts +627 -0
  164. package/src/wirings/scheduler/scheduler-runner.ts +29 -55
  165. package/src/wirings/scheduler/scheduler.types.ts +3 -9
  166. package/src/wirings/trigger/index.ts +2 -0
  167. package/src/wirings/trigger/trigger-runner.ts +96 -0
  168. package/src/wirings/trigger/trigger.types.ts +56 -0
  169. package/src/wirings/workflow/dsl/index.ts +30 -0
  170. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +312 -0
  171. package/src/wirings/workflow/dsl/workflow-runner.ts +27 -0
  172. package/src/wirings/workflow/graph/graph-node.ts +227 -0
  173. package/src/wirings/workflow/graph/graph-runner.ts +694 -0
  174. package/src/wirings/workflow/graph/index.ts +3 -0
  175. package/src/wirings/workflow/graph/workflow-graph.types.ts +126 -0
  176. package/src/wirings/workflow/index.ts +60 -12
  177. package/src/wirings/workflow/pikku-workflow-service.ts +401 -195
  178. package/src/wirings/workflow/wire-workflow.ts +94 -0
  179. package/src/wirings/workflow/workflow-utils.ts +120 -0
  180. package/src/wirings/workflow/workflow.types.ts +167 -188
  181. package/tsconfig.tsbuildinfo +1 -1
  182. package/dist/wirings/workflow/workflow-runner.d.ts +0 -35
  183. package/dist/wirings/workflow/workflow-runner.js +0 -68
  184. package/src/wirings/workflow/workflow-runner.ts +0 -85
@@ -3,28 +3,67 @@ import { pikkuState } from '../../pikku-state.js'
3
3
  import { getDurationInMilliseconds } from '../../time-utils.js'
4
4
  import {
5
5
  CoreConfig,
6
- CoreServices,
7
6
  CoreSingletonServices,
8
- CreateSessionServices,
9
- PikkuWiringTypes,
7
+ CreateWireServices,
8
+ PikkuWire,
10
9
  SerializedError,
11
10
  } from '../../types/core.types.js'
12
11
  import { QueueService } from '../queue/queue.types.js'
13
- import {
14
- WorkflowAsyncException,
15
- WorkflowCancelledException,
16
- WorkflowNotFoundError,
17
- WorkflowRunNotFound,
18
- } from './workflow-runner.js'
19
12
  import type {
20
- PikkuWorkflowInteraction,
13
+ PikkuWorkflowWire,
21
14
  StepState,
22
15
  WorkflowRun,
23
16
  WorkflowStatus,
24
- WorkflowService,
25
17
  WorkflowServiceConfig,
26
18
  WorkflowStepOptions,
27
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
+ }
28
67
 
29
68
  export class WorkflowServiceNotInitialized extends Error {}
30
69
  export class WorkflowStepNameNotString extends Error {
@@ -41,17 +80,39 @@ export class WorkflowStepNameNotString extends Error {
41
80
  export abstract class PikkuWorkflowService implements WorkflowService {
42
81
  private config: WorkflowServiceConfig | undefined
43
82
  private singletonServices: CoreSingletonServices | undefined
44
- private createSessionServices: CreateSessionServices | undefined
83
+ private createWireServices: CreateWireServices | undefined
84
+ private inlineRuns = new Set<string>()
45
85
 
46
86
  constructor() {}
47
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
+
48
109
  public setServices(
49
110
  singletonServices: CoreSingletonServices,
50
- createSessionServices: CreateSessionServices,
111
+ createWireServices: CreateWireServices,
51
112
  { workflow }: CoreConfig
52
113
  ) {
53
114
  this.singletonServices = singletonServices
54
- this.createSessionServices = createSessionServices
115
+ this.createWireServices = createWireServices
55
116
  this.config = {
56
117
  retries: workflow?.retries ?? 0,
57
118
  retryDelay: workflow?.retryDelay ?? 0,
@@ -59,7 +120,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
59
120
  workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
60
121
  stepWorkerQueueName:
61
122
  workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
62
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowStepSleeper',
123
+ sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
63
124
  }
64
125
  }
65
126
 
@@ -69,7 +130,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
69
130
  * @param input - Input data for the workflow
70
131
  * @returns Run ID
71
132
  */
72
- abstract createRun(workflowName: string, input: any): Promise<string>
133
+ abstract createRun(
134
+ workflowName: string,
135
+ input: any,
136
+ inline?: boolean
137
+ ): Promise<string>
73
138
 
74
139
  /**
75
140
  * Get a workflow run by ID
@@ -176,11 +241,86 @@ export abstract class PikkuWorkflowService implements WorkflowService {
176
241
  */
177
242
  abstract withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>
178
243
 
244
+ /**
245
+ * Execute function within a step lock to prevent concurrent step execution
246
+ * @param runId - Run ID
247
+ * @param stepName - Step name
248
+ * @param fn - Function to execute
249
+ * @returns Function result
250
+ */
251
+ abstract withStepLock<T>(
252
+ runId: string,
253
+ stepName: string,
254
+ fn: () => Promise<T>
255
+ ): Promise<T>
256
+
179
257
  /**
180
258
  * Close any open connections
181
259
  */
182
260
  abstract close(): Promise<void>
183
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
+
184
324
  // ============================================================================
185
325
  // Workflow Lifecycle Methods
186
326
  // ============================================================================
@@ -199,7 +339,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
199
339
  * Sets the step result to null and resumes the workflow
200
340
  * @param data - Sleeper input data
201
341
  */
202
- public async executeWorkflowSleep(
342
+ public async executeWorkflowSleepCompleted(
203
343
  runId: string,
204
344
  stepId: string
205
345
  ): Promise<void> {
@@ -226,13 +366,39 @@ export abstract class PikkuWorkflowService implements WorkflowService {
226
366
 
227
367
  /**
228
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)
229
372
  */
230
373
  public async startWorkflow<I>(
231
374
  name: string,
232
375
  input: I,
233
- rpcService: any
376
+ rpcService: any,
377
+ options?: { inline?: boolean; startNode?: string }
234
378
  ): Promise<{ runId: string }> {
235
- 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')
236
402
  const workflow = registrations.get(name)
237
403
 
238
404
  if (!workflow) {
@@ -240,82 +406,68 @@ export abstract class PikkuWorkflowService implements WorkflowService {
240
406
  }
241
407
 
242
408
  // Create workflow run in state
243
- const runId = await this.createRun(name, input)
409
+ const runId = await this.createRun(name, input, options?.inline)
244
410
 
245
- // If queue service is available, use remote execution (queue-based)
246
- // Otherwise, execute directly (inline/synchronous)
247
- if (this.singletonServices?.queueService) {
248
- // Queue orchestrator to start the workflow
249
- await this.resumeWorkflow(runId)
250
- } else {
251
- // No queue service - execute directly via runWorkflowJob
252
- await this.runWorkflowJob(runId, rpcService)
411
+ // Register inline run for fast lookup
412
+ if (options?.inline) {
413
+ this.inlineRuns.add(runId)
253
414
  }
254
415
 
255
- 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
+ }
256
432
  }
257
433
 
258
434
  public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
259
- // Get the run
260
435
  const run = await this.getRun(runId)
261
436
  if (!run) {
262
437
  throw new WorkflowRunNotFound(runId)
263
438
  }
264
439
 
265
- // Get workflow registration
266
- const registrations = pikkuState('workflows', 'registrations')
440
+ const registrations = pikkuState(null, 'workflows', 'registrations')
267
441
  const workflow = registrations.get(run.workflow)
268
442
  if (!workflow) {
269
443
  throw new WorkflowNotFoundError(run.workflow)
270
444
  }
271
445
 
272
- // Use lock to prevent concurrent execution
273
446
  await this.withRunLock(runId, async () => {
274
- // Create workflow interaction object
275
- const workflowInteraction = this.createWorkflowInteraction(
447
+ const meta = pikkuState(null, 'workflows', 'meta')
448
+ const workflowMeta = meta[run.workflow]
449
+
450
+ const workflowWire = this.createWorkflowWire(
276
451
  run.workflow,
277
452
  runId,
278
453
  rpcService
279
454
  )
280
-
281
- // Get function metadata
282
- const meta = pikkuState('workflows', 'meta')
283
- const workflowMeta = meta[run.workflow]
284
-
285
- // Execute workflow function with workflow interaction
455
+ const wire: PikkuWire = { workflow: workflowWire }
286
456
  try {
287
- const getAllServices = () => {
288
- let sessionServices = {}
289
- const interaction = { workflow: workflowInteraction }
290
- if (this.createSessionServices) {
291
- sessionServices = this.createSessionServices(
292
- this.singletonServices!,
293
- interaction,
294
- undefined
295
- )
296
- }
297
- return {
298
- ...this.singletonServices,
299
- ...sessionServices,
300
- ...interaction,
301
- } as CoreServices
302
- }
303
457
  const result = await runPikkuFunc(
304
- PikkuWiringTypes.workflow,
305
- workflowMeta.workflowName,
458
+ 'workflow',
459
+ workflowMeta.name,
306
460
  workflowMeta.pikkuFuncName,
307
461
  {
308
462
  singletonServices: this.singletonServices!,
309
- interaction: { workflow: workflowInteraction },
310
- getAllServices,
463
+ wire,
464
+ createWireServices: this.createWireServices,
311
465
  data: () => run.input,
312
466
  }
313
467
  )
314
468
 
315
- // Workflow completed successfully
316
469
  await this.updateRunStatus(runId, 'completed', result)
317
470
  } catch (error: any) {
318
- // Check if it's a WorkflowAsyncException
319
471
  if (error instanceof WorkflowAsyncException) {
320
472
  // Normal - workflow paused for step execution
321
473
  throw error
@@ -323,11 +475,15 @@ export abstract class PikkuWorkflowService implements WorkflowService {
323
475
 
324
476
  // Check if it's a WorkflowCancelledException
325
477
  if (error instanceof WorkflowCancelledException) {
326
- // 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
+ })
327
484
  throw error
328
485
  }
329
486
 
330
- // Real error - mark as failed
331
487
  await this.updateRunStatus(runId, 'failed', undefined, {
332
488
  message: error.message,
333
489
  stack: error.stack,
@@ -350,64 +506,87 @@ export abstract class PikkuWorkflowService implements WorkflowService {
350
506
  data: any,
351
507
  rpcService: any
352
508
  ): Promise<void> {
353
- // Get step state
354
- let stepState = await this.getStepState(runId, stepName)
509
+ // Use step-level lock to prevent concurrent execution of same step
510
+ await this.withStepLock(runId, stepName, async () => {
511
+ // Get step state
512
+ let stepState = await this.getStepState(runId, stepName)
355
513
 
356
- // Idempotency - if already succeeded, resume orchestrator and return
357
- if (stepState.status === 'succeeded') {
358
- await this.resumeWorkflow(runId)
359
- return
360
- }
514
+ // Idempotency - if already succeeded, resume orchestrator and return
515
+ if (stepState.status === 'succeeded') {
516
+ await this.resumeWorkflow(runId)
517
+ return
518
+ }
361
519
 
362
- // Log warning if already running (race condition)
363
- if (stepState.status === 'running') {
364
- return
365
- }
520
+ // Log warning if already running (race condition)
521
+ if (stepState.status === 'running') {
522
+ return
523
+ }
366
524
 
367
- // If status is 'failed', this is a retry - create new attempt history
368
- if (stepState.status === 'failed') {
369
- stepState = await this.createRetryAttempt(stepState.stepId, 'running')
370
- }
525
+ // If status is 'failed', this is a retry - create new attempt history
526
+ if (stepState.status === 'failed') {
527
+ stepState = await this.createRetryAttempt(stepState.stepId, 'running')
528
+ }
371
529
 
372
- if (stepState.status !== 'pending') {
373
- // Mark step as running (pending or failed status)
374
- await this.setStepRunning(stepState.stepId)
375
- }
530
+ if (stepState.status !== 'pending') {
531
+ // Mark step as running (pending or failed status)
532
+ await this.setStepRunning(stepState.stepId)
533
+ }
376
534
 
377
- try {
378
- // Execute RPC with workflow step context
379
- const result = await rpcService.rpcWithInteraction(rpcName, data, {
380
- workflowStep: {
381
- runId,
382
- stepId: stepState.stepId,
383
- attemptCount: stepState.attemptCount,
384
- },
385
- })
535
+ try {
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
386
546
 
387
- // Store result and mark succeeded
388
- await this.setStepResult(stepState.stepId, result)
547
+ // Execute as graph step with graph wire context
548
+ result = await executeGraphStep(
549
+ this,
550
+ rpcService,
551
+ runId,
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
+ }
389
568
 
390
- // Resume orchestrator to continue workflow
391
- try {
569
+ // Store result and mark succeeded
570
+ await this.setStepResult(stepState.stepId, result)
571
+
572
+ // Resume orchestrator to continue workflow
392
573
  await this.resumeWorkflow(runId)
393
- } catch (resumeError: any) {
394
- throw resumeError
395
- }
396
- } catch (error: any) {
397
- // Store error and mark failed
398
- await this.setStepError(stepState.stepId, error)
574
+ } catch (error: any) {
575
+ // Store error and mark failed
576
+ await this.setStepError(stepState.stepId, error)
399
577
 
400
- const maxAttempts = (stepState.retries ?? 0) + 1
401
- const retriesExhausted = stepState.attemptCount >= maxAttempts
578
+ const maxAttempts = (stepState.retries ?? 0) + 1
579
+ const retriesExhausted = stepState.attemptCount >= maxAttempts
402
580
 
403
- if (retriesExhausted) {
404
- // No more retries - resume orchestrator to mark workflow as failed
405
- await this.resumeWorkflow(runId)
406
- }
581
+ if (retriesExhausted) {
582
+ // No more retries - resume orchestrator to mark workflow as failed
583
+ await this.resumeWorkflow(runId)
584
+ }
407
585
 
408
- // Always throw so queue knows the job failed and can retry if needed
409
- throw error
410
- }
586
+ // Always throw so queue knows the job failed and can retry if needed
587
+ throw error
588
+ }
589
+ })
411
590
  }
412
591
 
413
592
  /**
@@ -461,7 +640,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
461
640
  let stepState: StepState
462
641
  try {
463
642
  stepState = await this.getStepState(runId, stepName)
464
- } catch (error: any) {
643
+ } catch {
465
644
  // Step doesn't exist - create it
466
645
  stepState = await this.insertStepState(
467
646
  runId,
@@ -498,8 +677,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
498
677
  // Step is pending - schedule it
499
678
  await this.setStepScheduled(stepState.stepId)
500
679
 
501
- // Enqueue step worker
502
- if (this.singletonServices!.queueService) {
680
+ // Enqueue step worker (unless inline mode)
681
+ if (!this.isInline(runId) && this.singletonServices!.queueService) {
503
682
  // Map step retry options to queue job options
504
683
  const retries = stepOptions?.retries ?? 0
505
684
  const retryDelay = stepOptions?.retryDelay
@@ -524,45 +703,52 @@ export abstract class PikkuWorkflowService implements WorkflowService {
524
703
  : undefined,
525
704
  }
526
705
  )
706
+ // Pause workflow - step will callback when done
707
+ throw new WorkflowAsyncException(runId, stepName)
527
708
  } else {
528
- // No queue service - execute locally
709
+ // Inline or no queue service - execute locally with retry loop
529
710
  const retries = stepOptions?.retries ?? 0
530
711
  const retryDelay = stepOptions?.retryDelay
531
- const attemptCount = stepState.attemptCount
532
-
533
- try {
534
- const result = await rpcService.rpcWithInteraction(rpcName, data, {
535
- workflowStep: {
536
- runId,
537
- stepId: stepState.stepId,
538
- attemptCount: stepState.attemptCount,
539
- },
540
- })
541
- await this.setStepResult(stepState.stepId, result)
542
- return result
543
- } catch (error: any) {
544
- // Record the error (marks step as failed)
545
- await this.setStepError(stepState.stepId, error)
546
-
547
- // Check if we should retry
548
- if (attemptCount < retries) {
549
- // Create a new pending retry attempt (copies metadata from failed step)
550
- await this.createRetryAttempt(stepState.stepId, 'pending')
551
-
552
- // Schedule orchestrator to retry after delay
553
- await this.scheduleOrchestratorRetry(runId, retryDelay)
712
+ let currentStepState = stepState
713
+
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
+ )
554
737
 
555
- // Pause workflow - orchestrator will replay and pick up new attempt
556
- throw new WorkflowAsyncException(runId, stepName)
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
+ }
557
749
  }
558
-
559
- // No more retries, fail the workflow
560
- throw error
561
750
  }
562
751
  }
563
-
564
- // Pause workflow - step will callback when done
565
- throw new WorkflowAsyncException(runId, stepName)
566
752
  }
567
753
 
568
754
  private async inlineStep(
@@ -575,7 +761,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
575
761
  let stepState: StepState
576
762
  try {
577
763
  stepState = await this.getStepState(runId, stepName)
578
- } catch (error: any) {
764
+ } catch {
579
765
  // Step doesn't exist - create it (inline, no RPC)
580
766
  stepState = await this.insertStepState(
581
767
  runId,
@@ -594,30 +780,68 @@ export abstract class PikkuWorkflowService implements WorkflowService {
594
780
  // Execute inline function
595
781
  const retries = stepOptions?.retries ?? this.getConfig().retries
596
782
  const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay
597
- const attemptCount = stepState.attemptCount
783
+ let currentStepState = stepState
784
+
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
+ )
598
805
 
599
- try {
600
- const result = await fn()
601
- await this.setStepResult(stepState.stepId, result)
602
- return result
603
- } catch (error: any) {
604
- // Record the error (marks step as failed)
605
- await this.setStepError(stepState.stepId, error)
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)
606
829
 
607
- // Check if we should retry
608
- if (attemptCount < retries) {
609
- // Create a new pending retry attempt (copies metadata from failed step)
610
- await this.createRetryAttempt(stepState.stepId, 'pending')
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')
611
834
 
612
- // Schedule orchestrator to retry after delay
613
- await this.scheduleOrchestratorRetry(runId, retryDelay)
835
+ // Schedule orchestrator to retry after delay
836
+ await this.scheduleOrchestratorRetry(runId, retryDelay)
614
837
 
615
- // Pause workflow - orchestrator will replay and pick up new attempt
616
- throw new WorkflowAsyncException(runId, stepName)
617
- }
838
+ // Pause workflow - orchestrator will replay and pick up new attempt
839
+ throw new WorkflowAsyncException(runId, stepName)
840
+ }
618
841
 
619
- // No more retries, fail the workflow
620
- throw error
842
+ // No more retries, fail the workflow
843
+ throw error
844
+ }
621
845
  }
622
846
  }
623
847
 
@@ -626,7 +850,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
626
850
  let stepState: StepState
627
851
  try {
628
852
  stepState = await this.getStepState(runId, stepName)
629
- } catch (error: any) {
853
+ } catch {
630
854
  // Step doesn't exist - create it (sleep step, no RPC)
631
855
  stepState = await this.insertStepState(
632
856
  runId,
@@ -650,9 +874,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
650
874
  // Step is pending - schedule it
651
875
  await this.setStepScheduled(stepState.stepId)
652
876
 
653
- // Check if we have queue service (remote mode) or inline mode
654
- if (this.singletonServices!.schedulerService) {
655
- // 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
656
880
  await this.singletonServices!.schedulerService.scheduleRPC(
657
881
  duration,
658
882
  this.getConfig().sleeperRPCName,
@@ -661,6 +885,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
661
885
  stepId: stepState.stepId,
662
886
  }
663
887
  )
888
+ // Pause workflow - sleep will callback when done
889
+ throw new WorkflowAsyncException(runId, stepName)
664
890
  } else {
665
891
  // Inline mode - use setTimeout with actual duration
666
892
  await new Promise((resolve) =>
@@ -669,29 +895,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
669
895
  await this.setStepResult(stepState.stepId, null)
670
896
  return
671
897
  }
672
-
673
- // Pause workflow - sleep will callback when done
674
- throw new WorkflowAsyncException(runId, stepName)
675
898
  }
676
899
 
677
- private async cancelWorkflow(runId: string, reason?: string) {
678
- // Update workflow run status to cancelled
679
- await this.updateRunStatus(runId, 'cancelled', undefined, {
680
- message: reason || 'Workflow cancelled by user',
681
- stack: '',
682
- code: 'WORKFLOW_CANCELLED',
683
- })
684
-
685
- // Throw cancellation exception to stop workflow execution
686
- throw new WorkflowCancelledException(runId, reason)
687
- }
688
-
689
- private createWorkflowInteraction(
900
+ private createWorkflowWire(
690
901
  workflowName: string,
691
902
  runId: string,
692
903
  rpcService: any
693
- ): PikkuWorkflowInteraction {
694
- const workflowInteraction: PikkuWorkflowInteraction = {
904
+ ): PikkuWorkflowWire {
905
+ const workflowWire: PikkuWorkflowWire = {
695
906
  workflowName,
696
907
  runId,
697
908
  getRun: async () => (await this.getRun(runId)) as WorkflowRun,
@@ -732,13 +943,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
732
943
  getDurationInMilliseconds(duration)
733
944
  )
734
945
  },
735
-
736
- // Implement workflow.cancel()
737
- cancel: async (reason?: string): Promise<void> => {
738
- await this.cancelWorkflow(runId, reason)
739
- },
740
946
  }
741
- return workflowInteraction
947
+ return workflowWire
742
948
  }
743
949
 
744
950
  private verifyStepName(stepName: string) {