@tyvm/knowhow 0.0.118 → 0.0.119
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 +1 -3
- package/src/agents/base/base.ts +72 -9
- package/src/agents/researcher/researcher.ts +9 -2
- package/src/agents/tools/list.ts +13 -2
- package/src/agents/tools/patch.ts +318 -32
- package/src/agents/tools/readFile.ts +48 -5
- package/src/chat/modules/AgentModule.ts +12 -0
- package/src/cli.ts +2 -0
- package/src/clients/anthropic.ts +12 -2
- package/src/clients/contextLimits.ts +77 -0
- package/src/commands/convert.ts +291 -0
- package/src/conversion.ts +15 -61
- package/src/index.ts +3 -0
- package/src/processors/TokenCompressor.ts +95 -9
- package/src/services/AgentSyncFs.ts +26 -4
- package/src/services/AgentSyncKnowhowWeb.ts +26 -4
- package/src/services/SyncedAgentWatcher.ts +8 -0
- package/src/services/conversion/ConversionService.ts +763 -0
- package/src/services/conversion/index.ts +2 -0
- package/src/services/conversion/types.ts +79 -0
- package/src/services/index.ts +8 -1
- package/src/services/modules/types.ts +2 -0
- package/src/services/watchers/FsSyncer.ts +6 -0
- package/src/services/watchers/RemoteSyncer.ts +5 -0
- package/tests/agents/tools/readFile.test.ts +88 -0
- package/tests/clients/AIClient.test.ts +5 -0
- package/tests/clients/contextLimits.test.ts +71 -0
- package/tests/patching/patchFileOutput.test.ts +217 -0
- package/tests/patching/regression-2026.test.ts +278 -0
- package/tests/processors/CustomVariables.test.ts +4 -4
- package/tests/processors/TokenCompressor.test.ts +59 -1
- package/tests/processors/tools/grepToolResponse.test.ts +72 -0
- package/tests/services/ConversionService.test.ts +154 -0
- package/tests/test.spec.ts +1 -1
- package/tests/unit/clients/AIClient.test.ts +8 -0
- package/ts_build/package.json +1 -3
- package/ts_build/src/agents/base/base.d.ts +3 -0
- package/ts_build/src/agents/base/base.js +46 -3
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +5 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +10 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/patch.js +202 -24
- package/ts_build/src/agents/tools/patch.js.map +1 -1
- package/ts_build/src/agents/tools/readFile.d.ts +1 -1
- package/ts_build/src/agents/tools/readFile.js +17 -4
- package/ts_build/src/agents/tools/readFile.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +12 -0
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +2 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +70 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/commands/convert.d.ts +2 -0
- package/ts_build/src/commands/convert.js +275 -0
- package/ts_build/src/commands/convert.js.map +1 -0
- package/ts_build/src/conversion.js +6 -38
- package/ts_build/src/conversion.js.map +1 -1
- package/ts_build/src/index.d.ts +2 -0
- package/ts_build/src/index.js +4 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +2 -0
- package/ts_build/src/processors/TokenCompressor.js +57 -7
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +21 -4
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +21 -4
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +3 -0
- package/ts_build/src/services/SyncedAgentWatcher.js +4 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/conversion/ConversionService.d.ts +18 -0
- package/ts_build/src/services/conversion/ConversionService.js +585 -0
- package/ts_build/src/services/conversion/ConversionService.js.map +1 -0
- package/ts_build/src/services/conversion/index.d.ts +2 -0
- package/ts_build/src/services/conversion/index.js +19 -0
- package/ts_build/src/services/conversion/index.js.map +1 -0
- package/ts_build/src/services/conversion/types.d.ts +49 -0
- package/ts_build/src/services/conversion/types.js +3 -0
- package/ts_build/src/services/conversion/types.js.map +1 -0
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +6 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/src/services/watchers/FsSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/FsSyncer.js +5 -0
- package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
- package/ts_build/src/services/watchers/RemoteSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js +4 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
- package/ts_build/tests/agents/tools/readFile.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/readFile.test.js +90 -0
- package/ts_build/tests/agents/tools/readFile.test.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +1 -0
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/contextLimits.test.d.ts +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +57 -0
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.d.ts +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.js +187 -0
- package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -0
- package/ts_build/tests/patching/regression-2026.test.js +214 -0
- package/ts_build/tests/patching/regression-2026.test.js.map +1 -1
- package/ts_build/tests/processors/CustomVariables.test.js +4 -4
- package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +37 -1
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/processors/tools/grepToolResponse.test.d.ts +1 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js +40 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -0
- package/ts_build/tests/services/ConversionService.test.d.ts +1 -0
- package/ts_build/tests/services/ConversionService.test.js +154 -0
- package/ts_build/tests/services/ConversionService.test.js.map +1 -0
- package/ts_build/tests/test.spec.js +1 -1
- package/ts_build/tests/test.spec.js.map +1 -1
- package/ts_build/tests/unit/clients/AIClient.test.js +3 -0
- package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -1
|
@@ -32,6 +32,12 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
32
32
|
private compressionThreshold: number = 4000;
|
|
33
33
|
private characterLimit: number = this.compressionThreshold * 4;
|
|
34
34
|
|
|
35
|
+
// Minimum size (in characters) a standalone chunk should have. Leftover content
|
|
36
|
+
// smaller than this is merged into an adjacent chunk so we never emit tiny,
|
|
37
|
+
// low-value chunks that force the agent into extra round-trips to reassemble a
|
|
38
|
+
// file. Defaults to half of the per-chunk character limit.
|
|
39
|
+
private minChunkCharacters: number = Math.floor(this.characterLimit / 2);
|
|
40
|
+
|
|
35
41
|
// Largest size retrievable without re-compressing
|
|
36
42
|
public maxTokens: number = this.compressionThreshold * 2;
|
|
37
43
|
|
|
@@ -56,6 +62,7 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
56
62
|
public setCompressionThreshold(threshold: number): void {
|
|
57
63
|
this.compressionThreshold = threshold;
|
|
58
64
|
this.characterLimit = threshold * 4; // Update character limit based on new threshold
|
|
65
|
+
this.minChunkCharacters = Math.floor(this.characterLimit / 2);
|
|
59
66
|
this.jsonCompressor.updateSettings(threshold, this.maxTokens);
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -88,7 +95,7 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
88
95
|
const chunkKeys: string[] = [];
|
|
89
96
|
let remaining = content;
|
|
90
97
|
|
|
91
|
-
// Split from the end, creating chunks that will be linked
|
|
98
|
+
// Split from the end, creating chunks that will be linked.
|
|
92
99
|
while (remaining.length > this.characterLimit) {
|
|
93
100
|
const chunkStart = remaining.length - this.characterLimit;
|
|
94
101
|
const chunk = remaining.substring(chunkStart);
|
|
@@ -96,9 +103,19 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
96
103
|
remaining = remaining.substring(0, chunkStart);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
// The remaining part becomes the first chunk
|
|
106
|
+
// The remaining part becomes the first chunk. To avoid producing a tiny,
|
|
107
|
+
// low-value leading chunk (which forces the agent into extra round-trips to
|
|
108
|
+
// reassemble a file), merge it into the next chunk when it falls below the
|
|
109
|
+
// minimum chunk size. Only split it out as its own chunk when it is large
|
|
110
|
+
// enough to stand on its own.
|
|
100
111
|
if (remaining.length > 0) {
|
|
101
|
-
chunks.
|
|
112
|
+
if (chunks.length > 0 && remaining.length < this.minChunkCharacters) {
|
|
113
|
+
// Prepend the small leftover onto the first existing chunk so every
|
|
114
|
+
// emitted chunk carries a meaningful amount of content.
|
|
115
|
+
chunks[0] = remaining + chunks[0];
|
|
116
|
+
} else {
|
|
117
|
+
chunks.unshift(remaining);
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
// Store chunks and create chain of references
|
|
@@ -131,7 +148,7 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
131
148
|
200
|
|
132
149
|
)}...\n[Use ${
|
|
133
150
|
this.toolName
|
|
134
|
-
} tool with key "${firstKey}" to retrieve content
|
|
151
|
+
} tool with key "${firstKey}" to retrieve the FULL content in one call (chunks are auto-stitched; pass fromLine/toLine for a ranged read)]\n[TIP: try jqToolResponse,grepToolResponse,tailToolResponse to filter/search/map with REAL line numbers without expanding at all]`;
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
/**
|
|
@@ -282,6 +299,38 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
282
299
|
return this.storage[key] || null;
|
|
283
300
|
}
|
|
284
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Retrieves the fully reassembled content for a key, following any
|
|
304
|
+
* `NEXT_CHUNK_KEY` references and stripping the chunk-linking markers. This
|
|
305
|
+
* lets callers get the complete content in a single call instead of chasing a
|
|
306
|
+
* chain of nested keys.
|
|
307
|
+
*/
|
|
308
|
+
retrieveFullString(key: string): string | null {
|
|
309
|
+
let currentKey: string | null = key;
|
|
310
|
+
let result = "";
|
|
311
|
+
let found = false;
|
|
312
|
+
const visited = new Set<string>();
|
|
313
|
+
|
|
314
|
+
while (currentKey && !visited.has(currentKey)) {
|
|
315
|
+
visited.add(currentKey);
|
|
316
|
+
const chunk = this.storage[currentKey];
|
|
317
|
+
if (chunk === undefined) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
found = true;
|
|
321
|
+
|
|
322
|
+
const nextMatch = chunk.match(/\n\n\[NEXT_CHUNK_KEY:\s*([^\s\]]+)\]/);
|
|
323
|
+
const nextKey = nextMatch ? nextMatch[1] : null;
|
|
324
|
+
|
|
325
|
+
// Strip the trailing NEXT_CHUNK_KEY marker before stitching.
|
|
326
|
+
const cleaned = chunk.replace(/\n\n\[NEXT_CHUNK_KEY:\s*[^\s\]]+\]/, "");
|
|
327
|
+
result += cleaned;
|
|
328
|
+
currentKey = nextKey;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return found ? result : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
285
334
|
storeString(key: string, value: string): void {
|
|
286
335
|
if (this.estimateTokens(value) > this.maxTokens) {
|
|
287
336
|
// adjust max tokens so we can always retrieve this without re-compressing
|
|
@@ -307,15 +356,42 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
307
356
|
if (toolsService) {
|
|
308
357
|
toolsService.addTools([expandTokensDefinition]);
|
|
309
358
|
toolsService.addFunctions({
|
|
310
|
-
[this.toolName]: (key: string) => {
|
|
311
|
-
|
|
359
|
+
[this.toolName]: (key: string, fromLine?: number, toLine?: number) => {
|
|
360
|
+
// Auto-stitch: follow any NEXT_CHUNK_KEY chain and return the full,
|
|
361
|
+
// reassembled content so the agent never has to chase nested keys.
|
|
362
|
+
const data = this.retrieveFullString(key);
|
|
312
363
|
|
|
313
|
-
if (
|
|
364
|
+
if (data === null) {
|
|
314
365
|
return `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
|
|
315
366
|
", "
|
|
316
367
|
)}`;
|
|
317
368
|
}
|
|
318
|
-
|
|
369
|
+
|
|
370
|
+
// Optional ranged read: return only the requested 1-based, inclusive
|
|
371
|
+
// line range, prefixed with real line numbers for easy mapping back.
|
|
372
|
+
const hasRange =
|
|
373
|
+
typeof fromLine === "number" || typeof toLine === "number";
|
|
374
|
+
if (!hasRange) {
|
|
375
|
+
return data;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const lines = data.split("\n");
|
|
379
|
+
const totalLines = lines.length;
|
|
380
|
+
const start = Math.max(1, typeof fromLine === "number" ? fromLine : 1);
|
|
381
|
+
const end = Math.min(
|
|
382
|
+
totalLines,
|
|
383
|
+
typeof toLine === "number" ? toLine : totalLines
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (start > end) {
|
|
387
|
+
return `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const numbered: string[] = [];
|
|
391
|
+
for (let i = start; i <= end; i++) {
|
|
392
|
+
numbered.push(`${i}: ${lines[i - 1]}`);
|
|
393
|
+
}
|
|
394
|
+
return numbered.join("\n");
|
|
319
395
|
},
|
|
320
396
|
});
|
|
321
397
|
}
|
|
@@ -420,7 +496,7 @@ export const expandTokensDefinition: Tool = {
|
|
|
420
496
|
function: {
|
|
421
497
|
name: "expandTokens",
|
|
422
498
|
description:
|
|
423
|
-
"Retrieve
|
|
499
|
+
"Retrieve compressed data that was stored during message processing. The full content is automatically reassembled (any chunk chain is followed for you, so you never need to chase NEXT_CHUNK_KEY references). Optionally pass fromLine/toLine (1-based, inclusive) to return just a range of lines, prefixed with real line numbers. NOTE: Can also use jqToolResponse, grepToolResponse, tailToolResponse to access/filter/search compressed content without needing to expand all tokens.",
|
|
424
500
|
parameters: {
|
|
425
501
|
type: "object",
|
|
426
502
|
positional: true,
|
|
@@ -429,6 +505,16 @@ export const expandTokensDefinition: Tool = {
|
|
|
429
505
|
type: "string",
|
|
430
506
|
description: "The key of the compressed data to retrieve",
|
|
431
507
|
},
|
|
508
|
+
fromLine: {
|
|
509
|
+
type: "number",
|
|
510
|
+
description:
|
|
511
|
+
"Optional 1-based start line (inclusive) for a ranged read of the reassembled content.",
|
|
512
|
+
},
|
|
513
|
+
toLine: {
|
|
514
|
+
type: "number",
|
|
515
|
+
description:
|
|
516
|
+
"Optional 1-based end line (inclusive). Defaults to the end of the content when omitted.",
|
|
517
|
+
},
|
|
432
518
|
},
|
|
433
519
|
required: ["key"],
|
|
434
520
|
},
|
|
@@ -220,10 +220,15 @@ export class AgentSyncFs {
|
|
|
220
220
|
);
|
|
221
221
|
this.lastInputContent = input;
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
223
|
+
// Detect slash commands in the input
|
|
224
|
+
if (input.trim().startsWith("/")) {
|
|
225
|
+
await this.handleInputCommand(agent, input.trim());
|
|
226
|
+
} else {
|
|
227
|
+
agent.addPendingUserMessage({
|
|
228
|
+
role: "user",
|
|
229
|
+
content: input,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
227
232
|
|
|
228
233
|
// Clear the input file after processing
|
|
229
234
|
await this.writeInput("");
|
|
@@ -233,6 +238,23 @@ export class AgentSyncFs {
|
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Handle slash commands sent via input.txt
|
|
243
|
+
*/
|
|
244
|
+
private async handleInputCommand(agent: BaseAgent, input: string): Promise<void> {
|
|
245
|
+
const [command, ...rest] = input.split(" ");
|
|
246
|
+
const message = rest.join(" ").trim() || undefined;
|
|
247
|
+
|
|
248
|
+
if (command === "/poke") {
|
|
249
|
+
console.log(`🫵 Received /poke command for task ${this.taskId}`);
|
|
250
|
+
agent.interrupt(message);
|
|
251
|
+
} else {
|
|
252
|
+
// Unknown command — treat as a regular message
|
|
253
|
+
console.warn(`⚠️ Unknown command "${command}", treating as message`);
|
|
254
|
+
agent.addPendingUserMessage({ role: "user", content: input });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
/**
|
|
237
259
|
* Wait for resume by monitoring status file
|
|
238
260
|
*/
|
|
@@ -151,10 +151,15 @@ export class AgentSyncKnowhowWeb {
|
|
|
151
151
|
// Inject pending messages into the agent
|
|
152
152
|
const messageIds: string[] = [];
|
|
153
153
|
for (const msg of pendingMessages) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
154
|
+
// Detect slash commands in messages
|
|
155
|
+
if (msg.message.trim().startsWith("/")) {
|
|
156
|
+
await this.handleMessageCommand(agent, msg.message.trim());
|
|
157
|
+
} else {
|
|
158
|
+
agent.addPendingUserMessage({
|
|
159
|
+
role: msg.role as "user" | "assistant",
|
|
160
|
+
content: msg.message,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
158
163
|
messageIds.push(msg.id);
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -167,6 +172,23 @@ export class AgentSyncKnowhowWeb {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Handle slash commands sent as pending messages
|
|
177
|
+
*/
|
|
178
|
+
private async handleMessageCommand(agent: BaseAgent, input: string): Promise<void> {
|
|
179
|
+
const [command, ...rest] = input.split(" ");
|
|
180
|
+
const message = rest.join(" ").trim() || undefined;
|
|
181
|
+
|
|
182
|
+
if (command === "/poke") {
|
|
183
|
+
console.log(`🫵 Received /poke command for task ${this.knowhowTaskId}`);
|
|
184
|
+
agent.interrupt(message);
|
|
185
|
+
} else {
|
|
186
|
+
// Unknown command — treat as a regular message
|
|
187
|
+
console.warn(`⚠️ Unknown command "${command}", treating as message`);
|
|
188
|
+
agent.addPendingUserMessage({ role: "user", content: input });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
170
192
|
/**
|
|
171
193
|
* Wait for the agent to be resumed or killed via API
|
|
172
194
|
* Polls the API every 2 seconds, with a 1 hour timeout
|
|
@@ -13,6 +13,8 @@ export interface SyncedAgentWatcher {
|
|
|
13
13
|
stopWatching(): void;
|
|
14
14
|
/** The task ID being watched */
|
|
15
15
|
taskId: string;
|
|
16
|
+
/** Interrupt the agent's current tool call or AI completion */
|
|
17
|
+
interrupt(message?: string): Promise<void>;
|
|
16
18
|
/** The agent name being watched */
|
|
17
19
|
agentName: string;
|
|
18
20
|
/** Send a message to the remote agent */
|
|
@@ -55,6 +57,7 @@ export interface AttachableAgent {
|
|
|
55
57
|
pause(): void | Promise<void>;
|
|
56
58
|
unpause(): void | Promise<void>;
|
|
57
59
|
kill(): void | Promise<void>;
|
|
60
|
+
interrupt(message?: string): void | Promise<void>;
|
|
58
61
|
addPendingUserMessage(message: Message): void;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -97,6 +100,11 @@ export class WatcherBackedAgent implements AttachableAgent {
|
|
|
97
100
|
this.agentEvents.emit(this.eventTypes.done, "Agent killed");
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
async interrupt(message?: string): Promise<void> {
|
|
104
|
+
await this.watcher.interrupt(message);
|
|
105
|
+
console.log("🫵 Interrupt sent to remote agent.");
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
addPendingUserMessage(message: Message): void {
|
|
101
109
|
// Fire-and-forget — errors are logged but not surfaced
|
|
102
110
|
const text = typeof message.content === "string" ? message.content : "";
|