@pikku/core 0.12.40 → 0.12.42

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 (53) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/function/function-runner.d.ts +2 -2
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/services/credential-wire-service.d.ts +2 -2
  6. package/dist/services/in-memory-workflow-service.d.ts +1 -0
  7. package/dist/services/in-memory-workflow-service.js +3 -0
  8. package/dist/services/pikku-user-id.d.ts +2 -2
  9. package/dist/services/workflow-service.d.ts +1 -0
  10. package/dist/types/core.types.d.ts +18 -5
  11. package/dist/utils.d.ts +3 -1
  12. package/dist/utils.js +2 -0
  13. package/dist/wirings/channel/channel-middleware-runner.d.ts +2 -2
  14. package/dist/wirings/channel/local/local-eventhub-service.js +7 -4
  15. package/dist/wirings/rpc/rpc-runner.d.ts +4 -4
  16. package/dist/wirings/rpc/rpc-runner.js +0 -16
  17. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  18. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -1
  19. package/dist/wirings/workflow/index.d.ts +1 -1
  20. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  21. package/dist/wirings/workflow/pikku-workflow-service.js +11 -4
  22. package/dist/wirings/workflow/workflow.types.d.ts +4 -2
  23. package/package.json +1 -1
  24. package/src/function/function-runner.ts +3 -2
  25. package/src/index.ts +7 -1
  26. package/src/services/credential-wire-service.ts +2 -2
  27. package/src/services/in-memory-workflow-service.test.ts +2 -2
  28. package/src/services/in-memory-workflow-service.ts +7 -1
  29. package/src/services/pikku-user-id.ts +2 -2
  30. package/src/services/workflow-service.ts +1 -0
  31. package/src/types/core.types.ts +22 -6
  32. package/src/utils.ts +12 -1
  33. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -2
  34. package/src/wirings/channel/channel-common.ts +8 -2
  35. package/src/wirings/channel/channel-middleware-runner.ts +3 -3
  36. package/src/wirings/channel/local/local-channel-runner.ts +7 -3
  37. package/src/wirings/channel/local/local-eventhub-service.ts +6 -3
  38. package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -4
  39. package/src/wirings/cli/cli-runner.ts +2 -2
  40. package/src/wirings/gateway/gateway-runner.ts +8 -2
  41. package/src/wirings/http/http-runner.ts +3 -2
  42. package/src/wirings/mcp/mcp-runner.ts +2 -2
  43. package/src/wirings/queue/queue-runner.ts +2 -2
  44. package/src/wirings/rpc/rpc-runner.ts +12 -24
  45. package/src/wirings/scheduler/scheduler-runner.ts +2 -2
  46. package/src/wirings/trigger/trigger-runner.ts +2 -2
  47. package/src/wirings/workflow/dsl/index.ts +1 -0
  48. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +10 -0
  49. package/src/wirings/workflow/index.ts +1 -0
  50. package/src/wirings/workflow/pikku-workflow-service.test.ts +3 -2
  51. package/src/wirings/workflow/pikku-workflow-service.ts +29 -10
  52. package/src/wirings/workflow/workflow.types.ts +4 -1
  53. package/tsconfig.tsbuildinfo +1 -1
@@ -178,8 +178,7 @@ export type JSONValue =
178
178
  /**
179
179
  * Utility type for making certain keys required and leaving the rest as optional.
180
180
  */
181
- export type PickRequired<T, K extends keyof T> = Required<Pick<T, K>> &
182
- Partial<T>
181
+ export type PickRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
183
182
 
184
183
  /**
185
184
  * Utility type for making certain keys optional while keeping the rest required.
@@ -285,7 +284,10 @@ export type PikkuWire<
285
284
  MCPTools extends string | never = never,
286
285
  TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
287
286
  TriggerOutput = unknown,
288
- > = Partial<{
287
+ > = {
288
+ /** Always present — lazily initialised on first access for every function invocation */
289
+ rpc: TypedRPC
290
+ } & Partial<{
289
291
  wireType: PikkuWiringTypes
290
292
  wireId: string
291
293
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
@@ -294,7 +296,6 @@ export type PikkuWire<
294
296
  functionId: string
295
297
  http: PikkuHTTP<In>
296
298
  mcp: PikkuMCP<MCPTools>
297
- rpc: TypedRPC
298
299
  channel: [IsChannel] extends [null]
299
300
  ? PikkuChannel<unknown, Out>
300
301
  : PikkuChannel<unknown, Out> | undefined
@@ -333,6 +334,11 @@ export type PikkuWire<
333
334
  }
334
335
  }>
335
336
 
337
+ /**
338
+ * Wire object as constructed by runners, before rpc is lazily added by the function runner.
339
+ */
340
+ export type PikkuRawWire = Omit<PikkuWire, 'rpc'>
341
+
336
342
  /**
337
343
  * A function that can wrap an wire and be called before or after
338
344
  */
@@ -341,7 +347,7 @@ export type CorePikkuMiddleware<
341
347
  UserSession extends CoreUserSession = CoreUserSession,
342
348
  > = (
343
349
  services: SingletonServices,
344
- wires: PikkuWire<unknown, unknown, false, UserSession>,
350
+ wires: PikkuWire,
345
351
  next: () => Promise<void>
346
352
  ) => Promise<void>
347
353
 
@@ -527,7 +533,7 @@ export type CreateWireServices<
527
533
  UserSession extends CoreUserSession = CoreUserSession,
528
534
  > = (
529
535
  services: SingletonServices,
530
- wire: PikkuWire<unknown, unknown, false, UserSession>
536
+ wire: PikkuRawWire
531
537
  ) => Promise<WireServices<Services, SingletonServices>>
532
538
 
533
539
  /**
@@ -538,6 +544,16 @@ export type CreateConfig<
538
544
  RemainingArgs extends any[] = unknown[],
539
545
  > = (variables?: VariablesService, ...args: RemainingArgs) => Promise<Config>
540
546
 
547
+ /** Server lifecycle hooks for startup and shutdown phases. */
548
+ export type ServerLifecycle<
549
+ SingletonServices extends CoreSingletonServices = CoreSingletonServices,
550
+ > = {
551
+ beforeStart?: (services: SingletonServices) => void | Promise<void>
552
+ afterStart?: (services: SingletonServices) => void | Promise<void>
553
+ beforeStop?: (services: SingletonServices) => void | Promise<void>
554
+ afterStop?: (services: SingletonServices) => void | Promise<void>
555
+ }
556
+
541
557
  /**
542
558
  * Represents the documentation for a route, including summary, description, tags, and errors.
543
559
  */
package/src/utils.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Logger } from './services/logger.js'
2
- import type { WireServices } from './types/core.types.js'
2
+ import type {
3
+ CoreSingletonServices,
4
+ ServerLifecycle,
5
+ WireServices,
6
+ } from './types/core.types.js'
3
7
  import { getSingletonServices, getAllPackageStates } from './pikku-state.js'
4
8
 
5
9
  export const closeWireServices = async (
@@ -127,3 +131,10 @@ export const stopSingletonServices = async (): Promise<void> => {
127
131
  await stopService(logger, name, service)
128
132
  }
129
133
  }
134
+
135
+ /** Wrap server lifecycle hooks so the inspector can discover them. */
136
+ export const pikkuServerLifecycle = <
137
+ SS extends CoreSingletonServices = CoreSingletonServices,
138
+ >(
139
+ lifecycle: ServerLifecycle<SS>
140
+ ): ServerLifecycle<SS> => lifecycle
@@ -1,4 +1,4 @@
1
- import type { PikkuWire, CoreUserSession } from '../../types/core.types.js'
1
+ import type { PikkuRawWire, CoreUserSession } from '../../types/core.types.js'
2
2
  import type {
3
3
  CoreAIAgent,
4
4
  AIAgentInput,
@@ -397,7 +397,7 @@ export async function buildToolDefs(
397
397
  needsApproval: needsApproval || undefined,
398
398
  approvalDescriptionFn,
399
399
  execute: async (toolInput: unknown) => {
400
- const wire: PikkuWire = params.sessionService
400
+ const wire: PikkuRawWire = params.sessionService
401
401
  ? { ...createMiddlewareSessionWireProps(params.sessionService) }
402
402
  : {}
403
403
  const rpcService = new ContextAwareRPCService(
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CoreSingletonServices,
3
3
  CorePikkuMiddleware,
4
+ PikkuRawWire,
4
5
  PikkuWire,
5
6
  MiddlewareMetadata,
6
7
  } from '../../types/core.types.js'
@@ -73,7 +74,7 @@ export const runChannelLifecycleWithMiddleware = async ({
73
74
  }
74
75
  )
75
76
 
76
- let wire: PikkuWire = { channel }
77
+ let wire: PikkuRawWire = { channel }
77
78
  if (allChannelMiddleware.length > 0) {
78
79
  wire = wrapChannelWithMiddleware(wire, services, allChannelMiddleware)
79
80
  }
@@ -90,7 +91,12 @@ export const runChannelLifecycleWithMiddleware = async ({
90
91
  }
91
92
 
92
93
  if (allMiddleware.length > 0) {
93
- return await runMiddleware(services, wire, allMiddleware, runLifecycle)
94
+ return await runMiddleware(
95
+ services,
96
+ wire as unknown as PikkuWire,
97
+ allMiddleware,
98
+ runLifecycle
99
+ )
94
100
  } else {
95
101
  return await runLifecycle()
96
102
  }
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  CoreSingletonServices,
3
3
  MiddlewareMetadata,
4
- PikkuWire,
4
+ PikkuRawWire,
5
5
  } from '../../types/core.types.js'
6
6
  import type { CorePikkuChannelMiddleware } from './channel.types.js'
7
7
  import { pikkuState } from '../../pikku-state.js'
@@ -87,10 +87,10 @@ export const combineChannelMiddleware = (
87
87
  }
88
88
 
89
89
  export function wrapChannelWithMiddleware<Out>(
90
- wire: PikkuWire,
90
+ wire: PikkuRawWire,
91
91
  services: CoreSingletonServices,
92
92
  middlewares: readonly CorePikkuChannelMiddleware[]
93
- ): PikkuWire {
93
+ ): PikkuRawWire {
94
94
  if (middlewares.length === 0 || !wire.channel) return wire
95
95
 
96
96
  const channel = wire.channel
@@ -4,7 +4,11 @@ import { closeWireServices } from '../../../utils.js'
4
4
  import { processMessageHandlers } from '../channel-handler.js'
5
5
  import type { CoreChannel, RunChannelOptions } from '../channel.types.js'
6
6
  import { PikkuLocalChannelHandler } from './local-channel-handler.js'
7
- import type { PikkuWire, WireServices } from '../../../types/core.types.js'
7
+ import type {
8
+ PikkuRawWire,
9
+ PikkuWire,
10
+ WireServices,
11
+ } from '../../../types/core.types.js'
8
12
  import { isProduction } from '../../../env.js'
9
13
  import { getErrorResponse } from '../../../errors/error-handler.js'
10
14
  import { handleHTTPError } from '../../../handle-error.js'
@@ -46,7 +50,7 @@ const runUpgradeMiddleware = async (
46
50
  {
47
51
  http,
48
52
  ...createMiddlewareSessionWireProps(userSession),
49
- },
53
+ } as unknown as PikkuWire,
50
54
  middleware
51
55
  )
52
56
  }
@@ -123,7 +127,7 @@ export const runLocalChannel = async ({
123
127
  singletonServices.logger
124
128
  )
125
129
  const channel = channelHandler.getChannel()
126
- const wire: PikkuWire = {
130
+ const wire: PikkuRawWire = {
127
131
  channel,
128
132
  ...createMiddlewareSessionWireProps(userSession),
129
133
  }
@@ -69,9 +69,12 @@ export class LocalEventHubService<
69
69
  if (channelId === toChannelId) continue // Skip sending to the sender
70
70
  const channel = this.channels.get(toChannelId)
71
71
  if (channel) {
72
- channel.send(data, isBinary)
73
- } else {
74
- // TODO: Websocket is closed, remove the channel from the topic
72
+ try {
73
+ channel.send(data, isBinary)
74
+ } catch {
75
+ // Channel closed (e.g. SSE ReadableStream controller already closed on browser disconnect) — clean up
76
+ this.onChannelClosed(toChannelId)
77
+ }
75
78
  }
76
79
  }
77
80
  }
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  CoreUserSession,
3
- PikkuWire,
3
+ PikkuRawWire,
4
4
  WireServices,
5
5
  } from '../../../types/core.types.js'
6
6
  import { closeWireServices } from '../../../utils.js'
@@ -120,7 +120,7 @@ export const runChannelConnect = async ({
120
120
  channel.getState = () => channelStore.getState(channelId) as any
121
121
  channel.clearState = () => channelStore.clearState(channelId)
122
122
 
123
- const wire: PikkuWire = {
123
+ const wire: PikkuRawWire = {
124
124
  channel,
125
125
  ...createMiddlewareSessionWireProps(userSession),
126
126
  }
@@ -216,7 +216,7 @@ export const runChannelDisconnect = async ({
216
216
  }
217
217
  }
218
218
 
219
- const wire: PikkuWire = {
219
+ const wire: PikkuRawWire = {
220
220
  channel,
221
221
  ...createMiddlewareSessionWireProps(userSession),
222
222
  }
@@ -288,7 +288,7 @@ export const runChannelMessage = async (
288
288
  }
289
289
  }
290
290
 
291
- const wire: PikkuWire = {
291
+ const wire: PikkuRawWire = {
292
292
  channel,
293
293
  ...createMiddlewareSessionWireProps(userSession),
294
294
  }
@@ -19,7 +19,7 @@ import type {
19
19
  CoreSingletonServices,
20
20
  CoreServices,
21
21
  CreateWireServices,
22
- PikkuWire,
22
+ PikkuRawWire,
23
23
  CreateConfig,
24
24
  CreateSingletonServices,
25
25
  } from '../../types/core.types.js'
@@ -356,7 +356,7 @@ export async function runCLICommand({
356
356
  singletonServices.sessionStore
357
357
  )
358
358
 
359
- const wire: PikkuWire = {
359
+ const wire: PikkuRawWire = {
360
360
  cli: {
361
361
  program,
362
362
  command: commandPath,
@@ -10,6 +10,7 @@ import type {
10
10
  } from './gateway.types.js'
11
11
  import type {
12
12
  PikkuWire,
13
+ PikkuRawWire,
13
14
  CorePikkuMiddleware,
14
15
  CoreSingletonServices,
15
16
  } from '../../types/core.types.js'
@@ -381,7 +382,7 @@ export const createListenerMessageHandler = (
381
382
  const parsed = adapter.parse(rawData)
382
383
  if (!parsed) return
383
384
 
384
- const wire: PikkuWire = {}
385
+ const wire: PikkuRawWire = {}
385
386
  const gateway: PikkuGateway = {
386
387
  gatewayName: name,
387
388
  senderId: parsed.senderId,
@@ -403,7 +404,12 @@ export const createListenerMessageHandler = (
403
404
  }
404
405
 
405
406
  if (allMiddleware.length > 0) {
406
- await runMiddleware(singletonServices, wire, allMiddleware, exec)
407
+ await runMiddleware(
408
+ singletonServices,
409
+ wire as unknown as PikkuWire,
410
+ allMiddleware,
411
+ exec
412
+ )
407
413
  } else {
408
414
  await exec()
409
415
  }
@@ -18,6 +18,7 @@ import type {
18
18
  CorePikkuMiddleware,
19
19
  CorePikkuMiddlewareGroup,
20
20
  WireServices,
21
+ PikkuRawWire,
21
22
  PikkuWire,
22
23
  PikkuWiringTypes,
23
24
  } from '../../types/core.types.js'
@@ -408,7 +409,7 @@ const executeRoute = async (
408
409
  }
409
410
  }
410
411
 
411
- const wire: PikkuWire = {
412
+ const wire: PikkuRawWire = {
412
413
  traceId: requestId,
413
414
  http,
414
415
  channel,
@@ -586,7 +587,7 @@ export const fetchData = async <In, Out>(
586
587
  if (apiType.toLowerCase() === 'options') {
587
588
  const httpGroups = pikkuState(null, 'middleware', 'httpGroup')
588
589
  const globalMiddleware = httpGroups['*']
589
- const wire: PikkuWire = { http: http! }
590
+ const wire = { http: http! } as unknown as PikkuWire
590
591
  if (globalMiddleware) {
591
592
  await runMiddleware(
592
593
  singletonServices,
@@ -1,4 +1,4 @@
1
- import type { PikkuWire } from '../../types/core.types.js'
1
+ import type { PikkuRawWire } from '../../types/core.types.js'
2
2
  import type {
3
3
  CoreMCPResource,
4
4
  CoreMCPPrompt,
@@ -217,7 +217,7 @@ async function runMCPPikkuFunc(
217
217
  const mcpSessionService = new PikkuSessionService(
218
218
  singletonServices.sessionStore
219
219
  )
220
- const wire: PikkuWire = {
220
+ const wire: PikkuRawWire = {
221
221
  mcp: mcpWire,
222
222
  ...createMiddlewareSessionWireProps(mcpSessionService),
223
223
  }
@@ -1,4 +1,4 @@
1
- import type { PikkuWire } from '../../types/core.types.js'
1
+ import type { PikkuRawWire } from '../../types/core.types.js'
2
2
  import type { CoreQueueWorker, QueueJob, PikkuQueue } from './queue.types.js'
3
3
  import type {
4
4
  CorePikkuFunctionConfig,
@@ -156,7 +156,7 @@ export async function runQueueJob({
156
156
  try {
157
157
  logger.info(`Processing job ${job.id} in queue ${job.queueName}`)
158
158
 
159
- const wire: PikkuWire = {
159
+ const wire: PikkuRawWire = {
160
160
  traceId: resolvedTraceId,
161
161
  queue,
162
162
  }
@@ -1,4 +1,8 @@
1
- import type { CoreServices, PikkuWire } from '../../types/core.types.js'
1
+ import type {
2
+ CoreServices,
3
+ PikkuWire,
4
+ PikkuRawWire,
5
+ } from '../../types/core.types.js'
2
6
  import type { SessionService } from '../../services/user-session-service.js'
3
7
  import type { CoreUserSession } from '../../types/core.types.js'
4
8
  import { runPikkuFunc } from '../../function/function-runner.js'
@@ -100,7 +104,7 @@ const resolvePikkuFunction = (
100
104
  export class ContextAwareRPCService {
101
105
  constructor(
102
106
  private services: CoreServices,
103
- private wire: PikkuWire,
107
+ private wire: PikkuRawWire,
104
108
  private options: {
105
109
  requiresAuth?: boolean
106
110
  sessionService?: SessionService<CoreUserSession>
@@ -136,16 +140,8 @@ export class ContextAwareRPCService {
136
140
  funcName: string,
137
141
  data: In
138
142
  ): Promise<Out> {
139
- const rpcDepth = this.wire.rpc?.depth || 0
140
- const updatedWire: PikkuWire = {
143
+ const updatedWire: PikkuRawWire = {
141
144
  ...this.wire,
142
- rpc: this.wire.rpc
143
- ? {
144
- ...this.wire.rpc,
145
- depth: rpcDepth + 1,
146
- global: false,
147
- }
148
- : undefined,
149
145
  }
150
146
 
151
147
  // Check addon namespace first (e.g. 'stripe:createCharge')
@@ -209,7 +205,7 @@ export class ContextAwareRPCService {
209
205
  private async invokeAddonFunction<In = any, Out = any>(
210
206
  namespacedFunction: string,
211
207
  data: In,
212
- wire: PikkuWire
208
+ wire: PikkuRawWire
213
209
  ): Promise<Out> {
214
210
  // Resolve namespace to package name
215
211
  const resolved = resolveNamespace(namespacedFunction)
@@ -248,19 +244,11 @@ export class ContextAwareRPCService {
248
244
  public async rpcWithWire<In = any, Out = any>(
249
245
  rpcName: string,
250
246
  data: In,
251
- wire: PikkuWire
247
+ wire: PikkuRawWire
252
248
  ): Promise<Out> {
253
- const rpcDepth = this.wire.rpc?.depth || 0
254
- const mergedWire: PikkuWire = {
249
+ const mergedWire: PikkuRawWire = {
255
250
  ...this.wire,
256
251
  ...wire,
257
- rpc: this.wire.rpc
258
- ? {
259
- ...this.wire.rpc,
260
- depth: rpcDepth + 1,
261
- global: false,
262
- }
263
- : undefined,
264
252
  }
265
253
 
266
254
  if (rpcName.includes(':')) {
@@ -438,7 +426,7 @@ export class PikkuRPCService<
438
426
  // Convenience function for initializing
439
427
  getContextRPCService(
440
428
  services: Services,
441
- wire: PikkuWire,
429
+ wire: PikkuRawWire,
442
430
  requiresAuthOrOptions?:
443
431
  | boolean
444
432
  | {
@@ -456,7 +444,7 @@ export class PikkuRPCService<
456
444
  : { requiresAuth: requiresAuthOrOptions }
457
445
  const serviceRPC = new ContextAwareRPCService(
458
446
  services,
459
- wire,
447
+ wire as PikkuWire,
460
448
  options,
461
449
  packageName
462
450
  )
@@ -1,4 +1,4 @@
1
- import type { PikkuWire, CoreUserSession } from '../../types/core.types.js'
1
+ import type { PikkuRawWire, CoreUserSession } from '../../types/core.types.js'
2
2
  import type { CoreScheduledTask } from './scheduler.types.js'
3
3
  import { getErrorResponse, PikkuError } from '../../errors/error-handler.js'
4
4
  import {
@@ -97,7 +97,7 @@ export async function runScheduledTask({
97
97
  }
98
98
 
99
99
  // Create the scheduled task wire object
100
- const wire: PikkuWire = {
100
+ const wire: PikkuRawWire = {
101
101
  traceId: resolvedTraceId,
102
102
  scheduledTask: {
103
103
  name,
@@ -1,4 +1,4 @@
1
- import type { PikkuWire } from '../../types/core.types.js'
1
+ import type { PikkuRawWire } from '../../types/core.types.js'
2
2
  import type {
3
3
  CoreTrigger,
4
4
  CoreTriggerSource,
@@ -99,7 +99,7 @@ export async function setupTrigger<TInput = unknown, TOutput = unknown>({
99
99
  )
100
100
  }
101
101
 
102
- const wire: PikkuWire = {
102
+ const wire: PikkuRawWire = {
103
103
  trigger: {
104
104
  invoke: (data: unknown) => {
105
105
  singletonServices.logger.info(`Trigger fired: ${name}`)
@@ -21,6 +21,7 @@ export type {
21
21
  InlineStepMeta,
22
22
  SleepStepMeta,
23
23
  CancelStepMeta,
24
+ SuspendStepMeta,
24
25
  SwitchCaseMeta,
25
26
  SwitchStepMeta,
26
27
  FilterStepMeta,
@@ -239,6 +239,15 @@ export interface SwitchStepMeta {
239
239
  defaultSteps?: WorkflowStepMeta[]
240
240
  }
241
241
 
242
+ /**
243
+ * Suspend step metadata (workflow.suspend())
244
+ */
245
+ export interface SuspendStepMeta {
246
+ type: 'suspend'
247
+ /** Reason string passed to workflow.suspend() — becomes the durable step key */
248
+ reason: string
249
+ }
250
+
242
251
  /**
243
252
  * Filter step metadata (array.filter)
244
253
  */
@@ -283,6 +292,7 @@ export type WorkflowStepMeta =
283
292
  | InlineStepMeta
284
293
  | SleepStepMeta
285
294
  | CancelStepMeta
295
+ | SuspendStepMeta
286
296
  | SwitchStepMeta
287
297
  | FilterStepMeta
288
298
  | ArrayPredicateStepMeta
@@ -101,6 +101,7 @@ export type {
101
101
  InlineStepMeta,
102
102
  SleepStepMeta,
103
103
  CancelStepMeta,
104
+ SuspendStepMeta,
104
105
  SetStepMeta,
105
106
  SwitchCaseMeta,
106
107
  SwitchStepMeta,
@@ -10,10 +10,11 @@ import {
10
10
  } from './pikku-workflow-service.js'
11
11
 
12
12
  describe('pikku-workflow-service worker registration', () => {
13
- test('registers sleeper function metadata', () => {
13
+ test('registers sleeper function metadata after wireQueueWorkers', () => {
14
14
  resetPikkuState()
15
15
 
16
- new InMemoryWorkflowService()
16
+ const ws = new InMemoryWorkflowService()
17
+ ws.wireQueueWorkers()
17
18
 
18
19
  const functionMeta = pikkuState(null, 'function', 'meta')
19
20
  assert.deepEqual(functionMeta.pikkuWorkflowSleeper, {
@@ -144,7 +144,10 @@ export class WorkflowDispatchException extends Error {
144
144
  public readonly stepName: string,
145
145
  options?: { cause?: unknown }
146
146
  ) {
147
- super(`Failed to dispatch workflow step '${stepName}' (run ${runId})`, options)
147
+ super(
148
+ `Failed to dispatch workflow step '${stepName}' (run ${runId})`,
149
+ options
150
+ )
148
151
  this.name = 'WorkflowDispatchException'
149
152
  }
150
153
  }
@@ -184,6 +187,16 @@ addError(WorkflowRunFailedError, {
184
187
  message: 'Workflow run failed.',
185
188
  })
186
189
 
190
+ export class WorkflowRunCancelledError extends PikkuError {
191
+ constructor() {
192
+ super('Workflow was cancelled')
193
+ }
194
+ }
195
+ addError(WorkflowRunCancelledError, {
196
+ status: 409,
197
+ message: 'Workflow was cancelled.',
198
+ })
199
+
187
200
  export class WorkflowServiceNotInitialized extends Error {}
188
201
  export class WorkflowStepNameNotString extends Error {
189
202
  constructor(stepName: any) {
@@ -237,16 +250,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
237
250
  }
238
251
  }
239
252
 
240
- public rewireQueueWorkers(): void {
241
- this.wireQueueWorkers()
242
- }
243
-
244
253
  /**
245
254
  * Wire the queue-based orchestrator/step/sleeper workers.
246
255
  * Subclasses that orchestrate without queues (e.g. Durable Objects) should
247
256
  * pass `wireQueues: false` to the base constructor and skip this entirely.
257
+ * Call this explicitly after adding addons dynamically.
248
258
  */
249
- protected wireQueueWorkers(): void {
259
+ public wireQueueWorkers(): void {
250
260
  const functions = pikkuState(null, 'function', 'functions')
251
261
  const functionsMeta = pikkuState(null, 'function', 'meta')
252
262
 
@@ -426,7 +436,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
426
436
  // Build step summaries from history (latest attempt per step)
427
437
  const stepMap = new Map<
428
438
  string,
429
- { status: StepStatus; startedAt?: Date; completedAt?: Date; attempts: number }
439
+ {
440
+ status: StepStatus
441
+ startedAt?: Date
442
+ completedAt?: Date
443
+ attempts: number
444
+ }
430
445
  >()
431
446
  for (const step of history) {
432
447
  const existing = stepMap.get(step.stepName)
@@ -920,7 +935,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
920
935
  const queueService = this.verifyQueueService()
921
936
  await queueService.add(
922
937
  this.getStepWorkerQueueName(rpcName),
923
- JSON.parse(JSON.stringify({ runId, stepName, rpcName, data, fromStepName })),
938
+ JSON.parse(
939
+ JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
940
+ ),
924
941
  this.resolveStepJobOptions(stepOptions)
925
942
  )
926
943
  }
@@ -1185,7 +1202,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1185
1202
  throw new WorkflowRunFailedError(run.error?.message)
1186
1203
  }
1187
1204
  if (run.status === 'cancelled') {
1188
- throw new Error('Workflow was cancelled')
1205
+ throw new WorkflowRunCancelledError()
1189
1206
  }
1190
1207
  return run.output
1191
1208
  }
@@ -1837,7 +1854,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1837
1854
  }
1838
1855
  if (WORKFLOW_END_STATES.has(childRun.status)) {
1839
1856
  if (childRun.status === 'failed') {
1840
- throw new Error(childRun.error?.message || 'Sub-workflow failed')
1857
+ throw new Error(
1858
+ childRun.error?.message || 'Sub-workflow failed'
1859
+ )
1841
1860
  }
1842
1861
  if (childRun.status === 'cancelled') {
1843
1862
  throw new Error('Sub-workflow was cancelled')
@@ -24,6 +24,7 @@ export type {
24
24
  InlineStepMeta,
25
25
  SleepStepMeta,
26
26
  CancelStepMeta,
27
+ SuspendStepMeta,
27
28
  SetStepMeta,
28
29
  SwitchCaseMeta,
29
30
  SwitchStepMeta,
@@ -54,8 +55,10 @@ export interface WorkflowServiceConfig {
54
55
  }
55
56
 
56
57
  export interface WorkflowPlannedStep {
57
- /** Human-readable step label for UI timeline */
58
+ /** Durable step key matches the runtime step name stored in the DB */
58
59
  stepName: string
60
+ /** Optional human-readable label for the UI timeline (falls back to stepName) */
61
+ displayName?: string
59
62
  }
60
63
 
61
64
  /**