@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
@@ -298,6 +298,11 @@ export interface CoreUserSession {
298
298
  * Populated by whoever builds the session — core reads them, never fetches.
299
299
  */
300
300
  scopes?: string[]
301
+ /**
302
+ * Restricts the session to functions declared `readonly`. The function runner
303
+ * throws `ReadonlySessionError` for anything else.
304
+ */
305
+ readonly?: boolean
301
306
  }
302
307
 
303
308
  /**
@@ -378,7 +383,11 @@ export type PikkuWire<
378
383
  MCPTools extends string | never = never,
379
384
  TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
380
385
  TriggerOutput = unknown,
381
- TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire,
386
+ // Defaulted to `any` rather than to `Out`: the emitted `TypedScenario<Out>`
387
+ // supplies the real context, while `PikkuWire`'s own defaults are what other
388
+ // generics constrain against — and a constraint that pinned the context to
389
+ // `Out` would reject every wire whose scenario output is anything else.
390
+ TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>,
382
391
  TypedActors extends ScenarioPersonas = ScenarioPersonas,
383
392
  > = {
384
393
  /** Always present — lazily initialised on first access for every function invocation */
@@ -386,6 +395,11 @@ export type PikkuWire<
386
395
  } & Partial<{
387
396
  wireType: PikkuWiringTypes
388
397
  wireId: string
398
+ /**
399
+ * A logger scoped to this invocation, when a host attaches one. Core never
400
+ * sets it; services that log fall back to the singleton logger.
401
+ */
402
+ logger: Logger
389
403
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
390
404
  traceId: string
391
405
  functionId: string
@@ -119,7 +119,7 @@ export interface PikkuPackageState {
119
119
  meta: HTTPWiringsMeta
120
120
  }
121
121
  channel: {
122
- channels: Map<string, CoreChannel<any, any>>
122
+ channels: Map<string, CoreChannel<any, any, any, any, any>>
123
123
  meta: ChannelsMeta
124
124
  }
125
125
  scheduler: {
@@ -119,12 +119,7 @@ function actorInstructions(persona: ResolvedPersona, task: string): string {
119
119
  : '',
120
120
  `Your goal in this conversation: ${task}.`,
121
121
  `Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
122
- // Every call the actor makes wants a schema'd object back, and a gateway
123
- // that cannot take a JSON *schema* degrades to OpenAI's `json_object`
124
- // mode — which refuses the request outright unless the word "json" appears
125
- // somewhere in the prompt. Saying it once here covers all three call sites
126
- // (turn, approvals, verdict), since each builds on these instructions, and
127
- // it costs nothing on providers that never needed telling.
122
+ // knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
128
123
  `Reply with json matching the schema you are given, and nothing else.`,
129
124
  ]
130
125
  .filter(Boolean)
@@ -0,0 +1,120 @@
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
+ import type { AIAgentInput, AIStreamChannel } from './ai-agent.types.js'
6
+ import type { StreamAIAgentOptions } from './ai-agent-prepare.js'
7
+ import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js'
8
+ import {
9
+ streamAIAgent,
10
+ resumeAIAgent,
11
+ interruptAIAgent,
12
+ } from './ai-agent-stream.js'
13
+ import { wrapChannelWithAGUI } from './ai-agent-agui.js'
14
+
15
+ export type AgentRPCOptions = {
16
+ sessionService?: SessionService<CoreUserSession>
17
+ }
18
+
19
+ /**
20
+ * `wire.rpc.agent`, implemented.
21
+ *
22
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
23
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
24
+ * primitive.
25
+ */
26
+ export const createAgentRPC = (
27
+ wire: PikkuRawWire,
28
+ options: AgentRPCOptions
29
+ ): PikkuRPC['agent'] => {
30
+ const params = () => ({
31
+ sessionService: options.sessionService,
32
+ getCredential: wire.getCredential?.bind(wire),
33
+ })
34
+
35
+ const streamingChannel = (): AIStreamChannel => {
36
+ const channel = wire.channel as unknown as AIStreamChannel | undefined
37
+ if (!channel) throw new Error('No channel available for streaming')
38
+ return channel
39
+ }
40
+
41
+ /** `run` and `approve` return the same shape; only the call differs. */
42
+ const asRunResult = (result: Awaited<ReturnType<typeof runAIAgent>>) => ({
43
+ runId: result.runId,
44
+ result: result.object ?? result.text,
45
+ usage: result.usage,
46
+ ...(result.status === 'suspended' && {
47
+ status: 'suspended' as const,
48
+ pendingApprovals: result.pendingApprovals,
49
+ }),
50
+ })
51
+
52
+ return {
53
+ run: async (agentName: string, input: AIAgentInput) =>
54
+ asRunResult(await runAIAgent(agentName, input, params())),
55
+
56
+ stream: async (
57
+ agentName: string,
58
+ input: {
59
+ message: string
60
+ threadId: string
61
+ resourceId: string
62
+ model?: string
63
+ temperature?: number
64
+ },
65
+ streamOptions?: StreamAIAgentOptions
66
+ ) => {
67
+ let currentRunId: string | undefined
68
+ await streamAIAgent(
69
+ agentName,
70
+ input,
71
+ wrapChannelWithAGUI(streamingChannel(), {
72
+ threadId: input.threadId,
73
+ getRunId: () => currentRunId,
74
+ }),
75
+ params(),
76
+ undefined,
77
+ {
78
+ ...streamOptions,
79
+ onRunCreated: (runId) => {
80
+ currentRunId = runId
81
+ streamOptions?.onRunCreated?.(runId)
82
+ },
83
+ }
84
+ )
85
+ },
86
+
87
+ resume: async (
88
+ runId: string,
89
+ input: { toolCallId: string; approved: boolean },
90
+ streamOptions?: StreamAIAgentOptions
91
+ ) => {
92
+ await resumeAIAgent(
93
+ { runId, ...input },
94
+ wrapChannelWithAGUI(streamingChannel(), { runId }),
95
+ params(),
96
+ streamOptions
97
+ )
98
+ },
99
+
100
+ interrupt: async (runId: string, reason?: 'speech' | 'user' | 'timeout') =>
101
+ interruptAIAgent(
102
+ { runId, ...(reason ? { reason } : {}) },
103
+ { sessionService: options.sessionService }
104
+ ),
105
+
106
+ approve: async (
107
+ runId: string,
108
+ approvals: { toolCallId: string; approved: boolean }[],
109
+ expectedAgentName?: string
110
+ ) =>
111
+ asRunResult(
112
+ await resumeAIAgentSync(
113
+ runId,
114
+ approvals,
115
+ { sessionService: options.sessionService },
116
+ expectedAgentName
117
+ )
118
+ ),
119
+ }
120
+ }
@@ -372,11 +372,7 @@ export function wrapChannelWithAGUI(
372
372
  break
373
373
  }
374
374
 
375
- // AG-UI has no event for speech, so it travels as CUSTOM like the other
376
- // pikku-specific ones. Dropping it instead — which is what this did —
377
- // means a voice agent reached over HTTP is inaudible: `voiceOutput`
378
- // synthesizes every sentence, the provider bills for it, and nothing
379
- // gets past the mapper.
375
+ // knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
380
376
  case 'audio-delta': {
381
377
  send({
382
378
  type: 'CUSTOM',
@@ -574,7 +574,7 @@ describe('non-streaming agent interruption', () => {
574
574
  updateRun: async (_runId: string, patch: unknown) => {
575
575
  updates.push(patch)
576
576
  },
577
- resolveApproval: async () => {},
577
+ resolveApproval: async () => true,
578
578
  },
579
579
  } as any)
580
580
 
@@ -648,6 +648,7 @@ describe('streaming resume interruption', () => {
648
648
  // for approvals that have already been given.
649
649
  resolveApproval: async () => {
650
650
  run.pendingApprovals = []
651
+ return true
651
652
  },
652
653
  },
653
654
  } as any)
@@ -19,7 +19,14 @@ export function resolveMemoryServices(
19
19
  } {
20
20
  const memoryConfig = agent.memory
21
21
  const storage = memoryConfig?.storage
22
- ? (singletonServices as any)[memoryConfig.storage]
22
+ ? // The service is named by string in agent config, so the lookup cannot be
23
+ // checked — only the shape it is required to have.
24
+ (
25
+ singletonServices as unknown as Record<
26
+ string,
27
+ AIStorageService | undefined
28
+ >
29
+ )[memoryConfig.storage]
23
30
  : singletonServices.aiStorage
24
31
  return { storage }
25
32
  }
@@ -12,7 +12,11 @@ import type {
12
12
  } from './ai-agent.types.js'
13
13
  import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
14
14
  import { PikkuError } from '../../errors/error-handler.js'
15
- import { checkAuthPermissions, runPermissions } from '../../permissions.js'
15
+ import {
16
+ checkAuthPermissions,
17
+ runPermissions,
18
+ type PermissionWire,
19
+ } from '../../permissions.js'
16
20
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
17
21
  import { ForbiddenError } from '../../errors/errors.js'
18
22
  import { verifyScopes } from '../../scopes.js'
@@ -319,8 +323,8 @@ export async function assertAgentAuthorized(
319
323
 
320
324
  await runPermissions({
321
325
  funcPermissions: agent.permissions,
322
- services: singletonServices as any,
323
- wire: wire as any,
326
+ services: singletonServices,
327
+ wire: wire as PermissionWire,
324
328
  data: {},
325
329
  packageName,
326
330
  label: 'agent',
@@ -395,7 +399,7 @@ export function createScopedChannel(
395
399
  toolName: event.toolName,
396
400
  args: event.args,
397
401
  ...(event.reason !== undefined ? { reason: event.reason } : {}),
398
- runId: (event as any).runId,
402
+ runId: event.runId,
399
403
  })
400
404
  return
401
405
  }
@@ -940,10 +944,7 @@ export async function prepareAgentRun(
940
944
 
941
945
  let messages: AIMessage[] = []
942
946
  if (storage) {
943
- // A tool whose run was interrupted may still be writing its result to this
944
- // thread. In voice the next turn lands within a second or two, so without
945
- // this the model would load context that is missing the very thing it is
946
- // about to be asked about.
947
+ // knowledge: decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md
947
948
  await awaitPendingInterruptNote(threadId)
948
949
  messages = await storage.getMessages(threadId, {
949
950
  lastN: memoryConfig?.lastMessages ?? 20,
@@ -959,10 +960,7 @@ export async function prepareAgentRun(
959
960
 
960
961
  const userContent: AIMessage['content'] = input.attachments?.length
961
962
  ? [
962
- // Omitted when there is nothing to say. An attachment on its own is a
963
- // real turn — a spoken one carries audio and no text at all — and an
964
- // empty text part alongside it is a part providers are entitled to
965
- // reject, for a caller who never wrote one.
963
+ // knowledge: decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md
966
964
  ...(input.message
967
965
  ? [{ type: 'text' as const, text: input.message }]
968
966
  : []),
@@ -43,6 +43,7 @@ const setup = (agentName: string, resourceId = 'u1') => {
43
43
  getRun: async () => suspendedRun(agentName, resourceId),
44
44
  resolveApproval: async (toolCallId: string) => {
45
45
  resolved.push(toolCallId)
46
+ return true
46
47
  },
47
48
  },
48
49
  } as any)
@@ -707,7 +707,7 @@ describe('resumeAIAgentSync', () => {
707
707
  createdAt: new Date(),
708
708
  updatedAt: new Date(),
709
709
  }),
710
- resolveApproval: async () => {},
710
+ resolveApproval: async () => true,
711
711
  updateRun: async () => {},
712
712
  },
713
713
  } as any)
@@ -784,6 +784,7 @@ describe('resumeAIAgentSync', () => {
784
784
  },
785
785
  resolveApproval: async (toolCallId: string, status: string) => {
786
786
  resolveCalls.push({ toolCallId, status })
787
+ return true
787
788
  },
788
789
  },
789
790
  aiStorage: {
@@ -816,6 +817,84 @@ describe('resumeAIAgentSync', () => {
816
817
  ])
817
818
  })
818
819
 
820
+ test('an approver that loses the claim never runs the tool', async () => {
821
+ addTestAgent('claim-agent')
822
+ pikkuState(null, 'agent', 'agentsMeta')['claim-agent'].tools = ['refund']
823
+ pikkuState(null, 'rpc', 'meta').refund = 'refund'
824
+ pikkuState(null, 'function', 'meta').refund = {
825
+ description: 'Refund',
826
+ sessionless: true,
827
+ }
828
+
829
+ let executions = 0
830
+ pikkuState(null, 'function', 'functions').set('refund', {
831
+ func: async () => {
832
+ executions++
833
+ return { refunded: true }
834
+ },
835
+ })
836
+
837
+ const run: AgentRunState = {
838
+ runId: 'run-claim',
839
+ agentName: 'claim-agent',
840
+ threadId: 'thread-claim',
841
+ resourceId: 'resource-claim',
842
+ status: 'suspended',
843
+ suspendReason: 'approval',
844
+ pendingApprovals: [
845
+ {
846
+ type: 'tool-call',
847
+ toolCallId: 'tc-claim',
848
+ toolName: 'refund',
849
+ args: {},
850
+ },
851
+ ],
852
+ usage: { inputTokens: 0, outputTokens: 0, model: 'test/test-model' },
853
+ createdAt: new Date(),
854
+ updatedAt: new Date(),
855
+ }
856
+
857
+ // Every concurrent approver reads the same `suspended` run with the same
858
+ // pending list; the store hands the claim to exactly one of them.
859
+ let claimsLeft = 1
860
+ pikkuState(null, 'package', 'singletonServices', {
861
+ logger: {
862
+ info: () => {},
863
+ warn: () => {},
864
+ error: () => {},
865
+ debug: () => {},
866
+ },
867
+ aiAgentRunner: {
868
+ run: async (): Promise<AIAgentStepResult> =>
869
+ makeStepResult({
870
+ text: 'Refunded',
871
+ usage: { inputTokens: 1, outputTokens: 1 },
872
+ }),
873
+ },
874
+ aiRunState: {
875
+ getRun: async () => run,
876
+ createRun: async () => 'unused',
877
+ updateRun: async () => {},
878
+ resolveApproval: async () => claimsLeft-- > 0,
879
+ },
880
+ aiStorage: {
881
+ saveMessages: async () => {},
882
+ getMessages: async () => [],
883
+ },
884
+ } as any)
885
+
886
+ const approvals = [{ toolCallId: 'tc-claim', approved: true }]
887
+ await resumeAIAgentSync('run-claim', approvals, asOwner('resource-claim'))
888
+ assert.equal(executions, 1, 'the winner ran the tool once')
889
+
890
+ await assert.rejects(
891
+ () =>
892
+ resumeAIAgentSync('run-claim', approvals, asOwner('resource-claim')),
893
+ /already resolved by another caller/
894
+ )
895
+ assert.equal(executions, 1, 'the loser did not run it a second time')
896
+ })
897
+
819
898
  test('records denied approvals and rejects agent mismatches', async () => {
820
899
  addTestAgent('resume-agent')
821
900
 
@@ -861,6 +940,7 @@ describe('resumeAIAgentSync', () => {
861
940
  updateRun: async () => {},
862
941
  resolveApproval: async (toolCallId: string, status: string) => {
863
942
  resolveCalls.push({ toolCallId, status })
943
+ return true
864
944
  },
865
945
  },
866
946
  aiStorage: {
@@ -959,7 +1039,7 @@ describe('resumeAIAgentSync', () => {
959
1039
  },
960
1040
  aiRunState: {
961
1041
  getRun: async () => run,
962
- resolveApproval: async () => {},
1042
+ resolveApproval: async () => true,
963
1043
  updateRun: async () => {},
964
1044
  },
965
1045
  aiStorage: {
@@ -43,6 +43,12 @@ import {
43
43
  trackToolExecution,
44
44
  } from './ai-agent-interrupt.js'
45
45
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
46
+ import {
47
+ applyInputMiddleware,
48
+ describeApprovals,
49
+ notifyAfterStep,
50
+ toAccumulatedStep,
51
+ } from './ai-agent-turn.js'
46
52
  import { resolveModelConfig } from './ai-agent-model-config.js'
47
53
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
48
54
  import { randomUUID } from './ai-agent-utils.js'
@@ -147,26 +153,20 @@ export async function runAIAgent(
147
153
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
148
154
  const sharedNotes: Record<string, unknown> = {}
149
155
 
150
- let modifiedMessages = runnerParams.messages
151
- let modifiedInstructions = runnerParams.instructions
152
- for (const mw of aiMiddlewares) {
153
- if (mw.modifyInput) {
154
- const result = await mw.modifyInput(singletonServices, {
155
- messages: modifiedMessages,
156
- instructions: modifiedInstructions,
157
- shared: sharedNotes,
158
- })
159
- modifiedMessages = result.messages
160
- modifiedInstructions = result.instructions
161
- }
162
- }
156
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
157
+ await applyInputMiddleware(
158
+ aiMiddlewares,
159
+ singletonServices,
160
+ {
161
+ messages: runnerParams.messages,
162
+ instructions: runnerParams.instructions,
163
+ },
164
+ sharedNotes
165
+ )
163
166
  runnerParams.messages = modifiedMessages
164
167
  runnerParams.instructions = modifiedInstructions
165
168
 
166
- // History records what the model was asked, which for a spoken turn is the
167
- // transcript rather than the base64 audio that arrived — see the same note on
168
- // the streaming path. Identity-checked, because a middleware may legitimately
169
- // replace the message list with something unrelated to this turn.
169
+ // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
170
170
  const lastModified = modifiedMessages[modifiedMessages.length - 1]
171
171
  const persistedUserMessage =
172
172
  lastModified?.id === userMessage.id ? lastModified : userMessage
@@ -181,10 +181,7 @@ export async function runAIAgent(
181
181
  updatedAt: new Date(),
182
182
  })
183
183
 
184
- // Registered on the same terms as `streamAIAgent`. A run created here is
185
- // visible to `interruptAIAgent` through `aiRunState` either way, so skipping
186
- // this would leave that call finding the run, passing the ownership check and
187
- // then failing to stop it — reported as if it were running on another host.
184
+ // knowledge: decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md
188
185
  const interruptHandle = registerInterruptibleRun(runId)
189
186
  runnerParams.abortSignal = interruptHandle.signal
190
187
  runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle)
@@ -216,52 +213,15 @@ export async function runAIAgent(
216
213
  totalUsage.inputTokens += stepResult.usage.inputTokens
217
214
  totalUsage.outputTokens += stepResult.usage.outputTokens
218
215
 
219
- for (const mw of aiMiddlewares) {
220
- if (mw.afterStep) {
221
- await mw.afterStep(singletonServices, {
222
- stepNumber: step,
223
- text: stepResult.text,
224
- toolCalls: stepResult.toolCalls,
225
- toolResults: stepResult.toolResults,
226
- usage: stepResult.usage,
227
- finishReason: stepResult.finishReason,
228
- })
229
- }
230
- }
216
+ await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult)
231
217
 
232
- accumulatedSteps.push({
233
- usage: stepResult.usage,
234
- toolCalls: stepResult.toolCalls.map((tc) => {
235
- const tr = stepResult.toolResults.find(
236
- (r) => r.toolCallId === tc.toolCallId
237
- )
238
- return {
239
- name: tc.toolName,
240
- args: tc.args as Record<string, unknown>,
241
- result:
242
- typeof tr?.result === 'string'
243
- ? tr.result
244
- : JSON.stringify(tr?.result ?? ''),
245
- }
246
- }),
247
- })
218
+ accumulatedSteps.push(toAccumulatedStep(stepResult))
248
219
 
249
220
  if (stepResult.toolCalls.length === 0) break
250
221
 
251
222
  const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools)
252
223
  if (approvalsNeeded.length > 0) {
253
- for (const approval of approvalsNeeded) {
254
- const toolDef = runnerParams.tools.find(
255
- (t) => t.name === approval.toolName
256
- )
257
- if (toolDef?.approvalDescriptionFn && !approval.reason) {
258
- try {
259
- approval.reason = await toolDef.approvalDescriptionFn(
260
- approval.args
261
- )
262
- } catch {}
263
- }
264
- }
224
+ await describeApprovals(approvalsNeeded, runnerParams.tools)
265
225
 
266
226
  const pendingApprovals = approvalsNeeded.map((a) =>
267
227
  a.agentRunId
@@ -385,10 +345,7 @@ export async function runAIAgent(
385
345
  usage: totalUsage,
386
346
  }
387
347
  } catch (error) {
388
- // An interrupt is not a failure, so it skips the `onError` hooks and never
389
- // becomes an `errorMessage`. Unlike the streaming path there is no partial
390
- // reply to hand back — nothing was delivered — so the caller gets a typed
391
- // throw it can tell apart from a provider outage instead of a result.
348
+ // knowledge: decisions/internals/an-agent-interrupt-is-not-a-failure.md
392
349
  const interruption = interruptHandle.interruption
393
350
  if (interruption || isAbortError(error)) {
394
351
  await aiRunState.updateRun(runId, { status: 'interrupted' })
@@ -470,11 +427,24 @@ export async function resumeAIAgentSync(
470
427
 
471
428
  const savedPendingApprovals = [...(run.pendingApprovals ?? [])]
472
429
 
430
+ // The read above is not a claim — concurrent resumes all see the same pending
431
+ // list. `resolveApproval` is the claim, and only the caller it returns true
432
+ // for may run the tool.
433
+ const claimedIds = new Set<string>()
473
434
  for (const { toolCallId, approved } of approvals) {
474
- await aiRunState.resolveApproval(
435
+ const claimed = await aiRunState.resolveApproval(
475
436
  toolCallId,
476
437
  approved ? 'approved' : 'denied'
477
438
  )
439
+ if (claimed) {
440
+ claimedIds.add(toolCallId)
441
+ }
442
+ }
443
+
444
+ if (approvals.length > 0 && claimedIds.size === 0) {
445
+ throw new Error(
446
+ `Approvals for run ${runId} were already resolved by another caller`
447
+ )
478
448
  }
479
449
 
480
450
  const { tools } = await buildToolDefs(
@@ -498,6 +468,8 @@ export async function resumeAIAgentSync(
498
468
  if (pending.type !== 'tool-call') continue
499
469
 
500
470
  const toolCallId = pending.toolCallId
471
+ if (!claimedIds.has(toolCallId)) continue
472
+
501
473
  let resultStr: string
502
474
 
503
475
  if (rejectedIds.has(toolCallId)) {
@@ -623,19 +595,13 @@ async function continueAfterToolResultSync(
623
595
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
624
596
  const sharedNotes: Record<string, unknown> = {}
625
597
 
626
- let modifiedMessages = trimmedMessages
627
- let modifiedInstructions = instructions
628
- for (const mw of aiMiddlewares) {
629
- if (mw.modifyInput) {
630
- const result = await mw.modifyInput(singletonServices, {
631
- messages: modifiedMessages,
632
- instructions: modifiedInstructions,
633
- shared: sharedNotes,
634
- })
635
- modifiedMessages = result.messages
636
- modifiedInstructions = result.instructions
637
- }
638
- }
598
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
599
+ await applyInputMiddleware(
600
+ aiMiddlewares,
601
+ singletonServices,
602
+ { messages: trimmedMessages, instructions: instructions },
603
+ sharedNotes
604
+ )
639
605
 
640
606
  const { tools: resumeTools } = await buildToolDefs(
641
607
  params,
@@ -684,52 +650,15 @@ async function continueAfterToolResultSync(
684
650
  totalUsage.inputTokens += stepResult.usage.inputTokens
685
651
  totalUsage.outputTokens += stepResult.usage.outputTokens
686
652
 
687
- for (const mw of aiMiddlewares) {
688
- if (mw.afterStep) {
689
- await mw.afterStep(singletonServices, {
690
- stepNumber: step,
691
- text: stepResult.text,
692
- toolCalls: stepResult.toolCalls,
693
- toolResults: stepResult.toolResults,
694
- usage: stepResult.usage,
695
- finishReason: stepResult.finishReason,
696
- })
697
- }
698
- }
653
+ await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult)
699
654
 
700
- accumulatedSteps.push({
701
- usage: stepResult.usage,
702
- toolCalls: stepResult.toolCalls.map((tc) => {
703
- const tr = stepResult.toolResults.find(
704
- (r) => r.toolCallId === tc.toolCallId
705
- )
706
- return {
707
- name: tc.toolName,
708
- args: tc.args as Record<string, unknown>,
709
- result:
710
- typeof tr?.result === 'string'
711
- ? tr.result
712
- : JSON.stringify(tr?.result ?? ''),
713
- }
714
- }),
715
- })
655
+ accumulatedSteps.push(toAccumulatedStep(stepResult))
716
656
 
717
657
  if (stepResult.toolCalls.length === 0) break
718
658
 
719
659
  const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools)
720
660
  if (approvalsNeeded.length > 0) {
721
- for (const approval of approvalsNeeded) {
722
- const toolDef = runnerParams.tools.find(
723
- (t) => t.name === approval.toolName
724
- )
725
- if (toolDef?.approvalDescriptionFn && !approval.reason) {
726
- try {
727
- approval.reason = await toolDef.approvalDescriptionFn(
728
- approval.args
729
- )
730
- } catch {}
731
- }
732
- }
661
+ await describeApprovals(approvalsNeeded, runnerParams.tools)
733
662
 
734
663
  const pendingApprovals = approvalsNeeded.map((a) =>
735
664
  a.agentRunId
@@ -832,7 +761,7 @@ async function continueAfterToolResultSync(
832
761
  run.threadId,
833
762
  run.resourceId,
834
763
  memoryConfig,
835
- null as any,
764
+ null,
836
765
  {
837
766
  ...result,
838
767
  text: outputText,