@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -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/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  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/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,12 @@
2
2
  function toCamelCase(str) {
3
3
  return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4
4
  }
5
+ /** Convert camelCase to kebab-case for display: "autoApply" → "auto-apply".
6
+ * Flags are stored camelCase (matching the function input field) but shown
7
+ * kebab; the parser accepts both forms via toCamelCase. */
8
+ function toKebabCase(str) {
9
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
10
+ }
5
11
  /**
6
12
  * Parses raw CLI arguments into structured data for a specific program
7
13
  */
@@ -77,6 +83,15 @@ export function parseCLIArguments(args, programName, allMeta) {
77
83
  }
78
84
  return result;
79
85
  }
86
+ // A group/parent command — has subcommands but no runnable function of its
87
+ // own — cannot be executed directly. Surface a routable error so the runner
88
+ // shows its subcommand help instead of attempting to run a missing function.
89
+ if (!commandMeta.pikkuFuncId &&
90
+ commandMeta.subcommands &&
91
+ Object.keys(commandMeta.subcommands).length > 0) {
92
+ result.errors.push(`Missing subcommand: ${result.commandPath.join(' ')}`);
93
+ return result;
94
+ }
80
95
  // Collect all available options (global + inherited)
81
96
  const availableOptions = collectAvailableOptions(meta, result.commandPath);
82
97
  // Parse remaining arguments as positionals and options
@@ -289,7 +304,7 @@ function applyOptionDefaults(optionDefs, options, result) {
289
304
  }
290
305
  // Check required
291
306
  if (def.required && !(name in options)) {
292
- result.errors.push(`Missing required option: --${name}`);
307
+ result.errors.push(`Missing required option: --${toKebabCase(name)}`);
293
308
  }
294
309
  // Validate choices
295
310
  if (def.choices && name in options) {
@@ -297,12 +312,12 @@ function applyOptionDefaults(optionDefs, options, result) {
297
312
  if (Array.isArray(value)) {
298
313
  for (const v of value) {
299
314
  if (!def.choices.includes(v)) {
300
- result.errors.push(`Invalid value for --${name}: ${v}. Valid choices: ${def.choices.join(', ')}`);
315
+ result.errors.push(`Invalid value for --${toKebabCase(name)}: ${v}. Valid choices: ${def.choices.join(', ')}`);
301
316
  }
302
317
  }
303
318
  }
304
319
  else if (!def.choices.includes(value)) {
305
- result.errors.push(`Invalid value for --${name}: ${value}. Valid choices: ${def.choices.join(', ')}`);
320
+ result.errors.push(`Invalid value for --${toKebabCase(name)}: ${value}. Valid choices: ${def.choices.join(', ')}`);
306
321
  }
307
322
  }
308
323
  }
@@ -390,7 +405,7 @@ function formatOptions(options, lines) {
390
405
  else {
391
406
  line += ' ';
392
407
  }
393
- line += `--${name}`;
408
+ line += `--${toKebabCase(name)}`;
394
409
  if (opt.default !== undefined && typeof opt.default !== 'boolean') {
395
410
  line += ` <value>`;
396
411
  }
@@ -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) {
@@ -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 {
@@ -63,6 +62,16 @@ const resolvePikkuFunction = (rpcName, packageName = null) => {
63
62
  rpcMeta = rpc[baseName];
64
63
  }
65
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
+ }
66
75
  if (!rpcMeta) {
67
76
  throw new RPCNotFoundError(rpcName);
68
77
  }
@@ -89,13 +98,14 @@ export class ContextAwareRPCService {
89
98
  }
90
99
  }
91
100
  else {
92
- functionMeta = pikkuState(null, 'function', 'meta')[funcName];
101
+ const resolved = resolvePikkuFunction(funcName, this.packageName);
102
+ functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[resolved.pikkuFuncId];
93
103
  }
94
104
  if (!functionMeta) {
95
105
  throw new RPCNotFoundError(funcName);
96
106
  }
97
107
  if (!functionMeta.expose) {
98
- throw new ForbiddenError();
108
+ throw new RPCNotFoundError(funcName);
99
109
  }
100
110
  return await this.rpc(funcName, data);
101
111
  }
@@ -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';
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '../../types/core.types.js';
2
- import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
2
+ import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, 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
  /**
@@ -35,6 +35,12 @@ export declare class WorkflowNotFoundError extends PikkuError {
35
35
  export declare class WorkflowRunNotFoundError extends PikkuError {
36
36
  constructor(runId: string);
37
37
  }
38
+ export declare class WorkflowRunFailedError extends PikkuError {
39
+ payload: {
40
+ message?: string;
41
+ };
42
+ constructor(message?: string);
43
+ }
38
44
  export declare class WorkflowServiceNotInitialized extends Error {
39
45
  }
40
46
  export declare class WorkflowStepNameNotString extends Error {
@@ -48,7 +54,19 @@ export declare class WorkflowStepNameNotString extends Error {
48
54
  export declare abstract class PikkuWorkflowService implements WorkflowService {
49
55
  private inlineRuns;
50
56
  protected get logger(): import("../../services/logger.js").Logger;
51
- constructor();
57
+ protected mirror?: WorkflowRunMirror;
58
+ constructor(options?: {
59
+ wireQueues?: boolean;
60
+ mirror?: WorkflowRunMirror;
61
+ });
62
+ private safeMirror;
63
+ rewireQueueWorkers(): void;
64
+ /**
65
+ * Wire the queue-based orchestrator/step/sleeper workers.
66
+ * Subclasses that orchestrate without queues (e.g. Durable Objects) should
67
+ * pass `wireQueues: false` to the base constructor and skip this entirely.
68
+ */
69
+ protected wireQueueWorkers(): void;
52
70
  /**
53
71
  * Check if a run is executing inline (without queues)
54
72
  */
@@ -62,7 +80,11 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
62
80
  */
63
81
  unregisterInlineRun(runId: string): void;
64
82
  registerWorkflowVersions(): Promise<void>;
65
- abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
83
+ createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
84
+ deterministic?: boolean;
85
+ plannedSteps?: WorkflowPlannedStep[];
86
+ }): Promise<string>;
87
+ protected abstract createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
66
88
  deterministic?: boolean;
67
89
  plannedSteps?: WorkflowPlannedStep[];
68
90
  }): Promise<string>;
@@ -90,7 +112,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
90
112
  * @param id - Run ID
91
113
  * @param status - New status
92
114
  */
93
- abstract updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
115
+ updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
116
+ protected abstract updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
94
117
  /**
95
118
  * Insert initial step state (called by orchestrator)
96
119
  * Creates pending step in both workflow_step and workflow_step_history
@@ -101,7 +124,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
101
124
  * @param stepOptions - Step options (retries, retryDelay)
102
125
  * @returns Step state with generated stepId
103
126
  */
104
- abstract insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
127
+ insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
128
+ protected abstract insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
105
129
  /**
106
130
  * Get step state by cache key (read-only)
107
131
  * @param runId - Run ID
@@ -114,33 +138,38 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
114
138
  * Updates both workflow_step and workflow_step_history
115
139
  * @param stepId - Step ID
116
140
  */
117
- abstract setStepRunning(stepId: string): Promise<void>;
141
+ setStepRunning(stepId: string): Promise<void>;
142
+ protected abstract setStepRunningImpl(stepId: string): Promise<void>;
118
143
  /**
119
144
  * Mark step as scheduled (queued for execution)
120
145
  * Updates both workflow_step and workflow_step_history
121
146
  * @param stepId - Step ID
122
147
  */
123
- abstract setStepScheduled(stepId: string): Promise<void>;
148
+ setStepScheduled(stepId: string): Promise<void>;
149
+ protected abstract setStepScheduledImpl(stepId: string): Promise<void>;
124
150
  /**
125
151
  * Store step result and mark as succeeded
126
152
  * Updates both workflow_step and workflow_step_history
127
153
  * @param stepId - Step ID
128
154
  * @param result - Step result
129
155
  */
130
- abstract setStepResult(stepId: string, result: any): Promise<void>;
156
+ setStepResult(stepId: string, result: any): Promise<void>;
157
+ protected abstract setStepResultImpl(stepId: string, result: any): Promise<void>;
131
158
  /**
132
159
  * Set the child workflow run ID on a step
133
160
  * @param stepId - Step ID
134
161
  * @param childRunId - Child workflow run ID
135
162
  */
136
- abstract setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
163
+ setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
164
+ protected abstract setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
137
165
  /**
138
166
  * Store step error and mark as failed
139
167
  * Updates both workflow_step and workflow_step_history
140
168
  * @param stepId - Step ID
141
169
  * @param error - Error object
142
170
  */
143
- abstract setStepError(stepId: string, error: Error): Promise<void>;
171
+ setStepError(stepId: string, error: Error): Promise<void>;
172
+ protected abstract setStepErrorImpl(stepId: string, error: Error): Promise<void>;
144
173
  /**
145
174
  * Create a new retry attempt for a failed step
146
175
  * Inserts new pending step in both workflow_step and workflow_step_history
@@ -149,7 +178,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
149
178
  * @param failedStepId - Failed step ID to copy from
150
179
  * @returns New step state for the retry attempt
151
180
  */
152
- abstract createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
181
+ createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
182
+ protected abstract createRetryAttemptImpl(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
153
183
  /**
154
184
  * Execute function within a run lock to prevent concurrent modifications
155
185
  * @param id - Run ID
@@ -198,22 +228,26 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
198
228
  * @param stepId - Step ID
199
229
  * @param branchKey - Branch key selected by graph.branch()
200
230
  */
201
- abstract setBranchTaken(stepId: string, branchKey: string): Promise<void>;
231
+ setBranchTaken(stepId: string, branchKey: string): Promise<void>;
232
+ protected abstract setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
202
233
  /**
203
234
  * Update a state variable in the workflow run's state
204
235
  * @param runId - Run ID
205
236
  * @param name - Variable name
206
237
  * @param value - Value to store
207
238
  */
208
- abstract updateRunState(runId: string, name: string, value: unknown): Promise<void>;
239
+ updateRunState(runId: string, name: string, value: unknown): Promise<void>;
240
+ protected abstract updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
209
241
  /**
210
242
  * Get the entire state object for a workflow run
211
243
  * @param runId - Run ID
212
244
  * @returns The state object with all variables
213
245
  */
214
246
  abstract getRunState(runId: string): Promise<Record<string, unknown>>;
215
- abstract upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
216
- abstract updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
247
+ upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
248
+ protected abstract upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
249
+ updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
250
+ protected abstract updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
217
251
  abstract getWorkflowVersion(name: string, graphHash: string): Promise<{
218
252
  graph: any;
219
253
  source: string;
@@ -228,7 +262,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
228
262
  * @param runId - Run ID
229
263
  */
230
264
  resumeWorkflow(runId: string, workflowName?: string): Promise<void>;
231
- queueStepWorker(runId: string, stepName: string, rpcName: string, data: any): Promise<void>;
265
+ queueStepWorker(runId: string, stepName: string, rpcName: string, data: any, stepOptions?: WorkflowStepOptions): Promise<void>;
232
266
  /**
233
267
  * Execute a workflow sleep step completion
234
268
  * Sets the step result to null and resumes the workflow
@@ -241,6 +275,32 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
241
275
  * @param retryDelay - Delay in milliseconds or duration string (optional)
242
276
  */
243
277
  protected scheduleOrchestratorRetry(runId: string, retryDelay?: number | string, workflowName?: string): Promise<void>;
278
+ /**
279
+ * Dispatch a workflow step to be executed asynchronously.
280
+ *
281
+ * Default implementation enqueues a step worker job via the queue service.
282
+ * Subclasses with non-queue transports (e.g. Durable Objects) override this
283
+ * to dispatch via their own mechanism (RPC to a step worker, etc.).
284
+ *
285
+ * On return, the workflow is paused via `WorkflowAsyncException` thrown by
286
+ * the caller; the step transport is responsible for calling back into the
287
+ * orchestrator (via `resumeWorkflow` or equivalent) when the step completes.
288
+ *
289
+ * @returns true if dispatch was async (caller should pause), false to fall
290
+ * through to the inline execution path.
291
+ */
292
+ protected dispatchStep(runId: string, stepName: string, rpcName: string, data: unknown, stepOptions?: WorkflowStepOptions): Promise<boolean>;
293
+ /**
294
+ * Schedule a workflow sleep wakeup at the given duration.
295
+ *
296
+ * Default implementation uses the scheduler service to enqueue a delayed
297
+ * sleeper RPC. Subclasses with native timer primitives (e.g. Durable Object
298
+ * alarms) override this to schedule directly without going through queues.
299
+ *
300
+ * @returns true if the wakeup was scheduled remotely (caller should pause),
301
+ * false to fall through to inline `setTimeout` behavior.
302
+ */
303
+ protected scheduleSleep(runId: string, stepId: string, duration: number | string): Promise<boolean>;
244
304
  /**
245
305
  * Start a new workflow run
246
306
  * Automatically detects workflow type (DSL or graph) from meta and executes accordingly
@@ -284,6 +344,11 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
284
344
  * Get the orchestrator queue name for a specific workflow.
285
345
  * Checks queue meta for a per-workflow queue first (e.g. wf-orchestrator-{name}),
286
346
  * falls back to the shared orchestrator queue.
347
+ *
348
+ * Reads from `queue.meta` (always populated globally) rather than
349
+ * `queue.registrations` (only populated for queues this unit consumes).
350
+ * In a per-unit deploy the orchestrator unit doesn't consume per-step
351
+ * queues — but it produces to them — so registrations would miss them.
287
352
  */
288
353
  protected getOrchestratorQueueName(workflowName?: string): string;
289
354
  protected getStepWorkerQueueName(rpcName?: string): string;