@pikku/core 0.12.80 → 0.12.83

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 (237) hide show
  1. package/CHANGELOG.md +345 -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 -16
  14. package/dist/services/index.js +5 -5
  15. package/dist/services/meta-service.d.ts +2 -1
  16. package/dist/services/scoped-credential-service.d.ts +21 -0
  17. package/dist/services/scoped-credential-service.js +53 -0
  18. package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
  19. package/dist/types/core.types.d.ts +2 -3
  20. package/dist/types/state.types.d.ts +19 -1
  21. package/dist/wirings/actor-flow/index.d.ts +1 -1
  22. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  23. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  24. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  25. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  26. package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
  27. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  28. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
  30. package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
  31. package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
  32. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
  33. package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
  35. package/dist/wirings/ai-agent/index.d.ts +8 -7
  36. package/dist/wirings/ai-agent/index.js +5 -4
  37. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  38. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  39. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  40. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  41. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  42. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  43. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  44. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  45. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  46. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  47. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  48. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  49. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  50. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  51. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  52. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  53. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  54. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  55. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  56. package/dist/wirings/ai-scorer/index.js +5 -0
  57. package/dist/wirings/channel/index.d.ts +5 -6
  58. package/dist/wirings/channel/index.js +3 -4
  59. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  60. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  61. package/dist/wirings/cli/channel/index.d.ts +1 -2
  62. package/dist/wirings/cli/channel/index.js +0 -1
  63. package/dist/wirings/cli/cli-runner.js +13 -1
  64. package/dist/wirings/credential/index.d.ts +1 -1
  65. package/dist/wirings/gateway/index.d.ts +1 -1
  66. package/dist/wirings/http/http-runner.js +8 -2
  67. package/dist/wirings/http/index.d.ts +1 -2
  68. package/dist/wirings/mcp/index.d.ts +1 -1
  69. package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
  70. package/dist/wirings/mcp/mcp-runner.js +18 -5
  71. package/dist/wirings/persona/index.d.ts +3 -4
  72. package/dist/wirings/persona/index.js +2 -3
  73. package/dist/wirings/queue/index.d.ts +1 -3
  74. package/dist/wirings/queue/index.js +1 -3
  75. package/dist/wirings/rpc/addon-runner.d.ts +8 -0
  76. package/dist/wirings/rpc/addon-runner.js +31 -3
  77. package/dist/wirings/rpc/rpc-runner.js +4 -0
  78. package/dist/wirings/rpc/rpc-types.d.ts +8 -0
  79. package/dist/wirings/rpc/wire-addon.d.ts +25 -0
  80. package/dist/wirings/rpc/wire-addon.js +8 -0
  81. package/dist/wirings/scheduler/index.d.ts +1 -1
  82. package/dist/wirings/trigger/index.d.ts +1 -1
  83. package/dist/wirings/virtual-user/index.d.ts +5 -6
  84. package/dist/wirings/virtual-user/index.js +2 -4
  85. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
  86. package/dist/wirings/workflow/feature.d.ts +2 -1
  87. package/dist/wirings/workflow/index.d.ts +5 -16
  88. package/dist/wirings/workflow/index.js +1 -9
  89. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
  90. package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
  91. package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
  92. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  93. package/dist/wirings/workflow/scenario.types.d.ts +37 -0
  94. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  95. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  96. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  97. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  98. package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
  99. package/dist/wirings/workflow/workflow-approval.js +65 -2
  100. package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
  101. package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
  102. package/dist/wirings/workflow/workflow.types.d.ts +2 -37
  103. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  104. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  105. package/knowledge/decisions/internals/index.md +30 -3
  106. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  107. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  108. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
  109. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  110. package/knowledge/decisions/security/index.md +3 -1
  111. package/knowledge/questions/index.md +1 -1
  112. package/package.json +3 -2
  113. package/scripts/generate-api-report.mts +143 -18
  114. package/src/api-report.test.ts +2 -2
  115. package/src/errors/index.ts +1 -1
  116. package/src/function/function-runner.test.ts +52 -0
  117. package/src/function/function-runner.ts +5 -9
  118. package/src/function/index.ts +0 -2
  119. package/src/index.ts +0 -35
  120. package/src/pikku-state.ts +5 -0
  121. package/src/public-surface.json +81 -118
  122. package/src/services/ai-agent-runner-service.ts +12 -1
  123. package/src/services/ai-run-state-service.ts +11 -0
  124. package/src/services/in-memory-ai-run-state-service.ts +13 -0
  125. package/src/services/index.ts +7 -58
  126. package/src/services/meta-service.ts +2 -4
  127. package/src/services/scoped-credential-service.test.ts +86 -0
  128. package/src/services/scoped-credential-service.ts +63 -0
  129. package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
  130. package/src/types/core.types.ts +4 -7
  131. package/src/types/state.types.ts +21 -1
  132. package/src/wirings/actor-flow/index.ts +0 -3
  133. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  134. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  135. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  136. package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
  137. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  138. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  139. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
  140. package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
  141. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  142. package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
  143. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  144. package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
  145. package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
  146. package/src/wirings/ai-agent/index.ts +2 -16
  147. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  148. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  149. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  150. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  151. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  152. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  153. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  154. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  155. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  156. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  157. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  158. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  159. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  160. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  161. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  162. package/src/wirings/ai-scorer/index.ts +24 -0
  163. package/src/wirings/channel/index.ts +1 -20
  164. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  165. package/src/wirings/channel/local/local-channel-runner.ts +8 -1
  166. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  167. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  168. package/src/wirings/cli/channel/index.ts +0 -7
  169. package/src/wirings/cli/cli-runner.test.ts +68 -0
  170. package/src/wirings/cli/cli-runner.ts +18 -1
  171. package/src/wirings/credential/index.ts +0 -1
  172. package/src/wirings/gateway/index.ts +0 -3
  173. package/src/wirings/http/http-runner.test.ts +66 -0
  174. package/src/wirings/http/http-runner.ts +10 -2
  175. package/src/wirings/http/index.ts +1 -1
  176. package/src/wirings/mcp/index.ts +0 -1
  177. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  178. package/src/wirings/mcp/mcp-runner.ts +35 -5
  179. package/src/wirings/persona/index.ts +0 -8
  180. package/src/wirings/queue/index.ts +0 -14
  181. package/src/wirings/rpc/addon-runner.ts +62 -3
  182. package/src/wirings/rpc/addon-secrets.test.ts +391 -0
  183. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  184. package/src/wirings/rpc/rpc-runner.ts +4 -0
  185. package/src/wirings/rpc/rpc-types.ts +8 -0
  186. package/src/wirings/rpc/wire-addon.ts +33 -0
  187. package/src/wirings/scheduler/index.ts +0 -1
  188. package/src/wirings/trigger/index.ts +0 -1
  189. package/src/wirings/virtual-user/index.ts +0 -16
  190. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
  191. package/src/wirings/workflow/feature.ts +2 -5
  192. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  193. package/src/wirings/workflow/index.ts +2 -68
  194. package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
  195. package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
  196. package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
  197. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  198. package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
  199. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  200. package/src/wirings/workflow/scenario.types.ts +63 -0
  201. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  202. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  203. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  204. package/src/wirings/workflow/workflow-approval.ts +113 -9
  205. package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
  206. package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
  207. package/src/wirings/workflow/workflow.types.ts +1 -63
  208. package/src/wirings-stay-decoupled.test.ts +6 -2
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/dist/internal.d.ts +0 -3
  211. package/dist/internal.js +0 -2
  212. package/dist/middleware/timeout.d.ts +0 -9
  213. package/dist/middleware/timeout.js +0 -15
  214. package/dist/pikku-response.d.ts +0 -6
  215. package/dist/pikku-response.js +0 -6
  216. package/dist/services/gopass-secrets.d.ts +0 -15
  217. package/dist/services/gopass-secrets.js +0 -76
  218. package/dist/services/http-scenario-actors.d.ts +0 -75
  219. package/dist/services/http-scenario-actors.js +0 -195
  220. package/dist/services/http-user-flow-actors.d.ts +0 -67
  221. package/dist/services/http-user-flow-actors.js +0 -193
  222. package/dist/services/scenario-actors-service.d.ts +0 -127
  223. package/dist/services/scenario-actors-service.js +0 -40
  224. package/dist/services/user-flow-actors-service.d.ts +0 -39
  225. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  226. package/dist/wirings/credential/wire-credential.js +0 -47
  227. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  228. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  229. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  230. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  231. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  232. package/dist/wirings/scope/wire-scope.js +0 -32
  233. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  234. package/dist/wirings/workflow/dsl/index.js +0 -4
  235. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  236. package/dist/wirings/workflow/graph/index.js +0 -4
  237. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
@@ -0,0 +1,86 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { ScopedCredentialService } from './scoped-credential-service.js'
5
+ import type { CredentialService } from './credential-service.js'
6
+
7
+ const createCredentialService = (): CredentialService => {
8
+ const store: Record<string, Record<string, unknown>> = {
9
+ 'user-1': { slack: 'slack-token', stripe: 'stripe-key' },
10
+ 'user-2': { slack: 'other-slack-token' },
11
+ }
12
+ return {
13
+ get: async (name, userId = 'user-1') =>
14
+ (store[userId]?.[name] ?? null) as never,
15
+ set: async (name, value, userId = 'user-1') => {
16
+ store[userId] = { ...store[userId], [name]: value }
17
+ },
18
+ delete: async (name, userId = 'user-1') => {
19
+ delete store[userId]?.[name]
20
+ },
21
+ has: async (name, userId = 'user-1') => name in (store[userId] ?? {}),
22
+ getAll: async (userId) => store[userId] ?? {},
23
+ getUsersWithCredential: async (name) =>
24
+ Object.keys(store).filter((userId) => name in store[userId]!),
25
+ getAllUsers: async () => Object.keys(store),
26
+ }
27
+ }
28
+
29
+ const scoped = (names: string[]) =>
30
+ new ScopedCredentialService(createCredentialService(), new Set(names))
31
+
32
+ describe('ScopedCredentialService', () => {
33
+ test('a declared credential is readable and writable', async () => {
34
+ const credentials = scoped(['slack'])
35
+
36
+ assert.equal(await credentials.get('slack'), 'slack-token')
37
+ assert.equal(await credentials.has('slack'), true)
38
+ await credentials.set('slack', 'refreshed')
39
+ assert.equal(await credentials.get('slack'), 'refreshed')
40
+ await credentials.delete('slack')
41
+ assert.equal(await credentials.get('slack'), null)
42
+ })
43
+
44
+ test('an undeclared credential is denied on every accessor', async () => {
45
+ const credentials = scoped(['slack'])
46
+
47
+ await assert.rejects(() => credentials.get('stripe'), /denied/i)
48
+ await assert.rejects(() => credentials.has('stripe'), /denied/i)
49
+ await assert.rejects(() => credentials.set('stripe', 'x'), /denied/i)
50
+ await assert.rejects(() => credentials.delete('stripe'), /denied/i)
51
+ await assert.rejects(
52
+ () => credentials.getUsersWithCredential('stripe'),
53
+ /denied/i
54
+ )
55
+ })
56
+
57
+ test('getAll returns only the declared credentials, not the whole user', async () => {
58
+ const credentials = scoped(['slack'])
59
+
60
+ assert.deepEqual(await credentials.getAll('user-1'), {
61
+ slack: 'slack-token',
62
+ })
63
+ })
64
+
65
+ test('enumerating every user of the app is denied outright', async () => {
66
+ const credentials = scoped(['slack'])
67
+
68
+ await assert.rejects(() => credentials.getAllUsers(), /denied/i)
69
+ })
70
+
71
+ test('the users of a declared credential are still visible', async () => {
72
+ const credentials = scoped(['slack'])
73
+
74
+ assert.deepEqual(await credentials.getUsersWithCredential('slack'), [
75
+ 'user-1',
76
+ 'user-2',
77
+ ])
78
+ })
79
+
80
+ test('an addon that declares nothing reaches nothing', async () => {
81
+ const credentials = scoped([])
82
+
83
+ await assert.rejects(() => credentials.get('slack'), /denied/i)
84
+ assert.deepEqual(await credentials.getAll('user-1'), {})
85
+ })
86
+ })
@@ -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()
@@ -17,11 +17,11 @@ import type { PikkuQueue, QueueService } from '../wirings/queue/queue.types.js'
17
17
  import type { PikkuCLI } from '../wirings/cli/cli.types.js'
18
18
  import type {
19
19
  PikkuWorkflowWire,
20
- PikkuScenarioWire,
21
20
  WorkflowService,
22
21
  WorkflowServiceConfig,
23
22
  WorkflowStepWire,
24
23
  } from '../wirings/workflow/workflow.types.js'
24
+ import type { PikkuScenarioWire } from '../wirings/workflow/scenario.types.js'
25
25
  import type {
26
26
  PikkuBrowserWire,
27
27
  PikkuScenarioStepWire,
@@ -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,
@@ -43,9 +47,9 @@ import type {
43
47
  } from '../wirings/scheduler/scheduler.types.js'
44
48
  import type {
45
49
  CoreWorkflow,
46
- CoreFeature,
47
50
  WorkflowsRuntimeMeta,
48
51
  } from '../wirings/workflow/workflow.types.js'
52
+ import type { CoreFeature } from '../wirings/workflow/scenario.types.js'
49
53
  import type {
50
54
  CoreTrigger,
51
55
  CoreTriggerSource,
@@ -98,6 +102,14 @@ 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
+ /** Secrets the host lends this instance, named as the addon reads them */
106
+ secretGrants?: string[]
107
+ /** Credentials the host lends this instance, named as the addon reads them */
108
+ credentialGrants?: string[]
109
+ /** Why this instance gets the whole `SecretService` rather than one scoped to its declared secrets */
110
+ globalSecrets?: string
111
+ /** Why this instance gets the whole `CredentialService` rather than one scoped to its declared credentials */
112
+ globalCredentials?: string
101
113
  /** Set by `wireRemoteAddon`: this namespace is consumed remotely over HTTP, not bundled */
102
114
  remote?: boolean
103
115
  /** Remote host base URL (wireRemoteAddon) — string or resolver over singleton services */
@@ -153,6 +165,10 @@ export interface PikkuPackageState {
153
165
  agent: {
154
166
  agents: Map<string, CoreAIAgent>
155
167
  agentsMeta: AIAgentMeta
168
+ scorers: Map<string, PikkuAIScorer>
169
+ scorersMeta: ScorerMeta
170
+ /** Alias -> `provider/model`, from the `models` table in pikku.config.json. */
171
+ modelAliases: Record<string, string>
156
172
  }
157
173
  gateway: {
158
174
  gateways: Map<string, CoreGateway>
@@ -204,5 +220,9 @@ export interface PikkuPackageState {
204
220
  { name: string; displayName: string; type: string; oauth2?: boolean }
205
221
  > | null
206
222
  requiredParentServices: string[] | null
223
+ /** The secret keys this package is allowed to read, emitted by the CLI from
224
+ * what the package declares and is seen to read. `null` when the package
225
+ * was never analysed, which scopes it to nothing. */
226
+ declaredSecrets: string[] | null
207
227
  }
208
228
  }
@@ -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
+ })