@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,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, usage }) => {
447
+ modifyOutput: async (_services, { text, messages }) => {
404
448
  const { workingMemory, cleanedText } = extractWorkingMemory(text)
405
- if (workingMemory && options.storage) {
406
- const existing =
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 { resolveModelConfig } from './ai-agent-model-config.js'
4
- import { resetPikkuState } from '../../pikku-state.js'
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(