@pikku/core 0.12.0 → 0.12.1

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 (262) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/errors/errors.d.ts +6 -0
  3. package/dist/errors/errors.js +10 -0
  4. package/dist/errors/index.d.ts +1 -0
  5. package/dist/errors/index.js +1 -0
  6. package/dist/function/function-runner.d.ts +3 -3
  7. package/dist/function/function-runner.js +66 -44
  8. package/dist/function/functions.types.d.ts +1 -0
  9. package/dist/handle-error.d.ts +2 -2
  10. package/dist/handle-error.js +2 -2
  11. package/dist/index.d.ts +5 -3
  12. package/dist/index.js +2 -1
  13. package/dist/internal.d.ts +3 -0
  14. package/dist/internal.js +2 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -1
  16. package/dist/middleware/auth-bearer.js +1 -1
  17. package/dist/middleware/auth-cookie.d.ts +2 -2
  18. package/dist/middleware/auth-cookie.js +1 -1
  19. package/dist/middleware/cors.d.ts +2 -2
  20. package/dist/middleware/cors.js +45 -37
  21. package/dist/middleware-runner.d.ts +1 -1
  22. package/dist/permissions.d.ts +2 -2
  23. package/dist/pikku-state.d.ts +9 -8
  24. package/dist/pikku-state.js +30 -17
  25. package/dist/schema.d.ts +4 -4
  26. package/dist/schema.js +9 -5
  27. package/dist/services/ai-agent-runner-service.d.ts +22 -3
  28. package/dist/services/ai-run-state-service.d.ts +1 -1
  29. package/dist/services/ai-storage-service.d.ts +1 -1
  30. package/dist/services/content-service.d.ts +6 -0
  31. package/dist/services/gopass-secrets.d.ts +1 -1
  32. package/dist/services/gopass-secrets.js +3 -0
  33. package/dist/services/in-memory-ai-run-state-service.d.ts +15 -0
  34. package/dist/services/in-memory-ai-run-state-service.js +44 -0
  35. package/dist/services/in-memory-trigger-service.d.ts +0 -1
  36. package/dist/services/in-memory-trigger-service.js +6 -8
  37. package/dist/services/index.d.ts +3 -2
  38. package/dist/services/index.js +1 -0
  39. package/dist/services/local-content.d.ts +2 -1
  40. package/dist/services/local-content.js +6 -1
  41. package/dist/services/local-secrets.d.ts +2 -2
  42. package/dist/services/local-variables.d.ts +1 -1
  43. package/dist/services/logger-console.d.ts +2 -1
  44. package/dist/services/scheduler-service.d.ts +0 -12
  45. package/dist/services/scheduler-service.js +0 -6
  46. package/dist/services/scoped-secret-service.d.ts +1 -1
  47. package/dist/services/trigger-service.d.ts +0 -7
  48. package/dist/services/user-session-service.d.ts +3 -3
  49. package/dist/services/workflow-service.d.ts +2 -3
  50. package/dist/types/core.types.d.ts +24 -18
  51. package/dist/types/state.types.d.ts +17 -15
  52. package/dist/utils.d.ts +5 -5
  53. package/dist/utils.js +12 -9
  54. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +5 -0
  55. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +146 -0
  56. package/dist/wirings/ai-agent/ai-agent-helpers.d.ts +28 -0
  57. package/dist/wirings/ai-agent/ai-agent-helpers.js +40 -0
  58. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +4 -5
  59. package/dist/wirings/ai-agent/ai-agent-prepare.js +66 -30
  60. package/dist/wirings/ai-agent/ai-agent-registry.d.ts +1 -1
  61. package/dist/wirings/ai-agent/ai-agent-runner.js +107 -8
  62. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
  63. package/dist/wirings/ai-agent/ai-agent-stream.js +246 -87
  64. package/dist/wirings/ai-agent/ai-agent.types.d.ts +61 -2
  65. package/dist/wirings/ai-agent/index.d.ts +3 -1
  66. package/dist/wirings/ai-agent/index.js +2 -0
  67. package/dist/wirings/channel/channel-common.d.ts +2 -2
  68. package/dist/wirings/channel/channel-handler.d.ts +3 -3
  69. package/dist/wirings/channel/channel-handler.js +4 -4
  70. package/dist/wirings/channel/channel-runner.d.ts +5 -5
  71. package/dist/wirings/channel/channel-runner.js +3 -2
  72. package/dist/wirings/channel/channel-store.d.ts +1 -1
  73. package/dist/wirings/channel/channel.types.d.ts +4 -6
  74. package/dist/wirings/channel/local/local-channel-runner.d.ts +7 -2
  75. package/dist/wirings/channel/local/local-channel-runner.js +4 -3
  76. package/dist/wirings/channel/local/local-eventhub-service.d.ts +2 -2
  77. package/dist/wirings/channel/log-channels.d.ts +1 -1
  78. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +1 -1
  79. package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +4 -4
  80. package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -19
  81. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +1 -1
  82. package/dist/wirings/cli/cli-runner.d.ts +2 -2
  83. package/dist/wirings/cli/cli.types.d.ts +3 -3
  84. package/dist/wirings/cli/command-parser.d.ts +1 -1
  85. package/dist/wirings/http/http-runner.d.ts +9 -9
  86. package/dist/wirings/http/http-runner.js +33 -14
  87. package/dist/wirings/http/http.types.d.ts +2 -6
  88. package/dist/wirings/http/log-http-routes.d.ts +1 -1
  89. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -1
  90. package/dist/wirings/http/pikku-fetch-http-response.d.ts +2 -2
  91. package/dist/wirings/http/pikku-fetch-http-response.js +6 -1
  92. package/dist/wirings/http/routers/http-router.d.ts +1 -1
  93. package/dist/wirings/http/routers/path-to-regex.d.ts +2 -2
  94. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  95. package/dist/wirings/mcp/mcp-runner.d.ts +0 -3
  96. package/dist/wirings/mcp/mcp-runner.js +4 -2
  97. package/dist/wirings/mcp/mcp.types.d.ts +2 -2
  98. package/dist/wirings/oauth2/oauth2-client.d.ts +3 -3
  99. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +1 -1
  100. package/dist/wirings/queue/index.d.ts +1 -1
  101. package/dist/wirings/queue/index.js +1 -1
  102. package/dist/wirings/queue/queue-runner.d.ts +1 -12
  103. package/dist/wirings/queue/queue-runner.js +4 -12
  104. package/dist/wirings/queue/queue.types.d.ts +3 -9
  105. package/dist/wirings/queue/register-queue-helper.d.ts +2 -2
  106. package/dist/wirings/queue/register-queue-helper.js +1 -1
  107. package/dist/wirings/queue/validate-worker-config.d.ts +1 -1
  108. package/dist/wirings/rpc/index.d.ts +2 -0
  109. package/dist/wirings/rpc/index.js +1 -0
  110. package/dist/wirings/rpc/rpc-runner.d.ts +36 -14
  111. package/dist/wirings/rpc/rpc-runner.js +56 -28
  112. package/dist/wirings/rpc/rpc-types.d.ts +14 -2
  113. package/dist/wirings/rpc/wire-addon.d.ts +10 -0
  114. package/dist/wirings/rpc/wire-addon.js +9 -0
  115. package/dist/wirings/scheduler/index.d.ts +1 -1
  116. package/dist/wirings/scheduler/index.js +1 -1
  117. package/dist/wirings/scheduler/log-schedulers.d.ts +1 -1
  118. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -10
  119. package/dist/wirings/scheduler/scheduler-runner.js +4 -25
  120. package/dist/wirings/scheduler/scheduler.types.d.ts +2 -2
  121. package/dist/wirings/trigger/pikku-trigger-service.d.ts +2 -6
  122. package/dist/wirings/trigger/pikku-trigger-service.js +11 -16
  123. package/dist/wirings/trigger/trigger-runner.d.ts +1 -5
  124. package/dist/wirings/trigger/trigger-runner.js +4 -3
  125. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -6
  126. package/dist/wirings/workflow/pikku-workflow-service.js +20 -29
  127. package/dist/wirings/workflow/workflow.types.d.ts +2 -2
  128. package/package.json +2 -1
  129. package/src/crypto-utils.test.ts +116 -0
  130. package/src/errors/error.test.ts +143 -2
  131. package/src/errors/errors.ts +10 -0
  132. package/src/errors/index.ts +1 -0
  133. package/src/function/function-runner.test.ts +2 -2
  134. package/src/function/function-runner.ts +72 -49
  135. package/src/function/functions.types.ts +1 -0
  136. package/src/handle-error.test.ts +361 -0
  137. package/src/handle-error.ts +4 -4
  138. package/src/index.ts +3 -10
  139. package/src/internal.ts +6 -0
  140. package/src/middleware/auth-apikey.test.ts +1 -1
  141. package/src/middleware/auth-bearer.test.ts +1 -1
  142. package/src/middleware/auth-bearer.ts +2 -5
  143. package/src/middleware/auth-cookie.test.ts +1 -1
  144. package/src/middleware/auth-cookie.ts +3 -5
  145. package/src/middleware/cors.test.ts +424 -0
  146. package/src/middleware/cors.ts +14 -6
  147. package/src/middleware/remote-auth.test.ts +488 -0
  148. package/src/middleware-runner.test.ts +1 -1
  149. package/src/middleware-runner.ts +1 -1
  150. package/src/permissions.test.ts +2 -2
  151. package/src/permissions.ts +2 -2
  152. package/src/pikku-state.test.ts +224 -0
  153. package/src/pikku-state.ts +40 -28
  154. package/src/schema.test.ts +198 -6
  155. package/src/schema.ts +11 -7
  156. package/src/services/ai-agent-runner-service.ts +15 -3
  157. package/src/services/ai-run-state-service.ts +1 -1
  158. package/src/services/ai-storage-service.ts +1 -1
  159. package/src/services/content-service.ts +7 -0
  160. package/src/services/gopass-secrets.ts +4 -1
  161. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  162. package/src/services/in-memory-trigger-service.ts +6 -10
  163. package/src/services/in-memory-workflow-service.test.ts +351 -0
  164. package/src/services/index.ts +2 -1
  165. package/src/services/local-content.ts +9 -3
  166. package/src/services/local-secrets.test.ts +80 -0
  167. package/src/services/local-secrets.ts +2 -2
  168. package/src/services/local-variables.test.ts +61 -0
  169. package/src/services/local-variables.ts +1 -1
  170. package/src/services/logger-console.test.ts +118 -0
  171. package/src/services/logger-console.ts +2 -1
  172. package/src/services/scheduler-service.ts +0 -18
  173. package/src/services/scoped-secret-service.test.ts +74 -0
  174. package/src/services/scoped-secret-service.ts +1 -1
  175. package/src/services/trigger-service.ts +0 -21
  176. package/src/services/typed-secret-service.test.ts +93 -0
  177. package/src/services/typed-variables-service.test.ts +73 -0
  178. package/src/services/user-session-service.test.ts +113 -0
  179. package/src/services/user-session-service.ts +3 -3
  180. package/src/services/workflow-service.ts +2 -12
  181. package/src/time-utils.test.ts +1 -1
  182. package/src/types/core.types.ts +25 -19
  183. package/src/types/state.types.ts +24 -15
  184. package/src/utils.test.ts +350 -0
  185. package/src/utils.ts +14 -13
  186. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +363 -0
  187. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +214 -0
  188. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  189. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  190. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  191. package/src/wirings/ai-agent/ai-agent-prepare.ts +79 -45
  192. package/src/wirings/ai-agent/ai-agent-registry.ts +1 -1
  193. package/src/wirings/ai-agent/ai-agent-runner.test.ts +245 -3
  194. package/src/wirings/ai-agent/ai-agent-runner.ts +121 -7
  195. package/src/wirings/ai-agent/ai-agent-stream.test.ts +430 -24
  196. package/src/wirings/ai-agent/ai-agent-stream.ts +377 -102
  197. package/src/wirings/ai-agent/ai-agent.types.ts +57 -2
  198. package/src/wirings/ai-agent/index.ts +11 -0
  199. package/src/wirings/channel/channel-common.ts +2 -2
  200. package/src/wirings/channel/channel-handler.ts +13 -9
  201. package/src/wirings/channel/channel-runner.ts +6 -6
  202. package/src/wirings/channel/channel-store.ts +1 -1
  203. package/src/wirings/channel/channel.types.ts +4 -8
  204. package/src/wirings/channel/local/local-channel-runner.test.ts +19 -12
  205. package/src/wirings/channel/local/local-channel-runner.ts +18 -13
  206. package/src/wirings/channel/local/local-eventhub-service.test.ts +2 -2
  207. package/src/wirings/channel/local/local-eventhub-service.ts +2 -2
  208. package/src/wirings/channel/log-channels.ts +1 -1
  209. package/src/wirings/channel/pikku-abstract-channel-handler.ts +1 -1
  210. package/src/wirings/channel/serverless/serverless-channel-runner.ts +37 -32
  211. package/src/wirings/cli/channel/cli-channel-runner.ts +1 -1
  212. package/src/wirings/cli/cli-runner.test.ts +1 -1
  213. package/src/wirings/cli/cli-runner.ts +11 -4
  214. package/src/wirings/cli/cli.types.ts +3 -3
  215. package/src/wirings/cli/command-parser.test.ts +1 -1
  216. package/src/wirings/cli/command-parser.ts +1 -1
  217. package/src/wirings/http/http-routes.test.ts +1 -1
  218. package/src/wirings/http/http-runner.test.ts +9 -17
  219. package/src/wirings/http/http-runner.ts +43 -27
  220. package/src/wirings/http/http.types.ts +1 -14
  221. package/src/wirings/http/log-http-routes.ts +1 -1
  222. package/src/wirings/http/pikku-fetch-http-request.ts +1 -1
  223. package/src/wirings/http/pikku-fetch-http-response.ts +12 -5
  224. package/src/wirings/http/routers/http-router.ts +1 -1
  225. package/src/wirings/http/routers/path-to-regex.test.ts +1 -1
  226. package/src/wirings/http/routers/path-to-regex.ts +4 -3
  227. package/src/wirings/mcp/mcp-endpoint-registry.ts +5 -1
  228. package/src/wirings/mcp/mcp-runner.ts +9 -15
  229. package/src/wirings/mcp/mcp.types.ts +2 -2
  230. package/src/wirings/oauth2/oauth2-client.ts +3 -3
  231. package/src/wirings/oauth2/wire-oauth2-credential.ts +1 -1
  232. package/src/wirings/queue/index.ts +0 -1
  233. package/src/wirings/queue/queue-runner.test.ts +52 -25
  234. package/src/wirings/queue/queue-runner.ts +8 -30
  235. package/src/wirings/queue/queue.types.ts +3 -13
  236. package/src/wirings/queue/register-queue-helper.ts +3 -5
  237. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  238. package/src/wirings/queue/validate-worker-config.ts +4 -1
  239. package/src/wirings/rpc/index.ts +2 -0
  240. package/src/wirings/rpc/rpc-runner.ts +92 -49
  241. package/src/wirings/rpc/rpc-types.ts +17 -2
  242. package/src/wirings/rpc/wire-addon.ts +20 -0
  243. package/src/wirings/scheduler/index.ts +0 -1
  244. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  245. package/src/wirings/scheduler/scheduler-runner.test.ts +49 -60
  246. package/src/wirings/scheduler/scheduler-runner.ts +9 -56
  247. package/src/wirings/scheduler/scheduler.types.ts +2 -2
  248. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  249. package/src/wirings/trigger/pikku-trigger-service.ts +17 -39
  250. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  251. package/src/wirings/trigger/trigger-runner.ts +8 -11
  252. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  253. package/src/wirings/workflow/graph/graph-runner.test.ts +14 -22
  254. package/src/wirings/workflow/graph/template.test.ts +49 -0
  255. package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -27
  256. package/src/wirings/workflow/pikku-workflow-service.ts +29 -45
  257. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  258. package/src/wirings/workflow/workflow.types.ts +2 -2
  259. package/tsconfig.tsbuildinfo +1 -1
  260. package/dist/wirings/workflow/workflow-utils.d.ts +0 -1
  261. package/dist/wirings/workflow/workflow-utils.js +0 -1
  262. package/src/wirings/workflow/workflow-utils.ts +0 -0
@@ -9,13 +9,17 @@ import type {
9
9
  CoreAIAgent,
10
10
  AIAgentMemoryConfig,
11
11
  } from './ai-agent.types.js'
12
- import { pikkuState } from '../../pikku-state.js'
12
+ import { pikkuState, getSingletonServices } from '../../pikku-state.js'
13
13
  import {
14
14
  combineChannelMiddleware,
15
15
  wrapChannelWithMiddleware,
16
16
  } from '../channel/channel-middleware-runner.js'
17
17
  import { randomUUID } from 'crypto'
18
18
  import type { AIStorageService } from '../../services/ai-storage-service.js'
19
+ import type {
20
+ AIAgentRunnerParams,
21
+ AIAgentStepResult,
22
+ } from '../../services/ai-agent-runner-service.js'
19
23
 
20
24
  import {
21
25
  parseWorkingMemory,
@@ -35,6 +39,10 @@ import {
35
39
  type StreamAIAgentOptions,
36
40
  type StreamContext,
37
41
  } from './ai-agent-prepare.js'
42
+ import {
43
+ createAssistantUIChannel,
44
+ parseAssistantUIInput,
45
+ } from './ai-agent-assistant-ui.js'
38
46
  import { resolveModelConfig } from './ai-agent-model-config.js'
39
47
  import type { AIRunStateService } from '../../services/ai-run-state-service.js'
40
48
  import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
@@ -42,6 +50,7 @@ import type { AIAgentRunnerService } from '../../services/ai-agent-runner-servic
42
50
  type PersistingChannel = AIStreamChannel & {
43
51
  fullText: string
44
52
  flush: () => Promise<void>
53
+ totalUsage: { inputTokens: number; outputTokens: number; model?: string }
45
54
  }
46
55
 
47
56
  function createPersistingChannel(
@@ -53,6 +62,14 @@ function createPersistingChannel(
53
62
  let stepText = ''
54
63
  let stepToolCalls: AIToolCall[] = []
55
64
  let stepToolResults: AIToolResult[] = []
65
+ const totalUsage: {
66
+ inputTokens: number
67
+ outputTokens: number
68
+ model?: string
69
+ } = {
70
+ inputTokens: 0,
71
+ outputTokens: 0,
72
+ }
56
73
 
57
74
  const flushStep = async () => {
58
75
  if (!storage) return
@@ -92,6 +109,9 @@ function createPersistingChannel(
92
109
  get fullText() {
93
110
  return fullText
94
111
  },
112
+ get totalUsage() {
113
+ return totalUsage
114
+ },
95
115
  flush: flushStep,
96
116
  close: () => parent.close(),
97
117
  send: (event: AIStreamEvent) => {
@@ -119,6 +139,9 @@ function createPersistingChannel(
119
139
  })
120
140
  break
121
141
  case 'usage':
142
+ totalUsage.inputTokens += event.tokens.input
143
+ totalUsage.outputTokens += event.tokens.output
144
+ if (event.model) totalUsage.model = event.model
122
145
  flushStep()
123
146
  break
124
147
  case 'done':
@@ -144,6 +167,7 @@ async function postStreamCleanup(
144
167
  aiRunState: AIRunStateService,
145
168
  runId: string
146
169
  ): Promise<void> {
170
+ const usage = persistingChannel.totalUsage
147
171
  let outputText = persistingChannel.fullText
148
172
  let outputMessages = messages
149
173
  for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
@@ -152,7 +176,10 @@ async function postStreamCleanup(
152
176
  const result = await mw.modifyOutput(singletonServices, {
153
177
  text: outputText,
154
178
  messages: outputMessages,
155
- usage: { inputTokens: 0, outputTokens: 0 },
179
+ usage: {
180
+ inputTokens: usage.inputTokens,
181
+ outputTokens: usage.outputTokens,
182
+ },
156
183
  })
157
184
  outputText = result.text
158
185
  outputMessages = result.messages
@@ -183,18 +210,254 @@ async function postStreamCleanup(
183
210
  }
184
211
  }
185
212
 
186
- await aiRunState.updateRun(runId, { status: 'completed' })
213
+ await aiRunState.updateRun(runId, {
214
+ status: 'completed',
215
+ ...(usage.model
216
+ ? {
217
+ usage: {
218
+ inputTokens: usage.inputTokens,
219
+ outputTokens: usage.outputTokens,
220
+ model: usage.model,
221
+ },
222
+ }
223
+ : {}),
224
+ })
225
+ }
226
+
227
+ type StepLoopParams = {
228
+ agent: CoreAIAgent
229
+ runnerParams: AIAgentRunnerParams
230
+ maxSteps: number
231
+ agentRunner: AIAgentRunnerService
232
+ persistingChannel: PersistingChannel
233
+ channel: AIStreamChannel
234
+ runId: string
235
+ aiMiddlewares: PikkuAIMiddlewareHooks[]
236
+ }
237
+
238
+ type StepLoopResult =
239
+ | { outcome: 'done' }
240
+ | { outcome: 'approval'; approval: ToolApprovalRequired }
241
+
242
+ async function runStreamStepLoop(
243
+ params: StepLoopParams
244
+ ): Promise<StepLoopResult> {
245
+ const {
246
+ agent,
247
+ runnerParams,
248
+ maxSteps,
249
+ agentRunner,
250
+ persistingChannel,
251
+ channel,
252
+ runId,
253
+ aiMiddlewares,
254
+ } = params
255
+
256
+ const singletonServices = getSingletonServices()
257
+
258
+ for (let step = 0; step < maxSteps; step++) {
259
+ if (agent.prepareStep) {
260
+ let stopped = false
261
+ await agent.prepareStep({
262
+ stepNumber: step,
263
+ messages: runnerParams.messages,
264
+ tools: runnerParams.tools,
265
+ toolChoice: runnerParams.toolChoice,
266
+ model: runnerParams.model,
267
+ stop: () => {
268
+ stopped = true
269
+ },
270
+ })
271
+ if (stopped) break
272
+ }
273
+
274
+ channel.send({ type: 'step-start', stepNumber: step })
275
+
276
+ const stepResult = await agentRunner.stream(runnerParams, persistingChannel)
277
+
278
+ for (const mw of aiMiddlewares) {
279
+ if (mw.afterStep) {
280
+ await mw.afterStep(singletonServices, {
281
+ stepNumber: step,
282
+ text: stepResult.text,
283
+ toolCalls: stepResult.toolCalls,
284
+ toolResults: stepResult.toolResults,
285
+ usage: stepResult.usage,
286
+ finishReason: stepResult.finishReason,
287
+ })
288
+ }
289
+ }
290
+
291
+ if (stepResult.toolCalls.length === 0) break
292
+
293
+ const approvalNeeded = checkForApproval(
294
+ stepResult,
295
+ runnerParams.tools,
296
+ runId
297
+ )
298
+ if (approvalNeeded) {
299
+ return { outcome: 'approval', approval: approvalNeeded }
300
+ }
301
+
302
+ appendStepMessages(runnerParams, stepResult)
303
+ }
304
+
305
+ return { outcome: 'done' }
306
+ }
307
+
308
+ function checkForApproval(
309
+ stepResult: AIAgentStepResult,
310
+ tools: AIAgentRunnerParams['tools'],
311
+ runId: string
312
+ ): ToolApprovalRequired | null {
313
+ for (const tc of stepResult.toolCalls) {
314
+ const toolDef = tools.find((t) => t.name === tc.toolName)
315
+
316
+ if (toolDef?.needsApproval) {
317
+ return new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args)
318
+ }
319
+
320
+ const tr = stepResult.toolResults.find(
321
+ (r) => r.toolCallId === tc.toolCallId
322
+ )
323
+ if (
324
+ tr?.result &&
325
+ typeof tr.result === 'object' &&
326
+ '__approvalRequired' in (tr.result as object)
327
+ ) {
328
+ const r = tr.result as {
329
+ toolName: string
330
+ args: unknown
331
+ reason?: string
332
+ displayToolName?: string
333
+ displayArgs?: unknown
334
+ agentRunId?: string
335
+ }
336
+ return new ToolApprovalRequired(
337
+ tc.toolCallId,
338
+ r.toolName,
339
+ r.args,
340
+ r.reason,
341
+ r.displayToolName,
342
+ r.displayArgs,
343
+ r.agentRunId
344
+ )
345
+ }
346
+ }
347
+ return null
348
+ }
349
+
350
+ function appendStepMessages(
351
+ runnerParams: AIAgentRunnerParams,
352
+ stepResult: AIAgentStepResult
353
+ ): void {
354
+ const assistantMsg: AIMessage = {
355
+ id: randomUUID(),
356
+ role: 'assistant',
357
+ content: stepResult.text || undefined,
358
+ toolCalls:
359
+ stepResult.toolCalls.length > 0
360
+ ? stepResult.toolCalls.map((tc) => ({
361
+ id: tc.toolCallId,
362
+ name: tc.toolName,
363
+ args: tc.args as Record<string, unknown>,
364
+ }))
365
+ : undefined,
366
+ createdAt: new Date(),
367
+ }
368
+ runnerParams.messages.push(assistantMsg)
369
+
370
+ if (stepResult.toolResults.length > 0) {
371
+ const toolMsg: AIMessage = {
372
+ id: randomUUID(),
373
+ role: 'tool',
374
+ toolResults: stepResult.toolResults.map((tr) => ({
375
+ id: tr.toolCallId,
376
+ name: tr.toolName,
377
+ result:
378
+ typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result),
379
+ })),
380
+ createdAt: new Date(),
381
+ }
382
+ runnerParams.messages.push(toolMsg)
383
+ }
384
+ }
385
+
386
+ function handleApproval(
387
+ err: ToolApprovalRequired,
388
+ runId: string,
389
+ channel: AIStreamChannel,
390
+ aiRunState: AIRunStateService,
391
+ persistingChannel: PersistingChannel
392
+ ): Promise<void> {
393
+ return (async () => {
394
+ await persistingChannel.flush()
395
+
396
+ const pendingApproval = err.agentRunId
397
+ ? {
398
+ type: 'agent-call' as const,
399
+ toolCallId: err.toolCallId,
400
+ agentName: err.toolName,
401
+ agentRunId: err.agentRunId,
402
+ displayToolName: err.displayToolName ?? err.toolName,
403
+ displayArgs: err.displayArgs ?? err.args,
404
+ }
405
+ : {
406
+ type: 'tool-call' as const,
407
+ toolCallId: err.toolCallId,
408
+ toolName: err.toolName,
409
+ args: err.args,
410
+ }
411
+
412
+ await aiRunState.updateRun(runId, {
413
+ status: 'suspended',
414
+ suspendReason: 'approval',
415
+ pendingApprovals: [pendingApproval],
416
+ })
417
+
418
+ const approvalEvent = {
419
+ type: 'approval-request' as const,
420
+ toolCallId: err.toolCallId,
421
+ toolName: err.displayToolName ?? err.toolName,
422
+ args: err.displayArgs ?? err.args,
423
+ reason: err.reason,
424
+ runId,
425
+ }
426
+ channel.send(approvalEvent as any)
427
+ channel.send({ type: 'done' })
428
+ channel.close()
429
+ })()
187
430
  }
188
431
 
189
432
  export async function streamAIAgent(
190
433
  agentName: string,
191
- input: { message: string; threadId: string; resourceId: string },
434
+ input:
435
+ | { message: string; threadId: string; resourceId: string }
436
+ | Record<string, unknown>,
192
437
  channel: AIStreamChannel,
193
438
  params: RunAIAgentParams,
194
439
  agentSessionMap?: Map<string, string>,
195
440
  options?: StreamAIAgentOptions
196
441
  ): Promise<void> {
197
442
  const sessionMap = agentSessionMap ?? new Map<string, string>()
443
+
444
+ const { agent: resolvedAgentForProtocol } = resolveAgent(agentName)
445
+ const inputRecord = input as Record<string, unknown>
446
+ const useUIMessageStream =
447
+ resolvedAgentForProtocol.protocol === 'ui-message-stream' ||
448
+ Array.isArray(inputRecord.messages)
449
+ let normalizedInput: { message: string; threadId: string; resourceId: string }
450
+ if (useUIMessageStream) {
451
+ normalizedInput = parseAssistantUIInput(inputRecord)
452
+ channel = createAssistantUIChannel(channel)
453
+ } else {
454
+ normalizedInput = input as {
455
+ message: string
456
+ threadId: string
457
+ resourceId: string
458
+ }
459
+ }
460
+
198
461
  const streamContext: StreamContext = { channel, options }
199
462
 
200
463
  const {
@@ -207,11 +470,18 @@ export async function streamAIAgent(
207
470
  threadId,
208
471
  userMessage,
209
472
  runnerParams,
473
+ maxSteps,
210
474
  missingRpcs,
211
475
  workingMemorySchemaName,
212
- } = await prepareAgentRun(agentName, input, params, sessionMap, streamContext)
476
+ } = await prepareAgentRun(
477
+ agentName,
478
+ normalizedInput,
479
+ params,
480
+ sessionMap,
481
+ streamContext
482
+ )
213
483
 
214
- const { singletonServices } = params
484
+ const singletonServices = getSingletonServices()
215
485
  const { aiRunState } = singletonServices
216
486
  if (!aiRunState) {
217
487
  throw new Error('AIRunStateService not available in singletonServices')
@@ -221,7 +491,7 @@ export async function streamAIAgent(
221
491
  await aiRunState.createRun({
222
492
  agentName,
223
493
  threadId,
224
- resourceId: input.resourceId,
494
+ resourceId: normalizedInput.resourceId,
225
495
  status: 'suspended',
226
496
  suspendReason: 'rpc-missing',
227
497
  missingRpcs,
@@ -254,7 +524,7 @@ export async function streamAIAgent(
254
524
  const runId = await aiRunState.createRun({
255
525
  agentName,
256
526
  threadId,
257
- resourceId: input.resourceId,
527
+ resourceId: normalizedInput.resourceId,
258
528
  status: 'running',
259
529
  usage: { inputTokens: 0, outputTokens: 0, model: agent.model },
260
530
  createdAt: new Date(),
@@ -311,7 +581,27 @@ export async function streamAIAgent(
311
581
  )
312
582
 
313
583
  try {
314
- await agentRunner.stream(runnerParams, persistingChannel)
584
+ const loopResult = await runStreamStepLoop({
585
+ agent,
586
+ runnerParams,
587
+ maxSteps,
588
+ agentRunner,
589
+ persistingChannel,
590
+ channel,
591
+ runId,
592
+ aiMiddlewares,
593
+ })
594
+
595
+ if (loopResult.outcome === 'approval') {
596
+ await handleApproval(
597
+ loopResult.approval,
598
+ runId,
599
+ channel,
600
+ aiRunState,
601
+ persistingChannel
602
+ )
603
+ return
604
+ }
315
605
 
316
606
  await postStreamCleanup(
317
607
  persistingChannel,
@@ -325,46 +615,23 @@ export async function streamAIAgent(
325
615
  aiRunState,
326
616
  runId
327
617
  )
328
- } catch (err) {
329
- if (err instanceof ToolApprovalRequired) {
330
- await persistingChannel.flush()
331
-
332
- const pendingApproval = err.agentRunId
333
- ? {
334
- type: 'agent-call' as const,
335
- toolCallId: err.toolCallId,
336
- agentName: err.toolName,
337
- agentRunId: err.agentRunId,
338
- displayToolName: err.displayToolName ?? err.toolName,
339
- displayArgs: err.displayArgs ?? err.args,
340
- }
341
- : {
342
- type: 'tool-call' as const,
343
- toolCallId: err.toolCallId,
344
- toolName: err.toolName,
345
- args: err.args,
346
- }
347
-
348
- await aiRunState.updateRun(runId, {
349
- status: 'suspended',
350
- suspendReason: 'approval',
351
- pendingApprovals: [pendingApproval],
352
- })
353
618
 
354
- const approvalEvent = {
355
- type: 'approval-request' as const,
356
- toolCallId: err.toolCallId,
357
- toolName: err.displayToolName ?? err.toolName,
358
- args: err.displayArgs ?? err.args,
359
- reason: err.reason,
360
- runId,
619
+ channel.send({ type: 'done' })
620
+ channel.close()
621
+ } catch (err) {
622
+ for (const mw of aiMiddlewares) {
623
+ if (mw.onError) {
624
+ try {
625
+ await mw.onError(singletonServices, {
626
+ error: err instanceof Error ? err : new Error(String(err)),
627
+ stepNumber: -1,
628
+ messages: runnerParams.messages,
629
+ })
630
+ } catch {
631
+ // onError hooks must not affect error flow
632
+ }
361
633
  }
362
- channel.send(approvalEvent as any)
363
- channel.send({ type: 'done' })
364
- channel.close()
365
- return
366
634
  }
367
-
368
635
  await aiRunState.updateRun(runId, { status: 'failed' })
369
636
  channel.send({
370
637
  type: 'error',
@@ -377,6 +644,7 @@ export async function streamAIAgent(
377
644
 
378
645
  export async function resumeAIAgent(
379
646
  input: {
647
+ runId: string
380
648
  toolCallId: string
381
649
  approved: boolean
382
650
  },
@@ -384,18 +652,25 @@ export async function resumeAIAgent(
384
652
  params: RunAIAgentParams,
385
653
  options?: StreamAIAgentOptions
386
654
  ): Promise<void> {
387
- const { singletonServices } = params
655
+ const singletonServices = getSingletonServices()
388
656
  const { aiRunState } = singletonServices
389
657
  if (!aiRunState) {
390
658
  throw new Error('AIRunStateService not available in singletonServices')
391
659
  }
392
660
 
393
- const found = await aiRunState.findRunByToolCallId(input.toolCallId)
394
- if (!found) {
395
- throw new Error(`No suspended run found for toolCallId ${input.toolCallId}`)
661
+ const run = await aiRunState.getRun(input.runId)
662
+ if (!run) {
663
+ throw new Error(`No run found for runId ${input.runId}`)
396
664
  }
397
665
 
398
- const { run, approval: pending } = found
666
+ const pending = run.pendingApprovals?.find(
667
+ (p) => p.toolCallId === input.toolCallId
668
+ )
669
+ if (!pending) {
670
+ throw new Error(
671
+ `No pending approval for toolCallId ${input.toolCallId} on run ${input.runId}`
672
+ )
673
+ }
399
674
 
400
675
  await aiRunState.resolveApproval(
401
676
  input.toolCallId,
@@ -476,7 +751,11 @@ export async function resumeAIAgent(
476
751
  })
477
752
 
478
753
  await resumeAIAgent(
479
- { toolCallId: subPending.toolCallId, approved: true },
754
+ {
755
+ runId: pending.agentRunId,
756
+ toolCallId: subPending.toolCallId,
757
+ approved: true,
758
+ },
480
759
  subChannel,
481
760
  params,
482
761
  options
@@ -510,14 +789,16 @@ export async function resumeAIAgent(
510
789
  channel,
511
790
  options: { ...options, requiresToolApproval: false },
512
791
  }
792
+ const aiMiddlewaresForResume: PikkuAIMiddlewareHooks[] =
793
+ agent.aiMiddleware ?? []
513
794
  const { tools } = buildToolDefs(
514
- singletonServices,
515
795
  params,
516
796
  new Map<string, string>(),
517
797
  run.resourceId,
518
798
  resolvedName,
519
799
  packageName,
520
- streamContext
800
+ streamContext,
801
+ aiMiddlewaresForResume
521
802
  )
522
803
 
523
804
  const matchingTool = tools.find((t) => t.name === pending.toolName)
@@ -529,14 +810,10 @@ export async function resumeAIAgent(
529
810
 
530
811
  const toolArgs =
531
812
  typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
532
- const { toolApprovalReason: _ignored, ...cleanArgs } = toolArgs as Record<
533
- string,
534
- unknown
535
- >
536
813
 
537
814
  let toolResult: unknown
538
815
  try {
539
- toolResult = await matchingTool.execute(cleanArgs)
816
+ toolResult = await matchingTool.execute(toolArgs)
540
817
  } catch (execErr) {
541
818
  toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
542
819
  }
@@ -598,7 +875,7 @@ async function continueAfterToolResult(
598
875
  aiRunState: AIRunStateService,
599
876
  options?: StreamAIAgentOptions
600
877
  ): Promise<void> {
601
- const { singletonServices } = params
878
+ const singletonServices = getSingletonServices()
602
879
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
603
880
  const meta = agentsMeta[resolvedName]
604
881
  const workingMemorySchemaName = meta?.workingMemorySchema ?? null
@@ -684,24 +961,25 @@ async function continueAfterToolResult(
684
961
 
685
962
  const streamContext: StreamContext = { channel, options }
686
963
  const resumeTools = buildToolDefs(
687
- singletonServices,
688
964
  params,
689
965
  new Map<string, string>(),
690
966
  run.resourceId,
691
967
  resolvedName,
692
968
  packageName,
693
- streamContext
969
+ streamContext,
970
+ aiMiddlewares
694
971
  ).tools
695
972
 
696
973
  const resolved = resolveModelConfig(resolvedName, agent)
974
+ const maxSteps = resolved.maxSteps ?? 10
697
975
 
698
- const runnerParams = {
976
+ const runnerParams: AIAgentRunnerParams = {
699
977
  model: resolved.model,
700
978
  temperature: resolved.temperature,
701
979
  instructions: modifiedInstructions,
702
980
  messages: modifiedMessages,
703
981
  tools: resumeTools,
704
- maxSteps: resolved.maxSteps ?? 10,
982
+ maxSteps: 1,
705
983
  toolChoice: (agent.toolChoice ?? 'auto') as 'auto' | 'required' | 'none',
706
984
  outputSchema: meta?.outputSchema
707
985
  ? pikkuState(packageName, 'misc', 'schemas').get(meta.outputSchema)
@@ -709,7 +987,27 @@ async function continueAfterToolResult(
709
987
  }
710
988
 
711
989
  try {
712
- await agentRunner.stream(runnerParams, persistingChannel)
990
+ const loopResult = await runStreamStepLoop({
991
+ agent,
992
+ runnerParams,
993
+ maxSteps,
994
+ agentRunner,
995
+ persistingChannel,
996
+ channel,
997
+ runId: run.runId,
998
+ aiMiddlewares,
999
+ })
1000
+
1001
+ if (loopResult.outcome === 'approval') {
1002
+ await handleApproval(
1003
+ loopResult.approval,
1004
+ run.runId,
1005
+ channel,
1006
+ aiRunState,
1007
+ persistingChannel
1008
+ )
1009
+ return
1010
+ }
713
1011
 
714
1012
  await postStreamCleanup(
715
1013
  persistingChannel,
@@ -723,46 +1021,23 @@ async function continueAfterToolResult(
723
1021
  aiRunState,
724
1022
  run.runId
725
1023
  )
726
- } catch (err) {
727
- if (err instanceof ToolApprovalRequired) {
728
- await persistingChannel.flush()
729
-
730
- const pendingApproval = err.agentRunId
731
- ? {
732
- type: 'agent-call' as const,
733
- toolCallId: err.toolCallId,
734
- agentName: err.toolName,
735
- agentRunId: err.agentRunId,
736
- displayToolName: err.displayToolName ?? err.toolName,
737
- displayArgs: err.displayArgs ?? err.args,
738
- }
739
- : {
740
- type: 'tool-call' as const,
741
- toolCallId: err.toolCallId,
742
- toolName: err.toolName,
743
- args: err.args,
744
- }
745
-
746
- await aiRunState.updateRun(run.runId, {
747
- status: 'suspended',
748
- suspendReason: 'approval',
749
- pendingApprovals: [pendingApproval],
750
- })
751
1024
 
752
- const approvalEvent = {
753
- type: 'approval-request' as const,
754
- toolCallId: err.toolCallId,
755
- toolName: err.displayToolName ?? err.toolName,
756
- args: err.displayArgs ?? err.args,
757
- reason: err.reason,
758
- runId: run.runId,
1025
+ channel.send({ type: 'done' })
1026
+ channel.close()
1027
+ } catch (err) {
1028
+ for (const mw of aiMiddlewares) {
1029
+ if (mw.onError) {
1030
+ try {
1031
+ await mw.onError(singletonServices, {
1032
+ error: err instanceof Error ? err : new Error(String(err)),
1033
+ stepNumber: -1,
1034
+ messages: runnerParams.messages,
1035
+ })
1036
+ } catch {
1037
+ // onError hooks must not affect error flow
1038
+ }
759
1039
  }
760
- channel.send(approvalEvent as any)
761
- channel.send({ type: 'done' })
762
- channel.close()
763
- return
764
1040
  }
765
-
766
1041
  await aiRunState.updateRun(run.runId, { status: 'failed' })
767
1042
  channel.send({
768
1043
  type: 'error',