@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.
- package/CHANGELOG.md +312 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -15
- package/dist/services/index.js +5 -5
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +0 -2
- package/dist/types/state.types.d.ts +13 -0
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.js +2 -0
- package/dist/wirings/rpc/rpc-types.d.ts +4 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/index.d.ts +6 -6
- package/dist/wirings/workflow/index.js +2 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +39 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -1
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +70 -94
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +3 -43
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +3 -6
- package/src/types/state.types.ts +16 -0
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +2 -0
- package/src/wirings/rpc/rpc-types.ts +4 -0
- package/src/wirings/rpc/wire-addon.ts +17 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -20
- package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +0 -9
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/services/user-flow-actors-service.js +0 -1
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- 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()
|
package/src/types/core.types.ts
CHANGED
|
@@ -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
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
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
|
> = {
|
package/src/types/state.types.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -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(),
|