@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
@@ -51,6 +51,13 @@ export interface AIToolResult {
51
51
  id: string
52
52
  name: string
53
53
  result: string
54
+ /**
55
+ * Set when the tool threw rather than returned. Carried separately from
56
+ * `result`, which is a rendered string by the time it is persisted — a tool
57
+ * may legitimately return text beginning `Error:`, so the prefix cannot be
58
+ * read as a failure signal.
59
+ */
60
+ error?: string
54
61
  }
55
62
 
56
63
  export interface AIMessage {
@@ -82,7 +89,13 @@ export interface AIMessage {
82
89
 
83
90
  export interface AIAgentStep {
84
91
  usage: { inputTokens: number; outputTokens: number }
85
- toolCalls?: { name: string; args: Record<string, unknown>; result: string }[]
92
+ toolCalls?: {
93
+ name: string
94
+ args: Record<string, unknown>
95
+ result: string
96
+ /** The failure message, when the tool threw rather than returned. */
97
+ error?: string
98
+ }[]
86
99
  }
87
100
 
88
101
  export interface AIAgentInputAttachment {
@@ -233,16 +246,40 @@ export interface PikkuAIMiddlewareHooks<
233
246
  | AIStreamEvent[]
234
247
  | null
235
248
 
249
+ /**
250
+ * The last chance to rewrite what the run produced, before it is persisted
251
+ * and returned.
252
+ *
253
+ * It does **not** run on a streamed run: there the text has already reached
254
+ * the client and each step is flushed to storage as it goes, so nothing could
255
+ * act on what this returned. Use {@link modifyOutputStream} to rewrite a
256
+ * streamed reply — a middleware that implements only this one is warned about
257
+ * when an agent it is attached to streams.
258
+ *
259
+ * `toolCalls` is here so a redaction pass covers the whole run record rather
260
+ * than just the visible answer: the tool arguments and results are persisted
261
+ * and handed to anything that grades the run, and scrubbing the reply alone
262
+ * leaves them untouched.
263
+ */
236
264
  modifyOutput?: (
237
265
  services: Services,
238
266
  ctx: {
239
267
  text: string
240
268
  messages: AIMessage[]
241
269
  usage: { inputTokens: number; outputTokens: number }
270
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>
242
271
  }
243
272
  ) =>
244
- | Promise<{ text: string; messages: AIMessage[] }>
245
- | { text: string; messages: AIMessage[] }
273
+ | Promise<{
274
+ text: string
275
+ messages: AIMessage[]
276
+ toolCalls?: NonNullable<AIAgentStep['toolCalls']>
277
+ }>
278
+ | {
279
+ text: string
280
+ messages: AIMessage[]
281
+ toolCalls?: NonNullable<AIAgentStep['toolCalls']>
282
+ }
246
283
 
247
284
  beforeToolCall?: (
248
285
  services: Services,
@@ -273,7 +310,13 @@ export interface PikkuAIMiddlewareHooks<
273
310
  stepNumber: number
274
311
  text: string
275
312
  toolCalls: { toolCallId: string; toolName: string; args: unknown }[]
276
- toolResults: { toolCallId: string; toolName: string; result: unknown }[]
313
+ toolResults: {
314
+ toolCallId: string
315
+ toolName: string
316
+ result: unknown
317
+ /** Set when the tool threw rather than returned. */
318
+ error?: string
319
+ }[]
277
320
  usage: { inputTokens: number; outputTokens: number }
278
321
  finishReason: string
279
322
  }
@@ -301,6 +344,7 @@ export type CoreAIAgent<
301
344
  PikkuPermission = CorePikkuPermission<any, any>,
302
345
  PikkuMiddleware = CorePikkuMiddleware<any>,
303
346
  Scope extends string = string,
347
+ Scorer extends string = string,
304
348
  > = {
305
349
  name: string
306
350
  description: string
@@ -327,6 +371,16 @@ export type CoreAIAgent<
327
371
  tools?: unknown[]
328
372
  agents?: unknown[]
329
373
  workflows?: unknown[]
374
+ /**
375
+ * Grades this agent's finished runs on live traffic, named by the generated
376
+ * `ScorerName` union rather than by `ref()` — a scorer is not a function, so
377
+ * there is nothing in the function map for a ref to resolve against.
378
+ *
379
+ * A reference-based judge listed here is never sampled: live traffic has no
380
+ * answer key. Scenarios name scorers directly and may grade with scorers an
381
+ * agent does not ship with.
382
+ */
383
+ scorers?: Scorer[]
330
384
  agentMode?: 'delegate' | 'supervise'
331
385
  memory?: AIAgentMemoryConfig
332
386
  maxSteps?: number
@@ -392,6 +446,12 @@ export type AIStreamEvent =
392
446
  toolCallId: string
393
447
  toolName: string
394
448
  result: unknown
449
+ /**
450
+ * The failure message, set when the tool threw rather than returned.
451
+ * Carried explicitly because a tool may legitimately return text that
452
+ * reads like an error, so `result` cannot be matched on to tell.
453
+ */
454
+ error?: string
395
455
  agent?: string
396
456
  session?: string
397
457
  }
@@ -413,7 +473,7 @@ export type AIStreamEvent =
413
473
  toolName: string
414
474
  args: unknown
415
475
  reason?: string
416
- runId?: string
476
+ runId: string
417
477
  agent?: string
418
478
  session?: string
419
479
  }
@@ -5,8 +5,9 @@ export {
5
5
  agentApprove,
6
6
  agentInterrupt,
7
7
  } from './ai-agent-helpers.js'
8
- export { wrapChannelWithAGUI, type AGUIEvent } from './ai-agent-agui.js'
8
+ export { wrapChannelWithAGUI } from './ai-agent-agui.js'
9
9
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js'
10
+ export { resolveModelAlias } from './ai-agent-model-config.js'
10
11
  export {
11
12
  streamAIAgent,
12
13
  resumeAIAgent,
@@ -14,7 +15,6 @@ export {
14
15
  } from './ai-agent-stream.js'
15
16
  export {
16
17
  voiceInput,
17
- readsAsNonSpeech,
18
18
  NoSpeechDetectedError,
19
19
  SPOKEN_TURN,
20
20
  SPOKEN_TRANSCRIPT,
@@ -27,21 +27,12 @@ export {
27
27
  } from './voice-output.js'
28
28
  export {
29
29
  AgentInterruptedError,
30
- awaitPendingInterruptNote,
31
- getInFlightTools,
32
- isAbortError,
33
- isRunInterruptible,
34
- persistOrphanedToolResults,
35
- registerInterruptibleRun,
36
30
  signalRunInterrupt,
37
- trackInterruptNote,
38
- trackToolExecution,
39
31
  } from './ai-agent-interrupt.js'
40
32
  export type {
41
33
  AgentInterruption,
42
34
  AgentInterruptResult,
43
35
  InterruptibleRunHandle,
44
- OrphanedToolResult,
45
36
  } from './ai-agent-interrupt.js'
46
37
  export {
47
38
  type RunAIAgentParams,
@@ -50,18 +41,13 @@ export {
50
41
  ToolCredentialRequired,
51
42
  canAccessThread,
52
43
  isOwnedByPrincipal,
53
- sessionPrincipals,
54
44
  threadOwnerConstraint,
55
45
  } from './ai-agent-prepare.js'
56
46
  export {
57
47
  addAIAgent,
58
- approveAIAgent,
59
- getAIAgents,
60
- getAIAgentsMeta,
61
48
  } from './ai-agent-registry.js'
62
49
  export type {
63
50
  AIAgentInput,
64
- AIAgentInputAttachment,
65
51
  AIAgentMeta,
66
52
  AIAgentMemoryConfig,
67
53
  AIAgentStep,
@@ -164,12 +164,7 @@ export const voiceInput = (config?: {
164
164
  // to send, and a message with no content is not a question.
165
165
  if (updatedContent.length === 0) throw new NoSpeechDetectedError()
166
166
 
167
- // Only when something was actually heard. A turn can carry audio that all
168
- // reads as non-speech and still have content — an image with a silent
169
- // caption clip — which leaves nothing above to throw. Recording `''` here
170
- // would send a transcript event saying the user said nothing, and a client
171
- // that tells "not transcribed yet" from "transcribed" by the key being
172
- // absent would render the turn as an empty bubble rather than a pending one.
167
+ // knowledge: decisions/internals/an-empty-transcript-is-not-recorded.md
173
168
  if (heard.length > 0) {
174
169
  shared[SPOKEN_TRANSCRIPT] = heard.join(' ')
175
170
  }
@@ -206,11 +206,7 @@ export const voiceOutput = (config?: {
206
206
  }
207
207
  if (!aiAgentRunner?.generateSpeech) return event
208
208
 
209
- // Only an explicit `false` silences the reply. `voiceInput` sets this on
210
- // every turn it handles, so `false` means a real user really typed; the
211
- // key being absent means nothing reported either way — no voice input is
212
- // wired — and that caller's replies are spoken exactly as they were before
213
- // this option existed.
209
+ // knowledge: decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md
214
210
  if (!config?.always && shared[SPOKEN_TURN] === false) return event
215
211
 
216
212
  /**
@@ -228,13 +224,7 @@ export const voiceOutput = (config?: {
228
224
  )
229
225
  }
230
226
 
231
- // Checked per sentence but announced once per reply: a bilingual answer
232
- // should still speak the half it can, and repeating the notice for
233
- // every sentence of a long one would bury the reply itself.
234
- //
235
- // Per sentence is also what makes the voice right. A reply that answers
236
- // in English and then quotes a Chinese title is two sentences in two
237
- // scripts, and each is synthesized in the voice its own script needs.
227
+ // knowledge: decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md
238
228
  let voice = config.voice
239
229
  if (config.speakableScripts) {
240
230
  const unspeakable = unspeakableScripts(text, config.speakableScripts)
@@ -0,0 +1,106 @@
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 { gradeRun } from './ai-scorer-grade.js'
5
+ import { pikkuAIJudge, pikkuAIScorer } from './ai-scorer.js'
6
+ import type { ScoreJob } from './ai-scorer.types.js'
7
+
8
+ const job = (overrides: Partial<ScoreJob> = {}): ScoreJob => ({
9
+ scorerName: 'correctness',
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
+ describe('gradeRun', () => {
20
+ beforeEach(() => resetPikkuState())
21
+
22
+ test('a scenario grade is returned and not written to the live record', async () => {
23
+ pikkuState(null, 'agent', 'scorers').set(
24
+ 'correctness',
25
+ pikkuAIScorer({
26
+ name: 'correctness',
27
+ description: 'Matches the answer key',
28
+ requiresReference: true,
29
+ score: (input) => ({
30
+ score: input.output === input.reference ? 1 : 0,
31
+ }),
32
+ })
33
+ )
34
+ let saves = 0
35
+
36
+ const result = await gradeRun(
37
+ job({ reference: 'Paris' }),
38
+ {
39
+ aiRunState: {
40
+ saveScore: async () => {
41
+ saves++
42
+ },
43
+ },
44
+ },
45
+ { persist: false }
46
+ )
47
+
48
+ assert.deepEqual(result, { score: 1 })
49
+ assert.equal(saves, 0)
50
+ })
51
+
52
+ test('a reference-based scorer sees the answer key it was given', async () => {
53
+ pikkuState(null, 'agent', 'scorers').set(
54
+ 'correctness',
55
+ pikkuAIScorer({
56
+ name: 'correctness',
57
+ description: 'Matches the answer key',
58
+ requiresReference: true,
59
+ score: (input) => ({
60
+ score: input.output === input.reference ? 1 : 0,
61
+ }),
62
+ })
63
+ )
64
+
65
+ const result = await gradeRun(
66
+ job({ reference: 'Lyon' }),
67
+ {},
68
+ {
69
+ persist: false,
70
+ }
71
+ )
72
+
73
+ assert.equal(result.score, 0)
74
+ })
75
+
76
+ test('a judge grades without a score function, and reports the model it used', async () => {
77
+ pikkuState(null, 'agent', 'scorers').set(
78
+ 'helpfulness',
79
+ pikkuAIJudge({
80
+ name: 'helpfulness',
81
+ description: 'Is the answer useful',
82
+ model: 'claude-opus-5',
83
+ goal: 'Grade helpfulness.',
84
+ })
85
+ )
86
+
87
+ const result = await gradeRun(
88
+ job({ scorerName: 'helpfulness' }),
89
+ {
90
+ aiAgentRunner: {
91
+ run: async () => ({
92
+ object: { score: 0.75, reason: 'Correct but terse.' },
93
+ usage: { inputTokens: 80, outputTokens: 20 },
94
+ }),
95
+ },
96
+ },
97
+ { persist: false }
98
+ )
99
+
100
+ assert.equal(result.score, 0.75)
101
+ assert.deepEqual(result.metadata, {
102
+ judgeModel: 'claude-opus-5',
103
+ judgeTokens: 100,
104
+ })
105
+ })
106
+ })
@@ -0,0 +1,55 @@
1
+ import { runJudge } from './ai-scorer-judge.js'
2
+ import { resolveAIScorer } from './ai-scorer-registry.js'
3
+ import type { ScoreJob, ScorerOutput } from './ai-scorer.types.js'
4
+
5
+ /**
6
+ * Grade one run with one scorer.
7
+ *
8
+ * The single path both callers take — the lane worker on live traffic and the
9
+ * scenario grading RPC — so a scenario's grade is the same computation the
10
+ * production sampler would have made, not an approximation of it.
11
+ *
12
+ * Persisting is optional because the two callers differ on it: a live grade is
13
+ * only useful once recorded, while a scenario asserts on the returned value and
14
+ * runs against servers that may have no run-state adapter at all.
15
+ */
16
+ export const gradeRun = async (
17
+ job: ScoreJob,
18
+ services: {
19
+ aiAgentRunner?: unknown
20
+ aiRunState?: {
21
+ saveScore: (score: {
22
+ runId: string
23
+ scorerName: string
24
+ score: number
25
+ reason?: string
26
+ metadata?: Record<string, unknown>
27
+ }) => Promise<void>
28
+ }
29
+ },
30
+ options: { persist: boolean }
31
+ ): Promise<ScorerOutput> => {
32
+ const { scorerName, ...input } = job
33
+ const scorer = resolveAIScorer(scorerName)
34
+
35
+ const result = scorer.score
36
+ ? await scorer.score(input, services)
37
+ : await runJudge(scorer, input, services.aiAgentRunner as never)
38
+
39
+ if (options.persist) {
40
+ if (!services.aiRunState) {
41
+ throw new Error(
42
+ `AI run state service not initialized: cannot record the '${scorerName}' grade of run ${job.runId}`
43
+ )
44
+ }
45
+ await services.aiRunState.saveScore({
46
+ runId: job.runId,
47
+ scorerName,
48
+ score: result.score,
49
+ ...(result.reason !== undefined ? { reason: result.reason } : {}),
50
+ ...(result.metadata !== undefined ? { metadata: result.metadata } : {}),
51
+ })
52
+ }
53
+
54
+ return result
55
+ }
@@ -0,0 +1,143 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { buildJudgePrompt, runJudge } from './ai-scorer-judge.js'
4
+ import { pikkuAIJudge } from './ai-scorer.js'
5
+ import type { ScorerInput } from './ai-scorer.types.js'
6
+ import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
7
+
8
+ const input = (overrides: Partial<ScorerInput> = {}): ScorerInput => ({
9
+ runId: 'run-1',
10
+ agentName: 'assistant',
11
+ input: 'what is the capital of France?',
12
+ output: 'Paris',
13
+ toolCalls: [],
14
+ usage: { inputTokens: 10, outputTokens: 5 },
15
+ ...overrides,
16
+ })
17
+
18
+ const runner = (
19
+ object: unknown,
20
+ seen?: { params: any }
21
+ ): AIAgentRunnerService =>
22
+ ({
23
+ run: async (params: any) => {
24
+ if (seen) seen.params = params
25
+ return { object, usage: { inputTokens: 100, outputTokens: 20 } }
26
+ },
27
+ }) as unknown as AIAgentRunnerService
28
+
29
+ describe('buildJudgePrompt', () => {
30
+ test('shows the answer key to a reference-based judge', () => {
31
+ const scorer = pikkuAIJudge({
32
+ name: 'correctness',
33
+ description: 'Is the answer right',
34
+ model: 'claude-opus-5',
35
+ goal: 'Grade correctness.',
36
+ requiresReference: true,
37
+ })
38
+
39
+ const prompt = buildJudgePrompt(
40
+ scorer.judge!,
41
+ input({ reference: 'Paris' })
42
+ )
43
+
44
+ assert.match(prompt, /Reference answer:\nParis/)
45
+ })
46
+
47
+ test('withholds a reference section when there is no answer key', () => {
48
+ const scorer = pikkuAIJudge({
49
+ name: 'helpfulness',
50
+ description: 'Is the answer useful',
51
+ model: 'claude-opus-5',
52
+ goal: 'Grade helpfulness.',
53
+ })
54
+
55
+ const prompt = buildJudgePrompt(scorer.judge!, input())
56
+
57
+ assert.doesNotMatch(prompt, /Reference answer/)
58
+ assert.match(prompt, /Grade helpfulness\./)
59
+ })
60
+
61
+ test('a scorer that supplies its own prompt replaces the framing entirely', () => {
62
+ const scorer = pikkuAIJudge({
63
+ name: 'custom',
64
+ description: 'Custom framing',
65
+ model: 'claude-opus-5',
66
+ goal: 'ignored',
67
+ prompt: (run) => `Only this: ${run.output}`,
68
+ })
69
+
70
+ assert.equal(buildJudgePrompt(scorer.judge!, input()), 'Only this: Paris')
71
+ })
72
+ })
73
+
74
+ describe('runJudge', () => {
75
+ const helpfulness = pikkuAIJudge({
76
+ name: 'helpfulness',
77
+ description: 'Is the answer useful',
78
+ model: 'claude-opus-5',
79
+ goal: 'Grade helpfulness.',
80
+ })
81
+
82
+ test('records the score, the reason and what the judgement cost', async () => {
83
+ const result = await runJudge(
84
+ helpfulness,
85
+ input(),
86
+ runner({ score: 0.8, reason: 'Direct and correct.' })
87
+ )
88
+
89
+ assert.equal(result.score, 0.8)
90
+ assert.equal(result.reason, 'Direct and correct.')
91
+ assert.deepEqual(result.metadata, {
92
+ judgeModel: 'claude-opus-5',
93
+ judgeTokens: 120,
94
+ })
95
+ })
96
+
97
+ test('clamps an out-of-range score rather than losing the judgement', async () => {
98
+ const high = await runJudge(helpfulness, input(), runner({ score: 1.4 }))
99
+ const low = await runJudge(helpfulness, input(), runner({ score: -2 }))
100
+
101
+ assert.equal(high.score, 1)
102
+ assert.equal(low.score, 0)
103
+ })
104
+
105
+ test('rejects a non-numeric score, which is not a judgement at all', async () => {
106
+ await assert.rejects(
107
+ () => runJudge(helpfulness, input(), runner({ score: 'great' })),
108
+ /non-numeric score/
109
+ )
110
+ })
111
+
112
+ test('gives the judge no tools, so its structured output is actually honoured', async () => {
113
+ const seen = { params: undefined as any }
114
+ await runJudge(helpfulness, input(), runner({ score: 1 }, seen))
115
+
116
+ assert.deepEqual(seen.params.tools, [])
117
+ assert.equal(seen.params.toolChoice, 'none')
118
+ assert.equal(seen.params.maxSteps, 1)
119
+ assert.equal(seen.params.model, 'claude-opus-5')
120
+ })
121
+
122
+ test('names the missing provider rather than failing as an undefined call', async () => {
123
+ await assert.rejects(
124
+ () => runJudge(helpfulness, input(), undefined),
125
+ /needs an AI provider/
126
+ )
127
+ })
128
+
129
+ test('refuses to grade a reference-based judge with no answer key', async () => {
130
+ const correctness = pikkuAIJudge({
131
+ name: 'correctness',
132
+ description: 'Is the answer right',
133
+ model: 'claude-opus-5',
134
+ goal: 'Grade correctness.',
135
+ requiresReference: true,
136
+ })
137
+
138
+ await assert.rejects(
139
+ () => runJudge(correctness, input(), runner({ score: 1 })),
140
+ /grades against a reference answer/
141
+ )
142
+ })
143
+ })
@@ -0,0 +1,120 @@
1
+ import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
2
+ import type { AIMessage } from '../ai-agent/ai-agent.types.js'
3
+ import { randomUUID } from '../ai-agent/ai-agent-utils.js'
4
+ import type {
5
+ PikkuAIScorer,
6
+ ScorerInput,
7
+ ScorerJudgeConfig,
8
+ ScorerOutput,
9
+ } from './ai-scorer.types.js'
10
+
11
+ const JUDGE_OUTPUT_SCHEMA = {
12
+ type: 'object',
13
+ properties: {
14
+ score: {
15
+ type: 'number',
16
+ minimum: 0,
17
+ maximum: 1,
18
+ description: 'How well the answer meets the rubric. 1 is best.',
19
+ },
20
+ reason: {
21
+ type: 'string',
22
+ description: 'One or two sentences explaining the score.',
23
+ },
24
+ },
25
+ required: ['score', 'reason'],
26
+ additionalProperties: false,
27
+ }
28
+
29
+ /**
30
+ * The message a judge grades when its scorer supplies no `prompt`.
31
+ *
32
+ * A reference-based judge is shown the answer key; a reference-free one is not,
33
+ * and grades the answer on its own merits.
34
+ */
35
+ export const buildJudgePrompt = (
36
+ judge: ScorerJudgeConfig,
37
+ input: ScorerInput
38
+ ): string => {
39
+ if (judge.prompt) return judge.prompt(input)
40
+
41
+ const sections = [`User asked:\n${input.input}`]
42
+ if (input.reference !== undefined) {
43
+ sections.push(`Reference answer:\n${input.reference}`)
44
+ }
45
+ sections.push(`Assistant answered:\n${input.output}`)
46
+ sections.push(judge.goal)
47
+ sections.push('Respond with a score 0..1 and a brief reason.')
48
+ return sections.join('\n\n')
49
+ }
50
+
51
+ /**
52
+ * Clamp rather than reject: a model that answers 1.2 has still made a legible
53
+ * judgement, and failing the job would lose it. A non-number has not.
54
+ */
55
+ const normalizeScore = (scorerName: string, value: unknown): number => {
56
+ if (typeof value !== 'number' || Number.isNaN(value)) {
57
+ throw new Error(
58
+ `Judge '${scorerName}' returned a non-numeric score: ${JSON.stringify(value)}`
59
+ )
60
+ }
61
+ return Math.min(1, Math.max(0, value))
62
+ }
63
+
64
+ /**
65
+ * Run an LLM judge and force the `{ score, reason }` contract, so a scorer
66
+ * author writes a rubric rather than a parser.
67
+ *
68
+ * A judge has no tools, which is the one case where an output schema is
69
+ * reliably honoured.
70
+ */
71
+ export const runJudge = async (
72
+ scorer: PikkuAIScorer<any>,
73
+ input: ScorerInput,
74
+ aiAgentRunner: AIAgentRunnerService | undefined
75
+ ): Promise<ScorerOutput> => {
76
+ const judge = scorer.judge
77
+ if (!judge) {
78
+ throw new Error(`Scorer '${scorer.name}' is not a judge`)
79
+ }
80
+ if (!aiAgentRunner) {
81
+ throw new Error(
82
+ `Judge '${scorer.name}' needs an AI provider, but no aiAgentRunner is registered in this process. ` +
83
+ `A worker deployed apart from the API has to register one to run the slow scoring lane.`
84
+ )
85
+ }
86
+ if (scorer.requiresReference && input.reference === undefined) {
87
+ throw new Error(
88
+ `Judge '${scorer.name}' grades against a reference answer, but none was supplied`
89
+ )
90
+ }
91
+
92
+ const messages: AIMessage[] = [
93
+ {
94
+ id: randomUUID(),
95
+ role: 'user',
96
+ content: buildJudgePrompt(judge, input),
97
+ createdAt: new Date(),
98
+ },
99
+ ]
100
+
101
+ const result = await aiAgentRunner.run({
102
+ model: judge.model,
103
+ instructions: judge.goal,
104
+ messages,
105
+ tools: [],
106
+ maxSteps: 1,
107
+ toolChoice: 'none',
108
+ outputSchema: JUDGE_OUTPUT_SCHEMA,
109
+ })
110
+
111
+ const object = (result.object ?? {}) as { score?: unknown; reason?: unknown }
112
+ return {
113
+ score: normalizeScore(scorer.name, object.score),
114
+ ...(typeof object.reason === 'string' ? { reason: object.reason } : {}),
115
+ metadata: {
116
+ judgeModel: judge.model,
117
+ judgeTokens: result.usage.inputTokens + result.usage.outputTokens,
118
+ },
119
+ }
120
+ }