@clinebot/agents 0.0.0 → 0.0.3
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/dist/agent.d.ts +4 -0
- package/dist/index.browser.js +20 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -15
- package/dist/index.node.js +18 -15
- package/dist/runtime/turn-processor.d.ts +1 -0
- package/dist/teams/multi-agent.d.ts +6 -0
- package/dist/teams/spawn-agent-tool.d.ts +10 -6
- package/dist/types.d.ts +66 -1
- package/package.json +7 -7
- package/src/agent.test.ts +186 -0
- package/src/agent.ts +255 -8
- package/src/hooks/engine.test.ts +1 -0
- package/src/index.ts +2 -0
- package/src/runtime/tool-orchestrator.test.ts +53 -0
- package/src/runtime/tool-orchestrator.ts +12 -1
- package/src/runtime/turn-processor.ts +56 -5
- package/src/teams/multi-agent.ts +96 -15
- package/src/teams/spawn-agent-tool.test.ts +92 -0
- package/src/teams/spawn-agent-tool.ts +65 -31
- package/src/teams/team-tools.test.ts +18 -0
- package/src/teams/team-tools.ts +12 -4
- package/src/types.ts +90 -2
package/src/agent.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { providers } from "@clinebot/llms";
|
|
8
|
+
import { nanoid } from "nanoid";
|
|
8
9
|
import { buildInitialUserContent } from "./agent-input.js";
|
|
9
10
|
import {
|
|
10
11
|
type ContributionRegistry,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
AgentResult,
|
|
27
28
|
AgentUsage,
|
|
28
29
|
BasicLogger,
|
|
30
|
+
ConsecutiveMistakeLimitDecision,
|
|
29
31
|
PendingToolCall,
|
|
30
32
|
Tool,
|
|
31
33
|
ToolApprovalResult,
|
|
@@ -37,6 +39,36 @@ import type {
|
|
|
37
39
|
const DEFAULT_REMINDER_TEXT =
|
|
38
40
|
"REMINDER: If you have gathered enough information to answer the user's question, please provide your final answer now without using any more tools.";
|
|
39
41
|
|
|
42
|
+
function isNonRecoverableApiError(error: Error): boolean {
|
|
43
|
+
const message = error.message.toLowerCase();
|
|
44
|
+
|
|
45
|
+
const nonRecoverableStatusCodes = [
|
|
46
|
+
400, 401, 403, 404, 405, 406, 409, 410, 429,
|
|
47
|
+
];
|
|
48
|
+
if (
|
|
49
|
+
nonRecoverableStatusCodes.some((code) =>
|
|
50
|
+
new RegExp(`(?:\\b|\\"code\\"\\s*:\\s*)${code}(?:\\b|\\s)`).test(message),
|
|
51
|
+
)
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
[
|
|
58
|
+
"not found",
|
|
59
|
+
"unsupported for",
|
|
60
|
+
"invalid api key",
|
|
61
|
+
"authentication",
|
|
62
|
+
"unauthorized",
|
|
63
|
+
"forbidden",
|
|
64
|
+
].some((s) => message.includes(s))
|
|
65
|
+
) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
function resolveKnownModelsFromConfig(
|
|
41
73
|
config: AgentConfig,
|
|
42
74
|
): Record<string, providers.ModelInfo> | undefined {
|
|
@@ -71,6 +103,7 @@ export class Agent {
|
|
|
71
103
|
| "tools"
|
|
72
104
|
| "maxParallelToolCalls"
|
|
73
105
|
| "apiTimeoutMs"
|
|
106
|
+
| "maxConsecutiveMistakes"
|
|
74
107
|
| "maxTokensPerTurn"
|
|
75
108
|
| "reminderAfterIterations"
|
|
76
109
|
| "reminderText"
|
|
@@ -102,15 +135,16 @@ export class Agent {
|
|
|
102
135
|
maxIterations: config.maxIterations,
|
|
103
136
|
maxParallelToolCalls: config.maxParallelToolCalls ?? 8,
|
|
104
137
|
apiTimeoutMs: config.apiTimeoutMs ?? 120000,
|
|
138
|
+
maxConsecutiveMistakes: config.maxConsecutiveMistakes ?? 0,
|
|
105
139
|
maxTokensPerTurn: config.maxTokensPerTurn ?? 8192,
|
|
106
|
-
reminderAfterIterations: config.reminderAfterIterations ??
|
|
140
|
+
reminderAfterIterations: config.reminderAfterIterations ?? 0,
|
|
107
141
|
reminderText: config.reminderText ?? DEFAULT_REMINDER_TEXT,
|
|
108
142
|
hookErrorMode: config.hookErrorMode ?? "ignore",
|
|
109
143
|
extensions: config.extensions ?? [],
|
|
110
144
|
toolPolicies: config.toolPolicies ?? {},
|
|
111
145
|
};
|
|
112
146
|
|
|
113
|
-
this.agentId = `agent_${Date.now()}_${
|
|
147
|
+
this.agentId = `agent_${Date.now()}_${nanoid(6)}`;
|
|
114
148
|
this.parentAgentId = config.parentAgentId ?? null;
|
|
115
149
|
this.conversationStore = new ConversationStore(
|
|
116
150
|
config.initialMessages ?? [],
|
|
@@ -410,6 +444,7 @@ export class Agent {
|
|
|
410
444
|
cacheWriteTokens: 0,
|
|
411
445
|
totalCost: undefined,
|
|
412
446
|
};
|
|
447
|
+
let consecutiveMistakes = 0;
|
|
413
448
|
|
|
414
449
|
try {
|
|
415
450
|
if (!this.conversationStore.isSessionStarted()) {
|
|
@@ -536,12 +571,49 @@ export class Agent {
|
|
|
536
571
|
);
|
|
537
572
|
}
|
|
538
573
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
574
|
+
let turn: Awaited<ReturnType<TurnProcessor["processTurn"]>>["turn"];
|
|
575
|
+
let assistantMessage:
|
|
576
|
+
| Awaited<
|
|
577
|
+
ReturnType<TurnProcessor["processTurn"]>
|
|
578
|
+
>["assistantMessage"]
|
|
579
|
+
| undefined;
|
|
580
|
+
try {
|
|
581
|
+
({ turn, assistantMessage } = await this.turnProcessor.processTurn(
|
|
582
|
+
this.conversationStore.getMessages(),
|
|
583
|
+
turnSystemPrompt,
|
|
584
|
+
this.config.tools,
|
|
585
|
+
abortSignal,
|
|
586
|
+
));
|
|
587
|
+
} catch (error) {
|
|
588
|
+
const errorObj =
|
|
589
|
+
error instanceof Error ? error : new Error(String(error));
|
|
590
|
+
const message = errorObj.message;
|
|
591
|
+
if (isNonRecoverableApiError(errorObj)) {
|
|
592
|
+
throw errorObj;
|
|
593
|
+
}
|
|
594
|
+
this.conversationStore.appendMessage({
|
|
595
|
+
role: "user",
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: `The previous turn failed with an API/runtime error: ${message}. Retry and continue from the latest state.`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
const shouldContinue = await this.recordMistake({
|
|
604
|
+
iteration,
|
|
605
|
+
reason: "api_error",
|
|
606
|
+
details: message,
|
|
607
|
+
consecutiveMistakes: () => consecutiveMistakes,
|
|
608
|
+
setConsecutiveMistakes: (value) => {
|
|
609
|
+
consecutiveMistakes = value;
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
if (shouldContinue) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
throw errorObj;
|
|
616
|
+
}
|
|
545
617
|
if (assistantMessage) {
|
|
546
618
|
this.conversationStore.appendMessage(assistantMessage);
|
|
547
619
|
}
|
|
@@ -586,7 +658,35 @@ export class Agent {
|
|
|
586
658
|
totalCost: totalUsage.totalCost,
|
|
587
659
|
});
|
|
588
660
|
|
|
661
|
+
if (turn.invalidToolCalls.length > 0) {
|
|
662
|
+
this.conversationStore.appendMessage({
|
|
663
|
+
role: "user",
|
|
664
|
+
content: [
|
|
665
|
+
{
|
|
666
|
+
type: "text",
|
|
667
|
+
text: this.buildInvalidToolCallFeedback(turn.invalidToolCalls),
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
});
|
|
671
|
+
const shouldContinue = await this.recordMistake({
|
|
672
|
+
iteration,
|
|
673
|
+
reason: "invalid_tool_call",
|
|
674
|
+
details: `${turn.invalidToolCalls.length} invalid tool call(s)`,
|
|
675
|
+
consecutiveMistakes: () => consecutiveMistakes,
|
|
676
|
+
setConsecutiveMistakes: (value) => {
|
|
677
|
+
consecutiveMistakes = value;
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
if (shouldContinue) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
throw new Error(
|
|
684
|
+
`maximum consecutive mistakes reached (${this.config.maxConsecutiveMistakes})`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
589
688
|
if (turn.toolCalls.length === 0) {
|
|
689
|
+
consecutiveMistakes = 0;
|
|
590
690
|
// Check completion guard before allowing the loop to end.
|
|
591
691
|
// If the guard returns a nudge string, inject it and continue.
|
|
592
692
|
const guardNudge = this.config.completionGuard?.();
|
|
@@ -648,6 +748,32 @@ export class Agent {
|
|
|
648
748
|
text: this.config.reminderText,
|
|
649
749
|
}),
|
|
650
750
|
);
|
|
751
|
+
const successfulToolCalls = toolResults.filter(
|
|
752
|
+
(record) => !record.error,
|
|
753
|
+
).length;
|
|
754
|
+
const failedToolCalls = toolResults.length - successfulToolCalls;
|
|
755
|
+
if (successfulToolCalls > 0) {
|
|
756
|
+
consecutiveMistakes = 0;
|
|
757
|
+
} else if (failedToolCalls > 0) {
|
|
758
|
+
const failedToolCallDetails =
|
|
759
|
+
this.buildFailedToolCallFeedback(toolResults);
|
|
760
|
+
const shouldContinue = await this.recordMistake({
|
|
761
|
+
iteration,
|
|
762
|
+
reason: "tool_execution_failed",
|
|
763
|
+
details: `${failedToolCalls} tool call(s) failed${
|
|
764
|
+
failedToolCallDetails ? `: ${failedToolCallDetails}` : ""
|
|
765
|
+
}`,
|
|
766
|
+
consecutiveMistakes: () => consecutiveMistakes,
|
|
767
|
+
setConsecutiveMistakes: (value) => {
|
|
768
|
+
consecutiveMistakes = value;
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
if (!shouldContinue) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`maximum consecutive mistakes reached (${this.config.maxConsecutiveMistakes})`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
651
777
|
|
|
652
778
|
this.emit({
|
|
653
779
|
type: "iteration_end",
|
|
@@ -770,6 +896,127 @@ export class Agent {
|
|
|
770
896
|
return result;
|
|
771
897
|
}
|
|
772
898
|
|
|
899
|
+
private buildInvalidToolCallFeedback(
|
|
900
|
+
invalidToolCalls: Array<{
|
|
901
|
+
id: string;
|
|
902
|
+
name?: string;
|
|
903
|
+
reason: "missing_name" | "missing_arguments" | "invalid_arguments";
|
|
904
|
+
}>,
|
|
905
|
+
): string {
|
|
906
|
+
const details = invalidToolCalls
|
|
907
|
+
.map((call) => {
|
|
908
|
+
const name = call.name?.trim() || "(unknown tool)";
|
|
909
|
+
const reason =
|
|
910
|
+
call.reason === "missing_name"
|
|
911
|
+
? "missing tool name"
|
|
912
|
+
: call.reason === "missing_arguments"
|
|
913
|
+
? "missing arguments"
|
|
914
|
+
: "arguments were invalid JSON";
|
|
915
|
+
return `${name} [${call.id}]: ${reason}`;
|
|
916
|
+
})
|
|
917
|
+
.join("; ");
|
|
918
|
+
return `One or more tool calls were invalid or missing required parameters (${details}). Retry with valid tool names and arguments.`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private buildFailedToolCallFeedback(toolResults: ToolCallRecord[]): string {
|
|
922
|
+
const failed = toolResults.filter((record) => !!record.error);
|
|
923
|
+
if (failed.length === 0) {
|
|
924
|
+
return "";
|
|
925
|
+
}
|
|
926
|
+
const details = failed
|
|
927
|
+
.slice(0, 3)
|
|
928
|
+
.map((record) => {
|
|
929
|
+
const message = String(record.error ?? "unknown tool error")
|
|
930
|
+
.replace(/\s+/g, " ")
|
|
931
|
+
.trim();
|
|
932
|
+
return `${record.name}: ${message}`;
|
|
933
|
+
})
|
|
934
|
+
.join("; ");
|
|
935
|
+
return failed.length > 3
|
|
936
|
+
? `${details}; +${failed.length - 3} more failed tool call(s)`
|
|
937
|
+
: details;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private async recordMistake(input: {
|
|
941
|
+
iteration: number;
|
|
942
|
+
reason: "api_error" | "invalid_tool_call" | "tool_execution_failed";
|
|
943
|
+
details?: string;
|
|
944
|
+
consecutiveMistakes: () => number;
|
|
945
|
+
setConsecutiveMistakes: (value: number) => void;
|
|
946
|
+
}): Promise<boolean> {
|
|
947
|
+
const next = input.consecutiveMistakes() + 1;
|
|
948
|
+
input.setConsecutiveMistakes(next);
|
|
949
|
+
const errorMessage =
|
|
950
|
+
input.details?.trim() || `consecutive mistake (${input.reason})`;
|
|
951
|
+
this.emit({
|
|
952
|
+
type: "error",
|
|
953
|
+
error: new Error(errorMessage),
|
|
954
|
+
recoverable: true,
|
|
955
|
+
iteration: input.iteration,
|
|
956
|
+
});
|
|
957
|
+
this.log("warn", "Recorded consecutive mistake", {
|
|
958
|
+
agentId: this.agentId,
|
|
959
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
960
|
+
runId: this.activeRunId || this.conversationStore.getConversationId(),
|
|
961
|
+
iteration: input.iteration,
|
|
962
|
+
reason: input.reason,
|
|
963
|
+
details: input.details,
|
|
964
|
+
consecutiveMistakes: next,
|
|
965
|
+
maxConsecutiveMistakes: this.config.maxConsecutiveMistakes,
|
|
966
|
+
});
|
|
967
|
+
const maxConsecutiveMistakes = this.config.maxConsecutiveMistakes;
|
|
968
|
+
if (!maxConsecutiveMistakes || next < maxConsecutiveMistakes) {
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const decision = await this.resolveConsecutiveMistakeDecision({
|
|
973
|
+
iteration: input.iteration,
|
|
974
|
+
consecutiveMistakes: next,
|
|
975
|
+
maxConsecutiveMistakes,
|
|
976
|
+
reason: input.reason,
|
|
977
|
+
details: input.details,
|
|
978
|
+
});
|
|
979
|
+
if (decision.action === "continue") {
|
|
980
|
+
const guidance = decision.guidance?.trim();
|
|
981
|
+
if (guidance) {
|
|
982
|
+
this.conversationStore.appendMessage({
|
|
983
|
+
role: "user",
|
|
984
|
+
content: [{ type: "text", text: guidance }],
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
input.setConsecutiveMistakes(0);
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private async resolveConsecutiveMistakeDecision(input: {
|
|
994
|
+
iteration: number;
|
|
995
|
+
consecutiveMistakes: number;
|
|
996
|
+
maxConsecutiveMistakes: number;
|
|
997
|
+
reason: "api_error" | "invalid_tool_call" | "tool_execution_failed";
|
|
998
|
+
details?: string;
|
|
999
|
+
}): Promise<ConsecutiveMistakeLimitDecision> {
|
|
1000
|
+
const callback = this.config.onConsecutiveMistakeLimitReached;
|
|
1001
|
+
if (!callback) {
|
|
1002
|
+
return {
|
|
1003
|
+
action: "stop",
|
|
1004
|
+
reason: `maximum consecutive mistakes reached (${input.maxConsecutiveMistakes})`,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
return await callback(input);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return {
|
|
1011
|
+
action: "stop",
|
|
1012
|
+
reason:
|
|
1013
|
+
error instanceof Error
|
|
1014
|
+
? error.message
|
|
1015
|
+
: `maximum consecutive mistakes reached (${input.maxConsecutiveMistakes})`,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
773
1020
|
private async ensureExtensionsInitialized(): Promise<void> {
|
|
774
1021
|
if (this.extensionsInitialized) {
|
|
775
1022
|
return;
|
package/src/hooks/engine.test.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ToolCallRecord } from "../index.js";
|
|
3
|
+
import { ToolOrchestrator } from "./tool-orchestrator.js";
|
|
4
|
+
|
|
5
|
+
function createOrchestrator(): ToolOrchestrator {
|
|
6
|
+
return new ToolOrchestrator({
|
|
7
|
+
getAgentId: () => "agent-1",
|
|
8
|
+
getConversationId: () => "conversation-1",
|
|
9
|
+
getParentAgentId: () => null,
|
|
10
|
+
emit: () => {},
|
|
11
|
+
dispatchLifecycle: async () => undefined,
|
|
12
|
+
authorizeToolCall: async () => ({ allowed: true }),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("ToolOrchestrator reminder cadence", () => {
|
|
17
|
+
it("injects reminder only once per interval after threshold", () => {
|
|
18
|
+
const orchestrator = createOrchestrator();
|
|
19
|
+
const results = [
|
|
20
|
+
{
|
|
21
|
+
id: "tool-1",
|
|
22
|
+
name: "example-tool",
|
|
23
|
+
input: {},
|
|
24
|
+
durationMs: 100,
|
|
25
|
+
startedAt: new Date(),
|
|
26
|
+
endedAt: new Date(),
|
|
27
|
+
output: { ok: true },
|
|
28
|
+
},
|
|
29
|
+
] satisfies ToolCallRecord[];
|
|
30
|
+
|
|
31
|
+
const at50 = orchestrator.buildToolResultMessage(results, 50, {
|
|
32
|
+
afterIterations: 50,
|
|
33
|
+
text: "reminder",
|
|
34
|
+
});
|
|
35
|
+
const at51 = orchestrator.buildToolResultMessage(results, 51, {
|
|
36
|
+
afterIterations: 50,
|
|
37
|
+
text: "reminder",
|
|
38
|
+
});
|
|
39
|
+
const at52 = orchestrator.buildToolResultMessage(results, 52, {
|
|
40
|
+
afterIterations: 50,
|
|
41
|
+
text: "reminder",
|
|
42
|
+
});
|
|
43
|
+
const at101 = orchestrator.buildToolResultMessage(results, 101, {
|
|
44
|
+
afterIterations: 50,
|
|
45
|
+
text: "reminder",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(at50.content).toHaveLength(1);
|
|
49
|
+
expect(at51.content).toHaveLength(2);
|
|
50
|
+
expect(at52.content).toHaveLength(1);
|
|
51
|
+
expect(at101.content).toHaveLength(2);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -162,7 +162,7 @@ export class ToolOrchestrator {
|
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
if (
|
|
165
|
+
if (shouldInjectReminder(iteration, reminder.afterIterations)) {
|
|
166
166
|
content.push({
|
|
167
167
|
type: "text" as const,
|
|
168
168
|
text: reminder.text,
|
|
@@ -175,3 +175,14 @@ export class ToolOrchestrator {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
+
|
|
179
|
+
function shouldInjectReminder(
|
|
180
|
+
iteration: number,
|
|
181
|
+
afterIterations: number,
|
|
182
|
+
): boolean {
|
|
183
|
+
return (
|
|
184
|
+
afterIterations > 0 &&
|
|
185
|
+
iteration > afterIterations &&
|
|
186
|
+
(iteration - 1) % afterIterations === 0
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -59,6 +59,7 @@ export class TurnProcessor {
|
|
|
59
59
|
string,
|
|
60
60
|
{ name?: string; arguments: string; signature?: string }
|
|
61
61
|
>();
|
|
62
|
+
const toolCallIdAliases = new Map<string, string>();
|
|
62
63
|
|
|
63
64
|
for await (const chunk of stream) {
|
|
64
65
|
if (abortSignal.aborted) {
|
|
@@ -96,7 +97,11 @@ export class TurnProcessor {
|
|
|
96
97
|
});
|
|
97
98
|
break;
|
|
98
99
|
case "tool_calls":
|
|
99
|
-
this.processToolCallChunk(
|
|
100
|
+
this.processToolCallChunk(
|
|
101
|
+
chunk,
|
|
102
|
+
pendingToolCallsMap,
|
|
103
|
+
toolCallIdAliases,
|
|
104
|
+
);
|
|
100
105
|
break;
|
|
101
106
|
case "usage":
|
|
102
107
|
usage.inputTokens = chunk.inputTokens;
|
|
@@ -115,6 +120,7 @@ export class TurnProcessor {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
const toolCalls = this.finalizePendingToolCalls(pendingToolCallsMap);
|
|
123
|
+
const invalidToolCalls = this.collectInvalidToolCalls(pendingToolCallsMap);
|
|
118
124
|
const assistantContent: providers.ContentBlock[] = [];
|
|
119
125
|
|
|
120
126
|
if (text) {
|
|
@@ -168,6 +174,7 @@ export class TurnProcessor {
|
|
|
168
174
|
text,
|
|
169
175
|
reasoning: reasoning || undefined,
|
|
170
176
|
toolCalls,
|
|
177
|
+
invalidToolCalls,
|
|
171
178
|
usage,
|
|
172
179
|
truncated,
|
|
173
180
|
responseId,
|
|
@@ -182,15 +189,28 @@ export class TurnProcessor {
|
|
|
182
189
|
string,
|
|
183
190
|
{ name?: string; arguments: string; signature?: string }
|
|
184
191
|
>,
|
|
192
|
+
aliasMap: Map<string, string>,
|
|
185
193
|
): void {
|
|
186
194
|
const { tool_call } = chunk;
|
|
187
|
-
const
|
|
188
|
-
|
|
195
|
+
const functionId = tool_call.function.id;
|
|
196
|
+
const callId = tool_call.call_id;
|
|
197
|
+
const canonicalId =
|
|
198
|
+
(functionId ? aliasMap.get(functionId) : undefined) ??
|
|
199
|
+
(callId ? aliasMap.get(callId) : undefined) ??
|
|
200
|
+
functionId ??
|
|
201
|
+
callId ??
|
|
202
|
+
`call_${Date.now()}`;
|
|
203
|
+
if (functionId) {
|
|
204
|
+
aliasMap.set(functionId, canonicalId);
|
|
205
|
+
}
|
|
206
|
+
if (callId) {
|
|
207
|
+
aliasMap.set(callId, canonicalId);
|
|
208
|
+
}
|
|
189
209
|
|
|
190
|
-
let pending = pendingMap.get(
|
|
210
|
+
let pending = pendingMap.get(canonicalId);
|
|
191
211
|
if (!pending) {
|
|
192
212
|
pending = { name: undefined, arguments: "" };
|
|
193
|
-
pendingMap.set(
|
|
213
|
+
pendingMap.set(canonicalId, pending);
|
|
194
214
|
}
|
|
195
215
|
|
|
196
216
|
if (tool_call.function.name) {
|
|
@@ -243,6 +263,37 @@ export class TurnProcessor {
|
|
|
243
263
|
return toolCalls;
|
|
244
264
|
}
|
|
245
265
|
|
|
266
|
+
private collectInvalidToolCalls(
|
|
267
|
+
pendingMap: Map<
|
|
268
|
+
string,
|
|
269
|
+
{ name?: string; arguments: string; signature?: string }
|
|
270
|
+
>,
|
|
271
|
+
): Array<{
|
|
272
|
+
id: string;
|
|
273
|
+
name?: string;
|
|
274
|
+
reason: "missing_name" | "missing_arguments" | "invalid_arguments";
|
|
275
|
+
}> {
|
|
276
|
+
const invalid: Array<{
|
|
277
|
+
id: string;
|
|
278
|
+
name?: string;
|
|
279
|
+
reason: "missing_name" | "missing_arguments" | "invalid_arguments";
|
|
280
|
+
}> = [];
|
|
281
|
+
for (const [id, pending] of pendingMap.entries()) {
|
|
282
|
+
if (!pending.name) {
|
|
283
|
+
invalid.push({ id, reason: "missing_name" });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!pending.arguments) {
|
|
287
|
+
invalid.push({ id, name: pending.name, reason: "missing_arguments" });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (this.tryParseJson(pending.arguments) === undefined) {
|
|
291
|
+
invalid.push({ id, name: pending.name, reason: "invalid_arguments" });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return invalid;
|
|
295
|
+
}
|
|
296
|
+
|
|
246
297
|
private tryParseJson(value: string): unknown | undefined {
|
|
247
298
|
const parsed = parseJsonStream(value);
|
|
248
299
|
return parsed === value ? undefined : parsed;
|