@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.
- package/CHANGELOG.md +345 -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 -16
- package/dist/services/index.js +5 -5
- package/dist/services/meta-service.d.ts +2 -1
- 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 +2 -3
- package/dist/types/state.types.d.ts +19 -1
- 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 +4 -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 +8 -0
- package/dist/wirings/rpc/addon-runner.js +31 -3
- package/dist/wirings/rpc/rpc-runner.js +4 -0
- package/dist/wirings/rpc/rpc-types.d.ts +8 -0
- package/dist/wirings/rpc/wire-addon.d.ts +25 -0
- package/dist/wirings/rpc/wire-addon.js +8 -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/feature.d.ts +2 -1
- package/dist/wirings/workflow/index.d.ts +5 -16
- package/dist/wirings/workflow/index.js +1 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +48 -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/scenario.types.d.ts +37 -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 +2 -37
- 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 -2
- 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 +81 -118
- 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 +7 -58
- package/src/services/meta-service.ts +2 -4
- 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 +4 -7
- package/src/types/state.types.ts +21 -1
- 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 +4 -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 +62 -3
- package/src/wirings/rpc/addon-secrets.test.ts +391 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +4 -0
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/rpc/wire-addon.ts +33 -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/feature.ts +2 -5
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -68
- package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
- 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/scenario.types.ts +63 -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 +1 -63
- 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/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
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
|
@@ -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(),
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CoreAIAgent,
|
|
5
5
|
AIAgentMemoryConfig,
|
|
6
6
|
AIAgentInput,
|
|
7
|
+
AIAgentStep,
|
|
7
8
|
AIMessage,
|
|
8
9
|
} from './ai-agent.types.js'
|
|
9
10
|
import type { AIStorageService } from '../../services/ai-storage-service.js'
|
|
@@ -143,13 +144,7 @@ export async function saveMessages(
|
|
|
143
144
|
result: {
|
|
144
145
|
text: string
|
|
145
146
|
uiSpec?: unknown
|
|
146
|
-
steps:
|
|
147
|
-
toolCalls?: {
|
|
148
|
-
name: string
|
|
149
|
-
args: Record<string, unknown>
|
|
150
|
-
result: string
|
|
151
|
-
}[]
|
|
152
|
-
}[]
|
|
147
|
+
steps: Pick<AIAgentStep, 'toolCalls'>[]
|
|
153
148
|
}
|
|
154
149
|
): Promise<string> {
|
|
155
150
|
const responseText = memoryConfig?.workingMemory
|
|
@@ -179,6 +174,7 @@ export async function saveMessages(
|
|
|
179
174
|
id: toolCallIds[i],
|
|
180
175
|
name: tc.name,
|
|
181
176
|
result: tc.result,
|
|
177
|
+
...(tc.error ? { error: tc.error } : {}),
|
|
182
178
|
})),
|
|
183
179
|
createdAt: new Date(),
|
|
184
180
|
})
|
|
@@ -370,6 +366,39 @@ export function stripWorkingMemoryForStreaming(text: string): string {
|
|
|
370
366
|
return output
|
|
371
367
|
}
|
|
372
368
|
|
|
369
|
+
async function persistWorkingMemory(
|
|
370
|
+
options: {
|
|
371
|
+
storage?: AIStorageService
|
|
372
|
+
threadId: string
|
|
373
|
+
workingMemorySchemaName?: string | null
|
|
374
|
+
logger?: Logger
|
|
375
|
+
schemaService?: SchemaService
|
|
376
|
+
},
|
|
377
|
+
workingMemory: Record<string, unknown>
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
if (!options.storage) return
|
|
380
|
+
|
|
381
|
+
const existing =
|
|
382
|
+
(await options.storage.getWorkingMemory(options.threadId, 'thread')) ?? {}
|
|
383
|
+
const merged = deepMergeWorkingMemory(existing, workingMemory)
|
|
384
|
+
|
|
385
|
+
if (options.schemaService && options.workingMemorySchemaName) {
|
|
386
|
+
try {
|
|
387
|
+
await options.schemaService.validateSchema(
|
|
388
|
+
options.workingMemorySchemaName,
|
|
389
|
+
merged
|
|
390
|
+
)
|
|
391
|
+
} catch (err) {
|
|
392
|
+
options.logger?.warn(
|
|
393
|
+
`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await options.storage.saveWorkingMemory(options.threadId, 'thread', merged)
|
|
400
|
+
}
|
|
401
|
+
|
|
373
402
|
export function createWorkingMemoryMiddleware(options: {
|
|
374
403
|
storage?: AIStorageService
|
|
375
404
|
threadId: string
|
|
@@ -382,6 +411,21 @@ export function createWorkingMemoryMiddleware(options: {
|
|
|
382
411
|
}> {
|
|
383
412
|
return {
|
|
384
413
|
modifyOutputStream: async (_services, { event, state }) => {
|
|
414
|
+
// A streamed step ends at its `usage` event, or at `done` for a model
|
|
415
|
+
// that reports no usage. That is the last moment the unstripped text is
|
|
416
|
+
// still reachable: it lives only in `state.rawText`, because the strip
|
|
417
|
+
// below runs before anything downstream — including the channel that
|
|
418
|
+
// accumulates what `modifyOutput` would later be handed.
|
|
419
|
+
if (event.type === 'usage' || event.type === 'done') {
|
|
420
|
+
const { workingMemory } = extractWorkingMemory(state.rawText ?? '')
|
|
421
|
+
state.rawText = ''
|
|
422
|
+
state.emittedVisibleText = ''
|
|
423
|
+
if (workingMemory) {
|
|
424
|
+
await persistWorkingMemory(options, workingMemory)
|
|
425
|
+
}
|
|
426
|
+
return event
|
|
427
|
+
}
|
|
428
|
+
|
|
385
429
|
if (event.type !== 'text-delta') return event
|
|
386
430
|
|
|
387
431
|
const rawText = `${state.rawText ?? ''}${event.text}`
|
|
@@ -400,38 +444,10 @@ export function createWorkingMemoryMiddleware(options: {
|
|
|
400
444
|
if (!delta) return null
|
|
401
445
|
return { ...event, text: delta }
|
|
402
446
|
},
|
|
403
|
-
modifyOutput: async (_services, { text, messages
|
|
447
|
+
modifyOutput: async (_services, { text, messages }) => {
|
|
404
448
|
const { workingMemory, cleanedText } = extractWorkingMemory(text)
|
|
405
|
-
if (workingMemory
|
|
406
|
-
|
|
407
|
-
(await options.storage.getWorkingMemory(
|
|
408
|
-
options.threadId,
|
|
409
|
-
'thread'
|
|
410
|
-
)) ?? {}
|
|
411
|
-
const merged = deepMergeWorkingMemory(existing, workingMemory)
|
|
412
|
-
|
|
413
|
-
let valid = true
|
|
414
|
-
if (options.schemaService && options.workingMemorySchemaName) {
|
|
415
|
-
try {
|
|
416
|
-
await options.schemaService.validateSchema(
|
|
417
|
-
options.workingMemorySchemaName,
|
|
418
|
-
merged
|
|
419
|
-
)
|
|
420
|
-
} catch (err) {
|
|
421
|
-
valid = false
|
|
422
|
-
options.logger?.warn(
|
|
423
|
-
`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (valid) {
|
|
429
|
-
await options.storage.saveWorkingMemory(
|
|
430
|
-
options.threadId,
|
|
431
|
-
'thread',
|
|
432
|
-
merged
|
|
433
|
-
)
|
|
434
|
-
}
|
|
449
|
+
if (workingMemory) {
|
|
450
|
+
await persistWorkingMemory(options, workingMemory)
|
|
435
451
|
}
|
|
436
452
|
|
|
437
453
|
return {
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { describe, test, beforeEach } from 'node:test'
|
|
1
|
+
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
resolveModelAlias,
|
|
5
|
+
resolveModelConfig,
|
|
6
|
+
} from './ai-agent-model-config.js'
|
|
7
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
8
|
+
|
|
9
|
+
const setAliases = (aliases: Record<string, string>) =>
|
|
10
|
+
pikkuState(null, 'agent', 'modelAliases', aliases)
|
|
5
11
|
|
|
6
12
|
beforeEach(() => {
|
|
7
13
|
resetPikkuState()
|
|
14
|
+
delete process.env.PIKKU_MODEL_ALIASES
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
delete process.env.PIKKU_MODEL_ALIASES
|
|
8
19
|
})
|
|
9
20
|
|
|
10
21
|
describe('resolveModelConfig', () => {
|
|
@@ -34,4 +45,62 @@ describe('resolveModelConfig', () => {
|
|
|
34
45
|
assert.strictEqual(result.temperature, undefined)
|
|
35
46
|
assert.strictEqual(result.maxSteps, undefined)
|
|
36
47
|
})
|
|
48
|
+
|
|
49
|
+
test('resolves an aliased agent model through the table', () => {
|
|
50
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
51
|
+
const result = resolveModelConfig('testAgent', {
|
|
52
|
+
model: 'cheap',
|
|
53
|
+
temperature: 0.2,
|
|
54
|
+
})
|
|
55
|
+
assert.strictEqual(result.model, 'openai/gpt-5-mini')
|
|
56
|
+
assert.strictEqual(result.temperature, 0.2)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('resolveModelAlias', () => {
|
|
61
|
+
test('a provider-qualified model is never treated as an alias', () => {
|
|
62
|
+
setAliases({ 'openai/gpt-4': 'anthropic/claude-3' })
|
|
63
|
+
assert.strictEqual(resolveModelAlias('openai/gpt-4'), 'openai/gpt-4')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('an alias resolves to its provider-qualified model', () => {
|
|
67
|
+
setAliases({
|
|
68
|
+
cheap: 'openai/gpt-5-mini',
|
|
69
|
+
tool: 'anthropic/claude-sonnet-5',
|
|
70
|
+
})
|
|
71
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'openai/gpt-5-mini')
|
|
72
|
+
assert.strictEqual(resolveModelAlias('tool'), 'anthropic/claude-sonnet-5')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('PIKKU_MODEL_ALIASES beats the generated table', () => {
|
|
76
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
77
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:anthropic/claude-haiku-4-5'
|
|
78
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'anthropic/claude-haiku-4-5')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('an env override splits on the first colon only', () => {
|
|
82
|
+
// A model id may itself contain a colon — `ollama/qwen2.5:7b` is the shape
|
|
83
|
+
// the runner's own error message cites. It stays provider-qualified so the
|
|
84
|
+
// resolved value is one `VercelAIAgentRunner.parseModel` would accept; a
|
|
85
|
+
// bare `bedrock:nova-lite:1` would pass here and fail the moment anything
|
|
86
|
+
// tried to use it.
|
|
87
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:ollama/qwen2.5:7b'
|
|
88
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'ollama/qwen2.5:7b')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('an env override leaves aliases it does not name alone', () => {
|
|
92
|
+
setAliases({ cheap: 'openai/gpt-5-mini', tool: 'openai/gpt-5' })
|
|
93
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:openai/gpt-5-nano'
|
|
94
|
+
assert.strictEqual(resolveModelAlias('tool'), 'openai/gpt-5')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('an unknown alias throws rather than reaching a provider', () => {
|
|
98
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
99
|
+
assert.throws(() => resolveModelAlias('expensive'), /expensive/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('the error names the aliases that do exist', () => {
|
|
103
|
+
setAliases({ cheap: 'openai/gpt-5-mini', tool: 'openai/gpt-5' })
|
|
104
|
+
assert.throws(() => resolveModelAlias('exspensive'), /cheap, tool/)
|
|
105
|
+
})
|
|
37
106
|
})
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
// knowledge: decisions/internals/ai-agent-model-config-stays-a-single-resolution-seam.md
|
|
2
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
3
|
+
|
|
4
|
+
const isProviderQualified = (model: string) => model.includes('/')
|
|
5
|
+
|
|
6
|
+
/** `PIKKU_MODEL_ALIASES=cheap:openai/gpt-5-mini,tool:anthropic/claude-sonnet-5` */
|
|
7
|
+
const envAliases = (): Record<string, string> => {
|
|
8
|
+
const raw = process.env.PIKKU_MODEL_ALIASES
|
|
9
|
+
if (!raw) return {}
|
|
10
|
+
const aliases: Record<string, string> = {}
|
|
11
|
+
for (const entry of raw.split(',')) {
|
|
12
|
+
// First colon only — a model id may contain its own.
|
|
13
|
+
const separator = entry.indexOf(':')
|
|
14
|
+
if (separator === -1) continue
|
|
15
|
+
const alias = entry.slice(0, separator).trim()
|
|
16
|
+
const model = entry.slice(separator + 1).trim()
|
|
17
|
+
if (alias && model) aliases[alias] = model
|
|
18
|
+
}
|
|
19
|
+
return aliases
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves a model name to a concrete `provider/model`. Aliases come from the
|
|
24
|
+
* `models` table in pikku.config.json; a name containing `/` is already
|
|
25
|
+
* concrete. Read from the main package, not the calling addon's — which model
|
|
26
|
+
* a tier points at is the hosting app's decision.
|
|
27
|
+
*/
|
|
28
|
+
export const resolveModelAlias = (model: string): string => {
|
|
29
|
+
if (isProviderQualified(model)) return model
|
|
30
|
+
|
|
31
|
+
const generated = pikkuState(null, 'agent', 'modelAliases')
|
|
32
|
+
const override = envAliases()[model]
|
|
33
|
+
const resolved = override ?? generated[model]
|
|
34
|
+
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
const known = Object.keys({ ...generated, ...envAliases() }).sort()
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unknown model alias '${model}'. ` +
|
|
39
|
+
(known.length
|
|
40
|
+
? `Known aliases: ${known.join(', ')}. `
|
|
41
|
+
: `No aliases are configured. `) +
|
|
42
|
+
`Add it to the "models" table in pikku.config.json, or name a ` +
|
|
43
|
+
`provider-qualified model such as 'openai/gpt-5-mini'.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return resolved
|
|
48
|
+
}
|
|
49
|
+
|
|
2
50
|
export function resolveModelConfig(
|
|
3
51
|
_agentName: string,
|
|
4
52
|
agent: { model: string; temperature?: number; maxSteps?: number }
|
|
5
53
|
): { model: string; temperature?: number; maxSteps?: number } {
|
|
6
54
|
return {
|
|
7
|
-
model: agent.model,
|
|
55
|
+
model: resolveModelAlias(agent.model),
|
|
8
56
|
temperature: agent.temperature,
|
|
9
57
|
maxSteps: agent.maxSteps,
|
|
10
58
|
}
|
|
@@ -565,6 +565,10 @@ export async function buildToolDefs(
|
|
|
565
565
|
variableOverrides: capturedAddonConfig?.variableOverrides,
|
|
566
566
|
credentialOverrides:
|
|
567
567
|
capturedAddonConfig?.credentialOverrides,
|
|
568
|
+
secretGrants: capturedAddonConfig?.secretGrants,
|
|
569
|
+
credentialGrants: capturedAddonConfig?.credentialGrants,
|
|
570
|
+
globalSecrets: capturedAddonConfig?.globalSecrets,
|
|
571
|
+
globalCredentials: capturedAddonConfig?.globalCredentials,
|
|
568
572
|
}
|
|
569
573
|
: undefined
|
|
570
574
|
services = await getOrCreatePackageSingletonServices(
|