@retrivora-ai/rag-engine 1.9.7 → 1.9.9

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 (60) hide show
  1. package/README.md +136 -136
  2. package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
  3. package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2327 -474
  7. package/dist/handlers/index.mjs +2329 -473
  8. package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
  9. package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
  10. package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
  11. package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
  12. package/dist/index.css +783 -550
  13. package/dist/index.d.mts +35 -7
  14. package/dist/index.d.ts +35 -7
  15. package/dist/index.js +419 -282
  16. package/dist/index.mjs +426 -292
  17. package/dist/server.d.mts +65 -6
  18. package/dist/server.d.ts +65 -6
  19. package/dist/server.js +2185 -317
  20. package/dist/server.mjs +2185 -317
  21. package/package.json +13 -8
  22. package/src/app/constants.tsx +37 -7
  23. package/src/app/page.tsx +2 -0
  24. package/src/components/ChatWidget.tsx +2 -1
  25. package/src/components/ChatWindow.tsx +4 -1
  26. package/src/components/CodeViewer.tsx +19 -14
  27. package/src/components/MarkdownComponents.tsx +44 -1
  28. package/src/components/MessageBubble.tsx +162 -50
  29. package/src/components/constants.tsx +228 -0
  30. package/src/config/RagConfig.ts +48 -2
  31. package/src/config/constants.ts +4 -0
  32. package/src/config/serverConfig.ts +15 -0
  33. package/src/core/ConfigResolver.ts +38 -6
  34. package/src/core/DatabaseStorage.ts +469 -0
  35. package/src/core/LicenseVerifier.ts +260 -0
  36. package/src/core/MultiAgentCoordinator.ts +239 -0
  37. package/src/core/Pipeline.ts +151 -18
  38. package/src/core/Retrivora.ts +7 -0
  39. package/src/core/VectorPlugin.ts +12 -3
  40. package/src/core/mcp.ts +261 -0
  41. package/src/handlers/index.ts +449 -63
  42. package/src/hooks/useRagChat.ts +96 -42
  43. package/src/hooks/useStoredMessages.ts +15 -4
  44. package/src/index.ts +3 -0
  45. package/src/llm/LLMFactory.ts +9 -1
  46. package/src/llm/providers/GroqProvider.ts +176 -0
  47. package/src/llm/providers/QwenProvider.ts +191 -0
  48. package/src/server.ts +7 -0
  49. package/src/types/chat.ts +14 -0
  50. package/src/types/props.ts +12 -0
  51. package/.env.example +0 -80
  52. package/LICENSE.txt +0 -21
  53. package/src/components/AmbientBackground.tsx +0 -29
  54. package/src/components/ArchitectureCard.tsx +0 -17
  55. package/src/components/ArchitectureCardsSection.tsx +0 -15
  56. package/src/components/DocViewer.tsx +0 -103
  57. package/src/components/Documentation.tsx +0 -121
  58. package/src/components/Hero.tsx +0 -59
  59. package/src/components/Lifecycle.tsx +0 -37
  60. package/src/components/Navbar.tsx +0 -55
@@ -0,0 +1,260 @@
1
+ import * as crypto from 'crypto';
2
+ import { ConfigurationException } from '../exceptions';
3
+
4
+ export interface LicensePayload {
5
+ projectId: string;
6
+ expiresAt: number; // UNIX timestamp in seconds
7
+ tier: string;
8
+ }
9
+
10
+ /**
11
+ * LicenseVerifier — handles cryptographic validation of signed JWT license keys.
12
+ * Enables zero-latency local license validation without external network calls.
13
+ */
14
+ export class LicenseVerifier {
15
+ // A simple in-memory cache to save CPU overhead of cryptographic signature checks.
16
+ private static readonly cache = new Map<
17
+ string,
18
+ { payload: LicensePayload; cachedAt: number }
19
+ >();
20
+ private static readonly CACHE_TTL_MS = 60 * 1000; // Cache verified license for 60 seconds
21
+
22
+ // Retrivora's Public Key used to verify the license key signature.
23
+ // In production, this matches the private key held by Retrivora SaaS.
24
+ private static readonly PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
25
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMieCkCBtTfByRNOserm
26
+ XM4T0Am29JyNzB4XQPe+WJJ56CN73H1HLXx9n1ByFcxkEJ9po+SURj5DnvUXwEy+
27
+ xstx33wDPpVmcMQe33j5CRYS0S4gICiNqIRN7XkTlJtrcXqrmh1/hdOAwfRbjO1T
28
+ NVtORHwUwWfI/lBLyxUPGtKPed+0fQ7NtcW4o00JXxs3EjCB5BV9CbnXoTjQb/4h
29
+ iwZof4l8rb7oaNzOsVg0RSyxsETX7Ex5sI0CjBz1p2mYp4oVhw/A/h8Ls7H0XzjC
30
+ +Eb6BLtsytt5JHoSp0jExLlCpDmpys2L+kETcBVx+IqiXtdN1n6KbkksvEDUVzLI
31
+ MwIDAQAB
32
+ -----END PUBLIC KEY-----`;
33
+
34
+ /**
35
+ * Decodes, verifies signature, and checks metadata of a license key.
36
+ *
37
+ * @param licenseKey - Base64url signed JWT license key.
38
+ * @param currentProjectId - Project namespace ID.
39
+ * @param publicKeyOverride - Optional override for unit/integration tests.
40
+ */
41
+ static verify(
42
+ licenseKey: string | undefined,
43
+ currentProjectId: string,
44
+ provider?: string,
45
+ publicKeyOverride?: string
46
+ ): LicensePayload {
47
+ const isProduction = process.env.NODE_ENV === 'production';
48
+ const now = Date.now();
49
+
50
+ // Check verification cache to bypass crypto computations on hot paths
51
+ if (licenseKey) {
52
+ const cacheKey = `${licenseKey}:${currentProjectId}:${provider || ''}:${publicKeyOverride || ''}`;
53
+ const cached = this.cache.get(cacheKey);
54
+ if (cached && now - cached.cachedAt < this.CACHE_TTL_MS) {
55
+ // Enforce expiration check even for cached items
56
+ const currentTimestampSec = Math.floor(now / 1000);
57
+ if (cached.payload.expiresAt && currentTimestampSec > cached.payload.expiresAt) {
58
+ this.cache.delete(cacheKey);
59
+ } else {
60
+ return cached.payload;
61
+ }
62
+ }
63
+ }
64
+
65
+ // 1. Development Fallback (Fail-open locally but log warning)
66
+ if (!licenseKey) {
67
+ if (isProduction) {
68
+ throw new ConfigurationException(
69
+ '[Retrivora SDK] License key (licenseKey) is required in production environments.'
70
+ );
71
+ }
72
+ console.warn(
73
+ `\x1b[33m[Retrivora SDK] WARNING: Running without a license key. This namespace (${currentProjectId}) is permitted for local development only.\x1b[0m`
74
+ );
75
+ return {
76
+ projectId: currentProjectId,
77
+ expiresAt: Math.floor(Date.now() / 1000) + 86400, // Valid for 24h
78
+ tier: 'hobby',
79
+ };
80
+ }
81
+
82
+ try {
83
+ const parts = licenseKey.split('.');
84
+ if (parts.length !== 3) {
85
+ throw new Error('Malformed token structure (expected 3 parts).');
86
+ }
87
+
88
+ const [headerB64, payloadB64, signatureB64] = parts;
89
+ const dataToVerify = `${headerB64}.${payloadB64}`;
90
+
91
+ const publicKey = publicKeyOverride ?? this.PUBLIC_KEY;
92
+
93
+ // 2. Cryptographic signature check
94
+ const signature = Buffer.from(signatureB64, 'base64url');
95
+ const data = Buffer.from(dataToVerify);
96
+
97
+ const isValid = crypto.verify(
98
+ 'sha256',
99
+ data,
100
+ publicKey,
101
+ signature
102
+ );
103
+
104
+ if (!isValid) {
105
+ throw new Error('Signature verification failed.');
106
+ }
107
+
108
+ // 3. Decode & validate payload checks
109
+ const payload: LicensePayload = JSON.parse(
110
+ Buffer.from(payloadB64, 'base64url').toString('utf8')
111
+ );
112
+
113
+ if (!payload.projectId) {
114
+ throw new Error('License payload is missing "projectId".');
115
+ }
116
+
117
+ if (payload.projectId !== currentProjectId) {
118
+ throw new Error(
119
+ `Project ID mismatch. License is bound to project namespace "${payload.projectId}" ` +
120
+ `but configuration has "${currentProjectId}".`
121
+ );
122
+ }
123
+
124
+ if (!payload.expiresAt) {
125
+ throw new Error('License payload is missing expiration ("expiresAt").');
126
+ }
127
+
128
+ const currentTimestampSec = Math.floor(Date.now() / 1000);
129
+ if (currentTimestampSec > payload.expiresAt) {
130
+ throw new Error(
131
+ `License key has expired. Expiration: ${new Date(
132
+ payload.expiresAt * 1000
133
+ ).toDateString()}`
134
+ );
135
+ }
136
+
137
+ // 4. Validate Vector DB Provider against Tier (Production only)
138
+ if (isProduction && provider) {
139
+ const tier = (payload.tier || '').toLowerCase();
140
+ const normalizedProvider = provider.toLowerCase();
141
+
142
+ if (tier === 'hobby') {
143
+ // Hobby tier only supports PostgreSQL/pgvector/Supabase
144
+ const allowedHobby = ['postgresql', 'pgvector', 'supabase'];
145
+ if (!allowedHobby.includes(normalizedProvider)) {
146
+ throw new Error(
147
+ `The database provider "${provider}" is not allowed on the Hobby tier. ` +
148
+ `Hobby tier is restricted to PostgreSQL and Supabase. ` +
149
+ `Please upgrade to a Developer Pro or Enterprise plan.`
150
+ );
151
+ }
152
+ } else if (tier === 'pro' || tier === 'developer_pro' || tier === 'premium' || tier === 'growth') {
153
+ // Developer Pro supports: PostgreSQL, Pinecone, Qdrant, MongoDB, Milvus, ChromaDB
154
+ const allowedPro = [
155
+ 'postgresql', 'pgvector', 'supabase',
156
+ 'pinecone', 'qdrant', 'mongodb', 'milvus',
157
+ 'chroma', 'chromadb'
158
+ ];
159
+ if (!allowedPro.includes(normalizedProvider)) {
160
+ throw new Error(
161
+ `The database provider "${provider}" is not allowed on the Developer Pro tier. ` +
162
+ `Please upgrade to an Enterprise plan.`
163
+ );
164
+ }
165
+ }
166
+ // Enterprise has access to all databases (including redis, weaviate, custom ones)
167
+ }
168
+
169
+ if (licenseKey) {
170
+ const cacheKey = `${licenseKey}:${currentProjectId}:${provider || ''}:${publicKeyOverride || ''}`;
171
+ this.cache.set(cacheKey, { payload, cachedAt: now });
172
+ }
173
+
174
+ return payload;
175
+ } catch (err) {
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ throw new ConfigurationException(
178
+ `[Retrivora SDK] License key validation failed: ${message}`
179
+ );
180
+ }
181
+ }
182
+
183
+ static async verifyIP(licenseKey: string | undefined): Promise<void> {
184
+ if (!licenseKey) return;
185
+ try {
186
+ const parts = licenseKey.split('.');
187
+ if (parts.length !== 3) return;
188
+ const [, payloadB64] = parts;
189
+ const payload: LicensePayload & { ipv4?: string; ipv6?: string } = JSON.parse(
190
+ Buffer.from(payloadB64, 'base64url').toString('utf8')
191
+ );
192
+
193
+ const tier = (payload.tier || '').toLowerCase();
194
+ // Skip check if Enterprise or Custom
195
+ if (tier === 'enterprise' || tier === 'custom') {
196
+ return;
197
+ }
198
+
199
+ // If Hobby (free_trial) or Developer Pro, perform the check
200
+ if (payload.ipv4 || payload.ipv6) {
201
+ let currentIpv4 = '';
202
+ let currentIpv6 = '';
203
+
204
+ try {
205
+ const res = await fetch('https://api4.ipify.org?format=json', { signal: AbortSignal.timeout(3000) });
206
+ const data = await res.json();
207
+ currentIpv4 = data.ip;
208
+ } catch (e) {}
209
+
210
+ try {
211
+ const res = await fetch('https://api6.ipify.org?format=json', { signal: AbortSignal.timeout(3000) });
212
+ const data = await res.json();
213
+ currentIpv6 = data.ip;
214
+ } catch (e) {}
215
+
216
+ const localIps: string[] = [];
217
+ try {
218
+ const osModule = require('os');
219
+ const interfaces = osModule.networkInterfaces();
220
+ for (const name of Object.keys(interfaces)) {
221
+ for (const iface of interfaces[name] || []) {
222
+ if (!iface.internal) {
223
+ localIps.push(iface.address);
224
+ }
225
+ }
226
+ }
227
+ } catch (e) {}
228
+
229
+ const isIpv4Match = payload.ipv4 && (
230
+ currentIpv4 === payload.ipv4 ||
231
+ localIps.includes(payload.ipv4)
232
+ );
233
+ const isIpv6Match = payload.ipv6 && (
234
+ currentIpv6 === payload.ipv6 ||
235
+ localIps.includes(payload.ipv6)
236
+ );
237
+
238
+ if (payload.ipv4 && payload.ipv6) {
239
+ if (!isIpv4Match && !isIpv6Match) {
240
+ throw new Error(
241
+ `License key access restricted. This license key was created for a different system (authorized IPv4: ${payload.ipv4}, IPv6: ${payload.ipv6}) and cannot be used on this machine.`
242
+ );
243
+ }
244
+ } else if (payload.ipv4 && !isIpv4Match) {
245
+ throw new Error(
246
+ `License key access restricted. This license key was created for a different system (authorized IPv4: ${payload.ipv4}) and cannot be used on this machine.`
247
+ );
248
+ } else if (payload.ipv6 && !isIpv6Match) {
249
+ throw new Error(
250
+ `License key access restricted. This license key was created for a different system (authorized IPv6: ${payload.ipv6}) and cannot be used on this machine.`
251
+ );
252
+ }
253
+ }
254
+ } catch (err: any) {
255
+ if (err.message.includes('License key access restricted')) {
256
+ throw new ConfigurationException(`[Retrivora SDK] ${err.message}`);
257
+ }
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,239 @@
1
+ import { ILLMProvider } from '../llm/ILLMProvider';
2
+ import { MCPRegistry } from './mcp';
3
+ import { ChatMessage, ChatResponse } from '../types';
4
+
5
+ interface CoordinatorOptions {
6
+ llmProvider: ILLMProvider;
7
+ mcpRegistry: MCPRegistry;
8
+ documentSearch: (query: string) => Promise<{ reply: string; sources: any[] }>;
9
+ maxIterations?: number;
10
+ }
11
+
12
+ export class MultiAgentCoordinator {
13
+ private llmProvider: ILLMProvider;
14
+ private mcpRegistry: MCPRegistry;
15
+ private documentSearch: (query: string) => Promise<{ reply: string; sources: any[] }>;
16
+ private maxIterations: number;
17
+
18
+ constructor(options: CoordinatorOptions) {
19
+ this.llmProvider = options.llmProvider;
20
+ this.mcpRegistry = options.mcpRegistry;
21
+ this.documentSearch = options.documentSearch;
22
+ this.maxIterations = options.maxIterations ?? 5;
23
+ }
24
+
25
+ /**
26
+ * Run the multi-agent coordination loop synchronously and return the final response.
27
+ */
28
+ async run(question: string, history: ChatMessage[] = []): Promise<ChatResponse> {
29
+ const generator = this.runStream(question, history);
30
+ let finalReply = '';
31
+ const sources: any[] = [];
32
+
33
+ for await (const chunk of generator) {
34
+ if (typeof chunk === 'string') {
35
+ finalReply += chunk;
36
+ } else if (chunk && typeof chunk === 'object') {
37
+ if ('reply' in chunk && chunk.reply) {
38
+ finalReply += chunk.reply;
39
+ }
40
+ if ('sources' in chunk && chunk.sources) {
41
+ sources.push(...chunk.sources);
42
+ }
43
+ }
44
+ }
45
+
46
+ return {
47
+ reply: finalReply,
48
+ sources,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Run the multi-agent coordination loop as an async stream, yielding intermediate thought blocks and tool execution events.
54
+ */
55
+ async *runStream(question: string, history: ChatMessage[] = []): AsyncGenerator<string | ChatResponse> {
56
+ // 1. Gather all tools
57
+ const mcpTools = await this.mcpRegistry.getAllTools();
58
+
59
+ // Construct system instructions with tools
60
+ let systemPrompt = `You are a goal-driven supervisor agent coordinating a RAG search engine and external MCP servers to solve the user's task.
61
+ You must think step-by-step before acting. Write your internal thinking process inside a \`<think>...</think>\` block first, then act or answer.
62
+
63
+ Available Tools:
64
+ - document_search(query: string): Search the vector database and knowledge base for documents.
65
+
66
+ ${mcpTools.length > 0 ? 'MCP Server Tools:' : ''}
67
+ ${mcpTools.map(mt => `- ${mt.tool.name}(${JSON.stringify(mt.tool.inputSchema?.properties || {})}): ${mt.tool.description || 'No description provided.'}`).join('\n')}
68
+
69
+ Tool Calling Protocol:
70
+ If you need to call a tool, write:
71
+ TOOL_CALL: <toolName>({ "argName": "value" })
72
+ And then STOP generating immediately.
73
+
74
+ Once you have gathered enough information to fully satisfy the user's query, write your final response directly after the closing \`</think>\` block.
75
+ `;
76
+
77
+ // 2. Setup message history for the supervisor
78
+ const supervisorHistory: ChatMessage[] = [
79
+ { role: 'system' as any, content: systemPrompt },
80
+ ...history,
81
+ { role: 'user', content: question }
82
+ ];
83
+
84
+ let iteration = 0;
85
+ const allSources: any[] = [];
86
+
87
+ while (iteration < this.maxIterations) {
88
+ iteration++;
89
+
90
+ const context = ''; // Context is populated by tool call results directly in message history
91
+ let fullModelResponse = '';
92
+ let textBuffer = '';
93
+ let inThinking = false;
94
+ let toolCallDetected: { name: string; args: any } | null = null;
95
+
96
+ if (!this.llmProvider.chatStream) {
97
+ // Fallback for non-streaming providers
98
+ const reply = await this.llmProvider.chat(supervisorHistory, context);
99
+ fullModelResponse = reply;
100
+
101
+ // Parse simulated thinking / tool call
102
+ const thinkIndex = reply.indexOf('<think>');
103
+ const endThinkIndex = reply.indexOf('</think>');
104
+
105
+ let thinkingText = '';
106
+ let answerText = reply;
107
+
108
+ if (thinkIndex !== -1 && endThinkIndex !== -1) {
109
+ thinkingText = reply.substring(thinkIndex + 7, endThinkIndex);
110
+ answerText = reply.substring(endThinkIndex + 8);
111
+ yield { type: 'thinking', text: thinkingText } as any;
112
+ }
113
+
114
+ const toolMatch = answerText.match(/TOOL_CALL:\s*(\w+)\s*\(([\s\S]*)\)/);
115
+ if (toolMatch) {
116
+ const toolName = toolMatch[1];
117
+ const toolArgsStr = toolMatch[2];
118
+ try {
119
+ const toolArgs = JSON.parse(toolArgsStr.trim());
120
+ toolCallDetected = { name: toolName, args: toolArgs };
121
+ } catch (e) {
122
+ console.error('[MultiAgentCoordinator] Failed to parse tool args:', toolArgsStr, e);
123
+ }
124
+ } else {
125
+ yield answerText;
126
+ }
127
+ } else {
128
+ // Run with streaming completion
129
+ const stream = this.llmProvider.chatStream(supervisorHistory, context);
130
+
131
+ for await (const chunk of stream) {
132
+ fullModelResponse += chunk;
133
+ textBuffer += chunk;
134
+
135
+ // Check for <think> tag
136
+ if (!inThinking && textBuffer.includes('<think>')) {
137
+ inThinking = true;
138
+ const idx = textBuffer.indexOf('<think>');
139
+ textBuffer = textBuffer.slice(idx + 7);
140
+ }
141
+
142
+ // Check for </think> tag
143
+ if (inThinking && textBuffer.includes('</think>')) {
144
+ inThinking = false;
145
+ const idx = textBuffer.indexOf('</think>');
146
+ const thinkingDelta = textBuffer.slice(0, idx);
147
+ yield { type: 'thinking', text: thinkingDelta } as any;
148
+ textBuffer = textBuffer.slice(idx + 8);
149
+ }
150
+
151
+ // If we are in thinking mode, yield thinking delta
152
+ if (inThinking && textBuffer.length > 0) {
153
+ yield { type: 'thinking', text: textBuffer } as any;
154
+ textBuffer = '';
155
+ }
156
+
157
+ // Check for TOOL_CALL during stream
158
+ if (!inThinking && textBuffer.includes('TOOL_CALL:')) {
159
+ const idx = textBuffer.indexOf('TOOL_CALL:');
160
+ const precedingText = textBuffer.slice(0, idx);
161
+ if (precedingText.trim()) {
162
+ yield precedingText;
163
+ }
164
+ // Await remainder of stream to get the full tool call signature
165
+ textBuffer = textBuffer.slice(idx);
166
+ }
167
+
168
+ // If not in thinking and no tool call prefix is blocking, yield final answer text
169
+ if (!inThinking && !textBuffer.includes('TOOL_CALL:') && textBuffer.length > 0) {
170
+ yield textBuffer;
171
+ textBuffer = '';
172
+ }
173
+ }
174
+
175
+ // Flush any remaining buffer text
176
+ if (textBuffer.trim()) {
177
+ const toolMatch = textBuffer.match(/TOOL_CALL:\s*(\w+)\s*\(([\s\S]*)\)/);
178
+ if (toolMatch) {
179
+ const toolName = toolMatch[1];
180
+ const toolArgsStr = toolMatch[2];
181
+ try {
182
+ const toolArgs = JSON.parse(toolArgsStr.trim());
183
+ toolCallDetected = { name: toolName, args: toolArgs };
184
+ } catch (e) {
185
+ console.error('[MultiAgentCoordinator] Failed to parse tool args:', toolArgsStr, e);
186
+ }
187
+ } else {
188
+ yield textBuffer;
189
+ }
190
+ }
191
+ }
192
+
193
+ // 3. Append the model response to the supervisor history
194
+ supervisorHistory.push({ role: 'assistant', content: fullModelResponse });
195
+
196
+ // 4. Handle tool execution if requested, else break the loop
197
+ if (toolCallDetected) {
198
+ const { name: toolName, args: toolArgs } = toolCallDetected;
199
+
200
+ yield {
201
+ type: 'thinking',
202
+ text: `\n[Agent Call] Executing tool "${toolName}" with parameters ${JSON.stringify(toolArgs)}...\n`
203
+ } as any;
204
+
205
+ let toolResultText = '';
206
+ try {
207
+ if (toolName === 'document_search') {
208
+ const searchRes = await this.documentSearch(toolArgs.query || '');
209
+ toolResultText = `document_search returned:\n${searchRes.reply}\n\nSources count: ${searchRes.sources.length}`;
210
+ if (searchRes.sources && searchRes.sources.length > 0) {
211
+ allSources.push(...searchRes.sources);
212
+ yield { reply: '', sources: searchRes.sources };
213
+ }
214
+ } else {
215
+ // Call MCP tool
216
+ const mcpRes = await this.mcpRegistry.callTool(toolName, toolArgs);
217
+ toolResultText = `MCP Tool "${toolName}" returned:\n${JSON.stringify(mcpRes.content)}`;
218
+ }
219
+ } catch (err: any) {
220
+ toolResultText = `Error calling tool "${toolName}": ${err.message}`;
221
+ }
222
+
223
+ // Append tool output to history
224
+ supervisorHistory.push({
225
+ role: 'user',
226
+ content: `TOOL_RESULT for ${toolName}:\n${toolResultText}`
227
+ });
228
+
229
+ yield {
230
+ type: 'thinking',
231
+ text: `[Agent Output] Tool "${toolName}" executed. Continuing reasoning...\n`
232
+ } as any;
233
+ } else {
234
+ // No tool call requested — response is complete
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ }