@pikku/core 0.12.19 → 0.12.21

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 (146) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/function-runner.js +53 -3
  6. package/dist/function/functions.types.d.ts +3 -2
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/content-service.d.ts +66 -37
  21. package/dist/services/in-memory-queue-service.js +1 -1
  22. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  23. package/dist/services/in-memory-workflow-service.js +31 -13
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/local-content.d.ts +10 -12
  26. package/dist/services/local-content.js +54 -38
  27. package/dist/services/user-session-service.d.ts +1 -1
  28. package/dist/testing/service-tests.js +24 -0
  29. package/dist/types/core.types.d.ts +4 -0
  30. package/dist/types/state.types.d.ts +2 -18
  31. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  32. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  33. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  34. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  35. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  36. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  37. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  38. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  39. package/dist/wirings/channel/channel-handler.js +1 -1
  40. package/dist/wirings/channel/channel-store.d.ts +13 -0
  41. package/dist/wirings/channel/channel.types.d.ts +3 -0
  42. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  43. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  44. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  45. package/dist/wirings/cli/cli-runner.js +24 -5
  46. package/dist/wirings/cli/command-parser.js +19 -4
  47. package/dist/wirings/http/http-runner.js +72 -36
  48. package/dist/wirings/http/http.types.d.ts +1 -0
  49. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  50. package/dist/wirings/http/web-request.js +32 -0
  51. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  52. package/dist/wirings/rpc/rpc-runner.js +45 -11
  53. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  54. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  55. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  56. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  57. package/dist/wirings/workflow/index.d.ts +1 -1
  58. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  59. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  60. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  61. package/package.json +1 -1
  62. package/run-tests.sh +4 -1
  63. package/src/dev/hot-reload.test.ts +62 -11
  64. package/src/dev/hot-reload.ts +28 -7
  65. package/src/errors/error-handler.ts +8 -2
  66. package/src/errors/error.test.ts +2 -0
  67. package/src/function/function-runner.test.ts +500 -10
  68. package/src/function/function-runner.ts +68 -3
  69. package/src/function/functions.types.ts +3 -2
  70. package/src/index.ts +22 -3
  71. package/src/middleware/index.ts +11 -2
  72. package/src/middleware/telemetry.ts +2 -2
  73. package/src/middleware-runner.test.ts +16 -16
  74. package/src/middleware-runner.ts +42 -30
  75. package/src/permissions.test.ts +27 -24
  76. package/src/permissions.ts +41 -25
  77. package/src/pikku-request.test.ts +35 -0
  78. package/src/pikku-request.ts +1 -1
  79. package/src/pikku-state.ts +2 -3
  80. package/src/run-tests-script.test.ts +18 -0
  81. package/src/services/ai-agent-runner-service.ts +1 -0
  82. package/src/services/content-service.ts +79 -51
  83. package/src/services/in-memory-queue-service.ts +1 -1
  84. package/src/services/in-memory-session-store.ts +1 -2
  85. package/src/services/in-memory-workflow-service.test.ts +33 -11
  86. package/src/services/in-memory-workflow-service.ts +49 -13
  87. package/src/services/index.ts +10 -1
  88. package/src/services/local-content.test.ts +54 -0
  89. package/src/services/local-content.ts +80 -53
  90. package/src/services/typed-credential-service.ts +3 -3
  91. package/src/services/typed-secret-service.ts +3 -3
  92. package/src/services/typed-variables-service.ts +3 -3
  93. package/src/services/user-session-service.ts +4 -4
  94. package/src/testing/service-tests.ts +30 -0
  95. package/src/types/core.types.ts +6 -2
  96. package/src/types/state.types.ts +2 -13
  97. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  98. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  99. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  100. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  101. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  102. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  103. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  104. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  105. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  106. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  107. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  108. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  109. package/src/wirings/channel/channel-handler.test.ts +272 -0
  110. package/src/wirings/channel/channel-handler.ts +1 -1
  111. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  112. package/src/wirings/channel/channel-store.ts +19 -0
  113. package/src/wirings/channel/channel.types.ts +4 -0
  114. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  115. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  116. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  117. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  118. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  119. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  120. package/src/wirings/cli/cli-runner.test.ts +255 -2
  121. package/src/wirings/cli/cli-runner.ts +31 -10
  122. package/src/wirings/cli/cli.types.ts +4 -8
  123. package/src/wirings/cli/command-parser.test.ts +83 -0
  124. package/src/wirings/cli/command-parser.ts +23 -4
  125. package/src/wirings/http/http-runner.test.ts +296 -1
  126. package/src/wirings/http/http-runner.ts +87 -57
  127. package/src/wirings/http/http.types.ts +11 -26
  128. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  129. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  130. package/src/wirings/http/web-request.test.ts +115 -0
  131. package/src/wirings/http/web-request.ts +43 -5
  132. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  133. package/src/wirings/queue/queue.types.ts +2 -5
  134. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  135. package/src/wirings/rpc/rpc-runner.ts +60 -21
  136. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  137. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  138. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  139. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  140. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  141. package/src/wirings/workflow/index.ts +1 -0
  142. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  143. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  144. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  145. package/src/wirings/workflow/workflow.types.ts +68 -5
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -239,8 +239,10 @@ const executeRoute = async (services, matchedRoute, http, options) => {
239
239
  response.setMode('stream');
240
240
  response.header('Content-Type', 'text/event-stream');
241
241
  response.header('Cache-Control', 'no-cache');
242
+ let sseState;
243
+ const channelId = createWeakUID();
242
244
  channel = {
243
- channelId: requestId,
245
+ channelId,
244
246
  openingData: await data(),
245
247
  send: (data) => {
246
248
  response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data);
@@ -253,7 +255,36 @@ const executeRoute = async (services, matchedRoute, http, options) => {
253
255
  response.close?.();
254
256
  },
255
257
  state: 'open',
258
+ setState: (s) => {
259
+ sseState = s;
260
+ },
261
+ getState: () => sseState,
262
+ clearState: () => {
263
+ sseState = undefined;
264
+ },
256
265
  };
266
+ // Register the SSE channel with the eventHub (if present) so that
267
+ // eventHub.publish() can deliver messages to SSE subscribers.
268
+ // The channel handler wraps the SSE channel to match PikkuChannelHandler.
269
+ if (singletonServices.eventHub?.onChannelOpened) {
270
+ const channelRef = channel;
271
+ const channelHandler = {
272
+ getChannel: () => channelRef,
273
+ send: (data, isBinary) => {
274
+ if (isBinary)
275
+ channelRef.sendBinary(data);
276
+ else
277
+ channelRef.send(data);
278
+ },
279
+ sendBinary: (data) => channelRef.sendBinary(data),
280
+ };
281
+ singletonServices.eventHub.onChannelOpened(channelHandler);
282
+ const originalClose = channel.close;
283
+ channel.close = () => {
284
+ singletonServices.eventHub.onChannelClosed(channelId);
285
+ originalClose();
286
+ };
287
+ }
257
288
  }
258
289
  const wire = {
259
290
  traceId: requestId,
@@ -264,22 +295,45 @@ const executeRoute = async (services, matchedRoute, http, options) => {
264
295
  getSession: () => userSession.get(),
265
296
  hasSessionChanged: () => userSession.sessionChanged,
266
297
  };
267
- result = await runPikkuFunc('http', `${meta.method}:${meta.route}`, meta.pikkuFuncId, {
268
- singletonServices,
269
- createWireServices,
270
- auth: route.auth !== false,
271
- data,
272
- inheritedMiddleware: meta.middleware,
273
- wireMiddleware: route.middleware,
274
- inheritedPermissions: meta.permissions,
275
- wirePermissions: route.permissions,
276
- coerceDataFromSchema: options.coerceDataFromSchema,
277
- tags: route.tags,
278
- wire,
279
- sessionService: userSession,
280
- packageName: meta.packageName,
281
- });
282
- if (!matchedRoute.route.sse) {
298
+ try {
299
+ result = await runPikkuFunc('http', `${meta.method}:${meta.route}`, meta.pikkuFuncId, {
300
+ singletonServices,
301
+ createWireServices,
302
+ auth: route.auth !== false,
303
+ data,
304
+ inheritedMiddleware: meta.middleware,
305
+ wireMiddleware: route.middleware,
306
+ inheritedPermissions: meta.permissions,
307
+ wirePermissions: route.permissions,
308
+ coerceDataFromSchema: options.coerceDataFromSchema,
309
+ tags: route.tags,
310
+ wire,
311
+ sessionService: userSession,
312
+ packageName: meta.packageName,
313
+ });
314
+ }
315
+ catch (e) {
316
+ if (matchedRoute.route.sse) {
317
+ singletonServices.logger.error(e instanceof Error ? e.message : e);
318
+ try {
319
+ const errorResponse = getErrorResponse(e);
320
+ http?.response?.arrayBuffer(JSON.stringify({
321
+ type: 'error',
322
+ errorText: errorResponse?.message ?? 'Internal server error',
323
+ }));
324
+ http?.response?.arrayBuffer(JSON.stringify({ type: 'done' }));
325
+ }
326
+ catch { }
327
+ channel?.close();
328
+ return wireServices ? { result, wireServices } : { result };
329
+ }
330
+ throw e;
331
+ }
332
+ if (matchedRoute.route.sse) {
333
+ // Flush headers after middleware has run so CORS/auth headers are included
334
+ http?.response?.flushHeaders?.();
335
+ }
336
+ else {
283
337
  if (result instanceof Response) {
284
338
  await applyWebResponse(http.response, result);
285
339
  }
@@ -405,25 +459,7 @@ export const fetchData = async (request, response, { skipUserSession = false, re
405
459
  return result;
406
460
  }
407
461
  catch (e) {
408
- if (matchedRoute?.route.sse) {
409
- // For SSE routes, send error through the stream since the response is already in stream mode
410
- scopedLogger.error(e instanceof Error ? e.message : e);
411
- try {
412
- const errorResponse = getErrorResponse(e);
413
- response.arrayBuffer(JSON.stringify({
414
- type: 'error',
415
- errorText: errorResponse?.message ?? 'Internal server error',
416
- }));
417
- response.arrayBuffer(JSON.stringify({ type: 'done' }));
418
- }
419
- catch (streamErr) {
420
- scopedLogger.error(`SSE error while sending error payload: ${streamErr instanceof Error ? streamErr.message : String(streamErr)}`);
421
- }
422
- response.close?.();
423
- }
424
- else {
425
- handleHTTPError(e, http, requestId, scopedLogger, logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
426
- }
462
+ handleHTTPError(e, http, requestId, scopedLogger, logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
427
463
  }
428
464
  finally {
429
465
  // Clean up any session-specific services created during processing
@@ -177,6 +177,7 @@ export interface PikkuHTTPResponse<Out = unknown> {
177
177
  redirect(location: string, status?: number): this;
178
178
  close?: () => void;
179
179
  setMode?: (mode: 'stream') => void;
180
+ flushHeaders?: () => void;
180
181
  }
181
182
  /**
182
183
  * Single route configuration - supports all wireHTTP options
@@ -89,7 +89,9 @@ export class PikkuFetchHTTPRequest {
89
89
  const merged = {};
90
90
  for (const part of parts) {
91
91
  for (const [key, value] of Object.entries(part)) {
92
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
92
+ if (key === '__proto__' ||
93
+ key === 'constructor' ||
94
+ key === 'prototype') {
93
95
  continue;
94
96
  }
95
97
  if (key in merged && !valuesAreEquivalent(merged[key], value)) {
@@ -57,17 +57,46 @@ export function toWebRequest(req, baseUrl) {
57
57
  });
58
58
  }
59
59
  const SKIP_RESPONSE_HEADERS = new Set(['content-length', 'transfer-encoding']);
60
+ function collectSetCookieHeaders(webResponse) {
61
+ const seen = new Set();
62
+ const values = [];
63
+ const add = (cookie) => {
64
+ if (!cookie || seen.has(cookie)) {
65
+ return;
66
+ }
67
+ seen.add(cookie);
68
+ values.push(cookie);
69
+ };
70
+ const headersWithGetSetCookie = webResponse.headers;
71
+ if (typeof headersWithGetSetCookie.getSetCookie === 'function') {
72
+ for (const cookie of headersWithGetSetCookie.getSetCookie()) {
73
+ add(cookie);
74
+ }
75
+ }
76
+ else {
77
+ webResponse.headers.forEach((value, name) => {
78
+ if (name.toLowerCase() === 'set-cookie') {
79
+ add(value);
80
+ }
81
+ });
82
+ }
83
+ return values;
84
+ }
60
85
  /**
61
86
  * Applies a Web API Response to a PikkuHTTPResponse.
62
87
  * Copies status, headers (including Set-Cookie), redirects, and body.
63
88
  */
64
89
  export async function applyWebResponse(res, webResponse) {
65
90
  res.status(webResponse.status);
91
+ const setCookieValues = collectSetCookieHeaders(webResponse);
66
92
  webResponse.headers.forEach((value, name) => {
67
93
  const lower = name.toLowerCase();
68
94
  if (SKIP_RESPONSE_HEADERS.has(lower)) {
69
95
  return;
70
96
  }
97
+ if (lower === 'set-cookie') {
98
+ return;
99
+ }
71
100
  if (lower === 'location') {
72
101
  res.redirect(value, webResponse.status);
73
102
  }
@@ -75,6 +104,9 @@ export async function applyWebResponse(res, webResponse) {
75
104
  res.header(name, value);
76
105
  }
77
106
  });
107
+ if (setCookieValues.length > 0) {
108
+ res.header('Set-Cookie', setCookieValues);
109
+ }
78
110
  const body = await webResponse.text();
79
111
  if (body) {
80
112
  if (res.send) {
@@ -18,10 +18,11 @@ export declare class ContextAwareRPCService {
18
18
  private services;
19
19
  private wire;
20
20
  private options;
21
+ private packageName;
21
22
  constructor(services: CoreServices, wire: PikkuWire, options: {
22
23
  requiresAuth?: boolean;
23
24
  sessionService?: SessionService<CoreUserSession>;
24
- });
25
+ }, packageName?: string | null);
25
26
  rpcExposed(funcName: string, data: any): Promise<any>;
26
27
  rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
27
28
  /**
@@ -96,7 +97,7 @@ export declare class PikkuRPCService<Services extends CoreServices, TypedRPC = P
96
97
  getContextRPCService(services: Services, wire: PikkuWire, requiresAuthOrOptions?: boolean | {
97
98
  requiresAuth?: boolean;
98
99
  sessionService?: SessionService<CoreUserSession>;
99
- } | undefined, depth?: number): TypedRPC;
100
+ } | undefined, depth?: number, packageName?: string | null): TypedRPC;
100
101
  }
101
102
  export declare const rpcService: PikkuRPCService<import("../../types/core.types.js").CoreSingletonServices<{
102
103
  logLevel?: import("../../services/logger.js").LogLevel;
@@ -1,6 +1,5 @@
1
1
  import { runPikkuFunc } from '../../function/function-runner.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
- import { ForbiddenError } from '../../errors/errors.js';
4
3
  import { PikkuError, addError } from '../../errors/error-handler.js';
5
4
  import { parseVersionedId } from '../../version.js';
6
5
  export class RPCNotFoundError extends PikkuError {
@@ -39,7 +38,22 @@ export const resolveNamespace = (namespacedFunction) => {
39
38
  addonConfig: pkgConfig,
40
39
  };
41
40
  };
42
- const getPikkuFunctionName = (rpcName) => {
41
+ /**
42
+ * Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
43
+ * the caller's addon package when provided. Returns the function name plus
44
+ * the package scope that resolved it (null = root), so callers can thread
45
+ * the scope into runPikkuFunc without a second lookup.
46
+ */
47
+ const resolvePikkuFunction = (rpcName, packageName = null) => {
48
+ // Addon-scoped calls: try the caller's package function meta first.
49
+ // (RPC meta only lives in root; addon functions are registered under their package.)
50
+ if (packageName) {
51
+ const pkgFunctions = pikkuState(packageName, 'function', 'meta');
52
+ const pkgMeta = pkgFunctions?.[rpcName];
53
+ if (pkgMeta) {
54
+ return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName };
55
+ }
56
+ }
43
57
  const rpc = pikkuState(null, 'rpc', 'meta');
44
58
  let rpcMeta = rpc[rpcName];
45
59
  if (!rpcMeta) {
@@ -48,20 +62,32 @@ const getPikkuFunctionName = (rpcName) => {
48
62
  rpcMeta = rpc[baseName];
49
63
  }
50
64
  }
65
+ if (!rpcMeta) {
66
+ const rootFunctions = pikkuState(null, 'function', 'meta');
67
+ const rootFunctionMeta = rootFunctions?.[rpcName];
68
+ if (rootFunctionMeta) {
69
+ return {
70
+ pikkuFuncId: rootFunctionMeta.pikkuFuncId || rpcName,
71
+ packageName: null,
72
+ };
73
+ }
74
+ }
51
75
  if (!rpcMeta) {
52
76
  throw new RPCNotFoundError(rpcName);
53
77
  }
54
- return rpcMeta;
78
+ return { pikkuFuncId: rpcMeta, packageName: null };
55
79
  };
56
80
  // Context-aware RPC client for use within services
57
81
  export class ContextAwareRPCService {
58
82
  services;
59
83
  wire;
60
84
  options;
61
- constructor(services, wire, options) {
85
+ packageName;
86
+ constructor(services, wire, options, packageName = null) {
62
87
  this.services = services;
63
88
  this.wire = wire;
64
89
  this.options = options;
90
+ this.packageName = packageName;
65
91
  }
66
92
  async rpcExposed(funcName, data) {
67
93
  let functionMeta;
@@ -72,13 +98,14 @@ export class ContextAwareRPCService {
72
98
  }
73
99
  }
74
100
  else {
75
- functionMeta = pikkuState(null, 'function', 'meta')[funcName];
101
+ const resolved = resolvePikkuFunction(funcName, this.packageName);
102
+ functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[resolved.pikkuFuncId];
76
103
  }
77
104
  if (!functionMeta) {
78
105
  throw new RPCNotFoundError(funcName);
79
106
  }
80
107
  if (!functionMeta.expose) {
81
- throw new ForbiddenError();
108
+ throw new RPCNotFoundError(funcName);
82
109
  }
83
110
  return await this.rpc(funcName, data);
84
111
  }
@@ -105,13 +132,18 @@ export class ContextAwareRPCService {
105
132
  // Not an addon — fall through to local lookup
106
133
  }
107
134
  }
108
- // Try local function, then fall back to deployment service (remote)
135
+ // Bare name: resolve via caller's package scope first (if any), then root.
136
+ // Note: intra-addon bare calls do NOT re-apply the addon's external
137
+ // addonConfig.auth/tags — those gates are only applied on the external
138
+ // 'namespace:func' boundary via invokeAddonFunction.
109
139
  try {
110
- return await runPikkuFunc('rpc', funcName, getPikkuFunctionName(funcName), {
140
+ const resolved = resolvePikkuFunction(funcName, this.packageName);
141
+ return await runPikkuFunc('rpc', funcName, resolved.pikkuFuncId, {
111
142
  auth: this.options.requiresAuth,
112
143
  singletonServices: this.services,
113
144
  data: () => data,
114
145
  wire: updatedWire,
146
+ packageName: resolved.packageName,
115
147
  });
116
148
  }
117
149
  catch (e) {
@@ -181,11 +213,13 @@ export class ContextAwareRPCService {
181
213
  return this.invokeAddonFunction(rpcName, data, mergedWire);
182
214
  }
183
215
  try {
184
- return await runPikkuFunc('rpc', rpcName, getPikkuFunctionName(rpcName), {
216
+ const resolved = resolvePikkuFunction(rpcName, this.packageName);
217
+ return await runPikkuFunc('rpc', rpcName, resolved.pikkuFuncId, {
185
218
  auth: this.options.requiresAuth,
186
219
  singletonServices: this.services,
187
220
  data: () => data,
188
221
  wire: mergedWire,
222
+ packageName: resolved.packageName,
189
223
  });
190
224
  }
191
225
  catch (e) {
@@ -269,12 +303,12 @@ export class ContextAwareRPCService {
269
303
  // RPC Service class for the global interface
270
304
  export class PikkuRPCService {
271
305
  // Convenience function for initializing
272
- getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0) {
306
+ getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0, packageName = null) {
273
307
  const options = typeof requiresAuthOrOptions === 'object' &&
274
308
  requiresAuthOrOptions !== null
275
309
  ? requiresAuthOrOptions
276
310
  : { requiresAuth: requiresAuthOrOptions };
277
- const serviceRPC = new ContextAwareRPCService(services, wire, options);
311
+ const serviceRPC = new ContextAwareRPCService(services, wire, options, packageName);
278
312
  return {
279
313
  depth,
280
314
  global: false,
@@ -26,6 +26,10 @@ export interface GraphNodeDef<RPCMap extends Record<string, RPCHandler>, NodeIds
26
26
  next?: NextConfig<NodeIds>;
27
27
  /** Error handling - node(s) to execute on error */
28
28
  onError?: NodeIds | NodeIds[];
29
+ /** Maximum retry attempts allowed (excludes the initial attempt) */
30
+ retries?: number;
31
+ /** Delay between retries — milliseconds, duration string, or 'exponential' */
32
+ retryDelay?: string | number;
29
33
  }
30
34
  /**
31
35
  * Helper to create a type-safe graph definition with RPC autocomplete.
@@ -87,6 +91,8 @@ type GraphNodeConfigMap<FuncMap extends Record<string, string>, RPCMap extends R
87
91
  next?: NextConfig<Extract<keyof FuncMap, string>>;
88
92
  input?: (ref: <N extends Extract<keyof FuncMap, string>, P extends keyof ComputeNodeOutputs<FuncMap, RPCMap>[N] & string>(nodeId: N, path: P) => TypedRef<ComputeNodeOutputs<FuncMap, RPCMap>[N][P]>) => InputWithRefs<ComputeNodeInputs<FuncMap, RPCMap>[K]>;
89
93
  onError?: Extract<keyof FuncMap, string> | Extract<keyof FuncMap, string>[];
94
+ retries?: number;
95
+ retryDelay?: string | number;
90
96
  };
91
97
  };
92
98
  /**
@@ -113,5 +119,7 @@ export declare function graph<NodeIds extends string = string>(nodes: Record<Nod
113
119
  next?: NextConfig<NodeIds>;
114
120
  input?: (ref: (nodeId: NodeIds, path: string) => RefValue) => Record<string, unknown>;
115
121
  onError?: NodeIds | NodeIds[];
122
+ retries?: number;
123
+ retryDelay?: string | number;
116
124
  }>): Record<NodeIds, GraphNodeConfig<NodeIds>>;
117
125
  export {};
@@ -20,6 +20,8 @@ export function createGraph() {
20
20
  input: def?.input,
21
21
  next: def?.next,
22
22
  onError: def?.onError,
23
+ retries: def?.retries,
24
+ retryDelay: def?.retryDelay,
23
25
  };
24
26
  }
25
27
  return result;
@@ -52,6 +54,8 @@ export function graph(nodes) {
52
54
  input: def.input,
53
55
  next: def.next,
54
56
  onError: def.onError,
57
+ retries: def.retries,
58
+ retryDelay: def.retryDelay,
55
59
  };
56
60
  }
57
61
  return result;
@@ -257,11 +257,13 @@ function areDependenciesSatisfied(node, completedNodeIds) {
257
257
  const deps = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
258
258
  return deps.every((dep) => completedNodeIds.has(dep));
259
259
  }
260
- async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcName, input) {
261
- await workflowService.insertStepState(runId, nodeId, rpcName, input, {
262
- retries: 0,
263
- });
264
- await workflowService.queueStepWorker(runId, nodeId, rpcName, input);
260
+ async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcName, input, nodeConfig) {
261
+ const stepOptions = {
262
+ retries: nodeConfig?.retries ?? 0,
263
+ retryDelay: nodeConfig?.retryDelay,
264
+ };
265
+ await workflowService.insertStepState(runId, nodeId, rpcName, input, stepOptions);
266
+ await workflowService.queueStepWorker(runId, nodeId, rpcName, input, stepOptions);
265
267
  }
266
268
  export async function continueGraph(workflowService, runId, graphName, overrideMeta) {
267
269
  const meta = overrideMeta ?? getWorkflowMeta(graphName);
@@ -332,7 +334,7 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
332
334
  const fetchedResults = await workflowService.getNodeResults(runId, referencedNodeIds);
333
335
  const nodeResults = { trigger: triggerInput, ...fetchedResults };
334
336
  const resolvedInput = resolveSerializedInput(node.input, nodeResults);
335
- await queueGraphNode(workflowService, runId, graphName, nodeId, node.rpcName, resolvedInput);
337
+ await queueGraphNode(workflowService, runId, graphName, nodeId, node.rpcName, resolvedInput, node);
336
338
  }
337
339
  }
338
340
  export async function executeGraphStep(workflowService, rpcService, runId, stepId, nodeId, rpcName, data, graphName) {
@@ -410,7 +412,7 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
410
412
  for (const errorNodeId of errorNodes) {
411
413
  const errorNode = meta.nodes[errorNodeId];
412
414
  if (errorNode) {
413
- await queueGraphNode(workflowService, runId, graphName, errorNodeId, errorNode.rpcName, { error: { message: error.message } });
415
+ await queueGraphNode(workflowService, runId, graphName, errorNodeId, errorNode.rpcName, { error: { message: error.message } }, errorNode);
414
416
  }
415
417
  }
416
418
  throw error;
@@ -430,7 +432,7 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
430
432
  if (!node)
431
433
  return;
432
434
  const rpcName = node.rpcName;
433
- const stepState = await workflowService.insertStepState(runId, nodeId, rpcName, input, { retries: 3 });
435
+ const stepState = await workflowService.insertStepState(runId, nodeId, rpcName, input, { retries: node.retries ?? 0, retryDelay: node.retryDelay });
434
436
  await workflowService.setStepRunning(stepState.stepId);
435
437
  const wireState = {};
436
438
  const graphWire = {
@@ -486,7 +488,7 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
486
488
  message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
487
489
  code: 'RPC_NOT_FOUND',
488
490
  });
489
- return;
491
+ throw new WorkflowSuspendedException(runId, 'RPC_NOT_FOUND');
490
492
  }
491
493
  await workflowService.setStepError(stepState.stepId, error);
492
494
  if (node?.onError) {
@@ -627,7 +629,7 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
627
629
  const resolvedInput = node.input && Object.keys(node.input).length > 0
628
630
  ? resolveSerializedInput(node.input, triggerNodeResults)
629
631
  : triggerInput;
630
- await queueGraphNode(workflowService, runId, graphName, nodeId, node.rpcName, resolvedInput);
632
+ await queueGraphNode(workflowService, runId, graphName, nodeId, node.rpcName, resolvedInput, node);
631
633
  }
632
634
  if (inline) {
633
635
  workflowService.unregisterInlineRun(runId);
@@ -45,6 +45,10 @@ export interface GraphNodeConfig<NodeIds extends string = string> {
45
45
  next?: NextConfig<NodeIds>;
46
46
  /** Error routing - node(s) to execute on error */
47
47
  onError?: NodeIds | NodeIds[];
48
+ /** Maximum retry attempts allowed (excludes the initial attempt) */
49
+ retries?: number;
50
+ /** Delay between retries — milliseconds, duration string, or 'exponential' */
51
+ retryDelay?: string | number;
48
52
  }
49
53
  /**
50
54
  * Graph wire context - available to functions running in a workflow graph
@@ -8,5 +8,5 @@ export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGr
8
8
  export { validateWorkflowWiring, computeEntryNodeIds, } from './graph/graph-validation.js';
9
9
  export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
10
10
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
11
- export type { WorkflowService, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
11
+ export type { WorkflowService, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
12
12
  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';