@pikku/core 0.12.78 → 0.12.80

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 (341) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -1,53 +1,23 @@
1
- import { runPikkuFunc, addFunction } from '../../function/function-runner.js'
2
- import {
3
- pikkuWorkflowWorkerFunc,
4
- pikkuWorkflowOrchestratorFunc,
5
- pikkuWorkflowSleeperFunc,
6
- } from './workflow-queue-workers.js'
7
- import { wireQueueWorker } from '../queue/queue-runner.js'
1
+ import { runPikkuFunc } from '../../function/function-runner.js'
8
2
  import {
9
3
  getSingletonServices,
10
4
  getCreateWireServices,
11
5
  pikkuState,
12
6
  } from '../../pikku-state.js'
13
7
  import { getDurationInMilliseconds } from '../../time-utils.js'
14
-
15
- const resolveWorkflowMeta = (
16
- name: string
17
- ): { meta: any; packageName: string | null; resolvedName: string } | null => {
18
- const rootMeta = pikkuState(null, 'workflows', 'meta')
19
- if (rootMeta[name]) {
20
- return { meta: rootMeta[name], packageName: null, resolvedName: name }
21
- }
22
-
23
- const colonIndex = name.indexOf(':')
24
- if (colonIndex !== -1) {
25
- const namespace = name.substring(0, colonIndex)
26
- const localName = name.substring(colonIndex + 1)
27
- const addons = pikkuState(null, 'addons', 'packages')
28
- const pkgConfig = addons?.get(namespace)
29
- if (pkgConfig) {
30
- const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta')
31
- if (addonMeta?.[localName]) {
32
- return {
33
- meta: addonMeta[localName],
34
- packageName: pkgConfig.package,
35
- resolvedName: localName,
36
- }
37
- }
38
- }
39
- }
40
-
41
- return null
42
- }
43
-
44
- const toKebab = (s: string) =>
45
- s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
46
- import type { PikkuWire, SerializedError } from '../../types/core.types.js'
47
- import type { QueueService } from '../queue/queue.types.js'
8
+ import type {
9
+ CoreUserSession,
10
+ PikkuRawWire,
11
+ SerializedError,
12
+ } from '../../types/core.types.js'
13
+ import type {
14
+ GroupConcurrencyConfig,
15
+ JobGroup,
16
+ JobOptions,
17
+ QueueService,
18
+ } from '../queue/queue.types.js'
48
19
  import type {
49
20
  ApprovalOutcome,
50
- CoreWorkflow,
51
21
  PikkuWorkflowWire,
52
22
  StepState,
53
23
  StepStatus,
@@ -64,6 +34,7 @@ import type {
64
34
  WorkflowStepOptions,
65
35
  } from './workflow.types.js'
66
36
  import {
37
+ ChildWorkflowStartedException,
67
38
  continueGraph,
68
39
  executeGraphStep,
69
40
  runWorkflowGraph,
@@ -71,232 +42,65 @@ import {
71
42
  } from './graph/graph-runner.js'
72
43
  import type { WorkflowService } from '../../services/workflow-service.js'
73
44
  import type { ScenarioPersonas } from '../../services/personas-service.js'
74
- import {
75
- PikkuError,
76
- addError,
77
- isExpectedError,
78
- } from '../../errors/error-handler.js'
45
+ import { isExpectedError } from '../../errors/error-handler.js'
46
+ import { PikkuMissingMetaError } from '../../errors/errors.js'
79
47
  import { RPCNotFoundError } from '../rpc/rpc-runner.js'
80
- import { ChildWorkflowStartedException } from './graph/graph-runner.js'
48
+ import type { PikkuRPC } from '../rpc/rpc-types.js'
81
49
  import { deriveInvocationId } from './workflow-invocation-id.js'
50
+ import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
82
51
  import {
83
52
  buildRunTimeline,
84
53
  reconstructStateAt,
85
54
  type RunTimeline,
86
55
  type ReconstructedRunState,
87
56
  } from './run-timeline.js'
57
+ import {
58
+ DEFAULT_STEP_RETRIES,
59
+ WORKFLOW_CHILD_POLL_MAX_MS,
60
+ WORKFLOW_END_STATES,
61
+ WORKFLOW_POLL_FACTOR,
62
+ WORKFLOW_POLL_MIN_MS,
63
+ WORKFLOW_TERMINAL_STATES,
64
+ } from './workflow-constants.js'
65
+ import {
66
+ WorkflowAsyncException,
67
+ WorkflowCancelledException,
68
+ WorkflowDispatchException,
69
+ WorkflowNotFoundError,
70
+ WorkflowRunCancelledError,
71
+ WorkflowRunFailedError,
72
+ WorkflowRunNotFoundError,
73
+ WorkflowStepFunctionMismatchError,
74
+ WorkflowStepNameNotString,
75
+ WorkflowSuspendedException,
76
+ } from './workflow-errors.js'
88
77
  import type {
89
- GroupConcurrencyConfig,
90
- JobGroup,
91
- JobOptions,
92
- PikkuWorkerConfig,
93
- } from '../queue/queue.types.js'
94
-
95
- export const DEFAULT_STEP_RETRIES = 5
96
-
97
- export class WorkflowAsyncException extends Error {
98
- constructor(
99
- public readonly runId: string,
100
- public readonly stepName: string
101
- ) {
102
- super(`Workflow paused at step: ${stepName}`)
103
- this.name = 'WorkflowAsyncException'
104
- }
105
- }
106
-
107
- export class WorkflowCancelledException extends Error {
108
- constructor(
109
- public readonly runId: string,
110
- public readonly reason?: string
111
- ) {
112
- super(reason || 'Workflow cancelled')
113
- this.name = 'WorkflowCancelledException'
114
- }
115
- }
116
-
117
- export class WorkflowSuspendedException extends Error {
118
- constructor(
119
- public readonly runId: string,
120
- public readonly reason: string
121
- ) {
122
- super(reason || 'Workflow suspended')
123
- this.name = 'WorkflowSuspendedException'
124
- }
125
- }
126
-
127
- export class WorkflowDispatchException extends Error {
128
- constructor(
129
- public readonly runId: string,
130
- public readonly stepName: string,
131
- options?: { cause?: unknown }
132
- ) {
133
- super(
134
- `Failed to dispatch workflow step '${stepName}' (run ${runId})`,
135
- options
136
- )
137
- this.name = 'WorkflowDispatchException'
138
- }
139
- }
140
-
141
- export class WorkflowNotFoundError extends PikkuError {
142
- constructor(name: string) {
143
- super(`Workflow not found: ${name}`)
144
- }
145
- }
146
- addError(WorkflowNotFoundError, {
147
- status: 404,
148
- message: 'Workflow not found.',
149
- })
150
-
151
- export class WorkflowRunNotFoundError extends PikkuError {
152
- constructor(runId: string) {
153
- super(`Workflow run not found: ${runId}`)
154
- }
155
- }
156
- addError(WorkflowRunNotFoundError, {
157
- status: 404,
158
- message: 'Workflow run not found.',
159
- })
160
-
161
- export class WorkflowRunFailedError extends PikkuError {
162
- public payload: { message?: string }
163
- constructor(message?: string) {
164
- super(`Workflow run failed: ${message ?? 'unknown'}`)
165
- this.payload = { message }
166
- }
167
- }
168
- addError(WorkflowRunFailedError, {
169
- status: 422,
170
- message: 'Workflow run failed.',
171
- })
172
-
173
- export class WorkflowRunCancelledError extends PikkuError {
174
- constructor() {
175
- super('Workflow was cancelled')
176
- }
177
- }
178
- addError(WorkflowRunCancelledError, {
179
- status: 409,
180
- message: 'Workflow was cancelled.',
181
- })
182
-
183
- export class WorkflowApprovalResolvedError extends PikkuError {
184
- public payload: {
185
- reason: string
186
- outcome: ApprovalOutcome<unknown>['status']
187
- }
188
- constructor(reason: string, outcome: ApprovalOutcome<unknown>['status']) {
189
- super(`Approval already ${outcome}: ${reason}`)
190
- this.payload = { reason, outcome }
191
- }
192
- }
193
- addError(WorkflowApprovalResolvedError, {
194
- status: 409,
195
- message: 'Approval has already been resolved.',
196
- })
197
-
198
- export class WorkflowServiceNotInitialized extends Error {}
199
- export class WorkflowStepNameNotString extends Error {
200
- constructor(stepName: any) {
201
- super(`Workflow step name must be a string. Received: ${typeof stepName}`)
202
- }
203
- }
204
-
205
- const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
206
- 'completed',
207
- 'failed',
208
- 'cancelled',
209
- 'suspended',
210
- ])
211
-
212
- export interface RunLifecycleContext {
213
- runId: string
214
- run: WorkflowRun
215
- workflowMeta: any
216
- workflow: CoreWorkflow
217
- wire: PikkuWire
218
- packageName: string | null
219
- }
220
-
221
- export interface WorkflowRunEngine {
222
- inlineStep(
223
- runId: string,
224
- logicalStepName: string,
225
- fn: Function,
226
- stepOptions?: WorkflowStepOptions,
227
- data?: any,
228
- funcName?: string
229
- ): Promise<any>
230
- updateRunStatus(
231
- runId: string,
232
- status: WorkflowStatus,
233
- output?: any,
234
- error?: SerializedError
235
- ): Promise<void>
236
- onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
237
- verifyStepName(stepName: unknown): void
238
- }
239
-
240
- export interface WorkflowRunExtension {
241
- attachRunContext(
242
- runId: string,
243
- workflowMeta: any,
244
- options?: Record<string, any>
245
- ): Promise<void>
246
- detachRunContext(runId: string): void
247
- decorateRunWire(
248
- wire: PikkuWire,
249
- context: {
250
- runId: string
251
- workflowMeta: any
252
- workflowWire: PikkuWorkflowWire
253
- }
254
- ): void
255
- decorateWorkflowWire(
256
- workflowWire: PikkuWorkflowWire,
257
- context: {
258
- name: string
259
- runId: string
260
- rpcService: any
261
- addonNamespace?: string | null
262
- }
263
- ): void
264
- onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
265
- onAfterRunFunc(
266
- context: RunLifecycleContext,
267
- outcome: 'completed' | 'failed' | 'interrupted',
268
- failure: unknown
269
- ): Promise<void>
270
- }
271
-
272
- const WORKFLOW_TERMINAL_STATES: ReadonlySet<string> = new Set([
273
- 'completed',
274
- 'failed',
275
- 'cancelled',
276
- ])
277
-
278
- /** Idle window before a `running` run with nothing in flight is treated as stalled. */
279
- const DEFAULT_STALLED_RUN_MS = 5 * 60_000
280
-
281
- /** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
282
- const DEFAULT_STALLED_RUN_LIMIT = 100
283
-
284
- const WORKFLOW_POLL_MIN_MS = 10
285
-
286
- const WORKFLOW_POLL_FACTOR = 1.6
287
-
288
- const WORKFLOW_CHILD_POLL_MAX_MS = 500
289
-
290
- type RunContext = {
291
- activeExecutions: number
292
- inline?: boolean
293
- ordinals: Map<string, number>
294
- lastStep?: string
295
- replay?: {
296
- steps?: Map<string, StepState>
297
- run?: WorkflowRun
298
- }
299
- }
78
+ RunContext,
79
+ RunLifecycleContext,
80
+ WorkflowRunEngine,
81
+ WorkflowRunExtension,
82
+ } from './workflow-run-engine.types.js'
83
+ import { resolveWorkflowMeta } from './workflow-meta-resolver.js'
84
+ import {
85
+ jobGroupFor,
86
+ orchestratorQueueName,
87
+ resolveWorkflowConfig,
88
+ stepJobOptions,
89
+ stepWorkerQueueName,
90
+ } from './workflow-queue-routing.js'
91
+ import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js'
92
+ import {
93
+ approvalStepNameFor,
94
+ evaluateApprovalStep,
95
+ recordApprovalDecision,
96
+ type ApprovalStore,
97
+ } from './workflow-approval.js'
98
+ import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
99
+ import {
100
+ RedispatchBackoff,
101
+ sweepStalledRuns,
102
+ sweepUndispatchedSteps,
103
+ } from './workflow-recovery.js'
300
104
 
301
105
  export abstract class PikkuWorkflowService implements WorkflowService {
302
106
  private runExtension?: WorkflowRunExtension
@@ -387,112 +191,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
387
191
  }
388
192
 
389
193
  public wireQueueWorkers(): void {
390
- const functions = pikkuState(null, 'function', 'functions')
391
- const functionsMeta = pikkuState(null, 'function', 'meta')
392
-
393
- const mkMeta = (funcId: string) => ({
394
- pikkuFuncId: funcId,
395
- sessionless: true,
396
- functionType: 'helper' as const,
397
- inputSchemaName: null,
398
- outputSchemaName: null,
194
+ wireWorkflowQueueWorkers({
195
+ strategy: this.queueStrategy,
196
+ concurrency: this.queueConcurrency,
197
+ groupConcurrency: this.queueGroupConcurrency,
198
+ getLogger: () => this.logger,
399
199
  })
400
-
401
- const queueMeta = pikkuState(null, 'queue', 'meta')
402
-
403
- const registerWorkflowFunc = (
404
- funcId: string,
405
- func: { func: unknown },
406
- queueName: string,
407
- config?: PikkuWorkerConfig
408
- ) => {
409
- if (functions.has(funcId)) return
410
- addFunction(funcId, func as never)
411
- if (!queueMeta[queueName]) {
412
- queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
413
- }
414
- wireQueueWorker({ name: queueName, func, config } as never)
415
- if (!functionsMeta[funcId]) {
416
- functionsMeta[funcId] = mkMeta(funcId)
417
- }
418
- }
419
-
420
- const sharedGroups = this.queueStrategy === 'shared-groups'
421
- const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
422
- ? {
423
- batchSize: this.queueConcurrency,
424
- groupConcurrency: this.queueGroupConcurrency,
425
- }
426
- : undefined
427
-
428
- registerWorkflowFunc(
429
- 'pikkuWorkflowOrchestrator',
430
- { func: pikkuWorkflowOrchestratorFunc },
431
- 'pikku-workflow-orchestrator',
432
- sharedQueueConfig
433
- )
434
- registerWorkflowFunc(
435
- 'pikkuWorkflowStepWorker',
436
- { func: pikkuWorkflowWorkerFunc },
437
- 'pikku-workflow-step-worker',
438
- sharedQueueConfig
439
- )
440
-
441
- const registerQueueWorkers = (queueMeta: Record<string, any>) => {
442
- for (const [queueName, meta] of Object.entries(queueMeta)) {
443
- if (functions.has(meta.pikkuFuncId)) continue
444
- if (queueName.startsWith('wf-orchestrator-')) {
445
- registerWorkflowFunc(
446
- meta.pikkuFuncId,
447
- { func: pikkuWorkflowOrchestratorFunc },
448
- queueName
449
- )
450
- } else if (queueName.startsWith('wf-step-')) {
451
- registerWorkflowFunc(
452
- meta.pikkuFuncId,
453
- { func: pikkuWorkflowWorkerFunc },
454
- queueName
455
- )
456
- }
457
- }
458
- }
459
-
460
- if (!sharedGroups) {
461
- registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
462
-
463
- const addons = pikkuState(null, 'addons', 'packages')
464
- if (addons) {
465
- for (const [, addon] of addons) {
466
- const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
467
- if (addonQueueMeta) {
468
- registerQueueWorkers(addonQueueMeta)
469
- }
470
- }
471
- }
472
- }
473
-
474
- const workflowCount = Object.keys(
475
- pikkuState(null, 'workflows', 'meta') ?? {}
476
- ).length
477
- const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
478
- name.startsWith('wf-orchestrator-')
479
- ).length
480
- if (workflowCount > 0 && perWorkflowQueues === 0) {
481
- this.logger?.warn?.(
482
- `[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
483
- `All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
484
- `Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
485
- )
486
- }
487
-
488
- if (!functions.has('pikkuWorkflowSleeper')) {
489
- addFunction('pikkuWorkflowSleeper', {
490
- func: pikkuWorkflowSleeperFunc,
491
- })
492
- }
493
- if (!functionsMeta.pikkuWorkflowSleeper) {
494
- functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper')
495
- }
496
200
  }
497
201
 
498
202
  protected async isInline(runId: string): Promise<boolean> {
@@ -755,7 +459,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
755
459
  const serialized: SerializedError = {
756
460
  message: error.message,
757
461
  stack: error.stack,
758
- code: (error as any).code,
462
+ code: (error as { code?: string }).code,
759
463
  expected: isExpectedError(error),
760
464
  }
761
465
  return mirror.setStepError(stepId, serialized)
@@ -777,7 +481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
777
481
  (mirror, newStep) =>
778
482
  mirror.createRetryAttempt(failedStepId, {
779
483
  ...newStep,
780
- stepName: (newStep as any).stepName ?? '',
484
+ stepName: (newStep as { stepName?: string }).stepName ?? '',
781
485
  })
782
486
  )
783
487
  }
@@ -931,62 +635,70 @@ export abstract class PikkuWorkflowService implements WorkflowService {
931
635
  }
932
636
 
933
637
  /**
934
- * Re-drive runs whose next move was lost, and report which were resumed.
935
- *
936
- * Arming a step is two writes to two systems — the step row, then the queue
937
- * or scheduler job — so a process that dies between them leaves a run that is
938
- * `running` with nothing in flight. Nothing notices: the run parks on a step
939
- * that will never complete and never error, so it neither finishes nor fails.
940
- * (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
941
- * step's insert and its timer, parking the run permanently.)
942
- *
943
- * Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
944
- * step state, and every settled step is memoized, so resuming a run that was
945
- * not actually stuck costs an orchestration pass and changes nothing. That
946
- * idempotence is what makes an idle-time heuristic safe here; a run that is
947
- * legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
638
+ * Runs holding a step that has sat `pending` since before `before`, paired
639
+ * with the step that flagged them.
948
640
  *
949
- * This is not self-starting. Call it from a scheduled task at whatever
950
- * interval suits the workload.
641
+ * Returns nothing by default so a store that cannot express the query keeps
642
+ * working unchanged and, because it does not opt in, gains no re-dispatches
643
+ * either. A store must have an atomic `withStepLock` before overriding this,
644
+ * or no concurrency for one to exclude: the relay makes duplicate dispatch
645
+ * routine, and the claim in `executeWorkflowStepInner` is what keeps a
646
+ * duplicate from becoming a second execution. `kysely-postgres` and
647
+ * `kysely-mysql` qualify on the lock, `in-memory` on being inline and
648
+ * single-process; `mongodb` and `kysely-sqlite` qualify on neither.
649
+ */
650
+ protected async findUndispatchedSteps(
651
+ _before: Date,
652
+ _limit: number
653
+ ): Promise<Array<{ runId: string; stepId: string }>> {
654
+ return []
655
+ }
656
+
657
+ private readonly redispatchBackoff = new RedispatchBackoff()
658
+
659
+ private get sweepDeps() {
660
+ return {
661
+ resume: (runId: string) => this.resumeWorkflow(runId),
662
+ logger: getSingletonServices()?.logger,
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Re-drive runs whose next move was lost. Not self-starting — call it from a
668
+ * scheduled task at whatever interval suits the workload.
951
669
  */
952
670
  public async recoverStalledRuns(options?: {
953
671
  stalledAfterMs?: number
954
672
  limit?: number
955
673
  }): Promise<{ resumed: string[] }> {
956
- const before = new Date(
957
- Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS)
674
+ return sweepStalledRuns(
675
+ (before, limit) => this.findStalledRunIds(before, limit),
676
+ options,
677
+ this.sweepDeps
958
678
  )
959
- const runIds = await this.findStalledRunIds(
960
- before,
961
- options?.limit ?? DEFAULT_STALLED_RUN_LIMIT
679
+ }
680
+
681
+ /**
682
+ * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
683
+ * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
684
+ * well under that.
685
+ */
686
+ public async relayUndispatchedSteps(options?: {
687
+ undispatchedAfterMs?: number
688
+ limit?: number
689
+ }): Promise<{ redispatched: string[] }> {
690
+ return sweepUndispatchedSteps(
691
+ (before, limit) => this.findUndispatchedSteps(before, limit),
692
+ this.redispatchBackoff,
693
+ options,
694
+ this.sweepDeps
962
695
  )
963
- const resumed: string[] = []
964
- for (const runId of runIds) {
965
- try {
966
- await this.resumeWorkflow(runId)
967
- resumed.push(runId)
968
- } catch (err) {
969
- // One unresumable run must not stop the sweep from recovering the rest.
970
- getSingletonServices()?.logger?.error(
971
- `Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
972
- )
973
- }
974
- }
975
- return { resumed }
976
696
  }
977
697
 
978
698
  protected resolveStepJobOptions(
979
699
  stepOptions?: WorkflowStepOptions
980
700
  ): JobOptions {
981
- const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES
982
- const retryDelay = stepOptions?.retryDelay
983
- const backoff =
984
- retryDelay !== undefined && retryDelay !== 'exponential'
985
- ? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
986
- : retries > 0 || retryDelay === 'exponential'
987
- ? 'exponential'
988
- : undefined
989
- return { attempts: retries + 1, ...(backoff ? { backoff } : {}) }
701
+ return stepJobOptions(stepOptions)
990
702
  }
991
703
 
992
704
  public async queueStepWorker(
@@ -1115,7 +827,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1115
827
  name: string,
1116
828
  input: I,
1117
829
  wire: WorkflowRunWire,
1118
- rpcService: any,
830
+ rpcService: PikkuRPC,
1119
831
  options?: {
1120
832
  inline?: boolean
1121
833
  startNode?: string
@@ -1215,7 +927,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1215
927
  public async runToCompletion<I>(
1216
928
  name: string,
1217
929
  input: I,
1218
- rpcService: any,
930
+ rpcService: PikkuRPC,
1219
931
  options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
1220
932
  ): Promise<any> {
1221
933
  const { runId } = await this.startWorkflow(
@@ -1337,7 +1049,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1337
1049
  return stepName
1338
1050
  }
1339
1051
 
1340
- public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
1052
+ public async runWorkflowJob(
1053
+ runId: string,
1054
+ rpcService: PikkuRPC
1055
+ ): Promise<void> {
1341
1056
  await this.beginReplay(runId)
1342
1057
  try {
1343
1058
  await this.runWorkflowJobInner(runId, rpcService)
@@ -1348,7 +1063,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1348
1063
 
1349
1064
  private async runWorkflowJobInner(
1350
1065
  runId: string,
1351
- rpcService: any
1066
+ rpcService: PikkuRPC
1352
1067
  ): Promise<void> {
1353
1068
  const run = await this.getRunIdentity(runId)
1354
1069
  if (!run) {
@@ -1386,6 +1101,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1386
1101
  }
1387
1102
 
1388
1103
  const registrations = pikkuState(pkgName, 'workflows', 'registrations')
1104
+ if (!workflowMeta) {
1105
+ throw new PikkuMissingMetaError(
1106
+ `Missing generated metadata for workflow '${run.workflow}'`
1107
+ )
1108
+ }
1109
+
1389
1110
  const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
1390
1111
  if (!workflow) {
1391
1112
  throw new WorkflowNotFoundError(run.workflow)
@@ -1402,11 +1123,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1402
1123
  addonNs
1403
1124
  )
1404
1125
  workflowWire.pikkuUserId = run.wire?.pikkuUserId
1405
- const wire: PikkuWire = {
1126
+ // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
1127
+ const wire: PikkuRawWire = {
1406
1128
  workflow: workflowWire,
1407
1129
  pikkuUserId: run.wire?.pikkuUserId,
1408
- session: rpcService?.wire?.session,
1409
- rpc: rpcService?.wire?.rpc,
1410
1130
  }
1411
1131
  this.runExtension?.decorateRunWire(wire, {
1412
1132
  runId,
@@ -1518,7 +1238,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1518
1238
  private async runVersionMismatchFallback(
1519
1239
  run: WorkflowRun,
1520
1240
  currentMeta: { source: string },
1521
- rpcService: any
1241
+ rpcService: PikkuRPC
1522
1242
  ): Promise<void> {
1523
1243
  const source = currentMeta.source
1524
1244
 
@@ -1549,7 +1269,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1549
1269
  stepName: string,
1550
1270
  rpcName: string,
1551
1271
  data: any,
1552
- rpcService: any
1272
+ rpcService: PikkuRPC
1553
1273
  ): Promise<void> {
1554
1274
  this.enterExecution(runId)
1555
1275
  try {
@@ -1570,10 +1290,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1570
1290
  stepName: string,
1571
1291
  rpcName: string,
1572
1292
  data: any,
1573
- rpcService: any
1293
+ rpcService: PikkuRPC
1574
1294
  ): Promise<void> {
1575
1295
  const claimed = await this.withStepLock(runId, stepName, async () => {
1576
1296
  const stepState = await this.getStepState(runId, stepName)
1297
+ // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
1298
+ if (
1299
+ stepState.rpcName !== undefined &&
1300
+ stepState.rpcName !== (rpcName ?? null)
1301
+ ) {
1302
+ throw new WorkflowStepFunctionMismatchError(runId, stepName)
1303
+ }
1577
1304
  if (stepState.status === 'succeeded' || stepState.status === 'running') {
1578
1305
  return null
1579
1306
  }
@@ -1620,7 +1347,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1620
1347
  runId,
1621
1348
  stepState.stepId,
1622
1349
  graphNodeId,
1623
- rpcName,
1350
+ workflowMeta!.nodes![graphNodeId].rpcName,
1624
1351
  data,
1625
1352
  run.workflow
1626
1353
  )
@@ -1632,7 +1359,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1632
1359
  id: rpcName,
1633
1360
  parentRunId: runId,
1634
1361
  parentStepId: stepState.stepId,
1635
- pikkuUserId: rpcService.wire?.pikkuUserId,
1362
+ pikkuUserId: run.wire?.pikkuUserId,
1636
1363
  }
1637
1364
  const shouldInline = !getSingletonServices()?.queueService
1638
1365
  const { runId: childRunId } = await this.startWorkflow(
@@ -1707,7 +1434,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1707
1434
 
1708
1435
  public async orchestrateWorkflow(
1709
1436
  runId: string,
1710
- rpcService: any
1437
+ rpcService: PikkuRPC
1711
1438
  ): Promise<void> {
1712
1439
  try {
1713
1440
  await this.runWorkflowJob(runId, rpcService)
@@ -1754,7 +1481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1754
1481
  stepState: StepState,
1755
1482
  rpcName: string,
1756
1483
  data: any,
1757
- rpcService: any,
1484
+ rpcService: PikkuRPC,
1758
1485
  knownRun?: WorkflowRun | null
1759
1486
  ): Promise<any> {
1760
1487
  const run = knownRun ?? (await this.getRunIdentity(runId))
@@ -1812,7 +1539,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1812
1539
  runId: string,
1813
1540
  stepName: string,
1814
1541
  onErrorRpcName: string,
1815
- rpcService: any,
1542
+ rpcService: PikkuRPC,
1816
1543
  error: Error
1817
1544
  ): Promise<void> {
1818
1545
  await this.rpcStep(
@@ -1830,7 +1557,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1830
1557
  logicalStepName: string,
1831
1558
  rpcName: string,
1832
1559
  data: any,
1833
- rpcService: any,
1560
+ rpcService: PikkuRPC,
1834
1561
  stepOptions?: WorkflowStepOptions
1835
1562
  ): Promise<any> {
1836
1563
  const fromStepName = this.lastStepName(runId)
@@ -1912,7 +1639,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1912
1639
  type: 'workflow',
1913
1640
  id: rpcName,
1914
1641
  parentRunId: runId,
1915
- pikkuUserId: rpcService.wire?.pikkuUserId,
1642
+ pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1916
1643
  }
1917
1644
  const { runId: childRunId } = await this.startWorkflow(
1918
1645
  rpcName,
@@ -2051,59 +1778,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2051
1778
  await this.setStepResult(stepState.stepId, null)
2052
1779
  }
2053
1780
 
2054
- private getSuspendStepName(reason: string): string {
2055
- return `__workflow_suspend:${reason}`
2056
- }
2057
-
2058
1781
  private async suspendStep(runId: string, reason: string): Promise<void> {
2059
1782
  const fromStepName = this.lastStepName(runId)
2060
- const suspendStepName = this.nextStepKey(
2061
- runId,
2062
- this.getSuspendStepName(reason)
2063
- )
2064
- await this.withStepLock(runId, suspendStepName, async () => {
2065
- let stepState: StepState
2066
- try {
2067
- stepState = await this.getStepState(runId, suspendStepName)
2068
- } catch {
2069
- stepState = await this.insertStepState(
2070
- runId,
2071
- suspendStepName,
2072
- 'pikkuWorkflowSuspend',
2073
- {
2074
- reason,
2075
- },
2076
- undefined,
2077
- fromStepName
2078
- )
2079
- }
2080
- if (!stepState.stepId) {
2081
- stepState = await this.insertStepState(
2082
- runId,
2083
- suspendStepName,
2084
- 'pikkuWorkflowSuspend',
2085
- {
2086
- reason,
2087
- },
2088
- undefined,
2089
- fromStepName
2090
- )
2091
- }
2092
-
2093
- if (stepState.status === 'succeeded') {
2094
- return
2095
- }
2096
-
2097
- if (stepState.status === 'pending') {
2098
- await this.setStepRunning(stepState.stepId)
2099
- }
2100
-
2101
- await this.setStepResult(stepState.stepId, {
1783
+ const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason))
1784
+ await this.withStepLock(runId, suspendStepName, () =>
1785
+ recordSuspension(
1786
+ this.approvalStore,
1787
+ runId,
2102
1788
  reason,
2103
- suspendedAt: new Date().toISOString(),
2104
- })
2105
- throw new WorkflowSuspendedException(runId, reason)
2106
- })
1789
+ suspendStepName,
1790
+ fromStepName
1791
+ )
1792
+ )
2107
1793
  }
2108
1794
 
2109
1795
  private async scheduleRunWake(runId: string, delay: number): Promise<void> {
@@ -2128,49 +1814,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2128
1814
  }
2129
1815
  }
2130
1816
 
2131
- private getApprovalStepName(reason: string): string {
2132
- return `__workflow_approval:${reason}`
2133
- }
2134
-
2135
- private approvalStateKey(stepName: string): string {
2136
- let hex = ''
2137
- for (const byte of new TextEncoder().encode(stepName)) {
2138
- hex += byte.toString(16).padStart(2, '0')
1817
+ private get approvalStore(): ApprovalStore {
1818
+ return {
1819
+ getStepState: (runId, stepName) => this.getStepState(runId, stepName),
1820
+ insertStepState: (
1821
+ runId,
1822
+ stepName,
1823
+ rpcName,
1824
+ input,
1825
+ output,
1826
+ fromStepName
1827
+ ) =>
1828
+ this.insertStepState(
1829
+ runId,
1830
+ stepName,
1831
+ rpcName,
1832
+ input,
1833
+ output,
1834
+ fromStepName
1835
+ ),
1836
+ setStepRunning: (stepId) => this.setStepRunning(stepId),
1837
+ setStepResult: (stepId, result) => this.setStepResult(stepId, result),
1838
+ getRunState: (runId) => this.getRunState(runId),
1839
+ updateRunState: (runId, key, value) =>
1840
+ this.updateRunState(runId, key, value),
1841
+ resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1842
+ scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
2139
1843
  }
2140
- return `__approval_${hex}`
2141
1844
  }
2142
1845
 
2143
1846
  public async approveStep(
2144
1847
  runId: string,
2145
1848
  reason: string,
2146
- decision: unknown
1849
+ decision: unknown,
1850
+ session?: CoreUserSession
2147
1851
  ): Promise<void> {
2148
- const stepName = this.getApprovalStepName(reason)
2149
- const stateKey = this.approvalStateKey(stepName)
1852
+ assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session)
2150
1853
 
2151
- let resolved: StepState | undefined
2152
- try {
2153
- resolved = await this.getStepState(runId, stepName)
2154
- } catch {
2155
- // knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
2156
- }
2157
- if (resolved?.stepId && resolved.status === 'succeeded') {
2158
- const outcome = resolved.result as ApprovalOutcome<unknown> | undefined
2159
- throw new WorkflowApprovalResolvedError(
2160
- reason,
2161
- outcome?.status ?? 'decided'
2162
- )
2163
- }
2164
-
2165
- const state = await this.getRunState(runId)
2166
- const record = (state[stateKey] ?? {}) as Record<string, unknown>
2167
- await this.updateRunState(runId, stateKey, {
2168
- ...record,
2169
- decision,
2170
- decidedAt: new Date().toISOString(),
2171
- error: undefined,
2172
- })
2173
- await this.resumeWorkflow(runId)
1854
+ return recordApprovalDecision(this.approvalStore, runId, reason, decision)
2174
1855
  }
2175
1856
 
2176
1857
  private async approvalStep(
@@ -2181,100 +1862,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2181
1862
  const fromStepName = this.lastStepName(runId)
2182
1863
  const approvalStepName = this.nextStepKey(
2183
1864
  runId,
2184
- this.getApprovalStepName(reason)
1865
+ approvalStepNameFor(reason)
1866
+ )
1867
+ return await this.withStepLock(runId, approvalStepName, () =>
1868
+ evaluateApprovalStep(
1869
+ this.approvalStore,
1870
+ runId,
1871
+ reason,
1872
+ approvalStepName,
1873
+ fromStepName,
1874
+ options
1875
+ )
2185
1876
  )
2186
- return await this.withStepLock(runId, approvalStepName, async () => {
2187
- let stepState: StepState
2188
- try {
2189
- stepState = await this.getStepState(runId, approvalStepName)
2190
- } catch {
2191
- stepState = await this.insertStepState(
2192
- runId,
2193
- approvalStepName,
2194
- 'pikkuWorkflowApproval',
2195
- { reason, expiry: options.expiry },
2196
- undefined,
2197
- fromStepName
2198
- )
2199
- }
2200
- if (!stepState.stepId) {
2201
- stepState = await this.insertStepState(
2202
- runId,
2203
- approvalStepName,
2204
- 'pikkuWorkflowApproval',
2205
- { reason, expiry: options.expiry },
2206
- undefined,
2207
- fromStepName
2208
- )
2209
- }
2210
-
2211
- if (stepState.status === 'succeeded') {
2212
- return stepState.result as ApprovalOutcome<unknown>
2213
- }
2214
-
2215
- const stateKey = this.approvalStateKey(approvalStepName)
2216
- let record = ((await this.getRunState(runId))[stateKey] ?? {}) as {
2217
- decision?: unknown
2218
- decidedAt?: string
2219
- expiresAt?: string
2220
- error?: unknown
2221
- }
2222
-
2223
- if (stepState.status === 'pending') {
2224
- await this.setStepRunning(stepState.stepId)
2225
- if (options.expiry !== undefined && !record.expiresAt) {
2226
- const expiresAt = new Date(
2227
- Date.now() + getDurationInMilliseconds(options.expiry)
2228
- ).toISOString()
2229
- record = { ...record, expiresAt }
2230
- await this.updateRunState(runId, stateKey, record)
2231
- await this.scheduleRunWake(
2232
- runId,
2233
- getDurationInMilliseconds(options.expiry)
2234
- )
2235
- }
2236
- }
2237
-
2238
- if (record.decision !== undefined) {
2239
- const validation = await options.schema['~standard'].validate(
2240
- record.decision
2241
- )
2242
- if (validation.issues) {
2243
- await this.updateRunState(runId, stateKey, {
2244
- ...record,
2245
- decision: undefined,
2246
- decidedAt: undefined,
2247
- error: validation.issues.map((issue) => ({
2248
- message: issue.message,
2249
- path: issue.path?.map((segment) =>
2250
- typeof segment === 'object' ? segment.key : segment
2251
- ),
2252
- })),
2253
- })
2254
- throw new WorkflowSuspendedException(runId, reason)
2255
- }
2256
- const outcome: ApprovalOutcome<unknown> = {
2257
- status: 'decided',
2258
- data: validation.value,
2259
- }
2260
- await this.setStepResult(stepState.stepId, outcome)
2261
- return outcome
2262
- }
2263
-
2264
- if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
2265
- const outcome: ApprovalOutcome<unknown> = { status: 'expired' }
2266
- await this.setStepResult(stepState.stepId, outcome)
2267
- return outcome
2268
- }
2269
-
2270
- throw new WorkflowSuspendedException(runId, reason)
2271
- })
2272
1877
  }
2273
1878
 
2274
1879
  public createWorkflowWire(
2275
1880
  name: string,
2276
1881
  runId: string,
2277
- rpcService: any,
1882
+ rpcService: PikkuRPC,
2278
1883
  addonNamespace?: string | null
2279
1884
  ): PikkuWorkflowWire {
2280
1885
  const workflowWire: PikkuWorkflowWire = {
@@ -2347,45 +1952,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2347
1952
  }
2348
1953
 
2349
1954
  private getConfig(): WorkflowServiceConfig {
2350
- const singletonServices = getSingletonServices()
2351
- const workflow = singletonServices.config?.workflow
2352
- return {
2353
- retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
2354
- retryDelay: workflow?.retryDelay ?? 0,
2355
- orchestratorQueueName:
2356
- workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
2357
- stepWorkerQueueName:
2358
- workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
2359
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
2360
- }
1955
+ return resolveWorkflowConfig()
2361
1956
  }
2362
1957
 
2363
1958
  protected getOrchestratorQueueName(workflowName?: string): string {
2364
- if (workflowName && this.queueStrategy !== 'shared-groups') {
2365
- const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
2366
- const meta = pikkuState(null, 'queue', 'meta')
2367
- if (meta[perWorkflow]) {
2368
- return perWorkflow
2369
- }
2370
- }
2371
- return this.getConfig().orchestratorQueueName
1959
+ return orchestratorQueueName(this.queueStrategy, workflowName)
2372
1960
  }
2373
1961
 
2374
1962
  protected getStepWorkerQueueName(rpcName?: string): string {
2375
- if (rpcName && this.queueStrategy !== 'shared-groups') {
2376
- const perStep = `wf-step-${toKebab(rpcName)}`
2377
- const meta = pikkuState(null, 'queue', 'meta')
2378
- if (meta[perStep]) {
2379
- return perStep
2380
- }
2381
- }
2382
- return this.getConfig().stepWorkerQueueName
1963
+ return stepWorkerQueueName(this.queueStrategy, rpcName)
2383
1964
  }
2384
1965
 
2385
1966
  protected getJobGroup(id?: string): JobGroup | undefined {
2386
- if (!id || this.queueStrategy !== 'shared-groups') {
2387
- return undefined
2388
- }
2389
- return { id, tier: id }
1967
+ return jobGroupFor(this.queueStrategy, id)
2390
1968
  }
2391
1969
  }