@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
@@ -0,0 +1,174 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
4
+ import { scoreFinishedRun } from './ai-scorer-live.js'
5
+ import { pikkuAIJudge, pikkuAIScorer } from './ai-scorer.js'
6
+ import type { PikkuAIScorer, ScorerInput } from './ai-scorer.types.js'
7
+
8
+ const register = (agentName: string, scorers: PikkuAIScorer[]) => {
9
+ pikkuState(null, 'agent', 'agents').set(agentName, {
10
+ name: agentName,
11
+ scorers: scorers.map((scorer) => scorer.name),
12
+ } as never)
13
+ for (const scorer of scorers) {
14
+ pikkuState(null, 'agent', 'scorers').set(scorer.name, scorer)
15
+ }
16
+ }
17
+
18
+ const run = (overrides: Partial<ScorerInput> = {}): ScorerInput => ({
19
+ runId: 'run-1',
20
+ agentName: 'assistant',
21
+ threadId: 'thread-1',
22
+ input: 'what is the capital of France?',
23
+ output: 'Paris',
24
+ toolCalls: [],
25
+ usage: { inputTokens: 10, outputTokens: 5 },
26
+ ...overrides,
27
+ })
28
+
29
+ const collectingServices = () => {
30
+ const added: { queueName: string; data: any }[] = []
31
+ const warnings: string[] = []
32
+ const errors: string[] = []
33
+ return {
34
+ added,
35
+ warnings,
36
+ errors,
37
+ services: {
38
+ logger: {
39
+ warn: (message: string) => warnings.push(message),
40
+ error: (message: string) => errors.push(message),
41
+ },
42
+ queueService: {
43
+ add: async (queueName: string, data: unknown) => {
44
+ added.push({ queueName, data })
45
+ return undefined
46
+ },
47
+ },
48
+ } as any,
49
+ }
50
+ }
51
+
52
+ const alwaysOne = pikkuAIScorer({
53
+ name: 'brevity',
54
+ description: 'Shorter is better',
55
+ score: () => ({ score: 1 }),
56
+ })
57
+
58
+ describe('scoreFinishedRun', () => {
59
+ beforeEach(() => {
60
+ resetPikkuState()
61
+ })
62
+
63
+ test('sends one message per scorer, each to its own lane', async () => {
64
+ const judge = pikkuAIJudge({
65
+ name: 'helpfulness',
66
+ description: 'Is the answer useful',
67
+ model: 'claude-opus-5',
68
+ goal: 'Grade helpfulness.',
69
+ })
70
+ register('assistant', [alwaysOne, judge])
71
+ const { added, services } = collectingServices()
72
+
73
+ await scoreFinishedRun(run(), services)
74
+
75
+ assert.deepEqual(
76
+ added.map((entry) => [entry.queueName, entry.data.scorerName]),
77
+ [
78
+ ['ai-score-fast', 'brevity'],
79
+ ['ai-score-slow', 'helpfulness'],
80
+ ]
81
+ )
82
+ })
83
+
84
+ test('never grades a reference-based judge, because live traffic has no answer key', async () => {
85
+ const correctness = pikkuAIJudge({
86
+ name: 'correctness',
87
+ description: 'Is the answer right',
88
+ model: 'claude-opus-5',
89
+ goal: 'Grade correctness.',
90
+ requiresReference: true,
91
+ })
92
+ register('assistant', [correctness])
93
+ const { added, services } = collectingServices()
94
+
95
+ await scoreFinishedRun(run(), services)
96
+
97
+ assert.deepEqual(added, [])
98
+ })
99
+
100
+ test('honours the sample rate rather than grading every run', async () => {
101
+ const sampled = pikkuAIScorer({
102
+ name: 'brevity',
103
+ description: 'Shorter is better',
104
+ sampleRate: 0,
105
+ score: () => ({ score: 1 }),
106
+ })
107
+ register('assistant', [sampled])
108
+ const { added, services } = collectingServices()
109
+
110
+ await scoreFinishedRun(run(), services)
111
+
112
+ assert.deepEqual(added, [])
113
+ })
114
+
115
+ test('warns about a scorer the agent named but nothing registered, rather than failing the run', async () => {
116
+ pikkuState(null, 'agent', 'agents').set('assistant', {
117
+ name: 'assistant',
118
+ scorers: ['nonexistent'],
119
+ } as never)
120
+ const { added, warnings, services } = collectingServices()
121
+
122
+ await scoreFinishedRun(run(), services)
123
+
124
+ assert.deepEqual(added, [])
125
+ assert.equal(warnings.length, 1)
126
+ assert.match(warnings[0]!, /nonexistent/)
127
+ })
128
+
129
+ test('says why nothing was graded when there is no queue to grade on', async () => {
130
+ register('assistant', [alwaysOne])
131
+ const { warnings, services } = collectingServices()
132
+ delete services.queueService
133
+
134
+ await scoreFinishedRun(run(), services)
135
+
136
+ assert.equal(warnings.length, 1)
137
+ assert.match(warnings[0]!, /no queue service is registered/)
138
+ })
139
+
140
+ test('one scorer that cannot be enqueued does not stop the others', async () => {
141
+ const judge = pikkuAIJudge({
142
+ name: 'helpfulness',
143
+ description: 'Is the answer useful',
144
+ model: 'claude-opus-5',
145
+ goal: 'Grade helpfulness.',
146
+ })
147
+ register('assistant', [alwaysOne, judge])
148
+ const { added, errors, services } = collectingServices()
149
+ const add = services.queueService.add
150
+ services.queueService.add = async (queueName: string, data: any) => {
151
+ if (data.scorerName === 'brevity') throw new Error('queue is down')
152
+ return add(queueName, data)
153
+ }
154
+
155
+ await scoreFinishedRun(run(), services)
156
+
157
+ assert.deepEqual(
158
+ added.map((entry) => entry.data.scorerName),
159
+ ['helpfulness']
160
+ )
161
+ assert.equal(errors.length, 1)
162
+ })
163
+
164
+ test('an agent that names no scorers touches the queue at all', async () => {
165
+ pikkuState(null, 'agent', 'agents').set('assistant', {
166
+ name: 'assistant',
167
+ } as never)
168
+ const { added, services } = collectingServices()
169
+
170
+ await scoreFinishedRun(run(), services)
171
+
172
+ assert.deepEqual(added, [])
173
+ })
174
+ })
@@ -0,0 +1,56 @@
1
+ import type { CoreSingletonServices } from '../../types/core.types.js'
2
+ import { scorersForAgent } from './ai-scorer-registry.js'
3
+ import { isSampled } from './ai-scorer-sampling.js'
4
+ import { SCORER_LANE_QUEUES, type ScorerInput } from './ai-scorer.types.js'
5
+
6
+ /**
7
+ * Grade one finished run on live traffic.
8
+ *
9
+ * Called as the terminal step of `finalizeAgentRun`, so it is structurally last
10
+ * — a developer cannot register anything after it, and cannot reorder it. It is
11
+ * also strictly best-effort: the client already has its answer, so nothing here
12
+ * may fail the run.
13
+ */
14
+ export const scoreFinishedRun = async (
15
+ run: ScorerInput,
16
+ services: Pick<CoreSingletonServices, 'logger'> & {
17
+ queueService?: { add: (queueName: string, data: unknown) => Promise<any> }
18
+ }
19
+ ): Promise<void> => {
20
+ const scorers = scorersForAgent(run.agentName, services.logger).filter(
21
+ // A reference-based judge grades against an answer key, and live traffic
22
+ // has none.
23
+ (scorer) => !scorer.requiresReference
24
+ )
25
+ if (scorers.length === 0) return
26
+
27
+ if (!services.queueService) {
28
+ services.logger?.warn(
29
+ `[pikku] Agent '${run.agentName}' declares scorers but no queue service is registered — skipping live scoring`
30
+ )
31
+ return
32
+ }
33
+
34
+ const sampled = scorers.filter((scorer) =>
35
+ isSampled(run.runId, scorer.name, scorer.sampleRate)
36
+ )
37
+
38
+ // One message per scorer, so each gets its own retry, isolation and lane.
39
+ await Promise.all(
40
+ sampled.map(async (scorer) => {
41
+ try {
42
+ await services.queueService!.add(SCORER_LANE_QUEUES[scorer.lane], {
43
+ ...run,
44
+ scorerName: scorer.name,
45
+ // No reference: a live run has no answer key.
46
+ reference: undefined,
47
+ })
48
+ } catch (error) {
49
+ services.logger?.error(
50
+ `[pikku] Failed to enqueue the '${scorer.name}' grade of run ${run.runId}`,
51
+ { error }
52
+ )
53
+ }
54
+ })
55
+ )
56
+ }
@@ -0,0 +1,63 @@
1
+ import { pikkuState } from '../../pikku-state.js'
2
+ import type { PikkuAIScorer } from './ai-scorer.types.js'
3
+
4
+ export const addAIScorer = (
5
+ scorerName: string,
6
+ scorer: PikkuAIScorer<any>,
7
+ packageName: string | null = null
8
+ ) => {
9
+ const scorersMeta = pikkuState(packageName, 'agent', 'scorersMeta')
10
+ if (!scorersMeta[scorerName]) {
11
+ console.warn(
12
+ `[pikku] Skipping AI scorer '${scorerName}' — metadata not found. Scorers must be declared in a *.scorer.ts file.`
13
+ )
14
+ return
15
+ }
16
+ const scorers = pikkuState(packageName, 'agent', 'scorers')
17
+ if (scorers.has(scorerName)) {
18
+ throw new Error(`AI scorer already exists: ${scorerName}`)
19
+ }
20
+ scorers.set(scorerName, scorer)
21
+ }
22
+
23
+ export const getAIScorers = () => pikkuState(null, 'agent', 'scorers')
24
+
25
+ export const getAIScorersMeta = () => pikkuState(null, 'agent', 'scorersMeta')
26
+
27
+ /**
28
+ * Resolve a scorer by name across every registered package, the way an agent's
29
+ * tools are resolved: a scorer declared in an addon is nameable by an app agent.
30
+ */
31
+ export const resolveAIScorer = (scorerName: string): PikkuAIScorer<any> => {
32
+ const scorer = pikkuState(null, 'agent', 'scorers').get(scorerName)
33
+ if (!scorer) {
34
+ throw new Error(`AI scorer not found: ${scorerName}`)
35
+ }
36
+ return scorer
37
+ }
38
+
39
+ /**
40
+ * The scorers an agent asked to be graded by.
41
+ *
42
+ * A name that resolves to nothing is warned about rather than thrown: a missing
43
+ * scorer must not take down a run that has already answered the user.
44
+ */
45
+ export const scorersForAgent = (
46
+ agentName: string,
47
+ logger?: { warn: (message: string) => void }
48
+ ): PikkuAIScorer<any>[] => {
49
+ const agent = pikkuState(null, 'agent', 'agents').get(agentName)
50
+ const names = agent?.scorers ?? []
51
+ const scorers: PikkuAIScorer<any>[] = []
52
+ for (const name of names) {
53
+ const scorer = pikkuState(null, 'agent', 'scorers').get(name)
54
+ if (!scorer) {
55
+ logger?.warn(
56
+ `[pikku] Agent '${agentName}' names scorer '${name}', which is not registered — skipping it`
57
+ )
58
+ continue
59
+ }
60
+ scorers.push(scorer)
61
+ }
62
+ return scorers
63
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isSampled } from './ai-scorer-sampling.js'
4
+
5
+ describe('isSampled', () => {
6
+ test('grades every run at a rate of 1 and none at 0', () => {
7
+ assert.equal(isSampled('run-1', 'relevance', 1), true)
8
+ assert.equal(isSampled('run-1', 'relevance', 0), false)
9
+ })
10
+
11
+ test('lands the same way every time it is asked, so a retried job is not regraded by chance', () => {
12
+ const first = isSampled('run-42', 'relevance', 0.5)
13
+ for (let i = 0; i < 20; i++) {
14
+ assert.equal(isSampled('run-42', 'relevance', 0.5), first)
15
+ }
16
+ })
17
+
18
+ test('two scorers at the same rate do not sample the same set of runs', () => {
19
+ const runs = Array.from({ length: 200 }, (_, i) => `run-${i}`)
20
+ const a = runs.filter((run) => isSampled(run, 'relevance', 0.5))
21
+ const b = runs.filter((run) => isSampled(run, 'toxicity', 0.5))
22
+ assert.notDeepEqual(a, b)
23
+ })
24
+
25
+ test('samples roughly the requested fraction over many runs', () => {
26
+ const runs = Array.from({ length: 2000 }, (_, i) => `run-${i}`)
27
+ const sampled = runs.filter((run) => isSampled(run, 'relevance', 0.25))
28
+ const rate = sampled.length / runs.length
29
+ assert.ok(
30
+ rate > 0.2 && rate < 0.3,
31
+ `expected roughly 0.25 of runs to be sampled, got ${rate}`
32
+ )
33
+ })
34
+ })
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Decide whether one run is in a scorer's sample.
3
+ *
4
+ * Deterministic on `(runId, scorerName)` rather than random, so re-running the
5
+ * decision — a retried job, a replayed run, a test — always lands the same way,
6
+ * and so two scorers at the same rate do not sample the same runs.
7
+ */
8
+ export const isSampled = (
9
+ runId: string,
10
+ scorerName: string,
11
+ sampleRate: number
12
+ ): boolean => {
13
+ if (sampleRate >= 1) return true
14
+ if (sampleRate <= 0) return false
15
+
16
+ const key = `${scorerName}:${runId}`
17
+ // FNV-1a: no crypto dependency, and stable across processes and platforms,
18
+ // which a language-level string hash is not guaranteed to be.
19
+ let hash = 0x811c9dc5
20
+ for (let i = 0; i < key.length; i++) {
21
+ hash ^= key.charCodeAt(i)
22
+ hash = Math.imul(hash, 0x01000193) >>> 0
23
+ }
24
+
25
+ // FNV-1a alone leaves its high bits barely moved between keys that differ
26
+ // only in their last characters — sequential run ids being exactly that —
27
+ // which biases the fraction actually sampled well away from the rate asked
28
+ // for. MurmurHash3's finalizer spreads the low bits back over the whole word.
29
+ hash ^= hash >>> 16
30
+ hash = Math.imul(hash, 0x85ebca6b) >>> 0
31
+ hash ^= hash >>> 13
32
+ hash = Math.imul(hash, 0xc2b2ae35) >>> 0
33
+ hash ^= hash >>> 16
34
+
35
+ return (hash >>> 0) / 0x100000000 < sampleRate
36
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, test, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ enableScoreSnapshots,
6
+ getScoreSnapshot,
7
+ recordScoreSnapshot,
8
+ resetScoreSnapshots,
9
+ } from './ai-scorer-snapshots.js'
10
+ import type { ScorerInput } from './ai-scorer.types.js'
11
+
12
+ const run = (runId: string): ScorerInput => ({
13
+ runId,
14
+ agentName: 'assistant',
15
+ input: 'Where is Paris?',
16
+ output: 'France.',
17
+ toolCalls: [],
18
+ usage: { inputTokens: 5, outputTokens: 2 },
19
+ })
20
+
21
+ describe('score snapshots', () => {
22
+ beforeEach(() => resetScoreSnapshots())
23
+
24
+ test('a process that was never asked to retain runs holds none', () => {
25
+ recordScoreSnapshot(run('run-1'))
26
+
27
+ assert.equal(getScoreSnapshot('run-1'), undefined)
28
+ })
29
+
30
+ test('a retained run is handed back whole, so a grade sees what live scoring saw', () => {
31
+ enableScoreSnapshots()
32
+
33
+ recordScoreSnapshot(run('run-1'))
34
+
35
+ assert.deepEqual(getScoreSnapshot('run-1'), run('run-1'))
36
+ })
37
+
38
+ test('the buffer is bounded, so a long-lived dev server cannot accumulate run content', () => {
39
+ enableScoreSnapshots(2)
40
+
41
+ recordScoreSnapshot(run('run-1'))
42
+ recordScoreSnapshot(run('run-2'))
43
+ recordScoreSnapshot(run('run-3'))
44
+
45
+ assert.equal(getScoreSnapshot('run-1'), undefined)
46
+ assert.equal(getScoreSnapshot('run-2')?.runId, 'run-2')
47
+ assert.equal(getScoreSnapshot('run-3')?.runId, 'run-3')
48
+ })
49
+ })
@@ -0,0 +1,46 @@
1
+ import type { ScorerInput } from './ai-scorer.types.js'
2
+
3
+ /**
4
+ * The last few finished runs, held so a scenario can grade one.
5
+ *
6
+ * A scenario cannot rebuild a `ScorerInput` from storage: the run record holds
7
+ * status and usage, and the prompt, answer and tool calls are spread across the
8
+ * thread's messages, where the boundary of a single run is not recoverable. So
9
+ * the runtime keeps the snapshot it already took at finalize — which is also
10
+ * what makes a scenario's grade comparable to a live one, since both grade the
11
+ * identical object, redactions included.
12
+ *
13
+ * Off unless something turns it on, and bounded when it is. A process that
14
+ * grades no scenarios holds nothing, and one that does cannot accumulate run
15
+ * content without limit.
16
+ */
17
+ const snapshots = new Map<string, ScorerInput>()
18
+
19
+ let limit = 0
20
+
21
+ /**
22
+ * Start retaining snapshots. Called where the scenario grading RPC is
23
+ * registered, so the buffer exists in exactly the processes that can read it —
24
+ * a development server — and never in a deployed bundle.
25
+ */
26
+ export const enableScoreSnapshots = (maxRuns = 50): void => {
27
+ limit = maxRuns
28
+ }
29
+
30
+ export const recordScoreSnapshot = (run: ScorerInput): void => {
31
+ if (limit === 0) return
32
+ snapshots.set(run.runId, run)
33
+ while (snapshots.size > limit) {
34
+ const oldest = snapshots.keys().next().value
35
+ if (oldest === undefined) break
36
+ snapshots.delete(oldest)
37
+ }
38
+ }
39
+
40
+ export const getScoreSnapshot = (runId: string): ScorerInput | undefined =>
41
+ snapshots.get(runId)
42
+
43
+ export const resetScoreSnapshots = (): void => {
44
+ snapshots.clear()
45
+ limit = 0
46
+ }
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
4
+ import { pikkuAIScoreWorkerFunc } from './ai-scorer-worker.js'
5
+ import { pikkuAIJudge, pikkuAIScorer } from './ai-scorer.js'
6
+ import type { PikkuAIScorer, ScoreJob } from './ai-scorer.types.js'
7
+
8
+ const job = (overrides: Partial<ScoreJob> = {}): ScoreJob => ({
9
+ scorerName: 'brevity',
10
+ runId: 'run-1',
11
+ agentName: 'assistant',
12
+ input: 'what is the capital of France?',
13
+ output: 'Paris',
14
+ toolCalls: [],
15
+ usage: { inputTokens: 10, outputTokens: 5 },
16
+ ...overrides,
17
+ })
18
+
19
+ const install = (scorer: PikkuAIScorer, aiAgentRunner?: unknown) => {
20
+ pikkuState(null, 'agent', 'scorers').set(scorer.name, scorer)
21
+ const saved: unknown[] = []
22
+ pikkuState(null, 'package', 'singletonServices', {
23
+ aiRunState: {
24
+ saveScore: async (score: unknown) => {
25
+ saved.push(score)
26
+ },
27
+ },
28
+ aiAgentRunner,
29
+ } as never)
30
+ return saved
31
+ }
32
+
33
+ describe('pikkuAIScoreWorkerFunc', () => {
34
+ beforeEach(() => {
35
+ resetPikkuState()
36
+ })
37
+
38
+ test('records what a heuristic scorer graded, against the run it graded', async () => {
39
+ const saved = install(
40
+ pikkuAIScorer({
41
+ name: 'brevity',
42
+ description: 'Shorter is better',
43
+ score: (input) => ({
44
+ score: input.output.length < 20 ? 1 : 0,
45
+ reason: 'Answered in one word.',
46
+ }),
47
+ })
48
+ )
49
+
50
+ await pikkuAIScoreWorkerFunc({}, job())
51
+
52
+ assert.deepEqual(saved, [
53
+ {
54
+ runId: 'run-1',
55
+ scorerName: 'brevity',
56
+ score: 1,
57
+ reason: 'Answered in one word.',
58
+ },
59
+ ])
60
+ })
61
+
62
+ test('hands a judge to the model rather than calling a score function it has not got', async () => {
63
+ const saved = install(
64
+ pikkuAIJudge({
65
+ name: 'helpfulness',
66
+ description: 'Is the answer useful',
67
+ model: 'claude-opus-5',
68
+ goal: 'Grade helpfulness.',
69
+ }),
70
+ {
71
+ run: async () => ({
72
+ object: { score: 0.5, reason: 'Terse.' },
73
+ usage: { inputTokens: 80, outputTokens: 20 },
74
+ }),
75
+ }
76
+ )
77
+
78
+ await pikkuAIScoreWorkerFunc({}, job({ scorerName: 'helpfulness' }))
79
+
80
+ assert.deepEqual(saved, [
81
+ {
82
+ runId: 'run-1',
83
+ scorerName: 'helpfulness',
84
+ score: 0.5,
85
+ reason: 'Terse.',
86
+ metadata: { judgeModel: 'claude-opus-5', judgeTokens: 100 },
87
+ },
88
+ ])
89
+ })
90
+
91
+ test('fails the job when there is nowhere to record the grade, so it is retried rather than lost', async () => {
92
+ pikkuState(null, 'agent', 'scorers').set(
93
+ 'brevity',
94
+ pikkuAIScorer({
95
+ name: 'brevity',
96
+ description: 'Shorter is better',
97
+ score: () => ({ score: 1 }),
98
+ })
99
+ )
100
+ pikkuState(null, 'package', 'singletonServices', {} as never)
101
+
102
+ await assert.rejects(
103
+ () => pikkuAIScoreWorkerFunc({}, job()),
104
+ /AI run state service not initialized/
105
+ )
106
+ })
107
+
108
+ test('fails on a scorer name that resolves to nothing', async () => {
109
+ install(
110
+ pikkuAIScorer({
111
+ name: 'brevity',
112
+ description: 'Shorter is better',
113
+ score: () => ({ score: 1 }),
114
+ })
115
+ )
116
+
117
+ await assert.rejects(
118
+ () => pikkuAIScoreWorkerFunc({}, job({ scorerName: 'gone' })),
119
+ /AI scorer not found: gone/
120
+ )
121
+ })
122
+ })
@@ -0,0 +1,69 @@
1
+ import { pikkuState } from '../../pikku-state.js'
2
+ import { addFunction } from '../../function/function-runner.js'
3
+ import { wireQueueWorker } from '../queue/queue-runner.js'
4
+ import { gradeRun } from './ai-scorer-grade.js'
5
+ import { SCORER_LANE_QUEUES, type ScoreJob } from './ai-scorer.types.js'
6
+
7
+ /**
8
+ * The one worker behind both lanes: it resolves the scorer by name and grades.
9
+ * The lane a job arrived on decides nothing except how long it may take.
10
+ */
11
+ export async function pikkuAIScoreWorkerFunc(
12
+ _services: Record<string, unknown>,
13
+ job: ScoreJob
14
+ ): Promise<void> {
15
+ const services = pikkuState(null, 'package', 'singletonServices')
16
+ if (!services) {
17
+ throw new Error(
18
+ `Singleton services not initialized: cannot run the '${job.scorerName}' grade of run ${job.runId}`
19
+ )
20
+ }
21
+
22
+ await gradeRun(job, services, { persist: true })
23
+ }
24
+
25
+ const SCORE_WORKER_FUNC_ID = 'pikkuAIScoreWorker'
26
+
27
+ /**
28
+ * Bind the two lane queues to the shared worker.
29
+ *
30
+ * Registered programmatically rather than emitted into the user's bootstrap:
31
+ * `wireQueueWorker` warns and returns for a name codegen produced no metadata
32
+ * for, so the metadata is synthesised here alongside the registration. This
33
+ * mirrors how workflows wire their own queues.
34
+ */
35
+ export const wireAIScorerQueueWorkers = (): void => {
36
+ // No scorers means no lanes: a deployment that grades nothing should not be
37
+ // left holding two queues nothing ever writes to.
38
+ if (pikkuState(null, 'agent', 'scorers').size === 0) return
39
+
40
+ const functions = pikkuState(null, 'function', 'functions')
41
+ const functionsMeta = pikkuState(null, 'function', 'meta')
42
+ const queueMeta = pikkuState(null, 'queue', 'meta')
43
+
44
+ if (!functions.has(SCORE_WORKER_FUNC_ID)) {
45
+ addFunction(SCORE_WORKER_FUNC_ID, { func: pikkuAIScoreWorkerFunc } as never)
46
+ }
47
+ if (!functionsMeta[SCORE_WORKER_FUNC_ID]) {
48
+ functionsMeta[SCORE_WORKER_FUNC_ID] = {
49
+ pikkuFuncId: SCORE_WORKER_FUNC_ID,
50
+ sessionless: true,
51
+ functionType: 'helper',
52
+ inputSchemaName: null,
53
+ outputSchemaName: null,
54
+ }
55
+ }
56
+
57
+ for (const queueName of Object.values(SCORER_LANE_QUEUES)) {
58
+ if (!queueMeta[queueName]) {
59
+ queueMeta[queueName] = {
60
+ pikkuFuncId: SCORE_WORKER_FUNC_ID,
61
+ name: queueName,
62
+ }
63
+ }
64
+ wireQueueWorker({
65
+ name: queueName,
66
+ func: { func: pikkuAIScoreWorkerFunc },
67
+ } as never)
68
+ }
69
+ }