@tyvm/knowhow 0.0.147 → 0.0.149
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 -1
- package/src/agents/base/base.ts +45 -32
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +55 -27
- package/src/ai.ts +4 -1
- package/src/auth/keyAuth.ts +10 -4
- package/src/auth/registerIdentity.ts +67 -0
- package/src/chat/CliChatService.ts +23 -2
- package/src/chat/modules/AgentModule.ts +39 -7
- package/src/chat/modules/InternalChatModule.ts +5 -0
- package/src/chat/modules/ToolCommandModule.ts +102 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +1 -1
- package/src/cli.ts +9 -2
- package/src/clients/http.ts +67 -7
- package/src/clients/index.ts +36 -1
- package/src/commands/agent.ts +1 -0
- package/src/commands/agents.ts +57 -9
- package/src/commands/misc.ts +17 -2
- package/src/commands/processes.ts +203 -0
- package/src/commands/services.ts +23 -1
- package/src/index.ts +1 -0
- package/src/processes/ProcessManager.ts +95 -0
- package/src/processes/managedProcessShim.ts +127 -0
- package/src/processors/Base64ImageDetector.ts +28 -4
- package/src/processors/JsonCompressor.ts +14 -0
- package/src/processors/TokenCompressor.ts +17 -14
- package/src/services/AgentService.ts +2 -2
- package/src/services/AgentSyncFs.ts +12 -1
- package/src/services/ExtensionsService.ts +25 -0
- package/src/services/LazyToolsService.ts +15 -20
- package/src/services/Tools.ts +10 -0
- package/src/services/index.ts +4 -0
- package/src/services/modules/computerUse.ts +3 -1
- package/src/services/modules/index.ts +6 -0
- package/src/services/modules/types.ts +4 -0
- package/tests/clients/AIClient.test.ts +98 -15
- package/tests/processes/managedProcessShim.test.ts +59 -0
- package/tests/processors/Base64ImageDetector.test.ts +21 -0
- package/tests/processors/TokenCompressor.test.ts +18 -0
- package/tests/unit/auth/registerIdentity.test.ts +93 -0
- package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
- package/tests/unit/commands/agents.test.ts +44 -1
- package/tests/unit/services/LazyToolsService.test.ts +24 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -1
- package/ts_build/src/agents/base/base.js +30 -21
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
- package/ts_build/src/agents/tools/execCommand.js +100 -223
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +14 -1
- package/ts_build/src/agents/tools/startAgentTask.js +37 -21
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +2 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/auth/keyAuth.js +10 -3
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/auth/registerIdentity.d.ts +7 -0
- package/ts_build/src/auth/registerIdentity.js +62 -0
- package/ts_build/src/auth/registerIdentity.js.map +1 -0
- package/ts_build/src/chat/CliChatService.d.ts +6 -0
- package/ts_build/src/chat/CliChatService.js +17 -2
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
- package/ts_build/src/chat/modules/AgentModule.js +27 -7
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +5 -0
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +1 -1
- package/ts_build/src/cli.js +6 -2
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +4 -0
- package/ts_build/src/clients/http.js +68 -6
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.js +23 -1
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -0
- package/ts_build/src/commands/agents.js +43 -8
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +12 -1
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/processes.d.ts +2 -0
- package/ts_build/src/commands/processes.js +282 -0
- package/ts_build/src/commands/processes.js.map +1 -0
- package/ts_build/src/commands/services.js +11 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +1 -0
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processes/ProcessManager.d.ts +51 -0
- package/ts_build/src/processes/ProcessManager.js +160 -0
- package/ts_build/src/processes/ProcessManager.js.map +1 -0
- package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
- package/ts_build/src/processes/managedProcessShim.js +167 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- package/ts_build/src/processors/Base64ImageDetector.d.ts +1 -0
- package/ts_build/src/processors/Base64ImageDetector.js +19 -2
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +7 -0
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +1 -1
- package/ts_build/src/processors/TokenCompressor.js +9 -8
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +1 -1
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.js +6 -1
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/ExtensionsService.d.ts +10 -0
- package/ts_build/src/services/ExtensionsService.js +17 -0
- package/ts_build/src/services/ExtensionsService.js.map +1 -0
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +5 -7
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +3 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +3 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/computerUse.d.ts +3 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/index.js +5 -0
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +3 -0
- package/ts_build/tests/clients/AIClient.test.js +73 -11
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/processes/managedProcessShim.test.d.ts +1 -0
- package/ts_build/tests/processes/managedProcessShim.test.js +92 -0
- package/ts_build/tests/processes/managedProcessShim.test.js.map +1 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js +18 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +15 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/auth/registerIdentity.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js +86 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
- package/ts_build/tests/unit/commands/agents.test.js +28 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -1
- package/ts_build/tests/unit/services/LazyToolsService.test.js +20 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -1
|
@@ -187,6 +187,98 @@ describe("AIClient", () => {
|
|
|
187
187
|
expect(result.model).toBe("unknown-model");
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
// ── New tests: explicit provider + slashed model prefix stripping ────────
|
|
191
|
+
|
|
192
|
+
it("knowhow provider: resolves 'anthropic/claude-sonnet-4-6' to model stored as 'anthropic/claude-sonnet-4-6'", () => {
|
|
193
|
+
aiClient.registerClient("knowhow", new FakeClient());
|
|
194
|
+
aiClient.registerModels("knowhow", [
|
|
195
|
+
"anthropic/claude-sonnet-4-6",
|
|
196
|
+
"openai/gpt-4",
|
|
197
|
+
"google/gemini-pro",
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
// Exact match: provider=knowhow, model=anthropic/claude-sonnet-4-6
|
|
201
|
+
const result = aiClient.detectProviderModel("knowhow", "anthropic/claude-sonnet-4-6");
|
|
202
|
+
expect(result.provider).toBe("knowhow");
|
|
203
|
+
expect(result.model).toBe("anthropic/claude-sonnet-4-6");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("knowhow provider: resolves bare 'claude-sonnet-4-6' to 'anthropic/claude-sonnet-4-6' stored on knowhow", () => {
|
|
207
|
+
aiClient.registerClient("knowhow", new FakeClient());
|
|
208
|
+
aiClient.registerModels("knowhow", [
|
|
209
|
+
"anthropic/claude-sonnet-4-6",
|
|
210
|
+
"openai/gpt-4",
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
// Bare model name — should resolve by stripping the provider prefix from
|
|
214
|
+
// the registered model and matching on the trailing segment
|
|
215
|
+
const result = aiClient.detectProviderModel("knowhow", "claude-sonnet-4-6");
|
|
216
|
+
expect(result.provider).toBe("knowhow");
|
|
217
|
+
expect(result.model).toBe("anthropic/claude-sonnet-4-6");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("fireworks provider: resolves 'kimi-k3' to 'accounts/fireworks/models/kimi-k3'", () => {
|
|
221
|
+
aiClient.registerClient("fireworks", new FakeClient());
|
|
222
|
+
aiClient.registerModels("fireworks", [
|
|
223
|
+
"accounts/fireworks/models/kimi-k3",
|
|
224
|
+
"accounts/fireworks/models/kimi-k2p7-code",
|
|
225
|
+
"accounts/fireworks/models/minimax-m3",
|
|
226
|
+
"accounts/fireworks/models/deepseek-v3-2",
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const result = aiClient.detectProviderModel("fireworks", "kimi-k3");
|
|
230
|
+
expect(result.provider).toBe("fireworks");
|
|
231
|
+
expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("fireworks provider: resolves 'accounts/fireworks/models/kimi-k3' (full path) correctly", () => {
|
|
235
|
+
aiClient.registerClient("fireworks", new FakeClient());
|
|
236
|
+
aiClient.registerModels("fireworks", [
|
|
237
|
+
"accounts/fireworks/models/kimi-k3",
|
|
238
|
+
"accounts/fireworks/models/deepseek-v4-pro",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Full path exact match
|
|
242
|
+
const result = aiClient.detectProviderModel("fireworks", "accounts/fireworks/models/kimi-k3");
|
|
243
|
+
expect(result.provider).toBe("fireworks");
|
|
244
|
+
expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("explicit provider: does NOT leak to a different provider when stripping slashes", () => {
|
|
248
|
+
aiClient.registerClient("knowhow", new FakeClient());
|
|
249
|
+
aiClient.registerModels("knowhow", ["anthropic/claude-sonnet-4-6"]);
|
|
250
|
+
|
|
251
|
+
// 'fake' provider doesn't have this model — should not find it on knowhow
|
|
252
|
+
const result = aiClient.detectProviderModel("fake", "claude-sonnet-4-6");
|
|
253
|
+
// Falls back: provider=fake, model=claude-sonnet-4-6 (no cross-provider leak)
|
|
254
|
+
expect(result.provider).toBe("fake");
|
|
255
|
+
expect(result.model).toBe("claude-sonnet-4-6");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("empty provider: finds 'claude-sonnet-4-6' cross-provider on knowhow (stored as 'anthropic/claude-sonnet-4-6')", () => {
|
|
259
|
+
aiClient.registerClient("knowhow", new FakeClient());
|
|
260
|
+
aiClient.registerModels("knowhow", [
|
|
261
|
+
"anthropic/claude-sonnet-4-6",
|
|
262
|
+
"openai/gpt-4",
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const result = aiClient.detectProviderModel("", "claude-sonnet-4-6");
|
|
266
|
+
expect(result.provider).toBe("knowhow");
|
|
267
|
+
expect(result.model).toBe("anthropic/claude-sonnet-4-6");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("empty provider: finds 'kimi-k3' cross-provider on fireworks (stored as 'accounts/fireworks/models/kimi-k3')", () => {
|
|
271
|
+
aiClient.registerClient("fireworks", new FakeClient());
|
|
272
|
+
aiClient.registerModels("fireworks", [
|
|
273
|
+
"accounts/fireworks/models/kimi-k3",
|
|
274
|
+
"accounts/fireworks/models/deepseek-v4-pro",
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
const result = aiClient.detectProviderModel("", "kimi-k3");
|
|
278
|
+
expect(result.provider).toBe("fireworks");
|
|
279
|
+
expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
|
|
280
|
+
});
|
|
281
|
+
|
|
190
282
|
it("should detect real provider when model exists", () => {
|
|
191
283
|
// Register an anthropic provider explicitly so we don't rely on env vars
|
|
192
284
|
aiClient.registerClient("anthropic", new FakeClient());
|
|
@@ -442,14 +534,14 @@ describe("AIClient", () => {
|
|
|
442
534
|
});
|
|
443
535
|
|
|
444
536
|
it("should handle empty provider and model strings", () => {
|
|
445
|
-
// Empty
|
|
446
|
-
|
|
447
|
-
expect(
|
|
448
|
-
expect(result.model.length).toBeGreaterThan(0);
|
|
537
|
+
// Empty provider + empty model: detectProviderModel returns the fallback (empty strings)
|
|
538
|
+
// getClient("", "") throws because "" is not a registered provider
|
|
539
|
+
expect(() => aiClient.getClient("", "")).toThrow();
|
|
449
540
|
|
|
450
541
|
const detection = aiClient.detectProviderModel("", "");
|
|
451
|
-
|
|
452
|
-
expect(detection?.
|
|
542
|
+
// Falls back to original values — both empty
|
|
543
|
+
expect(detection?.provider).toBe("");
|
|
544
|
+
expect(detection?.model).toBe("");
|
|
453
545
|
});
|
|
454
546
|
|
|
455
547
|
it("should handle malformed model formats", () => {
|
|
@@ -469,15 +561,6 @@ describe("AIClient", () => {
|
|
|
469
561
|
expect(detection).toBeDefined();
|
|
470
562
|
expect(detection?.provider).toBeDefined();
|
|
471
563
|
expect(detection?.model).toBeDefined();
|
|
472
|
-
// For malformed inputs that can't be parsed, should fallback to defaults
|
|
473
|
-
if (
|
|
474
|
-
input === "provider/" ||
|
|
475
|
-
input === "///" ||
|
|
476
|
-
input === "provider/model/"
|
|
477
|
-
) {
|
|
478
|
-
expect(detection?.provider?.length).toBeGreaterThan(0);
|
|
479
|
-
expect(detection?.model?.length).toBeGreaterThan(0);
|
|
480
|
-
}
|
|
481
564
|
});
|
|
482
565
|
});
|
|
483
566
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
|
|
6
|
+
describe("managed process shim", () => {
|
|
7
|
+
const testOnPosix = process.platform === "win32" ? test.skip : test;
|
|
8
|
+
|
|
9
|
+
testOnPosix("exits after a short command instead of leaking the shim process", async () => {
|
|
10
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-shim-test-"));
|
|
11
|
+
const configPath = path.join(directory, "launch.json");
|
|
12
|
+
const shimModule = path.resolve(__dirname, "../../src/processes/managedProcessShim");
|
|
13
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
id: "short-command",
|
|
15
|
+
command: process.execPath,
|
|
16
|
+
args: ["-e", "process.exit(0)"],
|
|
17
|
+
cwd: directory,
|
|
18
|
+
env: {},
|
|
19
|
+
background: true,
|
|
20
|
+
parentProcessId: null,
|
|
21
|
+
parentPid: null,
|
|
22
|
+
shell: false,
|
|
23
|
+
processesDir: path.dirname(directory),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const script = `require(${JSON.stringify(shimModule)})`
|
|
27
|
+
+ `.runManagedProcessShim(${JSON.stringify(configPath)})`
|
|
28
|
+
+ `.catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
29
|
+
const shim = spawn(process.execPath, ["-r", "ts-node/register/transpile-only", "-e", script], {
|
|
30
|
+
cwd: path.resolve(__dirname, "../.."),
|
|
31
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
let stderr = "";
|
|
34
|
+
shim.stderr!.setEncoding("utf8");
|
|
35
|
+
shim.stderr!.on("data", (chunk) => { stderr += chunk; });
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
shim.kill("SIGKILL");
|
|
41
|
+
reject(new Error(`Shim PID ${shim.pid} did not exit after its command completed`));
|
|
42
|
+
}, 10000);
|
|
43
|
+
shim.once("exit", (code, signal) => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
resolve({ code, signal });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(stderr).toBe("");
|
|
50
|
+
expect(result).toEqual({ code: 0, signal: null });
|
|
51
|
+
const status = JSON.parse(fs.readFileSync(path.join(directory, "status.json"), "utf8"));
|
|
52
|
+
expect(status).toMatchObject({ shimPid: shim.pid, state: "exited", exitCode: 0 });
|
|
53
|
+
expect(() => process.kill(shim.pid!, 0)).toThrow();
|
|
54
|
+
} finally {
|
|
55
|
+
if (shim.exitCode === null && shim.signalCode === null) shim.kill("SIGKILL");
|
|
56
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -220,6 +220,27 @@ describe("Base64ImageDetector", () => {
|
|
|
220
220
|
expect(content[0].image_url.url).toBe(imageDataUrl);
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
it("should not promote a compressed image URL placeholder to image content", () => {
|
|
224
|
+
const toolResponseJson = JSON.stringify({
|
|
225
|
+
type: "image_url",
|
|
226
|
+
image_url: {
|
|
227
|
+
url: "[COMPRESSED_JSON_PROPERTY - 24614 tokens]\nKey: compressed_image_url",
|
|
228
|
+
detail: "high",
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const modifiedMessages: Message[] = [
|
|
232
|
+
{
|
|
233
|
+
role: "tool",
|
|
234
|
+
content: toolResponseJson,
|
|
235
|
+
tool_call_id: "call_compressed_image",
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
detector.createProcessor()([], modifiedMessages);
|
|
240
|
+
|
|
241
|
+
expect(modifiedMessages[0].content).toBe(toolResponseJson);
|
|
242
|
+
});
|
|
243
|
+
|
|
223
244
|
it("should process tool messages with plain base64 string", () => {
|
|
224
245
|
const validPngBase64 =
|
|
225
246
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
|
|
@@ -415,6 +415,24 @@ describe("TokenCompressor", () => {
|
|
|
415
415
|
});
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
+
describe("image content compression", () => {
|
|
419
|
+
it("keeps image URLs intact when expanded", () => {
|
|
420
|
+
const imageUrl = `data:image/png;base64,${"A".repeat(20_000)}`;
|
|
421
|
+
const content = JSON.stringify({
|
|
422
|
+
type: "image_url",
|
|
423
|
+
image_url: { url: imageUrl, detail: "high" },
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const compressed = tokenCompressor.compressContent(content);
|
|
427
|
+
const key = compressed.match(/Key:\s*(\S+)/)?.[1];
|
|
428
|
+
|
|
429
|
+
expect(key).toBeDefined();
|
|
430
|
+
const expanded = tokenCompressor.retrieveFullString(key!);
|
|
431
|
+
expect(expanded).toBe(content);
|
|
432
|
+
expect(expanded).not.toContain("[COMPRESSED_JSON_PROPERTY");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
418
436
|
describe("tool function integration", () => {
|
|
419
437
|
it("should register expandTokens function correctly", () => {
|
|
420
438
|
const toolsServiceCalls = mockToolsService.addFunctions.mock.calls;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const registerPublicKey = jest.fn();
|
|
6
|
+
const exchangePublicKeyJwt = jest.fn();
|
|
7
|
+
const storeJwt = jest.fn();
|
|
8
|
+
const getOrCreatePublicKey = jest.fn();
|
|
9
|
+
const getConfig = jest.fn();
|
|
10
|
+
const updateConfig = jest.fn();
|
|
11
|
+
|
|
12
|
+
jest.mock("../../../src/auth/keyAuth", () => ({ registerPublicKey, exchangePublicKeyJwt, storeJwt }));
|
|
13
|
+
jest.mock("../../../src/auth/keyManager", () => ({ getOrCreatePublicKey }));
|
|
14
|
+
jest.mock("../../../src/config", () => ({ getConfig, updateConfig }));
|
|
15
|
+
|
|
16
|
+
import { decodeJwtPayload, extractOrgIdFromJwt, registerIdentity } from "../../../src/auth/registerIdentity";
|
|
17
|
+
|
|
18
|
+
function makeJwt(payload: Record<string, unknown>): string {
|
|
19
|
+
return `${Buffer.from("{}").toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.sig`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("registerIdentity", () => {
|
|
23
|
+
const originalCwd = process.cwd();
|
|
24
|
+
let workDir: string;
|
|
25
|
+
const sourceJwt = makeJwt({ org: "org-test" });
|
|
26
|
+
const publicKeyBase64 = Buffer.alloc(32, 5).toString("base64");
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
31
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-register-identity-"));
|
|
32
|
+
fs.mkdirSync(path.join(workDir, ".knowhow"), { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(workDir, ".knowhow", ".jwt"), sourceJwt);
|
|
34
|
+
process.chdir(workDir);
|
|
35
|
+
getOrCreatePublicKey.mockReturnValue({
|
|
36
|
+
publicKeyBase64,
|
|
37
|
+
fingerprint: "SHA256:test",
|
|
38
|
+
privateKeyPath: path.join(workDir, ".knowhow", "keys", "id_ed25519"),
|
|
39
|
+
});
|
|
40
|
+
registerPublicKey.mockResolvedValue(undefined);
|
|
41
|
+
exchangePublicKeyJwt.mockResolvedValue("identity.jwt.token");
|
|
42
|
+
getConfig.mockResolvedValue({ modelProviders: [] });
|
|
43
|
+
updateConfig.mockResolvedValue(undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
process.chdir(originalCwd);
|
|
48
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
49
|
+
jest.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("registers from the project JWT and stores the exchanged JWT", async () => {
|
|
53
|
+
await registerIdentity({ apiUrl: "https://api.example" });
|
|
54
|
+
expect(registerPublicKey).toHaveBeenCalledWith(sourceJwt, publicKeyBase64, "https://api.example");
|
|
55
|
+
expect(exchangePublicKeyJwt).toHaveBeenCalledWith("org-test", "https://api.example", undefined);
|
|
56
|
+
expect(storeJwt).toHaveBeenCalledWith("identity.jwt.token");
|
|
57
|
+
expect(updateConfig).toHaveBeenCalledWith(expect.objectContaining({
|
|
58
|
+
orgId: "org-test",
|
|
59
|
+
modelProviders: [{ provider: "knowhow" }],
|
|
60
|
+
cliIdentityPath: expect.stringContaining("id_ed25519"),
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses an explicitly selected identity", async () => {
|
|
65
|
+
await registerIdentity({ identityPath: "/keys/id", apiUrl: "https://api.example" });
|
|
66
|
+
expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/id");
|
|
67
|
+
expect(exchangePublicKeyJwt).toHaveBeenCalledWith("org-test", "https://api.example", "/keys/id");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("requires a nonempty project JWT", async () => {
|
|
71
|
+
fs.writeFileSync(path.join(workDir, ".knowhow", ".jwt"), " ");
|
|
72
|
+
await expect(registerIdentity()).rejects.toThrow("JWT file is empty");
|
|
73
|
+
expect(registerPublicKey).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not replace the JWT when registration fails", async () => {
|
|
77
|
+
registerPublicKey.mockRejectedValue(new Error("registration failed"));
|
|
78
|
+
await expect(registerIdentity()).rejects.toThrow("registration failed");
|
|
79
|
+
expect(storeJwt).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("JWT organization extraction", () => {
|
|
84
|
+
it("decodes payload and accepts canonical org", () => {
|
|
85
|
+
expect(decodeJwtPayload(makeJwt({ org: "one" }))).toMatchObject({ org: "one" });
|
|
86
|
+
expect(extractOrgIdFromJwt(makeJwt({ org: "one" }))).toBe("one");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects malformed and organization-less JWTs", () => {
|
|
90
|
+
expect(() => decodeJwtPayload("invalid")).toThrow("Invalid JWT format");
|
|
91
|
+
expect(() => extractOrgIdFromJwt(makeJwt({ sub: "user" }))).toThrow("organization claim");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ToolCommandModule } from "../../../src/chat/modules/ToolCommandModule";
|
|
2
|
+
import { LazyToolsService } from "../../../src/services/LazyToolsService";
|
|
3
|
+
import { ChatService } from "../../../src/chat/types";
|
|
4
|
+
|
|
5
|
+
function makeHarness() {
|
|
6
|
+
const tools = new LazyToolsService();
|
|
7
|
+
tools.addTools([{
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "hiddenEcho",
|
|
11
|
+
description: "echo",
|
|
12
|
+
parameters: { type: "object", properties: { value: { type: "string" } } },
|
|
13
|
+
},
|
|
14
|
+
}]);
|
|
15
|
+
tools.setFunction("hiddenEcho", ({ value }: { value: string }) => ({ value }));
|
|
16
|
+
|
|
17
|
+
const context: any = { plugins: [], chatHistory: [], activeAgentTaskId: "task-1" };
|
|
18
|
+
const service = {
|
|
19
|
+
getTools: () => tools,
|
|
20
|
+
getContext: () => context,
|
|
21
|
+
registerCommand: jest.fn(),
|
|
22
|
+
registerMode: jest.fn(),
|
|
23
|
+
} as unknown as ChatService;
|
|
24
|
+
const module = new ToolCommandModule();
|
|
25
|
+
return module.initialize(service).then(() => ({
|
|
26
|
+
command: module.getCommands()[0],
|
|
27
|
+
tools,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("ToolCommandModule", () => {
|
|
32
|
+
let log: jest.SpyInstance;
|
|
33
|
+
let error: jest.SpyInstance;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
log = jest.spyOn(console, "log").mockImplementation();
|
|
37
|
+
error = jest.spyOn(console, "error").mockImplementation();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => jest.restoreAllMocks());
|
|
41
|
+
|
|
42
|
+
test("executes compact JSON against a hidden lazy tool", async () => {
|
|
43
|
+
const { command, tools } = await makeHarness();
|
|
44
|
+
expect(tools.getToolNames()).not.toContain("hiddenEcho");
|
|
45
|
+
|
|
46
|
+
await command.handler(['{"name":"hiddenEcho","arguments":{"value":"hello world"}}']);
|
|
47
|
+
|
|
48
|
+
expect(log).toHaveBeenCalledWith('{\n "value": "hello world"\n}');
|
|
49
|
+
expect(error).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("accepts an OpenAI-style tool call", async () => {
|
|
53
|
+
const { command } = await makeHarness();
|
|
54
|
+
await command.handler([
|
|
55
|
+
'{"id":"call-1","function":{"name":"hiddenEcho","arguments":{"value":"ok"}}}',
|
|
56
|
+
]);
|
|
57
|
+
expect(log).toHaveBeenCalledWith('{\n "value": "ok"\n}');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("prints usage and actionable errors", async () => {
|
|
61
|
+
const { command } = await makeHarness();
|
|
62
|
+
await command.handler([]);
|
|
63
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Usage: /tool"));
|
|
64
|
+
|
|
65
|
+
await command.handler(["not-json"]);
|
|
66
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining("Invalid tool call JSON"));
|
|
67
|
+
|
|
68
|
+
await command.handler(['{"name":"missing","arguments":{}}']);
|
|
69
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining("Tool execution failed"));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -9,7 +9,7 @@ const setupServices = jest.fn().mockResolvedValue(undefined);
|
|
|
9
9
|
jest.mock("../../../src/chat", () => ({ startChat }));
|
|
10
10
|
jest.mock("../../../src/commands/services", () => ({ setupServices }));
|
|
11
11
|
|
|
12
|
-
import { addAgentsCommand } from "../../../src/commands/agents";
|
|
12
|
+
import { addAgentsCommand, getStatus } from "../../../src/commands/agents";
|
|
13
13
|
|
|
14
14
|
describe("agents resume terminal UX", () => {
|
|
15
15
|
let previousCwd: string;
|
|
@@ -66,3 +66,46 @@ describe("agents resume terminal UX", () => {
|
|
|
66
66
|
);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
describe("agents effective status", () => {
|
|
71
|
+
let previousCwd: string;
|
|
72
|
+
let tempDir: string;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
previousCwd = process.cwd();
|
|
76
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-agent-status-"));
|
|
77
|
+
process.chdir(tempDir);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
process.chdir(previousCwd);
|
|
82
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("keeps a running status when the recorded process is alive", () => {
|
|
86
|
+
expect(getStatus("task", { status: "running", pid: process.pid })).toBe(
|
|
87
|
+
"running"
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("reports a running task as failed when its process exited", () => {
|
|
92
|
+
expect(getStatus("task", { status: "running", pid: 2_147_483_647 })).toBe(
|
|
93
|
+
"failed"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("reports old legacy running records without a pid as stale", () => {
|
|
98
|
+
expect(
|
|
99
|
+
getStatus("task", {
|
|
100
|
+
status: "running",
|
|
101
|
+
lastUpdate: new Date(Date.now() - 10 * 60 * 1000).toISOString(),
|
|
102
|
+
})
|
|
103
|
+
).toBe("stale");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("never overrides a persisted terminal status", () => {
|
|
107
|
+
expect(getStatus("task", { status: "completed", pid: process.pid })).toBe(
|
|
108
|
+
"completed"
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -26,4 +26,28 @@ describe("LazyToolsService persisted tool restoration", () => {
|
|
|
26
26
|
"finalAnswer",
|
|
27
27
|
]);
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
test("calls a registered hidden tool without making it prompt-visible", async () => {
|
|
31
|
+
const tools = new LazyToolsService();
|
|
32
|
+
tools.addTools([tool("hiddenTool")]);
|
|
33
|
+
tools.setFunction("hiddenTool", ({ value }: { value: string }) => `got:${value}`);
|
|
34
|
+
|
|
35
|
+
expect(tools.getToolNames()).not.toContain("hiddenTool");
|
|
36
|
+
expect(tools.getFunctionNames()).toContain("hiddenTool");
|
|
37
|
+
|
|
38
|
+
const result = await tools.callTool({
|
|
39
|
+
id: "hidden-call",
|
|
40
|
+
type: "function",
|
|
41
|
+
function: {
|
|
42
|
+
name: "hiddenTool",
|
|
43
|
+
arguments: JSON.stringify({ value: "test" }),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.functionResp).toBe("got:test");
|
|
48
|
+
expect(tools.getToolNames()).not.toContain("hiddenTool");
|
|
49
|
+
expect(tools.listAvailableTools()).toMatchObject({
|
|
50
|
+
disabled: expect.arrayContaining(["hiddenTool"]),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
29
53
|
});
|
package/ts_build/package.json
CHANGED
|
@@ -192,5 +192,5 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
192
192
|
content: string;
|
|
193
193
|
} | null;
|
|
194
194
|
getTaskBreakdown(messages: Message[]): Promise<string>;
|
|
195
|
-
compressMessages(messages: Message[]
|
|
195
|
+
compressMessages(messages: Message[]): Promise<Message[]>;
|
|
196
196
|
}
|
|
@@ -647,8 +647,6 @@ let BaseAgent = class BaseAgent {
|
|
|
647
647
|
messages = this.formatInputMessages(messages);
|
|
648
648
|
this.updateCurrentThread(messages);
|
|
649
649
|
const isMissingTool = this.isRequiredToolMissing();
|
|
650
|
-
const startIndex = 0;
|
|
651
|
-
const endIndex = messages.length;
|
|
652
650
|
messages = await this.messageProcessor.processMessages(messages, "pre_call");
|
|
653
651
|
const interruptResponse = {
|
|
654
652
|
choices: [
|
|
@@ -784,7 +782,7 @@ let BaseAgent = class BaseAgent {
|
|
|
784
782
|
? `Compacting messages (manual /compact request): ${contextTokens} tokens`
|
|
785
783
|
: `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`);
|
|
786
784
|
this._forceCompact = false;
|
|
787
|
-
messages = await this.compressMessages(messages
|
|
785
|
+
messages = await this.compressMessages(messages);
|
|
788
786
|
this.startNewThread(messages);
|
|
789
787
|
this.lastPromptTokens = 0;
|
|
790
788
|
}
|
|
@@ -1041,8 +1039,7 @@ let BaseAgent = class BaseAgent {
|
|
|
1041
1039
|
const response = await this.createAgentCompletion({
|
|
1042
1040
|
model,
|
|
1043
1041
|
messages: taskBreakdownMessages,
|
|
1044
|
-
|
|
1045
|
-
tool_choice: "auto",
|
|
1042
|
+
tool_choice: "none",
|
|
1046
1043
|
long_ttl_cache: this.runTime() > 300_000,
|
|
1047
1044
|
...(this.reasoningEffort !== undefined && {
|
|
1048
1045
|
reasoning_effort: this.reasoningEffort,
|
|
@@ -1051,13 +1048,24 @@ let BaseAgent = class BaseAgent {
|
|
|
1051
1048
|
reasoning_summary: this.summarizeReasoning,
|
|
1052
1049
|
}),
|
|
1053
1050
|
});
|
|
1054
|
-
|
|
1051
|
+
const breakdownContent = response.choices
|
|
1052
|
+
.map((c) => c.message.content)
|
|
1053
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
1054
|
+
.join("\n\n");
|
|
1055
|
+
this.taskBreakdown = breakdownContent || "";
|
|
1055
1056
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1056
|
-
return this.taskBreakdown;
|
|
1057
|
+
return this.taskBreakdown || undefined;
|
|
1057
1058
|
}
|
|
1058
|
-
async compressMessages(messages
|
|
1059
|
-
|
|
1060
|
-
|
|
1059
|
+
async compressMessages(messages) {
|
|
1060
|
+
let resumeIndex = messages.length - 1;
|
|
1061
|
+
while (resumeIndex >= 0 &&
|
|
1062
|
+
messages[resumeIndex].role !== "assistant") {
|
|
1063
|
+
resumeIndex--;
|
|
1064
|
+
}
|
|
1065
|
+
const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
|
|
1066
|
+
const toCompress = messages.slice(0, compressionEnd);
|
|
1067
|
+
const resumeMessages = messages.slice(compressionEnd);
|
|
1068
|
+
this.log(`Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`);
|
|
1061
1069
|
const toCompressPrompt = `We are compressing our conversation to save memory.
|
|
1062
1070
|
Please summarize the conversation so far, so that we may continue the original task with a smaller context
|
|
1063
1071
|
|
|
@@ -1067,14 +1075,13 @@ let BaseAgent = class BaseAgent {
|
|
|
1067
1075
|
3. Next Steps - what we're about to do next to continue the user's original request.
|
|
1068
1076
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1069
1077
|
|
|
1070
|
-
Our initial task breakdown: ${this.taskBreakdown}
|
|
1078
|
+
${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
|
|
1071
1079
|
|
|
1072
|
-
This summary will
|
|
1080
|
+
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1073
1081
|
|
|
1074
1082
|
`;
|
|
1075
|
-
const model = this.getModel();
|
|
1076
1083
|
const compressMessagesPayload = [
|
|
1077
|
-
...
|
|
1084
|
+
...toCompress,
|
|
1078
1085
|
{
|
|
1079
1086
|
role: "user",
|
|
1080
1087
|
content: toCompressPrompt,
|
|
@@ -1082,28 +1089,30 @@ let BaseAgent = class BaseAgent {
|
|
|
1082
1089
|
];
|
|
1083
1090
|
const response = await this.createAgentCompletion({
|
|
1084
1091
|
messages: compressMessagesPayload,
|
|
1092
|
+
tool_choice: "none",
|
|
1085
1093
|
});
|
|
1086
|
-
const summaries = response.choices
|
|
1094
|
+
const summaries = response.choices
|
|
1095
|
+
.map((c) => c.message.content)
|
|
1096
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0);
|
|
1087
1097
|
this.summaries.push(...summaries);
|
|
1088
1098
|
const startMessages = [
|
|
1089
1099
|
{
|
|
1090
1100
|
role: "user",
|
|
1091
1101
|
content: `
|
|
1092
|
-
Initial task breakdown:
|
|
1093
|
-
${this.taskBreakdown}
|
|
1102
|
+
${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
|
|
1094
1103
|
|
|
1095
1104
|
We have just compressed the conversation to save memory:
|
|
1096
|
-
${JSON.stringify(summaries)}
|
|
1105
|
+
${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
|
|
1097
1106
|
|
|
1098
1107
|
Please continue the task from where we left off
|
|
1099
1108
|
`,
|
|
1100
1109
|
},
|
|
1101
1110
|
];
|
|
1102
|
-
const
|
|
1111
|
+
const systemMessages = toCompress.filter((m) => m.role === "system");
|
|
1103
1112
|
const newMessages = [
|
|
1104
|
-
...
|
|
1113
|
+
...systemMessages,
|
|
1105
1114
|
...startMessages,
|
|
1106
|
-
...
|
|
1115
|
+
...resumeMessages,
|
|
1107
1116
|
];
|
|
1108
1117
|
const oldLength = this.getMessagesLength(messages);
|
|
1109
1118
|
const newLength = this.getMessagesLength(newMessages);
|