@pikku/core 0.12.79 → 0.12.82

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 (479) hide show
  1. package/CHANGELOG.md +473 -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/errors/index.d.ts +1 -1
  7. package/dist/errors/index.js +1 -1
  8. package/dist/function/function-runner.js +22 -47
  9. package/dist/function/functions.types.d.ts +9 -5
  10. package/dist/function/index.d.ts +1 -1
  11. package/dist/index.d.ts +14 -19
  12. package/dist/index.js +6 -11
  13. package/dist/middleware/auth-cookie.d.ts +0 -4
  14. package/dist/middleware/auth-cookie.js +38 -2
  15. package/dist/middleware/cors.js +1 -0
  16. package/dist/middleware/index.d.ts +1 -1
  17. package/dist/middleware/index.js +1 -1
  18. package/dist/middleware/remote-auth.js +14 -3
  19. package/dist/permissions.d.ts +8 -10
  20. package/dist/permissions.js +0 -11
  21. package/dist/pikku-state.js +16 -5
  22. package/dist/schema.js +35 -1
  23. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  24. package/dist/services/ai-run-state-service.d.ts +17 -1
  25. package/dist/services/in-memory-ai-run-state-service.d.ts +6 -2
  26. package/dist/services/in-memory-ai-run-state-service.js +11 -1
  27. package/dist/services/in-memory-workflow-service.js +2 -0
  28. package/dist/services/index.d.ts +15 -15
  29. package/dist/services/index.js +5 -5
  30. package/dist/services/local-content-request-handler.d.ts +21 -0
  31. package/dist/services/local-content-request-handler.js +72 -53
  32. package/dist/services/local-content.d.ts +6 -0
  33. package/dist/services/local-content.js +14 -1
  34. package/dist/services/scoped-credential-service.d.ts +21 -0
  35. package/dist/services/scoped-credential-service.js +53 -0
  36. package/dist/services/workflow-service.d.ts +4 -2
  37. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  38. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  39. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/ai-storage-service-tests.js +302 -0
  41. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  43. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  45. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  47. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  49. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  50. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  51. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  52. package/dist/testing/service-tests/session-store-tests.js +43 -0
  53. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  54. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  55. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  56. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  57. package/dist/testing/service-tests.d.ts +6 -0
  58. package/dist/testing/service-tests.js +26 -791
  59. package/dist/types/core.types.d.ts +11 -3
  60. package/dist/types/state.types.d.ts +14 -1
  61. package/dist/wirings/actor-flow/index.d.ts +1 -1
  62. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  63. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  64. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  65. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  66. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  67. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  68. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  69. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  70. package/dist/wirings/ai-agent/ai-agent-memory.js +37 -18
  71. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  72. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  73. package/dist/wirings/ai-agent/ai-agent-prepare.js +5 -9
  74. package/dist/wirings/ai-agent/ai-agent-runner.js +89 -147
  75. package/dist/wirings/ai-agent/ai-agent-stream.js +109 -97
  76. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +57 -0
  77. package/dist/wirings/ai-agent/ai-agent-turn.js +82 -0
  78. package/dist/wirings/ai-agent/ai-agent.types.d.ts +47 -2
  79. package/dist/wirings/ai-agent/index.d.ts +8 -7
  80. package/dist/wirings/ai-agent/index.js +5 -4
  81. package/dist/wirings/ai-agent/voice-input.js +1 -6
  82. package/dist/wirings/ai-agent/voice-output.js +2 -12
  83. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  84. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  85. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  86. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  87. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  88. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  89. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  90. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  91. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  92. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  93. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  94. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  95. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  96. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  97. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  98. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  99. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  100. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  101. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  102. package/dist/wirings/ai-scorer/index.js +5 -0
  103. package/dist/wirings/channel/channel-common.js +1 -0
  104. package/dist/wirings/channel/channel-handler.js +3 -5
  105. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  106. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  107. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  108. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  109. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  110. package/dist/wirings/channel/channel-runner.js +16 -8
  111. package/dist/wirings/channel/channel.types.d.ts +2 -0
  112. package/dist/wirings/channel/index.d.ts +5 -6
  113. package/dist/wirings/channel/index.js +3 -4
  114. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  115. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  116. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  117. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  118. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  119. package/dist/wirings/cli/channel/index.d.ts +1 -2
  120. package/dist/wirings/cli/channel/index.js +0 -1
  121. package/dist/wirings/cli/cli-runner.js +17 -3
  122. package/dist/wirings/cli/cli.types.d.ts +0 -8
  123. package/dist/wirings/cli/command-parser.js +13 -0
  124. package/dist/wirings/credential/index.d.ts +1 -1
  125. package/dist/wirings/gateway/gateway-runner.js +9 -2
  126. package/dist/wirings/gateway/index.d.ts +1 -1
  127. package/dist/wirings/http/http-routes.js +2 -0
  128. package/dist/wirings/http/http-runner.d.ts +0 -10
  129. package/dist/wirings/http/http-runner.js +14 -15
  130. package/dist/wirings/http/http.types.d.ts +0 -10
  131. package/dist/wirings/http/index.d.ts +2 -3
  132. package/dist/wirings/http/index.js +1 -1
  133. package/dist/wirings/mcp/index.d.ts +1 -1
  134. package/dist/wirings/mcp/mcp-runner.d.ts +15 -7
  135. package/dist/wirings/mcp/mcp-runner.js +18 -11
  136. package/dist/wirings/persona/index.d.ts +3 -4
  137. package/dist/wirings/persona/index.js +2 -3
  138. package/dist/wirings/queue/index.d.ts +1 -3
  139. package/dist/wirings/queue/index.js +1 -3
  140. package/dist/wirings/rpc/addon-runner.d.ts +4 -0
  141. package/dist/wirings/rpc/addon-runner.js +19 -3
  142. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  143. package/dist/wirings/rpc/rpc-runner.js +55 -86
  144. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  145. package/dist/wirings/rpc/wire-addon.d.ts +13 -0
  146. package/dist/wirings/rpc/wire-addon.js +4 -0
  147. package/dist/wirings/scheduler/index.d.ts +1 -1
  148. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  149. package/dist/wirings/trigger/index.d.ts +1 -1
  150. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  151. package/dist/wirings/trigger/trigger-runner.js +1 -0
  152. package/dist/wirings/virtual-user/index.d.ts +5 -6
  153. package/dist/wirings/virtual-user/index.js +2 -4
  154. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  155. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  156. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  157. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +100 -3
  158. package/dist/wirings/workflow/feature.js +7 -4
  159. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  160. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  161. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  162. package/dist/wirings/workflow/index.d.ts +12 -7
  163. package/dist/wirings/workflow/index.js +7 -3
  164. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  165. package/dist/wirings/workflow/pikku-scenario-service.js +64 -10
  166. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  167. package/dist/wirings/workflow/pikku-workflow-service.js +85 -493
  168. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  169. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  170. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  171. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  172. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  173. package/dist/wirings/workflow/workflow-approval.d.ts +67 -0
  174. package/dist/wirings/workflow/workflow-approval.js +177 -0
  175. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  176. package/dist/wirings/workflow/workflow-constants.js +35 -0
  177. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  178. package/dist/wirings/workflow/workflow-errors.js +112 -0
  179. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  180. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  181. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  182. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  183. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  184. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  185. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  186. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  187. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  188. package/dist/wirings/workflow/workflow-run-ownership.d.ts +17 -0
  189. package/dist/wirings/workflow/workflow-run-ownership.js +30 -0
  190. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  191. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  192. package/dist/wirings/workflow/workflow.types.d.ts +8 -1
  193. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  194. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  195. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  196. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  197. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  198. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  199. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  200. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  201. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  202. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  203. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  204. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  205. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  206. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  207. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  208. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  209. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  210. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  211. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  212. package/knowledge/decisions/internals/index.md +42 -3
  213. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  214. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  215. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  216. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  217. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  218. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  219. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  220. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  221. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  222. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  223. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  224. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  225. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  226. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  227. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  228. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  229. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  230. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  231. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  232. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  233. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  234. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  235. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  236. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  237. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  238. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +50 -0
  239. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  240. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  241. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  242. package/knowledge/decisions/security/index.md +9 -0
  243. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  244. package/knowledge/questions/index.md +2 -1
  245. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  246. package/package.json +15 -3
  247. package/scripts/generate-api-report.d.mts +1 -0
  248. package/scripts/generate-api-report.mjs +89 -0
  249. package/scripts/generate-api-report.mts +343 -0
  250. package/src/api-report.test.ts +69 -0
  251. package/src/column-form.ts +1 -4
  252. package/src/crypto-utils.test.ts +15 -2
  253. package/src/dev/hot-reload.ts +2 -7
  254. package/src/ecosystem.ts +40 -0
  255. package/src/errors/error-handler.ts +5 -3
  256. package/src/errors/error.test.ts +4 -1
  257. package/src/errors/index.ts +1 -1
  258. package/src/function/function-runner.test.ts +53 -1
  259. package/src/function/function-runner.ts +33 -48
  260. package/src/function/functions.types.ts +15 -9
  261. package/src/function/index.ts +0 -2
  262. package/src/handle-error.ts +1 -1
  263. package/src/index.ts +3 -57
  264. package/src/middleware/auth-cookie.test.ts +50 -0
  265. package/src/middleware/auth-cookie.ts +38 -2
  266. package/src/middleware/cors.ts +2 -1
  267. package/src/middleware/index.ts +1 -1
  268. package/src/middleware/remote-auth.test.ts +43 -0
  269. package/src/middleware/remote-auth.ts +17 -3
  270. package/src/middleware-runner.ts +4 -2
  271. package/src/no-any-casts.test.ts +57 -0
  272. package/src/permissions.test.ts +3 -1
  273. package/src/permissions.ts +22 -24
  274. package/src/pikku-state.ts +22 -6
  275. package/src/public-surface.json +554 -0
  276. package/src/public-surface.json.README +25 -0
  277. package/src/public-surface.test.ts +105 -0
  278. package/src/removed-legacy-exports.test.ts +62 -0
  279. package/src/schema.test.ts +78 -0
  280. package/src/schema.ts +36 -1
  281. package/src/services/ai-agent-runner-service.ts +12 -1
  282. package/src/services/ai-run-state-service.ts +18 -1
  283. package/src/services/audit-service.ts +2 -2
  284. package/src/services/in-memory-ai-run-state-service.ts +16 -2
  285. package/src/services/in-memory-workflow-service.ts +2 -0
  286. package/src/services/index.ts +4 -47
  287. package/src/services/local-content-request-handler.test.ts +43 -5
  288. package/src/services/local-content-request-handler.ts +103 -74
  289. package/src/services/local-content.ts +15 -1
  290. package/src/services/local-email-service.ts +5 -1
  291. package/src/services/scoped-credential-service.test.ts +86 -0
  292. package/src/services/scoped-credential-service.ts +63 -0
  293. package/src/services/system-role-guard.test.ts +4 -1
  294. package/src/services/workflow-service.ts +9 -2
  295. package/src/side-effects-are-declared.test.ts +84 -0
  296. package/src/source-files-stay-composable.test.ts +41 -0
  297. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  298. package/src/testing/service-tests/ai-storage-service-tests.ts +379 -0
  299. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  300. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  301. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  302. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  303. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  304. package/src/testing/service-tests/session-store-tests.ts +62 -0
  305. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  306. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  307. package/src/testing/service-tests.ts +31 -1111
  308. package/src/types/core.types.ts +14 -3
  309. package/src/types/state.types.ts +17 -1
  310. package/src/wirings/actor-flow/index.ts +0 -3
  311. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  312. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  313. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  314. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  315. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  316. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  317. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  318. package/src/wirings/ai-agent/ai-agent-memory.ts +62 -39
  319. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  320. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  321. package/src/wirings/ai-agent/ai-agent-prepare.ts +12 -12
  322. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  323. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  324. package/src/wirings/ai-agent/ai-agent-runner.ts +120 -160
  325. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  326. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  327. package/src/wirings/ai-agent/ai-agent-stream.ts +152 -117
  328. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  329. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  330. package/src/wirings/ai-agent/ai-agent-turn.ts +122 -0
  331. package/src/wirings/ai-agent/ai-agent.types.ts +65 -5
  332. package/src/wirings/ai-agent/index.ts +2 -16
  333. package/src/wirings/ai-agent/voice-input.ts +1 -6
  334. package/src/wirings/ai-agent/voice-output.ts +2 -12
  335. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  336. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  337. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  338. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  339. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  340. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  341. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  342. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  343. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  344. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  345. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  346. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  347. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  348. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  349. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  350. package/src/wirings/ai-scorer/index.ts +24 -0
  351. package/src/wirings/channel/channel-common.ts +5 -2
  352. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  353. package/src/wirings/channel/channel-handler.ts +7 -7
  354. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  355. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  356. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  357. package/src/wirings/channel/channel-runner.ts +33 -20
  358. package/src/wirings/channel/channel.types.ts +2 -0
  359. package/src/wirings/channel/index.ts +1 -20
  360. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  361. package/src/wirings/channel/local/local-channel-runner.ts +9 -2
  362. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  363. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  364. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  365. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  366. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  367. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  368. package/src/wirings/cli/channel/index.ts +0 -7
  369. package/src/wirings/cli/cli-runner.test.ts +68 -0
  370. package/src/wirings/cli/cli-runner.ts +25 -5
  371. package/src/wirings/cli/cli.types.ts +0 -39
  372. package/src/wirings/cli/command-parser.test.ts +19 -0
  373. package/src/wirings/cli/command-parser.ts +17 -1
  374. package/src/wirings/credential/index.ts +0 -1
  375. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  376. package/src/wirings/gateway/gateway-runner.ts +27 -19
  377. package/src/wirings/gateway/index.ts +0 -3
  378. package/src/wirings/http/http-routes.ts +4 -1
  379. package/src/wirings/http/http-runner.test.ts +66 -0
  380. package/src/wirings/http/http-runner.ts +21 -28
  381. package/src/wirings/http/http.types.ts +0 -15
  382. package/src/wirings/http/index.ts +2 -8
  383. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  384. package/src/wirings/http/web-request.ts +1 -1
  385. package/src/wirings/mcp/index.ts +0 -1
  386. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  387. package/src/wirings/mcp/mcp-runner.ts +37 -21
  388. package/src/wirings/persona/index.ts +0 -8
  389. package/src/wirings/persona/persona-environments.test.ts +14 -3
  390. package/src/wirings/persona/persona.test.ts +13 -3
  391. package/src/wirings/persona/validate-personas.ts +5 -1
  392. package/src/wirings/queue/index.ts +0 -14
  393. package/src/wirings/queue/queue-runner.ts +1 -1
  394. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  395. package/src/wirings/rpc/addon-runner.ts +34 -3
  396. package/src/wirings/rpc/addon-secrets.test.ts +261 -0
  397. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  398. package/src/wirings/rpc/rpc-runner.ts +60 -136
  399. package/src/wirings/rpc/rpc-types.ts +11 -0
  400. package/src/wirings/rpc/wire-addon.ts +20 -1
  401. package/src/wirings/scheduler/index.ts +0 -1
  402. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  403. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  404. package/src/wirings/trigger/index.ts +0 -1
  405. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  406. package/src/wirings/trigger/trigger-runner.ts +7 -5
  407. package/src/wirings/virtual-user/index.ts +0 -16
  408. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  409. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  410. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  411. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  412. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  413. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  414. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  415. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +117 -4
  416. package/src/wirings/workflow/feature.ts +7 -4
  417. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  418. package/src/wirings/workflow/graph/graph-runner.test.ts +76 -2
  419. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  420. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  421. package/src/wirings/workflow/index.ts +14 -24
  422. package/src/wirings/workflow/pikku-scenario-service.ts +101 -27
  423. package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -14
  424. package/src/wirings/workflow/pikku-workflow-service.ts +214 -743
  425. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  426. package/src/wirings/workflow/scenario-hooks.test.ts +52 -0
  427. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  428. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  429. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  430. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  431. package/src/wirings/workflow/workflow-approval.ts +289 -0
  432. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  433. package/src/wirings/workflow/workflow-constants.ts +47 -0
  434. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  435. package/src/wirings/workflow/workflow-errors.ts +128 -0
  436. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  437. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  438. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  439. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  440. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  441. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  442. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  443. package/src/wirings/workflow/workflow-run-authority.test.ts +212 -0
  444. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  445. package/src/wirings/workflow/workflow-run-ownership.ts +37 -0
  446. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  447. package/src/wirings/workflow/workflow.types.ts +7 -9
  448. package/src/wirings-stay-decoupled.test.ts +127 -0
  449. package/tsconfig.json +1 -1
  450. package/tsconfig.tsbuildinfo +1 -1
  451. package/dist/internal.d.ts +0 -3
  452. package/dist/internal.js +0 -2
  453. package/dist/middleware/timeout.d.ts +0 -9
  454. package/dist/middleware/timeout.js +0 -15
  455. package/dist/pikku-response.d.ts +0 -6
  456. package/dist/pikku-response.js +0 -6
  457. package/dist/services/gopass-secrets.d.ts +0 -15
  458. package/dist/services/gopass-secrets.js +0 -76
  459. package/dist/services/http-scenario-actors.d.ts +0 -75
  460. package/dist/services/http-scenario-actors.js +0 -195
  461. package/dist/services/http-user-flow-actors.d.ts +0 -67
  462. package/dist/services/http-user-flow-actors.js +0 -193
  463. package/dist/services/scenario-actors-service.d.ts +0 -127
  464. package/dist/services/scenario-actors-service.js +0 -40
  465. package/dist/services/user-flow-actors-service.d.ts +0 -39
  466. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  467. package/dist/wirings/credential/wire-credential.js +0 -47
  468. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  469. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  470. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  471. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  472. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  473. package/dist/wirings/scope/wire-scope.js +0 -32
  474. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  475. package/dist/wirings/workflow/dsl/index.js +0 -4
  476. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  477. package/dist/wirings/workflow/graph/index.js +0 -4
  478. package/src/internal.ts +0 -10
  479. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/workflow-run-engine.types.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AIStreamChannel,
3
3
  AIStreamEvent,
4
+ AIAgentStep,
4
5
  AIMessage,
5
6
  AIToolCall,
6
7
  AIToolResult,
@@ -9,7 +10,12 @@ import type {
9
10
  CoreAIAgent,
10
11
  AIAgentMemoryConfig,
11
12
  } from './ai-agent.types.js'
13
+ import {
14
+ finalizeAgentRun,
15
+ lastUserMessageText,
16
+ } from './ai-agent-finalize.js'
12
17
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
18
+ import { applyInputMiddleware } from './ai-agent-turn.js'
13
19
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
14
20
  import { randomUUID } from './ai-agent-utils.js'
15
21
  import { SPOKEN_TRANSCRIPT } from './voice-input.js'
@@ -67,6 +73,8 @@ type PersistingChannel = AIStreamChannel & {
67
73
  fullText: string
68
74
  flush: (opts?: { interrupted?: boolean }) => Promise<void>
69
75
  totalUsage: { inputTokens: number; outputTokens: number; model?: string }
76
+ /** Every tool the run called, kept for the whole run rather than per step. */
77
+ runToolCalls: NonNullable<AIAgentStep['toolCalls']>
70
78
  }
71
79
 
72
80
  function createPersistingChannel(
@@ -88,6 +96,9 @@ function createPersistingChannel(
88
96
  inputTokens: 0,
89
97
  outputTokens: 0,
90
98
  }
99
+ // Survives the per-step flush below, which clears its own buffers: the run
100
+ // record needs every call the run made, not just the last step's.
101
+ const runToolCalls: NonNullable<AIAgentStep['toolCalls']> = []
91
102
 
92
103
  const flushStep = async (opts?: { interrupted?: boolean }) => {
93
104
  if (!storage) return
@@ -136,6 +147,8 @@ function createPersistingChannel(
136
147
  })
137
148
  }
138
149
 
150
+ const runToolCallIndex = new Map<string, number>()
151
+
139
152
  const channel: PersistingChannel = {
140
153
  channelId: parent.channelId,
141
154
  openingData: parent.openingData,
@@ -148,6 +161,9 @@ function createPersistingChannel(
148
161
  get totalUsage() {
149
162
  return totalUsage
150
163
  },
164
+ get runToolCalls() {
165
+ return runToolCalls
166
+ },
151
167
  flush: flushStep,
152
168
  close: () => parent.close(),
153
169
  sendBinary: (data) => parent.sendBinary(data),
@@ -156,6 +172,26 @@ function createPersistingChannel(
156
172
  // the client was streamed, and an interrupted run has to be able to
157
173
  // report the fragment it got through even with persistence turned off.
158
174
  if (event.type === 'text-delta') fullText += event.text
175
+ if (event.type === 'tool-call') {
176
+ runToolCallIndex.set(event.toolCallId, runToolCalls.length)
177
+ runToolCalls.push({
178
+ name: event.toolName,
179
+ args: event.args as Record<string, unknown>,
180
+ result: '',
181
+ })
182
+ }
183
+ if (event.type === 'tool-result') {
184
+ const index = runToolCallIndex.get(event.toolCallId)
185
+ const result =
186
+ typeof event.result === 'string'
187
+ ? event.result
188
+ : JSON.stringify(event.result)
189
+ const call = index === undefined ? undefined : runToolCalls[index]
190
+ if (call) {
191
+ call.result = result
192
+ if (event.error) call.error = event.error
193
+ }
194
+ }
159
195
  if (storage) {
160
196
  switch (event.type) {
161
197
  case 'text-delta':
@@ -176,6 +212,7 @@ function createPersistingChannel(
176
212
  typeof event.result === 'string'
177
213
  ? event.result
178
214
  : JSON.stringify(event.result),
215
+ ...(event.error ? { error: event.error } : {}),
179
216
  })
180
217
  break
181
218
  case 'generative-ui':
@@ -202,44 +239,67 @@ function createPersistingChannel(
202
239
  return channel
203
240
  }
204
241
 
242
+ /**
243
+ * Agents already warned about, so a per-request hook does not become a
244
+ * per-request log line.
245
+ */
246
+ const warnedUnstreamedOutputHooks = new Set<string>()
247
+
248
+ /**
249
+ * `modifyOutput` does not run on a streamed run at all. Nothing here could act
250
+ * on what it returns — the text has already reached the client, and
251
+ * `createPersistingChannel` flushes each step to storage as it goes, so by the
252
+ * time the run ends the transcript is already written.
253
+ *
254
+ * Rewriting on this path belongs to `modifyOutputStream`, which genuinely
255
+ * works: the stream middleware wraps the persisting channel, so what is stored
256
+ * and accumulated is already what the client was sent. A middleware that
257
+ * rewrites in `modifyOutput` only — a redaction hook, typically — is therefore
258
+ * silently ineffective when the agent is streamed, and is told so once.
259
+ */
260
+ const warnUnstreamedOutputHooks = (
261
+ agentName: string,
262
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
263
+ logger?: { warn: (...args: any[]) => void }
264
+ ) => {
265
+ if (warnedUnstreamedOutputHooks.has(agentName)) return
266
+ const unstreamed = aiMiddlewares.some(
267
+ (mw) => mw.modifyOutput && !mw.modifyOutputStream
268
+ )
269
+ if (!unstreamed) return
270
+ warnedUnstreamedOutputHooks.add(agentName)
271
+ logger?.warn(
272
+ `Agent '${agentName}' has AI middleware with modifyOutput but no modifyOutputStream — modifyOutput does not apply to streamed runs. Implement modifyOutputStream to affect a streamed reply.`
273
+ )
274
+ }
275
+
205
276
  async function postStreamCleanup(
206
277
  persistingChannel: PersistingChannel,
207
- aiMiddlewares: PikkuAIMiddlewareHooks[],
208
- singletonServices: any,
209
- messages: AIMessage[],
210
278
  aiRunState: AIRunStateService,
211
- runId: string
212
- ): Promise<void> {
213
- const usage = persistingChannel.totalUsage
214
- let outputText = persistingChannel.fullText
215
- let outputMessages = messages
216
- for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
217
- const mw = aiMiddlewares[i]
218
- if (mw.modifyOutput) {
219
- const result = await mw.modifyOutput(singletonServices, {
220
- text: outputText,
221
- messages: outputMessages,
222
- usage: {
223
- inputTokens: usage.inputTokens,
224
- outputTokens: usage.outputTokens,
225
- },
226
- })
227
- outputText = result.text
228
- outputMessages = result.messages
229
- }
279
+ runId: string,
280
+ run: {
281
+ agentName: string
282
+ threadId: string
283
+ resourceId?: string
284
+ input: string
230
285
  }
231
-
232
- await aiRunState.updateRun(runId, {
233
- status: 'completed',
234
- ...(usage.model
235
- ? {
236
- usage: {
237
- inputTokens: usage.inputTokens,
238
- outputTokens: usage.outputTokens,
239
- model: usage.model,
240
- },
241
- }
242
- : {}),
286
+ ): Promise<void> {
287
+ await finalizeAgentRun(aiRunState, {
288
+ runId,
289
+ agentName: run.agentName,
290
+ threadId: run.threadId,
291
+ resourceId: run.resourceId,
292
+ input: run.input,
293
+ // Already what the client received: the stream middleware wraps the
294
+ // persisting channel, so both were accumulated post-rewrite.
295
+ text: persistingChannel.fullText,
296
+ steps: [
297
+ {
298
+ usage: persistingChannel.totalUsage,
299
+ toolCalls: persistingChannel.runToolCalls,
300
+ },
301
+ ],
302
+ usage: persistingChannel.totalUsage,
243
303
  })
244
304
  }
245
305
 
@@ -676,38 +736,26 @@ export async function streamAIAgent(
676
736
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
677
737
  const sharedNotes: Record<string, unknown> = {}
678
738
 
679
- let modifiedMessages = runnerParams.messages
680
- let modifiedInstructions = runnerParams.instructions
681
- for (const mw of aiMiddlewares) {
682
- if (mw.modifyInput) {
683
- const result = await mw.modifyInput(singletonServices, {
684
- messages: modifiedMessages,
685
- instructions: modifiedInstructions,
686
- shared: sharedNotes,
687
- })
688
- modifiedMessages = result.messages
689
- modifiedInstructions = result.instructions
690
- }
691
- }
739
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
740
+ await applyInputMiddleware(
741
+ aiMiddlewares,
742
+ singletonServices,
743
+ {
744
+ messages: runnerParams.messages,
745
+ instructions: runnerParams.instructions,
746
+ },
747
+ sharedNotes
748
+ )
692
749
  runnerParams.messages = modifiedMessages
693
750
  runnerParams.instructions = modifiedInstructions
694
751
 
695
- // Sent on the raw channel, ahead of the run. A voice client sent audio and so
696
- // has no idea what it said; until this arrives its own message is a blank
697
- // bubble. Ahead of the run rather than alongside it because the answer starts
698
- // streaming within a few hundred milliseconds, and a question that appears
699
- // after its answer reads as the wrong question.
752
+ // knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
700
753
  const transcript = sharedNotes[SPOKEN_TRANSCRIPT]
701
754
  if (typeof transcript === 'string') {
702
755
  channel.send({ type: 'transcript', text: transcript })
703
756
  }
704
757
 
705
- // What goes into thread history is what the model was actually asked. For a
706
- // spoken turn that is not what arrived over the wire: the wire carried a
707
- // base64 audio blob, and persisting it would write megabytes of unreadable
708
- // data into the history while losing the only readable record of the turn.
709
- // Identity-checked rather than assumed — a middleware is free to rewrite the
710
- // message list into something with no relation to the turn.
758
+ // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
711
759
  const lastModified = modifiedMessages[modifiedMessages.length - 1]
712
760
  const persistedUserMessage =
713
761
  lastModified?.id === userMessage.id ? lastModified : userMessage
@@ -734,6 +782,8 @@ export async function streamAIAgent(
734
782
  await storage.saveMessages(threadId, [persistedUserMessage])
735
783
  }
736
784
 
785
+ warnUnstreamedOutputHooks(agentName, aiMiddlewares, singletonServices.logger)
786
+
737
787
  const streamMiddleware = aiMiddlewares
738
788
  .filter((mw) => mw.modifyOutputStream)
739
789
  .map((mw) => {
@@ -768,7 +818,7 @@ export async function streamAIAgent(
768
818
  {
769
819
  wireInheritedChannelMiddleware: meta?.channelMiddleware,
770
820
  wireChannelMiddleware: [
771
- ...((agent.channelMiddleware as any[]) ?? []),
821
+ ...(agent.channelMiddleware ?? []),
772
822
  ...streamMiddleware,
773
823
  ],
774
824
  }
@@ -792,10 +842,7 @@ export async function streamAIAgent(
792
842
 
793
843
  const credentialFilteredChannel: AIStreamChannel = {
794
844
  ...wrappedChannel,
795
- // Returns what the inner send returns. Middleware runs asynchronously, so
796
- // swallowing the promise here makes every `await channel.send(...)` upstream
797
- // a no-op — including the one that waits for a buffering hook to flush
798
- // before the channel closes.
845
+ // knowledge: decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md
799
846
  send: (event: AIStreamEvent) => {
800
847
  if (
801
848
  event.type === 'tool-result' &&
@@ -863,20 +910,14 @@ export async function streamAIAgent(
863
910
  return persistingChannel.fullText
864
911
  }
865
912
 
866
- await postStreamCleanup(
867
- persistingChannel,
868
- aiMiddlewares,
869
- singletonServices,
870
- runnerParams.messages,
871
- aiRunState,
872
- runId
873
- )
913
+ await postStreamCleanup(persistingChannel, aiRunState, runId, {
914
+ agentName,
915
+ threadId,
916
+ resourceId: input.resourceId,
917
+ input: lastUserMessageText(runnerParams.messages),
918
+ })
874
919
 
875
- // Through the middleware rather than straight at the channel, and awaited.
876
- // `done` is the only signal a stream hook gets that the reply is over, and
877
- // the ones that buffer need it: `voiceOutput` speaks a trailing fragment
878
- // that never reached a full stop, and waits for audio it has already paid
879
- // to synthesize. Sent raw, that work is discarded by the `close()` below.
920
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
880
921
  await outputChannel.send({ type: 'done' })
881
922
  channel.close()
882
923
  return persistingChannel.fullText
@@ -979,12 +1020,7 @@ export async function interruptAIAgent(
979
1020
  reason: input.reason ?? 'user',
980
1021
  })
981
1022
 
982
- // A run still marked `running` that this process cannot abort is executing on
983
- // another instance. Returning a bare `false` there is indistinguishable from
984
- // "it already finished", so the one deployment shape this registry does not
985
- // cover fails silently — an agent that simply refuses to shut up, with nothing
986
- // in the logs. Say so instead. See `signalRunInterrupt` for the fix (fan the
987
- // interrupt out over eventHub so every instance tries locally).
1023
+ // knowledge: decisions/internals/an-agent-run-owned-by-another-instance-says-so.md
988
1024
  if (!stopped && run.status === 'running') {
989
1025
  logger?.warn(
990
1026
  `Could not interrupt run ${input.runId}: it is running in another process. ` +
@@ -1037,10 +1073,18 @@ export async function resumeAIAgent(
1037
1073
 
1038
1074
  await assertAgentAuthorized(agent, params, packageName)
1039
1075
 
1040
- await aiRunState.resolveApproval(
1076
+ // The read above is not a claim — concurrent resumes all see the same pending
1077
+ // approval. `resolveApproval` is the claim, and the loser must not go on to
1078
+ // run the tool a second time.
1079
+ const claimed = await aiRunState.resolveApproval(
1041
1080
  input.toolCallId,
1042
1081
  input.approved ? 'approved' : 'denied'
1043
1082
  )
1083
+ if (!claimed) {
1084
+ throw new Error(
1085
+ `Approval for toolCallId ${input.toolCallId} was already resolved by another caller`
1086
+ )
1087
+ }
1044
1088
 
1045
1089
  const { storage } = resolveMemoryServices(agent, singletonServices)
1046
1090
  const memoryConfig = agent.memory
@@ -1199,16 +1243,17 @@ export async function resumeAIAgent(
1199
1243
  typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
1200
1244
 
1201
1245
  let toolResult: unknown
1202
- let isError = false
1246
+ let toolError: string | undefined
1203
1247
  try {
1204
1248
  toolResult = await matchingTool.execute(toolArgs)
1205
1249
  } catch (execErr: any) {
1206
1250
  if (execErr?.payload?.error === 'missing_credential') {
1207
1251
  toolResult = execErr.payload
1252
+ toolError = 'missing_credential'
1208
1253
  } else {
1209
- toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
1254
+ toolError = execErr instanceof Error ? execErr.message : String(execErr)
1255
+ toolResult = `Error: ${toolError}`
1210
1256
  }
1211
- isError = true
1212
1257
  }
1213
1258
 
1214
1259
  const resultStr =
@@ -1235,7 +1280,7 @@ export async function resumeAIAgent(
1235
1280
  toolCallId: input.toolCallId,
1236
1281
  toolName: pending.toolName,
1237
1282
  result: toolResult,
1238
- ...(isError ? { isError: true } : {}),
1283
+ ...(toolError ? { error: toolError } : {}),
1239
1284
  })
1240
1285
  }
1241
1286
 
@@ -1317,26 +1362,23 @@ async function continueAfterToolResult(
1317
1362
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
1318
1363
  const sharedNotes: Record<string, unknown> = {}
1319
1364
 
1320
- let modifiedMessages = trimmedMessages
1321
- let modifiedInstructions = instructions
1322
- for (const mw of aiMiddlewares) {
1323
- if (mw.modifyInput) {
1324
- const result = await mw.modifyInput(singletonServices, {
1325
- messages: modifiedMessages,
1326
- instructions: modifiedInstructions,
1327
- shared: sharedNotes,
1328
- })
1329
- modifiedMessages = result.messages
1330
- modifiedInstructions = result.instructions
1331
- }
1332
- }
1365
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
1366
+ await applyInputMiddleware(
1367
+ aiMiddlewares,
1368
+ singletonServices,
1369
+ { messages: trimmedMessages, instructions: instructions },
1370
+ sharedNotes
1371
+ )
1333
1372
 
1334
- // Resuming is as interruptible as the first turn. It is the same person
1335
- // listening to the same voice, and after an approval it is where most of the
1336
- // reply actually gets spoken — an approved delete is followed by the agent
1337
- // talking about it, and that is a normal thing to talk over.
1373
+ // knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
1338
1374
  const interruptHandle = registerInterruptibleRun(run.runId)
1339
1375
 
1376
+ warnUnstreamedOutputHooks(
1377
+ run.agentName,
1378
+ aiMiddlewares,
1379
+ singletonServices.logger
1380
+ )
1381
+
1340
1382
  const streamMiddleware = aiMiddlewares
1341
1383
  .filter((mw) => mw.modifyOutputStream)
1342
1384
  .map((mw) => {
@@ -1369,7 +1411,7 @@ async function continueAfterToolResult(
1369
1411
  {
1370
1412
  wireInheritedChannelMiddleware: meta?.channelMiddleware,
1371
1413
  wireChannelMiddleware: [
1372
- ...((agent.channelMiddleware as any[]) ?? []),
1414
+ ...(agent.channelMiddleware ?? []),
1373
1415
  ...streamMiddleware,
1374
1416
  ],
1375
1417
  }
@@ -1458,19 +1500,12 @@ async function continueAfterToolResult(
1458
1500
  return
1459
1501
  }
1460
1502
 
1461
- await postStreamCleanup(
1462
- persistingChannel,
1463
- aiMiddlewares,
1464
- singletonServices,
1465
- runnerParams.messages,
1466
- aiRunState,
1467
- run.runId
1468
- )
1503
+ await postStreamCleanup(persistingChannel, aiRunState, run.runId, {
1504
+ ...run,
1505
+ input: lastUserMessageText(runnerParams.messages),
1506
+ })
1469
1507
 
1470
- // Through the middleware, for the same reason as the first turn — and it
1471
- // matters more here. After an approval, most of what gets spoken is the
1472
- // agent talking about what it just did, so this is the half of the reply a
1473
- // dropped flush would silence.
1508
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
1474
1509
  await wrappedChannel.send({ type: 'done' })
1475
1510
  channel.close()
1476
1511
  } catch (err) {
@@ -114,7 +114,7 @@ const createHarness = (
114
114
  ],
115
115
  messages: [],
116
116
  }),
117
- resolveApproval: async () => {},
117
+ resolveApproval: async () => true,
118
118
  },
119
119
  } as any)
120
120
 
@@ -0,0 +1,67 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { toAccumulatedStep } from './ai-agent-turn.js'
5
+ import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
6
+
7
+ const stepResult = (
8
+ overrides?: Partial<AIAgentStepResult>
9
+ ): AIAgentStepResult => ({
10
+ text: '',
11
+ toolCalls: [],
12
+ toolResults: [],
13
+ usage: { inputTokens: 0, outputTokens: 0 },
14
+ finishReason: 'stop',
15
+ ...overrides,
16
+ })
17
+
18
+ describe('toAccumulatedStep', () => {
19
+ test('carries a tool failure as its own field, not only as rendered text', () => {
20
+ const step = toAccumulatedStep(
21
+ stepResult({
22
+ toolCalls: [
23
+ { toolCallId: 'call-1', toolName: 'lookupOrder', args: { id: 7 } },
24
+ ],
25
+ toolResults: [
26
+ {
27
+ toolCallId: 'call-1',
28
+ toolName: 'lookupOrder',
29
+ result: 'Error: order service unreachable',
30
+ error: 'order service unreachable',
31
+ },
32
+ ],
33
+ })
34
+ )
35
+
36
+ assert.deepEqual(step.toolCalls, [
37
+ {
38
+ name: 'lookupOrder',
39
+ args: { id: 7 },
40
+ result: 'Error: order service unreachable',
41
+ error: 'order service unreachable',
42
+ },
43
+ ])
44
+ })
45
+
46
+ test('leaves error unset on a tool that returned normally, even if it says Error', () => {
47
+ // A tool is allowed to return the word "Error" — which is exactly why
48
+ // "did this fail" cannot be answered by matching on the result text.
49
+ const step = toAccumulatedStep(
50
+ stepResult({
51
+ toolCalls: [
52
+ { toolCallId: 'call-1', toolName: 'searchLogs', args: { q: 'x' } },
53
+ ],
54
+ toolResults: [
55
+ {
56
+ toolCallId: 'call-1',
57
+ toolName: 'searchLogs',
58
+ result: 'Error: connection refused (1 match)',
59
+ },
60
+ ],
61
+ })
62
+ )
63
+
64
+ assert.equal(step.toolCalls[0].error, undefined)
65
+ assert.ok(!('error' in step.toolCalls[0]))
66
+ })
67
+ })
@@ -0,0 +1,122 @@
1
+ import type { CoreSingletonServices } from '../../types/core.types.js'
2
+ import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js'
3
+
4
+ /** Exactly what `modifyInput` accepts and returns, taken from the hook itself. */
5
+ type TurnInput = Pick<
6
+ Parameters<NonNullable<PikkuAIMiddlewareHooks['modifyInput']>>[1],
7
+ 'messages' | 'instructions'
8
+ >
9
+
10
+ /**
11
+ * Run every `modifyInput` hook in order, threading each result into the next.
12
+ *
13
+ * Shared by the four places a turn starts — first turn and resume, on both the
14
+ * streaming and non-streaming paths. `sharedNotes` is one bag per run that the
15
+ * caller keeps, because hooks use it to pass things forward (`voiceInput`
16
+ * leaves the transcript there for `voiceOutput`).
17
+ */
18
+ export const applyInputMiddleware = async (
19
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
20
+ singletonServices: CoreSingletonServices,
21
+ input: TurnInput,
22
+ sharedNotes: Record<string, unknown>
23
+ ): Promise<TurnInput> => {
24
+ let { messages, instructions } = input
25
+ for (const mw of aiMiddlewares) {
26
+ if (!mw.modifyInput) continue
27
+ const result = await mw.modifyInput(singletonServices, {
28
+ messages,
29
+ instructions,
30
+ shared: sharedNotes,
31
+ })
32
+ messages = result.messages
33
+ instructions = result.instructions
34
+ }
35
+ return { messages, instructions }
36
+ }
37
+
38
+ /** The step fields both agent paths hand to `afterStep`, taken from the hook. */
39
+ type StepResult = Omit<
40
+ Parameters<NonNullable<PikkuAIMiddlewareHooks['afterStep']>>[1],
41
+ 'stepNumber'
42
+ >
43
+
44
+ /**
45
+ * Notify every `afterStep` hook. The payload is the step verbatim plus its
46
+ * ordinal — a hook that wants the run's totals accumulates them itself.
47
+ */
48
+ export const notifyAfterStep = async (
49
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
50
+ singletonServices: CoreSingletonServices,
51
+ stepNumber: number,
52
+ stepResult: StepResult
53
+ ): Promise<void> => {
54
+ for (const mw of aiMiddlewares) {
55
+ if (!mw.afterStep) continue
56
+ await mw.afterStep(singletonServices, {
57
+ stepNumber,
58
+ text: stepResult.text,
59
+ toolCalls: stepResult.toolCalls,
60
+ toolResults: stepResult.toolResults,
61
+ usage: stepResult.usage,
62
+ finishReason: stepResult.finishReason,
63
+ })
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The step shape kept on the run: each tool call paired with its result.
69
+ *
70
+ * A result is stringified unless it already is a string, because this is what
71
+ * gets persisted and read back as history — an object there is unreadable.
72
+ */
73
+ export const toAccumulatedStep = (stepResult: StepResult) => ({
74
+ usage: stepResult.usage,
75
+ toolCalls: stepResult.toolCalls.map((tc) => {
76
+ const tr = stepResult.toolResults.find(
77
+ (r) => r.toolCallId === tc.toolCallId
78
+ )
79
+ return {
80
+ name: tc.toolName,
81
+ args: tc.args as Record<string, unknown>,
82
+ result:
83
+ typeof tr?.result === 'string'
84
+ ? tr.result
85
+ : JSON.stringify(tr?.result ?? ''),
86
+ ...(tr?.error ? { error: tr.error } : {}),
87
+ }
88
+ }),
89
+ })
90
+
91
+ type PendingApproval = {
92
+ toolName: string
93
+ args: unknown
94
+ reason?: string
95
+ }
96
+
97
+ type ToolDef = {
98
+ name: string
99
+ approvalDescriptionFn?: (args: any) => Promise<string> | string
100
+ }
101
+
102
+ /**
103
+ * Fill in each approval's human-readable reason from the tool's own
104
+ * `approvalDescriptionFn`, where it did not supply one.
105
+ *
106
+ * A description that throws is swallowed: the approval still has to be raised,
107
+ * and a gate that disappears because its label failed to render is the worst
108
+ * available outcome.
109
+ */
110
+ export const describeApprovals = async (
111
+ approvals: PendingApproval[],
112
+ tools: ToolDef[]
113
+ ): Promise<void> => {
114
+ for (const approval of approvals) {
115
+ if (approval.reason) continue
116
+ const toolDef = tools.find((t) => t.name === approval.toolName)
117
+ if (!toolDef?.approvalDescriptionFn) continue
118
+ try {
119
+ approval.reason = await toolDef.approvalDescriptionFn(approval.args)
120
+ } catch {}
121
+ }
122
+ }