@tyvm/knowhow 0.0.127 → 0.0.129
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/scripts/cache-miss-debug.ts +365 -0
- package/src/agents/base/base.ts +119 -54
- package/src/agents/tools/execCommand.ts +38 -4
- package/src/agents/tools/minimal/callTool.ts +45 -0
- package/src/agents/tools/minimal/definitions.ts +50 -0
- package/src/agents/tools/minimal/index.ts +3 -0
- package/src/agents/tools/minimal/inspectTools.ts +27 -0
- package/src/chat/modules/AgentModule.ts +27 -8
- package/src/clients/contextLimits.ts +9 -0
- package/src/clients/index.ts +6 -0
- package/src/clients/meta.ts +18 -0
- package/src/clients/pricing/anthropic.ts +13 -0
- package/src/clients/pricing/fireworks.ts +18 -12
- package/src/clients/pricing/index.ts +1 -0
- package/src/clients/pricing/meta.ts +12 -0
- package/src/clients/pricing/openai.ts +13 -0
- package/src/clients/pricing/xai.ts +3 -0
- package/src/commands/services.ts +1 -1
- package/src/processors/MinimalToolsMessageProcessor.ts +113 -0
- package/src/processors/TokenCompressor.ts +21 -4
- package/src/processors/index.ts +7 -1
- package/src/services/AgentSyncFs.ts +49 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/KnowhowClient.ts +1 -1
- package/src/services/LazyToolsService.ts +12 -0
- package/src/services/MinimalToolsService.ts +167 -0
- package/src/services/Tools.ts +9 -0
- package/src/services/index.ts +1 -0
- package/src/worker.ts +7 -0
- package/tests/processors/TokenCompressor.test.ts +45 -0
- package/tests/unit/services/MinimalToolsService.test.ts +424 -0
- package/ts_build/package.json +2 -1
- package/ts_build/src/agents/base/base.d.ts +4 -1
- package/ts_build/src/agents/base/base.js +62 -31
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +24 -3
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/minimal/callTool.d.ts +5 -0
- package/ts_build/src/agents/tools/minimal/callTool.js +28 -0
- package/ts_build/src/agents/tools/minimal/callTool.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/definitions.d.ts +4 -0
- package/ts_build/src/agents/tools/minimal/definitions.js +46 -0
- package/ts_build/src/agents/tools/minimal/definitions.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/index.d.ts +3 -0
- package/ts_build/src/agents/tools/minimal/index.js +20 -0
- package/ts_build/src/agents/tools/minimal/index.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.d.ts +2 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js +15 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js.map +1 -0
- package/ts_build/src/chat/modules/AgentModule.js +8 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +8 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +1 -0
- package/ts_build/src/clients/index.js +6 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/meta.d.ts +4 -0
- package/ts_build/src/clients/meta.js +15 -0
- package/ts_build/src/clients/meta.js.map +1 -0
- package/ts_build/src/clients/pricing/anthropic.d.ts +4 -0
- package/ts_build/src/clients/pricing/anthropic.js +12 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +2 -5
- package/ts_build/src/clients/pricing/fireworks.js +13 -11
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/clients/pricing/index.d.ts +1 -0
- package/ts_build/src/clients/pricing/index.js +5 -2
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/meta.d.ts +6 -0
- package/ts_build/src/clients/pricing/meta.js +8 -0
- package/ts_build/src/clients/pricing/meta.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +3 -0
- package/ts_build/src/clients/pricing/openai.js +12 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +1 -0
- package/ts_build/src/clients/pricing/xai.js +3 -0
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/processors/MinimalToolsMessageProcessor.d.ts +7 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js +61 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js.map +1 -0
- package/ts_build/src/processors/TokenCompressor.js +5 -2
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/processors/index.d.ts +2 -1
- package/ts_build/src/processors/index.js +3 -1
- package/ts_build/src/processors/index.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +30 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +14 -0
- package/ts_build/src/services/MinimalToolsService.js +80 -0
- package/ts_build/src/services/MinimalToolsService.js.map +1 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +1 -0
- package/ts_build/src/services/index.js +1 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +8 -0
- package/ts_build/src/worker.js +5 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +27 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/services/MinimalToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/MinimalToolsService.test.js +320 -0
- package/ts_build/tests/unit/services/MinimalToolsService.test.js.map +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Message, ToolCall } from "../clients/types";
|
|
2
|
+
import { MessageProcessorFunction } from "../services/MessageProcessor";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MinimalToolsMessageProcessor
|
|
6
|
+
*
|
|
7
|
+
* Works with MinimalToolsService to unwrap `callTool(name, args)` invocations
|
|
8
|
+
* in the message history, rewriting them as if the real tool had been called
|
|
9
|
+
* directly.
|
|
10
|
+
*
|
|
11
|
+
* This is registered on the `pre_tools` lifecycle so it runs right before tool
|
|
12
|
+
* calls are dispatched. It rewrites the last assistant message's tool_calls so
|
|
13
|
+
* that:
|
|
14
|
+
* callTool({ name: "readFile", args: { filePath: "..." } })
|
|
15
|
+
* becomes:
|
|
16
|
+
* readFile({ filePath: "..." })
|
|
17
|
+
*
|
|
18
|
+
* This means:
|
|
19
|
+
* 1. The `tools:` array sent to the AI never needs to change (cache stable).
|
|
20
|
+
* 2. The agent terminates correctly when callTool("finalAnswer", ...) is called,
|
|
21
|
+
* because after transformation the required tool name matches.
|
|
22
|
+
* 3. The conversation history looks clean — the model sees native tool calls in
|
|
23
|
+
* subsequent turns rather than nested callTool wrappers.
|
|
24
|
+
*/
|
|
25
|
+
export class MinimalToolsMessageProcessor {
|
|
26
|
+
/**
|
|
27
|
+
* Unwraps a single callTool ToolCall into the real underlying ToolCall.
|
|
28
|
+
* Returns null if this is not a callTool invocation.
|
|
29
|
+
*/
|
|
30
|
+
private unwrapCallTool(toolCall: ToolCall): ToolCall | null {
|
|
31
|
+
if (toolCall.function.name !== "callTool") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let parsed: { name?: string; args?: Record<string, any> };
|
|
36
|
+
try {
|
|
37
|
+
parsed =
|
|
38
|
+
typeof toolCall.function.arguments === "string"
|
|
39
|
+
? JSON.parse(toolCall.function.arguments)
|
|
40
|
+
: toolCall.function.arguments;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { name, args } = parsed;
|
|
46
|
+
if (!name || typeof name !== "string") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: toolCall.id,
|
|
52
|
+
type: "function",
|
|
53
|
+
function: {
|
|
54
|
+
name,
|
|
55
|
+
arguments: JSON.stringify(args ?? {}),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Processes the modified messages array in-place:
|
|
62
|
+
* - Finds assistant messages whose tool_calls contain callTool invocations.
|
|
63
|
+
* - Replaces those callTool entries with the real underlying tool calls.
|
|
64
|
+
*
|
|
65
|
+
* Only the most-recent assistant message (the one just added by the AI) is
|
|
66
|
+
* rewritten — older messages are left untouched so caching is not affected.
|
|
67
|
+
*/
|
|
68
|
+
private processMessages(
|
|
69
|
+
_originalMessages: Message[],
|
|
70
|
+
modifiedMessages: Message[]
|
|
71
|
+
): void {
|
|
72
|
+
// Walk backwards to find the last assistant message with tool_calls
|
|
73
|
+
for (let i = modifiedMessages.length - 1; i >= 0; i--) {
|
|
74
|
+
const msg = modifiedMessages[i];
|
|
75
|
+
if (msg.role !== "assistant" || !msg.tool_calls?.length) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let didUnwrap = false;
|
|
80
|
+
const newToolCalls: ToolCall[] = msg.tool_calls.map((tc) => {
|
|
81
|
+
const unwrapped = this.unwrapCallTool(tc);
|
|
82
|
+
if (unwrapped) {
|
|
83
|
+
didUnwrap = true;
|
|
84
|
+
return unwrapped;
|
|
85
|
+
}
|
|
86
|
+
return tc;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (didUnwrap) {
|
|
90
|
+
msg.tool_calls = newToolCalls;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only process the last assistant message
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns a MessageProcessorFunction suitable for registering on the
|
|
100
|
+
* `pre_tools` lifecycle of a MessageProcessor.
|
|
101
|
+
*/
|
|
102
|
+
createProcessor(): MessageProcessorFunction {
|
|
103
|
+
return (
|
|
104
|
+
originalMessages: Message[],
|
|
105
|
+
modifiedMessages: Message[]
|
|
106
|
+
): void => {
|
|
107
|
+
this.processMessages(originalMessages, modifiedMessages);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const globalMinimalToolsMessageProcessor =
|
|
113
|
+
new MinimalToolsMessageProcessor();
|
|
@@ -155,14 +155,26 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
155
155
|
* Check if content is already compressed
|
|
156
156
|
*/
|
|
157
157
|
private isAlreadyCompressed(content: string): boolean {
|
|
158
|
-
// Check for compressed
|
|
159
|
-
|
|
158
|
+
// Check for a compressed-string marker, but ONLY when the content *starts*
|
|
159
|
+
// with it. Compressor output always begins with "[COMPRESSED_STRING ...".
|
|
160
|
+
// A naive `.includes()` produced false positives for large payloads (e.g. a
|
|
161
|
+
// usage.json / message-history dump) that merely CONTAIN that substring deep
|
|
162
|
+
// inside their nested data — those files were left completely uncompressed
|
|
163
|
+
// because this guard incorrectly reported them as already compressed.
|
|
164
|
+
if (content.trimStart().startsWith("[COMPRESSED_STRING")) {
|
|
160
165
|
return true;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// Check for compressed JSON structure with schema key
|
|
169
|
+
// Only treat this as compressed when `_schema_key` is an actual TOP-LEVEL
|
|
170
|
+
// key of the parsed object — not merely present somewhere in the string.
|
|
164
171
|
const parsed = this.tryParseJson(content);
|
|
165
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
parsed &&
|
|
174
|
+
typeof parsed === "object" &&
|
|
175
|
+
!Array.isArray(parsed) &&
|
|
176
|
+
typeof parsed._schema_key === "string"
|
|
177
|
+
) {
|
|
166
178
|
return true;
|
|
167
179
|
}
|
|
168
180
|
|
|
@@ -296,7 +308,12 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
296
308
|
return async (originalMessages: Message[], modifiedMessages: Message[]) => {
|
|
297
309
|
// Find the index of the last assistant message so we only compress
|
|
298
310
|
// the most recent batch of tool responses (after the last agent output).
|
|
299
|
-
// This prevents re-compressing already-cached historical messages
|
|
311
|
+
// This prevents re-compressing already-cached historical messages, which
|
|
312
|
+
// would break prompt caching. Newly-added tool responses (the "current
|
|
313
|
+
// batch") are the ones that need compressing before they reach the model,
|
|
314
|
+
// and they appear after the last assistant message at the moment this
|
|
315
|
+
// processor runs (post_tools / pre_call), when the fresh tool response is
|
|
316
|
+
// still the tail of the message list.
|
|
300
317
|
let lastAssistantIndex = -1;
|
|
301
318
|
for (let i = modifiedMessages.length - 1; i >= 0; i--) {
|
|
302
319
|
if (modifiedMessages[i].role === "assistant") {
|
package/src/processors/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export { Base64ImageProcessor } from "./Base64ImageDetector";
|
|
2
2
|
export { CustomVariables } from "./CustomVariables";
|
|
3
3
|
export { TokenCompressor } from "./TokenCompressor";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
JsonCompressor,
|
|
6
|
+
JsonSchema,
|
|
7
|
+
CompressionMetadata,
|
|
8
|
+
JsonCompressorStorage,
|
|
9
|
+
} from "./JsonCompressor";
|
|
5
10
|
export { ToolResponseCache } from "./ToolResponseCache";
|
|
6
11
|
export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
|
|
7
12
|
export { HarmonyToolProcessor } from "./HarmonyToolProcessor";
|
|
13
|
+
export { MinimalToolsMessageProcessor } from "./MinimalToolsMessageProcessor";
|
|
@@ -33,6 +33,7 @@ export class AgentSyncFs {
|
|
|
33
33
|
private agent: BaseAgent | undefined;
|
|
34
34
|
private threadUpdateHandler: ((...args: any[]) => void) | undefined;
|
|
35
35
|
private doneHandler: ((...args: any[]) => void) | undefined;
|
|
36
|
+
private tokenUsageHandler: ((...args: any[]) => void) | undefined;
|
|
36
37
|
/**
|
|
37
38
|
* Tracks the most recent in-flight filesystem metadata update.
|
|
38
39
|
* The done handler awaits this before finalizing, preventing a race where
|
|
@@ -118,6 +119,33 @@ export class AgentSyncFs {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Append a token usage entry (including the full message chain sent for
|
|
124
|
+
* that call) to usage.json. This is intended for debugging cache-hit
|
|
125
|
+
* issues, where comparing the message chains and usage values between
|
|
126
|
+
* consecutive calls can reveal when a cache prefix was invalidated.
|
|
127
|
+
*/
|
|
128
|
+
private async appendUsageEntry(entry: any): Promise<void> {
|
|
129
|
+
if (!this.taskPath) return;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const usagePath = path.join(this.taskPath, "usage.json");
|
|
133
|
+
let entries: any[] = [];
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const existingData = await fs.readFile(usagePath, "utf8");
|
|
137
|
+
entries = JSON.parse(existingData);
|
|
138
|
+
} catch {
|
|
139
|
+
// File doesn't exist or is invalid, start fresh
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
entries.push(entry);
|
|
143
|
+
await fs.writeFile(usagePath, JSON.stringify(entries, null, 2), "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`❌ Failed to append usage entry:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
/**
|
|
122
150
|
* Update metadata file with current agent state
|
|
123
151
|
*/
|
|
@@ -352,6 +380,20 @@ export class AgentSyncFs {
|
|
|
352
380
|
this.threadUpdateHandler
|
|
353
381
|
);
|
|
354
382
|
|
|
383
|
+
// Listen to token usage events to record per-call usage + message chain
|
|
384
|
+
// (store reference for cleanup). This is written to usage.json so that
|
|
385
|
+
// cache-hit/cache-miss issues can be debugged by comparing the message
|
|
386
|
+
// chain and usage values sent/received on consecutive AI calls.
|
|
387
|
+
this.tokenUsageHandler = (usage: any) => {
|
|
388
|
+
this.appendUsageEntry(usage).catch((error) => {
|
|
389
|
+
console.error(`❌ Error recording token usage:`, error);
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
agent.agentEvents.on(
|
|
393
|
+
agent.eventTypes.tokenUsage,
|
|
394
|
+
this.tokenUsageHandler
|
|
395
|
+
);
|
|
396
|
+
|
|
355
397
|
// Listen to completion event to finalize task (store reference for cleanup)
|
|
356
398
|
this.doneHandler = (result: string) => {
|
|
357
399
|
if (!this.taskId) {
|
|
@@ -495,6 +537,13 @@ export class AgentSyncFs {
|
|
|
495
537
|
);
|
|
496
538
|
this.threadUpdateHandler = undefined;
|
|
497
539
|
}
|
|
540
|
+
if (this.tokenUsageHandler) {
|
|
541
|
+
this.agent.agentEvents.removeListener(
|
|
542
|
+
this.agent.eventTypes.tokenUsage,
|
|
543
|
+
this.tokenUsageHandler
|
|
544
|
+
);
|
|
545
|
+
this.tokenUsageHandler = undefined;
|
|
546
|
+
}
|
|
498
547
|
if (this.doneHandler) {
|
|
499
548
|
this.agent.agentEvents.removeListener(
|
|
500
549
|
this.agent.eventTypes.done,
|
|
@@ -89,7 +89,7 @@ export class AgentSyncKnowhowWeb {
|
|
|
89
89
|
try {
|
|
90
90
|
await this.client.updateChatTask(knowhowTaskId, {
|
|
91
91
|
threads: agent.getThreads(),
|
|
92
|
-
|
|
92
|
+
totalUsdCost: agent.getTotalCostUsd(),
|
|
93
93
|
inProgress,
|
|
94
94
|
...(result ? { result } : {}),
|
|
95
95
|
});
|
|
@@ -9,6 +9,18 @@ import {
|
|
|
9
9
|
} from "../agents/tools/lazy";
|
|
10
10
|
import { definitions } from "../agents/tools/lazy/definitions";
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
*
|
|
14
|
+
* This ToolService design allows an agent to start with a few enabled tools,
|
|
15
|
+
* search and enable additional tools
|
|
16
|
+
*
|
|
17
|
+
* One drawback: enabling a tool will bust the cache, causing you to have to pay extra after enabling new tools
|
|
18
|
+
* Benefit: for providers with limits on how many tools you can support, this tool service allows you to have the minimal set of tools you need to use in the tools array. Smaller context usage
|
|
19
|
+
*
|
|
20
|
+
* In a longer running session, where the agent's tools accumulate over multiple interactions, the cache busting is minimized
|
|
21
|
+
* In web sessions where you get a new ToolService each interaction, then the cache busting is going to happen with each enable
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
12
24
|
export class LazyToolsService extends ToolsService {
|
|
13
25
|
private allTools: Tool[] = [];
|
|
14
26
|
// Start with lazy tools enabled by default
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { ToolsService, ToolContext } from "./Tools";
|
|
2
|
+
import { Tool, ToolCall } from "../clients/types";
|
|
3
|
+
import { callTool } from "../agents/tools/minimal/callTool";
|
|
4
|
+
import { inspectTools } from "../agents/tools/minimal/inspectTools";
|
|
5
|
+
import { minimalDefinitions } from "../agents/tools/minimal/definitions";
|
|
6
|
+
import { finalAnswer } from "../agents/tools/finalAnswer";
|
|
7
|
+
import { includedTools } from "../agents/tools/list";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* MinimalToolsService
|
|
11
|
+
*
|
|
12
|
+
* A cache-stable ToolService: the `tools:` array sent to the AI is always the
|
|
13
|
+
* same small set of base tools (callTool, inspectTools, finalAnswer + any
|
|
14
|
+
* explicitly registered base tools). All other tools live in `allTools` and
|
|
15
|
+
* are callable via callTool(name, args).
|
|
16
|
+
*
|
|
17
|
+
* Benefits over LazyToolsService:
|
|
18
|
+
* - The `tools:` array NEVER changes during a session → zero cache busts from tool changes.
|
|
19
|
+
* - Works on all providers and older models (no provider-specific defer_loading needed).
|
|
20
|
+
* - Agents discover tools via inspectTools() and call them via callTool().
|
|
21
|
+
*/
|
|
22
|
+
export class MinimalToolsService extends ToolsService {
|
|
23
|
+
/** Full catalog of every registered tool (base + extended). */
|
|
24
|
+
private allToolsCatalog: Tool[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(context?: ToolContext) {
|
|
27
|
+
super(context);
|
|
28
|
+
this.registerMinimalTools();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers the immutable base tools.
|
|
33
|
+
* These are the only tools that ever appear in the `tools:` array.
|
|
34
|
+
*/
|
|
35
|
+
private registerMinimalTools(): void {
|
|
36
|
+
// finalAnswer definition is sourced from the included tools list
|
|
37
|
+
const finalAnswerDef = includedTools.find(
|
|
38
|
+
(t) => t.function.name === "finalAnswer"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const baseDefs: Tool[] = [
|
|
42
|
+
...(finalAnswerDef ? [finalAnswerDef as Tool] : []),
|
|
43
|
+
...minimalDefinitions,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Add base tool defs to both catalogs
|
|
47
|
+
this.allToolsCatalog.push(...baseDefs);
|
|
48
|
+
super.addTools(baseDefs);
|
|
49
|
+
|
|
50
|
+
// Register implementations
|
|
51
|
+
this.addFunctions({
|
|
52
|
+
finalAnswer: finalAnswer.bind(this),
|
|
53
|
+
callTool: callTool.bind(this),
|
|
54
|
+
inspectTools: inspectTools.bind(this),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Override addTools so that newly-registered tools go into the full catalog
|
|
60
|
+
* but NOT into the visible `tools` array (which stays stable).
|
|
61
|
+
*/
|
|
62
|
+
addTools(tools: Tool[]): void {
|
|
63
|
+
const existingNames = this.allToolsCatalog.map((t) => t.function.name);
|
|
64
|
+
const newTools = tools.filter(
|
|
65
|
+
(t) => !existingNames.includes(t.function.name)
|
|
66
|
+
);
|
|
67
|
+
this.allToolsCatalog.push(...newTools);
|
|
68
|
+
// Do NOT call super.addTools() — base tools array stays frozen.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
addTool(tool: Tool): void {
|
|
72
|
+
this.addTools([tool]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns only the stable base tools (what gets sent in `tools:` to the AI).
|
|
77
|
+
*/
|
|
78
|
+
getTools(): Tool[] {
|
|
79
|
+
return super.getTools();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns every tool in the catalog (base + extended). Used by callTool /
|
|
84
|
+
* inspectTools for dispatching and schema lookup.
|
|
85
|
+
*/
|
|
86
|
+
getAllTools(): Tool[] {
|
|
87
|
+
return this.allToolsCatalog;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Calls a tool by its resolved name, bypassing the enabled-tools check that
|
|
92
|
+
* the base class applies. Used internally by the callTool dispatch function.
|
|
93
|
+
* Returns the raw functionResp (unwrapped), used by the callTool meta-tool.
|
|
94
|
+
*/
|
|
95
|
+
async callToolByName(
|
|
96
|
+
toolCall: ToolCall,
|
|
97
|
+
resolvedName: string
|
|
98
|
+
): Promise<any> {
|
|
99
|
+
const result = await this.dispatchToTool(toolCall, resolvedName);
|
|
100
|
+
return result.functionResp;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shared dispatch logic: temporarily surfaces the target tool (if it's not
|
|
105
|
+
* already visible) so the base ToolsService.callTool can find its
|
|
106
|
+
* definition and implementation, then restores the visible set.
|
|
107
|
+
* Used both by direct calls (agent calls the tool by its real name) and by
|
|
108
|
+
* the callTool meta-tool (agent calls callTool({ name, args })).
|
|
109
|
+
*/
|
|
110
|
+
private async dispatchToTool(toolCall: ToolCall, resolvedName: string) {
|
|
111
|
+
// Temporarily surface the target tool in the visible list so base
|
|
112
|
+
// ToolsService.callTool can find its definition, then restore state.
|
|
113
|
+
const alreadyVisible = super
|
|
114
|
+
.getTools()
|
|
115
|
+
.some((t) => t.function.name === resolvedName);
|
|
116
|
+
|
|
117
|
+
if (!alreadyVisible) {
|
|
118
|
+
const toolDef = this.allToolsCatalog.find(
|
|
119
|
+
(t) => t.function.name === resolvedName
|
|
120
|
+
);
|
|
121
|
+
if (toolDef) {
|
|
122
|
+
super.addTools([toolDef]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = await super.callTool(toolCall, [resolvedName]);
|
|
127
|
+
|
|
128
|
+
// Remove the temporarily-added tool to keep the visible set stable
|
|
129
|
+
if (!alreadyVisible) {
|
|
130
|
+
this.tools = this.tools.filter((t) => t.function.name !== resolvedName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Delegates to the full catalog for resolution, then runs via callToolByName.
|
|
138
|
+
*/
|
|
139
|
+
async callTool(toolCall: ToolCall, enabledTools?: string[]): Promise<any> {
|
|
140
|
+
const functionName = toolCall.function.name;
|
|
141
|
+
|
|
142
|
+
// Base tools (callTool, inspectTools, finalAnswer) → standard dispatch
|
|
143
|
+
const isBaseTool = super
|
|
144
|
+
.getTools()
|
|
145
|
+
.some((t) => t.function.name === functionName);
|
|
146
|
+
|
|
147
|
+
if (isBaseTool) {
|
|
148
|
+
return super.callTool(toolCall, this.getToolNames());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Extended tools: called directly by name (not via the callTool meta-tool).
|
|
152
|
+
// These live only in allToolsCatalog, not in the frozen visible `tools`
|
|
153
|
+
// array, so we must temporarily surface them the same way callToolByName
|
|
154
|
+
// does, otherwise base ToolsService.getTool() won't find the definition
|
|
155
|
+
// and will throw "Tool ... not found" even though it's a registered tool.
|
|
156
|
+
const existsInCatalog = this.allToolsCatalog.some(
|
|
157
|
+
(t) => t.function.name === functionName
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (existsInCatalog) {
|
|
161
|
+
return this.dispatchToTool(toolCall, functionName);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Not found anywhere - let the base class produce the standard error.
|
|
165
|
+
return super.callTool(toolCall, [functionName]);
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/services/Tools.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface ToolContext {
|
|
|
25
25
|
metadata?: { [key: string]: any };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/*
|
|
29
|
+
*
|
|
30
|
+
* This ToolService design allows an agent to start with every tool
|
|
31
|
+
*
|
|
32
|
+
* Drawbacks:
|
|
33
|
+
* some providers have limits on how many tools you can use
|
|
34
|
+
* larger context bloat from having every tool defined in system prompt
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
28
37
|
export class ToolsService {
|
|
29
38
|
private context: ToolContext = {};
|
|
30
39
|
|
package/src/services/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from "./EmbeddingsService";
|
|
|
24
24
|
export * from "./S3";
|
|
25
25
|
export * from "./Tools";
|
|
26
26
|
export * from "./LazyToolsService";
|
|
27
|
+
export * from "./MinimalToolsService";
|
|
27
28
|
export * as MCP from "./Mcp";
|
|
28
29
|
export * from "./EmbeddingService";
|
|
29
30
|
export * from "./DockerService";
|
package/src/worker.ts
CHANGED
|
@@ -225,9 +225,16 @@ export async function worker(options?: {
|
|
|
225
225
|
console.log(
|
|
226
226
|
"Worker tools configured! Update knowhow.json to adjust which tools are allowed by the worker."
|
|
227
227
|
);
|
|
228
|
+
console.log(
|
|
229
|
+
"Tunnel is disabled by default. Set worker.tunnel.enabled = true in knowhow.json to enable it."
|
|
230
|
+
);
|
|
228
231
|
config.worker = {
|
|
229
232
|
...config.worker,
|
|
230
233
|
allowedTools: Tools.getToolNames(),
|
|
234
|
+
tunnel: {
|
|
235
|
+
enabled: false,
|
|
236
|
+
...config.worker?.tunnel,
|
|
237
|
+
},
|
|
231
238
|
};
|
|
232
239
|
await updateConfig(config);
|
|
233
240
|
return;
|
|
@@ -83,6 +83,51 @@ describe("TokenCompressor", () => {
|
|
|
83
83
|
expect(parsed.smallProp).toBe("small"); // Should remain unchanged
|
|
84
84
|
expect(parsed.largeProp).toContain("[COMPRESSED_JSON_PROPERTY"); // Should be compressed
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it("should compress large content that merely CONTAINS a compression marker deep inside (regression)", () => {
|
|
88
|
+
// Regression for the false-positive in isAlreadyCompressed(): a large
|
|
89
|
+
// payload (e.g. a usage.json / message-history dump) whose body contains
|
|
90
|
+
// the substring "[COMPRESSED_STRING" deep inside its nested data was
|
|
91
|
+
// incorrectly treated as already-compressed and returned uncompressed,
|
|
92
|
+
// letting 1MB+ raw tool responses flow into createCompletion untouched.
|
|
93
|
+
const nestedMarker =
|
|
94
|
+
'[COMPRESSED_STRING - 10155 tokens in 3 chunks]\\nKey: compressed_abc123';
|
|
95
|
+
// Build a big JSON array whose entries embed the marker far from the start.
|
|
96
|
+
const entry = JSON.stringify({
|
|
97
|
+
role: "tool",
|
|
98
|
+
name: "readFile",
|
|
99
|
+
content: nestedMarker,
|
|
100
|
+
filler: "z".repeat(500),
|
|
101
|
+
});
|
|
102
|
+
const bigContent = "[" + new Array(200).fill(entry).join(",") + "]";
|
|
103
|
+
|
|
104
|
+
// Sanity: the raw content contains the marker but is NOT at the start.
|
|
105
|
+
expect(bigContent.includes("[COMPRESSED_STRING")).toBe(true);
|
|
106
|
+
expect(bigContent.trimStart().startsWith("[COMPRESSED_STRING")).toBe(false);
|
|
107
|
+
|
|
108
|
+
const result = tokenCompressor.compressContent(bigContent);
|
|
109
|
+
|
|
110
|
+
// It MUST actually get compressed, not returned untouched.
|
|
111
|
+
expect(result.length).toBeLessThan(bigContent.length);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should NOT recompress content that genuinely starts with a compression marker", () => {
|
|
115
|
+
// Idempotency: real compressor output begins with "[COMPRESSED_STRING".
|
|
116
|
+
const alreadyCompressed =
|
|
117
|
+
"[COMPRESSED_STRING - 999 tokens in 2 chunks]\nKey: compressed_xyz\nPath: \nPreview: ...";
|
|
118
|
+
const result = tokenCompressor.compressContent(alreadyCompressed);
|
|
119
|
+
expect(result).toBe(alreadyCompressed);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should NOT recompress JSON whose top-level _schema_key marks it compressed", () => {
|
|
123
|
+
// Idempotency: JSON-compressed output carries a top-level _schema_key.
|
|
124
|
+
const compressedJson = JSON.stringify({
|
|
125
|
+
_schema_key: "compressed_schema_1",
|
|
126
|
+
data: "x".repeat(40000),
|
|
127
|
+
});
|
|
128
|
+
const result = tokenCompressor.compressContent(compressedJson);
|
|
129
|
+
expect(result).toBe(compressedJson);
|
|
130
|
+
});
|
|
86
131
|
});
|
|
87
132
|
|
|
88
133
|
describe("compressStringInChunks", () => {
|