@tyvm/knowhow 0.0.153 → 0.0.155
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 +72 -28
- package/src/agents/base/prompt.ts +3 -2
- package/src/agents/tools/list.ts +31 -28
- package/src/auth/keyAuth.ts +35 -3
- package/src/chat/modules/AgentModule.ts +98 -42
- package/src/clients/anthropic.ts +3 -1
- package/src/clients/contextLimits.ts +3 -1
- package/src/clients/gemini.ts +16 -4
- package/src/clients/http.ts +8 -5
- package/src/clients/index.ts +7 -4
- package/src/clients/knowhowMcp.ts +2 -2
- package/src/clients/openai.ts +44 -8
- package/src/clients/pricing/google.ts +11 -2
- package/src/clients/pricing/xai.ts +5 -1
- package/src/clients/xai.ts +6 -3
- 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 +18 -12
- package/src/worker.ts +62 -12
- package/tests/clients/reasoningEffort.test.ts +62 -1
- package/tests/unit/auth/keyAuth.test.ts +31 -2
- package/tests/unit/services/KnowhowClient.test.ts +84 -0
- package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +2 -1
- package/ts_build/src/agents/base/base.js +39 -26
- 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/list.js +0 -26
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/auth/keyAuth.d.ts +1 -0
- package/ts_build/src/auth/keyAuth.js +19 -2
- 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/anthropic.js +3 -1
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +2 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/gemini.js +14 -3
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/http.js +8 -5
- package/ts_build/src/clients/http.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/knowhowMcp.js +2 -2
- package/ts_build/src/clients/knowhowMcp.js.map +1 -1
- package/ts_build/src/clients/openai.js +36 -8
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/google.d.ts +1 -0
- package/ts_build/src/clients/pricing/google.js +7 -0
- package/ts_build/src/clients/pricing/google.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +1 -0
- package/ts_build/src/clients/pricing/xai.js +4 -0
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/clients/xai.js +6 -3
- package/ts_build/src/clients/xai.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 +11 -8
- package/ts_build/src/tunnel.js.map +1 -1
- package/ts_build/src/types.d.ts +2 -0
- 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 +49 -1
- package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
- package/ts_build/tests/unit/auth/keyAuth.test.js +22 -2
- package/ts_build/tests/unit/auth/keyAuth.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/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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
GoogleThinkingBudgetModels,
|
|
12
12
|
GoogleThinkingLevelModels,
|
|
13
|
+
Models,
|
|
13
14
|
} from "../../src/types";
|
|
14
15
|
import { GenericXAIClient } from "../../src/clients/xai";
|
|
15
16
|
|
|
@@ -44,15 +45,75 @@ describe("reasoning effort values", () => {
|
|
|
44
45
|
expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
it("explicitly disables reasoning in Responses API requests", async () => {
|
|
49
|
+
const client = new GenericOpenAiClient("fake-key");
|
|
50
|
+
const create = jest.fn().mockResolvedValue({
|
|
51
|
+
id: "resp_disabled",
|
|
52
|
+
status: "completed",
|
|
53
|
+
output: [{ type: "message", content: [{ type: "output_text", text: "summary" }] }],
|
|
54
|
+
usage: { input_tokens: 1, output_tokens: 1 },
|
|
55
|
+
});
|
|
56
|
+
(client.client as any).responses.create = create;
|
|
57
|
+
|
|
58
|
+
await client.createChatResponse({
|
|
59
|
+
model: Models.openai.GPT_56_Sol,
|
|
60
|
+
messages: [{ role: "user", content: "summarize" }],
|
|
61
|
+
reasoning_effort: "none",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(create).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({ reasoning: { effort: "none" } })
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("diagnoses empty Responses output but not valid tool-only output", async () => {
|
|
70
|
+
const client = new GenericOpenAiClient("fake-key");
|
|
71
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
72
|
+
const create = jest.fn()
|
|
73
|
+
.mockResolvedValueOnce({
|
|
74
|
+
id: "resp_empty",
|
|
75
|
+
status: "incomplete",
|
|
76
|
+
incomplete_details: { reason: "max_output_tokens" },
|
|
77
|
+
output: [{ type: "reasoning", summary: [] }],
|
|
78
|
+
usage: { input_tokens: 2, output_tokens: 3 },
|
|
79
|
+
})
|
|
80
|
+
.mockResolvedValueOnce({
|
|
81
|
+
id: "resp_tool",
|
|
82
|
+
status: "completed",
|
|
83
|
+
output: [{
|
|
84
|
+
type: "function_call", call_id: "call_1", name: "readFile", arguments: "{}",
|
|
85
|
+
}],
|
|
86
|
+
usage: { input_tokens: 2, output_tokens: 3 },
|
|
87
|
+
});
|
|
88
|
+
(client.client as any).responses.create = create;
|
|
89
|
+
const request = {
|
|
90
|
+
model: Models.openai.GPT_56_Sol,
|
|
91
|
+
messages: [{ role: "user" as const, content: "continue" }],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
await client.createChatResponse(request);
|
|
95
|
+
expect(warn).toHaveBeenCalledWith(
|
|
96
|
+
"[Responses API] Response contained no text or tool calls",
|
|
97
|
+
expect.objectContaining({ responseId: "resp_empty", status: "incomplete" })
|
|
98
|
+
);
|
|
99
|
+
warn.mockClear();
|
|
100
|
+
await client.createChatResponse(request);
|
|
101
|
+
expect(warn).not.toHaveBeenCalled();
|
|
102
|
+
warn.mockRestore();
|
|
103
|
+
});
|
|
104
|
+
|
|
47
105
|
it("maps all values to valid Gemini thinking levels and budgets", () => {
|
|
48
106
|
const client = new GenericGeminiClient("fake-key");
|
|
49
|
-
const levelModel = GoogleThinkingLevelModels
|
|
107
|
+
const levelModel = GoogleThinkingLevelModels.find(
|
|
108
|
+
(model) => model !== Models.google.Gemini_37_Flash
|
|
109
|
+
)!;
|
|
50
110
|
const budgetModel = GoogleThinkingBudgetModels[0];
|
|
51
111
|
|
|
52
112
|
expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
|
|
53
113
|
expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
|
|
54
114
|
expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
|
|
55
115
|
expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
|
|
116
|
+
expect(client.buildThinkingConfig(options(Models.google.Gemini_37_Flash, "minimal"))).toEqual({ thinkingLevel: "low" });
|
|
56
117
|
expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
|
|
57
118
|
expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
|
|
58
119
|
expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
|
|
@@ -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,
|
|
@@ -25,6 +26,7 @@ jest.mock("../../../src/auth/browserLogin", () => ({
|
|
|
25
26
|
|
|
26
27
|
import {
|
|
27
28
|
authenticateWithKey,
|
|
29
|
+
exchangeAvailablePublicKeyJwt,
|
|
28
30
|
exchangePublicKeyJwt,
|
|
29
31
|
hasKeyPair,
|
|
30
32
|
registerPublicKey,
|
|
@@ -69,14 +71,41 @@ describe("keyAuth", () => {
|
|
|
69
71
|
]);
|
|
70
72
|
});
|
|
71
73
|
|
|
74
|
+
it("uses the registered default identity when a configured identity is rejected", async () => {
|
|
75
|
+
post
|
|
76
|
+
.mockRejectedValueOnce({ status: 401 })
|
|
77
|
+
.mockResolvedValueOnce({
|
|
78
|
+
data: {
|
|
79
|
+
challengeId: "challenge_2",
|
|
80
|
+
message: "default key challenge",
|
|
81
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
82
|
+
},
|
|
83
|
+
status: 201,
|
|
84
|
+
})
|
|
85
|
+
.mockResolvedValueOnce({ data: { jwt: "default-key-jwt" }, status: 200 });
|
|
86
|
+
|
|
87
|
+
await expect(
|
|
88
|
+
exchangeAvailablePublicKeyJwt("org-1", "https://work.example", "/keys/stale")
|
|
89
|
+
).resolves.toBe("default-key-jwt");
|
|
90
|
+
|
|
91
|
+
expect(loadKeyPair.mock.calls).toEqual([
|
|
92
|
+
["/keys/stale"],
|
|
93
|
+
["/keys/default"],
|
|
94
|
+
]);
|
|
95
|
+
expect(signMessage).toHaveBeenCalledWith(
|
|
96
|
+
"default key challenge",
|
|
97
|
+
"/keys/default"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
72
101
|
it("falls back when the key is unavailable without signing a challenge", async () => {
|
|
73
|
-
post.
|
|
102
|
+
post.mockRejectedValue({ status: 401 });
|
|
74
103
|
|
|
75
104
|
await expect(
|
|
76
105
|
authenticateWithKey("org-1", "https://work.example", "/keys/work")
|
|
77
106
|
).resolves.toBe(false);
|
|
78
107
|
|
|
79
|
-
expect(post).toHaveBeenCalledTimes(
|
|
108
|
+
expect(post).toHaveBeenCalledTimes(2);
|
|
80
109
|
expect(signMessage).not.toHaveBeenCalled();
|
|
81
110
|
});
|
|
82
111
|
|
|
@@ -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,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
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -187,10 +187,11 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
187
187
|
stopObserving(id?: string, reason?: string): string;
|
|
188
188
|
getMessagesLength(messages: Message[]): number;
|
|
189
189
|
getContextTokenCount(messages: Message[]): number;
|
|
190
|
-
detectTruncatedToolCalls(toolCalls: ToolCall[], response: CompletionResponse): {
|
|
190
|
+
detectTruncatedToolCalls(toolCalls: ToolCall[], response: CompletionResponse, allToolCalls?: ToolCall[]): {
|
|
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
|
}
|
|
@@ -691,6 +691,7 @@ let BaseAgent = class BaseAgent {
|
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
693
|
this.log("agent response cost: " + response?.usd_cost);
|
|
694
|
+
const allResponseToolCalls = response.choices.flatMap((c) => c.message.tool_calls ?? []);
|
|
694
695
|
for (const choice of response.choices) {
|
|
695
696
|
messages.push(choice.message);
|
|
696
697
|
messages = await this.messageProcessor.processMessages(messages, "post_call");
|
|
@@ -700,7 +701,7 @@ let BaseAgent = class BaseAgent {
|
|
|
700
701
|
messages = await this.messageProcessor.processMessages(messages, "pre_tools");
|
|
701
702
|
const toolCalls = messages[messages.length - 1].tool_calls;
|
|
702
703
|
this.updateCurrentThread(messages);
|
|
703
|
-
const truncationWarning = this.detectTruncatedToolCalls(toolCalls, response);
|
|
704
|
+
const truncationWarning = this.detectTruncatedToolCalls(toolCalls, response, allResponseToolCalls);
|
|
704
705
|
if (truncationWarning) {
|
|
705
706
|
messages.push(truncationWarning);
|
|
706
707
|
this.updateCurrentThread(messages);
|
|
@@ -984,12 +985,13 @@ let BaseAgent = class BaseAgent {
|
|
|
984
985
|
}
|
|
985
986
|
return this.getMessagesLength(messages);
|
|
986
987
|
}
|
|
987
|
-
detectTruncatedToolCalls(toolCalls, response) {
|
|
988
|
+
detectTruncatedToolCalls(toolCalls, response, allToolCalls) {
|
|
988
989
|
const rawOutputTokens = response?.usage?.completion_tokens || 0;
|
|
989
990
|
const thinkingTokens = (response?.usage?.output_tokens_details?.thinking_tokens ?? 0) +
|
|
990
991
|
(response?.usage?.output_tokens_details?.reasoning_tokens ?? 0);
|
|
991
992
|
const outputTokens = Math.max(0, rawOutputTokens - thinkingTokens);
|
|
992
|
-
const
|
|
993
|
+
const callsForSizeCheck = allToolCalls ?? toolCalls;
|
|
994
|
+
const totalArgLength = callsForSizeCheck.reduce((sum, tc) => sum + (tc.function?.arguments?.length || 0), 0);
|
|
993
995
|
const expectedArgChars = outputTokens * 4;
|
|
994
996
|
const suspiciouslySmallArgs = outputTokens > 1000 && totalArgLength < expectedArgChars * 0.1;
|
|
995
997
|
for (const toolCall of toolCalls) {
|
|
@@ -1015,6 +1017,17 @@ let BaseAgent = class BaseAgent {
|
|
|
1015
1017
|
}
|
|
1016
1018
|
return null;
|
|
1017
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
|
+
}
|
|
1018
1031
|
async getTaskBreakdown(messages) {
|
|
1019
1032
|
if (this.taskBreakdown) {
|
|
1020
1033
|
return this.taskBreakdown;
|
|
@@ -1037,29 +1050,21 @@ let BaseAgent = class BaseAgent {
|
|
|
1037
1050
|
},
|
|
1038
1051
|
];
|
|
1039
1052
|
const response = await this.createAgentCompletion({
|
|
1040
|
-
model,
|
|
1041
1053
|
messages: taskBreakdownMessages,
|
|
1042
1054
|
tool_choice: "none",
|
|
1043
|
-
long_ttl_cache: this.runTime() > 300_000,
|
|
1044
|
-
...(this.reasoningEffort !== undefined && {
|
|
1045
|
-
reasoning_effort: this.reasoningEffort,
|
|
1046
|
-
}),
|
|
1047
|
-
...(this.summarizeReasoning !== undefined && {
|
|
1048
|
-
reasoning_summary: this.summarizeReasoning,
|
|
1049
|
-
}),
|
|
1050
1055
|
});
|
|
1051
|
-
const breakdownContent = response.choices
|
|
1052
|
-
|
|
1053
|
-
.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
+
const breakdownContent = this.extractContentFromMessages(response.choices?.map((c) => c.message));
|
|
1057
|
+
if (!breakdownContent) {
|
|
1058
|
+
console.log(JSON.stringify(response.choices, null, 2));
|
|
1059
|
+
throw new Error("Compaction task-breakdown request returned no text content");
|
|
1060
|
+
}
|
|
1061
|
+
this.taskBreakdown = breakdownContent;
|
|
1056
1062
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1057
|
-
return this.taskBreakdown
|
|
1063
|
+
return this.taskBreakdown;
|
|
1058
1064
|
}
|
|
1059
1065
|
async compressMessages(messages) {
|
|
1060
1066
|
let resumeIndex = messages.length - 1;
|
|
1061
|
-
while (resumeIndex >= 0 &&
|
|
1062
|
-
messages[resumeIndex].role !== "assistant") {
|
|
1067
|
+
while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
|
|
1063
1068
|
resumeIndex--;
|
|
1064
1069
|
}
|
|
1065
1070
|
const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
|
|
@@ -1075,7 +1080,9 @@ let BaseAgent = class BaseAgent {
|
|
|
1075
1080
|
3. Next Steps - what we're about to do next to continue the user's original request.
|
|
1076
1081
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1077
1082
|
|
|
1078
|
-
${this.taskBreakdown
|
|
1083
|
+
${this.taskBreakdown
|
|
1084
|
+
? `Our initial task breakdown: ${this.taskBreakdown}`
|
|
1085
|
+
: ""}
|
|
1079
1086
|
|
|
1080
1087
|
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1081
1088
|
|
|
@@ -1091,18 +1098,24 @@ let BaseAgent = class BaseAgent {
|
|
|
1091
1098
|
messages: compressMessagesPayload,
|
|
1092
1099
|
tool_choice: "none",
|
|
1093
1100
|
});
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
.
|
|
1097
|
-
|
|
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));
|
|
1104
|
+
throw new Error("Compaction summary request returned no text content");
|
|
1105
|
+
}
|
|
1106
|
+
this.summaries.push(summary);
|
|
1098
1107
|
const startMessages = [
|
|
1099
1108
|
{
|
|
1100
1109
|
role: "user",
|
|
1101
1110
|
content: `
|
|
1102
|
-
${this.taskBreakdown
|
|
1111
|
+
${this.taskBreakdown
|
|
1112
|
+
? `Initial task breakdown:\n ${this.taskBreakdown}`
|
|
1113
|
+
: "(No task breakdown available — summarize what you know from context above)"}
|
|
1103
1114
|
|
|
1104
1115
|
We have just compressed the conversation to save memory:
|
|
1105
|
-
${
|
|
1116
|
+
${summary.length > 0
|
|
1117
|
+
? JSON.stringify(summary)
|
|
1118
|
+
: "(summary unavailable — please continue from the task breakdown above)"}
|
|
1106
1119
|
|
|
1107
1120
|
Please continue the task from where we left off
|
|
1108
1121
|
`,
|