@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
@@ -3,7 +3,8 @@ import assert from 'node:assert/strict'
3
3
 
4
4
  import { resetPikkuState, pikkuState } from '../../pikku-state.js'
5
5
  import { runAIAgent } from './ai-agent-runner.js'
6
- import type { CoreAIAgent } from './ai-agent.types.js'
6
+ import type { CoreAIAgent, PikkuAIMiddlewareHooks } from './ai-agent.types.js'
7
+ import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
7
8
 
8
9
  beforeEach(() => {
9
10
  resetPikkuState()
@@ -29,6 +30,17 @@ const addTestAgent = (agentName: string) => {
29
30
  })
30
31
  }
31
32
 
33
+ const makeStepResult = (
34
+ overrides?: Partial<AIAgentStepResult>
35
+ ): AIAgentStepResult => ({
36
+ text: '',
37
+ toolCalls: [],
38
+ toolResults: [],
39
+ usage: { inputTokens: 0, outputTokens: 0 },
40
+ finishReason: 'stop',
41
+ ...overrides,
42
+ })
43
+
32
44
  describe('runAIAgent', () => {
33
45
  test('marks run as failed when runner throws', async () => {
34
46
  addTestAgent('failing-agent')
@@ -36,7 +48,7 @@ describe('runAIAgent', () => {
36
48
  const updates: unknown[] = []
37
49
  const expectedError = new Error('runner failed')
38
50
 
39
- const singletonServices = {
51
+ const mockServices = {
40
52
  logger: {
41
53
  info: () => {},
42
54
  warn: () => {},
@@ -56,6 +68,8 @@ describe('runAIAgent', () => {
56
68
  },
57
69
  } as any
58
70
 
71
+ pikkuState(null, 'package', 'singletonServices', mockServices)
72
+
59
73
  await assert.rejects(
60
74
  () =>
61
75
  runAIAgent(
@@ -65,11 +79,239 @@ describe('runAIAgent', () => {
65
79
  threadId: 'thread-1',
66
80
  resourceId: 'resource-1',
67
81
  },
68
- { singletonServices }
82
+ {}
69
83
  ),
70
84
  expectedError
71
85
  )
72
86
 
73
87
  assert.deepEqual(updates, [{ status: 'failed' }])
74
88
  })
89
+
90
+ test('loops through multiple steps and accumulates usage', async () => {
91
+ addTestAgent('multi-step-agent')
92
+
93
+ let callCount = 0
94
+ const mockServices = {
95
+ logger: {
96
+ info: () => {},
97
+ warn: () => {},
98
+ error: () => {},
99
+ debug: () => {},
100
+ },
101
+ aiAgentRunner: {
102
+ run: async (): Promise<AIAgentStepResult> => {
103
+ callCount++
104
+ if (callCount === 1) {
105
+ return makeStepResult({
106
+ text: '',
107
+ toolCalls: [
108
+ { toolCallId: 'tc-1', toolName: 'myTool', args: { q: 'test' } },
109
+ ],
110
+ toolResults: [
111
+ {
112
+ toolCallId: 'tc-1',
113
+ toolName: 'myTool',
114
+ result: 'tool output',
115
+ },
116
+ ],
117
+ usage: { inputTokens: 100, outputTokens: 50 },
118
+ finishReason: 'tool-calls',
119
+ })
120
+ }
121
+ return makeStepResult({
122
+ text: 'Final answer',
123
+ usage: { inputTokens: 80, outputTokens: 30 },
124
+ finishReason: 'stop',
125
+ })
126
+ },
127
+ },
128
+ aiRunState: {
129
+ createRun: async () => 'run-multi',
130
+ updateRun: async () => {},
131
+ },
132
+ } as any
133
+
134
+ pikkuState(null, 'package', 'singletonServices', mockServices)
135
+
136
+ const result = await runAIAgent(
137
+ 'multi-step-agent',
138
+ {
139
+ message: 'hello',
140
+ threadId: 'thread-multi',
141
+ resourceId: 'resource-multi',
142
+ },
143
+ {}
144
+ )
145
+
146
+ assert.equal(callCount, 2)
147
+ assert.equal(result.text, 'Final answer')
148
+ assert.equal(result.usage.inputTokens, 180)
149
+ assert.equal(result.usage.outputTokens, 80)
150
+ assert.equal(result.steps.length, 2)
151
+ })
152
+
153
+ test('afterStep hook is called for each step', async () => {
154
+ addTestAgent('afterstep-agent')
155
+
156
+ const afterStepCalls: unknown[] = []
157
+ const middleware: PikkuAIMiddlewareHooks = {
158
+ afterStep: async (_services, ctx) => {
159
+ afterStepCalls.push(ctx)
160
+ },
161
+ }
162
+ const agent = pikkuState(null, 'agent', 'agents').get('afterstep-agent')!
163
+ agent.aiMiddleware = [middleware] as any
164
+ pikkuState(null, 'agent', 'agents').set('afterstep-agent', agent)
165
+
166
+ let callCount = 0
167
+ const mockServices = {
168
+ logger: {
169
+ info: () => {},
170
+ warn: () => {},
171
+ error: () => {},
172
+ debug: () => {},
173
+ },
174
+ aiAgentRunner: {
175
+ run: async (): Promise<AIAgentStepResult> => {
176
+ callCount++
177
+ if (callCount === 1) {
178
+ return makeStepResult({
179
+ text: 'thinking...',
180
+ toolCalls: [
181
+ { toolCallId: 'tc-1', toolName: 'myTool', args: { q: 'test' } },
182
+ ],
183
+ toolResults: [
184
+ { toolCallId: 'tc-1', toolName: 'myTool', result: 'output' },
185
+ ],
186
+ usage: { inputTokens: 100, outputTokens: 50 },
187
+ finishReason: 'tool-calls',
188
+ })
189
+ }
190
+ return makeStepResult({
191
+ text: 'done',
192
+ usage: { inputTokens: 80, outputTokens: 30 },
193
+ finishReason: 'stop',
194
+ })
195
+ },
196
+ },
197
+ aiRunState: {
198
+ createRun: async () => 'run-afterstep',
199
+ updateRun: async () => {},
200
+ },
201
+ } as any
202
+
203
+ pikkuState(null, 'package', 'singletonServices', mockServices)
204
+
205
+ await runAIAgent(
206
+ 'afterstep-agent',
207
+ { message: 'hi', threadId: 't1', resourceId: 'r1' },
208
+ {}
209
+ )
210
+
211
+ assert.equal(afterStepCalls.length, 2)
212
+ const first = afterStepCalls[0] as any
213
+ assert.equal(first.stepNumber, 0)
214
+ assert.equal(first.finishReason, 'tool-calls')
215
+ assert.equal(first.toolCalls.length, 1)
216
+ const second = afterStepCalls[1] as any
217
+ assert.equal(second.stepNumber, 1)
218
+ assert.equal(second.finishReason, 'stop')
219
+ assert.equal(second.text, 'done')
220
+ })
221
+
222
+ test('onError hook is called when runner throws', async () => {
223
+ addTestAgent('onerror-agent')
224
+
225
+ const onErrorCalls: unknown[] = []
226
+ const middleware: PikkuAIMiddlewareHooks = {
227
+ onError: async (_services, ctx) => {
228
+ onErrorCalls.push(ctx)
229
+ },
230
+ }
231
+ const agent = pikkuState(null, 'agent', 'agents').get('onerror-agent')!
232
+ agent.aiMiddleware = [middleware] as any
233
+ pikkuState(null, 'agent', 'agents').set('onerror-agent', agent)
234
+
235
+ const expectedError = new Error('boom')
236
+ const mockServices = {
237
+ logger: {
238
+ info: () => {},
239
+ warn: () => {},
240
+ error: () => {},
241
+ debug: () => {},
242
+ },
243
+ aiAgentRunner: {
244
+ run: async () => {
245
+ throw expectedError
246
+ },
247
+ },
248
+ aiRunState: {
249
+ createRun: async () => 'run-err',
250
+ updateRun: async () => {},
251
+ },
252
+ } as any
253
+
254
+ pikkuState(null, 'package', 'singletonServices', mockServices)
255
+
256
+ await assert.rejects(
257
+ () =>
258
+ runAIAgent(
259
+ 'onerror-agent',
260
+ { message: 'hi', threadId: 't2', resourceId: 'r2' },
261
+ {}
262
+ ),
263
+ expectedError
264
+ )
265
+
266
+ assert.equal(onErrorCalls.length, 1)
267
+ const call = onErrorCalls[0] as any
268
+ assert.equal(call.error.message, 'boom')
269
+ assert.equal(call.stepNumber, -1)
270
+ })
271
+
272
+ test('onError hook failure does not suppress original error', async () => {
273
+ addTestAgent('onerror-throws-agent')
274
+
275
+ const middleware: PikkuAIMiddlewareHooks = {
276
+ onError: async () => {
277
+ throw new Error('hook error')
278
+ },
279
+ }
280
+ const agent = pikkuState(null, 'agent', 'agents').get(
281
+ 'onerror-throws-agent'
282
+ )!
283
+ agent.aiMiddleware = [middleware] as any
284
+ pikkuState(null, 'agent', 'agents').set('onerror-throws-agent', agent)
285
+
286
+ const expectedError = new Error('original error')
287
+ const mockServices = {
288
+ logger: {
289
+ info: () => {},
290
+ warn: () => {},
291
+ error: () => {},
292
+ debug: () => {},
293
+ },
294
+ aiAgentRunner: {
295
+ run: async () => {
296
+ throw expectedError
297
+ },
298
+ },
299
+ aiRunState: {
300
+ createRun: async () => 'run-safe',
301
+ updateRun: async () => {},
302
+ },
303
+ } as any
304
+
305
+ pikkuState(null, 'package', 'singletonServices', mockServices)
306
+
307
+ await assert.rejects(
308
+ () =>
309
+ runAIAgent(
310
+ 'onerror-throws-agent',
311
+ { message: 'hi', threadId: 't3', resourceId: 'r3' },
312
+ {}
313
+ ),
314
+ expectedError
315
+ )
316
+ })
75
317
  })
@@ -1,11 +1,16 @@
1
1
  import type {
2
2
  AIAgentInput,
3
3
  AIAgentOutput,
4
+ AIAgentStep,
5
+ AIMessage,
4
6
  PikkuAIMiddlewareHooks,
5
7
  } from './ai-agent.types.js'
8
+ import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
6
9
 
7
10
  import { saveMessages } from './ai-agent-memory.js'
8
11
  import { prepareAgentRun, type RunAIAgentParams } from './ai-agent-prepare.js'
12
+ import { getSingletonServices } from '../../pikku-state.js'
13
+ import { randomUUID } from 'crypto'
9
14
 
10
15
  export async function runAIAgent(
11
16
  agentName: string,
@@ -23,12 +28,13 @@ export async function runAIAgent(
23
28
  threadId,
24
29
  userMessage,
25
30
  runnerParams,
31
+ maxSteps,
26
32
  missingRpcs,
27
33
  workingMemoryJsonSchema,
28
34
  workingMemorySchemaName,
29
35
  } = await prepareAgentRun(agentName, input, params, sessionMap)
30
36
 
31
- const { singletonServices } = params
37
+ const singletonServices = getSingletonServices()
32
38
  const { aiRunState } = singletonServices
33
39
  if (!aiRunState) {
34
40
  throw new Error('AIRunStateService not available in singletonServices')
@@ -83,7 +89,102 @@ export async function runAIAgent(
83
89
  })
84
90
 
85
91
  try {
86
- const result = await agentRunner.run(runnerParams)
92
+ const accumulatedSteps: AIAgentStep[] = []
93
+ const totalUsage = { inputTokens: 0, outputTokens: 0 }
94
+ let lastStepResult: AIAgentStepResult | null = null
95
+
96
+ for (let step = 0; step < maxSteps; step++) {
97
+ if (agent.prepareStep) {
98
+ let stopped = false
99
+ await agent.prepareStep({
100
+ stepNumber: step,
101
+ messages: runnerParams.messages,
102
+ tools: runnerParams.tools,
103
+ toolChoice: runnerParams.toolChoice,
104
+ model: runnerParams.model,
105
+ stop: () => {
106
+ stopped = true
107
+ },
108
+ })
109
+ if (stopped) break
110
+ }
111
+
112
+ const stepResult = await agentRunner.run(runnerParams)
113
+ lastStepResult = stepResult
114
+
115
+ totalUsage.inputTokens += stepResult.usage.inputTokens
116
+ totalUsage.outputTokens += stepResult.usage.outputTokens
117
+
118
+ for (const mw of aiMiddlewares) {
119
+ if (mw.afterStep) {
120
+ await mw.afterStep(singletonServices, {
121
+ stepNumber: step,
122
+ text: stepResult.text,
123
+ toolCalls: stepResult.toolCalls,
124
+ toolResults: stepResult.toolResults,
125
+ usage: stepResult.usage,
126
+ finishReason: stepResult.finishReason,
127
+ })
128
+ }
129
+ }
130
+
131
+ accumulatedSteps.push({
132
+ usage: stepResult.usage,
133
+ toolCalls: stepResult.toolCalls.map((tc) => {
134
+ const tr = stepResult.toolResults.find(
135
+ (r) => r.toolCallId === tc.toolCallId
136
+ )
137
+ return {
138
+ name: tc.toolName,
139
+ args: tc.args as Record<string, unknown>,
140
+ result:
141
+ typeof tr?.result === 'string'
142
+ ? tr.result
143
+ : JSON.stringify(tr?.result ?? ''),
144
+ }
145
+ }),
146
+ })
147
+
148
+ if (stepResult.toolCalls.length === 0) break
149
+
150
+ const assistantMsg: AIMessage = {
151
+ id: randomUUID(),
152
+ role: 'assistant',
153
+ content: stepResult.text || undefined,
154
+ toolCalls: stepResult.toolCalls.map((tc) => ({
155
+ id: tc.toolCallId,
156
+ name: tc.toolName,
157
+ args: tc.args as Record<string, unknown>,
158
+ })),
159
+ createdAt: new Date(),
160
+ }
161
+ runnerParams.messages.push(assistantMsg)
162
+
163
+ if (stepResult.toolResults.length > 0) {
164
+ const toolMsg: AIMessage = {
165
+ id: randomUUID(),
166
+ role: 'tool',
167
+ toolResults: stepResult.toolResults.map((tr) => ({
168
+ id: tr.toolCallId,
169
+ name: tr.toolName,
170
+ result:
171
+ typeof tr.result === 'string'
172
+ ? tr.result
173
+ : JSON.stringify(tr.result),
174
+ })),
175
+ createdAt: new Date(),
176
+ }
177
+ runnerParams.messages.push(toolMsg)
178
+ }
179
+ }
180
+
181
+ const finalText = lastStepResult?.text ?? ''
182
+ const finalObject = lastStepResult?.object
183
+
184
+ const result = {
185
+ text: finalText,
186
+ steps: accumulatedSteps,
187
+ }
87
188
 
88
189
  const responseText = await saveMessages(
89
190
  storage,
@@ -108,7 +209,7 @@ export async function runAIAgent(
108
209
  const modResult = await mw.modifyOutput(singletonServices, {
109
210
  text: outputText,
110
211
  messages: outputMessages,
111
- usage: result.usage,
212
+ usage: totalUsage,
112
213
  })
113
214
  outputText = modResult.text
114
215
  outputMessages = modResult.messages
@@ -117,18 +218,31 @@ export async function runAIAgent(
117
218
 
118
219
  await aiRunState.updateRun(runId, {
119
220
  status: 'completed',
120
- usage: { ...result.usage, model: agent.model },
221
+ usage: { ...totalUsage, model: agent.model },
121
222
  })
122
223
 
123
224
  return {
124
225
  runId,
125
226
  text: outputText,
126
- object: result.object,
227
+ object: finalObject,
127
228
  threadId,
128
- steps: result.steps,
129
- usage: result.usage,
229
+ steps: accumulatedSteps,
230
+ usage: totalUsage,
130
231
  }
131
232
  } catch (error) {
233
+ for (const mw of aiMiddlewares) {
234
+ if (mw.onError) {
235
+ try {
236
+ await mw.onError(singletonServices, {
237
+ error: error instanceof Error ? error : new Error(String(error)),
238
+ stepNumber: -1,
239
+ messages: runnerParams.messages,
240
+ })
241
+ } catch {
242
+ // onError hooks must not affect error flow
243
+ }
244
+ }
245
+ }
132
246
  await aiRunState.updateRun(runId, {
133
247
  status: 'failed',
134
248
  })