@pikku/core 0.12.1 → 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.
Files changed (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/middleware-runner.js +1 -0
  12. package/dist/permissions.js +1 -0
  13. package/dist/pikku-state.js +7 -1
  14. package/dist/schema.js +1 -1
  15. package/dist/services/gateway-service.d.ts +19 -0
  16. package/dist/services/gateway-service.js +1 -0
  17. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  18. package/dist/services/in-memory-workflow-service.js +28 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/local-gateway-service.d.ts +22 -0
  22. package/dist/services/local-gateway-service.js +51 -0
  23. package/dist/services/workflow-service.d.ts +8 -2
  24. package/dist/testing/index.d.ts +2 -0
  25. package/dist/testing/index.js +1 -0
  26. package/dist/testing/service-tests.d.ts +31 -0
  27. package/dist/testing/service-tests.js +598 -0
  28. package/dist/types/core.types.d.ts +12 -2
  29. package/dist/types/state.types.d.ts +9 -2
  30. package/dist/utils/hash.d.ts +2 -0
  31. package/dist/utils/hash.js +23 -0
  32. package/dist/utils.js +1 -1
  33. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  34. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  35. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  36. package/dist/wirings/ai-agent/ai-agent-memory.js +19 -7
  37. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +141 -48
  39. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  40. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  41. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +126 -48
  43. package/dist/wirings/ai-agent/ai-agent.types.d.ts +57 -3
  44. package/dist/wirings/ai-agent/index.d.ts +2 -3
  45. package/dist/wirings/ai-agent/index.js +1 -2
  46. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  47. package/dist/wirings/channel/channel-handler.js +16 -2
  48. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  49. package/dist/wirings/channel/channel.types.d.ts +7 -7
  50. package/dist/wirings/channel/index.d.ts +1 -1
  51. package/dist/wirings/channel/local/local-channel-handler.d.ts +8 -1
  52. package/dist/wirings/channel/local/local-channel-handler.js +22 -3
  53. package/dist/wirings/channel/local/local-channel-runner.js +28 -5
  54. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  55. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  56. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  57. package/dist/wirings/cli/cli-runner.js +13 -2
  58. package/dist/wirings/cli/cli.types.d.ts +1 -0
  59. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  60. package/dist/wirings/gateway/gateway-runner.js +325 -0
  61. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  62. package/dist/wirings/gateway/gateway.types.js +1 -0
  63. package/dist/wirings/gateway/index.d.ts +2 -0
  64. package/dist/wirings/gateway/index.js +1 -0
  65. package/dist/wirings/http/http-routes.js +0 -1
  66. package/dist/wirings/http/http-runner.d.ts +1 -1
  67. package/dist/wirings/http/http-runner.js +28 -3
  68. package/dist/wirings/http/http.types.d.ts +3 -11
  69. package/dist/wirings/http/index.d.ts +2 -1
  70. package/dist/wirings/http/index.js +1 -0
  71. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  72. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  73. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  74. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  75. package/dist/wirings/http/web-request.d.ts +12 -0
  76. package/dist/wirings/http/web-request.js +85 -0
  77. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  78. package/dist/wirings/mcp/mcp-runner.js +13 -1
  79. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  80. package/dist/wirings/rpc/rpc-runner.js +36 -14
  81. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  82. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  83. package/dist/wirings/rpc/wire-addon.js +1 -1
  84. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  85. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  86. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  87. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  88. package/dist/wirings/workflow/graph/index.js +1 -0
  89. package/dist/wirings/workflow/index.d.ts +1 -1
  90. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  91. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  92. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  93. package/package.json +6 -3
  94. package/src/crypto-utils.test.ts +99 -1
  95. package/src/crypto-utils.ts +114 -0
  96. package/src/function/function-runner.ts +5 -5
  97. package/src/function/functions.types.ts +38 -1
  98. package/src/handle-error.test.ts +65 -2
  99. package/src/handle-error.ts +8 -4
  100. package/src/index.ts +14 -0
  101. package/src/middleware-runner.ts +1 -0
  102. package/src/permissions.ts +1 -0
  103. package/src/pikku-state.ts +8 -1
  104. package/src/schema.ts +1 -1
  105. package/src/services/gateway-service.ts +20 -0
  106. package/src/services/in-memory-workflow-service.ts +45 -2
  107. package/src/services/index.ts +2 -0
  108. package/src/services/local-gateway-service.ts +62 -0
  109. package/src/services/workflow-service.ts +12 -1
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/service-tests.ts +873 -0
  112. package/src/types/core.types.ts +12 -1
  113. package/src/types/state.types.ts +12 -2
  114. package/src/utils/hash.test.ts +68 -0
  115. package/src/utils/hash.ts +26 -0
  116. package/src/utils.ts +1 -1
  117. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  118. package/src/wirings/ai-agent/ai-agent-memory.ts +17 -7
  119. package/src/wirings/ai-agent/ai-agent-prepare.ts +194 -62
  120. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  121. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  122. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  123. package/src/wirings/ai-agent/ai-agent-stream.ts +195 -91
  124. package/src/wirings/ai-agent/ai-agent.types.ts +57 -3
  125. package/src/wirings/ai-agent/index.ts +3 -5
  126. package/src/wirings/channel/channel-handler.ts +41 -4
  127. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  128. package/src/wirings/channel/channel.types.ts +12 -7
  129. package/src/wirings/channel/index.ts +1 -0
  130. package/src/wirings/channel/local/local-channel-handler.ts +31 -3
  131. package/src/wirings/channel/local/local-channel-runner.ts +32 -8
  132. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  133. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  134. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  135. package/src/wirings/cli/cli-runner.ts +15 -2
  136. package/src/wirings/cli/cli.types.ts +1 -0
  137. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  138. package/src/wirings/gateway/gateway-runner.ts +411 -0
  139. package/src/wirings/gateway/gateway.types.ts +149 -0
  140. package/src/wirings/gateway/index.ts +13 -0
  141. package/src/wirings/http/http-routes.ts +0 -1
  142. package/src/wirings/http/http-runner.ts +38 -10
  143. package/src/wirings/http/http.types.ts +3 -12
  144. package/src/wirings/http/index.ts +3 -0
  145. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  146. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  147. package/src/wirings/http/web-request.test.ts +236 -0
  148. package/src/wirings/http/web-request.ts +104 -0
  149. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  150. package/src/wirings/mcp/mcp-runner.ts +30 -12
  151. package/src/wirings/rpc/rpc-runner.ts +45 -24
  152. package/src/wirings/rpc/rpc-types.ts +6 -1
  153. package/src/wirings/rpc/wire-addon.ts +2 -1
  154. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  155. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  156. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  157. package/src/wirings/workflow/graph/index.ts +5 -0
  158. package/src/wirings/workflow/index.ts +1 -0
  159. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  160. package/src/wirings/workflow/workflow.types.ts +9 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  163. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -146
  164. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  165. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -214
@@ -117,6 +117,7 @@ export async function runScheduledTask({
117
117
  tags: task.tags,
118
118
  wire,
119
119
  sessionService: userSession,
120
+ packageName: meta.packageName,
120
121
  })
121
122
  } catch (e: any) {
122
123
  const errorResponse = getErrorResponse(e)
@@ -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
+ }
@@ -12,3 +12,8 @@ export {
12
12
  type PikkuWorkflowGraphConfig,
13
13
  type PikkuWorkflowGraphResult,
14
14
  } from './wire-workflow-graph.js'
15
+ export {
16
+ validateWorkflowWiring,
17
+ computeEntryNodeIds,
18
+ generateMermaidDiagram,
19
+ } from './graph-validation.js'
@@ -34,6 +34,7 @@ export type {
34
34
  WorkflowServiceConfig,
35
35
  WorkflowRunWire,
36
36
  WorkflowStatus,
37
+ WorkflowVersionStatus,
37
38
  StepStatus,
38
39
  WorkflowRun,
39
40
  StepState,
@@ -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
- // Check if this is a graph workflow (source === 'graph')
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 */