@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
@@ -188,8 +188,6 @@ export type FunctionRuntimeMeta = {
188
188
  audit?: {
189
189
  durability: AuditDurability
190
190
  }
191
- /** Keeps the full `SecretService`. Set by the inspector, read by the runner. */
192
- secretBroker?: boolean
193
191
  version?: number
194
192
  approvalRequired?: boolean
195
193
  approvalDescription?: string
@@ -298,6 +296,11 @@ export interface CoreUserSession {
298
296
  * Populated by whoever builds the session — core reads them, never fetches.
299
297
  */
300
298
  scopes?: string[]
299
+ /**
300
+ * Restricts the session to functions declared `readonly`. The function runner
301
+ * throws `ReadonlySessionError` for anything else.
302
+ */
303
+ readonly?: boolean
301
304
  }
302
305
 
303
306
  /**
@@ -378,7 +381,10 @@ export type PikkuWire<
378
381
  MCPTools extends string | never = never,
379
382
  TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
380
383
  TriggerOutput = unknown,
381
- TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire,
384
+ // `any`, not `Out`: the emitted `TypedScenario<Out>` supplies the real
385
+ // context, and this default is what other generics constrain against. See
386
+ // `ScenarioContext`.
387
+ TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>,
382
388
  TypedActors extends ScenarioPersonas = ScenarioPersonas,
383
389
  > = {
384
390
  /** Always present — lazily initialised on first access for every function invocation */
@@ -386,6 +392,11 @@ export type PikkuWire<
386
392
  } & Partial<{
387
393
  wireType: PikkuWiringTypes
388
394
  wireId: string
395
+ /**
396
+ * A logger scoped to this invocation, when a host attaches one. Core never
397
+ * sets it; services that log fall back to the singleton logger.
398
+ */
399
+ logger: Logger
389
400
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
390
401
  traceId: string
391
402
  functionId: string
@@ -29,6 +29,10 @@ import type {
29
29
  CoreAIAgent,
30
30
  AIAgentMeta,
31
31
  } from '../wirings/ai-agent/ai-agent.types.js'
32
+ import type {
33
+ PikkuAIScorer,
34
+ ScorerMeta,
35
+ } from '../wirings/ai-scorer/ai-scorer.types.js'
32
36
  import type {
33
37
  CoreGateway,
34
38
  GatewaysMeta,
@@ -98,6 +102,10 @@ export interface PikkuPackageState {
98
102
  variableOverrides?: Record<string, string>
99
103
  /** Per-instance name-aliases: logical name the addon reads -> actual project credential name */
100
104
  credentialOverrides?: Record<string, string>
105
+ /** Why this instance gets the whole `SecretService` rather than one scoped to its declared secrets */
106
+ globalSecrets?: string
107
+ /** Why this instance gets the whole `CredentialService` rather than one scoped to its declared credentials */
108
+ globalCredentials?: string
101
109
  /** Set by `wireRemoteAddon`: this namespace is consumed remotely over HTTP, not bundled */
102
110
  remote?: boolean
103
111
  /** Remote host base URL (wireRemoteAddon) — string or resolver over singleton services */
@@ -119,7 +127,7 @@ export interface PikkuPackageState {
119
127
  meta: HTTPWiringsMeta
120
128
  }
121
129
  channel: {
122
- channels: Map<string, CoreChannel<any, any>>
130
+ channels: Map<string, CoreChannel<any, any, any, any, any>>
123
131
  meta: ChannelsMeta
124
132
  }
125
133
  scheduler: {
@@ -153,6 +161,10 @@ export interface PikkuPackageState {
153
161
  agent: {
154
162
  agents: Map<string, CoreAIAgent>
155
163
  agentsMeta: AIAgentMeta
164
+ scorers: Map<string, PikkuAIScorer>
165
+ scorersMeta: ScorerMeta
166
+ /** Alias -> `provider/model`, from the `models` table in pikku.config.json. */
167
+ modelAliases: Record<string, string>
156
168
  }
157
169
  gateway: {
158
170
  gateways: Map<string, CoreGateway>
@@ -204,5 +216,9 @@ export interface PikkuPackageState {
204
216
  { name: string; displayName: string; type: string; oauth2?: boolean }
205
217
  > | null
206
218
  requiredParentServices: string[] | null
219
+ /** The secret keys this package is allowed to read, emitted by the CLI from
220
+ * what the package declares and is seen to read. `null` when the package
221
+ * was never analysed, which scopes it to nothing. */
222
+ declaredSecrets: string[] | null
207
223
  }
208
224
  }
@@ -1,9 +1,6 @@
1
1
  export type {
2
- ActorFlowApprovalPolicy,
3
2
  ActorFlowVerdict,
4
3
  ConverseOptions,
5
- TargetAgentReply,
6
- TargetPendingApproval,
7
4
  TargetAgentDriver,
8
5
  } from './actor-flow.types.js'
9
6
  export {
@@ -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',
@@ -0,0 +1,186 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ applyOutputMiddleware,
5
+ finalizeAgentRun,
6
+ type FinalizedRun,
7
+ } from './ai-agent-finalize.js'
8
+ import type { AIAgentStep, PikkuAIMiddlewareHooks } from './ai-agent.types.js'
9
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
10
+
11
+ const step = (
12
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>
13
+ ): AIAgentStep => ({
14
+ usage: { inputTokens: 0, outputTokens: 0 },
15
+ toolCalls,
16
+ })
17
+
18
+ const call = (name: string, result: string) => ({
19
+ name,
20
+ args: {} as Record<string, unknown>,
21
+ result,
22
+ })
23
+
24
+ describe('applyOutputMiddleware', () => {
25
+ test('hands the middleware every tool call the run made, flattened across steps', async () => {
26
+ const seen: string[][] = []
27
+ const middleware: PikkuAIMiddlewareHooks[] = [
28
+ {
29
+ modifyOutput: (_services, ctx) => {
30
+ seen.push(ctx.toolCalls.map((c) => c.name))
31
+ return { text: ctx.text, messages: ctx.messages }
32
+ },
33
+ },
34
+ ]
35
+
36
+ await applyOutputMiddleware(
37
+ middleware,
38
+ {},
39
+ {
40
+ text: 'done',
41
+ messages: [],
42
+ steps: [step([call('search', 'a')]), step([call('fetch', 'b')])],
43
+ usage: { inputTokens: 1, outputTokens: 2 },
44
+ }
45
+ )
46
+
47
+ assert.deepEqual(seen, [['search', 'fetch']])
48
+ })
49
+
50
+ test('a rewritten tool call is redistributed back onto the step it came from', async () => {
51
+ const middleware: PikkuAIMiddlewareHooks[] = [
52
+ {
53
+ modifyOutput: (_services, ctx) => ({
54
+ text: ctx.text,
55
+ messages: ctx.messages,
56
+ toolCalls: ctx.toolCalls.map((c) => ({ ...c, result: '[redacted]' })),
57
+ }),
58
+ },
59
+ ]
60
+
61
+ const { steps } = await applyOutputMiddleware(
62
+ middleware,
63
+ {},
64
+ {
65
+ text: 'done',
66
+ messages: [],
67
+ steps: [
68
+ step([call('search', 'secret')]),
69
+ step([call('fetch', 'token')]),
70
+ ],
71
+ usage: { inputTokens: 0, outputTokens: 0 },
72
+ }
73
+ )
74
+
75
+ assert.deepEqual(
76
+ steps.map((s) => s.toolCalls?.map((c) => [c.name, c.result])),
77
+ [[['search', '[redacted]']], [['fetch', '[redacted]']]]
78
+ )
79
+ })
80
+
81
+ test('dropping a tool call collapses the calls into the last step rather than mis-attributing them', async () => {
82
+ const middleware: PikkuAIMiddlewareHooks[] = [
83
+ {
84
+ modifyOutput: (_services, ctx) => ({
85
+ text: ctx.text,
86
+ messages: ctx.messages,
87
+ toolCalls: ctx.toolCalls.filter((c) => c.name !== 'search'),
88
+ }),
89
+ },
90
+ ]
91
+
92
+ const { steps } = await applyOutputMiddleware(
93
+ middleware,
94
+ {},
95
+ {
96
+ text: 'done',
97
+ messages: [],
98
+ steps: [step([call('search', 'a')]), step([call('fetch', 'b')])],
99
+ usage: { inputTokens: 0, outputTokens: 0 },
100
+ }
101
+ )
102
+
103
+ assert.deepEqual(
104
+ steps.map((s) => s.toolCalls?.map((c) => c.name)),
105
+ [[], ['fetch']]
106
+ )
107
+ })
108
+
109
+ test('runs the chain in reverse registration order, each hook seeing the previous rewrite', async () => {
110
+ const order: string[] = []
111
+ const middleware: PikkuAIMiddlewareHooks[] = [
112
+ {
113
+ modifyOutput: (_services, ctx) => {
114
+ order.push(`first saw ${ctx.text}`)
115
+ return { text: `${ctx.text}+first`, messages: ctx.messages }
116
+ },
117
+ },
118
+ {
119
+ modifyOutput: (_services, ctx) => {
120
+ order.push(`second saw ${ctx.text}`)
121
+ return { text: `${ctx.text}+second`, messages: ctx.messages }
122
+ },
123
+ },
124
+ ]
125
+
126
+ const { text } = await applyOutputMiddleware(
127
+ middleware,
128
+ {},
129
+ {
130
+ text: 'raw',
131
+ messages: [],
132
+ steps: [],
133
+ usage: { inputTokens: 0, outputTokens: 0 },
134
+ }
135
+ )
136
+
137
+ assert.deepEqual(order, ['second saw raw', 'first saw raw+second'])
138
+ assert.equal(text, 'raw+second+first')
139
+ })
140
+ })
141
+
142
+ describe('finalizeAgentRun', () => {
143
+ const run = (usage: FinalizedRun['usage']): FinalizedRun => ({
144
+ runId: 'run-1',
145
+ agentName: 'assistant',
146
+ threadId: 'thread-1',
147
+ input: 'what is the capital of France?',
148
+ text: 'done',
149
+ steps: [],
150
+ usage,
151
+ })
152
+
153
+ test('completes the run and records its usage', async () => {
154
+ const updates: unknown[] = []
155
+ const aiRunState = {
156
+ updateRun: async (_runId: string, update: unknown) => {
157
+ updates.push(update)
158
+ },
159
+ } as unknown as AIRunStateService
160
+
161
+ await finalizeAgentRun(
162
+ aiRunState,
163
+ run({ inputTokens: 10, outputTokens: 20, model: 'gpt-4o' })
164
+ )
165
+
166
+ assert.deepEqual(updates, [
167
+ {
168
+ status: 'completed',
169
+ usage: { inputTokens: 10, outputTokens: 20, model: 'gpt-4o' },
170
+ },
171
+ ])
172
+ })
173
+
174
+ test('omits usage entirely when no model reported one', async () => {
175
+ const updates: any[] = []
176
+ const aiRunState = {
177
+ updateRun: async (_runId: string, update: unknown) => {
178
+ updates.push(update)
179
+ },
180
+ } as unknown as AIRunStateService
181
+
182
+ await finalizeAgentRun(aiRunState, run({ inputTokens: 0, outputTokens: 0 }))
183
+
184
+ assert.deepEqual(updates, [{ status: 'completed' }])
185
+ })
186
+ })
@@ -0,0 +1,197 @@
1
+ import type {
2
+ AIAgentStep,
3
+ AIMessage,
4
+ PikkuAIMiddlewareHooks,
5
+ } from './ai-agent.types.js'
6
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
7
+ import { pikkuState } from '../../pikku-state.js'
8
+ import { scoreFinishedRun } from '../ai-scorer/ai-scorer-live.js'
9
+ import { recordScoreSnapshot } from '../ai-scorer/ai-scorer-snapshots.js'
10
+
11
+ export type RunUsage = {
12
+ inputTokens: number
13
+ outputTokens: number
14
+ model?: string
15
+ }
16
+
17
+ /**
18
+ * Everything a finished run produced, after all middleware has had its say.
19
+ *
20
+ * One snapshot feeds persistence and anything that grades the run, so that what
21
+ * a scorer is shown is exactly what was stored — including the redactions.
22
+ */
23
+ export type FinalizedRun = {
24
+ runId: string
25
+ agentName: string
26
+ threadId: string
27
+ resourceId?: string
28
+ /** The prompt the run answered — what a scorer grades the answer against. */
29
+ input: string
30
+ text: string
31
+ steps: AIAgentStep[]
32
+ usage: RunUsage
33
+ }
34
+
35
+ /**
36
+ * The prompt a run answered: the most recent user turn, which is what the model
37
+ * was last asked. On a resumed run the earlier turns are context, not the ask.
38
+ */
39
+ export const lastUserMessageText = (messages: AIMessage[]): string => {
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const message = messages[i]
42
+ if (message?.role !== 'user') continue
43
+ if (typeof message.content === 'string') return message.content
44
+ if (Array.isArray(message.content)) {
45
+ return message.content
46
+ .filter((part) => part.type === 'text')
47
+ .map((part) => (part as { text: string }).text)
48
+ .join('\n')
49
+ }
50
+ }
51
+ return ''
52
+ }
53
+
54
+ const flattenToolCalls = (
55
+ steps: AIAgentStep[]
56
+ ): NonNullable<AIAgentStep['toolCalls']> =>
57
+ steps.flatMap((step) => step.toolCalls ?? [])
58
+
59
+ /**
60
+ * Push a rewritten flat tool-call list back onto the steps it came from,
61
+ * preserving the step boundaries. A hook that adds or drops calls would make
62
+ * the boundaries meaningless, so a length change collapses them into the last
63
+ * step rather than silently mis-attributing calls to the wrong step.
64
+ */
65
+ const distributeToolCalls = (
66
+ steps: AIAgentStep[],
67
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>
68
+ ): AIAgentStep[] => {
69
+ if (toolCalls.length !== flattenToolCalls(steps).length) {
70
+ return steps.map((step, index) => ({
71
+ ...step,
72
+ toolCalls: index === steps.length - 1 ? toolCalls : [],
73
+ }))
74
+ }
75
+
76
+ let cursor = 0
77
+ return steps.map((step) => {
78
+ if (!step.toolCalls) return step
79
+ const next = toolCalls.slice(cursor, cursor + step.toolCalls.length)
80
+ cursor += step.toolCalls.length
81
+ return { ...step, toolCalls: next }
82
+ })
83
+ }
84
+
85
+ /**
86
+ * Run the `modifyOutput` chain over a finished non-streaming run.
87
+ *
88
+ * Reverse order, matching the input chain: the middleware registered first
89
+ * wraps the others, so it sees the output last.
90
+ */
91
+ export const applyOutputMiddleware = async (
92
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
93
+ singletonServices: any,
94
+ input: {
95
+ text: string
96
+ messages: AIMessage[]
97
+ steps: AIAgentStep[]
98
+ usage: { inputTokens: number; outputTokens: number }
99
+ }
100
+ ): Promise<{ text: string; messages: AIMessage[]; steps: AIAgentStep[] }> => {
101
+ let text = input.text
102
+ let messages = input.messages
103
+ let steps = input.steps
104
+ let toolCalls = flattenToolCalls(steps)
105
+
106
+ for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
107
+ const mw = aiMiddlewares[i]
108
+ if (!mw.modifyOutput) continue
109
+ const result = await mw.modifyOutput(singletonServices, {
110
+ text,
111
+ messages,
112
+ usage: {
113
+ inputTokens: input.usage.inputTokens,
114
+ outputTokens: input.usage.outputTokens,
115
+ },
116
+ toolCalls,
117
+ })
118
+ text = result.text
119
+ messages = result.messages
120
+ if (result.toolCalls) {
121
+ toolCalls = result.toolCalls
122
+ steps = distributeToolCalls(steps, toolCalls)
123
+ }
124
+ }
125
+
126
+ return { text, messages, steps }
127
+ }
128
+
129
+ /**
130
+ * The one place a run ends successfully.
131
+ *
132
+ * Every path that completes a run — streamed, non-streamed, and resumed after a
133
+ * tool approval — goes through here, so that anything terminal is reachable
134
+ * from all of them and cannot be reordered by a middleware author. Nothing here
135
+ * may rewrite the run: by this point the output middleware has resolved and, on
136
+ * the streaming path, the client already has the reply.
137
+ */
138
+ export const finalizeAgentRun = async (
139
+ aiRunState: AIRunStateService,
140
+ run: FinalizedRun
141
+ ): Promise<void> => {
142
+ await aiRunState.updateRun(run.runId, {
143
+ status: 'completed',
144
+ ...(run.usage.model
145
+ ? {
146
+ usage: {
147
+ inputTokens: run.usage.inputTokens,
148
+ outputTokens: run.usage.outputTokens,
149
+ model: run.usage.model,
150
+ },
151
+ }
152
+ : {}),
153
+ })
154
+
155
+ // Read rather than `getSingletonServices()`: a process that never registered
156
+ // them grades nothing, which is not an error at the point a run has already
157
+ // succeeded.
158
+ const services = pikkuState(null, 'package', 'singletonServices')
159
+ if (!services) return
160
+
161
+ const snapshot = {
162
+ runId: run.runId,
163
+ agentName: run.agentName,
164
+ threadId: run.threadId,
165
+ ...(run.resourceId !== undefined ? { resourceId: run.resourceId } : {}),
166
+ input: run.input,
167
+ output: run.text,
168
+ toolCalls: run.steps.flatMap((step) =>
169
+ (step.toolCalls ?? []).map((call) => ({
170
+ name: call.name,
171
+ args: call.args,
172
+ result: call.result,
173
+ ...(call.error !== undefined ? { error: call.error } : {}),
174
+ }))
175
+ ),
176
+ usage: {
177
+ inputTokens: run.usage.inputTokens,
178
+ outputTokens: run.usage.outputTokens,
179
+ ...(run.usage.model !== undefined ? { model: run.usage.model } : {}),
180
+ },
181
+ }
182
+
183
+ // The same object a scenario grades, so an asserted score and a sampled one
184
+ // are the same measurement. A no-op unless a dev server turned retention on.
185
+ recordScoreSnapshot(snapshot)
186
+
187
+ // Best-effort and last: the client already has its answer, so a grading
188
+ // failure must not surface as a failed run.
189
+ try {
190
+ await scoreFinishedRun(snapshot, services)
191
+ } catch (error) {
192
+ services.logger?.error(
193
+ `[pikku] Live scoring failed for run ${run.runId}`,
194
+ error
195
+ )
196
+ }
197
+ }
@@ -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)
@@ -381,6 +381,7 @@ export const persistOrphanedToolResults = async (
381
381
  : typeof entry.result === 'string'
382
382
  ? entry.result
383
383
  : JSON.stringify(entry.result),
384
+ ...(entry.error ? { error: entry.error } : {}),
384
385
  })),
385
386
  undelivered: true,
386
387
  createdAt: new Date(),