@tyvm/knowhow 0.0.61 → 0.0.63

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/chat/modules/AgentModule.ts +6 -6
  3. package/src/processors/JsonCompressor.ts +496 -0
  4. package/src/processors/TokenCompressor.ts +194 -125
  5. package/src/processors/ToolResponseCache.ts +64 -11
  6. package/src/processors/index.ts +1 -0
  7. package/tests/compressor/bigstring.test.ts +352 -2
  8. package/tests/compressor/githubjson.txt +1 -0
  9. package/tests/compressor/toolResponseCache.test.ts +303 -0
  10. package/ts_build/package.json +1 -1
  11. package/ts_build/src/chat/modules/AgentModule.js +5 -4
  12. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  13. package/ts_build/src/processors/JsonCompressor.d.ts +36 -0
  14. package/ts_build/src/processors/JsonCompressor.js +295 -0
  15. package/ts_build/src/processors/JsonCompressor.js.map +1 -0
  16. package/ts_build/src/processors/TokenCompressor.d.ts +23 -5
  17. package/ts_build/src/processors/TokenCompressor.js +106 -70
  18. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  19. package/ts_build/src/processors/ToolResponseCache.d.ts +4 -2
  20. package/ts_build/src/processors/ToolResponseCache.js +50 -10
  21. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  22. package/ts_build/src/processors/index.d.ts +1 -0
  23. package/ts_build/src/processors/index.js +3 -1
  24. package/ts_build/src/processors/index.js.map +1 -1
  25. package/ts_build/tests/compressor/bigstring.test.js +209 -0
  26. package/ts_build/tests/compressor/bigstring.test.js.map +1 -1
  27. package/ts_build/tests/compressor/toolResponseCache.test.d.ts +1 -0
  28. package/ts_build/tests/compressor/toolResponseCache.test.js +240 -0
  29. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -0
@@ -1,12 +1,29 @@
1
1
  import { Message, Tool } from "../clients/types";
2
2
  import { MessageProcessorFunction } from "../services/MessageProcessor";
3
3
  import { ToolsService } from "../services";
4
+ import {
5
+ JsonCompressor,
6
+ JsonSchema,
7
+ CompressionMetadata,
8
+ JsonCompressorStorage,
9
+ } from "./JsonCompressor";
10
+
11
+ export interface KeyInfo {
12
+ key: string;
13
+ size: number;
14
+ preview: string;
15
+ tokens?: number;
16
+ type?: string;
17
+ depth?: number;
18
+ childKeys?: string[];
19
+ nextChunkKey?: string;
20
+ }
4
21
 
5
22
  interface TokenCompressorStorage {
6
23
  [key: string]: string;
7
24
  }
8
25
 
9
- export class TokenCompressor {
26
+ export class TokenCompressor implements JsonCompressorStorage {
10
27
  private storage: TokenCompressorStorage = {};
11
28
  private keyPrefix: string = "compressed_";
12
29
  private toolName: string = expandTokensDefinition.function.name;
@@ -16,38 +33,47 @@ export class TokenCompressor {
16
33
  private characterLimit: number = this.compressionThreshold * 4;
17
34
 
18
35
  // Largest size retrievable without re-compressing
19
- private maxTokens: number = this.compressionThreshold * 2;
36
+ public maxTokens: number = this.compressionThreshold * 2;
37
+
38
+ // JSON compression handler
39
+ private jsonCompressor: JsonCompressor;
20
40
 
21
41
  constructor(toolsService?: ToolsService) {
42
+ this.jsonCompressor = new JsonCompressor(
43
+ this,
44
+ this.compressionThreshold,
45
+ this.maxTokens,
46
+ this.toolName
47
+ );
22
48
  this.registerTool(toolsService);
23
49
  }
24
50
 
25
51
  // Rough token estimation (4 chars per token average)
26
- private estimateTokens(text: string): number {
52
+ public estimateTokens(text: string): number {
27
53
  return Math.ceil(text.length / 4);
28
54
  }
29
55
 
30
56
  public setCompressionThreshold(threshold: number): void {
31
57
  this.compressionThreshold = threshold;
32
58
  this.characterLimit = threshold * 4; // Update character limit based on new threshold
59
+ this.jsonCompressor.updateSettings(threshold, this.maxTokens);
33
60
  }
34
61
 
35
62
  // Internally adjust to ensure we can always retrieve data
36
63
  private setMaxTokens(maxTokens: number): void {
37
64
  if (maxTokens > this.maxTokens) {
38
65
  this.maxTokens = maxTokens;
66
+ this.jsonCompressor.updateSettings(this.compressionThreshold, maxTokens);
39
67
  }
40
68
  }
41
69
 
42
70
  /**
43
- * Attempts to parse content as JSON and returns parsed object if successful
71
+ * Attempts to parse content as JSON and returns parsed object if successful.
72
+ * Also handles MCP tool response format where actual data is in content[0].text
44
73
  */
45
- private tryParseJson(content: string): any | null {
46
- try {
47
- return JSON.parse(content);
48
- } catch {
49
- return null;
50
- }
74
+
75
+ public tryParseJson(content: string): any | null {
76
+ return this.jsonCompressor.tryParseJson(content);
51
77
  }
52
78
 
53
79
  /**
@@ -108,10 +134,33 @@ export class TokenCompressor {
108
134
  } tool with key "${firstKey}" to retrieve content. Follow NEXT_CHUNK_KEY references for complete content]`;
109
135
  }
110
136
 
137
+ /**
138
+ * Check if content is already compressed
139
+ */
140
+ private isAlreadyCompressed(content: string): boolean {
141
+ // Check for compressed string markers
142
+ if (content.includes("[COMPRESSED_STRING")) {
143
+ return true;
144
+ }
145
+
146
+ // Check for compressed JSON structure with schema key
147
+ const parsed = this.tryParseJson(content);
148
+ if (parsed && parsed._schema_key && typeof parsed._schema_key === "string") {
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
111
155
  /**
112
156
  * Enhanced content compression that handles both JSON and string chunking
113
157
  */
114
158
  public compressContent(content: string, path: string = ""): string {
159
+ // Check if already compressed - don't compress again
160
+ if (this.isAlreadyCompressed(content)) {
161
+ return content;
162
+ }
163
+
115
164
  const tokens = this.estimateTokens(content);
116
165
 
117
166
  // For nested properties (path !== ""), use maxTokens to avoid recompressing stored data
@@ -123,16 +172,50 @@ export class TokenCompressor {
123
172
  return content;
124
173
  }
125
174
 
126
- // Try to parse as JSON first
175
+ // Try to parse as JSON and generate schema
127
176
  const jsonObj = this.tryParseJson(content);
128
177
  if (jsonObj) {
178
+ // For MCP format, work with the actual data
179
+ const dataToCompress = jsonObj._mcp_format ? jsonObj._data : jsonObj;
180
+
181
+ // Generate and store schema
182
+ const schema = this.jsonCompressor.generateSchema(jsonObj);
183
+ const schemaKey = this.generateKey();
184
+ this.storeString(schemaKey, JSON.stringify(schema));
185
+
129
186
  // For JSON objects, compress individual properties
130
- const compressedObj = this.compressJsonProperties(jsonObj, path);
131
- const compressedContent = JSON.stringify(compressedObj, null, 2);
187
+ // Use a non-empty path to ensure compression logic is applied
188
+ const compressedObj = this.compressJsonProperties(
189
+ dataToCompress,
190
+ path || "data"
191
+ );
192
+
193
+ // If this was MCP format, wrap the result back
194
+ const finalCompressedObj = jsonObj._mcp_format
195
+ ? {
196
+ _mcp_format: true,
197
+ _raw_structure: jsonObj._raw_structure,
198
+ _data: compressedObj,
199
+ }
200
+ : compressedObj;
132
201
 
133
- // If compression reduced size significantly, return compressed version
202
+ // Add schema reference to the compressed result
203
+ const resultWithSchema =
204
+ typeof finalCompressedObj === "object" &&
205
+ !Array.isArray(finalCompressedObj)
206
+ ? { ...finalCompressedObj, _schema_key: schemaKey }
207
+ : { _schema_key: schemaKey, data: finalCompressedObj };
208
+ const compressedContent = JSON.stringify(resultWithSchema, null, 2);
209
+
210
+ // Check compression effectiveness
134
211
  const compressedTokens = this.estimateTokens(compressedContent);
135
- if (compressedTokens < tokens * 0.8) {
212
+
213
+ // For MCP format, we've successfully extracted and compressed the data
214
+ // The wrapper overhead is acceptable because we provide schema + structured access
215
+ // For non-MCP format, use the standard 60% threshold
216
+ const compressionThreshold = 0.6;
217
+
218
+ if (compressedTokens < tokens * compressionThreshold) {
136
219
  return compressedContent;
137
220
  }
138
221
  }
@@ -146,118 +229,10 @@ export class TokenCompressor {
146
229
  * Implements an efficient backward-iterating chunking strategy for large arrays.
147
230
  */
148
231
  public compressJsonProperties(obj: any, path: string = ""): any {
149
- if (
150
- path === "" &&
151
- this.estimateTokens(JSON.stringify(obj)) <= this.maxTokens
152
- ) {
153
- return obj;
154
- }
155
-
156
- if (Array.isArray(obj)) {
157
- // Step 1: Recursively compress all items first (depth-first).
158
- const processedItems = obj.map((item, index) =>
159
- this.compressJsonProperties(item, `${path}[${index}]`)
160
- );
161
-
162
- // Step 2: Early exit if the whole array is already small enough.
163
- // maxTokens allows us to fetch objects from the store without recompressing
164
-
165
- // Step 3: Iterate backwards, building chunks from the end.
166
- const finalArray: any[] = [];
167
- let currentChunk: any[] = [];
168
-
169
- for (let i = processedItems.length - 1; i >= 0; i--) {
170
- const item = processedItems[i];
171
- currentChunk.unshift(item); // Add item to the front of the current chunk
172
-
173
- const chunkString = JSON.stringify(currentChunk);
174
- const chunkTokens = this.estimateTokens(chunkString);
175
-
176
- if (chunkTokens > this.compressionThreshold) {
177
- const key = this.generateKey();
178
- this.storeString(key, chunkString);
179
-
180
- const stub = `[COMPRESSED_JSON_ARRAY_CHUNK - ${chunkTokens} tokens, ${
181
- currentChunk.length
182
- } items]\nKey: ${key}\nPath: ${path}[${i}...${
183
- i + currentChunk.length - 1
184
- }]\nPreview: ${chunkString.substring(0, 100)}...\n[Use ${
185
- this.toolName
186
- } tool with key "${key}" to retrieve this chunk]`;
187
- finalArray.unshift(stub); // Add stub to the start of our final result.
188
-
189
- currentChunk = [];
190
- }
191
- }
192
-
193
- // Step 4: After the loop, add any remaining items from the start of the
194
- // array that did not form a full chunk.
195
- if (currentChunk.length > 0) {
196
- finalArray.unshift(...currentChunk);
197
- }
198
- return finalArray;
199
- }
200
-
201
- // Handle objects - process all properties first (depth-first)
202
- if (obj && typeof obj === "object") {
203
- const result: any = {};
204
- for (const [key, value] of Object.entries(obj)) {
205
- const newPath = path ? `${path}.${key}` : key;
206
- result[key] = this.compressJsonProperties(value, newPath);
207
- }
208
-
209
- // After processing children, check if the entire object should be compressed
210
- const objectAsString = JSON.stringify(result);
211
- const tokens = this.estimateTokens(objectAsString);
212
- if (tokens > this.compressionThreshold) {
213
- const key = this.generateKey();
214
- this.storeString(key, objectAsString);
215
-
216
- return `[COMPRESSED_JSON_OBJECT - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nKeys: ${Object.keys(
217
- result
218
- ).join(", ")}\nPreview: ${objectAsString.substring(0, 200)}...\n[Use ${
219
- this.toolName
220
- } tool with key "${key}" to retrieve full content]`;
221
- }
222
- return result;
223
- }
224
-
225
- // Handle primitive values (strings, numbers, booleans, null)
226
- if (typeof obj === "string") {
227
- // First, check if this string contains JSON that we can parse and compress more granularly
228
- const parsedJson = this.tryParseJson(obj);
229
- if (parsedJson) {
230
- const compressedJson = this.compressJsonProperties(parsedJson, path);
231
- const compressedJsonString = JSON.stringify(compressedJson, null, 2);
232
-
233
- const originalTokens = this.estimateTokens(obj);
234
- const compressedTokens = this.estimateTokens(compressedJsonString);
235
-
236
- if (compressedTokens < originalTokens * 0.8) {
237
- return compressedJsonString;
238
- }
239
- }
240
-
241
- // If not JSON or compression wasn't effective, handle as regular string
242
- const tokens = this.estimateTokens(obj);
243
- if (tokens > this.compressionThreshold) {
244
- const key = this.generateKey();
245
- this.storeString(key, obj);
246
-
247
- return `[COMPRESSED_JSON_PROPERTY - ${tokens} tokens]\nKey: ${key}\nPath: ${path}\nPreview: ${obj.substring(
248
- 0,
249
- 200
250
- )}...\n[Use ${
251
- this.toolName
252
- } tool with key "${key}" to retrieve full content]`;
253
- }
254
- return obj;
255
- }
256
-
257
- return obj;
232
+ return this.jsonCompressor.compressJsonProperties(obj, path);
258
233
  }
259
234
 
260
- private generateKey(): string {
235
+ public generateKey(): string {
261
236
  return `${this.keyPrefix}${Date.now()}_${Math.random()
262
237
  .toString(36)
263
238
  .substr(2, 9)}`;
@@ -310,6 +285,7 @@ export class TokenCompressor {
310
285
 
311
286
  clearStorage(): void {
312
287
  this.storage = {};
288
+ this.jsonCompressor.clearDeduplication();
313
289
  }
314
290
 
315
291
  getStorageKeys(): string[] {
@@ -337,6 +313,99 @@ export class TokenCompressor {
337
313
  });
338
314
  }
339
315
  }
316
+
317
+ /**
318
+ * Get the schema for a compressed object
319
+ */
320
+ getSchema(key: string): JsonSchema | null {
321
+ const schemaKey = `${key}_schema`;
322
+ const schemaStr = this.storage[schemaKey];
323
+ if (!schemaStr) {
324
+ return null;
325
+ }
326
+ try {
327
+ return JSON.parse(schemaStr);
328
+ } catch (e) {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Get compressed properties for an object
335
+ */
336
+ getCompressedProperties(key: string): any | null {
337
+ const content = this.storage[key];
338
+ if (!content) {
339
+ return null;
340
+ }
341
+ try {
342
+ const metadata = JSON.parse(content) as CompressionMetadata;
343
+ return metadata.compressed_properties || null;
344
+ } catch (e) {
345
+ return null;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Get full object by merging high-signal and compressed properties
351
+ */
352
+ getFullObject(mainObj: any, compressedKey: string): any {
353
+ if (!mainObj || typeof mainObj !== "object") {
354
+ return mainObj;
355
+ }
356
+
357
+ const compressed = this.getCompressedProperties(compressedKey);
358
+ if (!compressed) {
359
+ return mainObj;
360
+ }
361
+
362
+ const {
363
+ _compressed_properties_key,
364
+ _compressed_property_names,
365
+ _compression_info,
366
+ ...highSignal
367
+ } = mainObj;
368
+ return { ...highSignal, ...compressed };
369
+ }
370
+
371
+ /**
372
+ * Extract all keys from compressed content
373
+ */
374
+ extractKeys(content: string): string[] {
375
+ const keys: string[] = [];
376
+ const keyPattern = /\$expandTokens\[([^\]]+)\]|Key:\s*([^\s\n]+)/g;
377
+ let match;
378
+ while ((match = keyPattern.exec(content)) !== null) {
379
+ const key = match[1] || match[2];
380
+ if (key && !keys.includes(key)) {
381
+ keys.push(key);
382
+ }
383
+ }
384
+ return keys;
385
+ }
386
+
387
+ /**
388
+ * Get the chain of keys for a given key (following NEXT_CHUNK_KEY references)
389
+ */
390
+ getKeyChain(key: string): KeyInfo[] {
391
+ const chain: KeyInfo[] = [];
392
+ let currentKey: string | null = key;
393
+
394
+ while (currentKey) {
395
+ const content = this.storage[currentKey];
396
+ if (!content) break;
397
+
398
+ chain.push({
399
+ key: currentKey,
400
+ size: content.length,
401
+ preview: content.substring(0, 100),
402
+ });
403
+
404
+ const nextMatch = content.match(/NEXT_CHUNK_KEY:\s*([^\s\n]+)/);
405
+ currentKey = nextMatch ? nextMatch[1] : null;
406
+ }
407
+ return chain;
408
+ }
340
409
  }
341
410
 
342
411
  export const expandTokensDefinition: Tool = {
@@ -1,6 +1,7 @@
1
1
  import { Message } from "../clients/types";
2
2
  import { MessageProcessorFunction } from "../services/MessageProcessor";
3
3
  import { ToolsService } from "../services";
4
+ import { JsonCompressor } from "./JsonCompressor";
4
5
  import {
5
6
  jqToolResponseDefinition,
6
7
  executeJqQuery,
@@ -32,20 +33,34 @@ export class ToolResponseCache {
32
33
  private storage: ToolResponseStorage = {};
33
34
  private metadataStorage: ToolResponseMetadataStorage = {};
34
35
  private toolNameMap: { [toolCallId: string]: string } = {};
36
+ private jsonCompressor: JsonCompressor;
35
37
 
36
- constructor(toolsService: ToolsService) {
38
+ constructor(toolsService: ToolsService, jsonCompressor?: JsonCompressor) {
39
+ // Use provided JsonCompressor or create a minimal storage adapter
40
+ this.jsonCompressor = jsonCompressor || this.createMinimalJsonCompressor();
37
41
  this.registerTool(toolsService);
38
42
  }
39
43
 
40
44
  /**
41
- * Attempts to parse content as JSON and returns parsed object if successful
45
+ * Creates a minimal JsonCompressor instance for JSON parsing utilities
46
+ * This is used when no JsonCompressor is provided to the constructor
42
47
  */
43
- private tryParseJson(content: string): any | null {
44
- try {
45
- return JSON.parse(content);
46
- } catch {
47
- return null;
48
- }
48
+ private createMinimalJsonCompressor(): JsonCompressor {
49
+ // Create a minimal storage adapter that satisfies JsonCompressorStorage interface
50
+ const minimalStorage = {
51
+ storeString: (key: string, value: string) => {
52
+ // No-op for ToolResponseCache's internal use
53
+ },
54
+ generateKey: () => {
55
+ return `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
56
+ },
57
+ estimateTokens: (text: string) => {
58
+ return Math.ceil(text.length / 4);
59
+ },
60
+ };
61
+
62
+ // Return a JsonCompressor instance with minimal settings
63
+ return new JsonCompressor(minimalStorage, 4000, 8000, "expandTokens");
49
64
  }
50
65
 
51
66
  /**
@@ -53,7 +68,7 @@ export class ToolResponseCache {
53
68
  */
54
69
  public parseNestedJsonStrings(obj: any): any {
55
70
  if (typeof obj === "string") {
56
- const parsed = this.tryParseJson(obj);
71
+ const parsed = this.jsonCompressor.tryParseJson(obj);
57
72
  if (parsed) {
58
73
  return this.parseNestedJsonStrings(parsed);
59
74
  }
@@ -90,8 +105,46 @@ export class ToolResponseCache {
90
105
  return;
91
106
  }
92
107
 
93
- // Store the original content for later JQ/grep manipulation
94
- this.storage[toolCallId] = content;
108
+ // Try to parse the content
109
+ const parsed = this.jsonCompressor.tryParseJson(content);
110
+
111
+ if (parsed && typeof parsed === 'object' && parsed._mcp_format === true && parsed._data) {
112
+ // For MCP format responses, store the data in a normalized structure
113
+ // This allows JQ queries to work directly against the data array
114
+ // Store as JSON string to maintain compatibility with existing query methods
115
+ this.storage[toolCallId] = JSON.stringify({
116
+ _mcp_format: true,
117
+ _raw_structure: parsed._raw_structure,
118
+ _data: parsed._data
119
+ });
120
+ } else if (parsed !== null) {
121
+ // Check if content is double-encoded by trying to parse again
122
+ // Only re-stringify if we detected and handled double-encoding
123
+ try {
124
+ const outerParse = JSON.parse(content);
125
+ if (typeof outerParse === 'string') {
126
+ // This is double-encoded JSON, store the fully parsed result
127
+ if (typeof parsed === 'object') {
128
+ this.storage[toolCallId] = JSON.stringify(parsed);
129
+ } else if (typeof parsed === 'string') {
130
+ // Parsed to a string, store it as-is
131
+ this.storage[toolCallId] = parsed;
132
+ } else {
133
+ // Store the original if we couldn't parse further
134
+ this.storage[toolCallId] = content;
135
+ }
136
+ } else {
137
+ // Not double-encoded, store original to preserve formatting
138
+ this.storage[toolCallId] = content;
139
+ }
140
+ } catch {
141
+ // Not valid JSON, store as-is
142
+ this.storage[toolCallId] = content;
143
+ }
144
+ } else {
145
+ // Could not parse as JSON, store as-is
146
+ this.storage[toolCallId] = content;
147
+ }
95
148
 
96
149
  // Store metadata for reference
97
150
  this.metadataStorage[toolCallId] = {
@@ -1,6 +1,7 @@
1
1
  export { Base64ImageDetector } from "./Base64ImageDetector";
2
2
  export { CustomVariables } from "./CustomVariables";
3
3
  export { TokenCompressor } from "./TokenCompressor";
4
+ export { JsonCompressor, JsonSchema, CompressionMetadata, JsonCompressorStorage } from "./JsonCompressor";
4
5
  export { ToolResponseCache } from "./ToolResponseCache";
5
6
  export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
6
7
  export { HarmonyToolProcessor } from "./HarmonyToolProcessor";