@pikku/core 0.12.4 → 0.12.6
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 +21 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/content-service.d.ts +2 -0
- package/dist/services/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +16 -11
- package/dist/services/workflow-service.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/http/http-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +5 -3
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +81 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +512 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +96 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1229 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +276 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
computeEntryNodeIds,
|
|
6
|
+
validateWorkflowWiring,
|
|
7
|
+
generateMermaidDiagram,
|
|
8
|
+
} from './graph-validation.js'
|
|
9
|
+
|
|
10
|
+
describe('computeEntryNodeIds', () => {
|
|
11
|
+
it('returns nodes not referenced by any other node', () => {
|
|
12
|
+
const nodes = {
|
|
13
|
+
start: { rpcName: 'a', next: 'middle' },
|
|
14
|
+
middle: { rpcName: 'b', next: 'end' },
|
|
15
|
+
end: { rpcName: 'c' },
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns multiple entry nodes for parallel starts', () => {
|
|
21
|
+
const nodes = {
|
|
22
|
+
a: { rpcName: 'x' },
|
|
23
|
+
b: { rpcName: 'y' },
|
|
24
|
+
c: { rpcName: 'z', next: 'a' },
|
|
25
|
+
}
|
|
26
|
+
const entries = computeEntryNodeIds(nodes)
|
|
27
|
+
assert.ok(entries.includes('b'))
|
|
28
|
+
assert.ok(entries.includes('c'))
|
|
29
|
+
assert.ok(!entries.includes('a'))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns empty for fully circular graph', () => {
|
|
33
|
+
const nodes = {
|
|
34
|
+
a: { rpcName: 'x', next: 'b' },
|
|
35
|
+
b: { rpcName: 'y', next: 'a' },
|
|
36
|
+
}
|
|
37
|
+
assert.deepEqual(computeEntryNodeIds(nodes), [])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('handles onError references', () => {
|
|
41
|
+
const nodes = {
|
|
42
|
+
start: { rpcName: 'a', next: 'ok', onError: 'err' },
|
|
43
|
+
ok: { rpcName: 'b' },
|
|
44
|
+
err: { rpcName: 'c' },
|
|
45
|
+
}
|
|
46
|
+
assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles branching next as object', () => {
|
|
50
|
+
const nodes = {
|
|
51
|
+
start: { rpcName: 'a', next: { yes: 'b', no: 'c' } },
|
|
52
|
+
b: { rpcName: 'x' },
|
|
53
|
+
c: { rpcName: 'y' },
|
|
54
|
+
}
|
|
55
|
+
assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('handles parallel next as array', () => {
|
|
59
|
+
const nodes = {
|
|
60
|
+
start: { rpcName: 'a', next: ['b', 'c'] },
|
|
61
|
+
b: { rpcName: 'x' },
|
|
62
|
+
c: { rpcName: 'y' },
|
|
63
|
+
}
|
|
64
|
+
assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('validateWorkflowWiring', () => {
|
|
69
|
+
const tools = ['toolA', 'toolB', 'toolC']
|
|
70
|
+
|
|
71
|
+
it('returns no errors for a valid workflow', () => {
|
|
72
|
+
const nodes = {
|
|
73
|
+
step1: { rpcName: 'toolA', input: {}, next: 'step2' },
|
|
74
|
+
step2: { rpcName: 'toolB', input: {} },
|
|
75
|
+
}
|
|
76
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
77
|
+
assert.deepEqual(errors, [])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('errors on missing rpcName', () => {
|
|
81
|
+
const nodes = {
|
|
82
|
+
step1: { input: {} },
|
|
83
|
+
}
|
|
84
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
85
|
+
assert.ok(errors.some((e) => e.includes("missing 'rpcName'")))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('errors on unknown tool', () => {
|
|
89
|
+
const nodes = {
|
|
90
|
+
step1: { rpcName: 'unknownTool', input: {} },
|
|
91
|
+
}
|
|
92
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
93
|
+
assert.ok(errors.some((e) => e.includes('unknown tool')))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('errors on next referencing unknown node', () => {
|
|
97
|
+
const nodes = {
|
|
98
|
+
step1: { rpcName: 'toolA', next: 'nonexistent' },
|
|
99
|
+
}
|
|
100
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
101
|
+
assert.ok(errors.some((e) => e.includes("unknown node 'nonexistent'")))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('errors on onError referencing unknown node', () => {
|
|
105
|
+
const nodes = {
|
|
106
|
+
step1: { rpcName: 'toolA', onError: 'missing' },
|
|
107
|
+
}
|
|
108
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
109
|
+
assert.ok(errors.some((e) => e.includes("unknown node 'missing'")))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('errors on input referencing unknown node', () => {
|
|
113
|
+
const nodes = {
|
|
114
|
+
step1: {
|
|
115
|
+
rpcName: 'toolA',
|
|
116
|
+
input: { field: { $ref: 'missing', path: 'x' } },
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
120
|
+
assert.ok(errors.some((e) => e.includes("unknown node 'missing'")))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('allows trigger refs without errors', () => {
|
|
124
|
+
const nodes = {
|
|
125
|
+
step1: {
|
|
126
|
+
rpcName: 'toolA',
|
|
127
|
+
input: { field: { $ref: 'trigger', path: 'title' } },
|
|
128
|
+
next: 'step2',
|
|
129
|
+
},
|
|
130
|
+
step2: { rpcName: 'toolB', input: {} },
|
|
131
|
+
}
|
|
132
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
133
|
+
assert.deepEqual(errors, [])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('allows valid node-to-node refs', () => {
|
|
137
|
+
const nodes = {
|
|
138
|
+
step1: { rpcName: 'toolA', input: {}, next: 'step2' },
|
|
139
|
+
step2: {
|
|
140
|
+
rpcName: 'toolB',
|
|
141
|
+
input: { data: { $ref: 'step1', path: 'result' } },
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
const errors = validateWorkflowWiring(nodes, tools)
|
|
145
|
+
assert.deepEqual(errors, [])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('generateMermaidDiagram', () => {
|
|
150
|
+
it('generates valid mermaid output', () => {
|
|
151
|
+
const nodes = {
|
|
152
|
+
start: { rpcName: 'toolA', next: 'end' },
|
|
153
|
+
end: { rpcName: 'toolB' },
|
|
154
|
+
}
|
|
155
|
+
const result = generateMermaidDiagram('test-wf', nodes, ['start'])
|
|
156
|
+
assert.ok(result.includes('graph TD'))
|
|
157
|
+
assert.ok(result.includes('start([toolA])'))
|
|
158
|
+
assert.ok(result.includes('end[toolB]'))
|
|
159
|
+
assert.ok(result.includes('start --> end'))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('shows error edges with dotted lines', () => {
|
|
163
|
+
const nodes = {
|
|
164
|
+
start: { rpcName: 'toolA', onError: 'handler' },
|
|
165
|
+
handler: { rpcName: 'toolB' },
|
|
166
|
+
}
|
|
167
|
+
const result = generateMermaidDiagram('test-wf', nodes, ['start'])
|
|
168
|
+
assert.ok(result.includes('start -.->|error| handler'))
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { pikkuState } from '../../../pikku-state.js'
|
|
2
|
+
import { resolveNamespace } from '../../rpc/rpc-runner.js'
|
|
3
|
+
|
|
4
|
+
function normalizeTargets(value: unknown): string[] {
|
|
5
|
+
if (!value) return []
|
|
6
|
+
if (typeof value === 'string') return [value]
|
|
7
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === 'string')
|
|
8
|
+
if (typeof value === 'object') {
|
|
9
|
+
const targets: string[] = []
|
|
10
|
+
for (const v of Object.values(value as Record<string, unknown>)) {
|
|
11
|
+
targets.push(...normalizeTargets(v))
|
|
12
|
+
}
|
|
13
|
+
return targets
|
|
14
|
+
}
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectRefs(value: unknown, refs: Set<string>): void {
|
|
19
|
+
if (
|
|
20
|
+
typeof value === 'object' &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
'$ref' in value &&
|
|
23
|
+
typeof (value as any).$ref === 'string'
|
|
24
|
+
) {
|
|
25
|
+
refs.add((value as any).$ref)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
for (const item of value) collectRefs(item, refs)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'object' && value !== null) {
|
|
33
|
+
for (const v of Object.values(value)) collectRefs(v, refs)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveToolMeta(toolName: string): {
|
|
38
|
+
fnMeta: any
|
|
39
|
+
schemas: Map<string, any>
|
|
40
|
+
} | null {
|
|
41
|
+
const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null
|
|
42
|
+
|
|
43
|
+
if (resolved) {
|
|
44
|
+
const fnMeta = pikkuState(resolved.package, 'function', 'meta')[
|
|
45
|
+
resolved.function
|
|
46
|
+
]
|
|
47
|
+
const schemas = pikkuState(resolved.package, 'misc', 'schemas')
|
|
48
|
+
return fnMeta ? { fnMeta, schemas } : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rpcMeta = pikkuState(null, 'rpc', 'meta')
|
|
52
|
+
const pikkuFuncId = rpcMeta[toolName]
|
|
53
|
+
if (!pikkuFuncId) return null
|
|
54
|
+
|
|
55
|
+
const fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
|
|
56
|
+
const schemas = pikkuState(null, 'misc', 'schemas')
|
|
57
|
+
return fnMeta ? { fnMeta, schemas } : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function computeEntryNodeIds(nodes: Record<string, any>): string[] {
|
|
61
|
+
const referenced = new Set<string>()
|
|
62
|
+
for (const node of Object.values(nodes)) {
|
|
63
|
+
if (node.next) {
|
|
64
|
+
for (const target of normalizeTargets(node.next)) {
|
|
65
|
+
referenced.add(target)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (node.onError) {
|
|
69
|
+
for (const target of normalizeTargets(node.onError)) {
|
|
70
|
+
referenced.add(target)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Object.keys(nodes).filter((id) => !referenced.has(id))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function validateWorkflowWiring(
|
|
78
|
+
nodes: Record<string, any>,
|
|
79
|
+
toolNames: string[]
|
|
80
|
+
): string[] {
|
|
81
|
+
const errors: string[] = []
|
|
82
|
+
const nodeIds = new Set(Object.keys(nodes))
|
|
83
|
+
const toolSet = new Set(toolNames)
|
|
84
|
+
|
|
85
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
86
|
+
if (!node.rpcName) {
|
|
87
|
+
errors.push(`Node '${nodeId}' is missing 'rpcName'`)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!toolSet.has(node.rpcName)) {
|
|
92
|
+
errors.push(
|
|
93
|
+
`Node '${nodeId}' references unknown tool '${node.rpcName}'. Available tools: ${toolNames.join(', ')}`
|
|
94
|
+
)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nextTargets = normalizeTargets(node.next)
|
|
99
|
+
for (const target of nextTargets) {
|
|
100
|
+
if (!nodeIds.has(target)) {
|
|
101
|
+
errors.push(
|
|
102
|
+
`Node '${nodeId}' routes to unknown node '${target}' in 'next'`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const errorTargets = normalizeTargets(node.onError)
|
|
108
|
+
for (const target of errorTargets) {
|
|
109
|
+
if (!nodeIds.has(target)) {
|
|
110
|
+
errors.push(
|
|
111
|
+
`Node '${nodeId}' routes to unknown node '${target}' in 'onError'`
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!node.input) continue
|
|
117
|
+
|
|
118
|
+
const refs = new Set<string>()
|
|
119
|
+
collectRefs(node.input, refs)
|
|
120
|
+
|
|
121
|
+
for (const ref of refs) {
|
|
122
|
+
if (ref === 'trigger' || ref === '$item') {
|
|
123
|
+
const targetMeta = resolveToolMeta(node.rpcName)
|
|
124
|
+
if (targetMeta?.fnMeta?.inputSchemaName) {
|
|
125
|
+
const targetSchema = targetMeta.schemas.get(
|
|
126
|
+
targetMeta.fnMeta.inputSchemaName
|
|
127
|
+
)
|
|
128
|
+
if (targetSchema?.properties) {
|
|
129
|
+
for (const [field, fieldValue] of Object.entries(
|
|
130
|
+
node.input as Record<string, any>
|
|
131
|
+
)) {
|
|
132
|
+
if (
|
|
133
|
+
typeof fieldValue === 'object' &&
|
|
134
|
+
fieldValue !== null &&
|
|
135
|
+
fieldValue.$ref === ref &&
|
|
136
|
+
!fieldValue.path
|
|
137
|
+
) {
|
|
138
|
+
const targetType = targetSchema.properties[field]?.type
|
|
139
|
+
if (targetType && targetType !== 'object') {
|
|
140
|
+
errors.push(
|
|
141
|
+
`Node '${nodeId}' input field '${field}' expects type '${targetType}', but references the whole ${ref} object without a path. Use { $ref: "${ref}", path: "${field}" } to extract the field.`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!nodeIds.has(ref)) {
|
|
152
|
+
errors.push(
|
|
153
|
+
`Node '${nodeId}' references unknown node '${ref}' in input`
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sourceNode = nodes[ref]
|
|
159
|
+
if (!sourceNode?.rpcName) continue
|
|
160
|
+
|
|
161
|
+
const sourceMeta = resolveToolMeta(sourceNode.rpcName)
|
|
162
|
+
if (!sourceMeta?.fnMeta?.outputSchemaName) continue
|
|
163
|
+
|
|
164
|
+
const sourceSchema = sourceMeta.schemas.get(
|
|
165
|
+
sourceMeta.fnMeta.outputSchemaName
|
|
166
|
+
)
|
|
167
|
+
if (!sourceSchema?.properties) continue
|
|
168
|
+
|
|
169
|
+
for (const [field, fieldValue] of Object.entries(
|
|
170
|
+
node.input as Record<string, any>
|
|
171
|
+
)) {
|
|
172
|
+
if (
|
|
173
|
+
typeof fieldValue === 'object' &&
|
|
174
|
+
fieldValue !== null &&
|
|
175
|
+
fieldValue.$ref === ref &&
|
|
176
|
+
fieldValue.path
|
|
177
|
+
) {
|
|
178
|
+
const pathRoot = fieldValue.path.split('.')[0]
|
|
179
|
+
if (
|
|
180
|
+
sourceSchema.properties &&
|
|
181
|
+
!(pathRoot in sourceSchema.properties)
|
|
182
|
+
) {
|
|
183
|
+
errors.push(
|
|
184
|
+
`Node '${nodeId}' input field '${field}' references path '${fieldValue.path}' but node '${ref}' (${sourceNode.rpcName}) output has no property '${pathRoot}'`
|
|
185
|
+
)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetMeta = resolveToolMeta(node.rpcName)
|
|
190
|
+
if (!targetMeta?.fnMeta?.inputSchemaName) continue
|
|
191
|
+
const targetSchema = targetMeta.schemas.get(
|
|
192
|
+
targetMeta.fnMeta.inputSchemaName
|
|
193
|
+
)
|
|
194
|
+
if (!targetSchema?.properties?.[field]) continue
|
|
195
|
+
|
|
196
|
+
const sourceType = sourceSchema.properties[pathRoot]?.type
|
|
197
|
+
const targetType = targetSchema.properties[field].type
|
|
198
|
+
if (sourceType && targetType && sourceType !== targetType) {
|
|
199
|
+
errors.push(
|
|
200
|
+
`Node '${nodeId}' input field '${field}' expects type '${targetType}', but node '${ref}' output field '${pathRoot}' is type '${sourceType}'`
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return errors
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function generateMermaidDiagram(
|
|
212
|
+
workflowName: string,
|
|
213
|
+
nodes: Record<string, any>,
|
|
214
|
+
entryNodeIds: string[]
|
|
215
|
+
): string {
|
|
216
|
+
const lines: string[] = ['graph TD']
|
|
217
|
+
|
|
218
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
219
|
+
const label = node.rpcName || nodeId
|
|
220
|
+
const isEntry = entryNodeIds.includes(nodeId)
|
|
221
|
+
lines.push(
|
|
222
|
+
` ${nodeId}${isEntry ? '([' : '['}${label}${isEntry ? '])' : ']'}`
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const nextTargets = normalizeTargets(node.next)
|
|
226
|
+
for (const target of nextTargets) {
|
|
227
|
+
lines.push(` ${nodeId} --> ${target}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const errorTargets = normalizeTargets(node.onError)
|
|
231
|
+
for (const target of errorTargets) {
|
|
232
|
+
lines.push(` ${nodeId} -.->|error| ${target}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return lines.join('\n')
|
|
237
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
continueGraph,
|
|
3
|
+
executeGraphStep,
|
|
4
|
+
onGraphNodeComplete,
|
|
5
|
+
runFromMeta,
|
|
6
|
+
runWorkflowGraph,
|
|
7
|
+
} from './graph-runner.js'
|
|
8
|
+
export { template } from './template.js'
|
|
9
|
+
export type { TemplateString } from './template.js'
|
|
10
|
+
export {
|
|
11
|
+
pikkuWorkflowGraph,
|
|
12
|
+
type PikkuWorkflowGraphConfig,
|
|
13
|
+
type PikkuWorkflowGraphResult,
|
|
14
|
+
} from './wire-workflow-graph.js'
|
|
15
|
+
export {
|
|
16
|
+
validateWorkflowWiring,
|
|
17
|
+
computeEntryNodeIds,
|
|
18
|
+
generateMermaidDiagram,
|
|
19
|
+
} from './graph-validation.js'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { template } from './template.js'
|
|
4
|
+
|
|
5
|
+
describe('template', () => {
|
|
6
|
+
test('should parse a simple template with one placeholder', () => {
|
|
7
|
+
const result = template('Hello $0!', [{ $ref: 'greeting' }])
|
|
8
|
+
assert.deepStrictEqual(result.$template.parts, ['Hello ', '!'])
|
|
9
|
+
assert.deepStrictEqual(result.$template.expressions, [{ $ref: 'greeting' }])
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('should parse template with multiple placeholders', () => {
|
|
13
|
+
const result = template('$0 and $1', [{ $ref: 'a' }, { $ref: 'b' }])
|
|
14
|
+
assert.deepStrictEqual(result.$template.parts, ['', ' and ', ''])
|
|
15
|
+
assert.strictEqual(result.$template.expressions.length, 2)
|
|
16
|
+
assert.strictEqual(result.$template.expressions[0].$ref, 'a')
|
|
17
|
+
assert.strictEqual(result.$template.expressions[1].$ref, 'b')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('should preserve path in references', () => {
|
|
21
|
+
const result = template('Value: $0', [{ $ref: 'node1', path: 'data.name' }])
|
|
22
|
+
assert.deepStrictEqual(result.$template.expressions[0], {
|
|
23
|
+
$ref: 'node1',
|
|
24
|
+
path: 'data.name',
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('should handle template with no placeholders', () => {
|
|
29
|
+
const result = template('No placeholders here', [])
|
|
30
|
+
assert.deepStrictEqual(result.$template.parts, ['No placeholders here'])
|
|
31
|
+
assert.deepStrictEqual(result.$template.expressions, [])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should handle out-of-range ref index as unknown', () => {
|
|
35
|
+
const result = template('$5', [{ $ref: 'a' }])
|
|
36
|
+
assert.deepStrictEqual(result.$template.expressions[0], { $ref: 'unknown' })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('should handle adjacent placeholders', () => {
|
|
40
|
+
const result = template('$0$1', [{ $ref: 'a' }, { $ref: 'b' }])
|
|
41
|
+
assert.deepStrictEqual(result.$template.parts, ['', '', ''])
|
|
42
|
+
assert.strictEqual(result.$template.expressions.length, 2)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('should omit path when not provided', () => {
|
|
46
|
+
const result = template('$0', [{ $ref: 'node' }])
|
|
47
|
+
assert.strictEqual(result.$template.expressions[0].path, undefined)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TemplateString = {
|
|
2
|
+
$template: {
|
|
3
|
+
parts: string[]
|
|
4
|
+
expressions: Array<{ $ref: string; path?: string }>
|
|
5
|
+
}
|
|
6
|
+
} & { __brand: 'TemplateString' }
|
|
7
|
+
|
|
8
|
+
export function template(
|
|
9
|
+
templateStr: string,
|
|
10
|
+
refs: Array<{ $ref: string; path?: string }>
|
|
11
|
+
): TemplateString {
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
const expressions: Array<{ $ref: string; path?: string }> = []
|
|
14
|
+
|
|
15
|
+
const regex = /\$(\d+)/g
|
|
16
|
+
let lastIndex = 0
|
|
17
|
+
let match
|
|
18
|
+
|
|
19
|
+
while ((match = regex.exec(templateStr)) !== null) {
|
|
20
|
+
parts.push(templateStr.slice(lastIndex, match.index))
|
|
21
|
+
|
|
22
|
+
const refIndex = parseInt(match[1]!, 10)
|
|
23
|
+
const refValue = refs[refIndex]
|
|
24
|
+
if (refValue) {
|
|
25
|
+
const expr: { $ref: string; path?: string } = { $ref: refValue.$ref }
|
|
26
|
+
if (refValue.path) {
|
|
27
|
+
expr.path = refValue.path
|
|
28
|
+
}
|
|
29
|
+
expressions.push(expr)
|
|
30
|
+
} else {
|
|
31
|
+
expressions.push({ $ref: 'unknown' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lastIndex = regex.lastIndex
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parts.push(templateStr.slice(lastIndex))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
$template: { parts, expressions },
|
|
41
|
+
} as TemplateString
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PikkuWorkflowGraphConfig<
|
|
2
|
+
FuncMap extends Record<string, string>,
|
|
3
|
+
T,
|
|
4
|
+
> {
|
|
5
|
+
disabled?: true
|
|
6
|
+
name?: string
|
|
7
|
+
description?: string
|
|
8
|
+
tags?: string[]
|
|
9
|
+
nodes: FuncMap
|
|
10
|
+
config?: T
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PikkuWorkflowGraphResult {
|
|
14
|
+
__type: 'pikkuWorkflowGraph'
|
|
15
|
+
name?: string
|
|
16
|
+
description?: string
|
|
17
|
+
tags?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pikkuWorkflowGraph<
|
|
21
|
+
const FuncMap extends Record<string, string>,
|
|
22
|
+
>(config: PikkuWorkflowGraphConfig<FuncMap, any>): PikkuWorkflowGraphResult {
|
|
23
|
+
return {
|
|
24
|
+
__type: 'pikkuWorkflowGraph',
|
|
25
|
+
name: config.name,
|
|
26
|
+
description: config.description,
|
|
27
|
+
tags: config.tags,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ref value - internal representation after input callback is evaluated
|
|
3
|
+
*/
|
|
4
|
+
export interface RefValue {
|
|
5
|
+
__isRef: true
|
|
6
|
+
nodeId: string
|
|
7
|
+
path?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to create a ref value
|
|
12
|
+
*/
|
|
13
|
+
export const createRef = (nodeId: string, path?: string): RefValue => ({
|
|
14
|
+
__isRef: true,
|
|
15
|
+
nodeId,
|
|
16
|
+
path,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a value is a ref
|
|
21
|
+
*/
|
|
22
|
+
export const isRef = (value: unknown): value is RefValue =>
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
value !== null &&
|
|
25
|
+
'__isRef' in value &&
|
|
26
|
+
(value as RefValue).__isRef === true
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Input reference for fetching step results
|
|
30
|
+
*/
|
|
31
|
+
export interface InputRef {
|
|
32
|
+
nodeId: string
|
|
33
|
+
path?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ref function type - provides type-safe references to other nodes' outputs
|
|
38
|
+
* The actual typing is done at the pikkuWorkflowGraph level
|
|
39
|
+
*/
|
|
40
|
+
export type RefFn<NodeIds extends string = string> = (
|
|
41
|
+
nodeId: NodeIds,
|
|
42
|
+
path?: string
|
|
43
|
+
) => RefValue
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Next node configuration - fully serializable
|
|
47
|
+
* - string: single next node
|
|
48
|
+
* - string[]: parallel execution (all run concurrently)
|
|
49
|
+
* - Record<string, string | string[]>: branching (function calls graph.branch('key'))
|
|
50
|
+
*/
|
|
51
|
+
export type NextConfig<NodeIds extends string = string> =
|
|
52
|
+
| NodeIds
|
|
53
|
+
| NodeIds[]
|
|
54
|
+
| Record<string, NodeIds | NodeIds[]>
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Graph node configuration - references functions by RPC name
|
|
58
|
+
*/
|
|
59
|
+
export interface GraphNodeConfig<NodeIds extends string = string> {
|
|
60
|
+
/** RPC function name */
|
|
61
|
+
func: string
|
|
62
|
+
/** Input mapping callback - receives ref function, returns input object */
|
|
63
|
+
input?: (ref: RefFn<NodeIds>) => Record<string, unknown>
|
|
64
|
+
/** Next nodes - string, array, or record for branching */
|
|
65
|
+
next?: NextConfig<NodeIds>
|
|
66
|
+
/** Error routing - node(s) to execute on error */
|
|
67
|
+
onError?: NodeIds | NodeIds[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Graph wire context - available to functions running in a workflow graph
|
|
72
|
+
*/
|
|
73
|
+
export interface PikkuGraphWire {
|
|
74
|
+
/** Workflow run ID */
|
|
75
|
+
runId: string
|
|
76
|
+
/** Graph name */
|
|
77
|
+
graphName: string
|
|
78
|
+
/** Current node ID */
|
|
79
|
+
nodeId: string
|
|
80
|
+
/**
|
|
81
|
+
* Select which branch to take for Record-based next config.
|
|
82
|
+
* Must be called if the node has a Record `next` configuration.
|
|
83
|
+
* @param key - The branch key to take (must match a key in the next Record)
|
|
84
|
+
*/
|
|
85
|
+
branch: (key: string) => void
|
|
86
|
+
/**
|
|
87
|
+
* Set a state variable value.
|
|
88
|
+
* @param name - Variable name
|
|
89
|
+
* @param value - Value to store
|
|
90
|
+
*/
|
|
91
|
+
setState: (name: string, value: unknown) => Promise<void>
|
|
92
|
+
/**
|
|
93
|
+
* Get the entire state object.
|
|
94
|
+
* @returns The state object with all variables
|
|
95
|
+
*/
|
|
96
|
+
getState: () => Promise<Record<string, unknown>>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Internal mutable state for graph wire (used by runner to capture branch)
|
|
101
|
+
*/
|
|
102
|
+
export interface GraphWireState {
|
|
103
|
+
branchKey?: string
|
|
104
|
+
}
|