@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
@@ -3,6 +3,7 @@ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolv
3
3
  import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
4
4
  import { AgentInterruptedError, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
5
5
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
6
+ import { applyInputMiddleware, describeApprovals, notifyAfterStep, toAccumulatedStep, } from './ai-agent-turn.js';
6
7
  import { resolveModelConfig } from './ai-agent-model-config.js';
7
8
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
8
9
  import { randomUUID } from './ai-agent-utils.js';
@@ -75,25 +76,13 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
75
76
  ];
76
77
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
77
78
  const sharedNotes = {};
78
- let modifiedMessages = runnerParams.messages;
79
- let modifiedInstructions = runnerParams.instructions;
80
- for (const mw of aiMiddlewares) {
81
- if (mw.modifyInput) {
82
- const result = await mw.modifyInput(singletonServices, {
83
- messages: modifiedMessages,
84
- instructions: modifiedInstructions,
85
- shared: sharedNotes,
86
- });
87
- modifiedMessages = result.messages;
88
- modifiedInstructions = result.instructions;
89
- }
90
- }
79
+ const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, {
80
+ messages: runnerParams.messages,
81
+ instructions: runnerParams.instructions,
82
+ }, sharedNotes);
91
83
  runnerParams.messages = modifiedMessages;
92
84
  runnerParams.instructions = modifiedInstructions;
93
- // History records what the model was asked, which for a spoken turn is the
94
- // transcript rather than the base64 audio that arrived — see the same note on
95
- // the streaming path. Identity-checked, because a middleware may legitimately
96
- // replace the message list with something unrelated to this turn.
85
+ // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
97
86
  const lastModified = modifiedMessages[modifiedMessages.length - 1];
98
87
  const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
99
88
  const runId = await aiRunState.createRun({
@@ -105,10 +94,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
105
94
  createdAt: new Date(),
106
95
  updatedAt: new Date(),
107
96
  });
108
- // Registered on the same terms as `streamAIAgent`. A run created here is
109
- // visible to `interruptAIAgent` through `aiRunState` either way, so skipping
110
- // this would leave that call finding the run, passing the ownership check and
111
- // then failing to stop it — reported as if it were running on another host.
97
+ // knowledge: decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md
112
98
  const interruptHandle = registerInterruptibleRun(runId);
113
99
  runnerParams.abortSignal = interruptHandle.signal;
114
100
  runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle);
@@ -136,44 +122,13 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
136
122
  lastStepResult = stepResult;
137
123
  totalUsage.inputTokens += stepResult.usage.inputTokens;
138
124
  totalUsage.outputTokens += stepResult.usage.outputTokens;
139
- for (const mw of aiMiddlewares) {
140
- if (mw.afterStep) {
141
- await mw.afterStep(singletonServices, {
142
- stepNumber: step,
143
- text: stepResult.text,
144
- toolCalls: stepResult.toolCalls,
145
- toolResults: stepResult.toolResults,
146
- usage: stepResult.usage,
147
- finishReason: stepResult.finishReason,
148
- });
149
- }
150
- }
151
- accumulatedSteps.push({
152
- usage: stepResult.usage,
153
- toolCalls: stepResult.toolCalls.map((tc) => {
154
- const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
155
- return {
156
- name: tc.toolName,
157
- args: tc.args,
158
- result: typeof tr?.result === 'string'
159
- ? tr.result
160
- : JSON.stringify(tr?.result ?? ''),
161
- };
162
- }),
163
- });
125
+ await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult);
126
+ accumulatedSteps.push(toAccumulatedStep(stepResult));
164
127
  if (stepResult.toolCalls.length === 0)
165
128
  break;
166
129
  const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools);
167
130
  if (approvalsNeeded.length > 0) {
168
- for (const approval of approvalsNeeded) {
169
- const toolDef = runnerParams.tools.find((t) => t.name === approval.toolName);
170
- if (toolDef?.approvalDescriptionFn && !approval.reason) {
171
- try {
172
- approval.reason = await toolDef.approvalDescriptionFn(approval.args);
173
- }
174
- catch { }
175
- }
176
- }
131
+ await describeApprovals(approvalsNeeded, runnerParams.tools);
177
132
  const pendingApprovals = approvalsNeeded.map((a) => a.agentRunId
178
133
  ? {
179
134
  type: 'agent-call',
@@ -270,10 +225,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
270
225
  };
271
226
  }
272
227
  catch (error) {
273
- // An interrupt is not a failure, so it skips the `onError` hooks and never
274
- // becomes an `errorMessage`. Unlike the streaming path there is no partial
275
- // reply to hand back — nothing was delivered — so the caller gets a typed
276
- // throw it can tell apart from a provider outage instead of a result.
228
+ // knowledge: decisions/internals/an-agent-interrupt-is-not-a-failure.md
277
229
  const interruption = interruptHandle.interruption;
278
230
  if (interruption || isAbortError(error)) {
279
231
  await aiRunState.updateRun(runId, { status: 'interrupted' });
@@ -331,8 +283,18 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
331
283
  const approvedIds = new Set(approvals.filter((a) => a.approved).map((a) => a.toolCallId));
332
284
  const rejectedIds = new Set(approvals.filter((a) => !a.approved).map((a) => a.toolCallId));
333
285
  const savedPendingApprovals = [...(run.pendingApprovals ?? [])];
286
+ // The read above is not a claim — concurrent resumes all see the same pending
287
+ // list. `resolveApproval` is the claim, and only the caller it returns true
288
+ // for may run the tool.
289
+ const claimedIds = new Set();
334
290
  for (const { toolCallId, approved } of approvals) {
335
- await aiRunState.resolveApproval(toolCallId, approved ? 'approved' : 'denied');
291
+ const claimed = await aiRunState.resolveApproval(toolCallId, approved ? 'approved' : 'denied');
292
+ if (claimed) {
293
+ claimedIds.add(toolCallId);
294
+ }
295
+ }
296
+ if (approvals.length > 0 && claimedIds.size === 0) {
297
+ throw new Error(`Approvals for run ${runId} were already resolved by another caller`);
336
298
  }
337
299
  const { tools } = await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, undefined, agent.aiMiddleware ?? []);
338
300
  const toolCallMessages = [];
@@ -340,6 +302,8 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
340
302
  if (pending.type !== 'tool-call')
341
303
  continue;
342
304
  const toolCallId = pending.toolCallId;
305
+ if (!claimedIds.has(toolCallId))
306
+ continue;
343
307
  let resultStr;
344
308
  if (rejectedIds.has(toolCallId)) {
345
309
  resultStr =
@@ -427,19 +391,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
427
391
  ];
428
392
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
429
393
  const sharedNotes = {};
430
- let modifiedMessages = trimmedMessages;
431
- let modifiedInstructions = instructions;
432
- for (const mw of aiMiddlewares) {
433
- if (mw.modifyInput) {
434
- const result = await mw.modifyInput(singletonServices, {
435
- messages: modifiedMessages,
436
- instructions: modifiedInstructions,
437
- shared: sharedNotes,
438
- });
439
- modifiedMessages = result.messages;
440
- modifiedInstructions = result.instructions;
441
- }
442
- }
394
+ const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, { messages: trimmedMessages, instructions: instructions }, sharedNotes);
443
395
  const { tools: resumeTools } = await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, undefined, aiMiddlewares);
444
396
  const resolved = resolveModelConfig(resolvedName, agent);
445
397
  const maxSteps = resolved.maxSteps ?? 10;
@@ -472,44 +424,13 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
472
424
  lastStepResult = stepResult;
473
425
  totalUsage.inputTokens += stepResult.usage.inputTokens;
474
426
  totalUsage.outputTokens += stepResult.usage.outputTokens;
475
- for (const mw of aiMiddlewares) {
476
- if (mw.afterStep) {
477
- await mw.afterStep(singletonServices, {
478
- stepNumber: step,
479
- text: stepResult.text,
480
- toolCalls: stepResult.toolCalls,
481
- toolResults: stepResult.toolResults,
482
- usage: stepResult.usage,
483
- finishReason: stepResult.finishReason,
484
- });
485
- }
486
- }
487
- accumulatedSteps.push({
488
- usage: stepResult.usage,
489
- toolCalls: stepResult.toolCalls.map((tc) => {
490
- const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
491
- return {
492
- name: tc.toolName,
493
- args: tc.args,
494
- result: typeof tr?.result === 'string'
495
- ? tr.result
496
- : JSON.stringify(tr?.result ?? ''),
497
- };
498
- }),
499
- });
427
+ await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult);
428
+ accumulatedSteps.push(toAccumulatedStep(stepResult));
500
429
  if (stepResult.toolCalls.length === 0)
501
430
  break;
502
431
  const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools);
503
432
  if (approvalsNeeded.length > 0) {
504
- for (const approval of approvalsNeeded) {
505
- const toolDef = runnerParams.tools.find((t) => t.name === approval.toolName);
506
- if (toolDef?.approvalDescriptionFn && !approval.reason) {
507
- try {
508
- approval.reason = await toolDef.approvalDescriptionFn(approval.args);
509
- }
510
- catch { }
511
- }
512
- }
433
+ await describeApprovals(approvalsNeeded, runnerParams.tools);
513
434
  const pendingApprovals = approvalsNeeded.map((a) => a.agentRunId
514
435
  ? {
515
436
  type: 'agent-call',
@@ -1,4 +1,5 @@
1
1
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
2
+ import { applyInputMiddleware } from './ai-agent-turn.js';
2
3
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
4
  import { randomUUID } from './ai-agent-utils.js';
4
5
  import { SPOKEN_TRANSCRIPT } from './voice-input.js';
@@ -414,36 +415,18 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
414
415
  ];
415
416
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
416
417
  const sharedNotes = {};
417
- let modifiedMessages = runnerParams.messages;
418
- let modifiedInstructions = runnerParams.instructions;
419
- for (const mw of aiMiddlewares) {
420
- if (mw.modifyInput) {
421
- const result = await mw.modifyInput(singletonServices, {
422
- messages: modifiedMessages,
423
- instructions: modifiedInstructions,
424
- shared: sharedNotes,
425
- });
426
- modifiedMessages = result.messages;
427
- modifiedInstructions = result.instructions;
428
- }
429
- }
418
+ const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, {
419
+ messages: runnerParams.messages,
420
+ instructions: runnerParams.instructions,
421
+ }, sharedNotes);
430
422
  runnerParams.messages = modifiedMessages;
431
423
  runnerParams.instructions = modifiedInstructions;
432
- // Sent on the raw channel, ahead of the run. A voice client sent audio and so
433
- // has no idea what it said; until this arrives its own message is a blank
434
- // bubble. Ahead of the run rather than alongside it because the answer starts
435
- // streaming within a few hundred milliseconds, and a question that appears
436
- // after its answer reads as the wrong question.
424
+ // knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
437
425
  const transcript = sharedNotes[SPOKEN_TRANSCRIPT];
438
426
  if (typeof transcript === 'string') {
439
427
  channel.send({ type: 'transcript', text: transcript });
440
428
  }
441
- // What goes into thread history is what the model was actually asked. For a
442
- // spoken turn that is not what arrived over the wire: the wire carried a
443
- // base64 audio blob, and persisting it would write megabytes of unreadable
444
- // data into the history while losing the only readable record of the turn.
445
- // Identity-checked rather than assumed — a middleware is free to rewrite the
446
- // message list into something with no relation to the turn.
429
+ // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
447
430
  const lastModified = modifiedMessages[modifiedMessages.length - 1];
448
431
  const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
449
432
  const runId = await aiRunState.createRun({
@@ -508,10 +491,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
508
491
  : persistingChannel;
509
492
  const credentialFilteredChannel = {
510
493
  ...wrappedChannel,
511
- // Returns what the inner send returns. Middleware runs asynchronously, so
512
- // swallowing the promise here makes every `await channel.send(...)` upstream
513
- // a no-op — including the one that waits for a buffering hook to flush
514
- // before the channel closes.
494
+ // knowledge: decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md
515
495
  send: (event) => {
516
496
  if (event.type === 'tool-result' &&
517
497
  event.result !== null &&
@@ -559,11 +539,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
559
539
  return persistingChannel.fullText;
560
540
  }
561
541
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, runId);
562
- // Through the middleware rather than straight at the channel, and awaited.
563
- // `done` is the only signal a stream hook gets that the reply is over, and
564
- // the ones that buffer need it: `voiceOutput` speaks a trailing fragment
565
- // that never reached a full stop, and waits for audio it has already paid
566
- // to synthesize. Sent raw, that work is discarded by the `close()` below.
542
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
567
543
  await outputChannel.send({ type: 'done' });
568
544
  channel.close();
569
545
  return persistingChannel.fullText;
@@ -650,12 +626,7 @@ export async function interruptAIAgent(input, params) {
650
626
  const stopped = signalRunInterrupt(input.runId, {
651
627
  reason: input.reason ?? 'user',
652
628
  });
653
- // A run still marked `running` that this process cannot abort is executing on
654
- // another instance. Returning a bare `false` there is indistinguishable from
655
- // "it already finished", so the one deployment shape this registry does not
656
- // cover fails silently — an agent that simply refuses to shut up, with nothing
657
- // in the logs. Say so instead. See `signalRunInterrupt` for the fix (fan the
658
- // interrupt out over eventHub so every instance tries locally).
629
+ // knowledge: decisions/internals/an-agent-run-owned-by-another-instance-says-so.md
659
630
  if (!stopped && run.status === 'running') {
660
631
  logger?.warn(`Could not interrupt run ${input.runId}: it is running in another process. ` +
661
632
  'Interrupts are process-local; fan them out over eventHub to support multiple instances.');
@@ -681,7 +652,13 @@ export async function resumeAIAgent(input, channel, params, options) {
681
652
  }
682
653
  const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
683
654
  await assertAgentAuthorized(agent, params, packageName);
684
- await aiRunState.resolveApproval(input.toolCallId, input.approved ? 'approved' : 'denied');
655
+ // The read above is not a claim — concurrent resumes all see the same pending
656
+ // approval. `resolveApproval` is the claim, and the loser must not go on to
657
+ // run the tool a second time.
658
+ const claimed = await aiRunState.resolveApproval(input.toolCallId, input.approved ? 'approved' : 'denied');
659
+ if (!claimed) {
660
+ throw new Error(`Approval for toolCallId ${input.toolCallId} was already resolved by another caller`);
661
+ }
685
662
  const { storage } = resolveMemoryServices(agent, singletonServices);
686
663
  const memoryConfig = agent.memory;
687
664
  const agentRunner = singletonServices.aiAgentRunner;
@@ -865,23 +842,8 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
865
842
  ];
866
843
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
867
844
  const sharedNotes = {};
868
- let modifiedMessages = trimmedMessages;
869
- let modifiedInstructions = instructions;
870
- for (const mw of aiMiddlewares) {
871
- if (mw.modifyInput) {
872
- const result = await mw.modifyInput(singletonServices, {
873
- messages: modifiedMessages,
874
- instructions: modifiedInstructions,
875
- shared: sharedNotes,
876
- });
877
- modifiedMessages = result.messages;
878
- modifiedInstructions = result.instructions;
879
- }
880
- }
881
- // Resuming is as interruptible as the first turn. It is the same person
882
- // listening to the same voice, and after an approval it is where most of the
883
- // reply actually gets spoken — an approved delete is followed by the agent
884
- // talking about it, and that is a normal thing to talk over.
845
+ const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, { messages: trimmedMessages, instructions: instructions }, sharedNotes);
846
+ // knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
885
847
  const interruptHandle = registerInterruptibleRun(run.runId);
886
848
  const streamMiddleware = aiMiddlewares
887
849
  .filter((mw) => mw.modifyOutputStream)
@@ -961,10 +923,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
961
923
  return;
962
924
  }
963
925
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, run.runId);
964
- // Through the middleware, for the same reason as the first turn — and it
965
- // matters more here. After an approval, most of what gets spoken is the
966
- // agent talking about what it just did, so this is the half of the reply a
967
- // dropped flush would silence.
926
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
968
927
  await wrappedChannel.send({ type: 'done' });
969
928
  channel.close();
970
929
  }
@@ -0,0 +1,56 @@
1
+ import type { CoreSingletonServices } from '../../types/core.types.js';
2
+ import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js';
3
+ /** Exactly what `modifyInput` accepts and returns, taken from the hook itself. */
4
+ type TurnInput = Pick<Parameters<NonNullable<PikkuAIMiddlewareHooks['modifyInput']>>[1], 'messages' | 'instructions'>;
5
+ /**
6
+ * Run every `modifyInput` hook in order, threading each result into the next.
7
+ *
8
+ * Shared by the four places a turn starts — first turn and resume, on both the
9
+ * streaming and non-streaming paths. `sharedNotes` is one bag per run that the
10
+ * caller keeps, because hooks use it to pass things forward (`voiceInput`
11
+ * leaves the transcript there for `voiceOutput`).
12
+ */
13
+ export declare const applyInputMiddleware: (aiMiddlewares: PikkuAIMiddlewareHooks[], singletonServices: CoreSingletonServices, input: TurnInput, sharedNotes: Record<string, unknown>) => Promise<TurnInput>;
14
+ /** The step fields both agent paths hand to `afterStep`, taken from the hook. */
15
+ type StepResult = Omit<Parameters<NonNullable<PikkuAIMiddlewareHooks['afterStep']>>[1], 'stepNumber'>;
16
+ /**
17
+ * Notify every `afterStep` hook. The payload is the step verbatim plus its
18
+ * ordinal — a hook that wants the run's totals accumulates them itself.
19
+ */
20
+ export declare const notifyAfterStep: (aiMiddlewares: PikkuAIMiddlewareHooks[], singletonServices: CoreSingletonServices, stepNumber: number, stepResult: StepResult) => Promise<void>;
21
+ /**
22
+ * The step shape kept on the run: each tool call paired with its result.
23
+ *
24
+ * A result is stringified unless it already is a string, because this is what
25
+ * gets persisted and read back as history — an object there is unreadable.
26
+ */
27
+ export declare const toAccumulatedStep: (stepResult: StepResult) => {
28
+ usage: {
29
+ inputTokens: number;
30
+ outputTokens: number;
31
+ };
32
+ toolCalls: {
33
+ name: string;
34
+ args: Record<string, unknown>;
35
+ result: string;
36
+ }[];
37
+ };
38
+ type PendingApproval = {
39
+ toolName: string;
40
+ args: unknown;
41
+ reason?: string;
42
+ };
43
+ type ToolDef = {
44
+ name: string;
45
+ approvalDescriptionFn?: (args: any) => Promise<string> | string;
46
+ };
47
+ /**
48
+ * Fill in each approval's human-readable reason from the tool's own
49
+ * `approvalDescriptionFn`, where it did not supply one.
50
+ *
51
+ * A description that throws is swallowed: the approval still has to be raised,
52
+ * and a gate that disappears because its label failed to render is the worst
53
+ * available outcome.
54
+ */
55
+ export declare const describeApprovals: (approvals: PendingApproval[], tools: ToolDef[]) => Promise<void>;
56
+ export {};
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Run every `modifyInput` hook in order, threading each result into the next.
3
+ *
4
+ * Shared by the four places a turn starts — first turn and resume, on both the
5
+ * streaming and non-streaming paths. `sharedNotes` is one bag per run that the
6
+ * caller keeps, because hooks use it to pass things forward (`voiceInput`
7
+ * leaves the transcript there for `voiceOutput`).
8
+ */
9
+ export const applyInputMiddleware = async (aiMiddlewares, singletonServices, input, sharedNotes) => {
10
+ let { messages, instructions } = input;
11
+ for (const mw of aiMiddlewares) {
12
+ if (!mw.modifyInput)
13
+ continue;
14
+ const result = await mw.modifyInput(singletonServices, {
15
+ messages,
16
+ instructions,
17
+ shared: sharedNotes,
18
+ });
19
+ messages = result.messages;
20
+ instructions = result.instructions;
21
+ }
22
+ return { messages, instructions };
23
+ };
24
+ /**
25
+ * Notify every `afterStep` hook. The payload is the step verbatim plus its
26
+ * ordinal — a hook that wants the run's totals accumulates them itself.
27
+ */
28
+ export const notifyAfterStep = async (aiMiddlewares, singletonServices, stepNumber, stepResult) => {
29
+ for (const mw of aiMiddlewares) {
30
+ if (!mw.afterStep)
31
+ continue;
32
+ await mw.afterStep(singletonServices, {
33
+ stepNumber,
34
+ text: stepResult.text,
35
+ toolCalls: stepResult.toolCalls,
36
+ toolResults: stepResult.toolResults,
37
+ usage: stepResult.usage,
38
+ finishReason: stepResult.finishReason,
39
+ });
40
+ }
41
+ };
42
+ /**
43
+ * The step shape kept on the run: each tool call paired with its result.
44
+ *
45
+ * A result is stringified unless it already is a string, because this is what
46
+ * gets persisted and read back as history — an object there is unreadable.
47
+ */
48
+ export const toAccumulatedStep = (stepResult) => ({
49
+ usage: stepResult.usage,
50
+ toolCalls: stepResult.toolCalls.map((tc) => {
51
+ const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
52
+ return {
53
+ name: tc.toolName,
54
+ args: tc.args,
55
+ result: typeof tr?.result === 'string'
56
+ ? tr.result
57
+ : JSON.stringify(tr?.result ?? ''),
58
+ };
59
+ }),
60
+ });
61
+ /**
62
+ * Fill in each approval's human-readable reason from the tool's own
63
+ * `approvalDescriptionFn`, where it did not supply one.
64
+ *
65
+ * A description that throws is swallowed: the approval still has to be raised,
66
+ * and a gate that disappears because its label failed to render is the worst
67
+ * available outcome.
68
+ */
69
+ export const describeApprovals = async (approvals, tools) => {
70
+ for (const approval of approvals) {
71
+ if (approval.reason)
72
+ continue;
73
+ const toolDef = tools.find((t) => t.name === approval.toolName);
74
+ if (!toolDef?.approvalDescriptionFn)
75
+ continue;
76
+ try {
77
+ approval.reason = await toolDef.approvalDescriptionFn(approval.args);
78
+ }
79
+ catch { }
80
+ }
81
+ };
@@ -400,7 +400,7 @@ export type AIStreamEvent = {
400
400
  toolName: string;
401
401
  args: unknown;
402
402
  reason?: string;
403
- runId?: string;
403
+ runId: string;
404
404
  agent?: string;
405
405
  session?: string;
406
406
  } | {
@@ -135,12 +135,7 @@ export const voiceInput = (config) => pikkuAIMiddleware({
135
135
  // to send, and a message with no content is not a question.
136
136
  if (updatedContent.length === 0)
137
137
  throw new NoSpeechDetectedError();
138
- // Only when something was actually heard. A turn can carry audio that all
139
- // reads as non-speech and still have content — an image with a silent
140
- // caption clip — which leaves nothing above to throw. Recording `''` here
141
- // would send a transcript event saying the user said nothing, and a client
142
- // that tells "not transcribed yet" from "transcribed" by the key being
143
- // absent would render the turn as an empty bubble rather than a pending one.
138
+ // knowledge: decisions/internals/an-empty-transcript-is-not-recorded.md
144
139
  if (heard.length > 0) {
145
140
  shared[SPOKEN_TRANSCRIPT] = heard.join(' ');
146
141
  }
@@ -105,11 +105,7 @@ export const voiceOutput = (config) => pikkuAIMiddleware({
105
105
  const { aiAgentRunner, logger } = services;
106
106
  if (!aiAgentRunner?.generateSpeech)
107
107
  return event;
108
- // Only an explicit `false` silences the reply. `voiceInput` sets this on
109
- // every turn it handles, so `false` means a real user really typed; the
110
- // key being absent means nothing reported either way — no voice input is
111
- // wired — and that caller's replies are spoken exactly as they were before
112
- // this option existed.
108
+ // knowledge: decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md
113
109
  if (!config?.always && shared[SPOKEN_TURN] === false)
114
110
  return event;
115
111
  /**
@@ -124,13 +120,7 @@ export const voiceOutput = (config) => pikkuAIMiddleware({
124
120
  if (!config?.model) {
125
121
  throw new Error('voiceOutput requires a speech model (e.g. openai/tts-1)');
126
122
  }
127
- // Checked per sentence but announced once per reply: a bilingual answer
128
- // should still speak the half it can, and repeating the notice for
129
- // every sentence of a long one would bury the reply itself.
130
- //
131
- // Per sentence is also what makes the voice right. A reply that answers
132
- // in English and then quotes a Chinese title is two sentences in two
133
- // scripts, and each is synthesized in the voice its own script needs.
123
+ // knowledge: decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md
134
124
  let voice = config.voice;
135
125
  if (config.speakableScripts) {
136
126
  const unspeakable = unspeakableScripts(text, config.speakableScripts);
@@ -12,6 +12,7 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
12
12
  });
13
13
  const allChannelMiddleware = combineChannelMiddleware('channel', `${channelConfig.name}:${lifecycleType}:cm`, {
14
14
  wireInheritedChannelMiddleware: channelMiddlewareMeta,
15
+ // knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
15
16
  wireChannelMiddleware: channelConfig.channelMiddleware,
16
17
  });
17
18
  let wire = {
@@ -46,7 +46,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
46
46
  ? `route '${routingProperty}:${routerValue}'`
47
47
  : 'the default message route';
48
48
  logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`);
49
- // TODO: Send error message back breaks typescript, but should be implemented somehow
49
+ // knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
50
50
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
51
51
  return;
52
52
  }
@@ -55,10 +55,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
55
55
  const pikkuFuncId = routeMeta.pikkuFuncId;
56
56
  // Get wire middleware: channel-level middleware + message-specific middleware
57
57
  const channelWireMiddleware = channelConfig.middleware || [];
58
- // Check if onMessage is a wrapper object vs direct function config:
59
- // - Direct config: onMessage.func is a plain Function
60
- // - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
61
- // - Simple wrapper: onMessage has both func (plain Function) and middleware properties
58
+ // knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
62
59
  const isWrapper = onMessage &&
63
60
  typeof onMessage === 'object' &&
64
61
  'func' in onMessage &&
@@ -83,6 +80,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
83
80
  inheritedMiddleware,
84
81
  wireMiddleware,
85
82
  inheritedChannelMiddleware: channelMeta?.channelMiddleware,
83
+ // knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
86
84
  wireChannelMiddleware: wireChannelMiddleware,
87
85
  coerceDataFromSchema: true,
88
86
  tags: channelConfig.tags,
@@ -1,12 +1,6 @@
1
1
  import type { DeploymentService } from '../../services/deployment-service.js';
2
2
  import { ChannelRPCRegistry } from './channel-rpc-registry.js';
3
3
  import { type ChannelRPCValidator } from './channel-rpc.types.js';
4
- /**
5
- * `remote` for channels that only ever flow one way — a server-sent stream, an
6
- * agent's output, a local CLI. Nothing is listening for a request on them, so
7
- * the call is refused rather than waiting out a timeout.
8
- */
9
- export declare const unsupportedChannelRemote: (funcName: string) => Promise<never>;
10
4
  /**
11
5
  * A `DeploymentService` whose transport is an open channel rather than an
12
6
  * address, so a server-side function can call back into a peer that has no
@@ -1,13 +1,5 @@
1
1
  import { ChannelRPCRegistry } from './channel-rpc-registry.js';
2
2
  import { CHANNEL_RPC_REQUEST, CHANNEL_RPC_RESPONSE, ChannelRPCError, isChannelRPCPending, isChannelRPCResponse, } from './channel-rpc.types.js';
3
- /**
4
- * `remote` for channels that only ever flow one way — a server-sent stream, an
5
- * agent's output, a local CLI. Nothing is listening for a request on them, so
6
- * the call is refused rather than waiting out a timeout.
7
- */
8
- export const unsupportedChannelRemote = async (funcName) => {
9
- throw new ChannelRPCError(`Cannot call "${funcName}" remotely: this channel has no peer that answers`, 'unsupported');
10
- };
11
3
  /**
12
4
  * A `DeploymentService` whose transport is an open channel rather than an
13
5
  * address, so a server-side function can call back into a peer that has no
@@ -88,3 +88,9 @@ export type ApprovalRequester = (request: {
88
88
  }) => Promise<boolean> | boolean;
89
89
  /** Checks one end of a call — arguments going out, or the answer coming back. */
90
90
  export type ChannelRPCValidator = (funcName: string, value: unknown) => Promise<void> | void;
91
+ /**
92
+ * `remote` for wires with no peer that answers — a server-sent stream, an
93
+ * agent's output, a local CLI. Nothing is listening for a request on them, so
94
+ * the call is refused rather than waiting out a timeout.
95
+ */
96
+ export declare const unsupportedChannelRemote: (funcName: string) => Promise<never>;
@@ -48,3 +48,11 @@ export const isCapabilityDef = (capability) => typeof capability === 'object' &&
48
48
  export const resolveCapability = (capability) => isCapabilityDef(capability)
49
49
  ? capability
50
50
  : { execute: capability, needsApproval: true };
51
+ /**
52
+ * `remote` for wires with no peer that answers — a server-sent stream, an
53
+ * agent's output, a local CLI. Nothing is listening for a request on them, so
54
+ * the call is refused rather than waiting out a timeout.
55
+ */
56
+ export const unsupportedChannelRemote = async (funcName) => {
57
+ throw new ChannelRPCError(`Cannot call "${funcName}" remotely: this channel has no peer that answers`, 'unsupported');
58
+ };
@@ -1,8 +1,6 @@
1
- import type { CorePikkuPermission } from '../../function/functions.types.js';
2
1
  import type { SessionService } from '../../services/user-session-service.js';
3
- import type { CorePikkuMiddleware } from '../../types/core.types.js';
4
2
  import type { ChannelMeta, CoreChannel, RunChannelOptions, RunChannelParams } from './channel.types.js';
5
- export declare const wireChannel: <In, Channel extends string, PikkuPermission extends CorePikkuPermission<In>, PikkuMiddleware extends CorePikkuMiddleware, ChannelFunction>(channel: CoreChannel<In, Channel, PikkuPermission, PikkuMiddleware, ChannelFunction>) => void;
3
+ export declare const wireChannel: <In, Channel extends string, ChannelConnect, ChannelDisconnect, ChannelFunctionMessage>(channel: CoreChannel<In, Channel, ChannelConnect, ChannelDisconnect, ChannelFunctionMessage>) => void;
6
4
  export declare const openChannel: ({ route, coerceDataFromSchema, request, }: Pick<CoreChannel<unknown, string>, "route"> & RunChannelParams<unknown> & {
7
5
  userSession: SessionService<any>;
8
6
  } & RunChannelOptions) => Promise<{