@pikku/core 0.12.4 → 0.12.5

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 (229) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/in-memory-workflow-service.js +7 -11
  9. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  10. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  11. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  12. package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
  13. package/package.json +3 -2
  14. package/src/crypto-utils.test.ts +214 -0
  15. package/src/crypto-utils.ts +213 -0
  16. package/src/dev/hot-reload.test.ts +484 -0
  17. package/src/dev/hot-reload.ts +212 -0
  18. package/src/errors/error-handler.ts +62 -0
  19. package/src/errors/error.test.ts +195 -0
  20. package/src/errors/errors.ts +374 -0
  21. package/src/errors/index.ts +2 -0
  22. package/src/factory-functions.test.ts +109 -0
  23. package/src/function/function-runner.test.ts +536 -0
  24. package/src/function/function-runner.ts +365 -0
  25. package/src/function/functions.types.ts +304 -0
  26. package/src/function/index.ts +5 -0
  27. package/src/handle-error.test.ts +424 -0
  28. package/src/handle-error.ts +69 -0
  29. package/src/index.ts +118 -0
  30. package/src/internal.ts +6 -0
  31. package/src/middleware/auth-apikey.test.ts +363 -0
  32. package/src/middleware/auth-apikey.ts +47 -0
  33. package/src/middleware/auth-bearer.test.ts +450 -0
  34. package/src/middleware/auth-bearer.ts +72 -0
  35. package/src/middleware/auth-cookie.test.ts +528 -0
  36. package/src/middleware/auth-cookie.ts +80 -0
  37. package/src/middleware/cors.test.ts +424 -0
  38. package/src/middleware/cors.ts +104 -0
  39. package/src/middleware/index.ts +5 -0
  40. package/src/middleware/remote-auth.test.ts +488 -0
  41. package/src/middleware/remote-auth.ts +68 -0
  42. package/src/middleware/timeout.ts +15 -0
  43. package/src/middleware-runner.test.ts +418 -0
  44. package/src/middleware-runner.ts +240 -0
  45. package/src/permissions.test.ts +434 -0
  46. package/src/permissions.ts +327 -0
  47. package/src/pikku-request.ts +23 -0
  48. package/src/pikku-response.ts +5 -0
  49. package/src/pikku-state.test.ts +224 -0
  50. package/src/pikku-state.ts +216 -0
  51. package/src/run-tests-script.test.ts +49 -0
  52. package/src/schema.test.ts +249 -0
  53. package/src/schema.ts +151 -0
  54. package/src/services/ai-agent-runner-service.ts +41 -0
  55. package/src/services/ai-run-state-service.ts +20 -0
  56. package/src/services/ai-storage-service.ts +39 -0
  57. package/src/services/content-service.ts +76 -0
  58. package/src/services/deployment-service.ts +22 -0
  59. package/src/services/gateway-service.ts +20 -0
  60. package/src/services/gopass-secrets.ts +98 -0
  61. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  62. package/src/services/in-memory-trigger-service.ts +64 -0
  63. package/src/services/in-memory-workflow-service.test.ts +351 -0
  64. package/src/services/in-memory-workflow-service.ts +502 -0
  65. package/src/services/index.ts +56 -0
  66. package/src/services/jwt-service.ts +30 -0
  67. package/src/services/local-content.ts +120 -0
  68. package/src/services/local-gateway-service.ts +62 -0
  69. package/src/services/local-secrets.test.ts +80 -0
  70. package/src/services/local-secrets.ts +94 -0
  71. package/src/services/local-variables.test.ts +61 -0
  72. package/src/services/local-variables.ts +39 -0
  73. package/src/services/logger-console.test.ts +118 -0
  74. package/src/services/logger-console.ts +98 -0
  75. package/src/services/logger.ts +57 -0
  76. package/src/services/scheduler-service.ts +86 -0
  77. package/src/services/schema-service.ts +33 -0
  78. package/src/services/scoped-secret-service.test.ts +74 -0
  79. package/src/services/scoped-secret-service.ts +41 -0
  80. package/src/services/secret-service.ts +38 -0
  81. package/src/services/trigger-service.ts +17 -0
  82. package/src/services/typed-secret-service.test.ts +93 -0
  83. package/src/services/typed-secret-service.ts +70 -0
  84. package/src/services/typed-variables-service.test.ts +73 -0
  85. package/src/services/typed-variables-service.ts +81 -0
  86. package/src/services/user-session-service.test.ts +113 -0
  87. package/src/services/user-session-service.ts +86 -0
  88. package/src/services/variables-service.ts +11 -0
  89. package/src/services/workflow-service.ts +95 -0
  90. package/src/testing/index.ts +2 -0
  91. package/src/testing/service-tests.ts +873 -0
  92. package/src/time-utils.test.ts +56 -0
  93. package/src/time-utils.ts +107 -0
  94. package/src/types/core.types.ts +478 -0
  95. package/src/types/state.types.ts +188 -0
  96. package/src/utils/hash.test.ts +68 -0
  97. package/src/utils/hash.ts +26 -0
  98. package/src/utils.test.ts +350 -0
  99. package/src/utils.ts +129 -0
  100. package/src/version.test.ts +80 -0
  101. package/src/version.ts +25 -0
  102. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  103. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  104. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  105. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  106. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  107. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  108. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  109. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  110. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  111. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  112. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  113. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  114. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  115. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  116. package/src/wirings/ai-agent/index.ts +37 -0
  117. package/src/wirings/channel/channel-common.ts +90 -0
  118. package/src/wirings/channel/channel-handler.ts +250 -0
  119. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  120. package/src/wirings/channel/channel-runner.ts +166 -0
  121. package/src/wirings/channel/channel-store.ts +29 -0
  122. package/src/wirings/channel/channel.types.ts +172 -0
  123. package/src/wirings/channel/define-channel-routes.ts +25 -0
  124. package/src/wirings/channel/eventhub-service.ts +34 -0
  125. package/src/wirings/channel/eventhub-store.ts +13 -0
  126. package/src/wirings/channel/index.ts +24 -0
  127. package/src/wirings/channel/local/index.ts +3 -0
  128. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  129. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  130. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  131. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  132. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  133. package/src/wirings/channel/log-channels.ts +20 -0
  134. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  135. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  136. package/src/wirings/channel/serverless/index.ts +5 -0
  137. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  138. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  139. package/src/wirings/cli/channel/index.ts +1 -0
  140. package/src/wirings/cli/cli-runner.test.ts +403 -0
  141. package/src/wirings/cli/cli-runner.ts +529 -0
  142. package/src/wirings/cli/cli.types.ts +358 -0
  143. package/src/wirings/cli/command-parser.test.ts +445 -0
  144. package/src/wirings/cli/command-parser.ts +504 -0
  145. package/src/wirings/cli/define-cli-commands.ts +24 -0
  146. package/src/wirings/cli/index.ts +19 -0
  147. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  148. package/src/wirings/gateway/gateway-runner.ts +411 -0
  149. package/src/wirings/gateway/gateway.types.ts +149 -0
  150. package/src/wirings/gateway/index.ts +13 -0
  151. package/src/wirings/http/http-routes.test.ts +322 -0
  152. package/src/wirings/http/http-routes.ts +197 -0
  153. package/src/wirings/http/http-runner.test.ts +144 -0
  154. package/src/wirings/http/http-runner.ts +588 -0
  155. package/src/wirings/http/http.types.ts +369 -0
  156. package/src/wirings/http/index.ts +28 -0
  157. package/src/wirings/http/log-http-routes.ts +22 -0
  158. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  159. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  160. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  161. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  162. package/src/wirings/http/routers/http-router.ts +13 -0
  163. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  164. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  165. package/src/wirings/http/web-request.test.ts +236 -0
  166. package/src/wirings/http/web-request.ts +104 -0
  167. package/src/wirings/mcp/index.ts +27 -0
  168. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  169. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  170. package/src/wirings/mcp/mcp-runner.ts +323 -0
  171. package/src/wirings/mcp/mcp.types.ts +216 -0
  172. package/src/wirings/node/index.ts +2 -0
  173. package/src/wirings/node/node.types.ts +23 -0
  174. package/src/wirings/oauth2/index.ts +3 -0
  175. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  176. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  177. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  178. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  179. package/src/wirings/queue/index.ts +31 -0
  180. package/src/wirings/queue/queue-runner.test.ts +710 -0
  181. package/src/wirings/queue/queue-runner.ts +192 -0
  182. package/src/wirings/queue/queue.types.ts +189 -0
  183. package/src/wirings/queue/register-queue-helper.ts +60 -0
  184. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  185. package/src/wirings/queue/validate-worker-config.ts +116 -0
  186. package/src/wirings/rpc/index.ts +4 -0
  187. package/src/wirings/rpc/rpc-runner.ts +460 -0
  188. package/src/wirings/rpc/rpc-types.ts +56 -0
  189. package/src/wirings/rpc/wire-addon.ts +21 -0
  190. package/src/wirings/scheduler/index.ts +11 -0
  191. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  192. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  193. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  194. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  195. package/src/wirings/secret/index.ts +9 -0
  196. package/src/wirings/secret/secret.types.ts +32 -0
  197. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  198. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  199. package/src/wirings/trigger/index.ts +10 -0
  200. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  201. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  202. package/src/wirings/trigger/trigger-runner.ts +135 -0
  203. package/src/wirings/trigger/trigger.types.ts +178 -0
  204. package/src/wirings/variable/index.ts +8 -0
  205. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  206. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  207. package/src/wirings/variable/variable.types.ts +22 -0
  208. package/src/wirings/workflow/dsl/index.ts +31 -0
  209. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  210. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  211. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  214. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  215. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  216. package/src/wirings/workflow/graph/index.ts +19 -0
  217. package/src/wirings/workflow/graph/template.test.ts +49 -0
  218. package/src/wirings/workflow/graph/template.ts +42 -0
  219. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  220. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  221. package/src/wirings/workflow/index.ts +82 -0
  222. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  223. package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
  224. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  225. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  226. package/src/wirings/workflow/workflow.types.ts +274 -0
  227. package/tsconfig.json +14 -0
  228. package/tsconfig.tsbuildinfo +1 -0
  229. package/lcov.info +0 -18891
@@ -0,0 +1,1153 @@
1
+ import type {
2
+ AIStreamChannel,
3
+ AIStreamEvent,
4
+ AIMessage,
5
+ AIToolCall,
6
+ AIToolResult,
7
+ PikkuAIMiddlewareHooks,
8
+ AgentRunState,
9
+ CoreAIAgent,
10
+ AIAgentMemoryConfig,
11
+ } from './ai-agent.types.js'
12
+ import { pikkuState, getSingletonServices } from '../../pikku-state.js'
13
+ import {
14
+ combineChannelMiddleware,
15
+ wrapChannelWithMiddleware,
16
+ } from '../channel/channel-middleware-runner.js'
17
+ import { randomUUID } from 'crypto'
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'
23
+
24
+ import {
25
+ parseWorkingMemory,
26
+ deepMergeWorkingMemory,
27
+ resolveMemoryServices,
28
+ loadContextMessages,
29
+ trimMessages,
30
+ } from './ai-agent-memory.js'
31
+ import {
32
+ prepareAgentRun,
33
+ resolveAgent,
34
+ buildInstructions,
35
+ buildToolDefs,
36
+ createScopedChannel,
37
+ ToolApprovalRequired,
38
+ type RunAIAgentParams,
39
+ type StreamAIAgentOptions,
40
+ type StreamContext,
41
+ } from './ai-agent-prepare.js'
42
+ import { resolveModelConfig } from './ai-agent-model-config.js'
43
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
44
+ import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
45
+
46
+ type PersistingChannel = AIStreamChannel & {
47
+ fullText: string
48
+ flush: () => Promise<void>
49
+ totalUsage: { inputTokens: number; outputTokens: number; model?: string }
50
+ }
51
+
52
+ function createPersistingChannel(
53
+ parent: AIStreamChannel,
54
+ storage: AIStorageService | undefined,
55
+ threadId: string
56
+ ): PersistingChannel {
57
+ let fullText = ''
58
+ let stepText = ''
59
+ let stepToolCalls: AIToolCall[] = []
60
+ let stepToolResults: AIToolResult[] = []
61
+ const totalUsage: {
62
+ inputTokens: number
63
+ outputTokens: number
64
+ model?: string
65
+ } = {
66
+ inputTokens: 0,
67
+ outputTokens: 0,
68
+ }
69
+
70
+ const flushStep = async () => {
71
+ if (!storage) return
72
+ const text = stepText
73
+ const calls = stepToolCalls
74
+ const results = stepToolResults
75
+ stepText = ''
76
+ stepToolCalls = []
77
+ stepToolResults = []
78
+ if (text || calls.length > 0) {
79
+ const assistantMsg: AIMessage = {
80
+ id: randomUUID(),
81
+ role: 'assistant',
82
+ content: text || undefined,
83
+ toolCalls: calls.length > 0 ? calls : undefined,
84
+ createdAt: new Date(),
85
+ }
86
+ const messages: AIMessage[] = [assistantMsg]
87
+ if (results.length > 0) {
88
+ messages.push({
89
+ id: randomUUID(),
90
+ role: 'tool',
91
+ toolResults: results,
92
+ createdAt: new Date(),
93
+ })
94
+ }
95
+ await storage.saveMessages(threadId, messages)
96
+ }
97
+ }
98
+
99
+ const channel: PersistingChannel = {
100
+ channelId: parent.channelId,
101
+ openingData: parent.openingData,
102
+ get state() {
103
+ return parent.state
104
+ },
105
+ get fullText() {
106
+ return fullText
107
+ },
108
+ get totalUsage() {
109
+ return totalUsage
110
+ },
111
+ flush: flushStep,
112
+ close: () => parent.close(),
113
+ sendBinary: (data) => parent.sendBinary(data),
114
+ send: (event: AIStreamEvent) => {
115
+ if (storage) {
116
+ switch (event.type) {
117
+ case 'text-delta':
118
+ stepText += event.text
119
+ fullText += event.text
120
+ break
121
+ case 'tool-call':
122
+ stepToolCalls.push({
123
+ id: event.toolCallId,
124
+ name: event.toolName,
125
+ args: event.args as Record<string, unknown>,
126
+ })
127
+ break
128
+ case 'tool-result':
129
+ stepToolResults.push({
130
+ id: event.toolCallId,
131
+ name: event.toolName,
132
+ result:
133
+ typeof event.result === 'string'
134
+ ? event.result
135
+ : JSON.stringify(event.result),
136
+ })
137
+ break
138
+ case 'usage':
139
+ totalUsage.inputTokens += event.tokens.input
140
+ totalUsage.outputTokens += event.tokens.output
141
+ if (event.model) totalUsage.model = event.model
142
+ flushStep()
143
+ break
144
+ case 'done':
145
+ flushStep()
146
+ break
147
+ }
148
+ }
149
+ parent.send(event)
150
+ },
151
+ }
152
+ return channel
153
+ }
154
+
155
+ async function postStreamCleanup(
156
+ persistingChannel: PersistingChannel,
157
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
158
+ singletonServices: any,
159
+ storage: AIStorageService | undefined,
160
+ memoryConfig: any,
161
+ threadId: string,
162
+ workingMemorySchemaName: string | null,
163
+ messages: AIMessage[],
164
+ aiRunState: AIRunStateService,
165
+ runId: string
166
+ ): Promise<void> {
167
+ const usage = persistingChannel.totalUsage
168
+ let outputText = persistingChannel.fullText
169
+ let outputMessages = messages
170
+ for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
171
+ const mw = aiMiddlewares[i]
172
+ if (mw.modifyOutput) {
173
+ const result = await mw.modifyOutput(singletonServices, {
174
+ text: outputText,
175
+ messages: outputMessages,
176
+ usage: {
177
+ inputTokens: usage.inputTokens,
178
+ outputTokens: usage.outputTokens,
179
+ },
180
+ })
181
+ outputText = result.text
182
+ outputMessages = result.messages
183
+ }
184
+ }
185
+
186
+ if (storage && memoryConfig?.workingMemory && outputText) {
187
+ const parsed = parseWorkingMemory(outputText)
188
+ if (parsed) {
189
+ const existing =
190
+ (await storage.getWorkingMemory(threadId, 'thread')) ?? {}
191
+ const merged = deepMergeWorkingMemory(existing, parsed)
192
+
193
+ if (singletonServices.schema && workingMemorySchemaName) {
194
+ try {
195
+ await singletonServices.schema.validateSchema(
196
+ workingMemorySchemaName,
197
+ merged
198
+ )
199
+ } catch (err) {
200
+ singletonServices.logger.warn(
201
+ `Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
202
+ )
203
+ }
204
+ }
205
+
206
+ await storage.saveWorkingMemory(threadId, 'thread', merged)
207
+ }
208
+ }
209
+
210
+ await aiRunState.updateRun(runId, {
211
+ status: 'completed',
212
+ ...(usage.model
213
+ ? {
214
+ usage: {
215
+ inputTokens: usage.inputTokens,
216
+ outputTokens: usage.outputTokens,
217
+ model: usage.model,
218
+ },
219
+ }
220
+ : {}),
221
+ })
222
+ }
223
+
224
+ type StepLoopParams = {
225
+ agent: CoreAIAgent
226
+ runnerParams: AIAgentRunnerParams
227
+ maxSteps: number
228
+ agentRunner: AIAgentRunnerService
229
+ persistingChannel: PersistingChannel
230
+ channel: AIStreamChannel
231
+ runId: string
232
+ aiMiddlewares: PikkuAIMiddlewareHooks[]
233
+ }
234
+
235
+ type StepLoopResult =
236
+ | { outcome: 'done' }
237
+ | { outcome: 'approval'; approvals: ToolApprovalRequired[] }
238
+
239
+ async function runStreamStepLoop(
240
+ params: StepLoopParams
241
+ ): Promise<StepLoopResult> {
242
+ const {
243
+ agent,
244
+ runnerParams,
245
+ maxSteps,
246
+ agentRunner,
247
+ persistingChannel,
248
+ channel,
249
+ runId,
250
+ aiMiddlewares,
251
+ } = params
252
+
253
+ const singletonServices = getSingletonServices()
254
+
255
+ for (let step = 0; step < maxSteps; step++) {
256
+ if (agent.prepareStep) {
257
+ let stopped = false
258
+ await agent.prepareStep({
259
+ stepNumber: step,
260
+ messages: runnerParams.messages,
261
+ tools: runnerParams.tools,
262
+ toolChoice: runnerParams.toolChoice,
263
+ model: runnerParams.model,
264
+ stop: () => {
265
+ stopped = true
266
+ },
267
+ })
268
+ if (stopped) break
269
+ }
270
+
271
+ channel.send({ type: 'step-start', stepNumber: step })
272
+
273
+ const stepResult = await agentRunner.stream(runnerParams, persistingChannel)
274
+
275
+ for (const mw of aiMiddlewares) {
276
+ if (mw.afterStep) {
277
+ await mw.afterStep(singletonServices, {
278
+ stepNumber: step,
279
+ text: stepResult.text,
280
+ toolCalls: stepResult.toolCalls,
281
+ toolResults: stepResult.toolResults,
282
+ usage: stepResult.usage,
283
+ finishReason: stepResult.finishReason,
284
+ })
285
+ }
286
+ }
287
+
288
+ if (stepResult.toolCalls.length === 0) break
289
+
290
+ const approvalsNeeded = checkForApprovals(
291
+ stepResult,
292
+ runnerParams.tools,
293
+ runId
294
+ )
295
+ if (approvalsNeeded.length > 0) {
296
+ // For each approval, call approvalDescriptionFn if available
297
+ for (const approval of approvalsNeeded) {
298
+ const toolDef = runnerParams.tools.find(
299
+ (t) => t.name === approval.toolName
300
+ )
301
+ if (toolDef?.approvalDescriptionFn && !approval.reason) {
302
+ try {
303
+ approval.reason = await toolDef.approvalDescriptionFn(approval.args)
304
+ } catch {
305
+ // If description generation fails, continue without it
306
+ }
307
+ }
308
+ }
309
+ return { outcome: 'approval', approvals: approvalsNeeded }
310
+ }
311
+
312
+ appendStepMessages(runnerParams, stepResult)
313
+ }
314
+
315
+ return { outcome: 'done' }
316
+ }
317
+
318
+ export function checkForApprovals(
319
+ stepResult: AIAgentStepResult,
320
+ tools: AIAgentRunnerParams['tools'],
321
+ runId: string
322
+ ): ToolApprovalRequired[] {
323
+ const approvals: ToolApprovalRequired[] = []
324
+ for (const tc of stepResult.toolCalls) {
325
+ const toolDef = tools.find((t) => t.name === tc.toolName)
326
+
327
+ if (toolDef?.needsApproval) {
328
+ approvals.push(
329
+ new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args)
330
+ )
331
+ continue
332
+ }
333
+
334
+ const tr = stepResult.toolResults.find(
335
+ (r) => r.toolCallId === tc.toolCallId
336
+ )
337
+ if (
338
+ tr?.result &&
339
+ typeof tr.result === 'object' &&
340
+ '__approvalRequired' in (tr.result as object)
341
+ ) {
342
+ const r = tr.result as {
343
+ toolName: string
344
+ args: unknown
345
+ reason?: string
346
+ displayToolName?: string
347
+ displayArgs?: unknown
348
+ agentRunId?: string
349
+ subApprovals?: Array<{
350
+ toolCallId: string
351
+ toolName: string
352
+ args: unknown
353
+ runId: string
354
+ }>
355
+ }
356
+ if (r.subApprovals?.length) {
357
+ for (const sub of r.subApprovals) {
358
+ approvals.push(
359
+ new ToolApprovalRequired(
360
+ sub.toolCallId,
361
+ r.toolName,
362
+ r.args,
363
+ undefined,
364
+ sub.toolName,
365
+ sub.args,
366
+ r.agentRunId
367
+ )
368
+ )
369
+ }
370
+ } else {
371
+ approvals.push(
372
+ new ToolApprovalRequired(
373
+ tc.toolCallId,
374
+ r.toolName,
375
+ r.args,
376
+ r.reason,
377
+ r.displayToolName,
378
+ r.displayArgs,
379
+ r.agentRunId
380
+ )
381
+ )
382
+ }
383
+ }
384
+ }
385
+ return approvals
386
+ }
387
+
388
+ export function appendStepMessages(
389
+ runnerParams: AIAgentRunnerParams,
390
+ stepResult: AIAgentStepResult
391
+ ): void {
392
+ const assistantMsg: AIMessage = {
393
+ id: randomUUID(),
394
+ role: 'assistant',
395
+ content: stepResult.text || undefined,
396
+ toolCalls:
397
+ stepResult.toolCalls.length > 0
398
+ ? stepResult.toolCalls.map((tc) => ({
399
+ id: tc.toolCallId,
400
+ name: tc.toolName,
401
+ args: tc.args as Record<string, unknown>,
402
+ }))
403
+ : undefined,
404
+ createdAt: new Date(),
405
+ }
406
+ runnerParams.messages.push(assistantMsg)
407
+
408
+ if (stepResult.toolResults.length > 0) {
409
+ const toolMsg: AIMessage = {
410
+ id: randomUUID(),
411
+ role: 'tool',
412
+ toolResults: stepResult.toolResults.map((tr) => ({
413
+ id: tr.toolCallId,
414
+ name: tr.toolName,
415
+ result:
416
+ typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result),
417
+ })),
418
+ createdAt: new Date(),
419
+ }
420
+ runnerParams.messages.push(toolMsg)
421
+ }
422
+ }
423
+
424
+ function handleApprovals(
425
+ approvals: ToolApprovalRequired[],
426
+ runId: string,
427
+ channel: AIStreamChannel,
428
+ aiRunState: AIRunStateService,
429
+ persistingChannel: PersistingChannel
430
+ ): Promise<void> {
431
+ return (async () => {
432
+ await persistingChannel.flush()
433
+
434
+ const pendingApprovals = approvals.map((err) =>
435
+ err.agentRunId
436
+ ? {
437
+ type: 'agent-call' as const,
438
+ toolCallId: err.toolCallId,
439
+ agentName: err.toolName,
440
+ agentRunId: err.agentRunId,
441
+ displayToolName: err.displayToolName ?? err.toolName,
442
+ displayArgs: err.displayArgs ?? err.args,
443
+ }
444
+ : {
445
+ type: 'tool-call' as const,
446
+ toolCallId: err.toolCallId,
447
+ toolName: err.toolName,
448
+ args: err.args,
449
+ }
450
+ )
451
+
452
+ await aiRunState.updateRun(runId, {
453
+ status: 'suspended',
454
+ suspendReason: 'approval',
455
+ pendingApprovals,
456
+ })
457
+
458
+ for (const err of approvals) {
459
+ const approvalEvent = {
460
+ type: 'approval-request' as const,
461
+ toolCallId: err.toolCallId,
462
+ toolName: err.displayToolName ?? err.toolName,
463
+ args: err.displayArgs ?? err.args,
464
+ reason: err.reason,
465
+ runId,
466
+ }
467
+ channel.send(approvalEvent as any)
468
+ }
469
+ channel.send({ type: 'done' })
470
+ channel.close()
471
+ })()
472
+ }
473
+
474
+ export async function streamAIAgent(
475
+ agentName: string,
476
+ input: {
477
+ message: string
478
+ threadId: string
479
+ resourceId: string
480
+ model?: string
481
+ temperature?: number
482
+ },
483
+ channel: AIStreamChannel,
484
+ params: RunAIAgentParams,
485
+ agentSessionMap?: Map<string, string>,
486
+ options?: StreamAIAgentOptions
487
+ ): Promise<string> {
488
+ const sessionMap = agentSessionMap ?? new Map<string, string>()
489
+
490
+ const normalizedInput = input
491
+
492
+ const streamContext: StreamContext = { channel, options }
493
+ // delegateState is attached after prepareAgentRun resolves the agent config
494
+
495
+ const {
496
+ agent,
497
+ packageName,
498
+ resolvedName,
499
+ agentRunner,
500
+ storage,
501
+ memoryConfig,
502
+ threadId,
503
+ userMessage,
504
+ runnerParams,
505
+ maxSteps,
506
+ missingRpcs,
507
+ workingMemorySchemaName,
508
+ } = await prepareAgentRun(
509
+ agentName,
510
+ normalizedInput,
511
+ params,
512
+ sessionMap,
513
+ streamContext
514
+ )
515
+
516
+ const singletonServices = getSingletonServices()
517
+ const { aiRunState } = singletonServices
518
+ if (!aiRunState) {
519
+ throw new Error('AIRunStateService not available in singletonServices')
520
+ }
521
+
522
+ if (missingRpcs.length > 0) {
523
+ await aiRunState.createRun({
524
+ agentName,
525
+ threadId,
526
+ resourceId: normalizedInput.resourceId,
527
+ status: 'suspended',
528
+ suspendReason: 'rpc-missing',
529
+ missingRpcs,
530
+ usage: { inputTokens: 0, outputTokens: 0, model: agent.model },
531
+ createdAt: new Date(),
532
+ updatedAt: new Date(),
533
+ })
534
+ channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs })
535
+ channel.send({ type: 'done' })
536
+ return ''
537
+ }
538
+
539
+ const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
540
+
541
+ let modifiedMessages = runnerParams.messages
542
+ let modifiedInstructions = runnerParams.instructions
543
+ for (const mw of aiMiddlewares) {
544
+ if (mw.modifyInput) {
545
+ const result = await mw.modifyInput(singletonServices, {
546
+ messages: modifiedMessages,
547
+ instructions: modifiedInstructions,
548
+ })
549
+ modifiedMessages = result.messages
550
+ modifiedInstructions = result.instructions
551
+ }
552
+ }
553
+ runnerParams.messages = modifiedMessages
554
+ runnerParams.instructions = modifiedInstructions
555
+
556
+ const runId = await aiRunState.createRun({
557
+ agentName,
558
+ threadId,
559
+ resourceId: normalizedInput.resourceId,
560
+ status: 'running',
561
+ usage: { inputTokens: 0, outputTokens: 0, model: agent.model },
562
+ createdAt: new Date(),
563
+ updatedAt: new Date(),
564
+ })
565
+
566
+ if (storage) {
567
+ await storage.saveMessages(threadId, [userMessage])
568
+ }
569
+
570
+ const streamMiddleware = aiMiddlewares
571
+ .filter((mw) => mw.modifyOutputStream)
572
+ .map((mw) => {
573
+ const state: Record<string, unknown> = {}
574
+ const allEvents: AIStreamEvent[] = []
575
+ return async (services: any, event: any, next: any) => {
576
+ allEvents.push(event)
577
+ const result = await mw.modifyOutputStream!(services, {
578
+ event,
579
+ allEvents,
580
+ state,
581
+ })
582
+ if (result == null) return
583
+ if (Array.isArray(result)) {
584
+ for (const r of result) await next(r)
585
+ } else {
586
+ await next(result)
587
+ }
588
+ }
589
+ })
590
+
591
+ const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
592
+ const meta = agentsMeta[resolvedName]
593
+ const allChannelMiddleware = combineChannelMiddleware(
594
+ 'agent',
595
+ `stream:${agentName}`,
596
+ {
597
+ wireInheritedChannelMiddleware: meta?.channelMiddleware,
598
+ wireChannelMiddleware: [
599
+ ...((agent.channelMiddleware as any[]) ?? []),
600
+ ...streamMiddleware,
601
+ ],
602
+ }
603
+ )
604
+
605
+ const wrappedChannel =
606
+ allChannelMiddleware.length > 0
607
+ ? (wrapChannelWithMiddleware(
608
+ { channel },
609
+ singletonServices,
610
+ allChannelMiddleware
611
+ ).channel as AIStreamChannel)
612
+ : channel
613
+
614
+ // In delegate mode (default), suppress parent's text from reaching the client
615
+ // AFTER a sub-agent has been called. If the parent responds directly (no delegation),
616
+ // its text goes through normally. Sub-agent text bypasses this path entirely
617
+ // (goes through subChannel → channel directly).
618
+ const isDelegateMode = agent.agentMode !== 'supervise' && meta.agents?.length
619
+ const delegateState = { delegated: false }
620
+ if (isDelegateMode) {
621
+ streamContext.delegateState = delegateState
622
+ }
623
+ const outputChannel = isDelegateMode
624
+ ? {
625
+ ...wrappedChannel,
626
+ send: (event: AIStreamEvent) => {
627
+ if (
628
+ delegateState.delegated &&
629
+ (event.type === 'text-delta' || event.type === 'reasoning-delta')
630
+ )
631
+ return
632
+ wrappedChannel.send(event)
633
+ },
634
+ delegateState,
635
+ }
636
+ : wrappedChannel
637
+
638
+ const persistingChannel = createPersistingChannel(
639
+ outputChannel,
640
+ storage,
641
+ threadId
642
+ )
643
+
644
+ try {
645
+ const loopResult = await runStreamStepLoop({
646
+ agent,
647
+ runnerParams,
648
+ maxSteps,
649
+ agentRunner,
650
+ persistingChannel,
651
+ channel,
652
+ runId,
653
+ aiMiddlewares,
654
+ })
655
+
656
+ if (loopResult.outcome === 'approval') {
657
+ await handleApprovals(
658
+ loopResult.approvals,
659
+ runId,
660
+ channel,
661
+ aiRunState,
662
+ persistingChannel
663
+ )
664
+ return persistingChannel.fullText
665
+ }
666
+
667
+ await postStreamCleanup(
668
+ persistingChannel,
669
+ aiMiddlewares,
670
+ singletonServices,
671
+ storage,
672
+ memoryConfig,
673
+ threadId,
674
+ workingMemorySchemaName,
675
+ runnerParams.messages,
676
+ aiRunState,
677
+ runId
678
+ )
679
+
680
+ channel.send({ type: 'done' })
681
+ channel.close()
682
+ return persistingChannel.fullText
683
+ } catch (err) {
684
+ for (const mw of aiMiddlewares) {
685
+ if (mw.onError) {
686
+ try {
687
+ await mw.onError(singletonServices, {
688
+ error: err instanceof Error ? err : new Error(String(err)),
689
+ stepNumber: -1,
690
+ messages: runnerParams.messages,
691
+ })
692
+ } catch {
693
+ // onError hooks must not affect error flow
694
+ }
695
+ }
696
+ }
697
+ await aiRunState.updateRun(runId, {
698
+ status: 'failed',
699
+ errorMessage: err instanceof Error ? err.message : String(err),
700
+ })
701
+ channel.send({
702
+ type: 'error',
703
+ message: err instanceof Error ? err.message : String(err),
704
+ })
705
+ channel.send({ type: 'done' })
706
+ channel.close()
707
+ return persistingChannel.fullText
708
+ }
709
+ }
710
+
711
+ export async function resumeAIAgent(
712
+ input: {
713
+ runId: string
714
+ toolCallId: string
715
+ approved: boolean
716
+ },
717
+ channel: AIStreamChannel,
718
+ params: RunAIAgentParams,
719
+ options?: StreamAIAgentOptions
720
+ ): Promise<void> {
721
+ const singletonServices = getSingletonServices()
722
+ const { aiRunState } = singletonServices
723
+ if (!aiRunState) {
724
+ throw new Error('AIRunStateService not available in singletonServices')
725
+ }
726
+
727
+ const run = await aiRunState.getRun(input.runId)
728
+ if (!run) {
729
+ throw new Error(`No run found for runId ${input.runId}`)
730
+ }
731
+
732
+ const pending = run.pendingApprovals?.find(
733
+ (p) => p.toolCallId === input.toolCallId
734
+ )
735
+ if (!pending) {
736
+ throw new Error(
737
+ `No pending approval for toolCallId ${input.toolCallId} on run ${input.runId}`
738
+ )
739
+ }
740
+
741
+ await aiRunState.resolveApproval(
742
+ input.toolCallId,
743
+ input.approved ? 'approved' : 'denied'
744
+ )
745
+
746
+ const { agent, packageName, resolvedName } = resolveAgent(run.agentName)
747
+ const { storage } = resolveMemoryServices(agent, singletonServices)
748
+ const memoryConfig = agent.memory
749
+ const agentRunner = singletonServices.aiAgentRunner
750
+ if (!agentRunner) {
751
+ throw new Error('AIAgentRunnerService not available in singletonServices')
752
+ }
753
+
754
+ if (!input.approved) {
755
+ if (pending.type === 'agent-call') {
756
+ await aiRunState.updateRun(pending.agentRunId, { status: 'failed' })
757
+ }
758
+
759
+ const denialResult =
760
+ 'The user explicitly declined this action. Inform them that it was declined and do not retry.'
761
+
762
+ if (storage) {
763
+ await storage.saveMessages(run.threadId, [
764
+ {
765
+ id: randomUUID(),
766
+ role: 'tool',
767
+ toolResults: [
768
+ {
769
+ id: input.toolCallId,
770
+ name:
771
+ pending.type === 'tool-call'
772
+ ? pending.toolName
773
+ : pending.agentName,
774
+ result: denialResult,
775
+ },
776
+ ],
777
+ createdAt: new Date(),
778
+ },
779
+ ])
780
+ }
781
+
782
+ // Check remaining pending approvals
783
+ const updatedRun = await aiRunState.getRun(run.runId)
784
+ const remaining = updatedRun?.pendingApprovals ?? []
785
+
786
+ if (remaining.length > 0) {
787
+ // Still waiting for more approvals - don't continue step loop
788
+ channel.send({ type: 'done' })
789
+ channel.close()
790
+ return
791
+ }
792
+
793
+ await aiRunState.updateRun(run.runId, { status: 'running' })
794
+
795
+ await continueAfterToolResult(
796
+ run,
797
+ agent,
798
+ packageName,
799
+ resolvedName,
800
+ storage,
801
+ memoryConfig,
802
+ agentRunner,
803
+ channel,
804
+ params,
805
+ aiRunState,
806
+ options
807
+ )
808
+ return
809
+ }
810
+
811
+ if (pending.type === 'agent-call') {
812
+ const subRun = await aiRunState.getRun(pending.agentRunId)
813
+ if (!subRun) {
814
+ throw new Error(`Sub-agent run not found: ${pending.agentRunId}`)
815
+ }
816
+ const subPending =
817
+ subRun.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId) ??
818
+ subRun.pendingApprovals?.[0]
819
+ if (!subPending) {
820
+ throw new Error(
821
+ `No pending approval on sub-agent run ${pending.agentRunId}`
822
+ )
823
+ }
824
+
825
+ const subChannel = createScopedChannel(channel, subRun.agentName, 'resume')
826
+ channel.send({
827
+ type: 'agent-call',
828
+ agentName: subRun.agentName,
829
+ session: 'resume',
830
+ input: null,
831
+ })
832
+
833
+ await resumeAIAgent(
834
+ {
835
+ runId: pending.agentRunId,
836
+ toolCallId: subPending.toolCallId,
837
+ approved: true,
838
+ },
839
+ subChannel,
840
+ params,
841
+ options
842
+ )
843
+
844
+ channel.send({
845
+ type: 'agent-result',
846
+ agentName: subRun.agentName,
847
+ session: 'resume',
848
+ result: null,
849
+ })
850
+
851
+ if (storage) {
852
+ await storage.saveMessages(run.threadId, [
853
+ {
854
+ id: randomUUID(),
855
+ role: 'tool',
856
+ toolResults: [
857
+ {
858
+ id: input.toolCallId,
859
+ name: pending.agentName,
860
+ result: 'Sub-agent completed successfully',
861
+ },
862
+ ],
863
+ createdAt: new Date(),
864
+ },
865
+ ])
866
+ }
867
+ } else {
868
+ const streamContext: StreamContext = {
869
+ channel,
870
+ options: { ...options, requiresToolApproval: false },
871
+ }
872
+ const aiMiddlewaresForResume: PikkuAIMiddlewareHooks[] =
873
+ agent.aiMiddleware ?? []
874
+ const { tools } = await buildToolDefs(
875
+ params,
876
+ new Map<string, string>(),
877
+ run.resourceId,
878
+ resolvedName,
879
+ packageName,
880
+ streamContext,
881
+ aiMiddlewaresForResume
882
+ )
883
+
884
+ const matchingTool = tools.find((t) => t.name === pending.toolName)
885
+ if (!matchingTool) {
886
+ throw new Error(
887
+ `Tool "${pending.toolName}" not found in agent definition`
888
+ )
889
+ }
890
+
891
+ const toolArgs =
892
+ typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
893
+
894
+ let toolResult: unknown
895
+ let isError = false
896
+ try {
897
+ toolResult = await matchingTool.execute(toolArgs)
898
+ } catch (execErr) {
899
+ toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
900
+ isError = true
901
+ }
902
+
903
+ const resultStr =
904
+ typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult)
905
+ if (storage) {
906
+ await storage.saveMessages(run.threadId, [
907
+ {
908
+ id: randomUUID(),
909
+ role: 'tool',
910
+ toolResults: [
911
+ {
912
+ id: input.toolCallId,
913
+ name: pending.toolName,
914
+ result: resultStr,
915
+ },
916
+ ],
917
+ createdAt: new Date(),
918
+ },
919
+ ])
920
+ }
921
+
922
+ channel.send({
923
+ type: 'tool-result',
924
+ toolCallId: input.toolCallId,
925
+ toolName: pending.toolName,
926
+ result: toolResult,
927
+ ...(isError ? { isError: true } : {}),
928
+ })
929
+ }
930
+
931
+ // Check remaining pending approvals after processing this one
932
+ const updatedRun = await aiRunState.getRun(run.runId)
933
+ const remaining = updatedRun?.pendingApprovals ?? []
934
+
935
+ if (remaining.length > 0) {
936
+ // Still waiting for more approvals - don't continue step loop
937
+ channel.send({ type: 'done' })
938
+ channel.close()
939
+ return
940
+ }
941
+
942
+ await aiRunState.updateRun(run.runId, { status: 'running' })
943
+
944
+ await continueAfterToolResult(
945
+ run,
946
+ agent,
947
+ packageName,
948
+ resolvedName,
949
+ storage,
950
+ memoryConfig,
951
+ agentRunner,
952
+ channel,
953
+ params,
954
+ aiRunState,
955
+ options
956
+ )
957
+ }
958
+
959
+ async function continueAfterToolResult(
960
+ run: AgentRunState,
961
+ agent: CoreAIAgent,
962
+ packageName: string | null,
963
+ resolvedName: string,
964
+ storage: AIStorageService | undefined,
965
+ memoryConfig: AIAgentMemoryConfig | undefined,
966
+ agentRunner: AIAgentRunnerService,
967
+ channel: AIStreamChannel,
968
+ params: RunAIAgentParams,
969
+ aiRunState: AIRunStateService,
970
+ options?: StreamAIAgentOptions
971
+ ): Promise<void> {
972
+ const singletonServices = getSingletonServices()
973
+ const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
974
+ const meta = agentsMeta[resolvedName]
975
+ const workingMemorySchemaName = meta?.workingMemorySchema ?? null
976
+
977
+ const messages = storage
978
+ ? await storage.getMessages(run.threadId, {
979
+ lastN: memoryConfig?.lastMessages ?? 20,
980
+ })
981
+ : []
982
+
983
+ const workingMemoryJsonSchema = workingMemorySchemaName
984
+ ? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
985
+ : undefined
986
+
987
+ const contextMessages = await loadContextMessages(
988
+ memoryConfig,
989
+ storage,
990
+ { message: '', threadId: run.threadId, resourceId: run.resourceId },
991
+ workingMemoryJsonSchema
992
+ )
993
+
994
+ const allMessages = [...contextMessages, ...messages]
995
+ const trimmedMessages = trimMessages(allMessages)
996
+
997
+ const instructions = await buildInstructions(resolvedName, packageName)
998
+
999
+ const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
1000
+ let modifiedMessages = trimmedMessages
1001
+ let modifiedInstructions = instructions
1002
+ for (const mw of aiMiddlewares) {
1003
+ if (mw.modifyInput) {
1004
+ const result = await mw.modifyInput(singletonServices, {
1005
+ messages: modifiedMessages,
1006
+ instructions: modifiedInstructions,
1007
+ })
1008
+ modifiedMessages = result.messages
1009
+ modifiedInstructions = result.instructions
1010
+ }
1011
+ }
1012
+
1013
+ const streamMiddleware = aiMiddlewares
1014
+ .filter((mw) => mw.modifyOutputStream)
1015
+ .map((mw) => {
1016
+ const state: Record<string, unknown> = {}
1017
+ const allEvents: AIStreamEvent[] = []
1018
+ return async (services: any, event: any, next: any) => {
1019
+ allEvents.push(event)
1020
+ const result = await mw.modifyOutputStream!(services, {
1021
+ event,
1022
+ allEvents,
1023
+ state,
1024
+ })
1025
+ if (result == null) return
1026
+ if (Array.isArray(result)) {
1027
+ for (const r of result) await next(r)
1028
+ } else {
1029
+ await next(result)
1030
+ }
1031
+ }
1032
+ })
1033
+
1034
+ const allChannelMiddleware = combineChannelMiddleware(
1035
+ 'agent',
1036
+ `stream:${run.agentName}`,
1037
+ {
1038
+ wireInheritedChannelMiddleware: meta?.channelMiddleware,
1039
+ wireChannelMiddleware: [
1040
+ ...((agent.channelMiddleware as any[]) ?? []),
1041
+ ...streamMiddleware,
1042
+ ],
1043
+ }
1044
+ )
1045
+
1046
+ const wrappedChannel =
1047
+ allChannelMiddleware.length > 0
1048
+ ? (wrapChannelWithMiddleware(
1049
+ { channel },
1050
+ singletonServices,
1051
+ allChannelMiddleware
1052
+ ).channel as AIStreamChannel)
1053
+ : channel
1054
+
1055
+ const persistingChannel = createPersistingChannel(
1056
+ wrappedChannel,
1057
+ storage,
1058
+ run.threadId
1059
+ )
1060
+
1061
+ const streamContext: StreamContext = { channel, options }
1062
+ const resumeTools = (
1063
+ await buildToolDefs(
1064
+ params,
1065
+ new Map<string, string>(),
1066
+ run.resourceId,
1067
+ resolvedName,
1068
+ packageName,
1069
+ streamContext,
1070
+ aiMiddlewares
1071
+ )
1072
+ ).tools
1073
+
1074
+ const resolved = resolveModelConfig(resolvedName, agent)
1075
+ const maxSteps = resolved.maxSteps ?? 10
1076
+
1077
+ const runnerParams: AIAgentRunnerParams = {
1078
+ model: resolved.model,
1079
+ temperature: resolved.temperature,
1080
+ instructions: modifiedInstructions,
1081
+ messages: modifiedMessages,
1082
+ tools: resumeTools,
1083
+ maxSteps: 1,
1084
+ toolChoice: (agent.toolChoice ?? 'auto') as 'auto' | 'required' | 'none',
1085
+ outputSchema: meta?.outputSchema
1086
+ ? pikkuState(packageName, 'misc', 'schemas').get(meta.outputSchema)
1087
+ : undefined,
1088
+ }
1089
+
1090
+ try {
1091
+ const loopResult = await runStreamStepLoop({
1092
+ agent,
1093
+ runnerParams,
1094
+ maxSteps,
1095
+ agentRunner,
1096
+ persistingChannel,
1097
+ channel,
1098
+ runId: run.runId,
1099
+ aiMiddlewares,
1100
+ })
1101
+
1102
+ if (loopResult.outcome === 'approval') {
1103
+ await handleApprovals(
1104
+ loopResult.approvals,
1105
+ run.runId,
1106
+ channel,
1107
+ aiRunState,
1108
+ persistingChannel
1109
+ )
1110
+ return
1111
+ }
1112
+
1113
+ await postStreamCleanup(
1114
+ persistingChannel,
1115
+ aiMiddlewares,
1116
+ singletonServices,
1117
+ storage,
1118
+ memoryConfig,
1119
+ run.threadId,
1120
+ workingMemorySchemaName,
1121
+ runnerParams.messages,
1122
+ aiRunState,
1123
+ run.runId
1124
+ )
1125
+
1126
+ channel.send({ type: 'done' })
1127
+ channel.close()
1128
+ } catch (err) {
1129
+ for (const mw of aiMiddlewares) {
1130
+ if (mw.onError) {
1131
+ try {
1132
+ await mw.onError(singletonServices, {
1133
+ error: err instanceof Error ? err : new Error(String(err)),
1134
+ stepNumber: -1,
1135
+ messages: runnerParams.messages,
1136
+ })
1137
+ } catch {
1138
+ // onError hooks must not affect error flow
1139
+ }
1140
+ }
1141
+ }
1142
+ await aiRunState.updateRun(run.runId, {
1143
+ status: 'failed',
1144
+ errorMessage: err instanceof Error ? err.message : String(err),
1145
+ })
1146
+ channel.send({
1147
+ type: 'error',
1148
+ message: err instanceof Error ? err.message : String(err),
1149
+ })
1150
+ channel.send({ type: 'done' })
1151
+ channel.close()
1152
+ }
1153
+ }