@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
@@ -1,7 +1,7 @@
1
1
  import { runPikkuFunc } from '../../function/function-runner.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { ForbiddenError } from '../../errors/errors.js';
4
- import { PikkuError } from '../../errors/error-handler.js';
4
+ import { PikkuError, addError } from '../../errors/error-handler.js';
5
5
  import { parseVersionedId } from '../../version.js';
6
6
  import { encryptJSON } from '../../crypto-utils.js';
7
7
  export class RPCNotFoundError extends PikkuError {
@@ -11,29 +11,33 @@ export class RPCNotFoundError extends PikkuError {
11
11
  this.rpcName = rpcName;
12
12
  }
13
13
  }
14
- import { runAIAgent } from '../ai-agent/ai-agent-runner.js';
14
+ addError(RPCNotFoundError, {
15
+ status: 404,
16
+ mcpCode: -32601,
17
+ message: 'RPC function not found.',
18
+ });
19
+ import { runAIAgent, resumeAIAgentSync } from '../ai-agent/ai-agent-runner.js';
15
20
  import { streamAIAgent, resumeAIAgent } from '../ai-agent/ai-agent-stream.js';
16
- import { approveAIAgent } from '../ai-agent/ai-agent-registry.js';
17
21
  /**
18
22
  * Resolve a namespaced function reference to package and function names
19
23
  * Uses pikkuState to look up the namespace -> package mapping
20
24
  */
21
- const resolveNamespace = (namespacedFunction) => {
25
+ export const resolveNamespace = (namespacedFunction) => {
22
26
  const colonIndex = namespacedFunction.indexOf(':');
23
27
  if (colonIndex === -1) {
24
28
  return null;
25
29
  }
26
30
  const namespace = namespacedFunction.substring(0, colonIndex);
27
31
  const functionName = namespacedFunction.substring(colonIndex + 1);
28
- const addons = pikkuState(null, 'rpc', 'addons');
32
+ const addons = pikkuState(null, 'addons', 'packages');
29
33
  const pkgConfig = addons.get(namespace);
30
34
  if (!pkgConfig) {
31
35
  return null;
32
36
  }
33
37
  return {
34
- namespace,
35
38
  package: pkgConfig.package,
36
39
  function: functionName,
40
+ addonConfig: pkgConfig,
37
41
  };
38
42
  };
39
43
  const getPikkuFunctionName = (rpcName) => {
@@ -72,7 +76,7 @@ export class ContextAwareRPCService {
72
76
  functionMeta = pikkuState(null, 'function', 'meta')[funcName];
73
77
  }
74
78
  if (!functionMeta) {
75
- throw new Error(`Function not found: ${funcName}`);
79
+ throw new RPCNotFoundError(funcName);
76
80
  }
77
81
  if (!functionMeta.expose) {
78
82
  throw new ForbiddenError();
@@ -125,9 +129,11 @@ export class ContextAwareRPCService {
125
129
  `Available functions: ${Object.keys(addonFunctionMeta).join(', ') || '(none)'}`);
126
130
  }
127
131
  const funcName = funcMeta.pikkuFuncId || resolved.function;
128
- const addonConfig = pikkuState(null, 'rpc', 'addons').get(resolved.namespace);
129
- const auth = addonConfig?.auth ?? this.options.requiresAuth;
130
- const tags = [...(addonConfig?.tags ?? []), ...(funcMeta.tags ?? [])];
132
+ const auth = resolved.addonConfig?.auth ?? this.options.requiresAuth;
133
+ const tags = [
134
+ ...(resolved.addonConfig?.tags ?? []),
135
+ ...(funcMeta.tags ?? []),
136
+ ];
131
137
  // Execute the function using runPikkuFunc with the addon package's state
132
138
  // We use the parent services (this.services) since addon packages share services
133
139
  // Pass the function's tags so tag-based middleware/permissions are applied
@@ -153,6 +159,9 @@ export class ContextAwareRPCService {
153
159
  }
154
160
  : undefined,
155
161
  };
162
+ if (rpcName.includes(':')) {
163
+ return this.invokeAddonFunction(rpcName, data, mergedWire);
164
+ }
156
165
  return runPikkuFunc('rpc', rpcName, getPikkuFunctionName(rpcName), {
157
166
  auth: this.options.requiresAuth,
158
167
  singletonServices: this.services,
@@ -182,6 +191,10 @@ export class ContextAwareRPCService {
182
191
  runId: result.runId,
183
192
  result: result.object ?? result.text,
184
193
  usage: result.usage,
194
+ ...(result.status === 'suspended' && {
195
+ status: 'suspended',
196
+ pendingApprovals: result.pendingApprovals,
197
+ }),
185
198
  };
186
199
  },
187
200
  stream: async (agentName, input, options) => {
@@ -197,9 +210,18 @@ export class ContextAwareRPCService {
197
210
  await resumeAIAgent({ runId, ...input }, channel, { sessionService: this.options.sessionService }, options);
198
211
  },
199
212
  approve: async (runId, approvals, expectedAgentName) => {
200
- if (!this.services.aiRunState)
201
- throw new Error('AIRunStateService not available');
202
- return approveAIAgent(this.services.aiRunState, runId, approvals, expectedAgentName);
213
+ const result = await resumeAIAgentSync(runId, approvals, {
214
+ sessionService: this.options.sessionService,
215
+ }, expectedAgentName);
216
+ return {
217
+ runId: result.runId,
218
+ result: result.object ?? result.text,
219
+ usage: result.usage,
220
+ ...(result.status === 'suspended' && {
221
+ status: 'suspended',
222
+ pendingApprovals: result.pendingApprovals,
223
+ }),
224
+ };
203
225
  },
204
226
  };
205
227
  }
@@ -211,7 +233,7 @@ export class ContextAwareRPCService {
211
233
  const colonIndex = funcName.indexOf(':');
212
234
  if (colonIndex !== -1) {
213
235
  const namespace = funcName.substring(0, colonIndex);
214
- const addons = pikkuState(null, 'rpc', 'addons');
236
+ const addons = pikkuState(null, 'addons', 'packages');
215
237
  const pkgConfig = addons.get(namespace);
216
238
  endpoint = pkgConfig?.rpcEndpoint;
217
239
  }
@@ -34,7 +34,12 @@ export interface RPCHandler<Input, Output> {
34
34
  * Resolved function reference from namespace
35
35
  */
36
36
  export interface ResolvedFunction {
37
- namespace: string;
38
37
  package: string;
39
38
  function: string;
39
+ addonConfig: {
40
+ package: string;
41
+ auth?: boolean;
42
+ tags?: string[];
43
+ rpcEndpoint?: string;
44
+ };
40
45
  }
@@ -3,6 +3,7 @@ export type WireAddonConfig = {
3
3
  package: string;
4
4
  rpcEndpoint?: string;
5
5
  auth?: boolean;
6
+ mcp?: boolean;
6
7
  tags?: string[];
7
8
  secretOverrides?: Record<string, string>;
8
9
  variableOverrides?: Record<string, string>;
@@ -1,6 +1,6 @@
1
1
  import { pikkuState } from '../../pikku-state.js';
2
2
  export const wireAddon = (config) => {
3
- pikkuState(null, 'rpc', 'addons').set(config.name, {
3
+ pikkuState(null, 'addons', 'packages').set(config.name, {
4
4
  package: config.package,
5
5
  rpcEndpoint: config.rpcEndpoint,
6
6
  auth: config.auth,
@@ -72,6 +72,7 @@ export async function runScheduledTask({ name, session, }) {
72
72
  tags: task.tags,
73
73
  wire,
74
74
  sessionService: userSession,
75
+ packageName: meta.packageName,
75
76
  });
76
77
  }
77
78
  catch (e) {
@@ -0,0 +1,3 @@
1
+ export declare function computeEntryNodeIds(nodes: Record<string, any>): string[];
2
+ export declare function validateWorkflowWiring(nodes: Record<string, any>, toolNames: string[]): string[];
3
+ export declare function generateMermaidDiagram(workflowName: string, nodes: Record<string, any>, entryNodeIds: string[]): string;
@@ -0,0 +1,175 @@
1
+ import { pikkuState } from '../../../pikku-state.js';
2
+ import { resolveNamespace } from '../../rpc/rpc-runner.js';
3
+ function normalizeTargets(value) {
4
+ if (!value)
5
+ return [];
6
+ if (typeof value === 'string')
7
+ return [value];
8
+ if (Array.isArray(value))
9
+ return value.filter((v) => typeof v === 'string');
10
+ if (typeof value === 'object') {
11
+ const targets = [];
12
+ for (const v of Object.values(value)) {
13
+ targets.push(...normalizeTargets(v));
14
+ }
15
+ return targets;
16
+ }
17
+ return [];
18
+ }
19
+ function collectRefs(value, refs) {
20
+ if (typeof value === 'object' &&
21
+ value !== null &&
22
+ '$ref' in value &&
23
+ typeof value.$ref === 'string') {
24
+ refs.add(value.$ref);
25
+ return;
26
+ }
27
+ if (Array.isArray(value)) {
28
+ for (const item of value)
29
+ collectRefs(item, refs);
30
+ return;
31
+ }
32
+ if (typeof value === 'object' && value !== null) {
33
+ for (const v of Object.values(value))
34
+ collectRefs(v, refs);
35
+ }
36
+ }
37
+ function resolveToolMeta(toolName) {
38
+ const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null;
39
+ if (resolved) {
40
+ const fnMeta = pikkuState(resolved.package, 'function', 'meta')[resolved.function];
41
+ const schemas = pikkuState(resolved.package, 'misc', 'schemas');
42
+ return fnMeta ? { fnMeta, schemas } : null;
43
+ }
44
+ const rpcMeta = pikkuState(null, 'rpc', 'meta');
45
+ const pikkuFuncId = rpcMeta[toolName];
46
+ if (!pikkuFuncId)
47
+ return null;
48
+ const fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId];
49
+ const schemas = pikkuState(null, 'misc', 'schemas');
50
+ return fnMeta ? { fnMeta, schemas } : null;
51
+ }
52
+ export function computeEntryNodeIds(nodes) {
53
+ const referenced = new Set();
54
+ for (const node of Object.values(nodes)) {
55
+ if (node.next) {
56
+ for (const target of normalizeTargets(node.next)) {
57
+ referenced.add(target);
58
+ }
59
+ }
60
+ if (node.onError) {
61
+ for (const target of normalizeTargets(node.onError)) {
62
+ referenced.add(target);
63
+ }
64
+ }
65
+ }
66
+ return Object.keys(nodes).filter((id) => !referenced.has(id));
67
+ }
68
+ export function validateWorkflowWiring(nodes, toolNames) {
69
+ const errors = [];
70
+ const nodeIds = new Set(Object.keys(nodes));
71
+ const toolSet = new Set(toolNames);
72
+ for (const [nodeId, node] of Object.entries(nodes)) {
73
+ if (!node.rpcName) {
74
+ errors.push(`Node '${nodeId}' is missing 'rpcName'`);
75
+ continue;
76
+ }
77
+ if (!toolSet.has(node.rpcName)) {
78
+ errors.push(`Node '${nodeId}' references unknown tool '${node.rpcName}'. Available tools: ${toolNames.join(', ')}`);
79
+ continue;
80
+ }
81
+ const nextTargets = normalizeTargets(node.next);
82
+ for (const target of nextTargets) {
83
+ if (!nodeIds.has(target)) {
84
+ errors.push(`Node '${nodeId}' routes to unknown node '${target}' in 'next'`);
85
+ }
86
+ }
87
+ const errorTargets = normalizeTargets(node.onError);
88
+ for (const target of errorTargets) {
89
+ if (!nodeIds.has(target)) {
90
+ errors.push(`Node '${nodeId}' routes to unknown node '${target}' in 'onError'`);
91
+ }
92
+ }
93
+ if (!node.input)
94
+ continue;
95
+ const refs = new Set();
96
+ collectRefs(node.input, refs);
97
+ for (const ref of refs) {
98
+ if (ref === 'trigger' || ref === '$item') {
99
+ const targetMeta = resolveToolMeta(node.rpcName);
100
+ if (targetMeta?.fnMeta?.inputSchemaName) {
101
+ const targetSchema = targetMeta.schemas.get(targetMeta.fnMeta.inputSchemaName);
102
+ if (targetSchema?.properties) {
103
+ for (const [field, fieldValue] of Object.entries(node.input)) {
104
+ if (typeof fieldValue === 'object' &&
105
+ fieldValue !== null &&
106
+ fieldValue.$ref === ref &&
107
+ !fieldValue.path) {
108
+ const targetType = targetSchema.properties[field]?.type;
109
+ if (targetType && targetType !== 'object') {
110
+ errors.push(`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.`);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ continue;
117
+ }
118
+ if (!nodeIds.has(ref)) {
119
+ errors.push(`Node '${nodeId}' references unknown node '${ref}' in input`);
120
+ continue;
121
+ }
122
+ const sourceNode = nodes[ref];
123
+ if (!sourceNode?.rpcName)
124
+ continue;
125
+ const sourceMeta = resolveToolMeta(sourceNode.rpcName);
126
+ if (!sourceMeta?.fnMeta?.outputSchemaName)
127
+ continue;
128
+ const sourceSchema = sourceMeta.schemas.get(sourceMeta.fnMeta.outputSchemaName);
129
+ if (!sourceSchema?.properties)
130
+ continue;
131
+ for (const [field, fieldValue] of Object.entries(node.input)) {
132
+ if (typeof fieldValue === 'object' &&
133
+ fieldValue !== null &&
134
+ fieldValue.$ref === ref &&
135
+ fieldValue.path) {
136
+ const pathRoot = fieldValue.path.split('.')[0];
137
+ if (sourceSchema.properties &&
138
+ !(pathRoot in sourceSchema.properties)) {
139
+ errors.push(`Node '${nodeId}' input field '${field}' references path '${fieldValue.path}' but node '${ref}' (${sourceNode.rpcName}) output has no property '${pathRoot}'`);
140
+ continue;
141
+ }
142
+ const targetMeta = resolveToolMeta(node.rpcName);
143
+ if (!targetMeta?.fnMeta?.inputSchemaName)
144
+ continue;
145
+ const targetSchema = targetMeta.schemas.get(targetMeta.fnMeta.inputSchemaName);
146
+ if (!targetSchema?.properties?.[field])
147
+ continue;
148
+ const sourceType = sourceSchema.properties[pathRoot]?.type;
149
+ const targetType = targetSchema.properties[field].type;
150
+ if (sourceType && targetType && sourceType !== targetType) {
151
+ errors.push(`Node '${nodeId}' input field '${field}' expects type '${targetType}', but node '${ref}' output field '${pathRoot}' is type '${sourceType}'`);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return errors;
158
+ }
159
+ export function generateMermaidDiagram(workflowName, nodes, entryNodeIds) {
160
+ const lines = ['graph TD'];
161
+ for (const [nodeId, node] of Object.entries(nodes)) {
162
+ const label = node.rpcName || nodeId;
163
+ const isEntry = entryNodeIds.includes(nodeId);
164
+ lines.push(` ${nodeId}${isEntry ? '([' : '['}${label}${isEntry ? '])' : ']'}`);
165
+ const nextTargets = normalizeTargets(node.next);
166
+ for (const target of nextTargets) {
167
+ lines.push(` ${nodeId} --> ${target}`);
168
+ }
169
+ const errorTargets = normalizeTargets(node.onError);
170
+ for (const target of errorTargets) {
171
+ lines.push(` ${nodeId} -.->|error| ${target}`);
172
+ }
173
+ }
174
+ return lines.join('\n');
175
+ }
@@ -2,3 +2,4 @@ export { continueGraph, executeGraphStep, onGraphNodeComplete, runFromMeta, runW
2
2
  export { template } from './template.js';
3
3
  export type { TemplateString } from './template.js';
4
4
  export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './wire-workflow-graph.js';
5
+ export { validateWorkflowWiring, computeEntryNodeIds, generateMermaidDiagram, } from './graph-validation.js';
@@ -1,3 +1,4 @@
1
1
  export { continueGraph, executeGraphStep, onGraphNodeComplete, runFromMeta, runWorkflowGraph, } from './graph-runner.js';
2
2
  export { template } from './template.js';
3
3
  export { pikkuWorkflowGraph, } from './wire-workflow-graph.js';
4
+ export { validateWorkflowWiring, computeEntryNodeIds, generateMermaidDiagram, } from './graph-validation.js';
@@ -6,5 +6,5 @@ export { addWorkflow } from './dsl/workflow-runner.js';
6
6
  export { template, type TemplateString } from './graph/template.js';
7
7
  export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './graph/wire-workflow-graph.js';
8
8
  export { workflow, workflowStart, workflowStatus, graphStart, } from './workflow-helpers.js';
9
- export type { WorkflowService, WorkflowServiceConfig, WorkflowRunWire, WorkflowStatus, StepStatus, WorkflowRun, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
9
+ export type { WorkflowService, WorkflowServiceConfig, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
10
10
  export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '../../types/core.types.js';
2
- import type { StepState, WorkflowRun, WorkflowRunWire, WorkflowStatus, WorkflowStepOptions } from './workflow.types.js';
2
+ import type { StepState, WorkflowRun, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
3
3
  import type { WorkflowService } from '../../services/workflow-service.js';
4
4
  import { PikkuError } from '../../errors/error-handler.js';
5
5
  /**
@@ -197,11 +197,17 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
197
197
  * @returns The state object with all variables
198
198
  */
199
199
  abstract getRunState(runId: string): Promise<Record<string, unknown>>;
200
- abstract upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string): Promise<void>;
200
+ abstract upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
201
+ abstract updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
201
202
  abstract getWorkflowVersion(name: string, graphHash: string): Promise<{
202
203
  graph: any;
203
204
  source: string;
204
205
  } | null>;
206
+ abstract getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
207
+ workflowName: string;
208
+ graphHash: string;
209
+ graph: any;
210
+ }>>;
205
211
  /**
206
212
  * Resume a paused workflow by triggering the orchestrator
207
213
  * @param runId - Run ID
@@ -162,8 +162,7 @@ export class PikkuWorkflowService {
162
162
  if (!workflowMeta) {
163
163
  throw new WorkflowNotFoundError(name);
164
164
  }
165
- // Check if this is a graph workflow (source === 'graph')
166
- if (workflowMeta.source === 'graph') {
165
+ if (workflowMeta.source === 'graph' || workflowMeta.source === 'ai-agent') {
167
166
  const shouldInline = options?.inline || !getSingletonServices()?.queueService;
168
167
  return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire);
169
168
  }
@@ -19,6 +19,10 @@ export interface WorkflowServiceConfig {
19
19
  * Workflow run status
20
20
  */
21
21
  export type WorkflowStatus = 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled';
22
+ /**
23
+ * Workflow version status (for AI-generated workflows)
24
+ */
25
+ export type WorkflowVersionStatus = 'draft' | 'active' | 'declined';
22
26
  /**
23
27
  * Workflow step status
24
28
  */
@@ -102,6 +106,11 @@ export interface WorkflowRunService {
102
106
  graph: any;
103
107
  source: string;
104
108
  } | null>;
109
+ getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
110
+ workflowName: string;
111
+ graphHash: string;
112
+ graph: any;
113
+ }>>;
105
114
  deleteRun(id: string): Promise<boolean>;
106
115
  }
107
116
  /**
@@ -167,7 +176,7 @@ export interface WorkflowRuntimeMeta {
167
176
  /** Pikku function name (for execution) */
168
177
  pikkuFuncId: string;
169
178
  /** Source type: 'dsl' (serializable), 'complex' (has inline steps), 'graph' */
170
- source: 'dsl' | 'complex' | 'graph';
179
+ source: 'dsl' | 'complex' | 'graph' | 'ai-agent';
171
180
  /** Optional description */
172
181
  description?: string;
173
182
  /** Tags for organization */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "./rpc": "./dist/wirings/rpc/index.js",
31
31
  "./mcp": "./dist/wirings/mcp/index.js",
32
32
  "./ai-agent": "./dist/wirings/ai-agent/index.js",
33
+ "./gateway": "./dist/wirings/gateway/index.js",
33
34
  "./cli": "./dist/wirings/cli/index.js",
34
35
  "./cli/channel": "./dist/wirings/cli/channel/index.js",
35
36
  "./node": "./dist/wirings/node/index.js",
@@ -40,8 +41,10 @@
40
41
  "./services": "./dist/services/index.js",
41
42
  "./services/local-content": "./dist/services/local-content.js",
42
43
  "./services/gopass-secrets": "./dist/services/gopass-secrets.js",
44
+ "./crypto-utils": "./dist/crypto-utils.js",
43
45
  "./internal": "./dist/internal.js",
44
- "./schema": "./dist/schema.js"
46
+ "./schema": "./dist/schema.js",
47
+ "./testing": "./dist/testing/index.js"
45
48
  },
46
49
  "dependencies": {
47
50
  "@standard-schema/spec": "^1.1.0",
@@ -50,7 +53,7 @@
50
53
  "picoquery": "^2.5.0"
51
54
  },
52
55
  "devDependencies": {
53
- "@types/node": "^24.10.12",
56
+ "@types/node": "^24.11.0",
54
57
  "tsx": "^4.21.0",
55
58
  "typescript": "^5.9"
56
59
  },
@@ -1,6 +1,15 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert'
3
- import { encryptJSON, decryptJSON } from './crypto-utils.js'
3
+ import {
4
+ encryptJSON,
5
+ decryptJSON,
6
+ generateDEK,
7
+ wrapDEK,
8
+ unwrapDEK,
9
+ envelopeEncrypt,
10
+ envelopeDecrypt,
11
+ envelopeRewrap,
12
+ } from './crypto-utils.js'
4
13
 
5
14
  describe('encryptJSON / decryptJSON', () => {
6
15
  const secret = 'test-secret-key-for-encryption'
@@ -114,3 +123,92 @@ describe('encryptJSON / decryptJSON', () => {
114
123
  assert.deepStrictEqual(decrypted, original)
115
124
  })
116
125
  })
126
+
127
+ describe('envelope encryption', () => {
128
+ const kek = 'my-key-encryption-key'
129
+
130
+ test('generateDEK produces unique keys', async () => {
131
+ const dek1 = await generateDEK()
132
+ const dek2 = await generateDEK()
133
+ assert.notStrictEqual(dek1, dek2)
134
+ assert.ok(dek1.length > 0)
135
+ })
136
+
137
+ test('wrapDEK / unwrapDEK round-trip', async () => {
138
+ const dek = await generateDEK()
139
+ const wrapped = await wrapDEK(kek, dek)
140
+ const unwrapped = await unwrapDEK(kek, wrapped)
141
+ assert.strictEqual(unwrapped, dek)
142
+ })
143
+
144
+ test('unwrapDEK fails with wrong KEK', async () => {
145
+ const dek = await generateDEK()
146
+ const wrapped = await wrapDEK(kek, dek)
147
+ await assert.rejects(
148
+ () => unwrapDEK('wrong-kek', wrapped),
149
+ (err: any) => err instanceof Error
150
+ )
151
+ })
152
+
153
+ test('envelopeEncrypt / envelopeDecrypt round-trip', async () => {
154
+ const original = {
155
+ apiKey: 'sk-secret-123',
156
+ endpoint: 'https://api.example.com',
157
+ }
158
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(kek, original)
159
+ const decrypted = await envelopeDecrypt(kek, ciphertext, wrappedDEK)
160
+ assert.deepStrictEqual(decrypted, original)
161
+ })
162
+
163
+ test('envelopeEncrypt produces unique ciphertexts and DEKs per call', async () => {
164
+ const value = { same: 'data' }
165
+ const r1 = await envelopeEncrypt(kek, value)
166
+ const r2 = await envelopeEncrypt(kek, value)
167
+ assert.notStrictEqual(r1.ciphertext, r2.ciphertext)
168
+ assert.notStrictEqual(r1.wrappedDEK, r2.wrappedDEK)
169
+ })
170
+
171
+ test('envelopeDecrypt fails with wrong KEK', async () => {
172
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(kek, 'secret')
173
+ await assert.rejects(
174
+ () => envelopeDecrypt('wrong-kek', ciphertext, wrappedDEK),
175
+ (err: any) => err instanceof Error
176
+ )
177
+ })
178
+
179
+ test('envelopeRewrap allows decryption with new KEK', async () => {
180
+ const newKEK = 'my-new-kek'
181
+ const original = { token: 'abc-123' }
182
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(kek, original)
183
+
184
+ const rewrapped = await envelopeRewrap(kek, newKEK, wrappedDEK)
185
+
186
+ await assert.rejects(
187
+ () => envelopeDecrypt(kek, ciphertext, rewrapped),
188
+ (err: any) => err instanceof Error
189
+ )
190
+
191
+ const decrypted = await envelopeDecrypt(newKEK, ciphertext, rewrapped)
192
+ assert.deepStrictEqual(decrypted, original)
193
+ })
194
+
195
+ test('envelopeRewrap does not change the ciphertext', async () => {
196
+ const original = { data: 'important' }
197
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(kek, original)
198
+ const rewrapped = await envelopeRewrap(kek, 'new-kek', wrappedDEK)
199
+
200
+ assert.notStrictEqual(rewrapped, wrappedDEK)
201
+
202
+ const decrypted = await envelopeDecrypt('new-kek', ciphertext, rewrapped)
203
+ assert.deepStrictEqual(decrypted, original)
204
+ })
205
+
206
+ test('envelope handles string secrets', async () => {
207
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(
208
+ kek,
209
+ 'plain-string-secret'
210
+ )
211
+ const decrypted = await envelopeDecrypt<string>(kek, ciphertext, wrappedDEK)
212
+ assert.strictEqual(decrypted, 'plain-string-secret')
213
+ })
214
+ })