@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
@@ -0,0 +1,694 @@
1
+ import type { PikkuWorkflowService } from '../pikku-workflow-service.js'
2
+ import type {
3
+ GraphNodeConfig,
4
+ RefValue,
5
+ RefFn,
6
+ NextConfig,
7
+ GraphWireState,
8
+ PikkuGraphWire,
9
+ WorkflowGraphDefinition,
10
+ } from './workflow-graph.types.js'
11
+ import { createRef, isRef } from './workflow-graph.types.js'
12
+ import { pikkuState } from '../../../pikku-state.js'
13
+
14
+ /**
15
+ * Add a workflow graph to the system
16
+ * This is called by the generated workflow wirings
17
+ */
18
+ export const addWorkflowGraph = (
19
+ workflowName: string,
20
+ graphResult: { graph: Record<string, GraphNodeConfig<string>>; wires?: any }
21
+ ) => {
22
+ // Get workflow metadata from inspector
23
+ const meta = pikkuState(null, 'workflows', 'meta')
24
+ const workflowMeta = meta[workflowName]
25
+ if (!workflowMeta) {
26
+ throw new Error(
27
+ `Workflow metadata not found for '${workflowName}'. Make sure to run the CLI to generate metadata.`
28
+ )
29
+ }
30
+
31
+ // Store workflow graph definition in state
32
+ const registrations = pikkuState(null, 'workflows', 'graphRegistrations')
33
+ registrations.set(workflowName, {
34
+ name: workflowName,
35
+ wires: graphResult.wires || {},
36
+ graph: graphResult.graph,
37
+ })
38
+ }
39
+
40
+ /**
41
+ * Get a registered workflow graph by name
42
+ */
43
+ export function getWorkflowGraph(
44
+ name: string
45
+ ): WorkflowGraphDefinition<any> | undefined {
46
+ const registrations = pikkuState(null, 'workflows', 'graphRegistrations')
47
+ return registrations.get(name)
48
+ }
49
+
50
+ /**
51
+ * Resolve next config to array of node IDs
52
+ * For branching (Record), uses the branch key from step
53
+ */
54
+ function resolveNextFromConfig(
55
+ next: NextConfig<string> | undefined,
56
+ branchKey?: string
57
+ ): string[] {
58
+ if (!next) return []
59
+
60
+ if (typeof next === 'string') return [next]
61
+ if (Array.isArray(next)) return next
62
+
63
+ // Record (branching) - use branch key set by graph.branch()
64
+ if (!branchKey || !(branchKey in next)) return []
65
+
66
+ const branchNext = next[branchKey]
67
+ return Array.isArray(branchNext) ? branchNext : [branchNext]
68
+ }
69
+
70
+ /**
71
+ * Template value - represents a string template with variable interpolation
72
+ */
73
+ interface TemplateValue {
74
+ $template: {
75
+ parts: string[]
76
+ expressions: Array<{ $ref: string; path?: string }>
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check if a value is a template
82
+ */
83
+ function isTemplate(value: unknown): value is TemplateValue {
84
+ return (
85
+ typeof value === 'object' &&
86
+ value !== null &&
87
+ '$template' in value &&
88
+ typeof (value as any).$template === 'object'
89
+ )
90
+ }
91
+
92
+ /**
93
+ * Create a template function for use in input callbacks
94
+ */
95
+ function createTemplate(templateStr: string, refs: RefValue[]): TemplateValue {
96
+ const parts: string[] = []
97
+ const expressions: Array<{ $ref: string; path?: string }> = []
98
+
99
+ const regex = /\$(\d+)/g
100
+ let lastIndex = 0
101
+ let match
102
+
103
+ while ((match = regex.exec(templateStr)) !== null) {
104
+ parts.push(templateStr.slice(lastIndex, match.index))
105
+ const refIndex = parseInt(match[1]!, 10)
106
+ const refValue = refs[refIndex]
107
+ if (refValue) {
108
+ expressions.push({ $ref: refValue.nodeId, path: refValue.path })
109
+ } else {
110
+ expressions.push({ $ref: 'unknown' })
111
+ }
112
+ lastIndex = regex.lastIndex
113
+ }
114
+ parts.push(templateStr.slice(lastIndex))
115
+
116
+ return { $template: { parts, expressions } }
117
+ }
118
+
119
+ /**
120
+ * Evaluate a node's input callback to get the input mapping
121
+ */
122
+ function evaluateInputCallback(
123
+ node: GraphNodeConfig
124
+ ): Record<string, unknown | RefValue | TemplateValue> {
125
+ if (!node.input) return {}
126
+
127
+ const ref: RefFn<string> = (targetNodeId: string, path?: string) =>
128
+ createRef(targetNodeId, path)
129
+
130
+ const template = (templateStr: string, refs: RefValue[]) =>
131
+ createTemplate(templateStr, refs)
132
+
133
+ // Call with both ref and template - input callback may accept 1 or 2 params
134
+ return (node.input as any)(ref, template)
135
+ }
136
+
137
+ /**
138
+ * Extract node IDs referenced in an input mapping (including from templates)
139
+ */
140
+ function extractReferencedNodeIds(
141
+ inputMapping: Record<string, unknown | RefValue | TemplateValue>
142
+ ): string[] {
143
+ const nodeIds: string[] = []
144
+ for (const value of Object.values(inputMapping)) {
145
+ if (isRef(value)) {
146
+ nodeIds.push(value.nodeId)
147
+ } else if (isTemplate(value)) {
148
+ for (const expr of value.$template.expressions) {
149
+ nodeIds.push(expr.$ref)
150
+ }
151
+ }
152
+ }
153
+ return [...new Set(nodeIds)]
154
+ }
155
+
156
+ /**
157
+ * Resolve a template value using node results
158
+ */
159
+ function resolveTemplate(
160
+ template: TemplateValue,
161
+ nodeResults: Record<string, any>
162
+ ): string {
163
+ const { parts, expressions } = template.$template
164
+ let result = ''
165
+ for (let i = 0; i < parts.length; i++) {
166
+ result += parts[i]
167
+ if (i < expressions.length) {
168
+ const expr = expressions[i]
169
+ const nodeResult = nodeResults[expr.$ref]
170
+ const value = expr.path
171
+ ? getValueAtPath(nodeResult, expr.path)
172
+ : nodeResult
173
+ result += String(value ?? '')
174
+ }
175
+ }
176
+ return result
177
+ }
178
+
179
+ /**
180
+ * Resolve input mapping using node results
181
+ */
182
+ function resolveInputMapping(
183
+ inputMapping: Record<string, unknown | RefValue | TemplateValue>,
184
+ nodeResults: Record<string, any>
185
+ ): Record<string, any> {
186
+ const resolved: Record<string, any> = {}
187
+
188
+ for (const [key, value] of Object.entries(inputMapping)) {
189
+ if (isRef(value)) {
190
+ const nodeResult = nodeResults[value.nodeId]
191
+ resolved[key] = value.path
192
+ ? getValueAtPath(nodeResult, value.path)
193
+ : nodeResult
194
+ } else if (isTemplate(value)) {
195
+ resolved[key] = resolveTemplate(value, nodeResults)
196
+ } else {
197
+ resolved[key] = value
198
+ }
199
+ }
200
+
201
+ return resolved
202
+ }
203
+
204
+ /**
205
+ * Get value at a dot-notation path from an object
206
+ */
207
+ function getValueAtPath(obj: any, path: string): any {
208
+ if (!path) return obj
209
+ const parts = path.split('.')
210
+ let current = obj
211
+ for (const part of parts) {
212
+ if (current == null) return undefined
213
+ current = current[part]
214
+ }
215
+ return current
216
+ }
217
+
218
+ /**
219
+ * Get the RPC name from a graph node's func
220
+ * Supports both function references (func.name) and string RPC names
221
+ */
222
+ function getRpcName(node: GraphNodeConfig): string {
223
+ const func = node.func as any
224
+ // If func is a string, use it directly as the RPC name
225
+ if (typeof func === 'string') {
226
+ return func
227
+ }
228
+ // Otherwise, it's a function reference - use its name
229
+ return func?.name || 'unknown'
230
+ }
231
+
232
+ /**
233
+ * Queue a graph node for execution
234
+ */
235
+ async function queueGraphNode(
236
+ workflowService: PikkuWorkflowService,
237
+ runId: string,
238
+ _graphName: string,
239
+ nodeId: string,
240
+ rpcName: string,
241
+ input: any
242
+ ): Promise<void> {
243
+ // Step name convention: node:<nodeId>
244
+ // Graph name is stored as the workflow name on the run
245
+ await workflowService.insertStepState(
246
+ runId,
247
+ `node:${nodeId}`,
248
+ rpcName,
249
+ input,
250
+ { retries: 3 }
251
+ )
252
+ await workflowService.resumeWorkflow(runId)
253
+ }
254
+
255
+ /**
256
+ * Continue graph execution
257
+ * Non-blocking - finds pending nodes, resolves inputs, queues them for execution
258
+ */
259
+ export async function continueGraph(
260
+ workflowService: PikkuWorkflowService,
261
+ runId: string,
262
+ graphName: string
263
+ ): Promise<void> {
264
+ const definition = getWorkflowGraph(graphName)
265
+ if (!definition) {
266
+ throw new Error(`Workflow graph '${graphName}' not found`)
267
+ }
268
+
269
+ const graph = definition.graph
270
+
271
+ // Get completed node IDs + branch keys (lightweight, no results)
272
+ const { completedNodeIds, branchKeys } =
273
+ await workflowService.getCompletedGraphState(runId)
274
+
275
+ // Find candidate next nodes from completed nodes
276
+ const candidateNodes: string[] = []
277
+
278
+ for (const nodeId of completedNodeIds) {
279
+ const node = graph[nodeId]
280
+ if (!node?.next) continue
281
+
282
+ const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId])
283
+ candidateNodes.push(...nextNodes)
284
+ }
285
+
286
+ if (candidateNodes.length === 0 && completedNodeIds.length > 0) {
287
+ // No more nodes to run - graph complete
288
+ await workflowService.updateRunStatus(runId, 'completed')
289
+ return
290
+ }
291
+
292
+ // Filter to only nodes that don't have a step yet
293
+ const nodesToQueue = await workflowService.getNodesWithoutSteps(
294
+ runId,
295
+ candidateNodes
296
+ )
297
+
298
+ // Queue each node for execution
299
+ for (const nodeId of nodesToQueue) {
300
+ const node = graph[nodeId]
301
+ if (!node) continue
302
+
303
+ // Evaluate input callback to get the mapping
304
+ const inputMapping = evaluateInputCallback(node)
305
+
306
+ // Only fetch results for nodes referenced in this node's input
307
+ const referencedNodeIds = extractReferencedNodeIds(inputMapping)
308
+ const nodeResults = await workflowService.getNodeResults(
309
+ runId,
310
+ referencedNodeIds
311
+ )
312
+
313
+ const resolvedInput = resolveInputMapping(inputMapping, nodeResults)
314
+ const rpcName = getRpcName(node)
315
+
316
+ await queueGraphNode(
317
+ workflowService,
318
+ runId,
319
+ graphName,
320
+ nodeId,
321
+ rpcName,
322
+ resolvedInput
323
+ )
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Execute a graph step with wire context.
329
+ * Called by the step worker when executing graph nodes.
330
+ */
331
+ export async function executeGraphStep(
332
+ workflowService: PikkuWorkflowService,
333
+ rpcService: any,
334
+ runId: string,
335
+ stepId: string,
336
+ stepName: string,
337
+ rpcName: string,
338
+ data: any,
339
+ graphName: string
340
+ ): Promise<any> {
341
+ const nodeId = stepName.replace(/^node:/, '')
342
+ const wireState: GraphWireState = {}
343
+ const graphWire: PikkuGraphWire = {
344
+ runId,
345
+ graphName,
346
+ nodeId,
347
+ branch: (key: string) => {
348
+ wireState.branchKey = key
349
+ },
350
+ setState: (name: string, value: unknown) =>
351
+ workflowService.updateRunState(runId, name, value),
352
+ getState: () => workflowService.getRunState(runId),
353
+ }
354
+
355
+ try {
356
+ // Execute the RPC with graph wire context
357
+ const result = await rpcService.rpcWithWire(rpcName, data, {
358
+ graph: graphWire,
359
+ })
360
+
361
+ // If branch was called, store the branch key
362
+ if (wireState.branchKey) {
363
+ await workflowService.setBranchTaken(stepId, wireState.branchKey)
364
+ }
365
+
366
+ return result
367
+ } catch (error) {
368
+ // Check if this node has an onError handler
369
+ const definition = getWorkflowGraph(graphName)
370
+ if (definition) {
371
+ const node = definition.graph[nodeId]
372
+ if (node?.onError) {
373
+ // Route to error handler nodes
374
+ const errorNodes = Array.isArray(node.onError)
375
+ ? node.onError
376
+ : [node.onError]
377
+ for (const errorNodeId of errorNodes) {
378
+ const errorNode = definition.graph[errorNodeId]
379
+ if (errorNode) {
380
+ const errorRpcName = getRpcName(errorNode)
381
+ // Queue error node with the error as input
382
+ await queueGraphNode(
383
+ workflowService,
384
+ runId,
385
+ graphName,
386
+ errorNodeId,
387
+ errorRpcName,
388
+ { error: { message: (error as Error).message } }
389
+ )
390
+ }
391
+ }
392
+ // Don't rethrow - error was handled by routing to error nodes
393
+ return
394
+ }
395
+ }
396
+ // No error handler - rethrow
397
+ throw error
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Handle node completion - re-triggers graph continuation
403
+ */
404
+ export async function onGraphNodeComplete(
405
+ workflowService: PikkuWorkflowService,
406
+ runId: string,
407
+ graphName: string
408
+ ): Promise<void> {
409
+ await continueGraph(workflowService, runId, graphName)
410
+ }
411
+
412
+ /**
413
+ * Execute a graph node inline (without queue)
414
+ */
415
+ async function executeGraphNodeInline(
416
+ workflowService: PikkuWorkflowService,
417
+ rpcService: any,
418
+ runId: string,
419
+ graphName: string,
420
+ nodeId: string,
421
+ input: any,
422
+ graph: Record<string, GraphNodeConfig>
423
+ ): Promise<void> {
424
+ const node = graph[nodeId]
425
+ if (!node) return
426
+
427
+ const rpcName = getRpcName(node)
428
+ const stepName = `node:${nodeId}`
429
+
430
+ // Insert step state
431
+ const stepState = await workflowService.insertStepState(
432
+ runId,
433
+ stepName,
434
+ rpcName,
435
+ input,
436
+ { retries: 3 }
437
+ )
438
+
439
+ await workflowService.setStepRunning(stepState.stepId)
440
+
441
+ // Execute with graph wire context
442
+ const wireState: GraphWireState = {}
443
+ const graphWire: PikkuGraphWire = {
444
+ runId,
445
+ graphName,
446
+ nodeId,
447
+ branch: (key: string) => {
448
+ wireState.branchKey = key
449
+ },
450
+ setState: (name: string, value: unknown) =>
451
+ workflowService.updateRunState(runId, name, value),
452
+ getState: () => workflowService.getRunState(runId),
453
+ }
454
+
455
+ try {
456
+ const result = await rpcService.rpcWithWire(rpcName, input, {
457
+ graph: graphWire,
458
+ })
459
+
460
+ // If branch was called, store the branch key
461
+ if (wireState.branchKey) {
462
+ await workflowService.setBranchTaken(
463
+ stepState.stepId,
464
+ wireState.branchKey
465
+ )
466
+ }
467
+
468
+ await workflowService.setStepResult(stepState.stepId, result)
469
+ } catch (error) {
470
+ await workflowService.setStepError(stepState.stepId, error as Error)
471
+
472
+ // Check if this node has an onError handler
473
+ const definition = getWorkflowGraph(graphName)
474
+ if (definition) {
475
+ const node = definition.graph[nodeId]
476
+ if (node?.onError) {
477
+ // Route to error handler nodes (inline)
478
+ const errorNodes = Array.isArray(node.onError)
479
+ ? node.onError
480
+ : [node.onError]
481
+ await Promise.all(
482
+ errorNodes.map((errorNodeId) =>
483
+ executeGraphNodeInline(
484
+ workflowService,
485
+ rpcService,
486
+ runId,
487
+ graphName,
488
+ errorNodeId,
489
+ { error: { message: (error as Error).message } },
490
+ graph
491
+ )
492
+ )
493
+ )
494
+ return
495
+ }
496
+ }
497
+ // No error handler - rethrow
498
+ throw error
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Continue graph execution inline (without queue)
504
+ * Executes nodes in parallel where possible using Promise.all
505
+ */
506
+ async function continueGraphInline(
507
+ workflowService: PikkuWorkflowService,
508
+ rpcService: any,
509
+ runId: string,
510
+ graphName: string,
511
+ graph: Record<string, GraphNodeConfig>,
512
+ triggerInput: any
513
+ ): Promise<void> {
514
+ while (true) {
515
+ // Get completed node IDs + branch keys (lightweight, no results)
516
+ const { completedNodeIds, branchKeys } =
517
+ await workflowService.getCompletedGraphState(runId)
518
+
519
+ // Find candidate next nodes from completed nodes
520
+ const candidateNodes: string[] = []
521
+
522
+ for (const nodeId of completedNodeIds) {
523
+ const node = graph[nodeId]
524
+ if (!node?.next) continue
525
+
526
+ const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId])
527
+ candidateNodes.push(...nextNodes)
528
+ }
529
+
530
+ if (candidateNodes.length === 0 && completedNodeIds.length > 0) {
531
+ // No more nodes to run - graph complete
532
+ await workflowService.updateRunStatus(runId, 'completed')
533
+ return
534
+ }
535
+
536
+ // Filter to only nodes that don't have a step yet
537
+ const nodesToExecute = await workflowService.getNodesWithoutSteps(
538
+ runId,
539
+ candidateNodes
540
+ )
541
+
542
+ if (nodesToExecute.length === 0) {
543
+ // No more nodes to execute - if we've completed at least one node, we're done
544
+ // This handles branching where only some paths are taken
545
+ if (completedNodeIds.length > 0) {
546
+ await workflowService.updateRunStatus(runId, 'completed')
547
+ }
548
+ return
549
+ }
550
+
551
+ // Execute all nodes in parallel
552
+ await Promise.all(
553
+ nodesToExecute.map(async (nodeId) => {
554
+ const node = graph[nodeId]
555
+ if (!node) return
556
+
557
+ // Evaluate input callback to get the mapping
558
+ const inputMapping = evaluateInputCallback(node)
559
+
560
+ // Only fetch results for nodes referenced in this node's input (excluding trigger)
561
+ const referencedNodeIds = extractReferencedNodeIds(inputMapping).filter(
562
+ (id) => id !== 'trigger'
563
+ )
564
+ const fetchedResults = await workflowService.getNodeResults(
565
+ runId,
566
+ referencedNodeIds
567
+ )
568
+
569
+ // Merge fetched results with trigger input
570
+ const nodeResults = { trigger: triggerInput, ...fetchedResults }
571
+
572
+ const resolvedInput = resolveInputMapping(inputMapping, nodeResults)
573
+
574
+ await executeGraphNodeInline(
575
+ workflowService,
576
+ rpcService,
577
+ runId,
578
+ graphName,
579
+ nodeId,
580
+ resolvedInput,
581
+ graph
582
+ )
583
+ })
584
+ )
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Start a workflow graph execution
590
+ * @param startNode - Optional starting node ID (from wire config). If not provided, uses wires.api from graph definition.
591
+ */
592
+ export async function runWorkflowGraph(
593
+ workflowService: PikkuWorkflowService,
594
+ graphName: string,
595
+ triggerInput: any,
596
+ rpcService?: any,
597
+ inline?: boolean,
598
+ startNode?: string
599
+ ): Promise<{ runId: string }> {
600
+ const definition = getWorkflowGraph(graphName)
601
+ if (!definition) {
602
+ throw new Error(`Workflow graph '${graphName}' not found`)
603
+ }
604
+
605
+ // Use startNode from caller if provided, otherwise use wires.api from graph definition
606
+ const apiStartNode = startNode ?? definition.wires?.api
607
+ const entryNodes: string[] = apiStartNode ? [apiStartNode] : []
608
+
609
+ if (entryNodes.length === 0) {
610
+ throw new Error(
611
+ `Workflow graph '${graphName}' has no wires.api defined and no startNode was provided`
612
+ )
613
+ }
614
+
615
+ const graph = definition.graph
616
+ const runId = await workflowService.createRun(graphName, triggerInput, inline)
617
+
618
+ // Register as inline for fast lookup
619
+ if (inline) {
620
+ workflowService.registerInlineRun(runId)
621
+ }
622
+
623
+ // Create nodeResults with trigger for resolving entry node inputs
624
+ const triggerNodeResults = { trigger: triggerInput }
625
+
626
+ try {
627
+ if (inline && rpcService) {
628
+ // Inline mode - execute entry nodes in parallel
629
+ await Promise.all(
630
+ entryNodes.map(async (nodeId) => {
631
+ const node = graph[nodeId]
632
+ if (!node) return
633
+
634
+ // Evaluate and resolve entry node input
635
+ const inputMapping = evaluateInputCallback(node)
636
+ const resolvedInput =
637
+ Object.keys(inputMapping).length > 0
638
+ ? resolveInputMapping(inputMapping, triggerNodeResults)
639
+ : triggerInput
640
+
641
+ await executeGraphNodeInline(
642
+ workflowService,
643
+ rpcService,
644
+ runId,
645
+ graphName,
646
+ nodeId,
647
+ resolvedInput,
648
+ graph
649
+ )
650
+ })
651
+ )
652
+
653
+ // Continue executing remaining nodes inline
654
+ await continueGraphInline(
655
+ workflowService,
656
+ rpcService,
657
+ runId,
658
+ graphName,
659
+ graph,
660
+ triggerInput
661
+ )
662
+ } else {
663
+ // Queue-based mode
664
+ for (const nodeId of entryNodes) {
665
+ const node = graph[nodeId]
666
+ if (!node) continue
667
+
668
+ // Evaluate and resolve entry node input
669
+ const inputMapping = evaluateInputCallback(node)
670
+ const resolvedInput =
671
+ Object.keys(inputMapping).length > 0
672
+ ? resolveInputMapping(inputMapping, triggerNodeResults)
673
+ : triggerInput
674
+
675
+ const rpcName = getRpcName(node)
676
+ await queueGraphNode(
677
+ workflowService,
678
+ runId,
679
+ graphName,
680
+ nodeId,
681
+ rpcName,
682
+ resolvedInput
683
+ )
684
+ }
685
+ }
686
+
687
+ return { runId }
688
+ } finally {
689
+ // Clean up inline tracking
690
+ if (inline) {
691
+ workflowService.unregisterInlineRun(runId)
692
+ }
693
+ }
694
+ }
@@ -0,0 +1,3 @@
1
+ export * from './workflow-graph.types.js'
2
+ export * from './graph-node.js'
3
+ export * from './graph-runner.js'