@tyvm/knowhow 0.0.106 → 0.0.107
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 +43 -4
- package/src/clients/anthropic.ts +8 -2
- package/src/clients/gemini.ts +14 -2
- package/src/clients/http.ts +4 -0
- package/src/clients/openai.ts +12 -1
- package/src/clients/pricing/openai.ts +1 -0
- package/src/clients/types.ts +24 -1
- package/src/clients/xai.ts +11 -1
- package/tests/clients/AIClient.test.ts +1 -1
- package/tests/clients/anthropic.test.ts +202 -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 +30 -4
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +8 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/gemini.js +10 -1
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/http.js +3 -0
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/openai.js +11 -1
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +1 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +11 -1
- package/ts_build/src/clients/xai.js +11 -1
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/tests/clients/AIClient.test.js +1 -1
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
- package/ts_build/tests/clients/anthropic.test.js +159 -0
- package/ts_build/tests/clients/anthropic.test.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -293,6 +293,33 @@ export abstract class BaseAgent implements IAgent {
|
|
|
293
293
|
this.easyFinalAnswer = value;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Detect if the model's response is a termination signal (e.g. "Done", "Complete", "Finished", "finalAnswer")
|
|
298
|
+
* This handles the case where an agent refuses to call finalAnswer and just says a short termination word.
|
|
299
|
+
*/
|
|
300
|
+
protected isTerminationResponse(content: string): boolean {
|
|
301
|
+
const trimmed = content.trim();
|
|
302
|
+
// Short response (≤ 3 words) that matches a termination word/phrase exactly
|
|
303
|
+
const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
|
|
304
|
+
if (wordCount <= 3) {
|
|
305
|
+
const terminationPattern = /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
|
|
306
|
+
if (terminationPattern.test(trimmed)) return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if the first 1-3 words indicate task completion (for longer responses)
|
|
310
|
+
// e.g. "Task complete: ...", "All done.", "No further changes needed.", "Confirmed complete."
|
|
311
|
+
const firstWords = trimmed.split(/\s+/).slice(0, 3).join(" ");
|
|
312
|
+
const firstWordPattern = /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
|
|
313
|
+
if (firstWordPattern.test(firstWords)) return true;
|
|
314
|
+
|
|
315
|
+
// If easyFinalAnswer mode is on, also match response starting with "✅" or numbered confirmation lists
|
|
316
|
+
if (this.easyFinalAnswer) {
|
|
317
|
+
if (trimmed.startsWith("✅") || /^[\d\.\-\*]/.test(trimmed)) return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
296
323
|
getEnabledTools() {
|
|
297
324
|
return this.tools
|
|
298
325
|
.getTools()
|
|
@@ -705,7 +732,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
705
732
|
"warn"
|
|
706
733
|
);
|
|
707
734
|
const error = response as any;
|
|
708
|
-
if ("response" in error && "data" in error.response) {
|
|
735
|
+
if (error != null && "response" in error && "data" in error.response) {
|
|
709
736
|
this.log(
|
|
710
737
|
`Response data: ${JSON.stringify(error.response.data, null, 2)}`,
|
|
711
738
|
"warn"
|
|
@@ -826,6 +853,18 @@ export abstract class BaseAgent implements IAgent {
|
|
|
826
853
|
|
|
827
854
|
// Early exit: not required to call tool
|
|
828
855
|
const firstMessage = response.choices[0].message;
|
|
856
|
+
// Auto-detect termination words: if the model is just saying "Done", "Complete", etc.
|
|
857
|
+
if (
|
|
858
|
+
response.choices.length === 1 &&
|
|
859
|
+
firstMessage.content &&
|
|
860
|
+
this.isTerminationResponse(firstMessage.content)
|
|
861
|
+
) {
|
|
862
|
+
this.log(`Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`);
|
|
863
|
+
this.status = this.eventTypes.done;
|
|
864
|
+
this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
|
|
865
|
+
return firstMessage.content;
|
|
866
|
+
}
|
|
867
|
+
|
|
829
868
|
if (
|
|
830
869
|
response.choices.length === 1 &&
|
|
831
870
|
firstMessage.content &&
|
|
@@ -879,7 +918,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
879
918
|
this.logStatus();
|
|
880
919
|
|
|
881
920
|
const continuation = `<Workflow>
|
|
882
|
-
workflow continues until you call one of ${this.requiredToolNames}.\n
|
|
921
|
+
workflow continues until you call one of ${JSON.stringify(this.requiredToolNames)}.\n
|
|
883
922
|
${statusMessage}
|
|
884
923
|
</Workflow>`;
|
|
885
924
|
|
|
@@ -925,7 +964,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
925
964
|
|
|
926
965
|
this.log(`Agent failed: ${e}`, "error");
|
|
927
966
|
|
|
928
|
-
if ("response" in e && "data" in e.response) {
|
|
967
|
+
if (e != null && typeof e === "object" && "response" in e && "data" in (e as any).response) {
|
|
929
968
|
this.log(
|
|
930
969
|
`Error response data: ${JSON.stringify(e.response.data, null, 2)}`,
|
|
931
970
|
"error"
|
|
@@ -1042,7 +1081,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1042
1081
|
toolCalls: ToolCall[],
|
|
1043
1082
|
response: CompletionResponse
|
|
1044
1083
|
): { role: string; content: string } | null {
|
|
1045
|
-
const outputTokens: number = response?.usage?.
|
|
1084
|
+
const outputTokens: number = response?.usage?.completion_tokens || 0;
|
|
1046
1085
|
const totalArgLength = toolCalls.reduce(
|
|
1047
1086
|
(sum, tc) => sum + (tc.function?.arguments?.length || 0),
|
|
1048
1087
|
0
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -207,7 +207,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
207
207
|
const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
|
|
208
208
|
const claudeMessages: MessageParam[] = messages
|
|
209
209
|
.filter((msg) => msg.role !== "system")
|
|
210
|
-
.filter((msg) => msg.content)
|
|
210
|
+
.filter((msg) => msg.content || msg.role === "tool")
|
|
211
211
|
.map((msg) => {
|
|
212
212
|
if (msg.role === "tool") {
|
|
213
213
|
const toolCall = toolCalls.find((tc) => tc.id === msg.tool_call_id);
|
|
@@ -412,7 +412,13 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
412
412
|
}),
|
|
413
413
|
|
|
414
414
|
model: options.model,
|
|
415
|
-
usage: response.usage
|
|
415
|
+
usage: response.usage ? {
|
|
416
|
+
prompt_tokens: response.usage.input_tokens ?? 0,
|
|
417
|
+
completion_tokens: response.usage.output_tokens ?? 0,
|
|
418
|
+
total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
|
|
419
|
+
cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
|
|
420
|
+
cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
|
|
421
|
+
} : undefined,
|
|
416
422
|
usd_cost: this.calculateCost(options.model, response.usage),
|
|
417
423
|
};
|
|
418
424
|
} catch (err) {
|
package/src/clients/gemini.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "../types";
|
|
27
27
|
import { GeminiTextPricing } from "./pricing";
|
|
28
28
|
import { ContextLimits } from "./contextLimits";
|
|
29
|
-
import { ModelModality } from "./types";
|
|
29
|
+
import { ModelModality, TokenUsage } from "./types";
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
32
|
GenericClient,
|
|
@@ -524,10 +524,22 @@ export class GenericGeminiClient implements GenericClient {
|
|
|
524
524
|
? this.calculateCost(options.model, usage)
|
|
525
525
|
: undefined;
|
|
526
526
|
|
|
527
|
+
// Map cachedContentTokenCount → prompt_tokens_details.cached_tokens so that
|
|
528
|
+
// base.ts can read cache hit tokens via usage.prompt_tokens_details?.cached_tokens
|
|
529
|
+
const cachedTokens = (usage as any)?.cachedContentTokenCount ?? 0;
|
|
530
|
+
const usageWithCache: TokenUsage | undefined = usage
|
|
531
|
+
? ({
|
|
532
|
+
prompt_tokens: (usage as any).promptTokenCount ?? 0,
|
|
533
|
+
completion_tokens: (usage as any).candidatesTokenCount ?? 0,
|
|
534
|
+
total_tokens: (usage as any).totalTokenCount,
|
|
535
|
+
prompt_tokens_details: { cached_tokens: cachedTokens },
|
|
536
|
+
} as TokenUsage)
|
|
537
|
+
: undefined;
|
|
538
|
+
|
|
527
539
|
return {
|
|
528
540
|
choices,
|
|
529
541
|
model: options.model,
|
|
530
|
-
usage,
|
|
542
|
+
usage: usageWithCache,
|
|
531
543
|
usd_cost: usdCost,
|
|
532
544
|
};
|
|
533
545
|
} catch (error) {
|
package/src/clients/http.ts
CHANGED
|
@@ -264,6 +264,10 @@ export class HttpClient implements GenericClient {
|
|
|
264
264
|
prompt_tokens: data.usage.input_tokens,
|
|
265
265
|
completion_tokens: data.usage.output_tokens,
|
|
266
266
|
total_tokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
267
|
+
prompt_tokens_details: {
|
|
268
|
+
cached_tokens:
|
|
269
|
+
data.usage.input_tokens_details?.cached_tokens ?? 0,
|
|
270
|
+
},
|
|
267
271
|
}
|
|
268
272
|
: undefined;
|
|
269
273
|
|
package/src/clients/openai.ts
CHANGED
|
@@ -176,7 +176,14 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
176
176
|
})),
|
|
177
177
|
|
|
178
178
|
model: options.model,
|
|
179
|
-
usage: response.usage
|
|
179
|
+
usage: response.usage ? {
|
|
180
|
+
prompt_tokens: response.usage.prompt_tokens ?? 0,
|
|
181
|
+
completion_tokens: response.usage.completion_tokens ?? 0,
|
|
182
|
+
total_tokens: response.usage.total_tokens,
|
|
183
|
+
prompt_tokens_details: {
|
|
184
|
+
cached_tokens: response.usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
185
|
+
},
|
|
186
|
+
} : undefined,
|
|
180
187
|
usd_cost: usdCost,
|
|
181
188
|
};
|
|
182
189
|
}
|
|
@@ -300,6 +307,10 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
300
307
|
completion_tokens: response.usage.output_tokens,
|
|
301
308
|
total_tokens:
|
|
302
309
|
response.usage.input_tokens + response.usage.output_tokens,
|
|
310
|
+
prompt_tokens_details: {
|
|
311
|
+
cached_tokens:
|
|
312
|
+
response.usage.input_tokens_details?.cached_tokens ?? 0,
|
|
313
|
+
},
|
|
303
314
|
}
|
|
304
315
|
: undefined;
|
|
305
316
|
|
package/src/clients/types.ts
CHANGED
|
@@ -63,13 +63,36 @@ export interface CompletionOptions {
|
|
|
63
63
|
reasoning_effort?: "low" | "medium" | "high";
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Normalised token-usage shape that every client must return.
|
|
68
|
+
* All clients must map their provider-specific field names into this structure
|
|
69
|
+
* so that base.ts can accurately track input/output and cache utilization.
|
|
70
|
+
*/
|
|
71
|
+
export interface TokenUsage {
|
|
72
|
+
/** Total input/prompt tokens consumed */
|
|
73
|
+
prompt_tokens: number;
|
|
74
|
+
/** Total output/completion tokens generated */
|
|
75
|
+
completion_tokens: number;
|
|
76
|
+
/** Convenience total (prompt + completion) */
|
|
77
|
+
total_tokens?: number;
|
|
78
|
+
/** Cache details */
|
|
79
|
+
prompt_tokens_details?: {
|
|
80
|
+
/** Tokens served from the prompt cache (reduces cost) */
|
|
81
|
+
cached_tokens: number;
|
|
82
|
+
};
|
|
83
|
+
/** Anthropic-style cache write tokens */
|
|
84
|
+
cache_creation_input_tokens?: number;
|
|
85
|
+
/** Anthropic-style cache read tokens (alternative field name) */
|
|
86
|
+
cache_read_input_tokens?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
export interface CompletionResponse {
|
|
67
90
|
choices: {
|
|
68
91
|
message: OutputMessage;
|
|
69
92
|
}[];
|
|
70
93
|
|
|
71
94
|
model: string;
|
|
72
|
-
usage:
|
|
95
|
+
usage: TokenUsage | undefined;
|
|
73
96
|
usd_cost?: number;
|
|
74
97
|
}
|
|
75
98
|
|
package/src/clients/xai.ts
CHANGED
|
@@ -99,7 +99,14 @@ export class GenericXAIClient implements GenericClient {
|
|
|
99
99
|
})),
|
|
100
100
|
|
|
101
101
|
model: options.model,
|
|
102
|
-
usage: response.usage
|
|
102
|
+
usage: response.usage ? {
|
|
103
|
+
prompt_tokens: response.usage.prompt_tokens ?? 0,
|
|
104
|
+
completion_tokens: response.usage.completion_tokens ?? 0,
|
|
105
|
+
total_tokens: response.usage.total_tokens,
|
|
106
|
+
prompt_tokens_details: {
|
|
107
|
+
cached_tokens: response.usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
108
|
+
},
|
|
109
|
+
} : undefined,
|
|
103
110
|
usd_cost: usdCost,
|
|
104
111
|
};
|
|
105
112
|
}
|
|
@@ -200,6 +207,9 @@ export class GenericXAIClient implements GenericClient {
|
|
|
200
207
|
prompt_tokens: data.usage.input_tokens,
|
|
201
208
|
completion_tokens: data.usage.output_tokens,
|
|
202
209
|
total_tokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
210
|
+
prompt_tokens_details: {
|
|
211
|
+
cached_tokens: data.usage.input_tokens_details?.cached_tokens ?? 0,
|
|
212
|
+
},
|
|
203
213
|
}
|
|
204
214
|
: undefined;
|
|
205
215
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { GenericAnthropicClient } from "../../src/clients/anthropic";
|
|
2
|
+
|
|
3
|
+
// We only need to test transformMessages, which doesn't require an API key
|
|
4
|
+
function createClient() {
|
|
5
|
+
return new GenericAnthropicClient("fake-key");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("GenericAnthropicClient.transformMessages", () => {
|
|
9
|
+
let client: GenericAnthropicClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
client = createClient();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should handle a simple user message", () => {
|
|
16
|
+
const messages = [
|
|
17
|
+
{ role: "user" as const, content: "Hello" },
|
|
18
|
+
];
|
|
19
|
+
const result = client.transformMessages(messages);
|
|
20
|
+
expect(result).toHaveLength(1);
|
|
21
|
+
expect(result[0].role).toBe("user");
|
|
22
|
+
expect(result[0].content).toBe("Hello");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should filter out system messages", () => {
|
|
26
|
+
const messages = [
|
|
27
|
+
{ role: "system" as const, content: "You are helpful" },
|
|
28
|
+
{ role: "user" as const, content: "Hello" },
|
|
29
|
+
];
|
|
30
|
+
const result = client.transformMessages(messages);
|
|
31
|
+
expect(result).toHaveLength(1);
|
|
32
|
+
expect(result[0].role).toBe("user");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should inject tool_use assistant block when processing tool result", () => {
|
|
36
|
+
// Simulates: assistant responds with tool_call (content: ""), then tool result comes back
|
|
37
|
+
const messages = [
|
|
38
|
+
{ role: "user" as const, content: "Use a tool" },
|
|
39
|
+
{
|
|
40
|
+
role: "assistant" as const,
|
|
41
|
+
content: "",
|
|
42
|
+
tool_calls: [
|
|
43
|
+
{
|
|
44
|
+
id: "toolu_abc123",
|
|
45
|
+
type: "function" as const,
|
|
46
|
+
function: {
|
|
47
|
+
name: "listAvailableTools",
|
|
48
|
+
arguments: "{}",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
role: "tool" as const,
|
|
55
|
+
tool_call_id: "toolu_abc123",
|
|
56
|
+
name: "listAvailableTools",
|
|
57
|
+
content: '{"enabled": ["finalAnswer"], "disabled": []}',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const result = client.transformMessages(messages);
|
|
62
|
+
|
|
63
|
+
// Should have: user msg, assistant tool_use block, user tool_result block
|
|
64
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
65
|
+
|
|
66
|
+
// Find the assistant message with tool_use
|
|
67
|
+
const assistantMsg = result.find(
|
|
68
|
+
(m) =>
|
|
69
|
+
m.role === "assistant" &&
|
|
70
|
+
Array.isArray(m.content) &&
|
|
71
|
+
(m.content as any[]).some((c) => c.type === "tool_use")
|
|
72
|
+
);
|
|
73
|
+
expect(assistantMsg).toBeDefined();
|
|
74
|
+
const toolUseBlock = (assistantMsg!.content as any[]).find(
|
|
75
|
+
(c) => c.type === "tool_use"
|
|
76
|
+
);
|
|
77
|
+
expect(toolUseBlock.id).toBe("toolu_abc123");
|
|
78
|
+
expect(toolUseBlock.name).toBe("listAvailableTools");
|
|
79
|
+
|
|
80
|
+
// Find the user message with tool_result
|
|
81
|
+
const userToolResult = result.find(
|
|
82
|
+
(m) =>
|
|
83
|
+
m.role === "user" &&
|
|
84
|
+
Array.isArray(m.content) &&
|
|
85
|
+
(m.content as any[]).some((c) => c.type === "tool_result")
|
|
86
|
+
);
|
|
87
|
+
expect(userToolResult).toBeDefined();
|
|
88
|
+
const toolResultBlock = (userToolResult!.content as any[]).find(
|
|
89
|
+
(c) => c.type === "tool_result"
|
|
90
|
+
);
|
|
91
|
+
expect(toolResultBlock.tool_use_id).toBe("toolu_abc123");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should not have undefined tool_use_id when assistant message has empty content with tool_calls", () => {
|
|
95
|
+
// This is the failing scenario: assistant has content: "" (falsy) but has tool_calls
|
|
96
|
+
const messages = [
|
|
97
|
+
{ role: "user" as const, content: "Use a tool" },
|
|
98
|
+
{
|
|
99
|
+
role: "assistant" as const,
|
|
100
|
+
content: "", // empty string - would be filtered by `msg.content` check
|
|
101
|
+
tool_calls: [
|
|
102
|
+
{
|
|
103
|
+
id: "toolu_abc123",
|
|
104
|
+
type: "function" as const,
|
|
105
|
+
function: {
|
|
106
|
+
name: "listAvailableTools",
|
|
107
|
+
arguments: "{}",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
role: "tool" as const,
|
|
114
|
+
tool_call_id: "toolu_abc123",
|
|
115
|
+
name: "listAvailableTools",
|
|
116
|
+
content: '{"enabled": ["finalAnswer"]}',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const result = client.transformMessages(messages);
|
|
121
|
+
|
|
122
|
+
// Find the user message with tool_result - tool_use_id must NOT be undefined
|
|
123
|
+
const userToolResult = result.find(
|
|
124
|
+
(m) =>
|
|
125
|
+
m.role === "user" &&
|
|
126
|
+
Array.isArray(m.content) &&
|
|
127
|
+
(m.content as any[]).some((c) => c.type === "tool_result")
|
|
128
|
+
);
|
|
129
|
+
expect(userToolResult).toBeDefined();
|
|
130
|
+
const toolResultBlock = (userToolResult!.content as any[]).find(
|
|
131
|
+
(c) => c.type === "tool_result"
|
|
132
|
+
);
|
|
133
|
+
// This should be "toolu_abc123", NOT undefined
|
|
134
|
+
expect(toolResultBlock.tool_use_id).toBe("toolu_abc123");
|
|
135
|
+
expect(toolResultBlock.tool_use_id).not.toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle multiple sequential tool calls", () => {
|
|
139
|
+
const messages = [
|
|
140
|
+
{ role: "user" as const, content: "Do two things" },
|
|
141
|
+
{
|
|
142
|
+
role: "assistant" as const,
|
|
143
|
+
content: "",
|
|
144
|
+
tool_calls: [
|
|
145
|
+
{
|
|
146
|
+
id: "toolu_111",
|
|
147
|
+
type: "function" as const,
|
|
148
|
+
function: { name: "toolOne", arguments: "{}" },
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
role: "tool" as const,
|
|
154
|
+
tool_call_id: "toolu_111",
|
|
155
|
+
name: "toolOne",
|
|
156
|
+
content: "result one",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
role: "assistant" as const,
|
|
160
|
+
content: "",
|
|
161
|
+
tool_calls: [
|
|
162
|
+
{
|
|
163
|
+
id: "toolu_222",
|
|
164
|
+
type: "function" as const,
|
|
165
|
+
function: { name: "toolTwo", arguments: "{}" },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
role: "tool" as const,
|
|
171
|
+
tool_call_id: "toolu_222",
|
|
172
|
+
name: "toolTwo",
|
|
173
|
+
content: "result two",
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const result = client.transformMessages(messages);
|
|
178
|
+
|
|
179
|
+
// Both tool results should have correct tool_use_ids
|
|
180
|
+
const toolResults = result
|
|
181
|
+
.filter((m) => m.role === "user" && Array.isArray(m.content))
|
|
182
|
+
.flatMap((m) => (m.content as any[]).filter((c) => c.type === "tool_result"));
|
|
183
|
+
|
|
184
|
+
expect(toolResults).toHaveLength(2);
|
|
185
|
+
const ids = toolResults.map((r) => r.tool_use_id);
|
|
186
|
+
expect(ids).toContain("toolu_111");
|
|
187
|
+
expect(ids).toContain("toolu_222");
|
|
188
|
+
expect(ids).not.toContain(undefined);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should not crash when response is undefined (Cannot use in operator bug)", () => {
|
|
192
|
+
// Test that the base agent undefined response check doesn't throw
|
|
193
|
+
// This tests the guard we added to base.ts
|
|
194
|
+
const undefinedLike = undefined as any;
|
|
195
|
+
// Should not throw "Cannot use 'in' operator to search for 'response' in undefined"
|
|
196
|
+
expect(() => {
|
|
197
|
+
if (undefinedLike != null && "response" in undefinedLike) {
|
|
198
|
+
// This should not be reached
|
|
199
|
+
}
|
|
200
|
+
}).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -96,6 +96,7 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
96
96
|
getClient(): GenericClient;
|
|
97
97
|
setClient(client: GenericClient): void;
|
|
98
98
|
setEasyFinalAnswer(value: boolean): void;
|
|
99
|
+
protected isTerminationResponse(content: string): boolean;
|
|
99
100
|
getEnabledTools(): Tool[];
|
|
100
101
|
getEnabledToolNames(): string[];
|
|
101
102
|
disableTool(toolName: string): void;
|
|
@@ -197,6 +197,24 @@ class BaseAgent {
|
|
|
197
197
|
setEasyFinalAnswer(value) {
|
|
198
198
|
this.easyFinalAnswer = value;
|
|
199
199
|
}
|
|
200
|
+
isTerminationResponse(content) {
|
|
201
|
+
const trimmed = content.trim();
|
|
202
|
+
const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
|
|
203
|
+
if (wordCount <= 3) {
|
|
204
|
+
const terminationPattern = /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
|
|
205
|
+
if (terminationPattern.test(trimmed))
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const firstWords = trimmed.split(/\s+/).slice(0, 3).join(" ");
|
|
209
|
+
const firstWordPattern = /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
|
|
210
|
+
if (firstWordPattern.test(firstWords))
|
|
211
|
+
return true;
|
|
212
|
+
if (this.easyFinalAnswer) {
|
|
213
|
+
if (trimmed.startsWith("✅") || /^[\d\.\-\*]/.test(trimmed))
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
200
218
|
getEnabledTools() {
|
|
201
219
|
return this.tools
|
|
202
220
|
.getTools()
|
|
@@ -476,7 +494,7 @@ class BaseAgent {
|
|
|
476
494
|
if (response?.usd_cost === undefined) {
|
|
477
495
|
this.log(`Response cost is undefined: ${JSON.stringify(response, null, 2)}`, "warn");
|
|
478
496
|
const error = response;
|
|
479
|
-
if ("response" in error && "data" in error.response) {
|
|
497
|
+
if (error != null && "response" in error && "data" in error.response) {
|
|
480
498
|
this.log(`Response data: ${JSON.stringify(error.response.data, null, 2)}`, "warn");
|
|
481
499
|
}
|
|
482
500
|
if (!response?.choices) {
|
|
@@ -541,6 +559,14 @@ class BaseAgent {
|
|
|
541
559
|
messages = await this.messageProcessor.processMessages(messages, "post_tools");
|
|
542
560
|
}
|
|
543
561
|
const firstMessage = response.choices[0].message;
|
|
562
|
+
if (response.choices.length === 1 &&
|
|
563
|
+
firstMessage.content &&
|
|
564
|
+
this.isTerminationResponse(firstMessage.content)) {
|
|
565
|
+
this.log(`Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`);
|
|
566
|
+
this.status = this.eventTypes.done;
|
|
567
|
+
this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
|
|
568
|
+
return firstMessage.content;
|
|
569
|
+
}
|
|
544
570
|
if (response.choices.length === 1 &&
|
|
545
571
|
firstMessage.content &&
|
|
546
572
|
this.easyFinalAnswer) {
|
|
@@ -573,7 +599,7 @@ class BaseAgent {
|
|
|
573
599
|
const statusMessage = this.getStatusMessage();
|
|
574
600
|
this.logStatus();
|
|
575
601
|
const continuation = `<Workflow>
|
|
576
|
-
workflow continues until you call one of ${this.requiredToolNames}.\n
|
|
602
|
+
workflow continues until you call one of ${JSON.stringify(this.requiredToolNames)}.\n
|
|
577
603
|
${statusMessage}
|
|
578
604
|
</Workflow>`;
|
|
579
605
|
messages.push({
|
|
@@ -607,7 +633,7 @@ class BaseAgent {
|
|
|
607
633
|
return this.call(userInput, _messages, retryCount + 1);
|
|
608
634
|
}
|
|
609
635
|
this.log(`Agent failed: ${e}`, "error");
|
|
610
|
-
if ("response" in e && "data" in e.response) {
|
|
636
|
+
if (e != null && typeof e === "object" && "response" in e && "data" in e.response) {
|
|
611
637
|
this.log(`Error response data: ${JSON.stringify(e.response.data, null, 2)}`, "error");
|
|
612
638
|
}
|
|
613
639
|
this.agentEvents.emit(this.eventTypes.done, e.message);
|
|
@@ -688,7 +714,7 @@ class BaseAgent {
|
|
|
688
714
|
return JSON.stringify(messages).split(" ").length;
|
|
689
715
|
}
|
|
690
716
|
detectTruncatedToolCalls(toolCalls, response) {
|
|
691
|
-
const outputTokens = response?.usage?.
|
|
717
|
+
const outputTokens = response?.usage?.completion_tokens || 0;
|
|
692
718
|
const totalArgLength = toolCalls.reduce((sum, tc) => sum + (tc.function?.arguments?.length || 0), 0);
|
|
693
719
|
const expectedArgChars = outputTokens * 4;
|
|
694
720
|
const suspiciouslySmallArgs = outputTokens > 1000 && totalArgLength < expectedArgChars * 0.1;
|