@tyvm/knowhow 0.0.16 → 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.
- package/package.json +2 -1
- package/src/agents/base/base.ts +25 -6
- package/src/agents/index.ts +2 -2
- package/src/agents/tools/agentCall.ts +0 -1
- package/src/agents/tools/execCommand.ts +95 -4
- package/src/agents/tools/list.ts +23 -19
- package/src/agents/tools/writeFile.ts +1 -1
- package/src/chat.ts +11 -0
- package/src/config.ts +3 -1
- package/src/processors/Base64ImageDetector.ts +190 -0
- package/src/processors/TokenCompressor.ts +357 -0
- package/src/processors/ToolResponseCache.ts +235 -0
- package/src/services/MessageProcessor.ts +107 -0
- package/src/services/Tools.ts +100 -1
- package/src/services/types.ts +57 -0
- package/ts_build/src/agents/base/base.d.ts +3 -1
- package/ts_build/src/agents/base/base.js +20 -5
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/index.d.ts +2 -2
- package/ts_build/src/agents/index.js +5 -3
- package/ts_build/src/agents/index.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +6 -1
- package/ts_build/src/agents/tools/execCommand.js +70 -4
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/expandTokens.d.ts +3 -0
- package/ts_build/src/agents/tools/expandTokens.js +33 -0
- package/ts_build/src/agents/tools/expandTokens.js.map +1 -0
- package/ts_build/src/agents/tools/getBigString.d.ts +3 -0
- package/ts_build/src/agents/tools/getBigString.js +33 -0
- package/ts_build/src/agents/tools/getBigString.js.map +1 -0
- package/ts_build/src/agents/tools/list.js +19 -17
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/writeFile.js +1 -1
- package/ts_build/src/agents/tools/writeFile.js.map +1 -1
- package/ts_build/src/chat.js +6 -0
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/config.js +1 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.d.ts +14 -0
- package/ts_build/src/processors/Base64ImageDetector.js +153 -0
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -0
- package/ts_build/src/processors/TokenCompressor.d.ts +28 -0
- package/ts_build/src/processors/TokenCompressor.js +226 -0
- package/ts_build/src/processors/TokenCompressor.js.map +1 -0
- package/ts_build/src/processors/ToolResponseCache.d.ts +22 -0
- package/ts_build/src/processors/ToolResponseCache.js +164 -0
- package/ts_build/src/processors/ToolResponseCache.js.map +1 -0
- package/ts_build/src/processors/ToolResponseManipulator.d.ts +22 -0
- package/ts_build/src/processors/ToolResponseManipulator.js +162 -0
- package/ts_build/src/processors/ToolResponseManipulator.js.map +1 -0
- package/ts_build/src/services/MessageProcessor.d.ts +17 -0
- package/ts_build/src/services/MessageProcessor.js +63 -0
- package/ts_build/src/services/MessageProcessor.js.map +1 -0
- package/ts_build/src/services/Tools.d.ts +12 -0
- package/ts_build/src/services/Tools.js +71 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/types.d.ts +32 -0
- package/ts_build/src/services/types.js +38 -0
- package/ts_build/src/services/types.js.map +1 -0
- package/ts_build/src/agents/configurable/OpenAIAgent.d.ts +0 -0
- package/ts_build/src/agents/configurable/OpenAIAgent.js +0 -1
- package/ts_build/src/agents/configurable/OpenAIAgent.js.map +0 -1
- package/ts_build/src/agents/tools/client.d.ts +0 -5
- package/ts_build/src/agents/tools/client.js +0 -21
- package/ts_build/src/agents/tools/client.js.map +0 -1
- package/ts_build/src/agents/tools/googleSearchTypes.d.ts +0 -74
- package/ts_build/src/agents/tools/googleSearchTypes.js +0 -3
- package/ts_build/src/agents/tools/googleSearchTypes.js.map +0 -1
- package/ts_build/src/commands/chat-ui.d.ts +0 -1
- package/ts_build/src/commands/chat-ui.js +0 -14
- package/ts_build/src/commands/chat-ui.js.map +0 -1
- package/ts_build/src/demo/chat-ui-demo.d.ts +0 -3
- package/ts_build/src/demo/chat-ui-demo.js +0 -20
- package/ts_build/src/demo/chat-ui-demo.js.map +0 -1
- package/ts_build/src/plugins/EmbeddingPluginV2.d.ts +0 -7
- package/ts_build/src/plugins/EmbeddingPluginV2.js +0 -41
- package/ts_build/src/plugins/EmbeddingPluginV2.js.map +0 -1
- package/ts_build/src/plugins/GitHubPluginV2.d.ts +0 -10
- package/ts_build/src/plugins/GitHubPluginV2.js +0 -57
- package/ts_build/src/plugins/GitHubPluginV2.js.map +0 -1
- package/ts_build/src/plugins/downloader/index.d.ts +0 -3
- package/ts_build/src/plugins/downloader/index.js +0 -41
- package/ts_build/src/plugins/downloader/index.js.map +0 -1
- package/ts_build/src/services/MessagePreprocessor.d.ts +0 -26
- package/ts_build/src/services/MessagePreprocessor.js +0 -190
- package/ts_build/src/services/MessagePreprocessor.js.map +0 -1
- package/ts_build/src/services/__tests__/MessagePreprocessor.test.d.ts +0 -1
- package/ts_build/src/services/__tests__/MessagePreprocessor.test.js +0 -117
- package/ts_build/src/services/__tests__/MessagePreprocessor.test.js.map +0 -1
- package/ts_build/src/terminal.d.ts +0 -1
- package/ts_build/src/terminal.js +0 -35
- package/ts_build/src/terminal.js.map +0 -1
- package/ts_build/src/ui/InkChatUI.d.ts +0 -1
- package/ts_build/src/ui/InkChatUI.js +0 -792
- package/ts_build/src/ui/InkChatUI.js.map +0 -1
- package/ts_build/src/ui/components/ChatInterface.d.ts +0 -15
- package/ts_build/src/ui/components/ChatInterface.js +0 -39
- package/ts_build/src/ui/components/ChatInterface.js.map +0 -1
- package/ts_build/src/ui/components/ChatMessage.d.ts +0 -8
- package/ts_build/src/ui/components/ChatMessage.js +0 -7
- package/ts_build/src/ui/components/ChatMessage.js.map +0 -1
- package/ts_build/src/ui/components/CommandPalette.d.ts +0 -8
- package/ts_build/src/ui/components/CommandPalette.js +0 -23
- package/ts_build/src/ui/components/CommandPalette.js.map +0 -1
- package/ts_build/src/ui/components/InputBar.d.ts +0 -8
- package/ts_build/src/ui/components/InputBar.js +0 -8
- package/ts_build/src/ui/components/InputBar.js.map +0 -1
- package/ts_build/src/ui/components/Sidebar.d.ts +0 -9
- package/ts_build/src/ui/components/Sidebar.js +0 -7
- package/ts_build/src/ui/components/Sidebar.js.map +0 -1
- package/ts_build/src/ui/components/StatusBar.d.ts +0 -10
- package/ts_build/src/ui/components/StatusBar.js +0 -8
- package/ts_build/src/ui/components/StatusBar.js.map +0 -1
- package/ts_build/src/ui/demo.d.ts +0 -3
- package/ts_build/src/ui/demo.js +0 -26
- package/ts_build/src/ui/demo.js.map +0 -1
- package/ts_build/src/ui/index.d.ts +0 -13
- package/ts_build/src/ui/index.js +0 -16
- package/ts_build/src/ui/index.js.map +0 -1
- package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.d.ts +0 -1
- package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js +0 -148
- package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js.map +0 -1
- package/ts_build/tests/services/MessagePreprocessor.test.d.ts +0 -1
- package/ts_build/tests/services/MessagePreprocessor.test.js +0 -117
- 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
|
+
};
|
|
@@ -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
|
+
}
|