@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
@@ -1,8 +1,50 @@
1
1
  import { runPikkuFunc } from '../../function/function-runner.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { getDurationInMilliseconds } from '../../time-utils.js';
4
- import { PikkuWiringTypes, } from '../../types/core.types.js';
5
- import { WorkflowAsyncException, WorkflowCancelledException, WorkflowNotFoundError, WorkflowRunNotFound, } from './workflow-runner.js';
4
+ import { executeGraphStep, runWorkflowGraph } from './graph/graph-runner.js';
5
+ import { PikkuError } from '../../errors/error-handler.js';
6
+ /**
7
+ * Exception thrown when workflow needs to pause for async step
8
+ */
9
+ export class WorkflowAsyncException extends Error {
10
+ runId;
11
+ stepName;
12
+ constructor(runId, stepName) {
13
+ super(`Workflow paused at step: ${stepName}`);
14
+ this.runId = runId;
15
+ this.stepName = stepName;
16
+ this.name = 'WorkflowAsyncException';
17
+ }
18
+ }
19
+ /**
20
+ * Exception thrown when workflow is cancelled
21
+ */
22
+ export class WorkflowCancelledException extends Error {
23
+ runId;
24
+ reason;
25
+ constructor(runId, reason) {
26
+ super(reason || 'Workflow cancelled');
27
+ this.runId = runId;
28
+ this.reason = reason;
29
+ this.name = 'WorkflowCancelledException';
30
+ }
31
+ }
32
+ /**
33
+ * Error class for workflow not found
34
+ */
35
+ export class WorkflowNotFoundError extends PikkuError {
36
+ constructor(name) {
37
+ super(`Workflow not found: ${name}`);
38
+ }
39
+ }
40
+ /**
41
+ * Error class for workflow not found
42
+ */
43
+ export class WorkflowRunNotFound extends PikkuError {
44
+ constructor(runId) {
45
+ super(`Workflow run not found: ${runId}`);
46
+ }
47
+ }
6
48
  export class WorkflowServiceNotInitialized extends Error {
7
49
  }
8
50
  export class WorkflowStepNameNotString extends Error {
@@ -18,17 +60,36 @@ export class WorkflowStepNameNotString extends Error {
18
60
  export class PikkuWorkflowService {
19
61
  config;
20
62
  singletonServices;
21
- createSessionServices;
63
+ createWireServices;
64
+ inlineRuns = new Set();
22
65
  constructor() { }
23
- setServices(singletonServices, createSessionServices, { workflow }) {
66
+ /**
67
+ * Check if a run is executing inline (without queues)
68
+ */
69
+ isInline(runId) {
70
+ return this.inlineRuns.has(runId);
71
+ }
72
+ /**
73
+ * Register a run as inline (for graph-runner to use)
74
+ */
75
+ registerInlineRun(runId) {
76
+ this.inlineRuns.add(runId);
77
+ }
78
+ /**
79
+ * Unregister a run from inline tracking
80
+ */
81
+ unregisterInlineRun(runId) {
82
+ this.inlineRuns.delete(runId);
83
+ }
84
+ setServices(singletonServices, createWireServices, { workflow }) {
24
85
  this.singletonServices = singletonServices;
25
- this.createSessionServices = createSessionServices;
86
+ this.createWireServices = createWireServices;
26
87
  this.config = {
27
88
  retries: workflow?.retries ?? 0,
28
89
  retryDelay: workflow?.retryDelay ?? 0,
29
90
  orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
30
91
  stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
31
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowStepSleeper',
92
+ sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
32
93
  };
33
94
  }
34
95
  // ============================================================================
@@ -47,7 +108,7 @@ export class PikkuWorkflowService {
47
108
  * Sets the step result to null and resumes the workflow
48
109
  * @param data - Sleeper input data
49
110
  */
50
- async executeWorkflowSleep(runId, stepId) {
111
+ async executeWorkflowSleepCompleted(runId, stepId) {
51
112
  await this.setStepResult(stepId, null);
52
113
  await this.resumeWorkflow(runId);
53
114
  }
@@ -62,81 +123,91 @@ export class PikkuWorkflowService {
62
123
  }
63
124
  /**
64
125
  * Start a new workflow run
126
+ * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
127
+ * @param options.inline - If true, execute workflow directly without queue service
128
+ * @param options.startNode - Starting node ID for graph workflows (from wire config)
65
129
  */
66
- async startWorkflow(name, input, rpcService) {
67
- const registrations = pikkuState('workflows', 'registrations');
130
+ async startWorkflow(name, input, rpcService, options) {
131
+ // Check meta to determine workflow type
132
+ const meta = pikkuState(null, 'workflows', 'meta');
133
+ const workflowMeta = meta[name];
134
+ if (!workflowMeta) {
135
+ throw new WorkflowNotFoundError(name);
136
+ }
137
+ // Check if this is a graph workflow (source === 'graph')
138
+ if (workflowMeta.source === 'graph') {
139
+ // Graph workflows use the graph scheduler
140
+ return runWorkflowGraph(this, name, input, rpcService, options?.inline, options?.startNode);
141
+ }
142
+ // DSL workflow - check registration exists
143
+ const registrations = pikkuState(null, 'workflows', 'registrations');
68
144
  const workflow = registrations.get(name);
69
145
  if (!workflow) {
70
146
  throw new WorkflowNotFoundError(name);
71
147
  }
72
148
  // Create workflow run in state
73
- const runId = await this.createRun(name, input);
74
- // If queue service is available, use remote execution (queue-based)
75
- // Otherwise, execute directly (inline/synchronous)
76
- if (this.singletonServices?.queueService) {
77
- // Queue orchestrator to start the workflow
78
- await this.resumeWorkflow(runId);
149
+ const runId = await this.createRun(name, input, options?.inline);
150
+ // Register inline run for fast lookup
151
+ if (options?.inline) {
152
+ this.inlineRuns.add(runId);
79
153
  }
80
- else {
81
- // No queue service - execute directly via runWorkflowJob
82
- await this.runWorkflowJob(runId, rpcService);
154
+ try {
155
+ // If inline mode or no queue service, execute directly
156
+ // Otherwise, use remote execution (queue-based)
157
+ if (options?.inline || !this.singletonServices?.queueService) {
158
+ await this.runWorkflowJob(runId, rpcService);
159
+ }
160
+ else {
161
+ await this.resumeWorkflow(runId);
162
+ }
163
+ return { runId };
164
+ }
165
+ finally {
166
+ // Clean up inline tracking
167
+ if (options?.inline) {
168
+ this.inlineRuns.delete(runId);
169
+ }
83
170
  }
84
- return { runId };
85
171
  }
86
172
  async runWorkflowJob(runId, rpcService) {
87
- // Get the run
88
173
  const run = await this.getRun(runId);
89
174
  if (!run) {
90
175
  throw new WorkflowRunNotFound(runId);
91
176
  }
92
- // Get workflow registration
93
- const registrations = pikkuState('workflows', 'registrations');
177
+ const registrations = pikkuState(null, 'workflows', 'registrations');
94
178
  const workflow = registrations.get(run.workflow);
95
179
  if (!workflow) {
96
180
  throw new WorkflowNotFoundError(run.workflow);
97
181
  }
98
- // Use lock to prevent concurrent execution
99
182
  await this.withRunLock(runId, async () => {
100
- // Create workflow interaction object
101
- const workflowInteraction = this.createWorkflowInteraction(run.workflow, runId, rpcService);
102
- // Get function metadata
103
- const meta = pikkuState('workflows', 'meta');
183
+ const meta = pikkuState(null, 'workflows', 'meta');
104
184
  const workflowMeta = meta[run.workflow];
105
- // Execute workflow function with workflow interaction
185
+ const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService);
186
+ const wire = { workflow: workflowWire };
106
187
  try {
107
- const getAllServices = () => {
108
- let sessionServices = {};
109
- const interaction = { workflow: workflowInteraction };
110
- if (this.createSessionServices) {
111
- sessionServices = this.createSessionServices(this.singletonServices, interaction, undefined);
112
- }
113
- return {
114
- ...this.singletonServices,
115
- ...sessionServices,
116
- ...interaction,
117
- };
118
- };
119
- const result = await runPikkuFunc(PikkuWiringTypes.workflow, workflowMeta.workflowName, workflowMeta.pikkuFuncName, {
188
+ const result = await runPikkuFunc('workflow', workflowMeta.name, workflowMeta.pikkuFuncName, {
120
189
  singletonServices: this.singletonServices,
121
- interaction: { workflow: workflowInteraction },
122
- getAllServices,
190
+ wire,
191
+ createWireServices: this.createWireServices,
123
192
  data: () => run.input,
124
193
  });
125
- // Workflow completed successfully
126
194
  await this.updateRunStatus(runId, 'completed', result);
127
195
  }
128
196
  catch (error) {
129
- // Check if it's a WorkflowAsyncException
130
197
  if (error instanceof WorkflowAsyncException) {
131
198
  // Normal - workflow paused for step execution
132
199
  throw error;
133
200
  }
134
201
  // Check if it's a WorkflowCancelledException
135
202
  if (error instanceof WorkflowCancelledException) {
136
- // Workflow was cancelled - status already updated, just rethrow
203
+ // Workflow was cancelled - update status and rethrow
204
+ await this.updateRunStatus(runId, 'cancelled', undefined, {
205
+ message: error.message || 'Workflow cancelled',
206
+ stack: '',
207
+ code: 'WORKFLOW_CANCELLED',
208
+ });
137
209
  throw error;
138
210
  }
139
- // Real error - mark as failed
140
211
  await this.updateRunStatus(runId, 'failed', undefined, {
141
212
  message: error.message,
142
213
  stack: error.stack,
@@ -151,56 +222,68 @@ export class PikkuWorkflowService {
151
222
  * Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
152
223
  */
153
224
  async executeWorkflowStep(runId, stepName, rpcName, data, rpcService) {
154
- // Get step state
155
- let stepState = await this.getStepState(runId, stepName);
156
- // Idempotency - if already succeeded, resume orchestrator and return
157
- if (stepState.status === 'succeeded') {
158
- await this.resumeWorkflow(runId);
159
- return;
160
- }
161
- // Log warning if already running (race condition)
162
- if (stepState.status === 'running') {
163
- return;
164
- }
165
- // If status is 'failed', this is a retry - create new attempt history
166
- if (stepState.status === 'failed') {
167
- stepState = await this.createRetryAttempt(stepState.stepId, 'running');
168
- }
169
- if (stepState.status !== 'pending') {
170
- // Mark step as running (pending or failed status)
171
- await this.setStepRunning(stepState.stepId);
172
- }
173
- try {
174
- // Execute RPC with workflow step context
175
- const result = await rpcService.rpcWithInteraction(rpcName, data, {
176
- workflowStep: {
177
- runId,
178
- stepId: stepState.stepId,
179
- attemptCount: stepState.attemptCount,
180
- },
181
- });
182
- // Store result and mark succeeded
183
- await this.setStepResult(stepState.stepId, result);
184
- // Resume orchestrator to continue workflow
185
- try {
225
+ // Use step-level lock to prevent concurrent execution of same step
226
+ await this.withStepLock(runId, stepName, async () => {
227
+ // Get step state
228
+ let stepState = await this.getStepState(runId, stepName);
229
+ // Idempotency - if already succeeded, resume orchestrator and return
230
+ if (stepState.status === 'succeeded') {
186
231
  await this.resumeWorkflow(runId);
232
+ return;
187
233
  }
188
- catch (resumeError) {
189
- throw resumeError;
234
+ // Log warning if already running (race condition)
235
+ if (stepState.status === 'running') {
236
+ return;
190
237
  }
191
- }
192
- catch (error) {
193
- // Store error and mark failed
194
- await this.setStepError(stepState.stepId, error);
195
- const maxAttempts = (stepState.retries ?? 0) + 1;
196
- const retriesExhausted = stepState.attemptCount >= maxAttempts;
197
- if (retriesExhausted) {
198
- // No more retries - resume orchestrator to mark workflow as failed
238
+ // If status is 'failed', this is a retry - create new attempt history
239
+ if (stepState.status === 'failed') {
240
+ stepState = await this.createRetryAttempt(stepState.stepId, 'running');
241
+ }
242
+ if (stepState.status !== 'pending') {
243
+ // Mark step as running (pending or failed status)
244
+ await this.setStepRunning(stepState.stepId);
245
+ }
246
+ try {
247
+ let result;
248
+ // Check if this is a graph node step (step name starts with 'node:')
249
+ if (stepName.startsWith('node:')) {
250
+ // Get the graph name from the workflow run
251
+ const run = await this.getRun(runId);
252
+ if (!run) {
253
+ throw new Error(`Workflow run not found: ${runId}`);
254
+ }
255
+ const graphName = run.workflow;
256
+ // Execute as graph step with graph wire context
257
+ result = await executeGraphStep(this, rpcService, runId, stepState.stepId, stepName, rpcName, data, graphName);
258
+ }
259
+ else {
260
+ // Execute RPC with workflow step context (regular workflow)
261
+ result = await rpcService.rpcWithWire(rpcName, data, {
262
+ workflowStep: {
263
+ runId,
264
+ stepId: stepState.stepId,
265
+ attemptCount: stepState.attemptCount,
266
+ },
267
+ });
268
+ }
269
+ // Store result and mark succeeded
270
+ await this.setStepResult(stepState.stepId, result);
271
+ // Resume orchestrator to continue workflow
199
272
  await this.resumeWorkflow(runId);
200
273
  }
201
- // Always throw so queue knows the job failed and can retry if needed
202
- throw error;
203
- }
274
+ catch (error) {
275
+ // Store error and mark failed
276
+ await this.setStepError(stepState.stepId, error);
277
+ const maxAttempts = (stepState.retries ?? 0) + 1;
278
+ const retriesExhausted = stepState.attemptCount >= maxAttempts;
279
+ if (retriesExhausted) {
280
+ // No more retries - resume orchestrator to mark workflow as failed
281
+ await this.resumeWorkflow(runId);
282
+ }
283
+ // Always throw so queue knows the job failed and can retry if needed
284
+ throw error;
285
+ }
286
+ });
204
287
  }
205
288
  /**
206
289
  * Orchestrate workflow execution (called by orchestrator)
@@ -236,7 +319,7 @@ export class PikkuWorkflowService {
236
319
  try {
237
320
  stepState = await this.getStepState(runId, stepName);
238
321
  }
239
- catch (error) {
322
+ catch {
240
323
  // Step doesn't exist - create it
241
324
  stepState = await this.insertStepState(runId, stepName, rpcName, data, stepOptions);
242
325
  }
@@ -260,8 +343,8 @@ export class PikkuWorkflowService {
260
343
  }
261
344
  // Step is pending - schedule it
262
345
  await this.setStepScheduled(stepState.stepId);
263
- // Enqueue step worker
264
- if (this.singletonServices.queueService) {
346
+ // Enqueue step worker (unless inline mode)
347
+ if (!this.isInline(runId) && this.singletonServices.queueService) {
265
348
  // Map step retry options to queue job options
266
349
  const retries = stepOptions?.retries ?? 0;
267
350
  const retryDelay = stepOptions?.retryDelay;
@@ -280,41 +363,47 @@ export class PikkuWorkflowService {
280
363
  ? 'exponential'
281
364
  : undefined,
282
365
  });
366
+ // Pause workflow - step will callback when done
367
+ throw new WorkflowAsyncException(runId, stepName);
283
368
  }
284
369
  else {
285
- // No queue service - execute locally
370
+ // Inline or no queue service - execute locally with retry loop
286
371
  const retries = stepOptions?.retries ?? 0;
287
372
  const retryDelay = stepOptions?.retryDelay;
288
- const attemptCount = stepState.attemptCount;
289
- try {
290
- const result = await rpcService.rpcWithInteraction(rpcName, data, {
291
- workflowStep: {
292
- runId,
293
- stepId: stepState.stepId,
294
- attemptCount: stepState.attemptCount,
295
- },
296
- });
297
- await this.setStepResult(stepState.stepId, result);
298
- return result;
299
- }
300
- catch (error) {
301
- // Record the error (marks step as failed)
302
- await this.setStepError(stepState.stepId, error);
303
- // Check if we should retry
304
- if (attemptCount < retries) {
305
- // Create a new pending retry attempt (copies metadata from failed step)
306
- await this.createRetryAttempt(stepState.stepId, 'pending');
307
- // Schedule orchestrator to retry after delay
308
- await this.scheduleOrchestratorRetry(runId, retryDelay);
309
- // Pause workflow - orchestrator will replay and pick up new attempt
310
- throw new WorkflowAsyncException(runId, stepName);
373
+ let currentStepState = stepState;
374
+ while (true) {
375
+ try {
376
+ await this.setStepRunning(currentStepState.stepId);
377
+ const result = await rpcService.rpcWithWire(rpcName, data, {
378
+ workflowStep: {
379
+ runId,
380
+ stepId: currentStepState.stepId,
381
+ attemptCount: currentStepState.attemptCount,
382
+ },
383
+ });
384
+ await this.setStepResult(currentStepState.stepId, result);
385
+ return result;
386
+ }
387
+ catch (error) {
388
+ // Record the error (marks step as failed)
389
+ await this.setStepError(currentStepState.stepId, error);
390
+ // Check if we should retry
391
+ if (currentStepState.attemptCount < retries) {
392
+ // Create a new pending retry attempt
393
+ currentStepState = await this.createRetryAttempt(currentStepState.stepId, 'pending');
394
+ // Wait for retry delay if specified
395
+ if (retryDelay) {
396
+ await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(retryDelay)));
397
+ }
398
+ // Continue loop to retry
399
+ }
400
+ else {
401
+ // No more retries, fail the workflow
402
+ throw error;
403
+ }
311
404
  }
312
- // No more retries, fail the workflow
313
- throw error;
314
405
  }
315
406
  }
316
- // Pause workflow - step will callback when done
317
- throw new WorkflowAsyncException(runId, stepName);
318
407
  }
319
408
  async inlineStep(runId, stepName, fn, stepOptions) {
320
409
  // Check if step already exists
@@ -322,7 +411,7 @@ export class PikkuWorkflowService {
322
411
  try {
323
412
  stepState = await this.getStepState(runId, stepName);
324
413
  }
325
- catch (error) {
414
+ catch {
326
415
  // Step doesn't exist - create it (inline, no RPC)
327
416
  stepState = await this.insertStepState(runId, stepName, null, // No RPC for inline steps
328
417
  null, // No data for inline steps
@@ -335,26 +424,60 @@ export class PikkuWorkflowService {
335
424
  // Execute inline function
336
425
  const retries = stepOptions?.retries ?? this.getConfig().retries;
337
426
  const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay;
338
- const attemptCount = stepState.attemptCount;
339
- try {
340
- const result = await fn();
341
- await this.setStepResult(stepState.stepId, result);
342
- return result;
427
+ let currentStepState = stepState;
428
+ // Check if we're running inline (in-memory) or remote (queue-based)
429
+ if (this.isInline(runId)) {
430
+ // Inline mode - execute with retry loop
431
+ while (true) {
432
+ try {
433
+ await this.setStepRunning(currentStepState.stepId);
434
+ const result = await fn();
435
+ await this.setStepResult(currentStepState.stepId, result);
436
+ return result;
437
+ }
438
+ catch (error) {
439
+ // Record the error (marks step as failed)
440
+ await this.setStepError(currentStepState.stepId, error);
441
+ // Check if we should retry
442
+ if (currentStepState.attemptCount < retries) {
443
+ // Create a new pending retry attempt
444
+ currentStepState = await this.createRetryAttempt(currentStepState.stepId, 'pending');
445
+ // Wait for retry delay if specified
446
+ if (retryDelay) {
447
+ await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(retryDelay)));
448
+ }
449
+ // Continue loop to retry
450
+ }
451
+ else {
452
+ // No more retries, fail the workflow
453
+ throw error;
454
+ }
455
+ }
456
+ }
343
457
  }
344
- catch (error) {
345
- // Record the error (marks step as failed)
346
- await this.setStepError(stepState.stepId, error);
347
- // Check if we should retry
348
- if (attemptCount < retries) {
349
- // Create a new pending retry attempt (copies metadata from failed step)
350
- await this.createRetryAttempt(stepState.stepId, 'pending');
351
- // Schedule orchestrator to retry after delay
352
- await this.scheduleOrchestratorRetry(runId, retryDelay);
353
- // Pause workflow - orchestrator will replay and pick up new attempt
354
- throw new WorkflowAsyncException(runId, stepName);
458
+ else {
459
+ // Remote mode - use queue-based retry
460
+ try {
461
+ await this.setStepRunning(currentStepState.stepId);
462
+ const result = await fn();
463
+ await this.setStepResult(currentStepState.stepId, result);
464
+ return result;
465
+ }
466
+ catch (error) {
467
+ // Record the error (marks step as failed)
468
+ await this.setStepError(currentStepState.stepId, error);
469
+ // Check if we should retry
470
+ if (currentStepState.attemptCount < retries) {
471
+ // Create a new pending retry attempt (copies metadata from failed step)
472
+ await this.createRetryAttempt(currentStepState.stepId, 'pending');
473
+ // Schedule orchestrator to retry after delay
474
+ await this.scheduleOrchestratorRetry(runId, retryDelay);
475
+ // Pause workflow - orchestrator will replay and pick up new attempt
476
+ throw new WorkflowAsyncException(runId, stepName);
477
+ }
478
+ // No more retries, fail the workflow
479
+ throw error;
355
480
  }
356
- // No more retries, fail the workflow
357
- throw error;
358
481
  }
359
482
  }
360
483
  async sleepStep(runId, stepName, duration) {
@@ -363,7 +486,7 @@ export class PikkuWorkflowService {
363
486
  try {
364
487
  stepState = await this.getStepState(runId, stepName);
365
488
  }
366
- catch (error) {
489
+ catch {
367
490
  // Step doesn't exist - create it (sleep step, no RPC)
368
491
  stepState = await this.insertStepState(runId, stepName, null, // No RPC for sleep steps
369
492
  { duration }, // Store duration as data
@@ -380,13 +503,15 @@ export class PikkuWorkflowService {
380
503
  }
381
504
  // Step is pending - schedule it
382
505
  await this.setStepScheduled(stepState.stepId);
383
- // Check if we have queue service (remote mode) or inline mode
384
- if (this.singletonServices.schedulerService) {
385
- // Remote mode - enqueue sleep worker with delay
506
+ // Check if inline mode or no scheduler service
507
+ if (!this.isInline(runId) && this.singletonServices.schedulerService) {
508
+ // Remote mode - schedule sleep via scheduler service
386
509
  await this.singletonServices.schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, {
387
510
  runId,
388
511
  stepId: stepState.stepId,
389
512
  });
513
+ // Pause workflow - sleep will callback when done
514
+ throw new WorkflowAsyncException(runId, stepName);
390
515
  }
391
516
  else {
392
517
  // Inline mode - use setTimeout with actual duration
@@ -394,21 +519,9 @@ export class PikkuWorkflowService {
394
519
  await this.setStepResult(stepState.stepId, null);
395
520
  return;
396
521
  }
397
- // Pause workflow - sleep will callback when done
398
- throw new WorkflowAsyncException(runId, stepName);
399
522
  }
400
- async cancelWorkflow(runId, reason) {
401
- // Update workflow run status to cancelled
402
- await this.updateRunStatus(runId, 'cancelled', undefined, {
403
- message: reason || 'Workflow cancelled by user',
404
- stack: '',
405
- code: 'WORKFLOW_CANCELLED',
406
- });
407
- // Throw cancellation exception to stop workflow execution
408
- throw new WorkflowCancelledException(runId, reason);
409
- }
410
- createWorkflowInteraction(workflowName, runId, rpcService) {
411
- const workflowInteraction = {
523
+ createWorkflowWire(workflowName, runId, rpcService) {
524
+ const workflowWire = {
412
525
  workflowName,
413
526
  runId,
414
527
  getRun: async () => (await this.getRun(runId)),
@@ -427,12 +540,8 @@ export class PikkuWorkflowService {
427
540
  this.verifyStepName(stepName);
428
541
  await this.sleepStep(runId, stepName, getDurationInMilliseconds(duration));
429
542
  },
430
- // Implement workflow.cancel()
431
- cancel: async (reason) => {
432
- await this.cancelWorkflow(runId, reason);
433
- },
434
543
  };
435
- return workflowInteraction;
544
+ return workflowWire;
436
545
  }
437
546
  verifyStepName(stepName) {
438
547
  if (typeof stepName !== 'string') {
@@ -0,0 +1,42 @@
1
+ import type { WorkflowWires } from './workflow.types.js';
2
+ import type { GraphNodeConfig } from './graph/workflow-graph.types.js';
3
+ /**
4
+ * Workflow definition with DSL function
5
+ */
6
+ export interface WorkflowDefinitionFunc {
7
+ wires: WorkflowWires;
8
+ func: {
9
+ func: (...args: any[]) => any;
10
+ } | ((...args: any[]) => any);
11
+ }
12
+ /**
13
+ * Workflow definition with graph
14
+ */
15
+ export interface WorkflowDefinitionGraph<T extends Record<string, GraphNodeConfig<string>> = Record<string, GraphNodeConfig<string>>> {
16
+ wires: WorkflowWires;
17
+ graph: T;
18
+ }
19
+ /**
20
+ * Union type for workflow definitions
21
+ */
22
+ export type WorkflowDefinition<T extends Record<string, GraphNodeConfig<string>> = Record<string, GraphNodeConfig<string>>> = WorkflowDefinitionFunc | WorkflowDefinitionGraph<T>;
23
+ /**
24
+ * Wire a workflow with triggers.
25
+ * Accepts either a DSL function (func) or a graph definition (graph).
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // DSL workflow
30
+ * wireWorkflow({
31
+ * wires: { http: { route: '/start', method: 'post' } },
32
+ * func: myWorkflowFunc,
33
+ * })
34
+ *
35
+ * // Graph workflow
36
+ * wireWorkflow({
37
+ * wires: { http: { route: '/graph-start', method: 'post' } },
38
+ * graph: myGraphWorkflow,
39
+ * })
40
+ * ```
41
+ */
42
+ export declare function wireWorkflow<T extends Record<string, GraphNodeConfig<string>>>(definition: WorkflowDefinitionFunc | WorkflowDefinitionGraph<T>): void;