@pikku/core 0.12.2 → 0.12.3
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 +18 -0
- package/dist/crypto-utils.d.ts +10 -0
- package/dist/crypto-utils.js +62 -0
- package/dist/function/function-runner.js +4 -4
- package/dist/function/functions.types.d.ts +26 -1
- package/dist/function/functions.types.js +16 -0
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +7 -3
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/pikku-state.js +3 -1
- package/dist/schema.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +8 -2
- package/dist/services/in-memory-workflow-service.js +28 -1
- package/dist/services/workflow-service.d.ts +8 -2
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/service-tests.d.ts +31 -0
- package/dist/testing/service-tests.js +598 -0
- package/dist/types/core.types.d.ts +9 -1
- package/dist/types/state.types.d.ts +4 -2
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
- package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
- package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -2
- package/dist/wirings/ai-agent/index.js +1 -2
- package/dist/wirings/channel/channel-handler.js +5 -1
- package/dist/wirings/channel/channel.types.d.ts +1 -7
- package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -3
- package/dist/wirings/channel/local/local-channel-runner.js +14 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
- package/dist/wirings/cli/cli-runner.js +10 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -0
- package/dist/wirings/http/http-routes.js +0 -1
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +25 -3
- package/dist/wirings/http/http.types.d.ts +3 -11
- package/dist/wirings/http/index.d.ts +2 -1
- package/dist/wirings/http/index.js +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
- package/dist/wirings/http/web-request.d.ts +12 -0
- package/dist/wirings/http/web-request.js +85 -0
- package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +13 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
- package/dist/wirings/rpc/rpc-runner.js +36 -14
- package/dist/wirings/rpc/rpc-types.d.ts +6 -1
- package/dist/wirings/rpc/wire-addon.d.ts +1 -0
- package/dist/wirings/rpc/wire-addon.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -0
- package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
- package/dist/wirings/workflow/graph/graph-validation.js +175 -0
- package/dist/wirings/workflow/graph/index.d.ts +1 -0
- package/dist/wirings/workflow/graph/index.js +1 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
- package/dist/wirings/workflow/workflow.types.d.ts +10 -1
- package/package.json +4 -2
- package/src/crypto-utils.test.ts +99 -1
- package/src/crypto-utils.ts +114 -0
- package/src/function/function-runner.ts +5 -5
- package/src/function/functions.types.ts +38 -1
- package/src/handle-error.test.ts +65 -2
- package/src/handle-error.ts +8 -4
- package/src/index.ts +14 -0
- package/src/pikku-state.ts +3 -1
- package/src/schema.ts +1 -1
- package/src/services/in-memory-workflow-service.ts +45 -2
- package/src/services/workflow-service.ts +12 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/types/core.types.ts +9 -1
- package/src/types/state.types.ts +4 -2
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +7 -6
- package/src/wirings/ai-agent/ai-agent-prepare.ts +175 -61
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +182 -89
- package/src/wirings/ai-agent/ai-agent.types.ts +23 -1
- package/src/wirings/ai-agent/index.ts +1 -5
- package/src/wirings/channel/channel-handler.ts +11 -2
- package/src/wirings/channel/channel.types.ts +1 -7
- package/src/wirings/channel/local/local-channel-handler.ts +5 -3
- package/src/wirings/channel/local/local-channel-runner.ts +17 -7
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +12 -2
- package/src/wirings/cli/cli.types.ts +1 -0
- package/src/wirings/http/http-routes.ts +0 -1
- package/src/wirings/http/http-runner.ts +35 -10
- package/src/wirings/http/http.types.ts +3 -12
- package/src/wirings/http/index.ts +3 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +30 -12
- package/src/wirings/rpc/rpc-runner.ts +45 -24
- package/src/wirings/rpc/rpc-types.ts +6 -1
- package/src/wirings/rpc/wire-addon.ts +2 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -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 +5 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
- package/src/wirings/workflow/workflow.types.ts +9 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
- package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
- package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
WorkflowRun,
|
|
14
14
|
WorkflowRunWire,
|
|
15
15
|
WorkflowStatus,
|
|
16
|
+
WorkflowVersionStatus,
|
|
16
17
|
WorkflowServiceConfig,
|
|
17
18
|
WorkflowStepOptions,
|
|
18
19
|
} from './workflow.types.js'
|
|
@@ -339,7 +340,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
339
340
|
name: string,
|
|
340
341
|
graphHash: string,
|
|
341
342
|
graph: any,
|
|
342
|
-
source: string
|
|
343
|
+
source: string,
|
|
344
|
+
status?: WorkflowVersionStatus
|
|
345
|
+
): Promise<void>
|
|
346
|
+
|
|
347
|
+
abstract updateWorkflowVersionStatus(
|
|
348
|
+
name: string,
|
|
349
|
+
graphHash: string,
|
|
350
|
+
status: WorkflowVersionStatus
|
|
343
351
|
): Promise<void>
|
|
344
352
|
|
|
345
353
|
abstract getWorkflowVersion(
|
|
@@ -347,6 +355,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
347
355
|
graphHash: string
|
|
348
356
|
): Promise<{ graph: any; source: string } | null>
|
|
349
357
|
|
|
358
|
+
abstract getAIGeneratedWorkflows(
|
|
359
|
+
agentName?: string
|
|
360
|
+
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
|
|
361
|
+
|
|
350
362
|
// ============================================================================
|
|
351
363
|
// Workflow Lifecycle Methods
|
|
352
364
|
// ============================================================================
|
|
@@ -426,8 +438,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
426
438
|
throw new WorkflowNotFoundError(name)
|
|
427
439
|
}
|
|
428
440
|
|
|
429
|
-
|
|
430
|
-
if (workflowMeta.source === 'graph') {
|
|
441
|
+
if (workflowMeta.source === 'graph' || workflowMeta.source === 'ai-agent') {
|
|
431
442
|
const shouldInline =
|
|
432
443
|
options?.inline || !getSingletonServices()?.queueService
|
|
433
444
|
return runWorkflowGraph(
|
|
@@ -60,6 +60,11 @@ export type WorkflowStatus =
|
|
|
60
60
|
| 'failed'
|
|
61
61
|
| 'cancelled'
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Workflow version status (for AI-generated workflows)
|
|
65
|
+
*/
|
|
66
|
+
export type WorkflowVersionStatus = 'draft' | 'active' | 'declined'
|
|
67
|
+
|
|
63
68
|
/**
|
|
64
69
|
* Workflow step status
|
|
65
70
|
*/
|
|
@@ -150,6 +155,9 @@ export interface WorkflowRunService {
|
|
|
150
155
|
name: string,
|
|
151
156
|
graphHash: string
|
|
152
157
|
): Promise<{ graph: any; source: string } | null>
|
|
158
|
+
getAIGeneratedWorkflows(
|
|
159
|
+
agentName?: string
|
|
160
|
+
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
|
|
153
161
|
deleteRun(id: string): Promise<boolean>
|
|
154
162
|
}
|
|
155
163
|
|
|
@@ -228,7 +236,7 @@ export interface WorkflowRuntimeMeta {
|
|
|
228
236
|
/** Pikku function name (for execution) */
|
|
229
237
|
pikkuFuncId: string
|
|
230
238
|
/** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph' */
|
|
231
|
-
source: 'dsl' | 'complex' | 'graph'
|
|
239
|
+
source: 'dsl' | 'complex' | 'graph' | 'ai-agent'
|
|
232
240
|
/** Optional description */
|
|
233
241
|
description?: string
|
|
234
242
|
/** Tags for organization */
|