@tyvm/knowhow 0.0.154 → 0.0.156
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 +44 -26
- package/src/agents/base/prompt.ts +3 -2
- package/src/agents/tools/index.ts +1 -0
- package/src/agents/tools/list.ts +2 -0
- package/src/agents/tools/loadImageAsBase64.ts +83 -0
- package/src/auth/keyAuth.ts +40 -8
- package/src/chat/modules/AgentModule.ts +98 -42
- package/src/clients/index.ts +7 -4
- package/src/clients/openai.ts +37 -4
- package/src/commands/services.ts +23 -18
- package/src/login.ts +22 -6
- package/src/processors/Base64ImageDetector.ts +12 -11
- package/src/services/KnowhowClient.ts +6 -5
- package/src/services/watchers/FsSyncer.ts +40 -2
- package/src/services/watchers/RemoteSyncer.ts +40 -2
- package/src/tunnel.ts +29 -13
- package/src/worker.ts +62 -12
- package/tests/clients/reasoningEffort.test.ts +57 -0
- package/tests/processors/Base64ImageDetector.test.ts +32 -0
- package/tests/unit/agents/tools/loadImageAsBase64.test.ts +44 -0
- package/tests/unit/auth/keyAuth.test.ts +52 -2
- package/tests/unit/login.test.ts +39 -1
- package/tests/unit/services/KnowhowClient.test.ts +84 -0
- package/tests/unit/services/Mcp.test.ts +56 -0
- package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
- package/tests/unit/tunnel.test.ts +51 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -0
- package/ts_build/src/agents/base/base.js +27 -22
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/base/prompt.d.ts +1 -1
- package/ts_build/src/agents/base/prompt.js +3 -2
- package/ts_build/src/agents/base/prompt.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +1 -0
- package/ts_build/src/agents/tools/index.js +1 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/loadImageAsBase64.d.ts +3 -0
- package/ts_build/src/agents/tools/loadImageAsBase64.js +79 -0
- package/ts_build/src/agents/tools/loadImageAsBase64.js.map +1 -0
- package/ts_build/src/auth/keyAuth.d.ts +1 -0
- package/ts_build/src/auth/keyAuth.js +24 -7
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -0
- package/ts_build/src/chat/modules/AgentModule.js +74 -34
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/clients/index.js +7 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.js +29 -4
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/commands/services.js +15 -8
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/login.js +19 -5
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.js +7 -3
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +5 -3
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/watchers/FsSyncer.d.ts +4 -0
- package/ts_build/src/services/watchers/FsSyncer.js +36 -2
- package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
- package/ts_build/src/services/watchers/RemoteSyncer.d.ts +4 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js +36 -2
- package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
- package/ts_build/src/tunnel.js +19 -8
- package/ts_build/src/tunnel.js.map +1 -1
- package/ts_build/src/worker.js +47 -10
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/clients/reasoningEffort.test.js +47 -0
- package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
- package/ts_build/tests/processors/Base64ImageDetector.test.js +28 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.d.ts +1 -0
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js +67 -0
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js +43 -2
- package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
- package/ts_build/tests/unit/login.test.js +29 -1
- package/ts_build/tests/unit/login.test.js.map +1 -1
- package/ts_build/tests/unit/services/KnowhowClient.test.d.ts +1 -0
- package/ts_build/tests/unit/services/KnowhowClient.test.js +69 -0
- package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -0
- package/ts_build/tests/unit/services/Mcp.test.d.ts +1 -0
- package/ts_build/tests/unit/services/Mcp.test.js +45 -0
- package/ts_build/tests/unit/services/Mcp.test.js.map +1 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js +78 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js.map +1 -0
- package/ts_build/tests/unit/tunnel.test.d.ts +1 -0
- package/ts_build/tests/unit/tunnel.test.js +47 -0
- package/ts_build/tests/unit/tunnel.test.js.map +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
loadImageAsBase64,
|
|
6
|
+
loadImageAsBase64Definition,
|
|
7
|
+
} from "../../../../src/agents/tools/loadImageAsBase64";
|
|
8
|
+
import { Base64ImageProcessor } from "../../../../src/processors/Base64ImageDetector";
|
|
9
|
+
import { ToolsService } from "../../../../src/services/Tools";
|
|
10
|
+
|
|
11
|
+
describe("loadImageAsBase64 tool", () => {
|
|
12
|
+
it("returns an image data URL with a MIME type detected from its bytes", async () => {
|
|
13
|
+
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "knowhow-image-"));
|
|
14
|
+
const filePath = path.join(directory, "incorrectly-named.jpg");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await fs.promises.writeFile(filePath, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
|
18
|
+
const result = JSON.parse(await loadImageAsBase64(filePath));
|
|
19
|
+
|
|
20
|
+
expect(result).toMatchObject({
|
|
21
|
+
type: "image_url",
|
|
22
|
+
image_url: { detail: "auto" },
|
|
23
|
+
});
|
|
24
|
+
expect(result.image_url.url).toMatch(/^data:image\/png;base64,/);
|
|
25
|
+
} finally {
|
|
26
|
+
await fs.promises.rm(directory, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("can also be registered by the image message processor without duplication", () => {
|
|
31
|
+
const toolsService = new ToolsService();
|
|
32
|
+
toolsService.defineTools(
|
|
33
|
+
[loadImageAsBase64Definition],
|
|
34
|
+
{ loadImageAsBase64 }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
new Base64ImageProcessor(toolsService);
|
|
38
|
+
|
|
39
|
+
expect(
|
|
40
|
+
toolsService.getToolNames().filter((name) => name === "loadImageAsBase64")
|
|
41
|
+
).toHaveLength(1);
|
|
42
|
+
expect(typeof toolsService.getFunction("loadImageAsBase64")).toBe("function");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -14,6 +14,7 @@ jest.mock("../../../src/utils/http", () => ({
|
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
jest.mock("../../../src/auth/keyManager", () => ({
|
|
17
|
+
getDefaultPrivateKeyPath: () => "/keys/default",
|
|
17
18
|
keyPairExists,
|
|
18
19
|
loadKeyPair,
|
|
19
20
|
signMessage,
|
|
@@ -23,11 +24,16 @@ jest.mock("../../../src/auth/browserLogin", () => ({
|
|
|
23
24
|
getCliUserAgent: () => "test-agent",
|
|
24
25
|
}));
|
|
25
26
|
|
|
27
|
+
import fs from "fs";
|
|
28
|
+
import os from "os";
|
|
29
|
+
import path from "path";
|
|
26
30
|
import {
|
|
27
31
|
authenticateWithKey,
|
|
32
|
+
exchangeAvailablePublicKeyJwt,
|
|
28
33
|
exchangePublicKeyJwt,
|
|
29
34
|
hasKeyPair,
|
|
30
35
|
registerPublicKey,
|
|
36
|
+
storeJwt,
|
|
31
37
|
} from "../../../src/auth/keyAuth";
|
|
32
38
|
|
|
33
39
|
describe("keyAuth", () => {
|
|
@@ -69,17 +75,61 @@ describe("keyAuth", () => {
|
|
|
69
75
|
]);
|
|
70
76
|
});
|
|
71
77
|
|
|
78
|
+
it("uses the registered default identity when a configured identity is rejected", async () => {
|
|
79
|
+
post
|
|
80
|
+
.mockRejectedValueOnce({ status: 401 })
|
|
81
|
+
.mockResolvedValueOnce({
|
|
82
|
+
data: {
|
|
83
|
+
challengeId: "challenge_2",
|
|
84
|
+
message: "default key challenge",
|
|
85
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
86
|
+
},
|
|
87
|
+
status: 201,
|
|
88
|
+
})
|
|
89
|
+
.mockResolvedValueOnce({ data: { jwt: "default-key-jwt" }, status: 200 });
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
exchangeAvailablePublicKeyJwt("org-1", "https://work.example", "/keys/stale")
|
|
93
|
+
).resolves.toBe("default-key-jwt");
|
|
94
|
+
|
|
95
|
+
expect(loadKeyPair.mock.calls).toEqual([
|
|
96
|
+
["/keys/stale"],
|
|
97
|
+
["/keys/default"],
|
|
98
|
+
]);
|
|
99
|
+
expect(signMessage).toHaveBeenCalledWith(
|
|
100
|
+
"default key challenge",
|
|
101
|
+
"/keys/default"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
72
105
|
it("falls back when the key is unavailable without signing a challenge", async () => {
|
|
73
|
-
post.
|
|
106
|
+
post.mockRejectedValue({ status: 401 });
|
|
74
107
|
|
|
75
108
|
await expect(
|
|
76
109
|
authenticateWithKey("org-1", "https://work.example", "/keys/work")
|
|
77
110
|
).resolves.toBe(false);
|
|
78
111
|
|
|
79
|
-
expect(post).toHaveBeenCalledTimes(
|
|
112
|
+
expect(post).toHaveBeenCalledTimes(2);
|
|
80
113
|
expect(signMessage).not.toHaveBeenCalled();
|
|
81
114
|
});
|
|
82
115
|
|
|
116
|
+
it("stores a refreshed JWT in the active isolated project", () => {
|
|
117
|
+
const originalCwd = process.cwd();
|
|
118
|
+
const temporaryProject = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-auth-"));
|
|
119
|
+
try {
|
|
120
|
+
process.chdir(temporaryProject);
|
|
121
|
+
|
|
122
|
+
storeJwt("fresh-project-jwt");
|
|
123
|
+
|
|
124
|
+
const jwtPath = path.join(temporaryProject, ".knowhow", ".jwt");
|
|
125
|
+
expect(fs.readFileSync(jwtPath, "utf8")).toBe("fresh-project-jwt");
|
|
126
|
+
expect(fs.statSync(jwtPath).mode & 0o777).toBe(0o600);
|
|
127
|
+
} finally {
|
|
128
|
+
process.chdir(originalCwd);
|
|
129
|
+
fs.rmSync(temporaryProject, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
83
133
|
it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
|
|
84
134
|
post.mockResolvedValueOnce({ data: {}, status: 201 });
|
|
85
135
|
|
package/tests/unit/login.test.ts
CHANGED
|
@@ -26,7 +26,10 @@ jest.mock("../../src/auth/keyAuth", () => ({
|
|
|
26
26
|
registerPublicKey,
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
|
-
jest.mock("../../src/auth/keyManager", () => ({
|
|
29
|
+
jest.mock("../../src/auth/keyManager", () => ({
|
|
30
|
+
getDefaultPrivateKeyPath: () => "/keys/default",
|
|
31
|
+
getOrCreatePublicKey,
|
|
32
|
+
}));
|
|
30
33
|
jest.mock("../../src/config", () => ({ getConfig, updateConfig }));
|
|
31
34
|
jest.mock("../../src/utils", () => ({ ask: jest.fn() }));
|
|
32
35
|
|
|
@@ -88,6 +91,41 @@ describe("login public-key bootstrap", () => {
|
|
|
88
91
|
);
|
|
89
92
|
});
|
|
90
93
|
|
|
94
|
+
it("does not persist a temporary identity path into project configuration", async () => {
|
|
95
|
+
const temporaryIdentity = path.join(os.tmpdir(), "knowhow-login-test-key");
|
|
96
|
+
getConfig.mockResolvedValue({
|
|
97
|
+
orgId: "org-1",
|
|
98
|
+
cliIdentityPath: "/tmp/knowhow-csrf-e2e-key",
|
|
99
|
+
modelProviders: [],
|
|
100
|
+
});
|
|
101
|
+
registerPublicKey.mockResolvedValueOnce(undefined);
|
|
102
|
+
|
|
103
|
+
await expect(login(false, temporaryIdentity)).resolves.toBeUndefined();
|
|
104
|
+
|
|
105
|
+
expect(getOrCreatePublicKey).toHaveBeenCalledWith(temporaryIdentity);
|
|
106
|
+
expect(updateConfig).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(updateConfig.mock.calls[0][0]).toEqual(
|
|
108
|
+
expect.not.objectContaining({
|
|
109
|
+
cliIdentityPath: expect.any(String),
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("persists the default identity selected during browser bootstrap", async () => {
|
|
115
|
+
registerPublicKey.mockResolvedValueOnce(undefined);
|
|
116
|
+
|
|
117
|
+
await expect(login(false)).resolves.toBeUndefined();
|
|
118
|
+
|
|
119
|
+
expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/default");
|
|
120
|
+
expect(updateConfig).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
orgId: "org-1",
|
|
123
|
+
cliIdentityPath: "/keys/default",
|
|
124
|
+
modelProviders: [{ provider: "knowhow" }],
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
91
129
|
it("safely falls back to browser PKCE when the selected key cannot authenticate", async () => {
|
|
92
130
|
hasKeyPair.mockReturnValue(true);
|
|
93
131
|
getConfig.mockResolvedValue({ orgId: "org-1", modelProviders: [] });
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
const mockGet = jest.fn();
|
|
4
|
+
const mockExchangePublicKeyJwt = jest.fn();
|
|
5
|
+
const mockStoreJwt = jest.fn();
|
|
6
|
+
|
|
7
|
+
jest.mock("../../../src/utils/http", () => {
|
|
8
|
+
const actual = jest.requireActual("../../../src/utils/http");
|
|
9
|
+
const mockedHttp = {
|
|
10
|
+
get: (...args: unknown[]) => mockGet(...args),
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
__esModule: true,
|
|
14
|
+
...actual,
|
|
15
|
+
http: mockedHttp,
|
|
16
|
+
default: mockedHttp,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
jest.mock("../../../src/config", () => ({
|
|
21
|
+
getConfigSync: jest.fn().mockReturnValue({
|
|
22
|
+
orgId: "org-1",
|
|
23
|
+
cliIdentityPath: "/tmp/identity",
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock("../../../src/auth/keyManager", () => ({
|
|
28
|
+
keyPairExists: jest.fn().mockReturnValue(true),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock("../../../src/auth/keyAuth", () => ({
|
|
32
|
+
exchangeAvailablePublicKeyJwt: (...args: unknown[]) => mockExchangePublicKeyJwt(...args),
|
|
33
|
+
storeJwt: (...args: unknown[]) => mockStoreJwt(...args),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import { KnowhowSimpleClient } from "../../../src/services/KnowhowClient";
|
|
37
|
+
|
|
38
|
+
describe("KnowhowSimpleClient identity renewal", () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.restoreAllMocks();
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("obtains a JWT from the registered identity before loading models when no JWT exists", async () => {
|
|
45
|
+
jest.spyOn(fs, "existsSync").mockReturnValue(false);
|
|
46
|
+
mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
|
|
47
|
+
mockGet.mockResolvedValueOnce({ data: { data: [] } });
|
|
48
|
+
|
|
49
|
+
const client = new KnowhowSimpleClient("https://api.example.test");
|
|
50
|
+
await client.getModels();
|
|
51
|
+
|
|
52
|
+
expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
|
|
53
|
+
"org-1",
|
|
54
|
+
"https://api.example.test",
|
|
55
|
+
"/tmp/identity"
|
|
56
|
+
);
|
|
57
|
+
expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
|
|
58
|
+
expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("replaces an expired JWT with one signed by the registered identity before loading models", async () => {
|
|
62
|
+
const expiredJwt = [
|
|
63
|
+
Buffer.from('{"alg":"none"}').toString("base64url"),
|
|
64
|
+
Buffer.from('{"exp":0}').toString("base64url"),
|
|
65
|
+
"old-signature",
|
|
66
|
+
].join(".");
|
|
67
|
+
jest.spyOn(fs, "existsSync").mockReturnValue(true);
|
|
68
|
+
jest.spyOn(fs, "readFileSync").mockReturnValue(expiredJwt);
|
|
69
|
+
mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
|
|
70
|
+
mockGet.mockResolvedValueOnce({ data: { data: [] } });
|
|
71
|
+
|
|
72
|
+
const client = new KnowhowSimpleClient("https://api.example.test");
|
|
73
|
+
await client.getModels();
|
|
74
|
+
|
|
75
|
+
expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
|
|
76
|
+
"org-1",
|
|
77
|
+
"https://api.example.test",
|
|
78
|
+
"/tmp/identity"
|
|
79
|
+
);
|
|
80
|
+
expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
|
|
81
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
interface CapturedTransportOptions {
|
|
6
|
+
requestInit?: { headers?: Record<string, string> };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let capturedOptions: CapturedTransportOptions | undefined;
|
|
10
|
+
|
|
11
|
+
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
12
|
+
StreamableHTTPClientTransport: jest.fn().mockImplementation(
|
|
13
|
+
(_url: URL, options: unknown) => {
|
|
14
|
+
capturedOptions = options as CapturedTransportOptions;
|
|
15
|
+
return { close: jest.fn() };
|
|
16
|
+
}
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { McpService } from "../../../src/services/Mcp";
|
|
21
|
+
|
|
22
|
+
describe("McpService JWT file transport", () => {
|
|
23
|
+
let temporaryDirectory: string;
|
|
24
|
+
let tokenFile: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
capturedOptions = undefined;
|
|
28
|
+
temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-mcp-"));
|
|
29
|
+
tokenFile = path.join(temporaryDirectory, ".jwt");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reads the refreshed JWT when the transport is created", () => {
|
|
37
|
+
fs.writeFileSync(tokenFile, "expired-jwt");
|
|
38
|
+
fs.writeFileSync(tokenFile, "fresh-jwt");
|
|
39
|
+
|
|
40
|
+
new McpService().createTransport({
|
|
41
|
+
name: "authenticated-remote",
|
|
42
|
+
url: "https://api.example.test/mcp",
|
|
43
|
+
authorization_token_file: tokenFile,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(capturedOptions).toEqual(
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
requestInit: {
|
|
49
|
+
headers: expect.objectContaining({
|
|
50
|
+
Authorization: "Bearer fresh-jwt",
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { FsSyncedAgentWatcher } from "../../../src/services/watchers/FsSyncer";
|
|
2
|
+
import { WebSyncedAgentWatcher } from "../../../src/services/watchers/RemoteSyncer";
|
|
3
|
+
|
|
4
|
+
const retained = { role: "assistant", content: "before compaction" };
|
|
5
|
+
const oldThread = [
|
|
6
|
+
{ role: "system", content: "instructions" },
|
|
7
|
+
{ role: "user", content: "task" },
|
|
8
|
+
retained,
|
|
9
|
+
];
|
|
10
|
+
const compactedThread = [
|
|
11
|
+
{ role: "system", content: "instructions" },
|
|
12
|
+
{ role: "user", content: "compressed summary" },
|
|
13
|
+
retained,
|
|
14
|
+
{ role: "assistant", content: "after compaction" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function seedCursor(watcher: any): void {
|
|
18
|
+
watcher.taskId = "task-1";
|
|
19
|
+
watcher.agentName = "Developer";
|
|
20
|
+
watcher.lastThreadIndex = 0;
|
|
21
|
+
watcher.lastThreadLength = oldThread.length;
|
|
22
|
+
watcher.lastMessageJson = JSON.stringify(retained);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("synced agent watcher thread rollover", () => {
|
|
26
|
+
it("continues rendering after filesystem agent compaction", async () => {
|
|
27
|
+
const watcher: any = new FsSyncedAgentWatcher();
|
|
28
|
+
seedCursor(watcher);
|
|
29
|
+
watcher.readMetadata = jest.fn().mockResolvedValue({
|
|
30
|
+
threads: [oldThread, compactedThread],
|
|
31
|
+
status: "running",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const messages: string[] = [];
|
|
35
|
+
watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
|
|
36
|
+
messages.push(data.message);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await watcher.onMetadataChanged();
|
|
40
|
+
|
|
41
|
+
expect(messages).toEqual(["after compaction"]);
|
|
42
|
+
expect(watcher.lastThreadIndex).toBe(1);
|
|
43
|
+
expect(watcher.lastThreadLength).toBe(compactedThread.length);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("continues rendering after web agent compaction", async () => {
|
|
47
|
+
const client = {
|
|
48
|
+
getTaskDetails: jest.fn().mockResolvedValue({
|
|
49
|
+
data: {
|
|
50
|
+
threads: [oldThread, compactedThread],
|
|
51
|
+
status: "running",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
const watcher: any = new WebSyncedAgentWatcher(client as any);
|
|
56
|
+
seedCursor(watcher);
|
|
57
|
+
|
|
58
|
+
const messages: string[] = [];
|
|
59
|
+
watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
|
|
60
|
+
messages.push(data.message);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await watcher.onPoll();
|
|
64
|
+
|
|
65
|
+
expect(messages).toEqual(["after compaction"]);
|
|
66
|
+
expect(watcher.lastThreadIndex).toBe(1);
|
|
67
|
+
expect(watcher.lastThreadLength).toBe(compactedThread.length);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("resets a cursor when a thread shrinks without changing index", async () => {
|
|
71
|
+
const watcher: any = new FsSyncedAgentWatcher();
|
|
72
|
+
seedCursor(watcher);
|
|
73
|
+
watcher.lastThreadLength = 10;
|
|
74
|
+
watcher.readMetadata = jest.fn().mockResolvedValue({
|
|
75
|
+
threads: [compactedThread],
|
|
76
|
+
status: "running",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const messages: string[] = [];
|
|
80
|
+
watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
|
|
81
|
+
messages.push(data.message);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await watcher.onMetadataChanged();
|
|
85
|
+
|
|
86
|
+
expect(messages).toEqual(["after compaction"]);
|
|
87
|
+
expect(watcher.lastThreadLength).toBe(compactedThread.length);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { extractTunnelDomain } from "../../src/tunnel";
|
|
2
|
+
|
|
3
|
+
describe("extractTunnelDomain", () => {
|
|
4
|
+
const originalTunnelWorkerDomain = process.env.TUNNEL_WORKER_DOMAIN;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
delete process.env.TUNNEL_WORKER_DOMAIN;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
if (originalTunnelWorkerDomain === undefined) {
|
|
12
|
+
delete process.env.TUNNEL_WORKER_DOMAIN;
|
|
13
|
+
} else {
|
|
14
|
+
process.env.TUNNEL_WORKER_DOMAIN = originalTunnelWorkerDomain;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("uses the local tunnel proxy for a localhost API URL", () => {
|
|
19
|
+
expect(extractTunnelDomain("http://localhost:4000")).toEqual({
|
|
20
|
+
domain: "worker.localhost:4000",
|
|
21
|
+
useHttps: false,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("uses the local tunnel proxy for loopback API URLs", () => {
|
|
26
|
+
expect(extractTunnelDomain("http://127.0.0.1:4000")).toEqual({
|
|
27
|
+
domain: "worker.localhost:4000",
|
|
28
|
+
useHttps: false,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("continues to select dev and production tunnel domains", () => {
|
|
33
|
+
expect(extractTunnelDomain("https://api.dev.knowhow.tyvm.ai")).toEqual({
|
|
34
|
+
domain: "worker.dev.tyvm-apps.com",
|
|
35
|
+
useHttps: true,
|
|
36
|
+
});
|
|
37
|
+
expect(extractTunnelDomain("https://api.knowhow.tyvm.ai")).toEqual({
|
|
38
|
+
domain: "worker.tyvm-apps.com",
|
|
39
|
+
useHttps: true,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("allows an explicit tunnel domain to override localhost detection", () => {
|
|
44
|
+
process.env.TUNNEL_WORKER_DOMAIN = "worker.override.example";
|
|
45
|
+
|
|
46
|
+
expect(extractTunnelDomain("http://localhost:4000")).toEqual({
|
|
47
|
+
domain: "worker.override.example",
|
|
48
|
+
useHttps: false,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -191,6 +191,7 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
191
191
|
role: string;
|
|
192
192
|
content: string;
|
|
193
193
|
} | null;
|
|
194
|
+
extractContentFromMessages(messages: Message[]): string;
|
|
194
195
|
getTaskBreakdown(messages: Message[]): Promise<string>;
|
|
195
196
|
compressMessages(messages: Message[]): Promise<Message[]>;
|
|
196
197
|
}
|
|
@@ -1017,6 +1017,17 @@ let BaseAgent = class BaseAgent {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
return null;
|
|
1019
1019
|
}
|
|
1020
|
+
extractContentFromMessages(messages) {
|
|
1021
|
+
const content = messages
|
|
1022
|
+
.filter((m) => typeof m.content === "string" && m.content.trim())
|
|
1023
|
+
.map((m) => m.content);
|
|
1024
|
+
const finalAnswer = messages
|
|
1025
|
+
.filter((m) => m.tool_calls?.length)
|
|
1026
|
+
.flatMap((t) => t.tool_calls)
|
|
1027
|
+
.filter((t) => t.function.name === "finalAnswer")
|
|
1028
|
+
.map((t) => t.function.arguments);
|
|
1029
|
+
return [...content, ...finalAnswer].join("\n\n");
|
|
1030
|
+
}
|
|
1020
1031
|
async getTaskBreakdown(messages) {
|
|
1021
1032
|
if (this.taskBreakdown) {
|
|
1022
1033
|
return this.taskBreakdown;
|
|
@@ -1039,22 +1050,12 @@ let BaseAgent = class BaseAgent {
|
|
|
1039
1050
|
},
|
|
1040
1051
|
];
|
|
1041
1052
|
const response = await this.createAgentCompletion({
|
|
1042
|
-
model,
|
|
1043
1053
|
messages: taskBreakdownMessages,
|
|
1044
1054
|
tool_choice: "none",
|
|
1045
|
-
long_ttl_cache: this.runTime() > 300_000,
|
|
1046
|
-
...(this.reasoningEffort !== undefined && {
|
|
1047
|
-
reasoning_effort: this.reasoningEffort,
|
|
1048
|
-
}),
|
|
1049
|
-
...(this.summarizeReasoning !== undefined && {
|
|
1050
|
-
reasoning_summary: this.summarizeReasoning,
|
|
1051
|
-
}),
|
|
1052
1055
|
});
|
|
1053
|
-
const breakdownContent = response.choices
|
|
1054
|
-
.map((c) => c.message.content)
|
|
1055
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
1056
|
-
.join("\n\n");
|
|
1056
|
+
const breakdownContent = this.extractContentFromMessages(response.choices?.map((c) => c.message));
|
|
1057
1057
|
if (!breakdownContent) {
|
|
1058
|
+
console.log(JSON.stringify(response.choices, null, 2));
|
|
1058
1059
|
throw new Error("Compaction task-breakdown request returned no text content");
|
|
1059
1060
|
}
|
|
1060
1061
|
this.taskBreakdown = breakdownContent;
|
|
@@ -1063,8 +1064,7 @@ let BaseAgent = class BaseAgent {
|
|
|
1063
1064
|
}
|
|
1064
1065
|
async compressMessages(messages) {
|
|
1065
1066
|
let resumeIndex = messages.length - 1;
|
|
1066
|
-
while (resumeIndex >= 0 &&
|
|
1067
|
-
messages[resumeIndex].role !== "assistant") {
|
|
1067
|
+
while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
|
|
1068
1068
|
resumeIndex--;
|
|
1069
1069
|
}
|
|
1070
1070
|
const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
|
|
@@ -1080,7 +1080,9 @@ let BaseAgent = class BaseAgent {
|
|
|
1080
1080
|
3. Next Steps - what we're about to do next to continue the user's original request.
|
|
1081
1081
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1082
1082
|
|
|
1083
|
-
${this.taskBreakdown
|
|
1083
|
+
${this.taskBreakdown
|
|
1084
|
+
? `Our initial task breakdown: ${this.taskBreakdown}`
|
|
1085
|
+
: ""}
|
|
1084
1086
|
|
|
1085
1087
|
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1086
1088
|
|
|
@@ -1096,21 +1098,24 @@ let BaseAgent = class BaseAgent {
|
|
|
1096
1098
|
messages: compressMessagesPayload,
|
|
1097
1099
|
tool_choice: "none",
|
|
1098
1100
|
});
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
.
|
|
1102
|
-
if (summaries.length === 0) {
|
|
1101
|
+
const summary = this.extractContentFromMessages(response.choices?.map((c) => c.message));
|
|
1102
|
+
if (summary.length === 0) {
|
|
1103
|
+
console.log(JSON.stringify(response.choices, null, 2));
|
|
1103
1104
|
throw new Error("Compaction summary request returned no text content");
|
|
1104
1105
|
}
|
|
1105
|
-
this.summaries.push(
|
|
1106
|
+
this.summaries.push(summary);
|
|
1106
1107
|
const startMessages = [
|
|
1107
1108
|
{
|
|
1108
1109
|
role: "user",
|
|
1109
1110
|
content: `
|
|
1110
|
-
${this.taskBreakdown
|
|
1111
|
+
${this.taskBreakdown
|
|
1112
|
+
? `Initial task breakdown:\n ${this.taskBreakdown}`
|
|
1113
|
+
: "(No task breakdown available — summarize what you know from context above)"}
|
|
1111
1114
|
|
|
1112
1115
|
We have just compressed the conversation to save memory:
|
|
1113
|
-
${
|
|
1116
|
+
${summary.length > 0
|
|
1117
|
+
? JSON.stringify(summary)
|
|
1118
|
+
: "(summary unavailable — please continue from the task breakdown above)"}
|
|
1114
1119
|
|
|
1115
1120
|
Please continue the task from where we left off
|
|
1116
1121
|
`,
|