@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
@@ -0,0 +1,363 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ createAssistantUIChannel,
6
+ parseAssistantUIInput,
7
+ } from './ai-agent-assistant-ui.js'
8
+ import type { AIStreamChannel, AIStreamEvent } from './ai-agent.types.js'
9
+
10
+ function createMockChannel(): AIStreamChannel & { sent: unknown[] } {
11
+ const sent: unknown[] = []
12
+ return {
13
+ channelId: 'test-channel',
14
+ openingData: undefined,
15
+ state: 'open' as const,
16
+ sent,
17
+ send: (data: unknown) => {
18
+ sent.push(data)
19
+ },
20
+ close: () => {},
21
+ }
22
+ }
23
+
24
+ describe('createAssistantUIChannel', () => {
25
+ test('sends start event before first content', () => {
26
+ const parent = createMockChannel()
27
+ const channel = createAssistantUIChannel(parent)
28
+
29
+ channel.send({ type: 'text-delta', text: 'Hello' })
30
+
31
+ assert.equal(parent.sent.length, 2)
32
+ const start = parent.sent[0] as any
33
+ assert.equal(start.type, 'start')
34
+ assert.ok(start.messageId)
35
+ })
36
+
37
+ test('sends start only once', () => {
38
+ const parent = createMockChannel()
39
+ const channel = createAssistantUIChannel(parent)
40
+
41
+ channel.send({ type: 'text-delta', text: 'Hello' })
42
+ channel.send({ type: 'text-delta', text: ' world' })
43
+
44
+ const startEvents = parent.sent.filter((e: any) => e.type === 'start')
45
+ assert.equal(startEvents.length, 1)
46
+ })
47
+
48
+ test('maps text-delta correctly', () => {
49
+ const parent = createMockChannel()
50
+ const channel = createAssistantUIChannel(parent)
51
+
52
+ channel.send({ type: 'text-delta', text: 'Hello' })
53
+
54
+ const delta = parent.sent[1] as any
55
+ assert.equal(delta.type, 'text-delta')
56
+ assert.equal(delta.textDelta, 'Hello')
57
+ })
58
+
59
+ test('maps reasoning-delta correctly', () => {
60
+ const parent = createMockChannel()
61
+ const channel = createAssistantUIChannel(parent)
62
+
63
+ channel.send({ type: 'reasoning-delta', text: 'thinking...' })
64
+
65
+ const delta = parent.sent[1] as any
66
+ assert.equal(delta.type, 'reasoning-delta')
67
+ assert.equal(delta.delta, 'thinking...')
68
+ })
69
+
70
+ test('expands tool-call into start/delta/end', () => {
71
+ const parent = createMockChannel()
72
+ const channel = createAssistantUIChannel(parent)
73
+
74
+ channel.send({
75
+ type: 'tool-call',
76
+ toolCallId: 'tc-1',
77
+ toolName: 'search',
78
+ args: { query: 'hello' },
79
+ })
80
+
81
+ assert.equal(parent.sent.length, 4)
82
+ const start = parent.sent[1] as any
83
+ assert.equal(start.type, 'tool-call-start')
84
+ assert.equal(start.id, 'tc-1')
85
+ assert.equal(start.toolCallId, 'tc-1')
86
+ assert.equal(start.toolName, 'search')
87
+
88
+ const delta = parent.sent[2] as any
89
+ assert.equal(delta.type, 'tool-call-delta')
90
+ assert.equal(delta.argsText, '{"query":"hello"}')
91
+
92
+ const end = parent.sent[3] as any
93
+ assert.equal(end.type, 'tool-call-end')
94
+ })
95
+
96
+ test('buffers tool-result and flushes after finish-step', () => {
97
+ const parent = createMockChannel()
98
+ const channel = createAssistantUIChannel(parent)
99
+
100
+ channel.send({
101
+ type: 'tool-call',
102
+ toolCallId: 'tc-1',
103
+ toolName: 'search',
104
+ args: { query: 'hello' },
105
+ })
106
+ channel.send({
107
+ type: 'tool-result',
108
+ toolCallId: 'tc-1',
109
+ toolName: 'search',
110
+ result: { data: [1, 2, 3] },
111
+ })
112
+
113
+ const beforeUsage = parent.sent.filter((e: any) => e.type === 'tool-result')
114
+ assert.equal(beforeUsage.length, 0)
115
+
116
+ channel.send({
117
+ type: 'usage',
118
+ tokens: { input: 100, output: 50 },
119
+ model: 'gpt-4o',
120
+ })
121
+
122
+ const afterUsage = parent.sent.filter((e: any) => e.type === 'tool-result')
123
+ assert.equal(afterUsage.length, 1)
124
+ assert.equal(afterUsage[0].toolCallId, 'tc-1')
125
+ assert.deepEqual(afterUsage[0].result, { data: [1, 2, 3] })
126
+ })
127
+
128
+ test('silently ignores agent-call (covered by tool-call)', () => {
129
+ const parent = createMockChannel()
130
+ const channel = createAssistantUIChannel(parent)
131
+
132
+ channel.send({
133
+ type: 'agent-call',
134
+ agentName: 'sub-agent',
135
+ session: 'sess-1',
136
+ input: { message: 'do something' },
137
+ })
138
+
139
+ assert.equal(parent.sent.length, 0)
140
+ })
141
+
142
+ test('silently ignores agent-result (covered by tool-result)', () => {
143
+ const parent = createMockChannel()
144
+ const channel = createAssistantUIChannel(parent)
145
+
146
+ channel.send({
147
+ type: 'agent-result',
148
+ agentName: 'sub-agent',
149
+ session: 'sess-1',
150
+ result: 'done!',
151
+ })
152
+
153
+ assert.equal(parent.sent.length, 0)
154
+ })
155
+
156
+ test('maps usage to finish-step with token counts', () => {
157
+ const parent = createMockChannel()
158
+ const channel = createAssistantUIChannel(parent)
159
+
160
+ channel.send({
161
+ type: 'usage',
162
+ tokens: { input: 100, output: 50 },
163
+ model: 'gpt-4o',
164
+ })
165
+
166
+ const step = parent.sent[1] as any
167
+ assert.equal(step.type, 'finish-step')
168
+ assert.equal(step.finishReason, 'stop')
169
+ assert.deepEqual(step.usage, { promptTokens: 100, completionTokens: 50 })
170
+ assert.equal(step.isContinued, false)
171
+ })
172
+
173
+ test('maps error correctly', () => {
174
+ const parent = createMockChannel()
175
+ const channel = createAssistantUIChannel(parent)
176
+
177
+ channel.send({ type: 'error', message: 'something went wrong' })
178
+
179
+ const err = parent.sent[1] as any
180
+ assert.equal(err.type, 'error')
181
+ assert.equal(err.errorText, 'something went wrong')
182
+ })
183
+
184
+ test('maps approval-request to data event', () => {
185
+ const parent = createMockChannel()
186
+ const channel = createAssistantUIChannel(parent)
187
+
188
+ const event: AIStreamEvent = {
189
+ type: 'approval-request',
190
+ toolCallId: 'tc-1',
191
+ toolName: 'deleteTodo',
192
+ args: { id: 5 },
193
+ }
194
+ channel.send(event)
195
+
196
+ const chunk = parent.sent[1] as any
197
+ assert.equal(chunk.type, 'data-approval-request')
198
+ assert.deepEqual(chunk.data, event)
199
+ })
200
+
201
+ test('maps suspended to data event', () => {
202
+ const parent = createMockChannel()
203
+ const channel = createAssistantUIChannel(parent)
204
+
205
+ const event: AIStreamEvent = {
206
+ type: 'suspended',
207
+ reason: 'rpc-missing',
208
+ missingRpcs: ['listTodos'],
209
+ }
210
+ channel.send(event)
211
+
212
+ const chunk = parent.sent[1] as any
213
+ assert.equal(chunk.type, 'data-suspended')
214
+ assert.deepEqual(chunk.data, event)
215
+ })
216
+
217
+ test('done produces finish + raw [DONE] string + calls close', () => {
218
+ const parent = createMockChannel()
219
+ let closed = false
220
+ parent.close = () => {
221
+ closed = true
222
+ }
223
+ const channel = createAssistantUIChannel(parent)
224
+
225
+ channel.send({ type: 'text-delta', text: 'hi' })
226
+ channel.send({ type: 'done' })
227
+
228
+ const finish = parent.sent[2] as any
229
+ assert.equal(finish.type, 'finish')
230
+ assert.equal(finish.finishReason, 'stop')
231
+
232
+ assert.equal(parent.sent[3], '[DONE]')
233
+ assert.ok(closed)
234
+ })
235
+
236
+ test('accumulates usage across multiple steps', () => {
237
+ const parent = createMockChannel()
238
+ const channel = createAssistantUIChannel(parent)
239
+
240
+ channel.send({
241
+ type: 'usage',
242
+ tokens: { input: 100, output: 50 },
243
+ model: 'gpt-4o',
244
+ })
245
+ channel.send({
246
+ type: 'usage',
247
+ tokens: { input: 200, output: 100 },
248
+ model: 'gpt-4o',
249
+ })
250
+ channel.send({ type: 'done' })
251
+
252
+ const finish = parent.sent.find((e: any) => e.type === 'finish') as any
253
+ assert.deepEqual(finish.usage, {
254
+ promptTokens: 300,
255
+ completionTokens: 150,
256
+ })
257
+ })
258
+
259
+ test('preserves parent channelId and openingData', () => {
260
+ const parent = createMockChannel()
261
+ parent.channelId = 'my-channel'
262
+ parent.openingData = { foo: 'bar' }
263
+
264
+ const channel = createAssistantUIChannel(parent)
265
+
266
+ assert.equal(channel.channelId, 'my-channel')
267
+ assert.deepEqual(channel.openingData, { foo: 'bar' })
268
+ })
269
+ })
270
+
271
+ describe('parseAssistantUIInput', () => {
272
+ test('extracts last user message with string content', () => {
273
+ const result = parseAssistantUIInput({
274
+ messages: [
275
+ { role: 'user', content: 'first message' },
276
+ { role: 'assistant', content: 'response' },
277
+ { role: 'user', content: 'second message' },
278
+ ],
279
+ threadId: 'thread-abc',
280
+ })
281
+
282
+ assert.equal(result.message, 'second message')
283
+ assert.equal(result.threadId, 'thread-abc')
284
+ assert.equal(result.resourceId, 'default')
285
+ })
286
+
287
+ test('handles parts format (UIMessage)', () => {
288
+ const result = parseAssistantUIInput({
289
+ messages: [
290
+ {
291
+ role: 'user',
292
+ parts: [{ type: 'text', text: 'hello from parts' }],
293
+ },
294
+ ],
295
+ })
296
+
297
+ assert.equal(result.message, 'hello from parts')
298
+ })
299
+
300
+ test('handles content array format', () => {
301
+ const result = parseAssistantUIInput({
302
+ messages: [
303
+ {
304
+ role: 'user',
305
+ content: [{ type: 'text', text: 'hello from content array' }],
306
+ },
307
+ ],
308
+ })
309
+
310
+ assert.equal(result.message, 'hello from content array')
311
+ })
312
+
313
+ test('uses input.id as threadId fallback', () => {
314
+ const result = parseAssistantUIInput({
315
+ messages: [{ role: 'user', content: 'hi' }],
316
+ id: 'id-fallback',
317
+ })
318
+
319
+ assert.equal(result.threadId, 'id-fallback')
320
+ })
321
+
322
+ test('generates UUID when no threadId or id', () => {
323
+ const result = parseAssistantUIInput({
324
+ messages: [{ role: 'user', content: 'hi' }],
325
+ })
326
+
327
+ assert.ok(result.threadId)
328
+ assert.match(
329
+ result.threadId,
330
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
331
+ )
332
+ })
333
+
334
+ test('uses defaults.resourceId when provided', () => {
335
+ const result = parseAssistantUIInput(
336
+ { messages: [{ role: 'user', content: 'hi' }] },
337
+ { resourceId: 'my-resource' }
338
+ )
339
+
340
+ assert.equal(result.resourceId, 'my-resource')
341
+ })
342
+
343
+ test('throws on empty messages array', () => {
344
+ assert.throws(
345
+ () => parseAssistantUIInput({ messages: [] }),
346
+ /non-empty messages array/
347
+ )
348
+ })
349
+
350
+ test('throws on missing messages', () => {
351
+ assert.throws(() => parseAssistantUIInput({}), /non-empty messages array/)
352
+ })
353
+
354
+ test('throws when no user message found', () => {
355
+ assert.throws(
356
+ () =>
357
+ parseAssistantUIInput({
358
+ messages: [{ role: 'assistant', content: 'no user here' }],
359
+ }),
360
+ /No user message found/
361
+ )
362
+ })
363
+ })
@@ -0,0 +1,214 @@
1
+ import type {
2
+ AIStreamChannel,
3
+ AIStreamEvent,
4
+ AIAgentInput,
5
+ } from './ai-agent.types.js'
6
+ import { randomUUID } from 'crypto'
7
+
8
+ type AssistantUIChunk =
9
+ | { type: 'start'; messageId: string }
10
+ | { type: 'text-delta'; textDelta: string }
11
+ | { type: 'reasoning-delta'; delta: string }
12
+ | {
13
+ type: 'tool-call-start'
14
+ id: string
15
+ toolCallId: string
16
+ toolName: string
17
+ }
18
+ | {
19
+ type: 'tool-call-delta'
20
+ argsText: string
21
+ }
22
+ | { type: 'tool-call-end' }
23
+ | {
24
+ type: 'tool-result'
25
+ toolCallId: string
26
+ result: unknown
27
+ }
28
+ | {
29
+ type: 'finish-step'
30
+ finishReason: string
31
+ usage?: { promptTokens: number; completionTokens: number }
32
+ isContinued: boolean
33
+ }
34
+ | {
35
+ type: 'finish'
36
+ finishReason: string
37
+ usage?: { promptTokens: number; completionTokens: number }
38
+ }
39
+ | { type: 'error'; errorText: string }
40
+ | { type: string; data: unknown }
41
+
42
+ export function createAssistantUIChannel(
43
+ parent: AIStreamChannel
44
+ ): AIStreamChannel {
45
+ const messageId = randomUUID()
46
+ let started = false
47
+ let totalInputTokens = 0
48
+ let totalOutputTokens = 0
49
+ let pendingToolResults: AssistantUIChunk[] = []
50
+ let hasToolCalls = false
51
+ let hasPendingApproval = false
52
+
53
+ const sendChunk = (chunk: AssistantUIChunk) => {
54
+ parent.send(chunk as any)
55
+ }
56
+
57
+ const ensureStarted = () => {
58
+ if (!started) {
59
+ started = true
60
+ sendChunk({ type: 'start', messageId })
61
+ }
62
+ }
63
+
64
+ const flushToolResults = () => {
65
+ for (const chunk of pendingToolResults) {
66
+ sendChunk(chunk)
67
+ }
68
+ pendingToolResults = []
69
+ }
70
+
71
+ return {
72
+ channelId: parent.channelId,
73
+ openingData: parent.openingData,
74
+ get state() {
75
+ return parent.state
76
+ },
77
+ close: () => parent.close(),
78
+ send: (event: AIStreamEvent) => {
79
+ switch (event.type) {
80
+ case 'text-delta':
81
+ ensureStarted()
82
+ sendChunk({ type: 'text-delta', textDelta: event.text })
83
+ break
84
+
85
+ case 'reasoning-delta':
86
+ ensureStarted()
87
+ sendChunk({ type: 'reasoning-delta', delta: event.text })
88
+ break
89
+
90
+ case 'tool-call':
91
+ ensureStarted()
92
+ hasToolCalls = true
93
+ sendChunk({
94
+ type: 'tool-call-start',
95
+ id: event.toolCallId,
96
+ toolCallId: event.toolCallId,
97
+ toolName: event.toolName,
98
+ })
99
+ sendChunk({
100
+ type: 'tool-call-delta',
101
+ argsText:
102
+ typeof event.args === 'string'
103
+ ? event.args
104
+ : JSON.stringify(event.args),
105
+ })
106
+ sendChunk({ type: 'tool-call-end' })
107
+ break
108
+
109
+ case 'tool-result':
110
+ ensureStarted()
111
+ pendingToolResults.push({
112
+ type: 'tool-result',
113
+ toolCallId: event.toolCallId,
114
+ result: event.result,
115
+ })
116
+ break
117
+
118
+ case 'agent-call':
119
+ case 'agent-result':
120
+ break
121
+
122
+ case 'usage': {
123
+ ensureStarted()
124
+ totalInputTokens += event.tokens.input
125
+ totalOutputTokens += event.tokens.output
126
+ const hasPending = pendingToolResults.length > 0
127
+ sendChunk({
128
+ type: 'finish-step',
129
+ finishReason: hasToolCalls ? 'tool-calls' : 'stop',
130
+ usage: {
131
+ promptTokens: event.tokens.input,
132
+ completionTokens: event.tokens.output,
133
+ },
134
+ isContinued: hasPending,
135
+ })
136
+ flushToolResults()
137
+ hasToolCalls = false
138
+ break
139
+ }
140
+
141
+ case 'error':
142
+ ensureStarted()
143
+ sendChunk({ type: 'error', errorText: event.message })
144
+ break
145
+
146
+ case 'approval-request':
147
+ ensureStarted()
148
+ hasPendingApproval = true
149
+ sendChunk({ type: 'data-approval-request', data: event })
150
+ break
151
+
152
+ case 'suspended':
153
+ ensureStarted()
154
+ sendChunk({ type: 'data-suspended', data: event })
155
+ break
156
+
157
+ case 'done':
158
+ ensureStarted()
159
+ flushToolResults()
160
+ sendChunk({
161
+ type: 'finish',
162
+ finishReason: hasPendingApproval ? 'tool-calls' : 'stop',
163
+ usage: {
164
+ promptTokens: totalInputTokens,
165
+ completionTokens: totalOutputTokens,
166
+ },
167
+ })
168
+ parent.send('[DONE]' as any)
169
+ parent.close()
170
+ break
171
+ }
172
+ },
173
+ }
174
+ }
175
+
176
+ export function parseAssistantUIInput(
177
+ input: Record<string, unknown>,
178
+ defaults?: { resourceId?: string }
179
+ ): AIAgentInput {
180
+ const messages = input.messages as Array<Record<string, unknown>> | undefined
181
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
182
+ throw new Error(
183
+ 'assistant-ui input must contain a non-empty messages array'
184
+ )
185
+ }
186
+
187
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user')
188
+ if (!lastUserMessage) {
189
+ throw new Error('No user message found in assistant-ui input')
190
+ }
191
+
192
+ let messageText: string
193
+ if (typeof lastUserMessage.content === 'string') {
194
+ messageText = lastUserMessage.content
195
+ } else if (Array.isArray(lastUserMessage.parts)) {
196
+ const textPart = (
197
+ lastUserMessage.parts as Array<Record<string, unknown>>
198
+ ).find((p) => p.type === 'text')
199
+ messageText = textPart ? String(textPart.text) : ''
200
+ } else if (Array.isArray(lastUserMessage.content)) {
201
+ const textPart = (
202
+ lastUserMessage.content as Array<Record<string, unknown>>
203
+ ).find((p) => p.type === 'text')
204
+ messageText = textPart ? String(textPart.text) : ''
205
+ } else {
206
+ messageText = ''
207
+ }
208
+
209
+ const threadId =
210
+ (input.threadId as string) ?? (input.id as string) ?? randomUUID()
211
+ const resourceId = defaults?.resourceId ?? 'default'
212
+
213
+ return { message: messageText, threadId, resourceId }
214
+ }