@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
@@ -0,0 +1,63 @@
1
+ import type { CredentialService } from './credential-service.js'
2
+
3
+ /**
4
+ * A `CredentialService` narrowed to the credentials a package declared.
5
+ *
6
+ * Unlike `ScopedSecretService` this stays writable — an addon refreshing its
7
+ * own OAuth token is the normal case. What it cannot do is reach a credential
8
+ * it never declared, or enumerate the app's users.
9
+ */
10
+ export class ScopedCredentialService implements CredentialService {
11
+ constructor(
12
+ private credentials: CredentialService,
13
+ private allowedNames: Set<string>
14
+ ) {}
15
+
16
+ private assertAllowed(name: string): void {
17
+ if (!this.allowedNames.has(name)) {
18
+ throw new Error(`Access denied to credential: ${name}`)
19
+ }
20
+ }
21
+
22
+ async get<T = unknown>(name: string, userId?: string): Promise<T | null> {
23
+ this.assertAllowed(name)
24
+ return this.credentials.get<T>(name, userId)
25
+ }
26
+
27
+ async set(name: string, value: unknown, userId?: string): Promise<void> {
28
+ this.assertAllowed(name)
29
+ return this.credentials.set(name, value, userId)
30
+ }
31
+
32
+ async delete(name: string, userId?: string): Promise<void> {
33
+ this.assertAllowed(name)
34
+ return this.credentials.delete(name, userId)
35
+ }
36
+
37
+ async has(name: string, userId?: string): Promise<boolean> {
38
+ this.assertAllowed(name)
39
+ return this.credentials.has(name, userId)
40
+ }
41
+
42
+ async getAll(userId: string): Promise<Record<string, unknown>> {
43
+ const all = await this.credentials.getAll(userId)
44
+ const scoped: Record<string, unknown> = {}
45
+ for (const name of this.allowedNames) {
46
+ if (name in all) {
47
+ scoped[name] = all[name]
48
+ }
49
+ }
50
+ return scoped
51
+ }
52
+
53
+ async getUsersWithCredential(name: string): Promise<string[]> {
54
+ this.assertAllowed(name)
55
+ return this.credentials.getUsersWithCredential(name)
56
+ }
57
+
58
+ async getAllUsers(): Promise<string[]> {
59
+ throw new Error(
60
+ 'Access denied: enumerating users is not allowed in a scoped credential service'
61
+ )
62
+ }
63
+ }
@@ -216,6 +216,99 @@ export const defineAiStorageServiceTests = (
216
216
  assert.ok(runs.every((r) => r.threadId === thread.id))
217
217
  })
218
218
 
219
+ test('saveScore and getScores', async () => {
220
+ const thread = await storage.createThread('resource-score')
221
+ const now = new Date()
222
+
223
+ const runId = await storage.createRun({
224
+ agentName: 'scored-agent',
225
+ threadId: thread.id,
226
+ resourceId: 'resource-score',
227
+ status: 'completed',
228
+ usage: { inputTokens: 10, outputTokens: 5, model: 'test' },
229
+ createdAt: now,
230
+ updatedAt: now,
231
+ })
232
+
233
+ await storage.saveScore({
234
+ runId,
235
+ scorerName: 'tool-error-rate',
236
+ score: 1,
237
+ })
238
+ await storage.saveScore({
239
+ runId,
240
+ scorerName: 'helpfulness',
241
+ score: 0.75,
242
+ reason: 'answered the question but skipped the fee',
243
+ metadata: { subScores: { accuracy: 0.9 }, judgeTokens: 412 },
244
+ })
245
+
246
+ const scores = await storage.getScores(runId)
247
+ assert.equal(scores.length, 2)
248
+
249
+ const byName = new Map(scores.map((s) => [s.scorerName, s]))
250
+
251
+ const clean = byName.get('tool-error-rate')
252
+ assert.ok(clean)
253
+ assert.equal(clean.score, 1)
254
+ assert.equal(clean.reason, undefined)
255
+ assert.equal(clean.metadata, undefined)
256
+
257
+ const helpful = byName.get('helpfulness')
258
+ assert.ok(helpful)
259
+ // A fractional score has to survive the round trip: an integer column
260
+ // would quietly turn every grade into 0 or 1.
261
+ assert.equal(helpful.score, 0.75)
262
+ assert.equal(helpful.reason, 'answered the question but skipped the fee')
263
+ assert.deepEqual(helpful.metadata, {
264
+ subScores: { accuracy: 0.9 },
265
+ judgeTokens: 412,
266
+ })
267
+ assert.ok(helpful.createdAt instanceof Date)
268
+ })
269
+
270
+ test('a re-grade appends rather than replacing the grade that was acted on', async () => {
271
+ const thread = await storage.createThread('resource-regrade')
272
+ const now = new Date()
273
+
274
+ const runId = await storage.createRun({
275
+ agentName: 'regraded-agent',
276
+ threadId: thread.id,
277
+ resourceId: 'resource-regrade',
278
+ status: 'completed',
279
+ usage: { inputTokens: 1, outputTokens: 1, model: 'test' },
280
+ createdAt: now,
281
+ updatedAt: now,
282
+ })
283
+
284
+ await storage.saveScore({ runId, scorerName: 'helpfulness', score: 0.2 })
285
+ await storage.saveScore({ runId, scorerName: 'helpfulness', score: 0.8 })
286
+
287
+ const scores = await storage.getScores(runId)
288
+ assert.equal(scores.length, 2)
289
+ assert.deepEqual(
290
+ scores.map((s) => s.score),
291
+ [0.2, 0.8]
292
+ )
293
+ })
294
+
295
+ test('getScores is empty for a run nothing graded', async () => {
296
+ const thread = await storage.createThread('resource-ungraded')
297
+ const now = new Date()
298
+
299
+ const runId = await storage.createRun({
300
+ agentName: 'ungraded-agent',
301
+ threadId: thread.id,
302
+ resourceId: 'resource-ungraded',
303
+ status: 'completed',
304
+ usage: { inputTokens: 1, outputTokens: 1, model: 'test' },
305
+ createdAt: now,
306
+ updatedAt: now,
307
+ })
308
+
309
+ assert.deepEqual(await storage.getScores(runId), [])
310
+ })
311
+
219
312
  test('resolveApproval', async () => {
220
313
  const thread = await storage.createThread('resource-8')
221
314
  const now = new Date()
@@ -188,8 +188,6 @@ export type FunctionRuntimeMeta = {
188
188
  audit?: {
189
189
  durability: AuditDurability
190
190
  }
191
- /** Keeps the full `SecretService`. Set by the inspector, read by the runner. */
192
- secretBroker?: boolean
193
191
  version?: number
194
192
  approvalRequired?: boolean
195
193
  approvalDescription?: string
@@ -383,10 +381,9 @@ export type PikkuWire<
383
381
  MCPTools extends string | never = never,
384
382
  TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
385
383
  TriggerOutput = unknown,
386
- // Defaulted to `any` rather than to `Out`: the emitted `TypedScenario<Out>`
387
- // supplies the real context, while `PikkuWire`'s own defaults are what other
388
- // generics constrain against — and a constraint that pinned the context to
389
- // `Out` would reject every wire whose scenario output is anything else.
384
+ // `any`, not `Out`: the emitted `TypedScenario<Out>` supplies the real
385
+ // context, and this default is what other generics constrain against. See
386
+ // `ScenarioContext`.
390
387
  TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>,
391
388
  TypedActors extends ScenarioPersonas = ScenarioPersonas,
392
389
  > = {
@@ -29,6 +29,10 @@ import type {
29
29
  CoreAIAgent,
30
30
  AIAgentMeta,
31
31
  } from '../wirings/ai-agent/ai-agent.types.js'
32
+ import type {
33
+ PikkuAIScorer,
34
+ ScorerMeta,
35
+ } from '../wirings/ai-scorer/ai-scorer.types.js'
32
36
  import type {
33
37
  CoreGateway,
34
38
  GatewaysMeta,
@@ -98,6 +102,10 @@ export interface PikkuPackageState {
98
102
  variableOverrides?: Record<string, string>
99
103
  /** Per-instance name-aliases: logical name the addon reads -> actual project credential name */
100
104
  credentialOverrides?: Record<string, string>
105
+ /** Why this instance gets the whole `SecretService` rather than one scoped to its declared secrets */
106
+ globalSecrets?: string
107
+ /** Why this instance gets the whole `CredentialService` rather than one scoped to its declared credentials */
108
+ globalCredentials?: string
101
109
  /** Set by `wireRemoteAddon`: this namespace is consumed remotely over HTTP, not bundled */
102
110
  remote?: boolean
103
111
  /** Remote host base URL (wireRemoteAddon) — string or resolver over singleton services */
@@ -153,6 +161,10 @@ export interface PikkuPackageState {
153
161
  agent: {
154
162
  agents: Map<string, CoreAIAgent>
155
163
  agentsMeta: AIAgentMeta
164
+ scorers: Map<string, PikkuAIScorer>
165
+ scorersMeta: ScorerMeta
166
+ /** Alias -> `provider/model`, from the `models` table in pikku.config.json. */
167
+ modelAliases: Record<string, string>
156
168
  }
157
169
  gateway: {
158
170
  gateways: Map<string, CoreGateway>
@@ -204,5 +216,9 @@ export interface PikkuPackageState {
204
216
  { name: string; displayName: string; type: string; oauth2?: boolean }
205
217
  > | null
206
218
  requiredParentServices: string[] | null
219
+ /** The secret keys this package is allowed to read, emitted by the CLI from
220
+ * what the package declares and is seen to read. `null` when the package
221
+ * was never analysed, which scopes it to nothing. */
222
+ declaredSecrets: string[] | null
207
223
  }
208
224
  }
@@ -1,9 +1,6 @@
1
1
  export type {
2
- ActorFlowApprovalPolicy,
3
2
  ActorFlowVerdict,
4
3
  ConverseOptions,
5
- TargetAgentReply,
6
- TargetPendingApproval,
7
4
  TargetAgentDriver,
8
5
  } from './actor-flow.types.js'
9
6
  export {
@@ -0,0 +1,186 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ applyOutputMiddleware,
5
+ finalizeAgentRun,
6
+ type FinalizedRun,
7
+ } from './ai-agent-finalize.js'
8
+ import type { AIAgentStep, PikkuAIMiddlewareHooks } from './ai-agent.types.js'
9
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
10
+
11
+ const step = (
12
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>
13
+ ): AIAgentStep => ({
14
+ usage: { inputTokens: 0, outputTokens: 0 },
15
+ toolCalls,
16
+ })
17
+
18
+ const call = (name: string, result: string) => ({
19
+ name,
20
+ args: {} as Record<string, unknown>,
21
+ result,
22
+ })
23
+
24
+ describe('applyOutputMiddleware', () => {
25
+ test('hands the middleware every tool call the run made, flattened across steps', async () => {
26
+ const seen: string[][] = []
27
+ const middleware: PikkuAIMiddlewareHooks[] = [
28
+ {
29
+ modifyOutput: (_services, ctx) => {
30
+ seen.push(ctx.toolCalls.map((c) => c.name))
31
+ return { text: ctx.text, messages: ctx.messages }
32
+ },
33
+ },
34
+ ]
35
+
36
+ await applyOutputMiddleware(
37
+ middleware,
38
+ {},
39
+ {
40
+ text: 'done',
41
+ messages: [],
42
+ steps: [step([call('search', 'a')]), step([call('fetch', 'b')])],
43
+ usage: { inputTokens: 1, outputTokens: 2 },
44
+ }
45
+ )
46
+
47
+ assert.deepEqual(seen, [['search', 'fetch']])
48
+ })
49
+
50
+ test('a rewritten tool call is redistributed back onto the step it came from', async () => {
51
+ const middleware: PikkuAIMiddlewareHooks[] = [
52
+ {
53
+ modifyOutput: (_services, ctx) => ({
54
+ text: ctx.text,
55
+ messages: ctx.messages,
56
+ toolCalls: ctx.toolCalls.map((c) => ({ ...c, result: '[redacted]' })),
57
+ }),
58
+ },
59
+ ]
60
+
61
+ const { steps } = await applyOutputMiddleware(
62
+ middleware,
63
+ {},
64
+ {
65
+ text: 'done',
66
+ messages: [],
67
+ steps: [
68
+ step([call('search', 'secret')]),
69
+ step([call('fetch', 'token')]),
70
+ ],
71
+ usage: { inputTokens: 0, outputTokens: 0 },
72
+ }
73
+ )
74
+
75
+ assert.deepEqual(
76
+ steps.map((s) => s.toolCalls?.map((c) => [c.name, c.result])),
77
+ [[['search', '[redacted]']], [['fetch', '[redacted]']]]
78
+ )
79
+ })
80
+
81
+ test('dropping a tool call collapses the calls into the last step rather than mis-attributing them', async () => {
82
+ const middleware: PikkuAIMiddlewareHooks[] = [
83
+ {
84
+ modifyOutput: (_services, ctx) => ({
85
+ text: ctx.text,
86
+ messages: ctx.messages,
87
+ toolCalls: ctx.toolCalls.filter((c) => c.name !== 'search'),
88
+ }),
89
+ },
90
+ ]
91
+
92
+ const { steps } = await applyOutputMiddleware(
93
+ middleware,
94
+ {},
95
+ {
96
+ text: 'done',
97
+ messages: [],
98
+ steps: [step([call('search', 'a')]), step([call('fetch', 'b')])],
99
+ usage: { inputTokens: 0, outputTokens: 0 },
100
+ }
101
+ )
102
+
103
+ assert.deepEqual(
104
+ steps.map((s) => s.toolCalls?.map((c) => c.name)),
105
+ [[], ['fetch']]
106
+ )
107
+ })
108
+
109
+ test('runs the chain in reverse registration order, each hook seeing the previous rewrite', async () => {
110
+ const order: string[] = []
111
+ const middleware: PikkuAIMiddlewareHooks[] = [
112
+ {
113
+ modifyOutput: (_services, ctx) => {
114
+ order.push(`first saw ${ctx.text}`)
115
+ return { text: `${ctx.text}+first`, messages: ctx.messages }
116
+ },
117
+ },
118
+ {
119
+ modifyOutput: (_services, ctx) => {
120
+ order.push(`second saw ${ctx.text}`)
121
+ return { text: `${ctx.text}+second`, messages: ctx.messages }
122
+ },
123
+ },
124
+ ]
125
+
126
+ const { text } = await applyOutputMiddleware(
127
+ middleware,
128
+ {},
129
+ {
130
+ text: 'raw',
131
+ messages: [],
132
+ steps: [],
133
+ usage: { inputTokens: 0, outputTokens: 0 },
134
+ }
135
+ )
136
+
137
+ assert.deepEqual(order, ['second saw raw', 'first saw raw+second'])
138
+ assert.equal(text, 'raw+second+first')
139
+ })
140
+ })
141
+
142
+ describe('finalizeAgentRun', () => {
143
+ const run = (usage: FinalizedRun['usage']): FinalizedRun => ({
144
+ runId: 'run-1',
145
+ agentName: 'assistant',
146
+ threadId: 'thread-1',
147
+ input: 'what is the capital of France?',
148
+ text: 'done',
149
+ steps: [],
150
+ usage,
151
+ })
152
+
153
+ test('completes the run and records its usage', async () => {
154
+ const updates: unknown[] = []
155
+ const aiRunState = {
156
+ updateRun: async (_runId: string, update: unknown) => {
157
+ updates.push(update)
158
+ },
159
+ } as unknown as AIRunStateService
160
+
161
+ await finalizeAgentRun(
162
+ aiRunState,
163
+ run({ inputTokens: 10, outputTokens: 20, model: 'gpt-4o' })
164
+ )
165
+
166
+ assert.deepEqual(updates, [
167
+ {
168
+ status: 'completed',
169
+ usage: { inputTokens: 10, outputTokens: 20, model: 'gpt-4o' },
170
+ },
171
+ ])
172
+ })
173
+
174
+ test('omits usage entirely when no model reported one', async () => {
175
+ const updates: any[] = []
176
+ const aiRunState = {
177
+ updateRun: async (_runId: string, update: unknown) => {
178
+ updates.push(update)
179
+ },
180
+ } as unknown as AIRunStateService
181
+
182
+ await finalizeAgentRun(aiRunState, run({ inputTokens: 0, outputTokens: 0 }))
183
+
184
+ assert.deepEqual(updates, [{ status: 'completed' }])
185
+ })
186
+ })
@@ -0,0 +1,197 @@
1
+ import type {
2
+ AIAgentStep,
3
+ AIMessage,
4
+ PikkuAIMiddlewareHooks,
5
+ } from './ai-agent.types.js'
6
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
7
+ import { pikkuState } from '../../pikku-state.js'
8
+ import { scoreFinishedRun } from '../ai-scorer/ai-scorer-live.js'
9
+ import { recordScoreSnapshot } from '../ai-scorer/ai-scorer-snapshots.js'
10
+
11
+ export type RunUsage = {
12
+ inputTokens: number
13
+ outputTokens: number
14
+ model?: string
15
+ }
16
+
17
+ /**
18
+ * Everything a finished run produced, after all middleware has had its say.
19
+ *
20
+ * One snapshot feeds persistence and anything that grades the run, so that what
21
+ * a scorer is shown is exactly what was stored — including the redactions.
22
+ */
23
+ export type FinalizedRun = {
24
+ runId: string
25
+ agentName: string
26
+ threadId: string
27
+ resourceId?: string
28
+ /** The prompt the run answered — what a scorer grades the answer against. */
29
+ input: string
30
+ text: string
31
+ steps: AIAgentStep[]
32
+ usage: RunUsage
33
+ }
34
+
35
+ /**
36
+ * The prompt a run answered: the most recent user turn, which is what the model
37
+ * was last asked. On a resumed run the earlier turns are context, not the ask.
38
+ */
39
+ export const lastUserMessageText = (messages: AIMessage[]): string => {
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const message = messages[i]
42
+ if (message?.role !== 'user') continue
43
+ if (typeof message.content === 'string') return message.content
44
+ if (Array.isArray(message.content)) {
45
+ return message.content
46
+ .filter((part) => part.type === 'text')
47
+ .map((part) => (part as { text: string }).text)
48
+ .join('\n')
49
+ }
50
+ }
51
+ return ''
52
+ }
53
+
54
+ const flattenToolCalls = (
55
+ steps: AIAgentStep[]
56
+ ): NonNullable<AIAgentStep['toolCalls']> =>
57
+ steps.flatMap((step) => step.toolCalls ?? [])
58
+
59
+ /**
60
+ * Push a rewritten flat tool-call list back onto the steps it came from,
61
+ * preserving the step boundaries. A hook that adds or drops calls would make
62
+ * the boundaries meaningless, so a length change collapses them into the last
63
+ * step rather than silently mis-attributing calls to the wrong step.
64
+ */
65
+ const distributeToolCalls = (
66
+ steps: AIAgentStep[],
67
+ toolCalls: NonNullable<AIAgentStep['toolCalls']>
68
+ ): AIAgentStep[] => {
69
+ if (toolCalls.length !== flattenToolCalls(steps).length) {
70
+ return steps.map((step, index) => ({
71
+ ...step,
72
+ toolCalls: index === steps.length - 1 ? toolCalls : [],
73
+ }))
74
+ }
75
+
76
+ let cursor = 0
77
+ return steps.map((step) => {
78
+ if (!step.toolCalls) return step
79
+ const next = toolCalls.slice(cursor, cursor + step.toolCalls.length)
80
+ cursor += step.toolCalls.length
81
+ return { ...step, toolCalls: next }
82
+ })
83
+ }
84
+
85
+ /**
86
+ * Run the `modifyOutput` chain over a finished non-streaming run.
87
+ *
88
+ * Reverse order, matching the input chain: the middleware registered first
89
+ * wraps the others, so it sees the output last.
90
+ */
91
+ export const applyOutputMiddleware = async (
92
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
93
+ singletonServices: any,
94
+ input: {
95
+ text: string
96
+ messages: AIMessage[]
97
+ steps: AIAgentStep[]
98
+ usage: { inputTokens: number; outputTokens: number }
99
+ }
100
+ ): Promise<{ text: string; messages: AIMessage[]; steps: AIAgentStep[] }> => {
101
+ let text = input.text
102
+ let messages = input.messages
103
+ let steps = input.steps
104
+ let toolCalls = flattenToolCalls(steps)
105
+
106
+ for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
107
+ const mw = aiMiddlewares[i]
108
+ if (!mw.modifyOutput) continue
109
+ const result = await mw.modifyOutput(singletonServices, {
110
+ text,
111
+ messages,
112
+ usage: {
113
+ inputTokens: input.usage.inputTokens,
114
+ outputTokens: input.usage.outputTokens,
115
+ },
116
+ toolCalls,
117
+ })
118
+ text = result.text
119
+ messages = result.messages
120
+ if (result.toolCalls) {
121
+ toolCalls = result.toolCalls
122
+ steps = distributeToolCalls(steps, toolCalls)
123
+ }
124
+ }
125
+
126
+ return { text, messages, steps }
127
+ }
128
+
129
+ /**
130
+ * The one place a run ends successfully.
131
+ *
132
+ * Every path that completes a run — streamed, non-streamed, and resumed after a
133
+ * tool approval — goes through here, so that anything terminal is reachable
134
+ * from all of them and cannot be reordered by a middleware author. Nothing here
135
+ * may rewrite the run: by this point the output middleware has resolved and, on
136
+ * the streaming path, the client already has the reply.
137
+ */
138
+ export const finalizeAgentRun = async (
139
+ aiRunState: AIRunStateService,
140
+ run: FinalizedRun
141
+ ): Promise<void> => {
142
+ await aiRunState.updateRun(run.runId, {
143
+ status: 'completed',
144
+ ...(run.usage.model
145
+ ? {
146
+ usage: {
147
+ inputTokens: run.usage.inputTokens,
148
+ outputTokens: run.usage.outputTokens,
149
+ model: run.usage.model,
150
+ },
151
+ }
152
+ : {}),
153
+ })
154
+
155
+ // Read rather than `getSingletonServices()`: a process that never registered
156
+ // them grades nothing, which is not an error at the point a run has already
157
+ // succeeded.
158
+ const services = pikkuState(null, 'package', 'singletonServices')
159
+ if (!services) return
160
+
161
+ const snapshot = {
162
+ runId: run.runId,
163
+ agentName: run.agentName,
164
+ threadId: run.threadId,
165
+ ...(run.resourceId !== undefined ? { resourceId: run.resourceId } : {}),
166
+ input: run.input,
167
+ output: run.text,
168
+ toolCalls: run.steps.flatMap((step) =>
169
+ (step.toolCalls ?? []).map((call) => ({
170
+ name: call.name,
171
+ args: call.args,
172
+ result: call.result,
173
+ ...(call.error !== undefined ? { error: call.error } : {}),
174
+ }))
175
+ ),
176
+ usage: {
177
+ inputTokens: run.usage.inputTokens,
178
+ outputTokens: run.usage.outputTokens,
179
+ ...(run.usage.model !== undefined ? { model: run.usage.model } : {}),
180
+ },
181
+ }
182
+
183
+ // The same object a scenario grades, so an asserted score and a sampled one
184
+ // are the same measurement. A no-op unless a dev server turned retention on.
185
+ recordScoreSnapshot(snapshot)
186
+
187
+ // Best-effort and last: the client already has its answer, so a grading
188
+ // failure must not surface as a failed run.
189
+ try {
190
+ await scoreFinishedRun(snapshot, services)
191
+ } catch (error) {
192
+ services.logger?.error(
193
+ `[pikku] Live scoring failed for run ${run.runId}`,
194
+ error
195
+ )
196
+ }
197
+ }
@@ -381,6 +381,7 @@ export const persistOrphanedToolResults = async (
381
381
  : typeof entry.result === 'string'
382
382
  ? entry.result
383
383
  : JSON.stringify(entry.result),
384
+ ...(entry.error ? { error: entry.error } : {}),
384
385
  })),
385
386
  undelivered: true,
386
387
  createdAt: new Date(),