@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
@@ -0,0 +1,105 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync } from 'node:fs'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+ import { dirname, join, resolve } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+ const packageRoot = join(srcRoot, '..')
9
+
10
+ const packageJson = JSON.parse(
11
+ readFileSync(join(packageRoot, 'package.json'), 'utf-8')
12
+ )
13
+ const expected: Record<string, string[]> = JSON.parse(
14
+ readFileSync(join(srcRoot, 'public-surface.json'), 'utf-8')
15
+ )
16
+
17
+ const subpaths = packageJson.exports as Record<string, string>
18
+
19
+ /**
20
+ * Every runtime export reachable through an entry point, keyed by subpath.
21
+ *
22
+ * Types are absent: they are erased before this can see them, so a type-only
23
+ * addition is not pinned here.
24
+ */
25
+ const actualSurface = async () => {
26
+ const surface: Record<string, string[]> = {}
27
+ for (const [subpath, dist] of Object.entries(subpaths)) {
28
+ const src = resolve(packageRoot, dist.replace('./dist/', './src/'))
29
+ const module = await import(pathToFileURL(src).href)
30
+ surface[subpath] = Object.keys(module).sort()
31
+ }
32
+ return surface
33
+ }
34
+
35
+ const REGENERATE =
36
+ 'Regenerate with the snippet in public-surface.json.README, and make sure ' +
37
+ 'every added name is one @pikku/core means to publish.'
38
+
39
+ describe('the published surface is the surface we meant to publish', () => {
40
+ test('every export subpath resolves to a source file', () => {
41
+ const missing = Object.entries(subpaths).filter(([, dist]) => {
42
+ try {
43
+ readFileSync(
44
+ resolve(packageRoot, dist.replace('./dist/', './src/')).replace(
45
+ /\.js$/,
46
+ '.ts'
47
+ )
48
+ )
49
+ return false
50
+ } catch {
51
+ return true
52
+ }
53
+ })
54
+
55
+ assert.deepEqual(
56
+ missing.map(([subpath]) => subpath),
57
+ [],
58
+ 'package.json exports point at files that do not exist in src'
59
+ )
60
+ })
61
+
62
+ test('no entry point gained or lost a runtime export', async () => {
63
+ const actual = await actualSurface()
64
+
65
+ assert.deepEqual(
66
+ Object.keys(actual).sort(),
67
+ Object.keys(expected).sort(),
68
+ `the set of export subpaths changed. ${REGENERATE}`
69
+ )
70
+
71
+ for (const subpath of Object.keys(expected)) {
72
+ const added = actual[subpath]!.filter(
73
+ (name) => !expected[subpath]!.includes(name)
74
+ )
75
+ const removed = expected[subpath]!.filter(
76
+ (name) => !actual[subpath]!.includes(name)
77
+ )
78
+
79
+ assert.deepEqual(
80
+ { added, removed },
81
+ { added: [], removed: [] },
82
+ `'${subpath}' changed its public exports.\n` +
83
+ (added.length ? ` added: ${added.join(', ')}\n` : '') +
84
+ (removed.length ? ` removed: ${removed.join(', ')}\n` : '') +
85
+ `${REGENERATE}`
86
+ )
87
+ }
88
+ })
89
+
90
+ test('the wildcard re-exports have not widened past their barrels', async () => {
91
+ // `errors/index.ts` and `dev/hot-reload.ts` re-export with `export *`, so a
92
+ // new export in the wrapped module is published without anyone editing an
93
+ // entry point. This pins the two that are reachable publicly.
94
+ const actual = await actualSurface()
95
+
96
+ assert.equal(
97
+ actual['./errors']!.every(
98
+ (name) => name.endsWith('Error') || name.startsWith('addError')
99
+ ),
100
+ true,
101
+ `'@pikku/core/errors' exports something that is not an error or an error ` +
102
+ `registrar: ${actual['./errors']!.filter((n) => !n.endsWith('Error') && !n.startsWith('addError')).join(', ')}`
103
+ )
104
+ })
105
+ })
@@ -0,0 +1,62 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync, readdirSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { join, dirname, relative } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+ const thisFile = fileURLToPath(import.meta.url)
9
+
10
+ /**
11
+ * Names removed from the public surface, each with the entry point that must
12
+ * no longer carry it.
13
+ *
14
+ * They survived as throwing stubs only so the pinned bootstrap CLI could
15
+ * resolve the import; `@pikku/cli@0.12.96` emits none of them, so the stubs
16
+ * went too.
17
+ */
18
+ const REMOVED = [
19
+ { name: 'addTagPermission', entry: './middleware/index.js' },
20
+ { name: 'addHTTPPermission', entry: './wirings/http/index.js' },
21
+ { name: 'ZodLike', entry: './index.js' },
22
+ ]
23
+
24
+ const collectSourceFiles = (directory: string): string[] =>
25
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
26
+ const entryPath = join(directory, entry.name)
27
+ if (entry.isDirectory()) {
28
+ return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
29
+ }
30
+ return /\.ts$/.test(entry.name) ? [entryPath] : []
31
+ })
32
+
33
+ describe('removed legacy exports stay removed', () => {
34
+ for (const { name, entry } of REMOVED) {
35
+ test(`${entry} no longer exports ${name}`, async () => {
36
+ const module = await import(entry)
37
+ assert.equal(
38
+ name in module,
39
+ false,
40
+ `${entry} exports ${name} again; it was removed in #972`
41
+ )
42
+ })
43
+ }
44
+
45
+ test('no source file mentions the removed permission wrappers', () => {
46
+ const offenders = collectSourceFiles(srcRoot)
47
+ .filter(
48
+ (file) =>
49
+ file !== thisFile &&
50
+ /\baddTagPermission\b|\baddHTTPPermission\b|\bZodLike\b/.test(
51
+ readFileSync(file, 'utf-8')
52
+ )
53
+ )
54
+ .map((file) => relative(srcRoot, file))
55
+
56
+ assert.deepEqual(
57
+ offenders,
58
+ [],
59
+ `removed names still referenced in:\n${offenders.join('\n')}`
60
+ )
61
+ })
62
+ })
@@ -265,6 +265,84 @@ describe('Schema', () => {
265
265
  await validateSchema(logger, schemaService, null, {})
266
266
  })
267
267
 
268
+ test('strips undefined-valued properties before validating', async () => {
269
+ addSchema('undefinedProps', {
270
+ properties: { name: { type: 'string' }, count: { type: 'number' } },
271
+ })
272
+ const logger = {
273
+ info: () => {},
274
+ warn: () => {},
275
+ error: () => {},
276
+ debug: () => {},
277
+ } as any
278
+ let seen: any
279
+ const schemaService = {
280
+ compileSchema: async () => {},
281
+ validateSchema: async (_name: string, data: any) => {
282
+ seen = data
283
+ },
284
+ getSchemaNames: () => new Set<string>(),
285
+ }
286
+ await validateSchema(logger, schemaService, 'undefinedProps', {
287
+ name: 'ada',
288
+ count: undefined,
289
+ })
290
+ assert.deepStrictEqual(seen, { name: 'ada' })
291
+ assert.ok(!('count' in seen))
292
+ })
293
+
294
+ test('strips undefined nested in objects and arrays', async () => {
295
+ addSchema('nestedUndefined', { properties: {} })
296
+ const logger = {
297
+ info: () => {},
298
+ warn: () => {},
299
+ error: () => {},
300
+ debug: () => {},
301
+ } as any
302
+ let seen: any
303
+ const schemaService = {
304
+ compileSchema: async () => {},
305
+ validateSchema: async (_name: string, data: any) => {
306
+ seen = data
307
+ },
308
+ getSchemaNames: () => new Set<string>(),
309
+ }
310
+ await validateSchema(logger, schemaService, 'nestedUndefined', {
311
+ nested: { keep: 1, drop: undefined },
312
+ list: [{ keep: 2, drop: undefined }],
313
+ })
314
+ assert.deepStrictEqual(seen, {
315
+ nested: { keep: 1 },
316
+ list: [{ keep: 2 }],
317
+ })
318
+ })
319
+
320
+ test('leaves Date instances intact while stripping', async () => {
321
+ addSchema('withDate', { properties: {} })
322
+ const logger = {
323
+ info: () => {},
324
+ warn: () => {},
325
+ error: () => {},
326
+ debug: () => {},
327
+ } as any
328
+ const when = new Date('2020-01-01T00:00:00.000Z')
329
+ let seen: any
330
+ const schemaService = {
331
+ compileSchema: async () => {},
332
+ validateSchema: async (_name: string, data: any) => {
333
+ seen = data
334
+ },
335
+ getSchemaNames: () => new Set<string>(),
336
+ }
337
+ await validateSchema(logger, schemaService, 'withDate', {
338
+ when,
339
+ drop: undefined,
340
+ })
341
+ assert.ok(seen.when instanceof Date)
342
+ assert.equal(seen.when.getTime(), when.getTime())
343
+ assert.ok(!('drop' in seen))
344
+ })
345
+
268
346
  test('should throw MissingSchemaError when schema not found', async () => {
269
347
  const logger = {
270
348
  info: () => {},
package/src/schema.ts CHANGED
@@ -135,6 +135,41 @@ export const coerceTopLevelDataFromSchema = (
135
135
  }
136
136
  }
137
137
 
138
+ /**
139
+ * Drops object properties whose value is literally `undefined`.
140
+ *
141
+ * JSON Schema has no way to describe such a property, and the validator refuses
142
+ * the whole instance rather than reporting it — `{ a: undefined }` throws
143
+ * "Instances of \"undefined\" type are not supported" instead of validating.
144
+ *
145
+ * That matters because whether a payload can contain one depends on how the
146
+ * call travelled. Over HTTP `JSON.stringify` drops these keys before the
147
+ * request is built, so they never arrive. Dispatched in-process — an inline
148
+ * workflow step calling an RPC — the object is handed over as-is, and
149
+ * `{ retries: data.maybeRetries }` with the field omitted by the caller is
150
+ * enough to kill the step. Normalising here makes the two paths agree, and an
151
+ * explicitly-undefined *required* field now reports as the missing property it
152
+ * is rather than as an internal error.
153
+ *
154
+ * Deliberately not a JSON round-trip: `coerceTopLevelDataFromSchema` puts real
155
+ * `Date` instances on the data first, and stringifying would flatten them back
156
+ * to strings the validator has never been given.
157
+ */
158
+ const stripUndefinedValues = <T>(value: T): T => {
159
+ if (Array.isArray(value)) return value.map(stripUndefinedValues) as T
160
+ if (value === null || typeof value !== 'object') return value
161
+ // Only plain objects: a Date, Map or class instance is data, not a bag of
162
+ // properties to be rewritten.
163
+ const prototype = Object.getPrototypeOf(value)
164
+ if (prototype !== Object.prototype && prototype !== null) return value
165
+
166
+ return Object.fromEntries(
167
+ Object.entries(value)
168
+ .filter(([, entry]) => entry !== undefined)
169
+ .map(([key, entry]) => [key, stripUndefinedValues(entry)])
170
+ ) as T
171
+ }
172
+
138
173
  export const validateSchema = async (
139
174
  logger: Logger,
140
175
  schemaService: SchemaService | undefined,
@@ -164,6 +199,6 @@ export const validateSchema = async (
164
199
  )
165
200
  }
166
201
  await schemaService.compileSchema(key, schema)
167
- await schemaService.validateSchema(key, data ?? {})
202
+ await schemaService.validateSchema(key, stripUndefinedValues(data ?? {}))
168
203
  }
169
204
  }
@@ -10,10 +10,16 @@ export interface AIRunStateService {
10
10
  updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>
11
11
  getRun(runId: string): Promise<AgentRunState | null>
12
12
  getRunsByThread(threadId: string): Promise<AgentRunState[]>
13
+ /**
14
+ * Claims an approval: removes it from the run's pending list and records the
15
+ * decision, as one atomic step. Returns whether THIS caller made the claim —
16
+ * concurrent approvals of the same tool call produce exactly one `true`, and
17
+ * only that caller may run the tool.
18
+ */
13
19
  resolveApproval(
14
20
  toolCallId: string,
15
21
  status: 'approved' | 'denied'
16
- ): Promise<void>
22
+ ): Promise<boolean>
17
23
  findRunByToolCallId(
18
24
  toolCallId: string
19
25
  ): Promise<{ run: AgentRunState; approval: PendingApproval } | null>
@@ -150,7 +150,7 @@ class DisabledInvocationAudit implements AuditLog {
150
150
  if (!this.warned) {
151
151
  this.warned = true
152
152
  // knowledge: decisions/security/a-dropped-audit-write-is-always-logged.md
153
- const logger = (this.wire as any).logger ?? this.logger
153
+ const logger = this.wire.logger ?? this.logger
154
154
  logger?.warn?.(
155
155
  `audit.write() dropped for '${this.wire.functionId ?? 'unknown function'}' — the function has no audit config (set audit: true on it)`
156
156
  )
@@ -207,7 +207,7 @@ class InvocationAuditLog implements AuditLog {
207
207
  await this.service.audit(event)
208
208
  }
209
209
  } catch (error) {
210
- const logger = (this.wire as any).logger ?? this.logger
210
+ const logger = this.wire.logger ?? this.logger
211
211
  logger?.warn?.('best-effort audit flush failed', error)
212
212
  }
213
213
  }
@@ -38,7 +38,7 @@ export class InMemoryAIRunStateService implements AIRunStateService {
38
38
  async resolveApproval(
39
39
  toolCallId: string,
40
40
  status: 'approved' | 'denied'
41
- ): Promise<void> {
41
+ ): Promise<boolean> {
42
42
  for (const run of this.runs.values()) {
43
43
  if (run.pendingApprovals) {
44
44
  const approval = run.pendingApprovals.find(
@@ -49,10 +49,11 @@ export class InMemoryAIRunStateService implements AIRunStateService {
49
49
  (a) => a.toolCallId !== toolCallId
50
50
  )
51
51
  run.updatedAt = new Date()
52
- return
52
+ return true
53
53
  }
54
54
  }
55
55
  }
56
+ return false
56
57
  }
57
58
 
58
59
  async findRunByToolCallId(
@@ -142,6 +142,7 @@ export class InMemoryWorkflowService
142
142
  const step: StepState & { stepName: string } = {
143
143
  stepId,
144
144
  status: 'pending',
145
+ rpcName,
145
146
  attemptCount: 1,
146
147
  retries: stepOptions?.retries,
147
148
  retryDelay: stepOptions?.retryDelay,
@@ -273,6 +274,7 @@ export class InMemoryWorkflowService
273
274
  const newStep: StepState & { stepName: string } = {
274
275
  stepId: newStepId,
275
276
  status,
277
+ rpcName: failedStep.rpcName,
276
278
  attemptCount: failedStep.attemptCount + 1,
277
279
  retries: failedStep.retries,
278
280
  retryDelay: failedStep.retryDelay,
@@ -301,6 +303,29 @@ export class InMemoryWorkflowService
301
303
  return newStep
302
304
  }
303
305
 
306
+ /**
307
+ * Safe to opt in despite the pass-through `withStepLock`: this service wires
308
+ * no queues and runs every step inline in one process, so the relay's
309
+ * redundant dispatch has no second holder to race with. See
310
+ * knowledge: decisions/internals/the-in-memory-workflow-service-is-inline-only-and-single-process.md
311
+ */
312
+ protected async findUndispatchedSteps(
313
+ before: Date,
314
+ limit: number
315
+ ): Promise<Array<{ runId: string; stepId: string }>> {
316
+ const undispatched: Array<{ runId: string; stepId: string }> = []
317
+ for (const [runId, run] of this.runs) {
318
+ if (run.status !== 'running') continue
319
+ for (const step of this.stepHistory.get(runId) ?? []) {
320
+ if (step.status !== 'pending') continue
321
+ if (step.updatedAt >= before) continue
322
+ undispatched.push({ runId, stepId: step.stepId })
323
+ if (undispatched.length >= limit) return undispatched
324
+ }
325
+ }
326
+ return undispatched
327
+ }
328
+
304
329
  protected async findStalledRunIds(
305
330
  before: Date,
306
331
  limit: number
@@ -45,10 +45,7 @@ export type {
45
45
  ScenarioRpcMap,
46
46
  ScenarioHttpResponse,
47
47
  } from './personas-service.js'
48
- // The persona runtime — `HttpPersona`, `createHttpPersonas`,
49
- // `readScenarioHttpResponse`, `postScenarioJson` — is exported from
50
- // `@pikku/core/persona`, not from here. Types are free; those are values, and
51
- // they reach the actor-flow and agent runners, which no production server runs.
48
+ // knowledge: decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md
52
49
  export type { HttpPersonasConfig } from './http-personas.js'
53
50
  export type { JWTService } from './jwt-service.js'
54
51
  export type {
@@ -81,11 +81,17 @@ describe('createLocalContentRequestHandler', () => {
81
81
  test('refuses an upload whose key escapes the content directory', async () => {
82
82
  writeFileSync(join(tmpDir, '..', 'pikku-handler-secret.txt'), 'secret')
83
83
  try {
84
+ // Signed for the escaping path itself, so it clears the signature gate
85
+ // and the path-escape guard is what rejects it.
86
+ const signed = await content.signURL({
87
+ url: '/upload/bucket/..%2f..%2fpikku-handler-secret.txt',
88
+ dateLessThan: new Date(Date.now() + 60_000),
89
+ } as any)
84
90
  const response = await handler(
85
- new Request(
86
- 'http://localhost/upload/bucket/..%2f..%2fpikku-handler-secret.txt',
87
- { method: 'PUT', body: 'overwritten' }
88
- )
91
+ new Request(`http://localhost${signed}`, {
92
+ method: 'PUT',
93
+ body: 'overwritten',
94
+ })
89
95
  )
90
96
  assert.equal(response?.status, 400)
91
97
  assert.equal(
@@ -104,6 +110,33 @@ describe('createLocalContentRequestHandler', () => {
104
110
  assert.equal(response?.status, 400)
105
111
  })
106
112
 
113
+ test('refuses an unsigned PUT to the upload prefix', async () => {
114
+ const put = await handler(
115
+ new Request('http://localhost/upload/bucket/evil.bin', {
116
+ method: 'PUT',
117
+ body: 'attacker-controlled',
118
+ })
119
+ )
120
+ assert.equal(put?.status, 403, 'an upload URL must be signed like a read')
121
+ })
122
+
123
+ test('refuses a PUT whose signature was minted for a different key', async () => {
124
+ const { uploadUrl } = await content.getUploadURL({
125
+ bucket: 'bucket',
126
+ fileKey: 'legit/one.bin',
127
+ contentType: 'application/octet-stream',
128
+ } as any)
129
+ const query = uploadUrl.slice(uploadUrl.indexOf('?'))
130
+ // Same signature, a different target path.
131
+ const put = await handler(
132
+ new Request(`http://localhost/upload/bucket/other.bin${query}`, {
133
+ method: 'PUT',
134
+ body: 'payload',
135
+ })
136
+ )
137
+ assert.equal(put?.status, 403)
138
+ })
139
+
107
140
  test('round-trips an upload and a signed read', async () => {
108
141
  const { uploadUrl } = await content.getUploadURL({
109
142
  bucket: 'bucket',
@@ -188,8 +221,13 @@ describe('createLocalContentRequestHandler', () => {
188
221
  })
189
222
 
190
223
  test('rejects a body over the size limit without writing it', async () => {
224
+ const { uploadUrl } = await content.getUploadURL({
225
+ bucket: 'bucket',
226
+ fileKey: 'big.bin',
227
+ contentType: 'application/octet-stream',
228
+ } as any)
191
229
  const response = await handler(
192
- new Request('http://localhost/upload/bucket/big.bin', {
230
+ new Request(`http://localhost${uploadUrl}`, {
193
231
  method: 'PUT',
194
232
  body: Buffer.alloc(1024 * 1024 + 1),
195
233
  })