@pikku/core 0.12.79 → 0.12.82

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 (479) hide show
  1. package/CHANGELOG.md +473 -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/errors/index.d.ts +1 -1
  7. package/dist/errors/index.js +1 -1
  8. package/dist/function/function-runner.js +22 -47
  9. package/dist/function/functions.types.d.ts +9 -5
  10. package/dist/function/index.d.ts +1 -1
  11. package/dist/index.d.ts +14 -19
  12. package/dist/index.js +6 -11
  13. package/dist/middleware/auth-cookie.d.ts +0 -4
  14. package/dist/middleware/auth-cookie.js +38 -2
  15. package/dist/middleware/cors.js +1 -0
  16. package/dist/middleware/index.d.ts +1 -1
  17. package/dist/middleware/index.js +1 -1
  18. package/dist/middleware/remote-auth.js +14 -3
  19. package/dist/permissions.d.ts +8 -10
  20. package/dist/permissions.js +0 -11
  21. package/dist/pikku-state.js +16 -5
  22. package/dist/schema.js +35 -1
  23. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  24. package/dist/services/ai-run-state-service.d.ts +17 -1
  25. package/dist/services/in-memory-ai-run-state-service.d.ts +6 -2
  26. package/dist/services/in-memory-ai-run-state-service.js +11 -1
  27. package/dist/services/in-memory-workflow-service.js +2 -0
  28. package/dist/services/index.d.ts +15 -15
  29. package/dist/services/index.js +5 -5
  30. package/dist/services/local-content-request-handler.d.ts +21 -0
  31. package/dist/services/local-content-request-handler.js +72 -53
  32. package/dist/services/local-content.d.ts +6 -0
  33. package/dist/services/local-content.js +14 -1
  34. package/dist/services/scoped-credential-service.d.ts +21 -0
  35. package/dist/services/scoped-credential-service.js +53 -0
  36. package/dist/services/workflow-service.d.ts +4 -2
  37. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  38. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  39. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/ai-storage-service-tests.js +302 -0
  41. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  43. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  45. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  47. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  49. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  50. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  51. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  52. package/dist/testing/service-tests/session-store-tests.js +43 -0
  53. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  54. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  55. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  56. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  57. package/dist/testing/service-tests.d.ts +6 -0
  58. package/dist/testing/service-tests.js +26 -791
  59. package/dist/types/core.types.d.ts +11 -3
  60. package/dist/types/state.types.d.ts +14 -1
  61. package/dist/wirings/actor-flow/index.d.ts +1 -1
  62. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  63. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  64. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  65. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  66. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  67. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  68. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  69. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  70. package/dist/wirings/ai-agent/ai-agent-memory.js +37 -18
  71. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  72. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  73. package/dist/wirings/ai-agent/ai-agent-prepare.js +5 -9
  74. package/dist/wirings/ai-agent/ai-agent-runner.js +89 -147
  75. package/dist/wirings/ai-agent/ai-agent-stream.js +109 -97
  76. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +57 -0
  77. package/dist/wirings/ai-agent/ai-agent-turn.js +82 -0
  78. package/dist/wirings/ai-agent/ai-agent.types.d.ts +47 -2
  79. package/dist/wirings/ai-agent/index.d.ts +8 -7
  80. package/dist/wirings/ai-agent/index.js +5 -4
  81. package/dist/wirings/ai-agent/voice-input.js +1 -6
  82. package/dist/wirings/ai-agent/voice-output.js +2 -12
  83. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  84. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  85. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  86. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  87. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  88. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  89. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  90. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  91. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  92. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  93. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  94. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  95. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  96. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  97. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  98. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  99. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  100. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  101. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  102. package/dist/wirings/ai-scorer/index.js +5 -0
  103. package/dist/wirings/channel/channel-common.js +1 -0
  104. package/dist/wirings/channel/channel-handler.js +3 -5
  105. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  106. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  107. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  108. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  109. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  110. package/dist/wirings/channel/channel-runner.js +16 -8
  111. package/dist/wirings/channel/channel.types.d.ts +2 -0
  112. package/dist/wirings/channel/index.d.ts +5 -6
  113. package/dist/wirings/channel/index.js +3 -4
  114. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  115. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  116. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  117. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  118. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  119. package/dist/wirings/cli/channel/index.d.ts +1 -2
  120. package/dist/wirings/cli/channel/index.js +0 -1
  121. package/dist/wirings/cli/cli-runner.js +17 -3
  122. package/dist/wirings/cli/cli.types.d.ts +0 -8
  123. package/dist/wirings/cli/command-parser.js +13 -0
  124. package/dist/wirings/credential/index.d.ts +1 -1
  125. package/dist/wirings/gateway/gateway-runner.js +9 -2
  126. package/dist/wirings/gateway/index.d.ts +1 -1
  127. package/dist/wirings/http/http-routes.js +2 -0
  128. package/dist/wirings/http/http-runner.d.ts +0 -10
  129. package/dist/wirings/http/http-runner.js +14 -15
  130. package/dist/wirings/http/http.types.d.ts +0 -10
  131. package/dist/wirings/http/index.d.ts +2 -3
  132. package/dist/wirings/http/index.js +1 -1
  133. package/dist/wirings/mcp/index.d.ts +1 -1
  134. package/dist/wirings/mcp/mcp-runner.d.ts +15 -7
  135. package/dist/wirings/mcp/mcp-runner.js +18 -11
  136. package/dist/wirings/persona/index.d.ts +3 -4
  137. package/dist/wirings/persona/index.js +2 -3
  138. package/dist/wirings/queue/index.d.ts +1 -3
  139. package/dist/wirings/queue/index.js +1 -3
  140. package/dist/wirings/rpc/addon-runner.d.ts +4 -0
  141. package/dist/wirings/rpc/addon-runner.js +19 -3
  142. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  143. package/dist/wirings/rpc/rpc-runner.js +55 -86
  144. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  145. package/dist/wirings/rpc/wire-addon.d.ts +13 -0
  146. package/dist/wirings/rpc/wire-addon.js +4 -0
  147. package/dist/wirings/scheduler/index.d.ts +1 -1
  148. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  149. package/dist/wirings/trigger/index.d.ts +1 -1
  150. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  151. package/dist/wirings/trigger/trigger-runner.js +1 -0
  152. package/dist/wirings/virtual-user/index.d.ts +5 -6
  153. package/dist/wirings/virtual-user/index.js +2 -4
  154. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  155. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  156. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  157. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +100 -3
  158. package/dist/wirings/workflow/feature.js +7 -4
  159. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  160. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  161. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  162. package/dist/wirings/workflow/index.d.ts +12 -7
  163. package/dist/wirings/workflow/index.js +7 -3
  164. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  165. package/dist/wirings/workflow/pikku-scenario-service.js +64 -10
  166. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  167. package/dist/wirings/workflow/pikku-workflow-service.js +85 -493
  168. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  169. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  170. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  171. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  172. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  173. package/dist/wirings/workflow/workflow-approval.d.ts +67 -0
  174. package/dist/wirings/workflow/workflow-approval.js +177 -0
  175. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  176. package/dist/wirings/workflow/workflow-constants.js +35 -0
  177. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  178. package/dist/wirings/workflow/workflow-errors.js +112 -0
  179. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  180. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  181. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  182. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  183. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  184. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  185. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  186. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  187. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  188. package/dist/wirings/workflow/workflow-run-ownership.d.ts +17 -0
  189. package/dist/wirings/workflow/workflow-run-ownership.js +30 -0
  190. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  191. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  192. package/dist/wirings/workflow/workflow.types.d.ts +8 -1
  193. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  194. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  195. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  196. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  197. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  198. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  199. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  200. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  201. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  202. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  203. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  204. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  205. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  206. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  207. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  208. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  209. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  210. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  211. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  212. package/knowledge/decisions/internals/index.md +42 -3
  213. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  214. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  215. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  216. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  217. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  218. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  219. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  220. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  221. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  222. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  223. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  224. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  225. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  226. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  227. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  228. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  229. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  230. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  231. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  232. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  233. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  234. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  235. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  236. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  237. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  238. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +50 -0
  239. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  240. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  241. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  242. package/knowledge/decisions/security/index.md +9 -0
  243. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  244. package/knowledge/questions/index.md +2 -1
  245. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  246. package/package.json +15 -3
  247. package/scripts/generate-api-report.d.mts +1 -0
  248. package/scripts/generate-api-report.mjs +89 -0
  249. package/scripts/generate-api-report.mts +343 -0
  250. package/src/api-report.test.ts +69 -0
  251. package/src/column-form.ts +1 -4
  252. package/src/crypto-utils.test.ts +15 -2
  253. package/src/dev/hot-reload.ts +2 -7
  254. package/src/ecosystem.ts +40 -0
  255. package/src/errors/error-handler.ts +5 -3
  256. package/src/errors/error.test.ts +4 -1
  257. package/src/errors/index.ts +1 -1
  258. package/src/function/function-runner.test.ts +53 -1
  259. package/src/function/function-runner.ts +33 -48
  260. package/src/function/functions.types.ts +15 -9
  261. package/src/function/index.ts +0 -2
  262. package/src/handle-error.ts +1 -1
  263. package/src/index.ts +3 -57
  264. package/src/middleware/auth-cookie.test.ts +50 -0
  265. package/src/middleware/auth-cookie.ts +38 -2
  266. package/src/middleware/cors.ts +2 -1
  267. package/src/middleware/index.ts +1 -1
  268. package/src/middleware/remote-auth.test.ts +43 -0
  269. package/src/middleware/remote-auth.ts +17 -3
  270. package/src/middleware-runner.ts +4 -2
  271. package/src/no-any-casts.test.ts +57 -0
  272. package/src/permissions.test.ts +3 -1
  273. package/src/permissions.ts +22 -24
  274. package/src/pikku-state.ts +22 -6
  275. package/src/public-surface.json +554 -0
  276. package/src/public-surface.json.README +25 -0
  277. package/src/public-surface.test.ts +105 -0
  278. package/src/removed-legacy-exports.test.ts +62 -0
  279. package/src/schema.test.ts +78 -0
  280. package/src/schema.ts +36 -1
  281. package/src/services/ai-agent-runner-service.ts +12 -1
  282. package/src/services/ai-run-state-service.ts +18 -1
  283. package/src/services/audit-service.ts +2 -2
  284. package/src/services/in-memory-ai-run-state-service.ts +16 -2
  285. package/src/services/in-memory-workflow-service.ts +2 -0
  286. package/src/services/index.ts +4 -47
  287. package/src/services/local-content-request-handler.test.ts +43 -5
  288. package/src/services/local-content-request-handler.ts +103 -74
  289. package/src/services/local-content.ts +15 -1
  290. package/src/services/local-email-service.ts +5 -1
  291. package/src/services/scoped-credential-service.test.ts +86 -0
  292. package/src/services/scoped-credential-service.ts +63 -0
  293. package/src/services/system-role-guard.test.ts +4 -1
  294. package/src/services/workflow-service.ts +9 -2
  295. package/src/side-effects-are-declared.test.ts +84 -0
  296. package/src/source-files-stay-composable.test.ts +41 -0
  297. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  298. package/src/testing/service-tests/ai-storage-service-tests.ts +379 -0
  299. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  300. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  301. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  302. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  303. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  304. package/src/testing/service-tests/session-store-tests.ts +62 -0
  305. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  306. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  307. package/src/testing/service-tests.ts +31 -1111
  308. package/src/types/core.types.ts +14 -3
  309. package/src/types/state.types.ts +17 -1
  310. package/src/wirings/actor-flow/index.ts +0 -3
  311. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  312. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  313. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  314. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  315. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  316. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  317. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  318. package/src/wirings/ai-agent/ai-agent-memory.ts +62 -39
  319. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  320. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  321. package/src/wirings/ai-agent/ai-agent-prepare.ts +12 -12
  322. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  323. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  324. package/src/wirings/ai-agent/ai-agent-runner.ts +120 -160
  325. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  326. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  327. package/src/wirings/ai-agent/ai-agent-stream.ts +152 -117
  328. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  329. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  330. package/src/wirings/ai-agent/ai-agent-turn.ts +122 -0
  331. package/src/wirings/ai-agent/ai-agent.types.ts +65 -5
  332. package/src/wirings/ai-agent/index.ts +2 -16
  333. package/src/wirings/ai-agent/voice-input.ts +1 -6
  334. package/src/wirings/ai-agent/voice-output.ts +2 -12
  335. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  336. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  337. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  338. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  339. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  340. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  341. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  342. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  343. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  344. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  345. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  346. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  347. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  348. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  349. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  350. package/src/wirings/ai-scorer/index.ts +24 -0
  351. package/src/wirings/channel/channel-common.ts +5 -2
  352. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  353. package/src/wirings/channel/channel-handler.ts +7 -7
  354. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  355. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  356. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  357. package/src/wirings/channel/channel-runner.ts +33 -20
  358. package/src/wirings/channel/channel.types.ts +2 -0
  359. package/src/wirings/channel/index.ts +1 -20
  360. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  361. package/src/wirings/channel/local/local-channel-runner.ts +9 -2
  362. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  363. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  364. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  365. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  366. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  367. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  368. package/src/wirings/cli/channel/index.ts +0 -7
  369. package/src/wirings/cli/cli-runner.test.ts +68 -0
  370. package/src/wirings/cli/cli-runner.ts +25 -5
  371. package/src/wirings/cli/cli.types.ts +0 -39
  372. package/src/wirings/cli/command-parser.test.ts +19 -0
  373. package/src/wirings/cli/command-parser.ts +17 -1
  374. package/src/wirings/credential/index.ts +0 -1
  375. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  376. package/src/wirings/gateway/gateway-runner.ts +27 -19
  377. package/src/wirings/gateway/index.ts +0 -3
  378. package/src/wirings/http/http-routes.ts +4 -1
  379. package/src/wirings/http/http-runner.test.ts +66 -0
  380. package/src/wirings/http/http-runner.ts +21 -28
  381. package/src/wirings/http/http.types.ts +0 -15
  382. package/src/wirings/http/index.ts +2 -8
  383. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  384. package/src/wirings/http/web-request.ts +1 -1
  385. package/src/wirings/mcp/index.ts +0 -1
  386. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  387. package/src/wirings/mcp/mcp-runner.ts +37 -21
  388. package/src/wirings/persona/index.ts +0 -8
  389. package/src/wirings/persona/persona-environments.test.ts +14 -3
  390. package/src/wirings/persona/persona.test.ts +13 -3
  391. package/src/wirings/persona/validate-personas.ts +5 -1
  392. package/src/wirings/queue/index.ts +0 -14
  393. package/src/wirings/queue/queue-runner.ts +1 -1
  394. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  395. package/src/wirings/rpc/addon-runner.ts +34 -3
  396. package/src/wirings/rpc/addon-secrets.test.ts +261 -0
  397. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  398. package/src/wirings/rpc/rpc-runner.ts +60 -136
  399. package/src/wirings/rpc/rpc-types.ts +11 -0
  400. package/src/wirings/rpc/wire-addon.ts +20 -1
  401. package/src/wirings/scheduler/index.ts +0 -1
  402. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  403. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  404. package/src/wirings/trigger/index.ts +0 -1
  405. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  406. package/src/wirings/trigger/trigger-runner.ts +7 -5
  407. package/src/wirings/virtual-user/index.ts +0 -16
  408. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  409. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  410. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  411. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  412. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  413. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  414. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  415. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +117 -4
  416. package/src/wirings/workflow/feature.ts +7 -4
  417. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  418. package/src/wirings/workflow/graph/graph-runner.test.ts +76 -2
  419. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  420. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  421. package/src/wirings/workflow/index.ts +14 -24
  422. package/src/wirings/workflow/pikku-scenario-service.ts +101 -27
  423. package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -14
  424. package/src/wirings/workflow/pikku-workflow-service.ts +214 -743
  425. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  426. package/src/wirings/workflow/scenario-hooks.test.ts +52 -0
  427. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  428. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  429. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  430. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  431. package/src/wirings/workflow/workflow-approval.ts +289 -0
  432. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  433. package/src/wirings/workflow/workflow-constants.ts +47 -0
  434. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  435. package/src/wirings/workflow/workflow-errors.ts +128 -0
  436. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  437. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  438. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  439. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  440. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  441. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  442. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  443. package/src/wirings/workflow/workflow-run-authority.test.ts +212 -0
  444. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  445. package/src/wirings/workflow/workflow-run-ownership.ts +37 -0
  446. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  447. package/src/wirings/workflow/workflow.types.ts +7 -9
  448. package/src/wirings-stay-decoupled.test.ts +127 -0
  449. package/tsconfig.json +1 -1
  450. package/tsconfig.tsbuildinfo +1 -1
  451. package/dist/internal.d.ts +0 -3
  452. package/dist/internal.js +0 -2
  453. package/dist/middleware/timeout.d.ts +0 -9
  454. package/dist/middleware/timeout.js +0 -15
  455. package/dist/pikku-response.d.ts +0 -6
  456. package/dist/pikku-response.js +0 -6
  457. package/dist/services/gopass-secrets.d.ts +0 -15
  458. package/dist/services/gopass-secrets.js +0 -76
  459. package/dist/services/http-scenario-actors.d.ts +0 -75
  460. package/dist/services/http-scenario-actors.js +0 -195
  461. package/dist/services/http-user-flow-actors.d.ts +0 -67
  462. package/dist/services/http-user-flow-actors.js +0 -193
  463. package/dist/services/scenario-actors-service.d.ts +0 -127
  464. package/dist/services/scenario-actors-service.js +0 -40
  465. package/dist/services/user-flow-actors-service.d.ts +0 -39
  466. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  467. package/dist/wirings/credential/wire-credential.js +0 -47
  468. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  469. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  470. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  471. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  472. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  473. package/dist/wirings/scope/wire-scope.js +0 -32
  474. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  475. package/dist/wirings/workflow/dsl/index.js +0 -4
  476. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  477. package/dist/wirings/workflow/graph/index.js +0 -4
  478. package/src/internal.ts +0 -10
  479. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/workflow-run-engine.types.js} +0 -0
@@ -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,251 +42,66 @@ 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 { approvalDeciderFrom } from './workflow-approval-policy.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
- /**
285
- * How long a step may sit `pending` before the relay assumes its dispatch was
286
- * lost. This is a bet that no healthy queue takes this long to move a job from
287
- * `pending` to `running`; set it above the observed p99 of that latency.
288
- */
289
- const DEFAULT_UNDISPATCHED_STEP_MS = 30_000
290
-
291
- /** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
292
- const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100
293
-
294
- /** First wait before a step already re-dispatched once is re-dispatched again. */
295
- const REDISPATCH_BACKOFF_MS = 30_000
296
-
297
- /** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
298
- const REDISPATCH_BACKOFF_MAX_MS = 10 * 60_000
299
-
300
- /** Bound on the in-process backoff map, so a long-lived process cannot grow it without bound. */
301
- const REDISPATCH_BACKOFF_MAX_ENTRIES = 10_000
302
-
303
- const WORKFLOW_POLL_MIN_MS = 10
304
-
305
- const WORKFLOW_POLL_FACTOR = 1.6
306
-
307
- const WORKFLOW_CHILD_POLL_MAX_MS = 500
308
-
309
- type RunContext = {
310
- activeExecutions: number
311
- inline?: boolean
312
- ordinals: Map<string, number>
313
- lastStep?: string
314
- replay?: {
315
- steps?: Map<string, StepState>
316
- run?: WorkflowRun
317
- }
318
- }
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 { auditApprovalDecision } from './workflow-approval-audit.js'
99
+ import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
100
+ import {
101
+ RedispatchBackoff,
102
+ sweepStalledRuns,
103
+ sweepUndispatchedSteps,
104
+ } from './workflow-recovery.js'
319
105
 
320
106
  export abstract class PikkuWorkflowService implements WorkflowService {
321
107
  private runExtension?: WorkflowRunExtension
@@ -406,112 +192,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
406
192
  }
407
193
 
408
194
  public wireQueueWorkers(): void {
409
- const functions = pikkuState(null, 'function', 'functions')
410
- const functionsMeta = pikkuState(null, 'function', 'meta')
411
-
412
- const mkMeta = (funcId: string) => ({
413
- pikkuFuncId: funcId,
414
- sessionless: true,
415
- functionType: 'helper' as const,
416
- inputSchemaName: null,
417
- outputSchemaName: null,
195
+ wireWorkflowQueueWorkers({
196
+ strategy: this.queueStrategy,
197
+ concurrency: this.queueConcurrency,
198
+ groupConcurrency: this.queueGroupConcurrency,
199
+ getLogger: () => this.logger,
418
200
  })
419
-
420
- const queueMeta = pikkuState(null, 'queue', 'meta')
421
-
422
- const registerWorkflowFunc = (
423
- funcId: string,
424
- func: { func: unknown },
425
- queueName: string,
426
- config?: PikkuWorkerConfig
427
- ) => {
428
- if (functions.has(funcId)) return
429
- addFunction(funcId, func as never)
430
- if (!queueMeta[queueName]) {
431
- queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
432
- }
433
- wireQueueWorker({ name: queueName, func, config } as never)
434
- if (!functionsMeta[funcId]) {
435
- functionsMeta[funcId] = mkMeta(funcId)
436
- }
437
- }
438
-
439
- const sharedGroups = this.queueStrategy === 'shared-groups'
440
- const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
441
- ? {
442
- batchSize: this.queueConcurrency,
443
- groupConcurrency: this.queueGroupConcurrency,
444
- }
445
- : undefined
446
-
447
- registerWorkflowFunc(
448
- 'pikkuWorkflowOrchestrator',
449
- { func: pikkuWorkflowOrchestratorFunc },
450
- 'pikku-workflow-orchestrator',
451
- sharedQueueConfig
452
- )
453
- registerWorkflowFunc(
454
- 'pikkuWorkflowStepWorker',
455
- { func: pikkuWorkflowWorkerFunc },
456
- 'pikku-workflow-step-worker',
457
- sharedQueueConfig
458
- )
459
-
460
- const registerQueueWorkers = (queueMeta: Record<string, any>) => {
461
- for (const [queueName, meta] of Object.entries(queueMeta)) {
462
- if (functions.has(meta.pikkuFuncId)) continue
463
- if (queueName.startsWith('wf-orchestrator-')) {
464
- registerWorkflowFunc(
465
- meta.pikkuFuncId,
466
- { func: pikkuWorkflowOrchestratorFunc },
467
- queueName
468
- )
469
- } else if (queueName.startsWith('wf-step-')) {
470
- registerWorkflowFunc(
471
- meta.pikkuFuncId,
472
- { func: pikkuWorkflowWorkerFunc },
473
- queueName
474
- )
475
- }
476
- }
477
- }
478
-
479
- if (!sharedGroups) {
480
- registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
481
-
482
- const addons = pikkuState(null, 'addons', 'packages')
483
- if (addons) {
484
- for (const [, addon] of addons) {
485
- const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
486
- if (addonQueueMeta) {
487
- registerQueueWorkers(addonQueueMeta)
488
- }
489
- }
490
- }
491
- }
492
-
493
- const workflowCount = Object.keys(
494
- pikkuState(null, 'workflows', 'meta') ?? {}
495
- ).length
496
- const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
497
- name.startsWith('wf-orchestrator-')
498
- ).length
499
- if (workflowCount > 0 && perWorkflowQueues === 0) {
500
- this.logger?.warn?.(
501
- `[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
502
- `All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
503
- `Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
504
- )
505
- }
506
-
507
- if (!functions.has('pikkuWorkflowSleeper')) {
508
- addFunction('pikkuWorkflowSleeper', {
509
- func: pikkuWorkflowSleeperFunc,
510
- })
511
- }
512
- if (!functionsMeta.pikkuWorkflowSleeper) {
513
- functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper')
514
- }
515
201
  }
516
202
 
517
203
  protected async isInline(runId: string): Promise<boolean> {
@@ -774,7 +460,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
774
460
  const serialized: SerializedError = {
775
461
  message: error.message,
776
462
  stack: error.stack,
777
- code: (error as any).code,
463
+ code: (error as { code?: string }).code,
778
464
  expected: isExpectedError(error),
779
465
  }
780
466
  return mirror.setStepError(stepId, serialized)
@@ -796,7 +482,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
796
482
  (mirror, newStep) =>
797
483
  mirror.createRetryAttempt(failedStepId, {
798
484
  ...newStep,
799
- stepName: (newStep as any).stepName ?? '',
485
+ stepName: (newStep as { stepName?: string }).stepName ?? '',
800
486
  })
801
487
  )
802
488
  }
@@ -949,51 +635,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
949
635
  return []
950
636
  }
951
637
 
952
- /**
953
- * Re-drive runs whose next move was lost, and report which were resumed.
954
- *
955
- * Arming a step is two writes to two systems — the step row, then the queue
956
- * or scheduler job — so a process that dies between them leaves a run that is
957
- * `running` with nothing in flight. Nothing notices: the run parks on a step
958
- * that will never complete and never error, so it neither finishes nor fails.
959
- * (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
960
- * step's insert and its timer, parking the run permanently.)
961
- *
962
- * Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
963
- * step state, and every settled step is memoized, so resuming a run that was
964
- * not actually stuck costs an orchestration pass and changes nothing. That
965
- * idempotence is what makes an idle-time heuristic safe here; a run that is
966
- * legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
967
- *
968
- * This is not self-starting. Call it from a scheduled task at whatever
969
- * interval suits the workload.
970
- */
971
- public async recoverStalledRuns(options?: {
972
- stalledAfterMs?: number
973
- limit?: number
974
- }): Promise<{ resumed: string[] }> {
975
- const before = new Date(
976
- Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS)
977
- )
978
- const runIds = await this.findStalledRunIds(
979
- before,
980
- options?.limit ?? DEFAULT_STALLED_RUN_LIMIT
981
- )
982
- const resumed: string[] = []
983
- for (const runId of runIds) {
984
- try {
985
- await this.resumeWorkflow(runId)
986
- resumed.push(runId)
987
- } catch (err) {
988
- // One unresumable run must not stop the sweep from recovering the rest.
989
- getSingletonServices()?.logger?.error(
990
- `Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
991
- )
992
- }
993
- }
994
- return { resumed }
995
- }
996
-
997
638
  /**
998
639
  * Runs holding a step that has sat `pending` since before `before`, paired
999
640
  * with the step that flagged them.
@@ -1014,110 +655,51 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1014
655
  return []
1015
656
  }
1016
657
 
658
+ private readonly redispatchBackoff = new RedispatchBackoff()
659
+
660
+ private get sweepDeps() {
661
+ return {
662
+ resume: (runId: string) => this.resumeWorkflow(runId),
663
+ logger: getSingletonServices()?.logger,
664
+ }
665
+ }
666
+
1017
667
  /**
1018
- * Re-drive steps whose dispatch was lost, and report which runs were nudged.
1019
- *
1020
- * Arming a step is two writes to two systems: the step row lands `pending`,
1021
- * then a queue or scheduler job is published. Nothing spans both, so a crash
1022
- * in between leaves a durable row that nothing will ever pick up — the run
1023
- * neither finishes nor fails. (Seen on a `workflow.sleep()`: a deploy restart
1024
- * landed between the sleep step's insert and its timer.)
1025
- *
1026
- * The row is the outbox record and this is the relay. Age is the only signal
1027
- * available — a step `pending` because its dispatch was lost is
1028
- * indistinguishable from one whose job is merely still queued — so a step
1029
- * past `undispatchedAfterMs` is re-dispatched regardless, and correctness
1030
- * rests on the claim rather than on the guess being right. A redundant
1031
- * dispatch costs one queue message: the loser reads `running` and returns
1032
- * without invoking anything.
1033
- *
1034
- * Re-dispatches back off per step (doubling from 30s, capped at 10m) so a
1035
- * genuine queue backlog is not amplified by a tick that keeps firing at the
1036
- * steps the backlog is already delaying. The backoff is per process and
1037
- * advisory — losing it on restart costs extra dispatches, never correctness.
1038
- *
1039
- * This is not self-starting. Call it from a scheduled task; ~30s suits a
1040
- * queue whose `pending`→`running` latency is well under that.
668
+ * Re-drive runs whose next move was lost. Not self-starting call it from a
669
+ * scheduled task at whatever interval suits the workload.
1041
670
  */
1042
- public async relayUndispatchedSteps(options?: {
1043
- undispatchedAfterMs?: number
671
+ public async recoverStalledRuns(options?: {
672
+ stalledAfterMs?: number
1044
673
  limit?: number
1045
- }): Promise<{ redispatched: string[] }> {
1046
- const before = new Date(
1047
- Date.now() -
1048
- (options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS)
1049
- )
1050
- const steps = await this.findUndispatchedSteps(
1051
- before,
1052
- options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT
674
+ }): Promise<{ resumed: string[] }> {
675
+ return sweepStalledRuns(
676
+ (before, limit) => this.findStalledRunIds(before, limit),
677
+ options,
678
+ this.sweepDeps
1053
679
  )
1054
-
1055
- // Backoff is keyed by run, not step, because the run is the unit of
1056
- // re-drive: `resumeWorkflow` replays the whole run and re-dispatches every
1057
- // step still owed a job. Holding off a single step while resuming its run
1058
- // would not suppress anything.
1059
- const now = Date.now()
1060
- const runIds = new Set<string>()
1061
- for (const { runId } of steps) {
1062
- const eligibleAt = this.redispatchBackoff.get(runId)
1063
- if (eligibleAt !== undefined && eligibleAt > now) {
1064
- continue
1065
- }
1066
- this.noteRedispatch(runId, now)
1067
- runIds.add(runId)
1068
- }
1069
-
1070
- const redispatched: string[] = []
1071
- for (const runId of runIds) {
1072
- try {
1073
- await this.resumeWorkflow(runId)
1074
- redispatched.push(runId)
1075
- } catch (err) {
1076
- // One unresumable run must not stop the tick from relaying the rest.
1077
- getSingletonServices()?.logger?.error(
1078
- `Failed to re-dispatch workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
1079
- )
1080
- }
1081
- }
1082
- return { redispatched }
1083
680
  }
1084
681
 
1085
- /** Advisory, per-process record of when a run may next be re-dispatched. */
1086
- private readonly redispatchBackoff = new Map<string, number>()
1087
-
1088
- private readonly redispatchDelays = new Map<string, number>()
1089
-
1090
- private noteRedispatch(runId: string, now: number): void {
1091
- const previous = this.redispatchDelays.get(runId)
1092
- const delay = Math.min(
1093
- previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2,
1094
- REDISPATCH_BACKOFF_MAX_MS
682
+ /**
683
+ * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
684
+ * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
685
+ * well under that.
686
+ */
687
+ public async relayUndispatchedSteps(options?: {
688
+ undispatchedAfterMs?: number
689
+ limit?: number
690
+ }): Promise<{ redispatched: string[] }> {
691
+ return sweepUndispatchedSteps(
692
+ (before, limit) => this.findUndispatchedSteps(before, limit),
693
+ this.redispatchBackoff,
694
+ options,
695
+ this.sweepDeps
1095
696
  )
1096
- // A run that settles is never returned again, so entries are only evicted by
1097
- // this bound — oldest first, which is also least recently re-dispatched.
1098
- if (this.redispatchBackoff.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
1099
- const oldest = this.redispatchBackoff.keys().next()
1100
- if (!oldest.done) {
1101
- this.redispatchBackoff.delete(oldest.value)
1102
- this.redispatchDelays.delete(oldest.value)
1103
- }
1104
- }
1105
- this.redispatchDelays.set(runId, delay)
1106
- this.redispatchBackoff.set(runId, now + delay)
1107
697
  }
1108
698
 
1109
699
  protected resolveStepJobOptions(
1110
700
  stepOptions?: WorkflowStepOptions
1111
701
  ): JobOptions {
1112
- const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES
1113
- const retryDelay = stepOptions?.retryDelay
1114
- const backoff =
1115
- retryDelay !== undefined && retryDelay !== 'exponential'
1116
- ? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
1117
- : retries > 0 || retryDelay === 'exponential'
1118
- ? 'exponential'
1119
- : undefined
1120
- return { attempts: retries + 1, ...(backoff ? { backoff } : {}) }
702
+ return stepJobOptions(stepOptions)
1121
703
  }
1122
704
 
1123
705
  public async queueStepWorker(
@@ -1246,7 +828,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1246
828
  name: string,
1247
829
  input: I,
1248
830
  wire: WorkflowRunWire,
1249
- rpcService: any,
831
+ rpcService: PikkuRPC,
1250
832
  options?: {
1251
833
  inline?: boolean
1252
834
  startNode?: string
@@ -1263,6 +845,22 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1263
845
  }
1264
846
 
1265
847
  if (workflowMeta.source === 'graph') {
848
+ // A caller-supplied startNode must be one of the graph's declared entry
849
+ // nodes. startWorkflow is the boundary the public
850
+ // `/workflow/:name/graph/:nodeId` route and triggers enter through, so
851
+ // without this a request could name any dependency-free node — one whose
852
+ // input reads only `trigger` — and fire its RPC directly with
853
+ // attacker-chosen data, skipping every upstream eligibility, validation or
854
+ // approval node. (Internal resume/replay drives runWorkflowGraph directly
855
+ // and is unaffected.)
856
+ if (
857
+ options?.startNode &&
858
+ !(workflowMeta.entryNodeIds ?? []).includes(options.startNode)
859
+ ) {
860
+ throw new Error(
861
+ `Workflow graph '${name}': '${options.startNode}' is not a declared entry node`
862
+ )
863
+ }
1266
864
  const shouldInline =
1267
865
  options?.inline || !getSingletonServices()?.queueService
1268
866
  return runWorkflowGraph(
@@ -1346,7 +944,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1346
944
  public async runToCompletion<I>(
1347
945
  name: string,
1348
946
  input: I,
1349
- rpcService: any,
947
+ rpcService: PikkuRPC,
1350
948
  options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
1351
949
  ): Promise<any> {
1352
950
  const { runId } = await this.startWorkflow(
@@ -1468,7 +1066,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1468
1066
  return stepName
1469
1067
  }
1470
1068
 
1471
- public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
1069
+ public async runWorkflowJob(
1070
+ runId: string,
1071
+ rpcService: PikkuRPC
1072
+ ): Promise<void> {
1472
1073
  await this.beginReplay(runId)
1473
1074
  try {
1474
1075
  await this.runWorkflowJobInner(runId, rpcService)
@@ -1479,7 +1080,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1479
1080
 
1480
1081
  private async runWorkflowJobInner(
1481
1082
  runId: string,
1482
- rpcService: any
1083
+ rpcService: PikkuRPC
1483
1084
  ): Promise<void> {
1484
1085
  const run = await this.getRunIdentity(runId)
1485
1086
  if (!run) {
@@ -1517,6 +1118,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1517
1118
  }
1518
1119
 
1519
1120
  const registrations = pikkuState(pkgName, 'workflows', 'registrations')
1121
+ if (!workflowMeta) {
1122
+ throw new PikkuMissingMetaError(
1123
+ `Missing generated metadata for workflow '${run.workflow}'`
1124
+ )
1125
+ }
1126
+
1520
1127
  const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
1521
1128
  if (!workflow) {
1522
1129
  throw new WorkflowNotFoundError(run.workflow)
@@ -1533,11 +1140,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1533
1140
  addonNs
1534
1141
  )
1535
1142
  workflowWire.pikkuUserId = run.wire?.pikkuUserId
1536
- const wire: PikkuWire = {
1143
+ // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
1144
+ const wire: PikkuRawWire = {
1537
1145
  workflow: workflowWire,
1538
1146
  pikkuUserId: run.wire?.pikkuUserId,
1539
- session: rpcService?.wire?.session,
1540
- rpc: rpcService?.wire?.rpc,
1541
1147
  }
1542
1148
  this.runExtension?.decorateRunWire(wire, {
1543
1149
  runId,
@@ -1649,7 +1255,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1649
1255
  private async runVersionMismatchFallback(
1650
1256
  run: WorkflowRun,
1651
1257
  currentMeta: { source: string },
1652
- rpcService: any
1258
+ rpcService: PikkuRPC
1653
1259
  ): Promise<void> {
1654
1260
  const source = currentMeta.source
1655
1261
 
@@ -1680,7 +1286,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1680
1286
  stepName: string,
1681
1287
  rpcName: string,
1682
1288
  data: any,
1683
- rpcService: any
1289
+ rpcService: PikkuRPC
1684
1290
  ): Promise<void> {
1685
1291
  this.enterExecution(runId)
1686
1292
  try {
@@ -1701,10 +1307,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1701
1307
  stepName: string,
1702
1308
  rpcName: string,
1703
1309
  data: any,
1704
- rpcService: any
1310
+ rpcService: PikkuRPC
1705
1311
  ): Promise<void> {
1706
1312
  const claimed = await this.withStepLock(runId, stepName, async () => {
1707
1313
  const stepState = await this.getStepState(runId, stepName)
1314
+ // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
1315
+ if (
1316
+ stepState.rpcName !== undefined &&
1317
+ stepState.rpcName !== (rpcName ?? null)
1318
+ ) {
1319
+ throw new WorkflowStepFunctionMismatchError(runId, stepName)
1320
+ }
1708
1321
  if (stepState.status === 'succeeded' || stepState.status === 'running') {
1709
1322
  return null
1710
1323
  }
@@ -1751,7 +1364,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1751
1364
  runId,
1752
1365
  stepState.stepId,
1753
1366
  graphNodeId,
1754
- rpcName,
1367
+ workflowMeta!.nodes![graphNodeId].rpcName,
1755
1368
  data,
1756
1369
  run.workflow
1757
1370
  )
@@ -1763,7 +1376,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1763
1376
  id: rpcName,
1764
1377
  parentRunId: runId,
1765
1378
  parentStepId: stepState.stepId,
1766
- pikkuUserId: rpcService.wire?.pikkuUserId,
1379
+ pikkuUserId: run.wire?.pikkuUserId,
1767
1380
  }
1768
1381
  const shouldInline = !getSingletonServices()?.queueService
1769
1382
  const { runId: childRunId } = await this.startWorkflow(
@@ -1838,7 +1451,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1838
1451
 
1839
1452
  public async orchestrateWorkflow(
1840
1453
  runId: string,
1841
- rpcService: any
1454
+ rpcService: PikkuRPC
1842
1455
  ): Promise<void> {
1843
1456
  try {
1844
1457
  await this.runWorkflowJob(runId, rpcService)
@@ -1885,7 +1498,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1885
1498
  stepState: StepState,
1886
1499
  rpcName: string,
1887
1500
  data: any,
1888
- rpcService: any,
1501
+ rpcService: PikkuRPC,
1889
1502
  knownRun?: WorkflowRun | null
1890
1503
  ): Promise<any> {
1891
1504
  const run = knownRun ?? (await this.getRunIdentity(runId))
@@ -1943,7 +1556,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1943
1556
  runId: string,
1944
1557
  stepName: string,
1945
1558
  onErrorRpcName: string,
1946
- rpcService: any,
1559
+ rpcService: PikkuRPC,
1947
1560
  error: Error
1948
1561
  ): Promise<void> {
1949
1562
  await this.rpcStep(
@@ -1961,7 +1574,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1961
1574
  logicalStepName: string,
1962
1575
  rpcName: string,
1963
1576
  data: any,
1964
- rpcService: any,
1577
+ rpcService: PikkuRPC,
1965
1578
  stepOptions?: WorkflowStepOptions
1966
1579
  ): Promise<any> {
1967
1580
  const fromStepName = this.lastStepName(runId)
@@ -2043,7 +1656,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2043
1656
  type: 'workflow',
2044
1657
  id: rpcName,
2045
1658
  parentRunId: runId,
2046
- pikkuUserId: rpcService.wire?.pikkuUserId,
1659
+ pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
2047
1660
  }
2048
1661
  const { runId: childRunId } = await this.startWorkflow(
2049
1662
  rpcName,
@@ -2182,59 +1795,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2182
1795
  await this.setStepResult(stepState.stepId, null)
2183
1796
  }
2184
1797
 
2185
- private getSuspendStepName(reason: string): string {
2186
- return `__workflow_suspend:${reason}`
2187
- }
2188
-
2189
1798
  private async suspendStep(runId: string, reason: string): Promise<void> {
2190
1799
  const fromStepName = this.lastStepName(runId)
2191
- const suspendStepName = this.nextStepKey(
2192
- runId,
2193
- this.getSuspendStepName(reason)
2194
- )
2195
- await this.withStepLock(runId, suspendStepName, async () => {
2196
- let stepState: StepState
2197
- try {
2198
- stepState = await this.getStepState(runId, suspendStepName)
2199
- } catch {
2200
- stepState = await this.insertStepState(
2201
- runId,
2202
- suspendStepName,
2203
- 'pikkuWorkflowSuspend',
2204
- {
2205
- reason,
2206
- },
2207
- undefined,
2208
- fromStepName
2209
- )
2210
- }
2211
- if (!stepState.stepId) {
2212
- stepState = await this.insertStepState(
2213
- runId,
2214
- suspendStepName,
2215
- 'pikkuWorkflowSuspend',
2216
- {
2217
- reason,
2218
- },
2219
- undefined,
2220
- fromStepName
2221
- )
2222
- }
2223
-
2224
- if (stepState.status === 'succeeded') {
2225
- return
2226
- }
2227
-
2228
- if (stepState.status === 'pending') {
2229
- await this.setStepRunning(stepState.stepId)
2230
- }
2231
-
2232
- await this.setStepResult(stepState.stepId, {
1800
+ const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason))
1801
+ await this.withStepLock(runId, suspendStepName, () =>
1802
+ recordSuspension(
1803
+ this.approvalStore,
1804
+ runId,
2233
1805
  reason,
2234
- suspendedAt: new Date().toISOString(),
2235
- })
2236
- throw new WorkflowSuspendedException(runId, reason)
2237
- })
1806
+ suspendStepName,
1807
+ fromStepName
1808
+ )
1809
+ )
2238
1810
  }
2239
1811
 
2240
1812
  private async scheduleRunWake(runId: string, delay: number): Promise<void> {
@@ -2259,49 +1831,51 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2259
1831
  }
2260
1832
  }
2261
1833
 
2262
- private getApprovalStepName(reason: string): string {
2263
- return `__workflow_approval:${reason}`
2264
- }
2265
-
2266
- private approvalStateKey(stepName: string): string {
2267
- let hex = ''
2268
- for (const byte of new TextEncoder().encode(stepName)) {
2269
- hex += byte.toString(16).padStart(2, '0')
1834
+ private get approvalStore(): ApprovalStore {
1835
+ return {
1836
+ getStepState: (runId, stepName) => this.getStepState(runId, stepName),
1837
+ insertStepState: (
1838
+ runId,
1839
+ stepName,
1840
+ rpcName,
1841
+ input,
1842
+ output,
1843
+ fromStepName
1844
+ ) =>
1845
+ this.insertStepState(
1846
+ runId,
1847
+ stepName,
1848
+ rpcName,
1849
+ input,
1850
+ output,
1851
+ fromStepName
1852
+ ),
1853
+ setStepRunning: (stepId) => this.setStepRunning(stepId),
1854
+ setStepResult: (stepId, result) => this.setStepResult(stepId, result),
1855
+ getRunState: (runId) => this.getRunState(runId),
1856
+ updateRunState: (runId, key, value) =>
1857
+ this.updateRunState(runId, key, value),
1858
+ resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1859
+ scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
1860
+ getRunOwner: async (runId) =>
1861
+ (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1862
+ auditApproval: (event) => auditApprovalDecision(event),
2270
1863
  }
2271
- return `__approval_${hex}`
2272
1864
  }
2273
1865
 
2274
1866
  public async approveStep(
2275
1867
  runId: string,
2276
1868
  reason: string,
2277
- decision: unknown
1869
+ decision: unknown,
1870
+ session?: CoreUserSession
2278
1871
  ): Promise<void> {
2279
- const stepName = this.getApprovalStepName(reason)
2280
- const stateKey = this.approvalStateKey(stepName)
2281
-
2282
- let resolved: StepState | undefined
2283
- try {
2284
- resolved = await this.getStepState(runId, stepName)
2285
- } catch {
2286
- // knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
2287
- }
2288
- if (resolved?.stepId && resolved.status === 'succeeded') {
2289
- const outcome = resolved.result as ApprovalOutcome<unknown> | undefined
2290
- throw new WorkflowApprovalResolvedError(
2291
- reason,
2292
- outcome?.status ?? 'decided'
2293
- )
2294
- }
2295
-
2296
- const state = await this.getRunState(runId)
2297
- const record = (state[stateKey] ?? {}) as Record<string, unknown>
2298
- await this.updateRunState(runId, stateKey, {
2299
- ...record,
1872
+ return recordApprovalDecision(
1873
+ this.approvalStore,
1874
+ runId,
1875
+ reason,
2300
1876
  decision,
2301
- decidedAt: new Date().toISOString(),
2302
- error: undefined,
2303
- })
2304
- await this.resumeWorkflow(runId)
1877
+ approvalDeciderFrom(session)
1878
+ )
2305
1879
  }
2306
1880
 
2307
1881
  private async approvalStep(
@@ -2312,100 +1886,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2312
1886
  const fromStepName = this.lastStepName(runId)
2313
1887
  const approvalStepName = this.nextStepKey(
2314
1888
  runId,
2315
- this.getApprovalStepName(reason)
1889
+ approvalStepNameFor(reason)
1890
+ )
1891
+ return await this.withStepLock(runId, approvalStepName, () =>
1892
+ evaluateApprovalStep(
1893
+ this.approvalStore,
1894
+ runId,
1895
+ reason,
1896
+ approvalStepName,
1897
+ fromStepName,
1898
+ options
1899
+ )
2316
1900
  )
2317
- return await this.withStepLock(runId, approvalStepName, async () => {
2318
- let stepState: StepState
2319
- try {
2320
- stepState = await this.getStepState(runId, approvalStepName)
2321
- } catch {
2322
- stepState = await this.insertStepState(
2323
- runId,
2324
- approvalStepName,
2325
- 'pikkuWorkflowApproval',
2326
- { reason, expiry: options.expiry },
2327
- undefined,
2328
- fromStepName
2329
- )
2330
- }
2331
- if (!stepState.stepId) {
2332
- stepState = await this.insertStepState(
2333
- runId,
2334
- approvalStepName,
2335
- 'pikkuWorkflowApproval',
2336
- { reason, expiry: options.expiry },
2337
- undefined,
2338
- fromStepName
2339
- )
2340
- }
2341
-
2342
- if (stepState.status === 'succeeded') {
2343
- return stepState.result as ApprovalOutcome<unknown>
2344
- }
2345
-
2346
- const stateKey = this.approvalStateKey(approvalStepName)
2347
- let record = ((await this.getRunState(runId))[stateKey] ?? {}) as {
2348
- decision?: unknown
2349
- decidedAt?: string
2350
- expiresAt?: string
2351
- error?: unknown
2352
- }
2353
-
2354
- if (stepState.status === 'pending') {
2355
- await this.setStepRunning(stepState.stepId)
2356
- if (options.expiry !== undefined && !record.expiresAt) {
2357
- const expiresAt = new Date(
2358
- Date.now() + getDurationInMilliseconds(options.expiry)
2359
- ).toISOString()
2360
- record = { ...record, expiresAt }
2361
- await this.updateRunState(runId, stateKey, record)
2362
- await this.scheduleRunWake(
2363
- runId,
2364
- getDurationInMilliseconds(options.expiry)
2365
- )
2366
- }
2367
- }
2368
-
2369
- if (record.decision !== undefined) {
2370
- const validation = await options.schema['~standard'].validate(
2371
- record.decision
2372
- )
2373
- if (validation.issues) {
2374
- await this.updateRunState(runId, stateKey, {
2375
- ...record,
2376
- decision: undefined,
2377
- decidedAt: undefined,
2378
- error: validation.issues.map((issue) => ({
2379
- message: issue.message,
2380
- path: issue.path?.map((segment) =>
2381
- typeof segment === 'object' ? segment.key : segment
2382
- ),
2383
- })),
2384
- })
2385
- throw new WorkflowSuspendedException(runId, reason)
2386
- }
2387
- const outcome: ApprovalOutcome<unknown> = {
2388
- status: 'decided',
2389
- data: validation.value,
2390
- }
2391
- await this.setStepResult(stepState.stepId, outcome)
2392
- return outcome
2393
- }
2394
-
2395
- if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
2396
- const outcome: ApprovalOutcome<unknown> = { status: 'expired' }
2397
- await this.setStepResult(stepState.stepId, outcome)
2398
- return outcome
2399
- }
2400
-
2401
- throw new WorkflowSuspendedException(runId, reason)
2402
- })
2403
1901
  }
2404
1902
 
2405
1903
  public createWorkflowWire(
2406
1904
  name: string,
2407
1905
  runId: string,
2408
- rpcService: any,
1906
+ rpcService: PikkuRPC,
2409
1907
  addonNamespace?: string | null
2410
1908
  ): PikkuWorkflowWire {
2411
1909
  const workflowWire: PikkuWorkflowWire = {
@@ -2478,45 +1976,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2478
1976
  }
2479
1977
 
2480
1978
  private getConfig(): WorkflowServiceConfig {
2481
- const singletonServices = getSingletonServices()
2482
- const workflow = singletonServices.config?.workflow
2483
- return {
2484
- retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
2485
- retryDelay: workflow?.retryDelay ?? 0,
2486
- orchestratorQueueName:
2487
- workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
2488
- stepWorkerQueueName:
2489
- workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
2490
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
2491
- }
1979
+ return resolveWorkflowConfig()
2492
1980
  }
2493
1981
 
2494
1982
  protected getOrchestratorQueueName(workflowName?: string): string {
2495
- if (workflowName && this.queueStrategy !== 'shared-groups') {
2496
- const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
2497
- const meta = pikkuState(null, 'queue', 'meta')
2498
- if (meta[perWorkflow]) {
2499
- return perWorkflow
2500
- }
2501
- }
2502
- return this.getConfig().orchestratorQueueName
1983
+ return orchestratorQueueName(this.queueStrategy, workflowName)
2503
1984
  }
2504
1985
 
2505
1986
  protected getStepWorkerQueueName(rpcName?: string): string {
2506
- if (rpcName && this.queueStrategy !== 'shared-groups') {
2507
- const perStep = `wf-step-${toKebab(rpcName)}`
2508
- const meta = pikkuState(null, 'queue', 'meta')
2509
- if (meta[perStep]) {
2510
- return perStep
2511
- }
2512
- }
2513
- return this.getConfig().stepWorkerQueueName
1987
+ return stepWorkerQueueName(this.queueStrategy, rpcName)
2514
1988
  }
2515
1989
 
2516
1990
  protected getJobGroup(id?: string): JobGroup | undefined {
2517
- if (!id || this.queueStrategy !== 'shared-groups') {
2518
- return undefined
2519
- }
2520
- return { id, tier: id }
1991
+ return jobGroupFor(this.queueStrategy, id)
2521
1992
  }
2522
1993
  }