@pikku/core 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/errors/error-handler.js +3 -3
- package/dist/function/function-runner.d.ts +4 -3
- package/dist/function/function-runner.js +60 -14
- package/dist/function/functions.types.d.ts +84 -5
- package/dist/function/functions.types.js +41 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/middleware/auth-apikey.d.ts +1 -1
- package/dist/middleware/auth-bearer.d.ts +1 -1
- package/dist/middleware/auth-cookie.d.ts +1 -1
- package/dist/middleware/timeout.d.ts +1 -1
- package/dist/middleware-runner.d.ts +3 -14
- package/dist/middleware-runner.js +8 -8
- package/dist/permissions.d.ts +3 -86
- package/dist/permissions.js +11 -67
- package/dist/pikku-state.d.ts +32 -104
- package/dist/pikku-state.js +140 -66
- package/dist/schema.d.ts +8 -6
- package/dist/schema.js +25 -13
- package/dist/services/workflow-service.d.ts +38 -0
- package/dist/services/workflow-service.js +1 -0
- package/dist/types/core.types.d.ts +12 -4
- package/dist/types/core.types.js +1 -1
- package/dist/types/state.types.d.ts +135 -0
- package/dist/types/state.types.js +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +45 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-runner.js +5 -5
- package/dist/wirings/channel/log-channels.js +1 -1
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +2 -2
- package/dist/wirings/cli/channel/cli-channel-runner.js +3 -2
- package/dist/wirings/cli/cli-runner.js +10 -10
- package/dist/wirings/cli/cli.types.d.ts +1 -0
- package/dist/wirings/forge-node/forge-node.types.d.ts +120 -0
- package/dist/wirings/forge-node/forge-node.types.js +38 -0
- package/dist/wirings/forge-node/index.d.ts +1 -0
- package/dist/wirings/forge-node/index.js +1 -0
- package/dist/wirings/http/http-runner.d.ts +2 -2
- package/dist/wirings/http/http-runner.js +21 -12
- package/dist/wirings/http/http.types.d.ts +14 -1
- package/dist/wirings/http/log-http-routes.js +1 -1
- package/dist/wirings/http/routers/path-to-regex.js +2 -2
- package/dist/wirings/mcp/mcp-runner.js +21 -21
- package/dist/wirings/queue/queue-runner.js +6 -6
- package/dist/wirings/rpc/index.d.ts +1 -1
- package/dist/wirings/rpc/index.js +1 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +7 -8
- package/dist/wirings/rpc/rpc-runner.js +62 -14
- package/dist/wirings/rpc/rpc-types.d.ts +7 -0
- package/dist/wirings/scheduler/log-schedulers.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +5 -5
- package/dist/wirings/trigger/index.d.ts +2 -0
- package/dist/wirings/trigger/index.js +2 -0
- package/dist/wirings/trigger/trigger-runner.d.ts +29 -0
- package/dist/wirings/trigger/trigger-runner.js +57 -0
- package/dist/wirings/trigger/trigger.types.d.ts +42 -0
- package/dist/wirings/trigger/trigger.types.js +1 -0
- package/dist/wirings/workflow/dsl/index.d.ts +5 -0
- package/dist/wirings/workflow/dsl/index.js +4 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +286 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.js +5 -0
- package/dist/wirings/workflow/dsl/workflow-runner.d.ts +5 -0
- package/dist/wirings/workflow/dsl/workflow-runner.js +22 -0
- package/dist/wirings/workflow/graph/graph-node.d.ts +122 -0
- package/dist/wirings/workflow/graph/graph-node.js +58 -0
- package/dist/wirings/workflow/graph/graph-runner.d.ts +35 -0
- package/dist/wirings/workflow/graph/graph-runner.js +452 -0
- package/dist/wirings/workflow/graph/index.d.ts +3 -0
- package/dist/wirings/workflow/graph/index.js +3 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +95 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.js +15 -0
- package/dist/wirings/workflow/index.d.ts +7 -12
- package/dist/wirings/workflow/index.js +6 -11
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +96 -6
- package/dist/wirings/workflow/pikku-workflow-service.js +230 -103
- package/dist/wirings/workflow/wire-workflow.d.ts +42 -0
- package/dist/wirings/workflow/wire-workflow.js +53 -0
- package/dist/wirings/workflow/workflow-utils.d.ts +23 -0
- package/dist/wirings/workflow/workflow-utils.js +66 -0
- package/dist/wirings/workflow/workflow.types.d.ts +133 -211
- package/package.json +10 -1
- package/src/errors/error-handler.ts +3 -3
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +86 -16
- package/src/function/functions.types.ts +126 -4
- package/src/index.ts +10 -5
- package/src/middleware-runner.ts +16 -9
- package/src/permissions.test.ts +28 -86
- package/src/permissions.ts +21 -80
- package/src/pikku-state.ts +156 -201
- package/src/schema.ts +42 -13
- package/src/services/workflow-service.ts +74 -0
- package/src/types/core.types.ts +12 -3
- package/src/types/state.types.ts +195 -0
- package/src/utils.ts +55 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-runner.ts +5 -5
- package/src/wirings/channel/local/local-channel-runner.test.ts +3 -1
- package/src/wirings/channel/log-channels.ts +1 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +2 -2
- package/src/wirings/cli/channel/cli-channel-runner.ts +4 -3
- package/src/wirings/cli/cli-runner.test.ts +19 -19
- package/src/wirings/cli/cli-runner.ts +10 -9
- package/src/wirings/cli/cli.types.ts +1 -0
- package/src/wirings/forge-node/forge-node.types.ts +135 -0
- package/src/wirings/forge-node/index.ts +1 -0
- package/src/wirings/http/http-runner.test.ts +2 -2
- package/src/wirings/http/http-runner.ts +30 -13
- package/src/wirings/http/http.types.ts +14 -0
- package/src/wirings/http/log-http-routes.ts +1 -1
- package/src/wirings/http/routers/path-to-regex.test.ts +30 -19
- package/src/wirings/http/routers/path-to-regex.ts +2 -2
- package/src/wirings/mcp/mcp-runner.ts +21 -21
- package/src/wirings/queue/queue-runner.test.ts +19 -19
- package/src/wirings/queue/queue-runner.ts +6 -6
- package/src/wirings/rpc/index.ts +1 -1
- package/src/wirings/rpc/rpc-runner.ts +88 -22
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/scheduler/log-schedulers.ts +1 -1
- package/src/wirings/scheduler/scheduler-runner.test.ts +27 -27
- package/src/wirings/scheduler/scheduler-runner.ts +5 -5
- package/src/wirings/trigger/index.ts +2 -0
- package/src/wirings/trigger/trigger-runner.ts +96 -0
- package/src/wirings/trigger/trigger.types.ts +56 -0
- package/src/wirings/workflow/dsl/index.ts +30 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +312 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +27 -0
- package/src/wirings/workflow/graph/graph-node.ts +227 -0
- package/src/wirings/workflow/graph/graph-runner.ts +694 -0
- package/src/wirings/workflow/graph/index.ts +3 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +126 -0
- package/src/wirings/workflow/index.ts +53 -26
- package/src/wirings/workflow/pikku-workflow-service.ts +333 -117
- package/src/wirings/workflow/wire-workflow.ts +94 -0
- package/src/wirings/workflow/workflow-utils.ts +120 -0
- package/src/wirings/workflow/workflow.types.ts +166 -277
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/workflow/workflow-runner.d.ts +0 -34
- package/dist/wirings/workflow/workflow-runner.js +0 -65
- package/src/function/function-runner.ts.bak +0 -188
- package/src/wirings/workflow/workflow-runner.ts +0 -72
|
@@ -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
|
+
}
|