@tyvm/knowhow 0.0.15 → 0.0.17

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 (129) hide show
  1. package/package.json +2 -1
  2. package/src/agents/base/base.ts +25 -6
  3. package/src/agents/index.ts +2 -2
  4. package/src/agents/tools/agentCall.ts +0 -1
  5. package/src/agents/tools/execCommand.ts +95 -4
  6. package/src/agents/tools/list.ts +23 -19
  7. package/src/agents/tools/writeFile.ts +1 -1
  8. package/src/chat.ts +11 -0
  9. package/src/config.ts +3 -1
  10. package/src/processors/Base64ImageDetector.ts +190 -0
  11. package/src/processors/TokenCompressor.ts +357 -0
  12. package/src/processors/ToolResponseCache.ts +235 -0
  13. package/src/services/Mcp.ts +4 -1
  14. package/src/services/MessageProcessor.ts +107 -0
  15. package/src/services/Tools.ts +100 -1
  16. package/src/services/types.ts +57 -0
  17. package/ts_build/src/agents/base/base.d.ts +3 -1
  18. package/ts_build/src/agents/base/base.js +20 -5
  19. package/ts_build/src/agents/base/base.js.map +1 -1
  20. package/ts_build/src/agents/index.d.ts +2 -2
  21. package/ts_build/src/agents/index.js +5 -3
  22. package/ts_build/src/agents/index.js.map +1 -1
  23. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  24. package/ts_build/src/agents/tools/execCommand.d.ts +6 -1
  25. package/ts_build/src/agents/tools/execCommand.js +70 -4
  26. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  27. package/ts_build/src/agents/tools/expandTokens.d.ts +3 -0
  28. package/ts_build/src/agents/tools/expandTokens.js +33 -0
  29. package/ts_build/src/agents/tools/expandTokens.js.map +1 -0
  30. package/ts_build/src/agents/tools/getBigString.d.ts +3 -0
  31. package/ts_build/src/agents/tools/getBigString.js +33 -0
  32. package/ts_build/src/agents/tools/getBigString.js.map +1 -0
  33. package/ts_build/src/agents/tools/list.js +19 -17
  34. package/ts_build/src/agents/tools/list.js.map +1 -1
  35. package/ts_build/src/agents/tools/writeFile.js +1 -1
  36. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  37. package/ts_build/src/chat.js +6 -0
  38. package/ts_build/src/chat.js.map +1 -1
  39. package/ts_build/src/config.js +1 -1
  40. package/ts_build/src/config.js.map +1 -1
  41. package/ts_build/src/processors/Base64ImageDetector.d.ts +14 -0
  42. package/ts_build/src/processors/Base64ImageDetector.js +153 -0
  43. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -0
  44. package/ts_build/src/processors/TokenCompressor.d.ts +28 -0
  45. package/ts_build/src/processors/TokenCompressor.js +226 -0
  46. package/ts_build/src/processors/TokenCompressor.js.map +1 -0
  47. package/ts_build/src/processors/ToolResponseCache.d.ts +22 -0
  48. package/ts_build/src/processors/ToolResponseCache.js +164 -0
  49. package/ts_build/src/processors/ToolResponseCache.js.map +1 -0
  50. package/ts_build/src/processors/ToolResponseManipulator.d.ts +22 -0
  51. package/ts_build/src/processors/ToolResponseManipulator.js +162 -0
  52. package/ts_build/src/processors/ToolResponseManipulator.js.map +1 -0
  53. package/ts_build/src/services/Mcp.js +3 -1
  54. package/ts_build/src/services/Mcp.js.map +1 -1
  55. package/ts_build/src/services/MessageProcessor.d.ts +17 -0
  56. package/ts_build/src/services/MessageProcessor.js +63 -0
  57. package/ts_build/src/services/MessageProcessor.js.map +1 -0
  58. package/ts_build/src/services/Tools.d.ts +12 -0
  59. package/ts_build/src/services/Tools.js +71 -0
  60. package/ts_build/src/services/Tools.js.map +1 -1
  61. package/ts_build/src/services/types.d.ts +32 -0
  62. package/ts_build/src/services/types.js +38 -0
  63. package/ts_build/src/services/types.js.map +1 -0
  64. package/ts_build/src/agents/configurable/OpenAIAgent.d.ts +0 -0
  65. package/ts_build/src/agents/configurable/OpenAIAgent.js +0 -1
  66. package/ts_build/src/agents/configurable/OpenAIAgent.js.map +0 -1
  67. package/ts_build/src/agents/tools/client.d.ts +0 -5
  68. package/ts_build/src/agents/tools/client.js +0 -21
  69. package/ts_build/src/agents/tools/client.js.map +0 -1
  70. package/ts_build/src/agents/tools/googleSearchTypes.d.ts +0 -74
  71. package/ts_build/src/agents/tools/googleSearchTypes.js +0 -3
  72. package/ts_build/src/agents/tools/googleSearchTypes.js.map +0 -1
  73. package/ts_build/src/commands/chat-ui.d.ts +0 -1
  74. package/ts_build/src/commands/chat-ui.js +0 -14
  75. package/ts_build/src/commands/chat-ui.js.map +0 -1
  76. package/ts_build/src/demo/chat-ui-demo.d.ts +0 -3
  77. package/ts_build/src/demo/chat-ui-demo.js +0 -20
  78. package/ts_build/src/demo/chat-ui-demo.js.map +0 -1
  79. package/ts_build/src/plugins/EmbeddingPluginV2.d.ts +0 -7
  80. package/ts_build/src/plugins/EmbeddingPluginV2.js +0 -41
  81. package/ts_build/src/plugins/EmbeddingPluginV2.js.map +0 -1
  82. package/ts_build/src/plugins/GitHubPluginV2.d.ts +0 -10
  83. package/ts_build/src/plugins/GitHubPluginV2.js +0 -57
  84. package/ts_build/src/plugins/GitHubPluginV2.js.map +0 -1
  85. package/ts_build/src/plugins/downloader/index.d.ts +0 -3
  86. package/ts_build/src/plugins/downloader/index.js +0 -41
  87. package/ts_build/src/plugins/downloader/index.js.map +0 -1
  88. package/ts_build/src/services/MessagePreprocessor.d.ts +0 -26
  89. package/ts_build/src/services/MessagePreprocessor.js +0 -190
  90. package/ts_build/src/services/MessagePreprocessor.js.map +0 -1
  91. package/ts_build/src/services/__tests__/MessagePreprocessor.test.d.ts +0 -1
  92. package/ts_build/src/services/__tests__/MessagePreprocessor.test.js +0 -117
  93. package/ts_build/src/services/__tests__/MessagePreprocessor.test.js.map +0 -1
  94. package/ts_build/src/terminal.d.ts +0 -1
  95. package/ts_build/src/terminal.js +0 -35
  96. package/ts_build/src/terminal.js.map +0 -1
  97. package/ts_build/src/ui/InkChatUI.d.ts +0 -1
  98. package/ts_build/src/ui/InkChatUI.js +0 -792
  99. package/ts_build/src/ui/InkChatUI.js.map +0 -1
  100. package/ts_build/src/ui/components/ChatInterface.d.ts +0 -15
  101. package/ts_build/src/ui/components/ChatInterface.js +0 -39
  102. package/ts_build/src/ui/components/ChatInterface.js.map +0 -1
  103. package/ts_build/src/ui/components/ChatMessage.d.ts +0 -8
  104. package/ts_build/src/ui/components/ChatMessage.js +0 -7
  105. package/ts_build/src/ui/components/ChatMessage.js.map +0 -1
  106. package/ts_build/src/ui/components/CommandPalette.d.ts +0 -8
  107. package/ts_build/src/ui/components/CommandPalette.js +0 -23
  108. package/ts_build/src/ui/components/CommandPalette.js.map +0 -1
  109. package/ts_build/src/ui/components/InputBar.d.ts +0 -8
  110. package/ts_build/src/ui/components/InputBar.js +0 -8
  111. package/ts_build/src/ui/components/InputBar.js.map +0 -1
  112. package/ts_build/src/ui/components/Sidebar.d.ts +0 -9
  113. package/ts_build/src/ui/components/Sidebar.js +0 -7
  114. package/ts_build/src/ui/components/Sidebar.js.map +0 -1
  115. package/ts_build/src/ui/components/StatusBar.d.ts +0 -10
  116. package/ts_build/src/ui/components/StatusBar.js +0 -8
  117. package/ts_build/src/ui/components/StatusBar.js.map +0 -1
  118. package/ts_build/src/ui/demo.d.ts +0 -3
  119. package/ts_build/src/ui/demo.js +0 -26
  120. package/ts_build/src/ui/demo.js.map +0 -1
  121. package/ts_build/src/ui/index.d.ts +0 -13
  122. package/ts_build/src/ui/index.js +0 -16
  123. package/ts_build/src/ui/index.js.map +0 -1
  124. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.d.ts +0 -1
  125. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js +0 -148
  126. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js.map +0 -1
  127. package/ts_build/tests/services/MessagePreprocessor.test.d.ts +0 -1
  128. package/ts_build/tests/services/MessagePreprocessor.test.js +0 -117
  129. package/ts_build/tests/services/MessagePreprocessor.test.js.map +0 -1
@@ -0,0 +1,357 @@
1
+ import { Message, Tool } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+ import { ToolsService } from "../services";
4
+
5
+ interface TokenCompressorStorage {
6
+ [key: string]: string;
7
+ }
8
+
9
+ export class TokenCompressor {
10
+ private storage: TokenCompressorStorage = {};
11
+ private maxTokens: number = 20000;
12
+ private compressionRatio: number = 0.1;
13
+ private keyPrefix: string = "compressed_";
14
+ private jsonPropertyThreshold: number = 20000;
15
+ private toolName: string = expandTokensDefinition.function.name;
16
+ private characterLimit: number = 16000; // ~4000 tokens
17
+
18
+ constructor(toolsService?: ToolsService) {
19
+ this.registerTool(toolsService);
20
+ }
21
+
22
+ // Rough token estimation (4 chars per token average)
23
+ private estimateTokens(text: string): number {
24
+ return Math.ceil(text.length / 4);
25
+ }
26
+
27
+ /**
28
+ * Attempts to parse content as JSON and returns parsed object if successful
29
+ */
30
+ private tryParseJson(content: string): any | null {
31
+ try {
32
+ return JSON.parse(content);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Compresses a string into chunks from the end, creating a chain of references
40
+ */
41
+ public compressStringInChunks(content: string, path: string = ""): string {
42
+ if (content.length <= this.characterLimit) {
43
+ return content;
44
+ }
45
+
46
+ const chunks: string[] = [];
47
+ const chunkKeys: string[] = [];
48
+ let remaining = content;
49
+
50
+ // Split from the end, creating chunks that will be linked
51
+ while (remaining.length > this.characterLimit) {
52
+ const chunkStart = remaining.length - this.characterLimit;
53
+ const chunk = remaining.substring(chunkStart);
54
+ chunks.unshift(chunk); // Add to beginning since we're working backwards
55
+ remaining = remaining.substring(0, chunkStart);
56
+ }
57
+
58
+ // The remaining part becomes the first chunk
59
+ if (remaining.length > 0) {
60
+ chunks.unshift(remaining);
61
+ }
62
+
63
+ // Store chunks and create chain of references
64
+ for (let i = chunks.length - 1; i >= 0; i--) {
65
+ const key = this.generateKey();
66
+ chunkKeys.unshift(key);
67
+
68
+ let chunkContent = chunks[i];
69
+
70
+ // Add reference to next chunk if it exists
71
+ if (i < chunks.length - 1) {
72
+ const nextKey = chunkKeys[i + 1];
73
+ chunkContent += `\n\n[NEXT_CHUNK_KEY: ${nextKey}]`;
74
+ }
75
+
76
+ this.storage[key] = chunkContent;
77
+ }
78
+
79
+ // Return reference to the first chunk
80
+ const firstKey = chunkKeys[0];
81
+ const totalTokens = this.estimateTokens(content);
82
+ const chunkCount = chunks.length;
83
+
84
+ return `[COMPRESSED_STRING - ${totalTokens} tokens in ${chunkCount} chunks]\nKey: ${firstKey}\nPath: ${path}\nPreview: ${content.substring(
85
+ 0,
86
+ 200
87
+ )}...\n[Use ${
88
+ this.toolName
89
+ } tool with key "${firstKey}" to retrieve content. Follow NEXT_CHUNK_KEY references for complete content]`;
90
+ }
91
+
92
+ /**
93
+ * Enhanced content compression that handles both JSON and string chunking
94
+ */
95
+ public compressContent(content: string, path: string = ""): string {
96
+ const tokens = this.estimateTokens(content);
97
+
98
+ if (tokens <= this.maxTokens) {
99
+ return content;
100
+ }
101
+
102
+ // Try to parse as JSON first
103
+ const jsonObj = this.tryParseJson(content);
104
+ if (jsonObj) {
105
+ // For JSON objects, compress individual properties
106
+ const compressedObj = this.compressJsonProperties(jsonObj, path);
107
+ const compressedContent = JSON.stringify(compressedObj, null, 2);
108
+
109
+ // If compression reduced size significantly, return compressed version
110
+ const compressedTokens = this.estimateTokens(compressedContent);
111
+ if (compressedTokens < tokens * 0.8) {
112
+ return compressedContent;
113
+ }
114
+ }
115
+
116
+ // For strings or when JSON compression wasn't effective, use chunking
117
+ return this.compressStringInChunks(content, path);
118
+ }
119
+
120
+ /**
121
+ * Compresses large properties within a JSON object using depth-first traversal.
122
+ * Implements an efficient backward-iterating chunking strategy for large arrays.
123
+ */
124
+ public compressJsonProperties(obj: any, path: string = ""): any {
125
+ if (Array.isArray(obj)) {
126
+ // Step 1: Recursively compress all items first (depth-first).
127
+ const processedItems = obj.map((item, index) =>
128
+ this.compressJsonProperties(item, `${path}[${index}]`)
129
+ );
130
+
131
+ // Step 2: Early exit if the whole array is already small enough.
132
+ // Leeway of 30% over, to avoid re-compression of retrievals
133
+ const initialTokens = this.estimateTokens(JSON.stringify(processedItems));
134
+ if (initialTokens <= this.jsonPropertyThreshold * 1.3) {
135
+ return processedItems;
136
+ }
137
+
138
+ // Step 3: Iterate backwards, building chunks from the end.
139
+ const finalArray: any[] = [];
140
+ let currentChunk: any[] = [];
141
+
142
+ for (let i = processedItems.length - 1; i >= 0; i--) {
143
+ const item = processedItems[i];
144
+ currentChunk.unshift(item); // Add item to the front of the current chunk
145
+
146
+ const chunkString = JSON.stringify(currentChunk);
147
+ const chunkTokens = this.estimateTokens(chunkString);
148
+
149
+ if (chunkTokens > this.jsonPropertyThreshold) {
150
+ const key = this.generateKey();
151
+ this.storage[key] = chunkString;
152
+
153
+ const stub = `[COMPRESSED_JSON_ARRAY_CHUNK - ${chunkTokens} tokens, ${
154
+ currentChunk.length
155
+ } items]\nKey: ${key}\nPath: ${path}[${i}...${
156
+ i + currentChunk.length - 1
157
+ }]\nPreview: ${chunkString.substring(0, 100)}...\n[Use ${
158
+ this.toolName
159
+ } tool with key "${key}" to retrieve this chunk]`;
160
+ finalArray.unshift(stub); // Add stub to the start of our final result.
161
+
162
+ currentChunk = [];
163
+ }
164
+ }
165
+
166
+ // Step 4: After the loop, add any remaining items from the start of the
167
+ // array that did not form a full chunk.
168
+ if (currentChunk.length > 0) {
169
+ finalArray.unshift(...currentChunk);
170
+ }
171
+ return finalArray;
172
+ }
173
+
174
+ // Handle objects - process all properties first (depth-first)
175
+ if (obj && typeof obj === "object") {
176
+ const result: any = {};
177
+ for (const [key, value] of Object.entries(obj)) {
178
+ const newPath = path ? `${path}.${key}` : key;
179
+ result[key] = this.compressJsonProperties(value, newPath);
180
+ }
181
+
182
+ // After processing children, check if the entire object should be compressed
183
+ const objectAsString = JSON.stringify(result);
184
+ const tokens = this.estimateTokens(objectAsString);
185
+ if (tokens > this.jsonPropertyThreshold) {
186
+ const key = this.generateKey();
187
+ this.storage[key] = objectAsString;
188
+
189
+ return `[COMPRESSED_JSON_OBJECT - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nKeys: ${Object.keys(
190
+ result
191
+ ).join(", ")}\nPreview: ${objectAsString.substring(0, 200)}...\n[Use ${
192
+ this.toolName
193
+ } tool with key "${key}" to retrieve full content]`;
194
+ }
195
+ return result;
196
+ }
197
+
198
+ // Handle primitive values (strings, numbers, booleans, null)
199
+ if (typeof obj === "string") {
200
+ // First, check if this string contains JSON that we can parse and compress more granularly
201
+ const parsedJson = this.tryParseJson(obj);
202
+ if (parsedJson) {
203
+ const compressedJson = this.compressJsonProperties(parsedJson, path);
204
+ const compressedJsonString = JSON.stringify(compressedJson, null, 2);
205
+
206
+ const originalTokens = this.estimateTokens(obj);
207
+ const compressedTokens = this.estimateTokens(compressedJsonString);
208
+
209
+ if (compressedTokens < originalTokens * 0.8) {
210
+ return compressedJsonString;
211
+ }
212
+ }
213
+
214
+ // If not JSON or compression wasn't effective, handle as regular string
215
+ const tokens = this.estimateTokens(obj);
216
+ if (tokens > this.characterLimit * 4) {
217
+ const key = this.generateKey();
218
+ this.storage[key] = obj;
219
+
220
+ return `[COMPRESSED_JSON_PROPERTY - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nPreview: ${obj.substring(
221
+ 0,
222
+ 200
223
+ )}...\n[Use ${
224
+ this.toolName
225
+ } tool with key "${key}" to retrieve full content]`;
226
+ }
227
+ return obj;
228
+ }
229
+
230
+ return obj;
231
+ }
232
+
233
+ private generateKey(): string {
234
+ return `${this.keyPrefix}${Date.now()}_${Math.random()
235
+ .toString(36)
236
+ .substr(2, 9)}`;
237
+ }
238
+
239
+ public compressToolCall(message: Message): void {
240
+ if (message.tool_calls) {
241
+ for (const toolCall of message.tool_calls) {
242
+ if (toolCall.function.arguments) {
243
+ const args = toolCall.function.arguments;
244
+ const tokens = this.estimateTokens(args);
245
+
246
+ if (tokens > this.maxTokens) {
247
+ const key = this.generateKey();
248
+ this.storage[key] = args;
249
+
250
+ const compressed = `[COMPRESSED TOOL ARGS - ${tokens} tokens]\nKey: ${key}\nPreview: ${args.substring(
251
+ 0,
252
+ 200
253
+ )}...\n[Use ${
254
+ this.toolName
255
+ } tool with key "${key}" to retrieve full arguments]`;
256
+
257
+ toolCall.function.arguments = compressed;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ public async compressMessage(message: Message) {
265
+ // The previous check for 'isDecompressionToolResponse' is no longer necessary.
266
+ // The new chunking strategy returns manageable chunks that won't meet the
267
+ // compression threshold, naturally preventing cycles.
268
+
269
+ // Compress content if it's a string
270
+ if (typeof message.content === "string") {
271
+ message.content = this.compressContent(message.content);
272
+ }
273
+ // Handle array content (multimodal)
274
+ else if (Array.isArray(message.content)) {
275
+ for (const item of message.content) {
276
+ if (item.type === "text" && item.text) {
277
+ item.text = this.compressContent(item.text);
278
+ }
279
+ }
280
+ }
281
+
282
+ // Compress tool calls
283
+ this.compressToolCall(message);
284
+ }
285
+
286
+ createProcessor(
287
+ filterFn?: (msg: Message) => boolean
288
+ ): MessageProcessorFunction {
289
+ return async (originalMessages: Message[], modifiedMessages: Message[]) => {
290
+ for (const message of modifiedMessages) {
291
+ if (filterFn && !filterFn(message)) {
292
+ continue;
293
+ }
294
+ await this.compressMessage(message);
295
+ }
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Retrieves a single chunk of stored data.
301
+ * If the data was chunked, the returned string will contain a `NEXT_CHUNK_KEY`
302
+ * that the agent can use to retrieve the subsequent part of the content.
303
+ */
304
+ retrieveString(key: string): string | null {
305
+ return this.storage[key] || null;
306
+ }
307
+
308
+ clearStorage(): void {
309
+ this.storage = {};
310
+ }
311
+
312
+ getStorageKeys(): string[] {
313
+ return Object.keys(this.storage);
314
+ }
315
+
316
+ getStorageSize(): number {
317
+ return Object.keys(this.storage).length;
318
+ }
319
+
320
+ registerTool(toolsService?: ToolsService): void {
321
+ if (toolsService && !toolsService.getTool(this.toolName)) {
322
+ toolsService.addTool(expandTokensDefinition);
323
+ toolsService.addFunctions({
324
+ [this.toolName]: (key: string) => {
325
+ const data = this.retrieveString(key);
326
+
327
+ if (!data) {
328
+ return `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
329
+ ", "
330
+ )}`;
331
+ }
332
+ return data;
333
+ },
334
+ });
335
+ }
336
+ }
337
+ }
338
+
339
+ export const expandTokensDefinition: Tool = {
340
+ type: "function",
341
+ function: {
342
+ name: "expandTokens",
343
+ description:
344
+ "Retrieve a chunk of compressed data that was stored during message processing. The returned content may contain a `NEXT_CHUNK_KEY` to retrieve subsequent chunks.",
345
+ parameters: {
346
+ type: "object",
347
+ positional: true,
348
+ properties: {
349
+ key: {
350
+ type: "string",
351
+ description: "The key of the compressed data to retrieve",
352
+ },
353
+ },
354
+ required: ["key"],
355
+ },
356
+ },
357
+ };
@@ -0,0 +1,235 @@
1
+ import { Message } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+ import { ToolsService } from "../services";
4
+ import { Tool } from "../clients";
5
+ import * as jq from "node-jq";
6
+
7
+ interface ToolResponseStorage {
8
+ [toolCallId: string]: string;
9
+ }
10
+
11
+ interface ToolResponseMetadata {
12
+ toolCallId: string;
13
+ originalLength: number;
14
+ storedAt: number;
15
+ }
16
+
17
+ interface ToolResponseMetadataStorage {
18
+ [toolCallId: string]: ToolResponseMetadata;
19
+ }
20
+
21
+ export class ToolResponseCache {
22
+ private storage: ToolResponseStorage = {};
23
+ private metadataStorage: ToolResponseMetadataStorage = {};
24
+ private toolName: string = jqToolResponseDefinition.function.name;
25
+
26
+ constructor(toolsService: ToolsService) {
27
+ this.registerTool(toolsService);
28
+ }
29
+
30
+ /**
31
+ * Attempts to parse content as JSON and returns parsed object if successful
32
+ */
33
+ private tryParseJson(content: string): any | null {
34
+ try {
35
+ return JSON.parse(content);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Recursively searches for JSON strings within an object and parses them
43
+ */
44
+ private parseNestedJsonStrings(obj: any): any {
45
+ if (typeof obj === "string") {
46
+ const parsed = this.tryParseJson(obj);
47
+ if (parsed) {
48
+ return this.parseNestedJsonStrings(parsed);
49
+ }
50
+ return obj;
51
+ }
52
+
53
+ if (Array.isArray(obj)) {
54
+ return obj.map((item) => this.parseNestedJsonStrings(item));
55
+ }
56
+
57
+ if (obj && typeof obj === "object") {
58
+ const result: any = {};
59
+ for (const [key, value] of Object.entries(obj)) {
60
+ result[key] = this.parseNestedJsonStrings(value);
61
+ }
62
+ return result;
63
+ }
64
+
65
+ return obj;
66
+ }
67
+
68
+ /**
69
+ * Stores a tool response for later manipulation
70
+ */
71
+ private storeToolResponse(content: string, toolCallId: string): void {
72
+ // Always store the original content for later JQ manipulation
73
+ this.storage[toolCallId] = content;
74
+
75
+ // Store metadata for reference
76
+ this.metadataStorage[toolCallId] = {
77
+ toolCallId,
78
+ originalLength: content.length,
79
+ storedAt: Date.now(),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Processes messages to store tool responses silently
85
+ */
86
+ private async processMessage(message: Message): Promise<void> {
87
+ // Only process tool response messages
88
+ if (
89
+ message.role !== "tool" ||
90
+ !message.tool_call_id ||
91
+ typeof message.content !== "string"
92
+ ) {
93
+ return;
94
+ }
95
+
96
+ // Store the tool response silently without modifying the message
97
+ this.storeToolResponse(message.content, message.tool_call_id);
98
+ }
99
+
100
+ /**
101
+ * Creates a message processor function that stores tool responses silently
102
+ */
103
+ createProcessor(
104
+ filterFn?: (msg: Message) => boolean
105
+ ): MessageProcessorFunction {
106
+ return async (originalMessages: Message[], modifiedMessages: Message[]) => {
107
+ for (const message of modifiedMessages) {
108
+ if (filterFn && !filterFn(message)) {
109
+ continue;
110
+ }
111
+ await this.processMessage(message);
112
+ }
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Retrieves and processes tool response data with JQ query
118
+ */
119
+ async queryToolResponse(
120
+ toolCallId: string,
121
+ jqQuery: string
122
+ ): Promise<string> {
123
+ const data = this.storage[toolCallId];
124
+
125
+ if (!data) {
126
+ const availableIds = Object.keys(this.storage);
127
+ return `Error: No tool response found for toolCallId "${toolCallId}". Available IDs: ${availableIds.join(
128
+ ", "
129
+ )}`;
130
+ }
131
+
132
+ try {
133
+ // Parse the data as JSON (handles nested JSON strings)
134
+ const parsedData = this.parseNestedJsonStrings(data);
135
+
136
+ // Execute JQ query
137
+ const result = await jq.run(jqQuery, parsedData, { input: "json" });
138
+
139
+ // Return the result as a string
140
+ if (typeof result === "string") {
141
+ return result;
142
+ } else {
143
+ return JSON.stringify(result, null, 2);
144
+ }
145
+ } catch (error: any) {
146
+ // If JQ fails, try to provide helpful error message
147
+ let errorMessage = `JQ Query Error: ${error.message}`;
148
+
149
+ // Try to parse as JSON to see if it's valid
150
+ const jsonObj = this.tryParseJson(data);
151
+ if (!jsonObj) {
152
+ errorMessage += `\nNote: The tool response data is not valid JSON. Raw data preview:\n${data.substring(
153
+ 0,
154
+ 300
155
+ )}...`;
156
+ } else {
157
+ errorMessage += `\nData structure preview:\n${JSON.stringify(
158
+ jsonObj,
159
+ null,
160
+ 2
161
+ ).substring(0, 500)}...`;
162
+ }
163
+
164
+ return errorMessage;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Retrieves the raw tool response data
170
+ */
171
+ retrieveRawResponse(toolCallId: string): string | null {
172
+ return this.storage[toolCallId] || null;
173
+ }
174
+
175
+ /**
176
+ * Clears all stored tool responses
177
+ */
178
+ clearStorage(): void {
179
+ this.storage = {};
180
+ this.metadataStorage = {};
181
+ }
182
+
183
+ /**
184
+ * Gets all stored tool call IDs
185
+ */
186
+ getStorageKeys(): string[] {
187
+ return Object.keys(this.storage);
188
+ }
189
+
190
+ /**
191
+ * Gets the number of stored tool responses
192
+ */
193
+ getStorageSize(): number {
194
+ return Object.keys(this.storage).length;
195
+ }
196
+
197
+ /**
198
+ * Registers the jqToolResponse tool with the ToolsService
199
+ */
200
+ registerTool(toolsService: ToolsService): void {
201
+ if (!toolsService.getTool(this.toolName)) {
202
+ toolsService.addTool(jqToolResponseDefinition);
203
+ toolsService.addFunctions({
204
+ [this.toolName]: async (toolCallId: string, jqQuery: string) => {
205
+ return await this.queryToolResponse(toolCallId, jqQuery);
206
+ },
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ export const jqToolResponseDefinition: Tool = {
213
+ type: "function",
214
+ function: {
215
+ name: "jqToolResponse",
216
+ description:
217
+ "Execute a JQ query on a stored tool response to extract specific data. Use this when you need to extract specific information from any tool response that has been stored. Many MCP tool responses store data in nested structures like .content[0].text where the actual data array is located.",
218
+ parameters: {
219
+ type: "object",
220
+ positional: true,
221
+ properties: {
222
+ toolCallId: {
223
+ type: "string",
224
+ description: "The toolCallId of the stored tool response",
225
+ },
226
+ jqQuery: {
227
+ type: "string",
228
+ description:
229
+ "The JQ query to execute on the tool response data. Examples: '.content[0].text | map(.title)' (extract titles from MCP array), '.content[0].text | map(select(.createdAt > \"2025-01-01\"))' (filter MCP items by date) ",
230
+ },
231
+ },
232
+ required: ["toolCallId", "jqQuery"],
233
+ },
234
+ },
235
+ };
@@ -261,7 +261,10 @@ export class McpService {
261
261
  }
262
262
 
263
263
  toOpenAiTool(index: number, tool: McpTool) {
264
- const mcpName = this.config[index].name;
264
+ const mcpName = this.config[index]?.name
265
+ ?.toLowerCase()
266
+ ?.replaceAll(" ", "_");
267
+
265
268
  const prefix = mcpName
266
269
  ? `${this.mcpPrefix}_${index}_${mcpName}`
267
270
  : `${this.mcpPrefix}_${index}`;
@@ -0,0 +1,107 @@
1
+ import { Message } from "../clients/types";
2
+
3
+ export type ProcessorLifecycle = "initial_call" | "per_call" | "post_call";
4
+
5
+ export type MessageProcessorFunction = (
6
+ originalMessages: Message[],
7
+ modifiedMessages: Message[]
8
+ ) => Promise<void> | void;
9
+
10
+ export interface ProcessorRegistration {
11
+ processor: MessageProcessorFunction;
12
+ priority: number;
13
+ }
14
+
15
+ export class MessageProcessor {
16
+ private processors: Map<ProcessorLifecycle, ProcessorRegistration[]> =
17
+ new Map();
18
+
19
+ constructor() {
20
+ // Initialize lifecycle maps
21
+ this.processors.set("initial_call", []);
22
+ this.processors.set("per_call", []);
23
+ this.processors.set("post_call", []);
24
+ }
25
+
26
+ setProcessors(
27
+ lifecycle: ProcessorLifecycle,
28
+ processors: MessageProcessorFunction[]
29
+ ): void {
30
+ const registrations: ProcessorRegistration[] = processors.map((proc) => ({
31
+ processor: proc,
32
+ priority: 0, // Default priority
33
+ }));
34
+
35
+ // Sort by priority (higher priority first)
36
+ registrations.sort((a, b) => b.priority - a.priority);
37
+
38
+ this.processors.set(lifecycle, registrations);
39
+ }
40
+
41
+ registerProcessor(
42
+ lifecycle: ProcessorLifecycle,
43
+ processor: MessageProcessorFunction,
44
+ priority: number = 0
45
+ ): void {
46
+ const registrations = this.processors.get(lifecycle) || [];
47
+ registrations.push({ processor, priority });
48
+
49
+ // Sort by priority (higher priority first)
50
+ registrations.sort((a, b) => b.priority - a.priority);
51
+
52
+ this.processors.set(lifecycle, registrations);
53
+ }
54
+
55
+ removeProcessor(
56
+ lifecycle: ProcessorLifecycle,
57
+ processor: MessageProcessorFunction
58
+ ): void {
59
+ const registrations = this.processors.get(lifecycle) || [];
60
+ const filtered = registrations.filter((reg) => reg.processor !== processor);
61
+ this.processors.set(lifecycle, filtered);
62
+ }
63
+
64
+ async processMessages(
65
+ messages: Message[],
66
+ lifecycle: ProcessorLifecycle
67
+ ): Promise<Message[]> {
68
+ const registrations = this.processors.get(lifecycle) || [];
69
+
70
+ if (registrations.length === 0) {
71
+ return messages;
72
+ }
73
+
74
+ // Create a deep copy of the messages to avoid modifying the original
75
+ const modifiedMessages = JSON.parse(JSON.stringify(messages));
76
+
77
+ // Execute processors in priority order
78
+ for (const registration of registrations) {
79
+ try {
80
+ await registration.processor(messages, modifiedMessages);
81
+ } catch (error) {
82
+ console.error(`Message processor error in ${lifecycle}:`, error);
83
+ // Continue with other processors even if one fails
84
+ }
85
+ }
86
+
87
+ return modifiedMessages;
88
+ }
89
+
90
+ getProcessorsForLifecycle(
91
+ lifecycle: ProcessorLifecycle
92
+ ): MessageProcessorFunction[] {
93
+ const registrations = this.processors.get(lifecycle) || [];
94
+ return registrations.map((reg) => reg.processor);
95
+ }
96
+
97
+ clearProcessors(lifecycle?: ProcessorLifecycle): void {
98
+ if (lifecycle) {
99
+ this.processors.set(lifecycle, []);
100
+ } else {
101
+ this.processors.clear();
102
+ this.processors.set("initial_call", []);
103
+ this.processors.set("per_call", []);
104
+ this.processors.set("post_call", []);
105
+ }
106
+ }
107
+ }