@pikku/core 0.12.79 → 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 (330) hide show
  1. package/CHANGELOG.md +161 -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.js +2 -0
  24. package/dist/services/local-content-request-handler.d.ts +21 -0
  25. package/dist/services/local-content-request-handler.js +72 -53
  26. package/dist/services/local-content.d.ts +6 -0
  27. package/dist/services/local-content.js +14 -1
  28. package/dist/services/workflow-service.d.ts +4 -2
  29. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  30. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  31. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  33. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  34. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  35. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  36. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  37. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  38. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  39. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  41. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  43. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/session-store-tests.js +43 -0
  45. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  47. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  49. package/dist/testing/service-tests.d.ts +6 -0
  50. package/dist/testing/service-tests.js +26 -791
  51. package/dist/types/core.types.d.ts +11 -1
  52. package/dist/types/state.types.d.ts +1 -1
  53. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  54. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  55. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  56. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  57. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  58. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  59. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  60. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  61. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  62. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  63. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  64. package/dist/wirings/ai-agent/voice-input.js +1 -6
  65. package/dist/wirings/ai-agent/voice-output.js +2 -12
  66. package/dist/wirings/channel/channel-common.js +1 -0
  67. package/dist/wirings/channel/channel-handler.js +3 -5
  68. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  69. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  70. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  71. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  72. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  73. package/dist/wirings/channel/channel-runner.js +16 -8
  74. package/dist/wirings/channel/channel.types.d.ts +2 -0
  75. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  76. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  77. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  78. package/dist/wirings/cli/cli-runner.js +4 -2
  79. package/dist/wirings/cli/cli.types.d.ts +0 -8
  80. package/dist/wirings/cli/command-parser.js +13 -0
  81. package/dist/wirings/gateway/gateway-runner.js +9 -2
  82. package/dist/wirings/http/http-routes.js +2 -0
  83. package/dist/wirings/http/http-runner.d.ts +0 -10
  84. package/dist/wirings/http/http-runner.js +6 -13
  85. package/dist/wirings/http/http.types.d.ts +0 -10
  86. package/dist/wirings/http/index.d.ts +1 -1
  87. package/dist/wirings/http/index.js +1 -1
  88. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  89. package/dist/wirings/mcp/mcp-runner.js +0 -6
  90. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  91. package/dist/wirings/rpc/rpc-runner.js +53 -86
  92. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  93. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  94. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  95. package/dist/wirings/trigger/trigger-runner.js +1 -0
  96. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  97. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  98. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  100. package/dist/wirings/workflow/feature.js +7 -4
  101. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  102. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  103. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  104. package/dist/wirings/workflow/index.d.ts +7 -2
  105. package/dist/wirings/workflow/index.js +5 -1
  106. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  107. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  108. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  109. package/dist/wirings/workflow/pikku-workflow-service.js +71 -493
  110. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  111. package/dist/wirings/workflow/workflow-approval.js +114 -0
  112. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  113. package/dist/wirings/workflow/workflow-constants.js +35 -0
  114. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  115. package/dist/wirings/workflow/workflow-errors.js +112 -0
  116. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  117. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  118. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  119. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  120. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  121. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  122. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  123. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  124. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  125. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  126. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  127. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  128. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  129. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  130. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  131. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  132. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  133. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  134. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  135. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  136. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  137. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  138. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  139. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  140. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  141. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  142. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  143. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  144. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  145. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  146. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  147. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  148. package/knowledge/decisions/internals/index.md +38 -26
  149. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  150. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  151. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  152. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  153. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  154. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  155. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  156. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  157. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  158. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  159. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  160. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  161. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  162. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  163. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  164. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  165. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  166. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  167. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  168. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  169. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  170. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  171. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  172. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  173. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  174. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  175. package/knowledge/decisions/security/index.md +7 -0
  176. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  177. package/knowledge/questions/index.md +2 -1
  178. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  179. package/package.json +13 -3
  180. package/scripts/generate-api-report.d.mts +1 -0
  181. package/scripts/generate-api-report.mjs +89 -0
  182. package/scripts/generate-api-report.mts +218 -0
  183. package/src/api-report.test.ts +69 -0
  184. package/src/column-form.ts +1 -4
  185. package/src/crypto-utils.test.ts +15 -2
  186. package/src/dev/hot-reload.ts +2 -7
  187. package/src/ecosystem.ts +40 -0
  188. package/src/errors/error-handler.ts +5 -3
  189. package/src/errors/error.test.ts +4 -1
  190. package/src/function/function-runner.test.ts +1 -1
  191. package/src/function/function-runner.ts +28 -39
  192. package/src/function/functions.types.ts +15 -9
  193. package/src/handle-error.ts +1 -1
  194. package/src/index.ts +3 -22
  195. package/src/middleware/auth-cookie.test.ts +50 -0
  196. package/src/middleware/auth-cookie.ts +38 -2
  197. package/src/middleware/cors.ts +2 -1
  198. package/src/middleware/index.ts +1 -1
  199. package/src/middleware/remote-auth.test.ts +43 -0
  200. package/src/middleware/remote-auth.ts +17 -3
  201. package/src/middleware-runner.ts +4 -2
  202. package/src/no-any-casts.test.ts +57 -0
  203. package/src/permissions.test.ts +3 -1
  204. package/src/permissions.ts +22 -24
  205. package/src/pikku-state.ts +17 -6
  206. package/src/public-surface.json +578 -0
  207. package/src/public-surface.json.README +25 -0
  208. package/src/public-surface.test.ts +105 -0
  209. package/src/removed-legacy-exports.test.ts +62 -0
  210. package/src/schema.test.ts +78 -0
  211. package/src/schema.ts +36 -1
  212. package/src/services/ai-run-state-service.ts +7 -1
  213. package/src/services/audit-service.ts +2 -2
  214. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  215. package/src/services/in-memory-workflow-service.ts +2 -0
  216. package/src/services/index.ts +1 -4
  217. package/src/services/local-content-request-handler.test.ts +43 -5
  218. package/src/services/local-content-request-handler.ts +103 -74
  219. package/src/services/local-content.ts +15 -1
  220. package/src/services/local-email-service.ts +5 -1
  221. package/src/services/system-role-guard.test.ts +4 -1
  222. package/src/services/workflow-service.ts +9 -2
  223. package/src/side-effects-are-declared.test.ts +84 -0
  224. package/src/source-files-stay-composable.test.ts +41 -0
  225. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  226. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  227. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  228. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  229. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  230. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  231. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  232. package/src/testing/service-tests/session-store-tests.ts +62 -0
  233. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  234. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  235. package/src/testing/service-tests.ts +31 -1111
  236. package/src/types/core.types.ts +15 -1
  237. package/src/types/state.types.ts +1 -1
  238. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  239. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  240. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  241. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  242. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  243. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  244. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  245. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  246. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  247. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  248. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  249. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  250. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  251. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  252. package/src/wirings/ai-agent/voice-input.ts +1 -6
  253. package/src/wirings/ai-agent/voice-output.ts +2 -12
  254. package/src/wirings/channel/channel-common.ts +5 -2
  255. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  256. package/src/wirings/channel/channel-handler.ts +7 -7
  257. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  258. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  259. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  260. package/src/wirings/channel/channel-runner.ts +33 -20
  261. package/src/wirings/channel/channel.types.ts +2 -0
  262. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  263. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  264. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  265. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  266. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  267. package/src/wirings/cli/cli-runner.ts +7 -4
  268. package/src/wirings/cli/cli.types.ts +0 -39
  269. package/src/wirings/cli/command-parser.test.ts +19 -0
  270. package/src/wirings/cli/command-parser.ts +17 -1
  271. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  272. package/src/wirings/gateway/gateway-runner.ts +27 -19
  273. package/src/wirings/http/http-routes.ts +4 -1
  274. package/src/wirings/http/http-runner.ts +11 -26
  275. package/src/wirings/http/http.types.ts +0 -15
  276. package/src/wirings/http/index.ts +1 -7
  277. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  278. package/src/wirings/http/web-request.ts +1 -1
  279. package/src/wirings/mcp/mcp-runner.ts +2 -16
  280. package/src/wirings/persona/persona-environments.test.ts +14 -3
  281. package/src/wirings/persona/persona.test.ts +13 -3
  282. package/src/wirings/persona/validate-personas.ts +5 -1
  283. package/src/wirings/queue/queue-runner.ts +1 -1
  284. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  285. package/src/wirings/rpc/rpc-runner.ts +58 -136
  286. package/src/wirings/rpc/rpc-types.ts +7 -0
  287. package/src/wirings/rpc/wire-addon.ts +3 -1
  288. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  289. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  290. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  291. package/src/wirings/trigger/trigger-runner.ts +7 -5
  292. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  293. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  294. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  295. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  296. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  297. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  298. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  299. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  300. package/src/wirings/workflow/feature.ts +7 -4
  301. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  302. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  303. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  304. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  305. package/src/wirings/workflow/index.ts +12 -4
  306. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  307. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  308. package/src/wirings/workflow/pikku-workflow-service.ts +190 -743
  309. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  310. package/src/wirings/workflow/workflow-approval.ts +185 -0
  311. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  312. package/src/wirings/workflow/workflow-constants.ts +47 -0
  313. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  314. package/src/wirings/workflow/workflow-errors.ts +128 -0
  315. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  316. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  317. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  318. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  319. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  320. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  321. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  322. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  323. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  324. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  325. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  326. package/src/wirings/workflow/workflow.types.ts +7 -0
  327. package/src/wirings-stay-decoupled.test.ts +123 -0
  328. package/tsconfig.json +1 -1
  329. package/tsconfig.tsbuildinfo +1 -1
  330. package/src/internal.ts +0 -10
@@ -233,6 +233,11 @@ export interface CoreUserSession {
233
233
  * Populated by whoever builds the session — core reads them, never fetches.
234
234
  */
235
235
  scopes?: string[];
236
+ /**
237
+ * Restricts the session to functions declared `readonly`. The function runner
238
+ * throws `ReadonlySessionError` for anything else.
239
+ */
240
+ readonly?: boolean;
236
241
  }
237
242
  /**
238
243
  * Kept structural so core stays independent of any one auth package —
@@ -299,12 +304,17 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
299
304
  */
300
305
  auth?: () => Promise<AuthInstance>;
301
306
  }
302
- export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire, TypedActors extends ScenarioPersonas = ScenarioPersonas> = {
307
+ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>, TypedActors extends ScenarioPersonas = ScenarioPersonas> = {
303
308
  /** Always present — lazily initialised on first access for every function invocation */
304
309
  rpc: TypedRPC;
305
310
  } & Partial<{
306
311
  wireType: PikkuWiringTypes;
307
312
  wireId: string;
313
+ /**
314
+ * A logger scoped to this invocation, when a host attaches one. Core never
315
+ * sets it; services that log fall back to the singleton logger.
316
+ */
317
+ logger: Logger;
308
318
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
309
319
  traceId: string;
310
320
  functionId: string;
@@ -63,7 +63,7 @@ export interface PikkuPackageState {
63
63
  meta: HTTPWiringsMeta;
64
64
  };
65
65
  channel: {
66
- channels: Map<string, CoreChannel<any, any>>;
66
+ channels: Map<string, CoreChannel<any, any, any, any, any>>;
67
67
  meta: ChannelsMeta;
68
68
  };
69
69
  scheduler: {
@@ -70,12 +70,7 @@ function actorInstructions(persona, task) {
70
70
  : '',
71
71
  `Your goal in this conversation: ${task}.`,
72
72
  `Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
73
- // Every call the actor makes wants a schema'd object back, and a gateway
74
- // that cannot take a JSON *schema* degrades to OpenAI's `json_object`
75
- // mode — which refuses the request outright unless the word "json" appears
76
- // somewhere in the prompt. Saying it once here covers all three call sites
77
- // (turn, approvals, verdict), since each builds on these instructions, and
78
- // it costs nothing on providers that never needed telling.
73
+ // knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
79
74
  `Reply with json matching the schema you are given, and nothing else.`,
80
75
  ]
81
76
  .filter(Boolean)
@@ -0,0 +1,15 @@
1
+ import type { PikkuRawWire } from '../../types/core.types.js';
2
+ import type { SessionService } from '../../services/user-session-service.js';
3
+ import type { CoreUserSession } from '../../types/core.types.js';
4
+ import type { PikkuRPC } from '../rpc/rpc-types.js';
5
+ export type AgentRPCOptions = {
6
+ sessionService?: SessionService<CoreUserSession>;
7
+ };
8
+ /**
9
+ * `wire.rpc.agent`, implemented.
10
+ *
11
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
12
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
13
+ * primitive.
14
+ */
15
+ export declare const createAgentRPC: (wire: PikkuRawWire, options: AgentRPCOptions) => PikkuRPC["agent"];
@@ -0,0 +1,53 @@
1
+ import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
2
+ import { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
3
+ import { wrapChannelWithAGUI } from './ai-agent-agui.js';
4
+ /**
5
+ * `wire.rpc.agent`, implemented.
6
+ *
7
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
8
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
9
+ * primitive.
10
+ */
11
+ export const createAgentRPC = (wire, options) => {
12
+ const params = () => ({
13
+ sessionService: options.sessionService,
14
+ getCredential: wire.getCredential?.bind(wire),
15
+ });
16
+ const streamingChannel = () => {
17
+ const channel = wire.channel;
18
+ if (!channel)
19
+ throw new Error('No channel available for streaming');
20
+ return channel;
21
+ };
22
+ /** `run` and `approve` return the same shape; only the call differs. */
23
+ const asRunResult = (result) => ({
24
+ runId: result.runId,
25
+ result: result.object ?? result.text,
26
+ usage: result.usage,
27
+ ...(result.status === 'suspended' && {
28
+ status: 'suspended',
29
+ pendingApprovals: result.pendingApprovals,
30
+ }),
31
+ });
32
+ return {
33
+ run: async (agentName, input) => asRunResult(await runAIAgent(agentName, input, params())),
34
+ stream: async (agentName, input, streamOptions) => {
35
+ let currentRunId;
36
+ await streamAIAgent(agentName, input, wrapChannelWithAGUI(streamingChannel(), {
37
+ threadId: input.threadId,
38
+ getRunId: () => currentRunId,
39
+ }), params(), undefined, {
40
+ ...streamOptions,
41
+ onRunCreated: (runId) => {
42
+ currentRunId = runId;
43
+ streamOptions?.onRunCreated?.(runId);
44
+ },
45
+ });
46
+ },
47
+ resume: async (runId, input, streamOptions) => {
48
+ await resumeAIAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
49
+ },
50
+ interrupt: async (runId, reason) => interruptAIAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
51
+ approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAIAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName)),
52
+ };
53
+ };
@@ -296,11 +296,7 @@ export function wrapChannelWithAGUI(inner, options) {
296
296
  });
297
297
  break;
298
298
  }
299
- // AG-UI has no event for speech, so it travels as CUSTOM like the other
300
- // pikku-specific ones. Dropping it instead — which is what this did —
301
- // means a voice agent reached over HTTP is inaudible: `voiceOutput`
302
- // synthesizes every sentence, the provider bills for it, and nothing
303
- // gets past the mapper.
299
+ // knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
304
300
  case 'audio-delta': {
305
301
  send({
306
302
  type: 'CUSTOM',
@@ -2,7 +2,9 @@ import { randomUUID } from 'crypto';
2
2
  export function resolveMemoryServices(agent, singletonServices) {
3
3
  const memoryConfig = agent.memory;
4
4
  const storage = memoryConfig?.storage
5
- ? singletonServices[memoryConfig.storage]
5
+ ? // The service is named by string in agent config, so the lookup cannot be
6
+ // checked — only the shape it is required to have.
7
+ singletonServices[memoryConfig.storage]
6
8
  : singletonServices.aiStorage;
7
9
  return { storage };
8
10
  }
@@ -1,5 +1,5 @@
1
1
  import { PikkuError } from '../../errors/error-handler.js';
2
- import { checkAuthPermissions, runPermissions } from '../../permissions.js';
2
+ import { checkAuthPermissions, runPermissions, } from '../../permissions.js';
3
3
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
4
4
  import { ForbiddenError } from '../../errors/errors.js';
5
5
  import { verifyScopes } from '../../scopes.js';
@@ -648,10 +648,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
648
648
  }
649
649
  let messages = [];
650
650
  if (storage) {
651
- // A tool whose run was interrupted may still be writing its result to this
652
- // thread. In voice the next turn lands within a second or two, so without
653
- // this the model would load context that is missing the very thing it is
654
- // about to be asked about.
651
+ // knowledge: decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md
655
652
  await awaitPendingInterruptNote(threadId);
656
653
  messages = await storage.getMessages(threadId, {
657
654
  lastN: memoryConfig?.lastMessages ?? 20,
@@ -660,10 +657,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
660
657
  const contextMessages = await loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema);
661
658
  const userContent = input.attachments?.length
662
659
  ? [
663
- // Omitted when there is nothing to say. An attachment on its own is a
664
- // real turn — a spoken one carries audio and no text at all — and an
665
- // empty text part alongside it is a part providers are entitled to
666
- // reject, for a caller who never wrote one.
660
+ // knowledge: decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md
667
661
  ...(input.message
668
662
  ? [{ type: 'text', text: input.message }]
669
663
  : []),
@@ -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 {};