@multiplayer-app/ai-agent-node 0.0.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 (210) hide show
  1. package/.env.example +45 -0
  2. package/README.md +611 -0
  3. package/config.example.json +73 -0
  4. package/dist/config.d.ts +35 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +44 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/helpers/AIHelper.d.ts +23 -0
  9. package/dist/helpers/AIHelper.d.ts.map +1 -0
  10. package/dist/helpers/AIHelper.js +326 -0
  11. package/dist/helpers/AIHelper.js.map +1 -0
  12. package/dist/helpers/AIHelper.test.d.ts +2 -0
  13. package/dist/helpers/AIHelper.test.d.ts.map +1 -0
  14. package/dist/helpers/AIHelper.test.js +332 -0
  15. package/dist/helpers/AIHelper.test.js.map +1 -0
  16. package/dist/helpers/ConfigHelper.d.ts +20 -0
  17. package/dist/helpers/ConfigHelper.d.ts.map +1 -0
  18. package/dist/helpers/ConfigHelper.js +118 -0
  19. package/dist/helpers/ConfigHelper.js.map +1 -0
  20. package/dist/helpers/ContextLimiter.d.ts +82 -0
  21. package/dist/helpers/ContextLimiter.d.ts.map +1 -0
  22. package/dist/helpers/ContextLimiter.js +165 -0
  23. package/dist/helpers/ContextLimiter.js.map +1 -0
  24. package/dist/helpers/FileHelper.d.ts +31 -0
  25. package/dist/helpers/FileHelper.d.ts.map +1 -0
  26. package/dist/helpers/FileHelper.js +175 -0
  27. package/dist/helpers/FileHelper.js.map +1 -0
  28. package/dist/helpers/SetupHelper.d.ts +5 -0
  29. package/dist/helpers/SetupHelper.d.ts.map +1 -0
  30. package/dist/helpers/SetupHelper.js +32 -0
  31. package/dist/helpers/SetupHelper.js.map +1 -0
  32. package/dist/helpers/index.d.ts +6 -0
  33. package/dist/helpers/index.d.ts.map +1 -0
  34. package/dist/helpers/index.js +6 -0
  35. package/dist/helpers/index.js.map +1 -0
  36. package/dist/index.d.ts +18 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +17 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libs/index.d.ts +4 -0
  41. package/dist/libs/index.d.ts.map +1 -0
  42. package/dist/libs/index.js +4 -0
  43. package/dist/libs/index.js.map +1 -0
  44. package/dist/libs/kafka/config.d.ts +5 -0
  45. package/dist/libs/kafka/config.d.ts.map +1 -0
  46. package/dist/libs/kafka/config.js +5 -0
  47. package/dist/libs/kafka/config.js.map +1 -0
  48. package/dist/libs/kafka/consumer.d.ts +16 -0
  49. package/dist/libs/kafka/consumer.d.ts.map +1 -0
  50. package/dist/libs/kafka/consumer.js +126 -0
  51. package/dist/libs/kafka/consumer.js.map +1 -0
  52. package/dist/libs/kafka/index.d.ts +3 -0
  53. package/dist/libs/kafka/index.d.ts.map +1 -0
  54. package/dist/libs/kafka/index.js +3 -0
  55. package/dist/libs/kafka/index.js.map +1 -0
  56. package/dist/libs/kafka/kafka.d.ts +3 -0
  57. package/dist/libs/kafka/kafka.d.ts.map +1 -0
  58. package/dist/libs/kafka/kafka.js +24 -0
  59. package/dist/libs/kafka/kafka.js.map +1 -0
  60. package/dist/libs/kafka/producer.d.ts +11 -0
  61. package/dist/libs/kafka/producer.d.ts.map +1 -0
  62. package/dist/libs/kafka/producer.js +44 -0
  63. package/dist/libs/kafka/producer.js.map +1 -0
  64. package/dist/libs/logger/config.d.ts +5 -0
  65. package/dist/libs/logger/config.d.ts.map +1 -0
  66. package/dist/libs/logger/config.js +6 -0
  67. package/dist/libs/logger/config.js.map +1 -0
  68. package/dist/libs/logger/index.d.ts +10 -0
  69. package/dist/libs/logger/index.d.ts.map +1 -0
  70. package/dist/libs/logger/index.js +20 -0
  71. package/dist/libs/logger/index.js.map +1 -0
  72. package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
  73. package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
  74. package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
  75. package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
  76. package/dist/libs/logger/logger.d.ts +42 -0
  77. package/dist/libs/logger/logger.d.ts.map +1 -0
  78. package/dist/libs/logger/logger.js +44 -0
  79. package/dist/libs/logger/logger.js.map +1 -0
  80. package/dist/libs/s3/config.d.ts +7 -0
  81. package/dist/libs/s3/config.d.ts.map +1 -0
  82. package/dist/libs/s3/config.js +7 -0
  83. package/dist/libs/s3/config.js.map +1 -0
  84. package/dist/libs/s3/index.d.ts +4 -0
  85. package/dist/libs/s3/index.d.ts.map +1 -0
  86. package/dist/libs/s3/index.js +4 -0
  87. package/dist/libs/s3/index.js.map +1 -0
  88. package/dist/libs/s3/s3.lib.d.ts +25 -0
  89. package/dist/libs/s3/s3.lib.d.ts.map +1 -0
  90. package/dist/libs/s3/s3.lib.js +202 -0
  91. package/dist/libs/s3/s3.lib.js.map +1 -0
  92. package/dist/processors/ChatProcessor.d.ts +66 -0
  93. package/dist/processors/ChatProcessor.d.ts.map +1 -0
  94. package/dist/processors/ChatProcessor.js +610 -0
  95. package/dist/processors/ChatProcessor.js.map +1 -0
  96. package/dist/processors/ModelsProcessor.d.ts +11 -0
  97. package/dist/processors/ModelsProcessor.d.ts.map +1 -0
  98. package/dist/processors/ModelsProcessor.js +30 -0
  99. package/dist/processors/ModelsProcessor.js.map +1 -0
  100. package/dist/processors/index.d.ts +3 -0
  101. package/dist/processors/index.d.ts.map +1 -0
  102. package/dist/processors/index.js +3 -0
  103. package/dist/processors/index.js.map +1 -0
  104. package/dist/services/AIService.d.ts +48 -0
  105. package/dist/services/AIService.d.ts.map +1 -0
  106. package/dist/services/AIService.js +196 -0
  107. package/dist/services/AIService.js.map +1 -0
  108. package/dist/services/InternalEventsHandler.d.ts +21 -0
  109. package/dist/services/InternalEventsHandler.d.ts.map +1 -0
  110. package/dist/services/InternalEventsHandler.js +56 -0
  111. package/dist/services/InternalEventsHandler.js.map +1 -0
  112. package/dist/services/KafkaService.d.ts +35 -0
  113. package/dist/services/KafkaService.d.ts.map +1 -0
  114. package/dist/services/KafkaService.js +120 -0
  115. package/dist/services/KafkaService.js.map +1 -0
  116. package/dist/services/ModelFetcher.d.ts +54 -0
  117. package/dist/services/ModelFetcher.d.ts.map +1 -0
  118. package/dist/services/ModelFetcher.js +247 -0
  119. package/dist/services/ModelFetcher.js.map +1 -0
  120. package/dist/services/RedisService.d.ts +90 -0
  121. package/dist/services/RedisService.d.ts.map +1 -0
  122. package/dist/services/RedisService.js +236 -0
  123. package/dist/services/RedisService.js.map +1 -0
  124. package/dist/services/SocketService.d.ts +39 -0
  125. package/dist/services/SocketService.d.ts.map +1 -0
  126. package/dist/services/SocketService.js +128 -0
  127. package/dist/services/SocketService.js.map +1 -0
  128. package/dist/services/index.d.ts +7 -0
  129. package/dist/services/index.d.ts.map +1 -0
  130. package/dist/services/index.js +7 -0
  131. package/dist/services/index.js.map +1 -0
  132. package/dist/store/AgentStore.d.ts +48 -0
  133. package/dist/store/AgentStore.d.ts.map +1 -0
  134. package/dist/store/AgentStore.js +98 -0
  135. package/dist/store/AgentStore.js.map +1 -0
  136. package/dist/store/ArtifactStore.d.ts +13 -0
  137. package/dist/store/ArtifactStore.d.ts.map +1 -0
  138. package/dist/store/ArtifactStore.js +27 -0
  139. package/dist/store/ArtifactStore.js.map +1 -0
  140. package/dist/store/ConfigStore.d.ts +89 -0
  141. package/dist/store/ConfigStore.d.ts.map +1 -0
  142. package/dist/store/ConfigStore.js +214 -0
  143. package/dist/store/ConfigStore.js.map +1 -0
  144. package/dist/store/ConfigStore.test.d.ts +2 -0
  145. package/dist/store/ConfigStore.test.d.ts.map +1 -0
  146. package/dist/store/ConfigStore.test.js +259 -0
  147. package/dist/store/ConfigStore.test.js.map +1 -0
  148. package/dist/store/ModelStore.d.ts +44 -0
  149. package/dist/store/ModelStore.d.ts.map +1 -0
  150. package/dist/store/ModelStore.js +81 -0
  151. package/dist/store/ModelStore.js.map +1 -0
  152. package/dist/store/ModelStore.test.d.ts +2 -0
  153. package/dist/store/ModelStore.test.d.ts.map +1 -0
  154. package/dist/store/ModelStore.test.js +390 -0
  155. package/dist/store/ModelStore.test.js.map +1 -0
  156. package/dist/store/index.d.ts +5 -0
  157. package/dist/store/index.d.ts.map +1 -0
  158. package/dist/store/index.js +5 -0
  159. package/dist/store/index.js.map +1 -0
  160. package/dist/tools/generateChartTool.d.ts +24 -0
  161. package/dist/tools/generateChartTool.d.ts.map +1 -0
  162. package/dist/tools/generateChartTool.js +124 -0
  163. package/dist/tools/generateChartTool.js.map +1 -0
  164. package/dist/tools/proposeFormValuesTool.d.ts +35 -0
  165. package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
  166. package/dist/tools/proposeFormValuesTool.js +56 -0
  167. package/dist/tools/proposeFormValuesTool.js.map +1 -0
  168. package/package.json +71 -0
  169. package/src/config.ts +46 -0
  170. package/src/helpers/AIHelper.test.ts +375 -0
  171. package/src/helpers/AIHelper.ts +353 -0
  172. package/src/helpers/ConfigHelper.ts +130 -0
  173. package/src/helpers/ContextLimiter.ts +228 -0
  174. package/src/helpers/FileHelper.ts +197 -0
  175. package/src/helpers/SetupHelper.ts +35 -0
  176. package/src/helpers/index.ts +5 -0
  177. package/src/index.ts +18 -0
  178. package/src/libs/index.ts +3 -0
  179. package/src/libs/kafka/config.ts +4 -0
  180. package/src/libs/kafka/consumer.ts +161 -0
  181. package/src/libs/kafka/index.ts +2 -0
  182. package/src/libs/kafka/kafka.ts +27 -0
  183. package/src/libs/kafka/producer.ts +48 -0
  184. package/src/libs/logger/config.ts +4 -0
  185. package/src/libs/logger/index.ts +21 -0
  186. package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
  187. package/src/libs/logger/logger.ts +60 -0
  188. package/src/libs/s3/config.ts +7 -0
  189. package/src/libs/s3/index.ts +3 -0
  190. package/src/libs/s3/s3.lib.ts +284 -0
  191. package/src/processors/ChatProcessor.ts +713 -0
  192. package/src/processors/ModelsProcessor.ts +34 -0
  193. package/src/processors/index.ts +2 -0
  194. package/src/services/AIService.ts +241 -0
  195. package/src/services/InternalEventsHandler.ts +61 -0
  196. package/src/services/KafkaService.ts +142 -0
  197. package/src/services/ModelFetcher.ts +286 -0
  198. package/src/services/RedisService.ts +285 -0
  199. package/src/services/SocketService.ts +153 -0
  200. package/src/services/index.ts +6 -0
  201. package/src/store/AgentStore.ts +138 -0
  202. package/src/store/ArtifactStore.ts +29 -0
  203. package/src/store/ConfigStore.test.ts +314 -0
  204. package/src/store/ConfigStore.ts +239 -0
  205. package/src/store/ModelStore.test.ts +473 -0
  206. package/src/store/ModelStore.ts +93 -0
  207. package/src/store/index.ts +4 -0
  208. package/src/tools/generateChartTool.ts +131 -0
  209. package/src/tools/proposeFormValuesTool.ts +67 -0
  210. package/tsconfig.json +24 -0
@@ -0,0 +1,713 @@
1
+ import type {AgentChatRepository, AgentMessageRepository} from '@multiplayer-app/ai-agent-db';
2
+ import {
3
+ AgentArtifact,
4
+ AgentChat,
5
+ AgentChatResponse,
6
+ AgentAttachment,
7
+ AgentMessage,
8
+ AgentStatus,
9
+ AgentToolCallStatus,
10
+ ChatType,
11
+ MessageRole,
12
+ SendMessagePayload,
13
+ SortOrder,
14
+ StreamChunk,
15
+ StreamChunkType,
16
+ SendMessagePayloadWithApproval,
17
+ SendMessagePayloadWithContent,
18
+ AdditionalContext
19
+ } from '@multiplayer-app/ai-agent-types';
20
+ import { ArtifactStore } from '../store';
21
+ import { socketService } from '../services';
22
+ import { kafkaService } from '../services';
23
+ import { AIHelper } from '../helpers';
24
+ import { AgentProcessEventType, AgentProcessEvent, agentStore } from '../store';
25
+ import { GenerateTextOptions } from '../services';
26
+ import { ContextLimiter } from '../helpers';
27
+ import { config } from '../config';
28
+ import { PassThrough, pipeline } from 'stream';
29
+ import { promisify } from 'util';
30
+ import { logger } from '../libs/logger';
31
+
32
+ const pipelineAsync = promisify(pipeline);
33
+
34
+
35
+ export class ChatProcessor {
36
+ constructor(
37
+ private chatRepository: AgentChatRepository,
38
+ private messageRepository: AgentMessageRepository,
39
+ private artifactStore: ArtifactStore
40
+ ) { }
41
+
42
+ private getTemporaryTitle(contextKey: string): string {
43
+ return `${contextKey} session ${new Date().toISOString()}`;
44
+ }
45
+
46
+ async listChats(params?: {
47
+ contextKey?: string;
48
+ limit?: number;
49
+ userId?: string;
50
+ sortField?: string;
51
+ sortOrder?: SortOrder;
52
+ }): Promise<AgentChatResponse[]> {
53
+ // Build filter object from params
54
+ const filter: Partial<AgentChat> = {};
55
+ if (params?.userId) {
56
+ filter.userId = params.userId;
57
+ }
58
+ if (params?.contextKey) {
59
+ filter.contextKey = params.contextKey;
60
+ }
61
+
62
+ // Build query options for sort and limit with defaults
63
+ const options = {
64
+ sort: {
65
+ field: params?.sortField ?? 'updatedAt',
66
+ order: params?.sortOrder ?? SortOrder.Desc
67
+ },
68
+ limit: params?.limit
69
+ };
70
+
71
+ // Use aggregation to fetch chats with related messages
72
+ return this.chatRepository.findWithMessages(
73
+ filter,
74
+ options
75
+ );
76
+ }
77
+
78
+ async getChat(chatId: string): Promise<AgentChatResponse> {
79
+ const chat = await this.chatRepository.findById(chatId);
80
+ if (!chat) {
81
+ throw new Error('Chat not found');
82
+ }
83
+ const messages = await this.messageRepository.findByChatId(chatId);
84
+ return {
85
+ ...chat,
86
+ messages
87
+ };
88
+ }
89
+
90
+ async deleteChat(chatId: string): Promise<void> {
91
+ const deleted = await this.chatRepository.delete(chatId);
92
+ if (!deleted) {
93
+ throw new Error('Chat not found');
94
+ }
95
+ this.artifactStore.deleteArtifacts(chatId);
96
+ }
97
+
98
+ async upsertAndGetChat(payload: SendMessagePayload, excludeSocketId?: string) {
99
+ const targetUserId = payload.userId ?? 'guest';
100
+ let chat: AgentChat | null = null;
101
+
102
+ if (payload.chatId) {
103
+ chat = await this.chatRepository.findById(payload.chatId);
104
+ if (!chat) {
105
+ throw new Error('Chat not found');
106
+ }
107
+ // Ensure chat is associated with the caller's userId
108
+ if (chat.userId && chat.userId !== targetUserId) {
109
+ throw new Error('Chat does not belong to this user');
110
+ }
111
+ }
112
+
113
+ if (!chat) {
114
+ chat = await this.createChat(payload as SendMessagePayloadWithContent, excludeSocketId);
115
+ } else if ('model' in payload) {
116
+ chat.model = payload.model || undefined;
117
+ await this.chatRepository.update(chat.id, { model: chat.model });
118
+ }
119
+
120
+ return chat;
121
+ }
122
+
123
+ async getChatTitle(payload: {
124
+ chatId?: string;
125
+ content?: string;
126
+ contextKey: string;
127
+ userId?: string;
128
+ }): Promise<string> {
129
+ if (payload.chatId) {
130
+ const chat = await this.chatRepository.findById(payload.chatId);
131
+ if (chat && chat.title) {
132
+ return chat.title;
133
+ }
134
+ }
135
+
136
+ const content = payload.content;
137
+ if (!content) {
138
+ return this.getTemporaryTitle(payload.contextKey)
139
+ }
140
+ return AIHelper.generateTitleForMessage(payload.contextKey, [{
141
+ role: MessageRole.User,
142
+ content: content
143
+ }]);
144
+ }
145
+
146
+ async createMessage(chat: AgentChat, role: MessageRole, messageData: Pick<AgentMessage, 'content' | 'agentName'>, attachments?: AgentAttachment[], excludeSocketId?: string): Promise<AgentMessage> {
147
+ const message = await this.messageRepository.create({
148
+ chat: chat.id,
149
+ role,
150
+ content: messageData.content,
151
+ agentName: messageData.agentName,
152
+ attachments: attachments ?? [],
153
+ reasoning: "",
154
+ toolCalls: []
155
+ });
156
+ if (chat.userId) {
157
+ socketService.emitMessageUpdate(chat.userId, message, excludeSocketId);
158
+ }
159
+ return message;
160
+ }
161
+
162
+ async createChat(payload: SendMessagePayload, excludeSocketId?: string): Promise<AgentChat> {
163
+ const targetUserId = payload.userId ?? 'guest';
164
+ const contextKey = 'contextKey' in payload ? payload.contextKey : 'default';
165
+ const metadata = 'metadata' in payload ? payload.metadata : {};
166
+ const chat = await this.chatRepository.create({
167
+ title: this.getTemporaryTitle(contextKey),
168
+ type: ChatType.Chat,
169
+ status: AgentStatus.Streaming,
170
+ contextKey,
171
+ userId: targetUserId,
172
+ metadata,
173
+ ...(payload.model ? { model: payload.model } : {})
174
+ });
175
+ await kafkaService.sendChatTitleGenerationEvent(chat.id);
176
+ const initialChatResponse: AgentChatResponse = {
177
+ ...chat,
178
+ messages: []
179
+ };
180
+ socketService.emitChatUpdate(targetUserId, initialChatResponse, excludeSocketId);
181
+
182
+ return chat;
183
+ }
184
+
185
+ /**
186
+ * Persist a client-side action against a tool call (generic mechanism).
187
+ * This updates the message containing the tool call and optionally appends a System message.
188
+ */
189
+ async recordToolCallAction(params: {
190
+ chatId: string;
191
+ toolCallId: string;
192
+ action: string;
193
+ data: Record<string, unknown>;
194
+ systemMessage?: string;
195
+ excludeSocketId?: string;
196
+ }): Promise<{ ok: true }> {
197
+ const chat = await this.chatRepository.findById(params.chatId);
198
+ if (!chat) {
199
+ throw new Error('Chat not found');
200
+ }
201
+
202
+ const messages = await this.messageRepository.findByChatId(params.chatId);
203
+ const message = messages.find((m) => (m.toolCalls ?? []).some((c) => c.id === params.toolCallId));
204
+ if (!message) {
205
+ throw new Error('Tool call not found');
206
+ }
207
+
208
+ const toolCalls = message.toolCalls ?? [];
209
+ const target = toolCalls.find((c) => c.id === params.toolCallId);
210
+ if (!target) {
211
+ throw new Error('Tool call not found');
212
+ }
213
+
214
+ const now = new Date().toISOString();
215
+ const output = (target.output ?? {}) as Record<string, unknown>;
216
+ const mp = (output['_mp'] && typeof output['_mp'] === 'object' && !Array.isArray(output['_mp']))
217
+ ? (output['_mp'] as Record<string, unknown>)
218
+ : {};
219
+ const actions = Array.isArray(mp['actions']) ? (mp['actions'] as unknown[]) : [];
220
+
221
+ const nextMp = {
222
+ ...mp,
223
+ lastAction: {
224
+ action: params.action,
225
+ at: now,
226
+ data: params.data ?? {},
227
+ },
228
+ actions: [
229
+ ...actions,
230
+ {
231
+ action: params.action,
232
+ at: now,
233
+ data: params.data ?? {},
234
+ }
235
+ ]
236
+ };
237
+
238
+ target.output = {
239
+ ...output,
240
+ _mp: nextMp,
241
+ };
242
+
243
+ const updatedMessage: AgentMessage = {
244
+ ...message,
245
+ toolCalls,
246
+ };
247
+
248
+ await this.messageRepository.update(message.id, { toolCalls });
249
+ // Bump chat updatedAt so listChats ordering reflects the action.
250
+ await this.chatRepository.update(chat.id, { updatedAt: now } as any);
251
+ socketService.emitMessageUpdate(chat.userId, updatedMessage, params.excludeSocketId);
252
+
253
+ if (params.systemMessage) {
254
+ //todo discuss support for system messages
255
+ //await this.createMessage(chat, MessageRole.System, params.systemMessage, undefined, params.excludeSocketId);
256
+ }
257
+
258
+ return { ok: true };
259
+ }
260
+
261
+ private async streamMessageStep(params: { chat: AgentChat, assistantMessage: AgentMessage, existingMessages: AgentMessage[], signal: AbortSignal, excludeSocketId?: string, agentOptions: Omit<GenerateTextOptions, 'messages'> }): Promise<void> {
262
+ const { chat, assistantMessage, existingMessages, signal, excludeSocketId, agentOptions } = params;
263
+ if (signal.aborted) {
264
+ return;
265
+ }
266
+ const result = await AIHelper.streamAssistantResponse(existingMessages, signal, agentOptions);
267
+
268
+ try {
269
+ for await (const chunk of result.fullStream) {
270
+ if (chunk.type === 'error') {
271
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Error, data: chunk.error as Error });
272
+ assistantMessage.content = (chunk.error as Record<string, string>).message || 'Sorry, I cannot process you request due to the error';
273
+ await this.messageRepository.update(assistantMessage.id, assistantMessage);
274
+ await this.chatRepository.update(chat.id, { status: AgentStatus.Error });
275
+ continue
276
+ }
277
+ if (chunk.type === 'abort') {
278
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Aborted, data: undefined });
279
+ await this.chatRepository.update(chat.id, { status: AgentStatus.Aborted });
280
+ await this.messageRepository.update(assistantMessage.id, assistantMessage);
281
+ socketService.emitMessageUpdate(chat.userId, assistantMessage, excludeSocketId);
282
+
283
+ continue;
284
+ }
285
+ if (chunk.type === 'finish') {
286
+ const index = existingMessages.findIndex(({ id }) => id === assistantMessage.id);
287
+
288
+ if (index === -1){
289
+ existingMessages.push(assistantMessage)
290
+ } else {
291
+ existingMessages[index] = assistantMessage;
292
+ }
293
+ if (chunk.totalUsage && typeof chunk.totalUsage === 'object') {
294
+ const totalUsage = chunk.totalUsage as { promptTokens?: number; completionTokens?: number; totalTokens?: number };
295
+ assistantMessage.tokens = totalUsage.totalTokens ??
296
+ ((totalUsage.promptTokens ?? 0) + (totalUsage.completionTokens ?? 0));
297
+ }
298
+
299
+ await this.messageRepository.update(assistantMessage.id, { ...assistantMessage });
300
+ if (chunk.finishReason === 'stop') {
301
+ if (assistantMessage.toolCalls?.some(toolCall => toolCall.requiresConfirmation && toolCall.approvalId && !toolCall.output)) {
302
+ await this.chatRepository.update(chat.id, { status: AgentStatus.WaitingForUserAction });
303
+ } else if (!assistantMessage.content && assistantMessage.toolCalls?.length && assistantMessage.toolCalls[assistantMessage.toolCalls?.length - 1].output) {
304
+ // handle tool-calls stop, openrouter provider does not return valid type
305
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: assistantMessage });
306
+ await this.streamMessageStep(params);
307
+ continue;
308
+ } else {
309
+ await this.chatRepository.update(chat.id, {status: AgentStatus.Finished});
310
+ }
311
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
312
+ continue;
313
+ }
314
+ if (chunk.finishReason === 'tool-calls') {
315
+ if (assistantMessage.toolCalls?.some(toolCall => toolCall.requiresConfirmation && toolCall.approvalId && !toolCall.output)) {
316
+ await this.chatRepository.update(chat.id, { status: AgentStatus.WaitingForUserAction });
317
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
318
+ } else {
319
+ await agentStore.shareAgentProcessEvent(chat.id, {
320
+ type: AgentProcessEventType.Update,
321
+ data: assistantMessage
322
+ });
323
+ await this.streamMessageStep(params);
324
+ }
325
+ continue;
326
+ }
327
+ //todo: Handle other finish reasons // length, content, error, other, undefined
328
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Finished, data: assistantMessage });
329
+ continue;
330
+ }
331
+ if (chunk.type === 'text-delta') {
332
+ assistantMessage.content += chunk.text;
333
+ const updatedMessage: AgentMessage = {
334
+ ...assistantMessage,
335
+ content: assistantMessage.content
336
+ };
337
+ socketService.emitMessageUpdate(chat.userId, updatedMessage, excludeSocketId);
338
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: updatedMessage });
339
+ await this.messageRepository.update(assistantMessage.id, { content: assistantMessage.content });
340
+ continue;
341
+ }
342
+ if (chunk.type === 'tool-input-start') {
343
+ if (!assistantMessage.toolCalls) assistantMessage.toolCalls = [];
344
+ assistantMessage.toolCalls.push({
345
+ id: chunk.id,
346
+ name: chunk.toolName,
347
+ status: AgentToolCallStatus.Pending,
348
+ input: {},
349
+ });
350
+ continue;
351
+ }
352
+ if (chunk.type === 'tool-call') {//Todo possible place to handle user-approval
353
+ const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
354
+ if (toolCall) {
355
+ toolCall.status = AgentToolCallStatus.Running;
356
+ toolCall.input = chunk.input;
357
+ }
358
+ continue;
359
+ }
360
+ if (chunk.type === 'tool-error') {
361
+ const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
362
+ if (toolCall) {
363
+ toolCall.status = AgentToolCallStatus.Failed;
364
+ toolCall.error = JSON.stringify(chunk.error);
365
+ }
366
+ continue;
367
+ }
368
+ if (chunk.type === 'tool-approval-request') {
369
+ const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCall.toolCallId);
370
+
371
+ if (toolCall) {
372
+ toolCall.requiresConfirmation = true;
373
+ toolCall.approvalId = chunk.approvalId;
374
+ }
375
+ continue;
376
+ }
377
+ if (chunk.type === 'tool-result') {
378
+ const toolCall = assistantMessage.toolCalls?.find(toolCall => toolCall.id === chunk.toolCallId);
379
+
380
+ if (toolCall) {
381
+ if (chunk.output.requiresApproval) {
382
+ toolCall.requiresConfirmation = true;
383
+ continue;
384
+ }
385
+ toolCall.status = AgentToolCallStatus.Succeeded;
386
+ toolCall.output = chunk.output;
387
+ }
388
+ continue;
389
+ }
390
+ if (chunk.type === 'reasoning-delta') {
391
+ if (!assistantMessage.reasoning) assistantMessage.reasoning = ''
392
+ assistantMessage.reasoning += chunk.text;
393
+ socketService.emitMessageUpdate(chat.userId, assistantMessage, excludeSocketId);
394
+ await this.messageRepository.update(assistantMessage.id, { reasoning: assistantMessage.reasoning });
395
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: assistantMessage });
396
+ continue;
397
+ }
398
+ }
399
+ } catch (streamError) {
400
+ if (streamError instanceof Error && streamError.name === 'AbortError') {
401
+ return;
402
+ }
403
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Error, data: streamError as Error });
404
+ throw streamError;
405
+ }
406
+ }
407
+
408
+ private isApprovalPayload(payload: SendMessagePayload): payload is SendMessagePayloadWithApproval {
409
+ return 'approvalId' in payload;
410
+ }
411
+
412
+ private async processApprovalPayload(chat: AgentChat, payload: SendMessagePayloadWithApproval & { context?: AdditionalContext }) {
413
+ const assistantMessage = await this.messageRepository.findById(payload.messageId);
414
+ if (!assistantMessage || assistantMessage.chat !== chat.id) {
415
+ throw new Error(`Assistant message with id ${payload.messageId} not found`);
416
+ }
417
+ const agentOptions = await AIHelper.getAgentOptions({
418
+ agentName: assistantMessage.agentName,
419
+ context: payload.context
420
+ });
421
+ const toolCall = assistantMessage.toolCalls?.find(tc => tc.approvalId === payload.approvalId);
422
+ if (!toolCall) {
423
+ // continue, tool is not found
424
+ return {
425
+ assistantMessage,
426
+ agentOptions,
427
+ }
428
+ }
429
+
430
+ toolCall.approved = payload.approved;
431
+ toolCall.reason = payload.reason;
432
+
433
+ if (!toolCall.approved) {
434
+ toolCall.output = {
435
+ type: 'execution-denied',
436
+ reason: toolCall.reason
437
+ }
438
+ toolCall.status = AgentToolCallStatus.Failed;
439
+ } else {
440
+ const executeFunction = agentOptions.tools?.[toolCall.name]?.execute;
441
+ if (executeFunction) {
442
+ try {
443
+ toolCall.output = await executeFunction(toolCall.input, {toolCallId: toolCall.id, messages: []})
444
+ toolCall.status = AgentToolCallStatus.Succeeded;
445
+ } catch(error) {
446
+ logger.error(error);
447
+ toolCall.status = AgentToolCallStatus.Failed;
448
+ toolCall.output = {
449
+ type: 'execution-denied',
450
+ reason: 'Tool failed with error'
451
+ }
452
+ }
453
+ } else {
454
+ toolCall.output = {
455
+ type: 'execution-denied',
456
+ reason: 'Tool was not found'
457
+ }
458
+ toolCall.status = AgentToolCallStatus.Failed;
459
+ }
460
+ }
461
+
462
+ await this.messageRepository.update(assistantMessage.id, assistantMessage);
463
+ return {
464
+ assistantMessage,
465
+ agentOptions,
466
+ }
467
+ }
468
+
469
+ async processNewUserMessage(chat: AgentChat, payload: SendMessagePayloadWithContent & { context?: AdditionalContext }, prevMessages: AgentMessage[], excludeSocketId?: string) {
470
+ const userMessage = await this.createMessage(chat, MessageRole.User, {content: payload.content, agentName: undefined}, payload.attachments);
471
+ await agentStore.shareAgentProcessEvent(chat.id, { type: AgentProcessEventType.Update, data: userMessage });
472
+
473
+ const agentOptions = await AIHelper.getAgentOptions({
474
+ contextKey: chat.contextKey,
475
+ messages: await AIHelper.convertMessages([...prevMessages, userMessage], true),
476
+ context: payload.context,
477
+ agentName: undefined,
478
+ modelId: chat.model,
479
+ });
480
+
481
+ const assistantMessage = await this.createMessage(chat, MessageRole.Assistant, {content: ' ', agentName: agentOptions.name}, undefined, excludeSocketId);
482
+ assistantMessage.content = ''
483
+
484
+ return {
485
+ userMessage,
486
+ assistantMessage,
487
+ agentOptions,
488
+ }
489
+ }
490
+
491
+ async streamMessage(chat: AgentChat, payload: SendMessagePayload & { context?: AdditionalContext }, excludeSocketId?: string, onAgentStart?: () => void): Promise<void> {
492
+ try {
493
+ const abortController = agentStore.registerAgentProcess(chat.id);
494
+ onAgentStart?.()
495
+ let assistantMessage: AgentMessage;
496
+ let agentOptions: Omit<GenerateTextOptions, 'messages'>;
497
+
498
+ const existingMessages = await this.messageRepository.findByChatId(chat.id);
499
+
500
+ // Limit context to prevent exceeding token limits
501
+ const limitedMessages = ContextLimiter.limitContext(
502
+ existingMessages,
503
+ {
504
+ maxMessages: config.ai.maxContextMessages,
505
+ keepFirstUserMessage: true,
506
+ keepSystemMessages: true,
507
+ }
508
+ );
509
+
510
+ if (this.isApprovalPayload(payload)) {
511
+ const result = await this.processApprovalPayload(chat, payload);
512
+ assistantMessage = result.assistantMessage;
513
+ agentOptions = result.agentOptions;
514
+ const foundIndex = limitedMessages.findIndex(m => m.id === result.assistantMessage.id);
515
+ if(foundIndex === -1) {
516
+ limitedMessages.push(result.assistantMessage);
517
+ } else {
518
+ limitedMessages[foundIndex] = result.assistantMessage;
519
+ }
520
+ } else {
521
+ const result = await this.processNewUserMessage(chat, payload, limitedMessages, excludeSocketId);
522
+ assistantMessage = result.assistantMessage;
523
+ agentOptions = result.agentOptions;
524
+ limitedMessages.push(result.userMessage);
525
+ }
526
+
527
+ await this.streamMessageStep({
528
+ chat,
529
+ existingMessages: limitedMessages,
530
+ assistantMessage,
531
+ agentOptions,
532
+ excludeSocketId: excludeSocketId,
533
+ signal: abortController.signal,
534
+ })
535
+ } catch (error) {
536
+ throw error;
537
+ }
538
+ }
539
+
540
+ listArtifacts(chatId: string): AgentArtifact[] {
541
+ return this.artifactStore.listArtifacts(chatId);
542
+ }
543
+
544
+ private eventToChunk(event: AgentProcessEvent): StreamChunk | null {
545
+ switch (event.type) {
546
+ case AgentProcessEventType.Update:
547
+ return {
548
+ type: StreamChunkType.Message,
549
+ message: event.data
550
+ };
551
+
552
+ case AgentProcessEventType.Error:
553
+ return {
554
+ type: StreamChunkType.Error,
555
+ error: event.data.message || 'Unknown error occurred'
556
+ };
557
+ case AgentProcessEventType.Finished:
558
+ return {
559
+ type: StreamChunkType.Message,
560
+ message: event.data
561
+ };
562
+ case AgentProcessEventType.Aborted:
563
+ // Aborted events don't produce chunks, they just signal stream end
564
+ return null;
565
+ case AgentProcessEventType.Stop:
566
+ // Stop events don't produce chunks
567
+ return null;
568
+ default:
569
+ return null;
570
+ }
571
+ }
572
+
573
+ getMessageStream(chatId: string): PassThrough {
574
+ const stream = new PassThrough();
575
+ let ended = false;
576
+
577
+ const endStream = (error?: unknown) => {
578
+ if (ended) return;
579
+ ended = true;
580
+
581
+ if (error) {
582
+ logger.error({ error }, 'Message stream error');
583
+ }
584
+
585
+ if (!stream.destroyed && !stream.writableEnded) {
586
+ try {
587
+ stream.write('data: [DONE]\n\n');
588
+ stream.end();
589
+ } catch (e) {
590
+ logger.error({ e }, 'Failed to end SSE stream');
591
+ }
592
+ }
593
+ };
594
+
595
+ const pushChunk = (chunk: StreamChunk) => {
596
+ if (ended || stream.destroyed || stream.writableEnded) return;
597
+ stream.write(`data: ${JSON.stringify(chunk)}\n\n`);
598
+ };
599
+
600
+ const listener = (event: AgentProcessEvent) => {
601
+ if (ended) return;
602
+
603
+ try {
604
+ const chunk = this.eventToChunk(event);
605
+ if (chunk) {
606
+ pushChunk(chunk);
607
+ }
608
+
609
+ if (
610
+ event.type === AgentProcessEventType.Finished ||
611
+ event.type === AgentProcessEventType.Error ||
612
+ event.type === AgentProcessEventType.Aborted
613
+ ) {
614
+ endStream();
615
+ agentStore.removeListener(chatId, listener);
616
+ }
617
+ } catch (err) {
618
+ pushChunk({
619
+ type: StreamChunkType.Error,
620
+ error: err instanceof Error ? err.message : 'Unknown error occurred',
621
+ });
622
+ endStream(err);
623
+ agentStore.removeListener(chatId, listener);
624
+ }
625
+ };
626
+
627
+ // Cleanup on consumer side closing
628
+ const closeHandler = () => {
629
+ agentStore.removeListener(chatId, listener);
630
+ };
631
+
632
+ const errorHandler = (err: Error) => {
633
+ logger.error({ err }, 'Readable stream error');
634
+ agentStore.removeListener(chatId, listener);
635
+ endStream(err);
636
+ };
637
+
638
+ stream.on('close', closeHandler);
639
+ stream.on('error', errorHandler);
640
+
641
+ const listenerAttached = agentStore.addListener(chatId, listener);
642
+ if (!listenerAttached) {
643
+ // Remove stream listeners if listener attachment failed to prevent memory leak
644
+ stream.removeListener('close', closeHandler);
645
+ stream.removeListener('error', errorHandler);
646
+ endStream(new Error('Listener could not be attached'));
647
+ }
648
+
649
+ return stream;
650
+ }
651
+
652
+
653
+ async createMessageStream(
654
+ payload: SendMessagePayload & { context?: AdditionalContext },
655
+ excludeSocketId?: string
656
+ ): Promise<PassThrough> {
657
+ const stream = new PassThrough();
658
+
659
+ (async () => {
660
+ try {
661
+ const chat = await this.upsertAndGetChat(payload, excludeSocketId);
662
+
663
+ // Send chat metadata first
664
+ if (!('chatId' in payload) && !('messageId' in payload) && chat) {
665
+ stream.write(
666
+ `data: ${JSON.stringify({
667
+ type: StreamChunkType.Chat,
668
+ chatId: chat.id,
669
+ chat,
670
+ })}\n\n`
671
+ );
672
+ }
673
+
674
+ await this.streamMessage(chat, payload, excludeSocketId, async () => {
675
+ if (stream.destroyed || stream.writableEnded){
676
+ return;
677
+ }
678
+ const messageStream = this.getMessageStream(chat.id);
679
+
680
+ // If client disconnects, destroy inner stream
681
+ stream.on('close', () => {
682
+ if (!messageStream.destroyed) {
683
+ messageStream.destroy();
684
+ }
685
+ });
686
+
687
+ await pipelineAsync(
688
+ messageStream,
689
+ stream
690
+ );
691
+ });
692
+ } catch (error) {
693
+ logger.error({ error }, 'createMessageStream failed');
694
+
695
+ if (!stream.destroyed && !stream.writableEnded) {
696
+ stream.write(
697
+ `data: ${JSON.stringify({
698
+ type: StreamChunkType.Error,
699
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
700
+ })}\n\n`
701
+ );
702
+ stream.write('data: [DONE]\n\n');
703
+ stream.end();
704
+ }
705
+ }
706
+ })().catch((err) => {
707
+ logger.error({ err }, 'Unhandled stream task error');
708
+ if (!stream.destroyed) stream.destroy(err);
709
+ });
710
+
711
+ return stream;
712
+ }
713
+ }