@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,66 @@
1
+ ---
2
+ type: decision
3
+ title: An unref'd timer cannot be awaited under node:test
4
+ description: The channel RPC registry unrefs its timeout so a pending call never holds a process open, which makes that timeout unawaitable in a test and silently cancels the rest of the file
5
+ tags: core, channel, testing
6
+ ---
7
+
8
+ # An unref'd timer cannot be awaited under node:test
9
+
10
+ `ChannelRPCRegistry.register()` schedules the call's timeout and then unrefs it:
11
+
12
+ ```ts
13
+ // Never hold the process open waiting on a peer that may not answer.
14
+ timer?.unref?.()
15
+ ```
16
+
17
+ That is correct, and should stay. A server shutting down must not be held open
18
+ for up to 30 seconds per in-flight reverse RPC.
19
+
20
+ It also means the timeout can never fire in a test that awaits it. An unref'd
21
+ timer does not keep the event loop alive, so when the pending call is the only
22
+ thing left the loop drains first, and node:test reports:
23
+
24
+ ```
25
+ Promise resolution is still pending but the event loop has already resolved
26
+ ```
27
+
28
+ The damage is not local. That one test hangs, the runner cancels the **file**,
29
+ and every later test in it is reported `cancelledByParent` — 32 of them in
30
+ `channel-rpc.test.ts`, of which only two were the actual problem. Worse, the
31
+ count moves: under a full-suite run other ref'd handles sometimes keep the loop
32
+ alive long enough for the timer to land, so the file reported `32 cancelled` or
33
+ `1 fail + 31 cancelled` depending on timing. Two of the "failures" were tests
34
+ that pass perfectly well on their own.
35
+
36
+ Reproducible with no project code at all — a bare `node --test` file where one
37
+ test awaits a ref'd timer and one awaits an unref'd one passes the first and
38
+ cancels the second.
39
+
40
+ **The fix belongs in the test, not the registry.** Use the test-scoped mock
41
+ clock and drive it by hand:
42
+
43
+ ```ts
44
+ test('times out instead of hanging forever', async (t) => {
45
+ t.mock.timers.enable({ apis: ['setTimeout'] })
46
+ const registry = new ChannelRPCRegistry(10)
47
+ const call = registry.register()
48
+
49
+ const rejected = assert.rejects(call.promise, ...) // attach BEFORE ticking
50
+ t.mock.timers.tick(10)
51
+ await rejected
52
+ })
53
+ ```
54
+
55
+ `t.mock.timers` is scoped to the test and resets itself, so tests that want a
56
+ real clock are unaffected. Attach the rejection handler before ticking, or the
57
+ rejection lands unhandled.
58
+
59
+ **What this rules out:** dropping the `unref` to make the tests pass. That
60
+ trades a test-only annoyance for a production one — every in-flight call would
61
+ then pin the process during shutdown. Also rules out reading a cancelled count
62
+ in this file as a count of broken tests; check which test actually hangs first,
63
+ because the rest are collateral.
64
+
65
+ Related: [[the-api-report-pins-members-not-just-names]] for the other case where
66
+ a measurement in this repo counted the wrong thing convincingly.
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: decision
3
+ title: `getState<T>()` lets the caller name any type, so every implementation asserts
4
+ description: The type parameter appears only in the return position with nothing to infer from, so it is a caller-chosen claim rather than a checked fact
5
+ tags: core, channel
6
+ ---
7
+
8
+ # `getState<T>()` lets the caller name any type, so every implementation asserts
9
+
10
+ `PikkuChannel` declares its per-connection scratch state as:
11
+
12
+ ```ts
13
+ setState<T>(state: T): Promise<void> | void
14
+ getState<T>(): Promise<T | undefined> | T | undefined
15
+ ```
16
+
17
+ `T` on `getState` appears only in the return position, with no parameter to
18
+ infer it from. So `T` is whatever the caller writes, and the call site gets that
19
+ type back with nothing having checked it. `setState<T>` is separately generic,
20
+ so the two are not even tied to each other — a channel can be written with one
21
+ shape and read as another and neither call complains.
22
+
23
+ Every implementation therefore holds its state in a concrete variable and
24
+ asserts on the way out. The SSE channel in `http-runner.ts` and the CLI channel
25
+ in `cli-runner.ts` both do exactly that, and the assertion is unavoidable: no
26
+ concrete value is assignable to a type the caller has not yet chosen.
27
+
28
+ **What this rules out:** deleting the assertions in the implementations — they
29
+ are forced by the signature, not by sloppiness. Fixing this properly means
30
+ making the channel generic in its state type (`PikkuChannel<Opening, Out,
31
+ Remote, State>`) so `setState` and `getState` agree and inference has something
32
+ to work from, which changes the type of every channel in every app. Until that
33
+ is worth doing, the assertions stay and this note explains why they exist.
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: decision
3
+ title: Gateway middleware sees wire.rpc on webhook and websocket, but not on listener
4
+ description: The listener transport invokes its handler directly, so middleware runs outside any runPikkuFunc call and the lazily-attached rpc is absent
5
+ tags: core, gateway
6
+ ---
7
+
8
+ # Gateway middleware sees `wire.rpc` on webhook and websocket, but not on listener
9
+
10
+ `runPikkuFunc` in `packages/core/src/function/function-runner.ts` attaches `rpc`
11
+ to the invocation wire with `Object.defineProperty`, as a lazy getter that
12
+ replaces itself on first read — and restores the previous descriptor in its
13
+ `finally`. `rpc` therefore exists on a wire *only for the duration of a function
14
+ invocation*. That is why `PikkuRawWire` is `Omit<PikkuWire, 'rpc'>`: it is the
15
+ wire as a runner constructs it, before the function runner adds `rpc`.
16
+
17
+ The three gateway transports reach middleware differently:
18
+
19
+ - **webhook** and **websocket** register their handlers with `addFunction`, so
20
+ the handler body already runs inside a `runPikkuFunc` invocation. The `wire`
21
+ it receives is a full `PikkuWire`, and `config.middleware` runs inside that
22
+ same invocation — `wire.rpc` is live.
23
+ - **listener** has no such wrapper. `createListenerMessageHandler` is handed
24
+ straight to `adapter.init()` by the `GatewayService`, builds
25
+ `const wire: PikkuRawWire = {}` itself, and runs `config.middleware` *before*
26
+ `invoke()` reaches `runPikkuFunc`. There is no invocation in progress, so
27
+ `wire.rpc` is `undefined`.
28
+
29
+ A gateway middleware that calls `wire.rpc.invoke(...)` therefore works on two
30
+ transports and throws on the third, with nothing in the types to say so — the
31
+ call into `runMiddleware` needs an assertion, because `runMiddleware` is typed
32
+ off `CorePikkuMiddleware`, whose wire parameter is `PikkuWire`.
33
+
34
+ **What this rules out:** treating the assertion at that call as noise to be
35
+ deleted. It is naming a real gap. It also rules out "just widen
36
+ `CorePikkuMiddleware` to accept `PikkuRawWire`" as a free fix — that would make
37
+ `rpc` optional for *every* middleware in the framework, pushing the problem onto
38
+ every consumer to satisfy one transport.
39
+
40
+ **Still open:** whether the listener path should wrap its handler in a
41
+ `runPikkuFunc` invocation the way the other two do, which would make the three
42
+ transports behave identically and remove the assertion. Nobody has argued
43
+ against it; it simply has not been done.
@@ -0,0 +1,26 @@
1
+ ---
2
+ type: decision
3
+ title: Hot reload writes into the function map captured at startup, not pikkuState's current one
4
+ description: A dev-server watcher may have swapped in a codegen-scoped map whose writes are discarded on restore, and schemas are deliberately left alone
5
+ tags: core, dev
6
+ ---
7
+
8
+ # Hot reload writes into the function map captured at startup
9
+
10
+ `reloadGeneratedMeta` registers every function-config export it finds, replacing
11
+ known functions and adding new ones. It writes into the map captured when the
12
+ reloader started, **not** into whatever `pikkuState` currently returns.
13
+
14
+ A dev-server watcher may have temporarily swapped in a codegen-scoped map while
15
+ regenerating. Writes into that map are discarded when it restores the real one,
16
+ so a reload that used the current map would appear to succeed and silently lose
17
+ every function it registered.
18
+
19
+ Schemas are deliberately not touched on this path. A function config's
20
+ `input`/`output` hold raw schema objects as the author wrote them, while the
21
+ schema map carries the JSON Schema that codegen produced. They are different
22
+ representations of the same thing, and mixing them crashed reloads.
23
+
24
+ **What this rules out:** "simplifying" the reloader to call `pikkuState(...)` at
25
+ write time, and extending it to keep `input`/`output` and the schema map in sync
26
+ without first converting between the two representations.
@@ -10,13 +10,22 @@ A rule about how core behaves: what runs where, in what order, and what a
10
10
  caller is entitled to assume.
11
11
 
12
12
  <!-- pikku:knowledge-index -->
13
+ - [A non-streaming agent run registers with aiRunState on the same terms as a streaming one](a-non-streaming-agent-run-registers-with-airunstate-too.md) — Otherwise interruptAIAgent finds the run, passes the ownership check, then cannot stop it — and reports that as if the run were on another host
14
+ - [A resumed turn is as interruptible as the first one](a-resumed-agent-turn-is-as-interruptible-as-the-first.md) — It is the same person listening to the same voice, and after an approval it is where most of the reply actually gets spoken
15
+ - [A scenario step's prose template is offered to a virtual user unfilled](a-scenario-step-template-is-offered-unfilled.md) — A reporter fills placeholders from a run that happened; there is no run yet, and the filled form would answer the question the user is there to answer
13
16
  - [A secret that fails to decrypt fails the whole read](a-secret-that-fails-to-decrypt-fails-the-whole-read.md) — getSecrets throws naming the key and its key_version rather than omitting the row, because a silent omission surfaces as an unrelated failure much later
17
+ - [A virtual user decides whether to trust its notes once per turn, by one roll](a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md) — The difference between the stale, newcomer and auditor dispositions is expressed as a single probability rather than as prose in each prompt
18
+ - [A virtual user run is not a workflow and not a queued job](a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md) — runVirtualUser writes its record, dispatches the run without awaiting it, and returns the id — because an exploratory run has nothing to replay and the record already carries what a queue would be holding
19
+ - [A wall-clock threshold is a load test in disguise](a-wall-clock-threshold-is-a-load-test-in-disguise.md) — The KEK derivation test asserted a fixed 50ms budget for work that took 10ms, which went red about one run in five once the suite was large enough to compete for the machine
20
+ - [A workflow's wire is built from the run record, not from the RPC service](a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md) — The RPC service exposes no wire, so every rpcService.wire read was undefined; the run record is the only thing that carries the caller across a step boundary
14
21
  - [An actor conversation starts from a seeded kickoff message](actor-flow-conversations-seed-a-hidden-kickoff-message.md) — The actor's first turn needs a non-empty message list because providers reject an empty prompt; the seed is an instruction and stays out of the transcript
15
22
  - [The actor-flow conversation engine only sees a transport-agnostic target driver](actor-flow-drives-the-target-through-a-transport-seam.md) — The engine never imports the agent runner; the target is injected as run/approve, so scenarios exercise the real wire path
16
23
  - [An actor-flow verdict is the persona's self-evaluation, not an assertion](actor-flow-verdicts-are-llm-self-evaluations.md) — The engine returns what the actor judged plus the transcript; deterministic checks stay with the caller
17
24
  - [Addon package roots resolve by walking node module search paths](addon-package-roots-resolve-by-walking-node-module-search-paths.md) — LocalMetaService finds an addon's directory by scanning resolve.paths, not by require.resolve, because addon packages expose no main entry
18
25
  - [Addon singleton services are cached per namespace, not per package](addon-singleton-services-are-cached-per-namespace-not-per-package.md) — Each wireAddon instance gets its own services built from its own overrides, at the cost of one service graph per wired instance
19
26
  - [Bare workflow names from inside an addon are prefixed with the consumer's namespace](addon-workflow-names-are-prefixed-with-the-consumer-namespace.md) — The addon's workflowService is proxied so an addon never has to hardcode the name its consumer chose
27
+ - [Loading agent context waits for a tool result that may still be landing](agent-context-waits-for-a-tool-result-still-being-written.md) — An interrupted run's tool can still be writing to the thread, and in voice the next turn arrives within seconds — soon enough to load context missing what it is about to be asked about
28
+ - [Agent speech travels as a CUSTOM AG-UI event rather than being dropped](agent-speech-travels-as-a-custom-agui-event.md) — AG-UI has no speech event, and dropping it makes a voice agent reached over HTTP silently inaudible while the provider still bills for the audio
20
29
  - [The AG-UI bridge obeys the client's event-ordering contract exactly](ai-agent-agui-bridge-obeys-the-client-ordering-contract.md) — RUN_STARTED opens lazily, RUN_FINISHED fires once on done, and step names are globally sequential — a violation makes the client drop the whole stream
21
30
  - [Audio chunks are labelled with the format the provider actually returned](ai-agent-audio-chunks-carry-the-format-the-provider-returned.md) — The configured format is only a request, so the response's own format wins with the request and pcm16 as fallbacks
22
31
  - [A credential-required tool result never reaches the client or the transcript](ai-agent-credential-suspensions-hide-the-tool-result.md) — The run suspends with credential-request events instead, leaving the tool call unresulted so it can be resumed after connecting
@@ -30,88 +39,91 @@ caller is entitled to assume.
30
39
  - [A tool's execute() failure is logged before the AI SDK swallows it](ai-agent-tool-execute-failures-are-logged-unconditionally.md) — Every agent tool is wrapped in a logging try/catch, because a thrown tool error otherwise becomes a conversational reply and is invisible server-side
31
40
  - [Voice input transcribes audio parts sequentially and in place](ai-agent-voice-input-transcribes-audio-parts-in-place.md) — Each audio part is replaced by its text where it sat, one at a time, bounding concurrent downloads and preserving content order
32
41
  - [Working memory is persisted only when the merged value validates](ai-agent-working-memory-is-persisted-only-when-valid.md) — A failed schema check logs and drops the update rather than saving it, because invalid state poisons every later read
42
+ - [An interrupt is not a failure, and the non-streaming path throws rather than returning](an-agent-interrupt-is-not-a-failure.md) — It skips the onError hooks and never becomes an errorMessage; with no partial reply to hand back, a typed throw is what distinguishes it from a provider outage
43
+ - [An interrupt for a run owned by another instance says so, rather than returning false](an-agent-run-owned-by-another-instance-says-so.md) — A bare false is indistinguishable from "already finished", which is the one deployment shape the in-process registry cannot cover
44
+ - [A wrapped agent-stream send must return the inner send's promise](an-agent-stream-send-must-return-the-inner-sends-promise.md) — Middleware runs asynchronously, so dropping the returned promise turns every awaited channel.send upstream into a no-op
45
+ - [A message with nothing to say carries no text part at all](an-empty-text-part-is-omitted-from-an-agent-message.md) — An attachment on its own is a real turn, and providers are entitled to reject an empty text part sitting beside it
46
+ - [A transcript is recorded only when something was actually heard](an-empty-transcript-is-not-recorded.md) — Recording an empty string sends a transcript event saying the user said nothing, which renders as an empty bubble rather than a pending one
47
+ - [An unref'd timer cannot be awaited under node:test](an-unref-d-timer-cannot-be-awaited-under-node-test.md) — The channel RPC registry unrefs its timeout so a pending call never holds a process open, which makes that timeout unawaitable in a test and silently cancels the rest of the file
33
48
  - [Channel message handlers accept three config shapes](channel-message-handlers-accept-three-config-shapes.md) — onMessage may be a function config, a wrapper with middleware, or a wrapper around a function config — the runtime discriminates structurally
34
49
  - [Channel middleware caching covers only statically resolved middleware](channel-middleware-caches-only-statically-resolved-middleware.md) — Inherited tag/named middleware is cached per uid; per-run closures are appended fresh every call, at the cost of re-allocating the array
50
+ - [`getState<T>()` lets the caller name any type, so every implementation asserts](channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md) — The type parameter appears only in the return position with nothing to infer from, so it is a caller-chosen claim rather than a checked fact
35
51
  - [Channel state is per-socket, session state is per-user](channel-state-is-per-socket-session-state-is-per-user.md) — ChannelStore holds connection-scoped scratch data keyed by channelId, deliberately separate from the pikkuUserId-keyed SessionStore
36
52
  - [A channel's user id is persisted only after onConnect middleware has run](channel-user-id-is-persisted-after-onconnect-middleware-runs.md) — The channelId to pikkuUserId mapping is written post-onConnect, because auth middleware is what establishes the session
37
53
  - [CLI option names are camelCase in state and kebab-case on the command line](cli-option-names-are-camelcase-in-state-and-kebab-on-the-command-line.md) — Option keys match the function's input field names so they can be plucked by schema, and are converted to kebab only for display and parsing
38
54
  - [CLI parse errors are routed by message prefix](cli-parse-errors-are-routed-by-message-prefix.md) — The CLI runners decide between printing help and printing errors by string-matching the prefixes the parser writes, so those message strings are an interface
39
55
  - [CLI stdout is reserved for machine-readable output](cli-stdout-is-reserved-for-machine-readable-output.md) — The default renderer emits single-line NDJSON, diagnostics go to stderr, and --json only hijacks rendering for commands that declared a renderer
40
56
  - [CLI unknown long options warn instead of failing](cli-unknown-long-options-warn-instead-of-failing.md) — Unrecognised --long options are accepted, warned about and dropped so older binaries tolerate newer invocations, while unknown short flags stay hard errors
41
- - [A column's at-rest form is an axis of its own](core-column-form-is-an-axis-of-its-own.md) — How a value is stored is independent of how sensitive it is, so form carries a required nominal brand on writes while classification stays optional on reads
42
- - [The data-classification brand is an optional property](core-data-classification-brand-is-an-optional-property.md) — Making __classification__ required would break ordinary Kysely operands, so the brand only constrains values flowing out
43
57
  - [The function runner restores the wire fields it overwrites](core-function-runner-restores-the-wire-fields-it-overwrites.md) — One wire object is reused across nested calls, so functionId, audit, addonNamespace and rpc are saved and put back in a finally
44
58
  - [Hot reload merges generated meta and never replaces it](core-hot-reload-merges-generated-meta-never-replaces-it.md) — Reloading codegen output must preserve runtime-registered meta, which no generated JSON contains
45
59
  - [Hot reload owns its module registry instead of re-importing](core-hot-reload-owns-its-module-registry.md) — Dev reload transpiles to CJS and runs modules through vm.compileFunction, because the native ESM loader map cannot be evicted
46
60
  - [Middleware order is resolution scope first, then priority](core-middleware-order-is-scope-then-priority.md) — Middleware is collected global to function, then stably sorted by priority, deduped, frozen and cached per wire
47
- - [Schema defaults are applied on every transport, not just HTTP](core-schema-defaults-apply-on-every-transport.md) — Defaults belong to the schema rather than the call's encoding, so they run unconditionally and are cloned per request
48
61
  - [Scopes are an AND gate, separate from permissions](core-scopes-are-an-and-gate-separate-from-permissions.md) — Every declared scope must be held, so adding one can only narrow access — permissions OR, and can only widen it
49
- - [Pikku state is a global map written only at registration time](core-state-is-a-global-map-written-only-at-registration-time.md) — A symbol-keyed globalThis map holds the wiring registry; nothing per-request may ever be written to it
50
62
  - [Email meta is read uncached because codegen rewrites it mid-session](email-meta-is-read-uncached-because-codegen-rewrites-it-mid-session.md) — getEmailMeta re-reads its file on every call, unlike every other meta accessor, because the file appears and changes during a long-lived session
51
63
  - [Gateway adapters resolve lazily and are promise-cached](gateway-adapters-resolve-lazily-and-are-promise-cached.md) — wireGateway accepts an adapter factory because real adapters need boot-time secrets, which forces the webhook GET route to be registered unconditionally
64
+ - [Gateway middleware sees wire.rpc on webhook and websocket, but not on listener](gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md) — The listener transport invokes its handler directly, so middleware runs outside any runPikkuFunc call and the lazily-attached rpc is absent
52
65
  - [Gateway webhook challenges echo bytes not JSON](gateway-webhook-challenges-echo-bytes-not-json.md) — String verification challenges are returned raw with returnsJSON false, because platforms byte-compare the echo and JSON quoting fails the handshake
53
66
  - [Gateway wiring is a meta-wiring over HTTP and channels](gateway-wiring-is-a-meta-wiring-over-http-and-channels.md) — wireGateway writes handler implementations into the HTTP and channel state directly while the inspector compiles the corresponding meta, so runtime registration deliberately writes no meta
54
67
  - [Generated src paths in pikku meta are absolute](generated-src-paths-in-pikku-meta-are-absolute.md) — emailsMeta.src is resolved by the CLI at generation time, so reading through the project-relative helpers produces a wrong compound path
68
+ - [Hot reload writes into the function map captured at startup, not pikkuState's current one](hot-reload-writes-into-the-function-map-captured-at-startup.md) — A dev-server watcher may have swapped in a codegen-scoped map whose writes are discarded on restore, and schemas are deliberately left alone
55
69
  - [HTTP request bodies are read once and shared between consumers](http-request-bodies-are-read-once-and-shared.md) — The fetch request wrapper memoises the single-use body and builds web Requests lazily, at the cost of holding the whole body in memory
56
70
  - [HTTP route groups cascade config in a fixed, per-field order](http-route-groups-cascade-config-in-a-fixed-order.md) — basePath concatenates, tags and middleware merge outward-in, auth is overridden by the innermost group
57
- - [The HTTP router matches normalized paths but returns the registered path](http-router-matches-normalized-paths-but-returns-registered-ones.md) — Matching normalizes the leading slash while the match result carries the original key, because pikkuState is keyed by the registered string
58
- - [The HTTP runner logs through a trace-scoped logger, functions do not](http-runner-logs-through-a-trace-scoped-logger-functions-do-not.md) — Only runner-internal logging is scoped to the request id; functions keep the singleton logger for compatibility
59
- - [Set-Cookie headers are appended individually, never joined](http-set-cookie-headers-are-appended-never-joined.md) — Every cookie gets its own header line, because Set-Cookie is the one header comma-joining corrupts
60
- - [HTTP SSE streams flush headers only after middleware has run](http-sse-streams-flush-headers-only-after-middleware.md) — SSE responses defer the header flush and register with the eventHub, at the cost of a stricter ordering the runner must preserve
61
71
  - [HTTP wiring without generated metadata is skipped, not fatal](http-wiring-without-metadata-is-skipped-not-fatal.md) — wireHTTP warns and returns when a route has no metadata, so partial deploy units still boot
62
72
  - [In a scenario a 4xx is data, not an exception](in-a-scenario-a-4xx-is-data-not-an-exception.md) — postScenarioJson and invokeRaw report the status instead of throwing, because a refusal is the expected outcome of a permissions scenario
63
73
  - [In-memory workflow history aliases the live step object](in-memory-workflow-history-aliases-the-live-step-object.md) — stepHistory pushes the same StepState reference that steps holds, so later mutations to a step are visible in its history entry
64
74
  - [Istanbul statement counts attach to the start line only](istanbul-statement-counts-attach-to-the-start-line-only.md) — The istanbul coverage reader credits a statement's hits to its first line, so an enclosing multi-line statement cannot mask an unexecuted inner one
65
75
  - [Local trigger and gateway services assume a single process](local-trigger-and-gateway-services-assume-a-single-process.md) — InMemoryTriggerService and LocalGatewayService start every listener unconditionally with no distributed claiming, so a second instance duplicates every event
66
76
  - [Node-only builtins are imported dynamically](node-only-builtins-are-imported-dynamically.md) — V8CoverageService imports node:inspector inside start() so the module stays loadable on runtimes that have no such builtin
77
+ - [One project-shape check, called by both validators](one-project-shape-check-two-validators.md) — workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts
78
+ - [Only functions marked `expose: true` enter a virtual user's catalogue](only-exposed-functions-enter-a-virtual-user-catalogue.md) — Absent is not permissive — an unexposed function 404s over rpc, so offering one spends a step to learn nothing about the product
67
79
  - [Queue group concurrency keeps one shared queue fair](queue-group-concurrency-keeps-one-shared-queue-fair.md) — Per-group slot caps let many producers share a single queue instead of one queue each, at the cost of a cap that must stay under batchSize
68
80
  - [Queue jobs always carry an explicit attempts count](queue-jobs-always-carry-an-explicit-attempts-count.md) — resolveJobOptions always passes attempts so a queue backend can never apply its own retry default, and an explicit retries of 0 is honoured
69
81
  - [Remote addons dispatch over HTTP instead of through local function meta](remote-addons-dispatch-over-http-instead-of-local-meta.md) — wireRemoteAddon ships the addon as a devDependency and posts to the host, so there is deliberately no local handler to resolve
70
- - [Bare RPC names resolve through the caller's package scope before root](rpc-names-resolve-through-package-scope-before-root.md) — An addon's own functions win over root RPC meta for bare names, and the resolving scope is returned so it can be threaded into runPikkuFunc
71
82
  - [Scenario agent calls sign in on 401 only](scenario-agent-calls-sign-in-on-401-only.md) — postAgent sends the first request with whatever session it holds and logs in only if refused, so an actor can converse with a no-auth agent with no sign-in wiring
83
+ - [`given` and `when` are sugar for each other; `then` is not](scenario-given-and-when-are-sugar-but-then-is-not.md) — The phase decides whether a step's bindings are alternatives or witnesses, so the same step function called as `when` and as `then` runs differently
72
84
  - [Scenario meta lives apart from app meta but merges when read off disk](scenario-meta-lives-apart-from-app-meta-but-merges-when-read-off-disk.md) — Scenarios generate into .pikku/scenarios so no app module imports them, yet MetaService folds them back into the workflow and function meta
73
85
  - [Scenario steps return drained response records](scenario-steps-return-drained-response-records.md) — A scenario step hands back ScenarioHttpResponse rather than a Response, because the body stream reads once and the return value crosses into the run record
86
+ - [Scenarios, steps and personas live in files named for them](scenarios-live-in-files-named-for-them.md) — pikkuScenario/pikkuScenarioStep must be in *.scenario.ts/*.steps.ts and definePersonas in *.virtual-user.ts, an error because the mixing is only cheap to undo while it is one file
74
87
  - [Scope roots may be co-declared by an addon and its host app](scope-roots-may-be-co-declared-by-an-addon-and-its-host-app.md) — flattenScopeDefinitions dedupes ids because the same root can legitimately be declared twice, and every consumer requires one entry per scope
75
88
  - [Serverless channel disconnect must tolerate a missing channel](serverless-channel-disconnect-must-tolerate-a-missing-channel.md) — A failed channel lookup on disconnect returns quietly, because serverless runtimes deliver disconnect more than once
89
+ - [sideEffects is an allowlist, because core genuinely has some](side-effects-are-an-allowlist-not-a-boolean.md) — The error registry is built by addError calls that run on import, so claiming sideEffects:false would let a bundler drop it and leave getErrorResponse unable to find any error
90
+ - [Speech synthesis picks a voice per sentence but announces a limitation once](speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md) — A bilingual reply should speak the half it can, and repeating the notice for every sentence would bury the reply itself
91
+ - [The actor's instructions say the word "json" because a degraded gateway demands it](the-actor-prompt-says-json-because-of-json-object-mode.md) — A gateway that cannot take a JSON schema falls back to OpenAI's json_object mode, which refuses any request whose prompt does not contain the word
92
+ - [The agent `done` event goes through the middleware and is awaited](the-agent-done-event-goes-through-the-middleware-and-is-awaited.md) — `done` is the only end-of-reply signal a stream hook gets, and buffering hooks flush on it — sending it raw discards work already paid for
93
+ - [The API report pins members, because the export list only pins names](the-api-report-pins-members-not-just-names.md) — public-surface.json catches an export appearing or vanishing; it cannot see a method added to an interface, which is the change that breaks a consumer's build
76
94
  - [The dev queue copies prod timing and serialization semantics](the-dev-queue-copies-prod-timing-and-serialization-semantics.md) — InMemoryQueueService dispatches via setTimeout, retries with backoff, and JSON round-trips every payload so dev behaviour matches a real backend
95
+ - [The ecosystem entry point carries the adapter surface, so the root can promise stability](the-ecosystem-entry-point-carries-the-adapter-surface.md) — runPikkuFunc and the singleton-service accessors are what a runtime adapter reaches for; their signatures move, and the package root should not promise otherwise
77
96
  - [The embedding model is pinned per service and doc/query embedding is split](the-embedding-model-is-pinned-per-service-and-doc-query-embedding-is-split.md) — AIEmbeddingService fixes its model at construction so index and query share a vector space, and separates embedDocuments from embedQuery for asymmetric models
78
97
  - [The in-memory workflow service is inline-only and single-process](the-in-memory-workflow-service-is-inline-only-and-single-process.md) — InMemoryWorkflowService wires no queues and implements withRunLock/withStepLock as pass-throughs, because inline execution has no second holder to exclude
79
98
  - [The KEK salt is scoped to the key version, not the secret](the-kek-salt-is-scoped-to-the-key-version.md) — One stored salt per key version means N secrets cost one derivation, which is the point of envelope encryption
99
+ - [The middleware resolution cache is deliberately unbounded](the-middleware-resolution-cache-is-deliberately-unbounded.md) — Its keyspace is the set of registered wires, not request traffic, and middleware is dynamic — so eviction would buy nothing and cost the dedupe guarantee
100
+ - [The per-invocation rpc view is a class, because an object literal with a getter is slow to build](the-per-invocation-rpc-view-is-a-class.md) — An accessor declared on an object literal is defined per instance, which drops the literal off V8's fast construction path — measured at 1.15µs against 0.47µs, on every request
101
+ - [The persona runtime is exported from @pikku/core/persona, never from services](the-persona-runtime-is-exported-from-the-persona-entry-point.md) — Those values reach the actor-flow and agent runners, which no production server runs — and an unbundled deploy loads whatever the import graph names
80
102
  - [The schema service is never stubbed, or tests validate nothing](the-schema-service-is-never-stubbed.md) — createStubProxy returns undefined for the schema property so the real schema service is built — a stubbed one turns validation into a silent no-op
81
- - [One project-shape check, called by both validators](one-project-shape-check-two-validators.md) — workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts
82
- - [Scenarios, steps and personas live in files named for them](scenarios-live-in-files-named-for-them.md) — pikkuScenario/pikkuScenarioStep must be in *.scenario.ts/*.steps.ts and definePersonas in *.virtual-user.ts, an error because the mixing is only cheap to undo while it is one file
103
+ - [A voice turn's transcript is sent before the run starts, on the raw channel](the-transcript-event-is-sent-ahead-of-the-run.md) — The client does not know what it said, and an answer starts streaming within a few hundred milliseconds a question arriving after its answer reads as the wrong question
104
+ - [The catalogue is the only gate on what a virtual user may call](the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md) — Narrowing happens once at derivation, so there is no second check in the run loop that could drift out of step with it
105
+ - [The worker disposition is the only one not testing anything](the-worker-disposition-is-the-one-that-is-not-testing.md) — It is doing the job, so it abandons rarely, runs cool, and is told to stop and say so rather than guess
106
+ - [Thread history records what the model was asked, which for a spoken turn is the transcript](thread-history-records-the-transcript-not-the-audio.md) — The wire carried a base64 audio blob; persisting it writes megabytes of unreadable data and loses the only readable record of the turn
83
107
  - [Trigger declaration is split from trigger source](trigger-declaration-is-split-from-trigger-source.md) — Triggers are declared everywhere but subscribed only in the trigger worker, so app processes never open the underlying subscription
84
108
  - [TypedSecretService caches for the process lifetime](typed-secret-service-caches-for-the-process-lifetime.md) — Resolved secrets are cached with no TTL, so a secret rotated out of band is not picked up until restart — tracked as pikkujs/pikku#964
85
109
  - [Both validators check personas through one shared module, and a persona counts from either the meta or the source](validate-checks-personas-through-a-shared-module.md) — workspace validate and fabric validate are separate implementations, so the persona/actor/knowledge checks live in persona-checks.ts and are called by both
110
+ - [A scenario's step order for a virtual user is insertion order, not a graph traversal](virtual-user-step-order-comes-from-insertion-order.md) — The CLI writes nodes in declaration order, so following `next` would buy an ordering that is already true and cost a traversal that has to interpret branches
111
+ - [Only an explicit `false` silences a spoken reply](voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md) — The key being absent means no voice input is wired and nothing reported either way, so those callers keep the behaviour they had before the option existed
86
112
  - [Webhook delivery history records every attempt, best effort](webhook-delivery-history-records-every-attempt-best-effort.md) — The webhook worker persists each attempt before it throws, and a failure to persist is logged rather than allowed to mask the delivery result
87
113
  - [Webhook service collaborators are constructor args, not locator lookups](webhook-service-collaborators-are-constructor-args-not-locator-lookups.md) — QueueWebhookService takes its queue as a constructor parameter so a project wiring webhooks without a queue fails to compile instead of at first send
88
114
  - [Whether a run is inline is read from the run record](whether-a-run-is-inline-is-read-from-the-run-record.md) — The runContexts map is a read-through cache over WorkflowRun.inline and a lifetime for replay ordinals, never the answer to what a run is
115
+ - [Wiring registries erase the generics their wire* functions capture](wiring-registries-erase-the-generics-their-wire-functions-capture.md) — A registry is typed at unknown so every wiring shares one map; storing a generic config in it needs an assertion, because function parameters are contravariant
89
116
  - [Workflow approval expiry is decided from a recorded deadline, not from a timer firing](workflow-approval-expiry-is-decided-from-a-recorded-deadline.md) — The wake-up job is best-effort liveness; losing, duplicating or delaying it cannot change the gate's answer
90
117
  - [Core declares the scenario browser surface structurally and never imports a driver](workflow-core-never-imports-a-browser-driver.md) — `@pikku/core` must stay dependency-free for edge runtimes, so playwright augments the interface instead of being imported by it
91
118
  - [Workflow DSL meta keeps runtime expressions in their own field, apart from literal values](workflow-dsl-meta-separates-runtime-expressions-from-literals.md) — A string `value` regenerates as a string literal; an `expression` regenerates as code, so the two can never share a field
92
- - [A feature resolves its scenarios by object identity, never by name or shape](workflow-features-resolve-scenarios-by-object-identity.md) — An unregistered scenario comes back explicitly unresolved rather than silently running as something else
93
119
  - [Inline and queued workflow graph runs share one transition planner](workflow-graph-inline-and-queued-runs-share-one-planner.md) — A second, weaker inline traversal would lose joins, cycle revisits and step provenance that the queued path has
94
120
  - [Workflow graph node notes are non-semantic and excluded from the graph hash](workflow-graph-node-notes-are-excluded-from-the-graph-hash.md) — Documentation on a node must not count as a topology change, or editing a comment redeploys the workflow
95
- - [An inline workflow run reports its run id the moment it exists, because a failure throws instead of returning](workflow-inline-runs-report-their-run-id-before-they-can-fail.md) — `onRunCreated` is the only moment guaranteed to happen whether the run passes, fails or suspends
96
121
  - [`invocationId` is a workflow step's dedupe key; `stepId` is store-specific and must never be used as one](workflow-invocation-id-is-the-dedupe-key-not-step-id.md) — The invocation id is a frozen UUIDv5 of runId + stepName, identical across retries on every backend
97
- - [A workflow step goes through the queue only if its function opts in, and there is no inline fallback](workflow-queued-step-dispatch-requires-an-explicit-opt-in.md) — `workflowQueued: true` is the whole decision; a missing queue service is a hard error, not a silent downgrade
98
122
  - [Workflows get their own queues by default, and queue names are resolved from queue meta](workflow-queues-are-per-workflow-by-default.md) — Per-workflow queues stop one slow step head-of-line-blocking every other workflow; `shared-groups` trades that for one set of pollers
99
- - [A workflow step name repeated in one run gets an ordinal suffix, and the first reach stays bare](workflow-repeated-step-names-get-an-ordinal-suffix.md) — `name`, `name#1`, `name#2` keys each reach separately without changing the durable key of any existing run
100
- - [A workflow replay reads its steps once and caches only the run's immutable half](workflow-replay-reads-its-steps-once-and-caches-only-the-immutable-half.md) — The per-replay snapshot collapses O(N^2) step reads to one, but caching mutable run fields would make the replay read a lie
101
123
  - [Workflow step retries are owned by the workflow, never by the queue](workflow-retries-are-owned-by-the-workflow-not-the-queue.md) — A step's retry count is resolved once and always passed to the queue as `attempts`, so the queue can never apply its own default
102
124
  - [Workflow run capabilities are extensions, not subclasses](workflow-run-capabilities-are-extensions-not-subclasses.md) — Scenario support lives in a separate module behind `setRunExtension` because a bundler drops an unused module but never an unused class member
103
- - [The workflow run mirror is an observability sink, never a second source of truth](workflow-run-mirror-is-never-a-source-of-truth.md) — Every mirrored write happens after the authoritative write lands, and a mirror failure can never fail the workflow
104
125
  - [Workflow run polling starts short and backs off to the caller's ceiling](workflow-run-polling-backs-off-to-the-callers-ceiling.md) — `pollIntervalMs` is a ceiling, not a cadence, and the wait lives in its own method so the schedule can be asserted without the clock
105
- - [The workflow run timeline is a pure fold over durable history, with the row's status as the authority](workflow-run-timeline-is-a-pure-fold-over-durable-history.md) — No IO in the fold keeps time-travel transport-independent; the terminal event comes from `status`, not from a timestamp every backend populates
106
126
  - [Scenario steps default to no retries, and a whole poll is one durable step](workflow-scenario-assertions-never-retry-and-record-one-step.md) — Retrying a failed assertion is wrong for a test primitive; recording the poll as one step means replay returns the outcome, not the loop
107
- - [Scenario lifecycle hooks are a scenario-only affordance and never mask the failure they follow](workflow-scenario-hooks-are-a-scenario-only-affordance.md) — A durable workflow replays, so a callback that reruns each replay has no honest meaning there
108
127
  - [Scenario prose is rendered out of typed calls, not parsed into them](workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md) — The inversion of cucumber — a readable report with no regex step registry to maintain
109
- - [A quarantined scenario states its reason in code, not in a CI invocation](workflow-scenario-quarantine-reason-lives-in-code.md) — `skip` carries the why next to the scenario it applies to, and naming the scenario explicitly still runs it
110
128
  - [Scenario step targets are string literals so the inspector can read them statically](workflow-scenario-step-targets-are-string-literals-for-the-inspector.md) — `step/given/when/then` mirror `do`'s RPC shape because the extractor reads a literal, not an imported symbol
111
- - [A step's compensation handler runs as a durable step of its own, and never compensates itself](workflow-step-compensation-runs-as-its-own-durable-step.md) — A refund or rollback must not fire twice on replay, so `onError` is recorded as `<step>:onError` with retries disabled
112
- - [A failed workflow step dispatch is transient infrastructure, not a run failure](workflow-step-dispatch-failure-is-transient-not-a-run-failure.md) — Queue-unreachable errors leave the run running and the step pending so the orchestrator replays; marking the run failed loses it
113
- - [A workflow step lock is held only to claim the step, never across its execution](workflow-step-lock-is-held-only-to-claim-the-step.md) — Holding the advisory lock — and its pooled connection — across step work exhausted the connection pool and self-deadlocked
114
- - [A workflow step's recorded `rpcName` is provenance only — nothing dispatches off it](workflow-step-rpc-name-is-provenance-only.md) — It exists so a reader can join a runtime step row back to the declaration that produced it, especially when the durable name was built in a loop
115
- - [A suspend or approval `reason` is the step's durable identity, not just a message](workflow-suspend-and-approval-reasons-are-durable-step-identities.md) — The reason is namespaced and used raw as the step key, so it must be derived deterministically across replays
116
- - [A suspended workflow run keeps its in-process context; only terminal runs release it](workflow-suspended-runs-keep-their-in-process-context.md) — `suspended` is absent from the terminal set on purpose, and a context is dropped only when nothing is holding it open
117
129
  <!-- /pikku:knowledge-index -->
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: Only functions marked `expose: true` enter a virtual user's catalogue
4
+ description: Absent is not permissive — an unexposed function 404s over rpc, so offering one spends a step to learn nothing about the product
5
+ tags: core, virtual-user
6
+ ---
7
+
8
+ # Only functions marked `expose: true` enter a virtual user's catalogue
9
+
10
+ `expose: true` is what puts a function on the rpc transport, which is what the
11
+ shipped target is called over. The catalogue therefore treats an absent `expose`
12
+ as excluded rather than as unspecified-and-allowed.
13
+
14
+ Absent is not permissive here: an unexposed function returns 404, and offering
15
+ it costs the user a step and teaches nothing about the product. On the e2e app,
16
+ 34 of 72 functions are in exactly that state — close to half a catalogue that
17
+ cannot be called at all.
18
+
19
+ **What this rules out:** defaulting the check to "include unless explicitly
20
+ hidden", which reads as the safer default and produces a catalogue that is
21
+ mostly dead ends.
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: decision
3
+ title: `given` and `when` are sugar for each other; `then` is not
4
+ description: The phase decides whether a step's bindings are alternatives or witnesses, so the same step function called as `when` and as `then` runs differently
5
+ tags: core, workflow
6
+ ---
7
+
8
+ # `given` and `when` are sugar for each other; `then` is not
9
+
10
+ All three run a named `pikkuScenarioStep` as one durable step. `given` and
11
+ `when` differ only in the prose a reporter renders — the step behaves
12
+ identically either way.
13
+
14
+ `then` is a different operation. The phase is what decides how a step's surface
15
+ bindings are treated: for an action, the bindings are *alternatives* and one is
16
+ chosen; for an assertion, they are *witnesses* and every applicable one runs and
17
+ must agree. So the same step function invoked as `when` and as `then` executes a
18
+ different number of times against a different number of surfaces.
19
+
20
+ See `resolveScenarioSurfaces` for the resolution itself.
21
+
22
+ **What this rules out:** collapsing the three into one call with a cosmetic
23
+ label, or letting a caller pass the phase as data. The phase changes execution,
24
+ so it has to be visible at the call site.
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: decision
3
+ title: sideEffects is an allowlist, because core genuinely has some
4
+ description: The error registry is built by addError calls that run on import, so claiming sideEffects:false would let a bundler drop it and leave getErrorResponse unable to find any error
5
+ tags: core, packaging
6
+ ---
7
+
8
+ # `sideEffects` is an allowlist, not `false`
9
+
10
+ Without a `sideEffects` field a bundler must assume every module in the package
11
+ does something merely by being imported, so it cannot drop any of them. That is
12
+ the state core shipped in, and it costs every bundled consumer the whole package.
13
+
14
+ The reflex fix — `"sideEffects": false` — would be untrue. Five modules run code
15
+ at import, 59 top-level calls between them, and every one is `addError(...)`
16
+ registering an error class in the runtime registry:
17
+
18
+ - `errors/errors.js` (44)
19
+ - `wirings/workflow/pikku-scenario-service.js` (6)
20
+ - `wirings/workflow/workflow-errors.js` (5)
21
+ - `wirings/rpc/rpc-runner.js` (3)
22
+ - `wirings/rpc/remote-addon-auth.js` (1)
23
+
24
+ A bundler that dropped `errors/errors.js` because nothing imported a binding
25
+ from it would leave `getErrorResponse` unable to map any error to a status. The
26
+ app would build, and every error would come back as a generic 500.
27
+
28
+ So the field names those five exactly. Everything else can be tree-shaken.
29
+
30
+ **What this rules out:** flipping this to `false` as a performance change, and
31
+ adding a module-level `addError` (or any other registration) without adding the
32
+ module here. `side-effects-are-declared.test.ts` walks the source for top-level
33
+ calls and fails in both directions — a module with side effects missing from the
34
+ list, and a listed module that no longer has any.
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: decision
3
+ title: Speech synthesis picks a voice per sentence but announces a limitation once
4
+ description: A bilingual reply should speak the half it can, and repeating the notice for every sentence would bury the reply itself
5
+ tags: core, ai-agent
6
+ ---
7
+
8
+ # Speech synthesis picks a voice per sentence, and warns once per reply
9
+
10
+ `voiceOutput` checks the script of each sentence separately, but emits any
11
+ "cannot speak this" notice only once for the whole reply.
12
+
13
+ Per sentence is what makes the voice correct. A reply that answers in English
14
+ and then quotes a Chinese title is two sentences in two scripts, and each is
15
+ synthesized in the voice its own script needs. Checking once for the whole reply
16
+ would pick one voice and mispronounce the other half.
17
+
18
+ Announcing once is what keeps the reply audible. A bilingual answer should still
19
+ speak the part it can, and repeating the notice for every sentence of a long
20
+ reply buries the answer under its own caveats.
21
+
22
+ **What this rules out:** moving the script check up to the reply level to save
23
+ work, and moving the notice down to the sentence level for consistency with it.
24
+ The two belong at different granularities on purpose.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: The actor's instructions say the word "json" because a degraded gateway demands it
4
+ description: A gateway that cannot take a JSON schema falls back to OpenAI's json_object mode, which refuses any request whose prompt does not contain the word
5
+ tags: core, actor-flow
6
+ ---
7
+
8
+ # The actor's instructions say the word "json"
9
+
10
+ Every call the actor makes wants a schema'd object back. A gateway that cannot
11
+ accept a JSON *schema* degrades to OpenAI's `json_object` response mode, and
12
+ that mode refuses the request outright — a hard API error, not a bad answer —
13
+ unless the literal word "json" appears somewhere in the prompt.
14
+
15
+ The word is therefore in the shared instruction block, once. All three call
16
+ sites (turn, approvals, verdict) build on that block, so one mention covers
17
+ them, and it costs nothing on providers that never needed telling.
18
+
19
+ **What this rules out:** tidying the instructions by removing a word that reads
20
+ as redundant. It is load-bearing for exactly one provider configuration, and its
21
+ absence fails the request rather than degrading the output — so the failure will
22
+ not look like a prompt problem.
@@ -0,0 +1,26 @@
1
+ ---
2
+ type: decision
3
+ title: The agent `done` event goes through the middleware and is awaited
4
+ description: `done` is the only end-of-reply signal a stream hook gets, and buffering hooks flush on it — sending it raw discards work already paid for
5
+ tags: core, ai-agent
6
+ ---
7
+
8
+ # The agent `done` event goes through the middleware and is awaited
9
+
10
+ Both the first turn and the post-approval resume send their terminating `done`
11
+ event through the stream middleware rather than straight at the channel, and
12
+ await it.
13
+
14
+ `done` is the only signal a stream hook receives that the reply is over. The
15
+ hooks that buffer need it to flush: `voiceOutput` holds a trailing fragment that
16
+ never reached a full stop, and waits on audio it has already asked the provider
17
+ to synthesize and already been billed for. Sending `done` raw skips those hooks
18
+ entirely, and the `close()` immediately after throws the buffered work away.
19
+
20
+ It matters more on the resume path than the first turn. After an approval, most
21
+ of what gets spoken is the agent describing what it just did — so a dropped
22
+ flush silences the larger half of the reply.
23
+
24
+ **What this rules out:** treating the terminating event as a special case that
25
+ can bypass the chain because "nothing comes after it". Something does: the
26
+ flush.
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: decision
3
+ title: The API report pins members, because the export list only pins names
4
+ description: public-surface.json catches an export appearing or vanishing; it cannot see a method added to an interface, which is the change that breaks a consumer's build
5
+ tags: core, api
6
+ ---
7
+
8
+ # The API report pins members, not just names
9
+
10
+ `public-surface.json` records `Object.keys(module)` for every entry point. That
11
+ is a real guard — it caught a new `./node-host-resolver` subpath and five
12
+ renames during a rebase — but it only sees *names*.
13
+
14
+ It cannot see:
15
+
16
+ - a method added to `MetaService`, which has 32 members
17
+ - a field on `ChannelMeta` becoming required
18
+ - a parameter added to a method on `PikkuWorkflowService`, which has 104
19
+ - any change at all to an `interface`, since types are erased before
20
+ `Object.keys` can enumerate them
21
+
22
+ Those are the changes that break a consumer's build, and there are far more of
23
+ them than there are exports — roughly two members for every name. The report's
24
+ own summary counts both, live, so the figure is not repeated here to rot: an
25
+ earlier hand-count in this note was already wrong by 40 members within a day of
26
+ being written.
27
+
28
+ Demonstrated rather than assumed: adding `probeAddedMember(): void` to
29
+ `MetaService` leaves `public-surface.test.ts` passing 3/0, and fails
30
+ `api-report.test.ts`.
31
+
32
+ So `api-report.md` is generated from the type checker — every exported symbol
33
+ with its full signature, interfaces and classes rendered as their declarations
34
+ so member changes land in the diff. It is committed, and the test fails when
35
+ the code and the report disagree. Regenerate with `yarn api-report`.
36
+
37
+ **What this rules out:** treating `public-surface.json` as the API guard. It
38
+ guards the front door; this guards the rooms. Both are needed, and the export
39
+ list is the cheaper of the two to run — which is why it stayed.
40
+
41
+ Re-exports are resolved through `getAliasedSymbol` before the signature is
42
+ taken. Without that, 624 of the symbols reported as `any`, which would have
43
+ pinned nothing while looking like it did.
@@ -0,0 +1,58 @@
1
+ ---
2
+ type: decision
3
+ title: The ecosystem entry point carries the adapter surface, so the root can promise stability
4
+ description: runPikkuFunc and the singleton-service accessors are what a runtime adapter reaches for; their signatures move, and the package root should not promise otherwise
5
+ tags: core, api
6
+ ---
7
+
8
+ # The ecosystem entry point carries the adapter surface
9
+
10
+ `@pikku/core` has two kinds of consumer. An **application** writes functions and
11
+ wires them: `pikkuFunc`, `wireHTTP`, `PikkuWire`, the error catalogue. A
12
+ **runtime adapter** — the express, fastify, uWS, Next, Cloudflare and Lambda
13
+ packages, plus tests — reaches past that to drive the runner itself.
14
+
15
+ Those two surfaces have measurably different stability. Over the nine months
16
+ since `PikkuWire` was introduced, its app-facing shape took 22 additions and
17
+ **zero removals** — every original field is still there. Over the same period
18
+ `runPikkuFunc` was reshaped: `allServices`/`getAllServices` became
19
+ `singletonServices`/`createWireServices`, the positional arguments became a
20
+ `wireType`/`wireId` pair plus an options object, and `session` became `auth`.
21
+
22
+ Both were exported from the package root. A compatibility promise made there
23
+ would therefore have been a promise about the weaker of the two.
24
+
25
+ So the adapter surface moved to `@pikku/core/ecosystem`: `runPikkuFunc`, the
26
+ singleton-service accessors, and the registration calls the code generator emits
27
+ — `addFunction`, `addGlobalMiddleware`, `addMiddleware` — alongside `pikkuState`
28
+ and `httpRouter`, which were already separated for the same reason. The wire
29
+ runners (`runQueueJob`, `runScheduledTask`, `runCLICommand`, `runMCP*`) left the
30
+ root too, but for a different reason: they already lived on their wire subpaths,
31
+ which is where every runtime package imports them from, so the root copies were
32
+ redundant. 108 runtime exports became 95.
33
+
34
+ **The name took two attempts.** Not `/internal`: the generated bootstrap
35
+ imports from here, so the specifier appears in the user's own `.pikku`
36
+ directory, and telling someone they are touching internals when the code
37
+ generator put it there is both wrong and self-defeating — it could never be
38
+ broken anyway. Not `/runtime` either: that reads as runtime-versus-compile-time,
39
+ i.e. _the real API_, which is the opposite of the intended signal, and
40
+ `packages/runtimes/*` already claims the word while `packages/cli` is the
41
+ largest consumer here at 22 files.
42
+
43
+ `/ecosystem` says the true thing: you are building a package in the Pikku
44
+ ecosystem — a runtime, a service, an addon, the CLI. `/internal` remains as an
45
+ alias to the same module because the pinned bootstrap CLI still emits it.
46
+
47
+ Nothing was deleted. What stayed public is what applications actually
48
+ hand-write: `addTagMiddleware` (8 files), `addGlobalPermission`, `fetch` (31),
49
+ `wireAddon`, and the authoring helpers.
50
+
51
+ **What this rules out:** re-exporting anything from `/internal` at the package
52
+ root for convenience. The split is the whole point — `/internal` may change in
53
+ any release, and the root may not. It also rules out treating `/internal` as
54
+ private: it is a published entry point that runtime authors are expected to use,
55
+ just without the compatibility guarantee.
56
+
57
+ `public-surface.json` pins both, so moving a symbol across the line is a visible
58
+ diff rather than an accident.