@pikku/core 0.12.80 → 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 (231) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/dist/errors/index.d.ts +1 -1
  3. package/dist/errors/index.js +1 -1
  4. package/dist/function/function-runner.js +2 -5
  5. package/dist/function/index.d.ts +1 -1
  6. package/dist/index.d.ts +11 -11
  7. package/dist/index.js +3 -3
  8. package/dist/pikku-state.js +4 -0
  9. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  10. package/dist/services/ai-run-state-service.d.ts +10 -0
  11. package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
  12. package/dist/services/in-memory-ai-run-state-service.js +9 -0
  13. package/dist/services/index.d.ts +15 -15
  14. package/dist/services/index.js +5 -5
  15. package/dist/services/scoped-credential-service.d.ts +21 -0
  16. package/dist/services/scoped-credential-service.js +53 -0
  17. package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
  18. package/dist/types/core.types.d.ts +0 -2
  19. package/dist/types/state.types.d.ts +13 -0
  20. package/dist/wirings/actor-flow/index.d.ts +1 -1
  21. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  22. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  23. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  24. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  25. package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
  26. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  27. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  28. package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -0
  29. package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
  30. package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
  31. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
  32. package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
  33. package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
  34. package/dist/wirings/ai-agent/index.d.ts +8 -7
  35. package/dist/wirings/ai-agent/index.js +5 -4
  36. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  37. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  38. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  39. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  40. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  41. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  42. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  43. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  44. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  45. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  46. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  47. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  48. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  49. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  50. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  51. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  52. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  53. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  54. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  55. package/dist/wirings/ai-scorer/index.js +5 -0
  56. package/dist/wirings/channel/index.d.ts +5 -6
  57. package/dist/wirings/channel/index.js +3 -4
  58. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  59. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  60. package/dist/wirings/cli/channel/index.d.ts +1 -2
  61. package/dist/wirings/cli/channel/index.js +0 -1
  62. package/dist/wirings/cli/cli-runner.js +13 -1
  63. package/dist/wirings/credential/index.d.ts +1 -1
  64. package/dist/wirings/gateway/index.d.ts +1 -1
  65. package/dist/wirings/http/http-runner.js +8 -2
  66. package/dist/wirings/http/index.d.ts +1 -2
  67. package/dist/wirings/mcp/index.d.ts +1 -1
  68. package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
  69. package/dist/wirings/mcp/mcp-runner.js +18 -5
  70. package/dist/wirings/persona/index.d.ts +3 -4
  71. package/dist/wirings/persona/index.js +2 -3
  72. package/dist/wirings/queue/index.d.ts +1 -3
  73. package/dist/wirings/queue/index.js +1 -3
  74. package/dist/wirings/rpc/addon-runner.d.ts +4 -0
  75. package/dist/wirings/rpc/addon-runner.js +19 -3
  76. package/dist/wirings/rpc/rpc-runner.js +2 -0
  77. package/dist/wirings/rpc/rpc-types.d.ts +4 -0
  78. package/dist/wirings/rpc/wire-addon.d.ts +13 -0
  79. package/dist/wirings/rpc/wire-addon.js +4 -0
  80. package/dist/wirings/scheduler/index.d.ts +1 -1
  81. package/dist/wirings/trigger/index.d.ts +1 -1
  82. package/dist/wirings/virtual-user/index.d.ts +5 -6
  83. package/dist/wirings/virtual-user/index.js +2 -4
  84. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
  85. package/dist/wirings/workflow/index.d.ts +6 -6
  86. package/dist/wirings/workflow/index.js +2 -2
  87. package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
  88. package/dist/wirings/workflow/pikku-scenario-service.js +39 -13
  89. package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
  90. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  91. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  92. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  93. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  94. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  95. package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
  96. package/dist/wirings/workflow/workflow-approval.js +65 -2
  97. package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
  98. package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
  99. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  100. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  101. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  102. package/knowledge/decisions/internals/index.md +30 -3
  103. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  104. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  105. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
  106. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  107. package/knowledge/decisions/security/index.md +3 -1
  108. package/knowledge/questions/index.md +1 -1
  109. package/package.json +3 -1
  110. package/scripts/generate-api-report.mts +143 -18
  111. package/src/api-report.test.ts +2 -2
  112. package/src/errors/index.ts +1 -1
  113. package/src/function/function-runner.test.ts +52 -0
  114. package/src/function/function-runner.ts +5 -9
  115. package/src/function/index.ts +0 -2
  116. package/src/index.ts +0 -35
  117. package/src/pikku-state.ts +5 -0
  118. package/src/public-surface.json +70 -94
  119. package/src/services/ai-agent-runner-service.ts +12 -1
  120. package/src/services/ai-run-state-service.ts +11 -0
  121. package/src/services/in-memory-ai-run-state-service.ts +13 -0
  122. package/src/services/index.ts +3 -43
  123. package/src/services/scoped-credential-service.test.ts +86 -0
  124. package/src/services/scoped-credential-service.ts +63 -0
  125. package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
  126. package/src/types/core.types.ts +3 -6
  127. package/src/types/state.types.ts +16 -0
  128. package/src/wirings/actor-flow/index.ts +0 -3
  129. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  130. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  131. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  132. package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
  133. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  134. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  135. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -0
  136. package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
  137. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  138. package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
  139. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  140. package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
  141. package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
  142. package/src/wirings/ai-agent/index.ts +2 -16
  143. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  144. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  145. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  146. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  147. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  148. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  149. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  150. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  151. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  152. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  153. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  154. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  155. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  156. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  157. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  158. package/src/wirings/ai-scorer/index.ts +24 -0
  159. package/src/wirings/channel/index.ts +1 -20
  160. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  161. package/src/wirings/channel/local/local-channel-runner.ts +8 -1
  162. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  163. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  164. package/src/wirings/cli/channel/index.ts +0 -7
  165. package/src/wirings/cli/cli-runner.test.ts +68 -0
  166. package/src/wirings/cli/cli-runner.ts +18 -1
  167. package/src/wirings/credential/index.ts +0 -1
  168. package/src/wirings/gateway/index.ts +0 -3
  169. package/src/wirings/http/http-runner.test.ts +66 -0
  170. package/src/wirings/http/http-runner.ts +10 -2
  171. package/src/wirings/http/index.ts +1 -1
  172. package/src/wirings/mcp/index.ts +0 -1
  173. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  174. package/src/wirings/mcp/mcp-runner.ts +35 -5
  175. package/src/wirings/persona/index.ts +0 -8
  176. package/src/wirings/queue/index.ts +0 -14
  177. package/src/wirings/rpc/addon-runner.ts +34 -3
  178. package/src/wirings/rpc/addon-secrets.test.ts +261 -0
  179. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  180. package/src/wirings/rpc/rpc-runner.ts +2 -0
  181. package/src/wirings/rpc/rpc-types.ts +4 -0
  182. package/src/wirings/rpc/wire-addon.ts +17 -0
  183. package/src/wirings/scheduler/index.ts +0 -1
  184. package/src/wirings/trigger/index.ts +0 -1
  185. package/src/wirings/virtual-user/index.ts +0 -16
  186. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
  187. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  188. package/src/wirings/workflow/index.ts +2 -20
  189. package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
  190. package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
  191. package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
  192. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  193. package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
  194. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  195. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  196. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  197. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  198. package/src/wirings/workflow/workflow-approval.ts +113 -9
  199. package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
  200. package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
  201. package/src/wirings/workflow/workflow.types.ts +0 -9
  202. package/src/wirings-stay-decoupled.test.ts +6 -2
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/dist/internal.d.ts +0 -3
  205. package/dist/internal.js +0 -2
  206. package/dist/middleware/timeout.d.ts +0 -9
  207. package/dist/middleware/timeout.js +0 -15
  208. package/dist/pikku-response.d.ts +0 -6
  209. package/dist/pikku-response.js +0 -6
  210. package/dist/services/gopass-secrets.d.ts +0 -15
  211. package/dist/services/gopass-secrets.js +0 -76
  212. package/dist/services/http-scenario-actors.d.ts +0 -75
  213. package/dist/services/http-scenario-actors.js +0 -195
  214. package/dist/services/http-user-flow-actors.d.ts +0 -67
  215. package/dist/services/http-user-flow-actors.js +0 -193
  216. package/dist/services/scenario-actors-service.d.ts +0 -127
  217. package/dist/services/scenario-actors-service.js +0 -40
  218. package/dist/services/user-flow-actors-service.d.ts +0 -39
  219. package/dist/services/user-flow-actors-service.js +0 -1
  220. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  221. package/dist/wirings/credential/wire-credential.js +0 -47
  222. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  223. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  224. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  225. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  226. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  227. package/dist/wirings/scope/wire-scope.js +0 -32
  228. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  229. package/dist/wirings/workflow/dsl/index.js +0 -4
  230. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  231. package/dist/wirings/workflow/graph/index.js +0 -4
@@ -4,6 +4,7 @@ import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
4
4
  import { AgentInterruptedError, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
5
5
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
6
6
  import { applyInputMiddleware, describeApprovals, notifyAfterStep, toAccumulatedStep, } from './ai-agent-turn.js';
7
+ import { applyOutputMiddleware, finalizeAgentRun, lastUserMessageText, } from './ai-agent-finalize.js';
7
8
  import { resolveModelConfig } from './ai-agent-model-config.js';
8
9
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
9
10
  import { randomUUID } from './ai-agent-utils.js';
@@ -192,27 +193,25 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
192
193
  text: finalText,
193
194
  steps: accumulatedSteps,
194
195
  };
195
- let outputText = finalText;
196
- let outputMessages = runnerParams.messages;
197
- for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
198
- const mw = aiMiddlewares[i];
199
- if (mw.modifyOutput) {
200
- const modResult = await mw.modifyOutput(singletonServices, {
201
- text: outputText,
202
- messages: outputMessages,
203
- usage: totalUsage,
204
- });
205
- outputText = modResult.text;
206
- outputMessages = modResult.messages;
207
- }
208
- }
196
+ const { text: outputText, steps: outputSteps } = await applyOutputMiddleware(aiMiddlewares, singletonServices, {
197
+ text: finalText,
198
+ messages: runnerParams.messages,
199
+ steps: result.steps,
200
+ usage: totalUsage,
201
+ });
209
202
  await saveMessages(storage, threadId, input.resourceId, memoryConfig, persistedUserMessage, {
210
- ...result,
211
203
  text: outputText,
204
+ steps: outputSteps,
212
205
  uiSpec: structuredOutput.uiSpec,
213
206
  });
214
- await aiRunState.updateRun(runId, {
215
- status: 'completed',
207
+ await finalizeAgentRun(aiRunState, {
208
+ runId,
209
+ agentName,
210
+ threadId,
211
+ resourceId: input.resourceId,
212
+ input: lastUserMessageText(runnerParams.messages),
213
+ text: outputText,
214
+ steps: outputSteps,
216
215
  usage: { ...totalUsage, model: agent.model },
217
216
  });
218
217
  return {
@@ -220,7 +219,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
220
219
  text: outputText,
221
220
  object: finalObject,
222
221
  threadId,
223
- steps: accumulatedSteps,
222
+ steps: outputSteps,
224
223
  usage: totalUsage,
225
224
  };
226
225
  }
@@ -305,6 +304,7 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
305
304
  if (!claimedIds.has(toolCallId))
306
305
  continue;
307
306
  let resultStr;
307
+ let toolError;
308
308
  if (rejectedIds.has(toolCallId)) {
309
309
  resultStr =
310
310
  'The user explicitly declined this action. Inform them that it was declined and do not retry.';
@@ -330,7 +330,8 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
330
330
  resultStr = JSON.stringify(err.payload);
331
331
  }
332
332
  else {
333
- resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`;
333
+ toolError = err instanceof Error ? err.message : String(err);
334
+ resultStr = `Error: ${toolError}`;
334
335
  }
335
336
  }
336
337
  }
@@ -344,6 +345,7 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
344
345
  ? JSON.parse(pending.args)
345
346
  : pending.args,
346
347
  result: resultStr,
348
+ ...(toolError ? { error: toolError } : {}),
347
349
  });
348
350
  }
349
351
  if (storage && toolCallMessages.length > 0) {
@@ -361,9 +363,23 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
361
363
  ]);
362
364
  }
363
365
  await aiRunState.updateRun(runId, { status: 'running' });
364
- return continueAfterToolResultSync(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, params, aiRunState);
366
+ return continueAfterToolResultSync(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, params, aiRunState,
367
+ // The approved tools were executed here, before the model was re-entered,
368
+ // so they belong to the run's step record — otherwise a tool that failed
369
+ // after approval leaves no trace on the run at all.
370
+ toolCallMessages.length > 0
371
+ ? {
372
+ usage: { inputTokens: 0, outputTokens: 0 },
373
+ toolCalls: toolCallMessages.map((tc) => ({
374
+ name: tc.toolName,
375
+ args: (tc.args ?? {}),
376
+ result: tc.result,
377
+ ...(tc.error ? { error: tc.error } : {}),
378
+ })),
379
+ }
380
+ : undefined);
365
381
  }
366
- async function continueAfterToolResultSync(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, params, aiRunState) {
382
+ async function continueAfterToolResultSync(run, agent, packageName, resolvedName, storage, memoryConfig, agentRunner, params, aiRunState, resumedToolStep) {
367
383
  const singletonServices = getSingletonServices();
368
384
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
369
385
  const meta = agentsMeta[resolvedName];
@@ -416,6 +432,11 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
416
432
  runnerParams.abortSignal = interruptHandle.signal;
417
433
  runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle);
418
434
  try {
435
+ // Kept out of `accumulatedSteps` deliberately: that array drives
436
+ // `saveMessages`, and the approved tool's messages were already written to
437
+ // the thread before the model was re-entered. It belongs to the run's step
438
+ // record, not to persistence.
439
+ const withResumedStep = (steps) => resumedToolStep ? [resumedToolStep, ...steps] : steps;
419
440
  const accumulatedSteps = [];
420
441
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
421
442
  let lastStepResult = null;
@@ -475,7 +496,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
475
496
  runId: run.runId,
476
497
  text: suspendedText,
477
498
  threadId: run.threadId,
478
- steps: accumulatedSteps,
499
+ steps: withResumedStep(accumulatedSteps),
479
500
  usage: totalUsage,
480
501
  status: 'suspended',
481
502
  pendingApprovals: approvalsNeeded.map((a) => ({
@@ -495,26 +516,26 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
495
516
  text: finalText,
496
517
  steps: accumulatedSteps,
497
518
  };
498
- let outputText = finalText;
499
- let outputMessages = runnerParams.messages;
500
- for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
501
- const mw = aiMiddlewares[i];
502
- if (mw.modifyOutput) {
503
- const modResult = await mw.modifyOutput(singletonServices, {
504
- text: outputText,
505
- messages: outputMessages,
506
- usage: totalUsage,
507
- });
508
- outputText = modResult.text;
509
- outputMessages = modResult.messages;
510
- }
511
- }
519
+ const { text: outputText, steps: outputSteps } = await applyOutputMiddleware(aiMiddlewares, singletonServices, {
520
+ text: finalText,
521
+ messages: runnerParams.messages,
522
+ steps: withResumedStep(result.steps),
523
+ usage: totalUsage,
524
+ });
512
525
  await saveMessages(storage, run.threadId, run.resourceId, memoryConfig, null, {
513
- ...result,
514
526
  text: outputText,
527
+ // The approved tool's messages were written before the model was
528
+ // re-entered, so only the steps this leg generated are persisted here.
529
+ steps: accumulatedSteps,
515
530
  });
516
- await aiRunState.updateRun(run.runId, {
517
- status: 'completed',
531
+ await finalizeAgentRun(aiRunState, {
532
+ runId: run.runId,
533
+ agentName: resolvedName,
534
+ threadId: run.threadId,
535
+ resourceId: run.resourceId,
536
+ input: lastUserMessageText(runnerParams.messages),
537
+ text: outputText,
538
+ steps: outputSteps,
518
539
  usage: { ...totalUsage, model: agent.model },
519
540
  });
520
541
  return {
@@ -522,7 +543,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
522
543
  text: outputText,
523
544
  object: finalObject,
524
545
  threadId: run.threadId,
525
- steps: accumulatedSteps,
546
+ steps: outputSteps,
526
547
  usage: totalUsage,
527
548
  };
528
549
  }
@@ -1,3 +1,4 @@
1
+ import { finalizeAgentRun, lastUserMessageText, } from './ai-agent-finalize.js';
1
2
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
2
3
  import { applyInputMiddleware } from './ai-agent-turn.js';
3
4
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
@@ -18,6 +19,9 @@ function createPersistingChannel(parent, storage, threadId, logger) {
18
19
  inputTokens: 0,
19
20
  outputTokens: 0,
20
21
  };
22
+ // Survives the per-step flush below, which clears its own buffers: the run
23
+ // record needs every call the run made, not just the last step's.
24
+ const runToolCalls = [];
21
25
  const flushStep = async (opts) => {
22
26
  if (!storage)
23
27
  return;
@@ -63,6 +67,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
63
67
  });
64
68
  });
65
69
  };
70
+ const runToolCallIndex = new Map();
66
71
  const channel = {
67
72
  channelId: parent.channelId,
68
73
  openingData: parent.openingData,
@@ -75,6 +80,9 @@ function createPersistingChannel(parent, storage, threadId, logger) {
75
80
  get totalUsage() {
76
81
  return totalUsage;
77
82
  },
83
+ get runToolCalls() {
84
+ return runToolCalls;
85
+ },
78
86
  flush: flushStep,
79
87
  close: () => parent.close(),
80
88
  sendBinary: (data) => parent.sendBinary(data),
@@ -84,6 +92,26 @@ function createPersistingChannel(parent, storage, threadId, logger) {
84
92
  // report the fragment it got through even with persistence turned off.
85
93
  if (event.type === 'text-delta')
86
94
  fullText += event.text;
95
+ if (event.type === 'tool-call') {
96
+ runToolCallIndex.set(event.toolCallId, runToolCalls.length);
97
+ runToolCalls.push({
98
+ name: event.toolName,
99
+ args: event.args,
100
+ result: '',
101
+ });
102
+ }
103
+ if (event.type === 'tool-result') {
104
+ const index = runToolCallIndex.get(event.toolCallId);
105
+ const result = typeof event.result === 'string'
106
+ ? event.result
107
+ : JSON.stringify(event.result);
108
+ const call = index === undefined ? undefined : runToolCalls[index];
109
+ if (call) {
110
+ call.result = result;
111
+ if (event.error)
112
+ call.error = event.error;
113
+ }
114
+ }
87
115
  if (storage) {
88
116
  switch (event.type) {
89
117
  case 'text-delta':
@@ -103,6 +131,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
103
131
  result: typeof event.result === 'string'
104
132
  ? event.result
105
133
  : JSON.stringify(event.result),
134
+ ...(event.error ? { error: event.error } : {}),
106
135
  });
107
136
  break;
108
137
  case 'generative-ui':
@@ -129,36 +158,49 @@ function createPersistingChannel(parent, storage, threadId, logger) {
129
158
  };
130
159
  return channel;
131
160
  }
132
- async function postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, messages, aiRunState, runId) {
133
- const usage = persistingChannel.totalUsage;
134
- let outputText = persistingChannel.fullText;
135
- let outputMessages = messages;
136
- for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
137
- const mw = aiMiddlewares[i];
138
- if (mw.modifyOutput) {
139
- const result = await mw.modifyOutput(singletonServices, {
140
- text: outputText,
141
- messages: outputMessages,
142
- usage: {
143
- inputTokens: usage.inputTokens,
144
- outputTokens: usage.outputTokens,
145
- },
146
- });
147
- outputText = result.text;
148
- outputMessages = result.messages;
149
- }
150
- }
151
- await aiRunState.updateRun(runId, {
152
- status: 'completed',
153
- ...(usage.model
154
- ? {
155
- usage: {
156
- inputTokens: usage.inputTokens,
157
- outputTokens: usage.outputTokens,
158
- model: usage.model,
159
- },
160
- }
161
- : {}),
161
+ /**
162
+ * Agents already warned about, so a per-request hook does not become a
163
+ * per-request log line.
164
+ */
165
+ const warnedUnstreamedOutputHooks = new Set();
166
+ /**
167
+ * `modifyOutput` does not run on a streamed run at all. Nothing here could act
168
+ * on what it returns — the text has already reached the client, and
169
+ * `createPersistingChannel` flushes each step to storage as it goes, so by the
170
+ * time the run ends the transcript is already written.
171
+ *
172
+ * Rewriting on this path belongs to `modifyOutputStream`, which genuinely
173
+ * works: the stream middleware wraps the persisting channel, so what is stored
174
+ * and accumulated is already what the client was sent. A middleware that
175
+ * rewrites in `modifyOutput` only — a redaction hook, typically — is therefore
176
+ * silently ineffective when the agent is streamed, and is told so once.
177
+ */
178
+ const warnUnstreamedOutputHooks = (agentName, aiMiddlewares, logger) => {
179
+ if (warnedUnstreamedOutputHooks.has(agentName))
180
+ return;
181
+ const unstreamed = aiMiddlewares.some((mw) => mw.modifyOutput && !mw.modifyOutputStream);
182
+ if (!unstreamed)
183
+ return;
184
+ warnedUnstreamedOutputHooks.add(agentName);
185
+ logger?.warn(`Agent '${agentName}' has AI middleware with modifyOutput but no modifyOutputStream — modifyOutput does not apply to streamed runs. Implement modifyOutputStream to affect a streamed reply.`);
186
+ };
187
+ async function postStreamCleanup(persistingChannel, aiRunState, runId, run) {
188
+ await finalizeAgentRun(aiRunState, {
189
+ runId,
190
+ agentName: run.agentName,
191
+ threadId: run.threadId,
192
+ resourceId: run.resourceId,
193
+ input: run.input,
194
+ // Already what the client received: the stream middleware wraps the
195
+ // persisting channel, so both were accumulated post-rewrite.
196
+ text: persistingChannel.fullText,
197
+ steps: [
198
+ {
199
+ usage: persistingChannel.totalUsage,
200
+ toolCalls: persistingChannel.runToolCalls,
201
+ },
202
+ ],
203
+ usage: persistingChannel.totalUsage,
162
204
  });
163
205
  }
164
206
  async function runStreamStepLoop(params) {
@@ -448,6 +490,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
448
490
  if (storage) {
449
491
  await storage.saveMessages(threadId, [persistedUserMessage]);
450
492
  }
493
+ warnUnstreamedOutputHooks(agentName, aiMiddlewares, singletonServices.logger);
451
494
  const streamMiddleware = aiMiddlewares
452
495
  .filter((mw) => mw.modifyOutputStream)
453
496
  .map((mw) => {
@@ -538,7 +581,12 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
538
581
  await handleCredentialRequests(loopResult.credentialRequests, runId, channel, aiRunState, persistingChannel);
539
582
  return persistingChannel.fullText;
540
583
  }
541
- await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, runId);
584
+ await postStreamCleanup(persistingChannel, aiRunState, runId, {
585
+ agentName,
586
+ threadId,
587
+ resourceId: input.resourceId,
588
+ input: lastUserMessageText(runnerParams.messages),
589
+ });
542
590
  // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
543
591
  await outputChannel.send({ type: 'done' });
544
592
  channel.close();
@@ -766,18 +814,19 @@ export async function resumeAIAgent(input, channel, params, options) {
766
814
  }
767
815
  const toolArgs = typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args;
768
816
  let toolResult;
769
- let isError = false;
817
+ let toolError;
770
818
  try {
771
819
  toolResult = await matchingTool.execute(toolArgs);
772
820
  }
773
821
  catch (execErr) {
774
822
  if (execErr?.payload?.error === 'missing_credential') {
775
823
  toolResult = execErr.payload;
824
+ toolError = 'missing_credential';
776
825
  }
777
826
  else {
778
- toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
827
+ toolError = execErr instanceof Error ? execErr.message : String(execErr);
828
+ toolResult = `Error: ${toolError}`;
779
829
  }
780
- isError = true;
781
830
  }
782
831
  const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
783
832
  if (storage) {
@@ -801,7 +850,7 @@ export async function resumeAIAgent(input, channel, params, options) {
801
850
  toolCallId: input.toolCallId,
802
851
  toolName: pending.toolName,
803
852
  result: toolResult,
804
- ...(isError ? { isError: true } : {}),
853
+ ...(toolError ? { error: toolError } : {}),
805
854
  });
806
855
  }
807
856
  const updatedRun = await aiRunState.getRun(run.runId);
@@ -845,6 +894,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
845
894
  const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, { messages: trimmedMessages, instructions: instructions }, sharedNotes);
846
895
  // knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
847
896
  const interruptHandle = registerInterruptibleRun(run.runId);
897
+ warnUnstreamedOutputHooks(run.agentName, aiMiddlewares, singletonServices.logger);
848
898
  const streamMiddleware = aiMiddlewares
849
899
  .filter((mw) => mw.modifyOutputStream)
850
900
  .map((mw) => {
@@ -922,7 +972,10 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
922
972
  await handleCredentialRequests(loopResult.credentialRequests, run.runId, channel, aiRunState, persistingChannel);
923
973
  return;
924
974
  }
925
- await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, run.runId);
975
+ await postStreamCleanup(persistingChannel, aiRunState, run.runId, {
976
+ ...run,
977
+ input: lastUserMessageText(runnerParams.messages),
978
+ });
926
979
  // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
927
980
  await wrappedChannel.send({ type: 'done' });
928
981
  channel.close();
@@ -30,6 +30,7 @@ export declare const toAccumulatedStep: (stepResult: StepResult) => {
30
30
  outputTokens: number;
31
31
  };
32
32
  toolCalls: {
33
+ error?: string | undefined;
33
34
  name: string;
34
35
  args: Record<string, unknown>;
35
36
  result: string;
@@ -55,6 +55,7 @@ export const toAccumulatedStep = (stepResult) => ({
55
55
  result: typeof tr?.result === 'string'
56
56
  ? tr.result
57
57
  : JSON.stringify(tr?.result ?? ''),
58
+ ...(tr?.error ? { error: tr.error } : {}),
58
59
  };
59
60
  }),
60
61
  });
@@ -43,6 +43,13 @@ export interface AIToolResult {
43
43
  id: string;
44
44
  name: string;
45
45
  result: string;
46
+ /**
47
+ * Set when the tool threw rather than returned. Carried separately from
48
+ * `result`, which is a rendered string by the time it is persisted — a tool
49
+ * may legitimately return text beginning `Error:`, so the prefix cannot be
50
+ * read as a failure signal.
51
+ */
52
+ error?: string;
46
53
  }
47
54
  export interface AIMessage {
48
55
  id: string;
@@ -79,6 +86,8 @@ export interface AIAgentStep {
79
86
  name: string;
80
87
  args: Record<string, unknown>;
81
88
  result: string;
89
+ /** The failure message, when the tool threw rather than returned. */
90
+ error?: string;
82
91
  }[];
83
92
  }
84
93
  export interface AIAgentInputAttachment {
@@ -216,6 +225,21 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
216
225
  */
217
226
  signal?: AbortSignal;
218
227
  }) => Promise<AIStreamEvent | AIStreamEvent[] | null> | AIStreamEvent | AIStreamEvent[] | null;
228
+ /**
229
+ * The last chance to rewrite what the run produced, before it is persisted
230
+ * and returned.
231
+ *
232
+ * It does **not** run on a streamed run: there the text has already reached
233
+ * the client and each step is flushed to storage as it goes, so nothing could
234
+ * act on what this returned. Use {@link modifyOutputStream} to rewrite a
235
+ * streamed reply — a middleware that implements only this one is warned about
236
+ * when an agent it is attached to streams.
237
+ *
238
+ * `toolCalls` is here so a redaction pass covers the whole run record rather
239
+ * than just the visible answer: the tool arguments and results are persisted
240
+ * and handed to anything that grades the run, and scrubbing the reply alone
241
+ * leaves them untouched.
242
+ */
219
243
  modifyOutput?: (services: Services, ctx: {
220
244
  text: string;
221
245
  messages: AIMessage[];
@@ -223,12 +247,15 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
223
247
  inputTokens: number;
224
248
  outputTokens: number;
225
249
  };
250
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>;
226
251
  }) => Promise<{
227
252
  text: string;
228
253
  messages: AIMessage[];
254
+ toolCalls?: NonNullable<AIAgentStep['toolCalls']>;
229
255
  }> | {
230
256
  text: string;
231
257
  messages: AIMessage[];
258
+ toolCalls?: NonNullable<AIAgentStep['toolCalls']>;
232
259
  };
233
260
  beforeToolCall?: (services: Services, ctx: {
234
261
  toolName: string;
@@ -262,6 +289,8 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
262
289
  toolCallId: string;
263
290
  toolName: string;
264
291
  result: unknown;
292
+ /** Set when the tool threw rather than returned. */
293
+ error?: string;
265
294
  }[];
266
295
  usage: {
267
296
  inputTokens: number;
@@ -282,7 +311,7 @@ export type AIAgentMemoryConfig = {
282
311
  lastMessages?: number;
283
312
  workingMemory?: unknown;
284
313
  };
285
- export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>, Scope extends string = string> = {
314
+ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>, Scope extends string = string, Scorer extends string = string> = {
286
315
  name: string;
287
316
  description: string;
288
317
  summary?: string;
@@ -308,6 +337,16 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
308
337
  tools?: unknown[];
309
338
  agents?: unknown[];
310
339
  workflows?: unknown[];
340
+ /**
341
+ * Grades this agent's finished runs on live traffic, named by the generated
342
+ * `ScorerName` union rather than by `ref()` — a scorer is not a function, so
343
+ * there is nothing in the function map for a ref to resolve against.
344
+ *
345
+ * A reference-based judge listed here is never sampled: live traffic has no
346
+ * answer key. Scenarios name scorers directly and may grade with scorers an
347
+ * agent does not ship with.
348
+ */
349
+ scorers?: Scorer[];
311
350
  agentMode?: 'delegate' | 'supervise';
312
351
  memory?: AIAgentMemoryConfig;
313
352
  maxSteps?: number;
@@ -382,6 +421,12 @@ export type AIStreamEvent = {
382
421
  toolCallId: string;
383
422
  toolName: string;
384
423
  result: unknown;
424
+ /**
425
+ * The failure message, set when the tool threw rather than returned.
426
+ * Carried explicitly because a tool may legitimately return text that
427
+ * reads like an error, so `result` cannot be matched on to tell.
428
+ */
429
+ error?: string;
385
430
  agent?: string;
386
431
  session?: string;
387
432
  } | {
@@ -1,11 +1,12 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from './ai-agent-helpers.js';
2
- export { wrapChannelWithAGUI, type AGUIEvent } from './ai-agent-agui.js';
2
+ export { wrapChannelWithAGUI } from './ai-agent-agui.js';
3
3
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
4
+ export { resolveModelAlias } from './ai-agent-model-config.js';
4
5
  export { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
5
- export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
6
+ export { voiceInput, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
6
7
  export { voiceOutput, unspeakableScripts, voiceForText, type SpeakableScripts, } from './voice-output.js';
7
- export { AgentInterruptedError, awaitPendingInterruptNote, getInFlightTools, isAbortError, isRunInterruptible, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
8
- export type { AgentInterruption, AgentInterruptResult, InterruptibleRunHandle, OrphanedToolResult, } from './ai-agent-interrupt.js';
9
- export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, sessionPrincipals, threadOwnerConstraint, } from './ai-agent-prepare.js';
10
- export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
11
- export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
8
+ export { AgentInterruptedError, signalRunInterrupt, } from './ai-agent-interrupt.js';
9
+ export type { AgentInterruption, AgentInterruptResult, InterruptibleRunHandle, } from './ai-agent-interrupt.js';
10
+ export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './ai-agent-prepare.js';
11
+ export { addAIAgent, } from './ai-agent-registry.js';
12
+ export type { AIAgentInput, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
@@ -1,9 +1,10 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from './ai-agent-helpers.js';
2
2
  export { wrapChannelWithAGUI } from './ai-agent-agui.js';
3
3
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
4
+ export { resolveModelAlias } from './ai-agent-model-config.js';
4
5
  export { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
5
- export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
6
+ export { voiceInput, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
6
7
  export { voiceOutput, unspeakableScripts, voiceForText, } from './voice-output.js';
7
- export { AgentInterruptedError, awaitPendingInterruptNote, getInFlightTools, isAbortError, isRunInterruptible, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
8
- export { ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, sessionPrincipals, threadOwnerConstraint, } from './ai-agent-prepare.js';
9
- export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
8
+ export { AgentInterruptedError, signalRunInterrupt, } from './ai-agent-interrupt.js';
9
+ export { ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './ai-agent-prepare.js';
10
+ export { addAIAgent, } from './ai-agent-registry.js';
@@ -0,0 +1,26 @@
1
+ import type { ScoreJob, ScorerOutput } from './ai-scorer.types.js';
2
+ /**
3
+ * Grade one run with one scorer.
4
+ *
5
+ * The single path both callers take — the lane worker on live traffic and the
6
+ * scenario grading RPC — so a scenario's grade is the same computation the
7
+ * production sampler would have made, not an approximation of it.
8
+ *
9
+ * Persisting is optional because the two callers differ on it: a live grade is
10
+ * only useful once recorded, while a scenario asserts on the returned value and
11
+ * runs against servers that may have no run-state adapter at all.
12
+ */
13
+ export declare const gradeRun: (job: ScoreJob, services: {
14
+ aiAgentRunner?: unknown;
15
+ aiRunState?: {
16
+ saveScore: (score: {
17
+ runId: string;
18
+ scorerName: string;
19
+ score: number;
20
+ reason?: string;
21
+ metadata?: Record<string, unknown>;
22
+ }) => Promise<void>;
23
+ };
24
+ }, options: {
25
+ persist: boolean;
26
+ }) => Promise<ScorerOutput>;
@@ -0,0 +1,33 @@
1
+ import { runJudge } from './ai-scorer-judge.js';
2
+ import { resolveAIScorer } from './ai-scorer-registry.js';
3
+ /**
4
+ * Grade one run with one scorer.
5
+ *
6
+ * The single path both callers take — the lane worker on live traffic and the
7
+ * scenario grading RPC — so a scenario's grade is the same computation the
8
+ * production sampler would have made, not an approximation of it.
9
+ *
10
+ * Persisting is optional because the two callers differ on it: a live grade is
11
+ * only useful once recorded, while a scenario asserts on the returned value and
12
+ * runs against servers that may have no run-state adapter at all.
13
+ */
14
+ export const gradeRun = async (job, services, options) => {
15
+ const { scorerName, ...input } = job;
16
+ const scorer = resolveAIScorer(scorerName);
17
+ const result = scorer.score
18
+ ? await scorer.score(input, services)
19
+ : await runJudge(scorer, input, services.aiAgentRunner);
20
+ if (options.persist) {
21
+ if (!services.aiRunState) {
22
+ throw new Error(`AI run state service not initialized: cannot record the '${scorerName}' grade of run ${job.runId}`);
23
+ }
24
+ await services.aiRunState.saveScore({
25
+ runId: job.runId,
26
+ scorerName,
27
+ score: result.score,
28
+ ...(result.reason !== undefined ? { reason: result.reason } : {}),
29
+ ...(result.metadata !== undefined ? { metadata: result.metadata } : {}),
30
+ });
31
+ }
32
+ return result;
33
+ };
@@ -0,0 +1,17 @@
1
+ import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js';
2
+ import type { PikkuAIScorer, ScorerInput, ScorerJudgeConfig, ScorerOutput } from './ai-scorer.types.js';
3
+ /**
4
+ * The message a judge grades when its scorer supplies no `prompt`.
5
+ *
6
+ * A reference-based judge is shown the answer key; a reference-free one is not,
7
+ * and grades the answer on its own merits.
8
+ */
9
+ export declare const buildJudgePrompt: (judge: ScorerJudgeConfig, input: ScorerInput) => string;
10
+ /**
11
+ * Run an LLM judge and force the `{ score, reason }` contract, so a scorer
12
+ * author writes a rubric rather than a parser.
13
+ *
14
+ * A judge has no tools, which is the one case where an output schema is
15
+ * reliably honoured.
16
+ */
17
+ export declare const runJudge: (scorer: PikkuAIScorer<any>, input: ScorerInput, aiAgentRunner: AIAgentRunnerService | undefined) => Promise<ScorerOutput>;