@tanstack/ai 0.41.0 → 0.42.0
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/esm/activities/chat/agent-loop-strategies.d.ts +40 -3
- package/dist/esm/activities/chat/agent-loop-strategies.js +4 -0
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +5 -0
- package/dist/esm/activities/chat/index.js +102 -33
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/types.d.ts +34 -2
- package/package.json +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +58 -0
- package/skills/ai-core/tool-calling/SKILL.md +2 -0
- package/src/activities/chat/agent-loop-strategies.ts +43 -3
- package/src/activities/chat/index.ts +144 -36
- package/src/index.ts +1 -0
- package/src/types.ts +34 -2
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { AgentLoopStrategy } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Creates a strategy that continues for a maximum number of
|
|
3
|
+
* Creates a strategy that continues for a maximum number of **model turns**
|
|
4
|
+
* (iterations), not tool calls.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
+
* One iteration can still emit many parallel tool calls. Prefer
|
|
7
|
+
* {@link maxToolCalls} (and optionally `maxToolCallsPerTurn` on `chat()`)
|
|
8
|
+
* when you need a tool-call budget.
|
|
9
|
+
*
|
|
10
|
+
* @param max - Maximum number of model turns to allow
|
|
6
11
|
* @returns AgentLoopStrategy that stops after max iterations
|
|
7
12
|
*
|
|
8
13
|
* @example
|
|
@@ -12,11 +17,42 @@ import { AgentLoopStrategy } from '../../types.js';
|
|
|
12
17
|
* model: "gpt-4o",
|
|
13
18
|
* messages: [...],
|
|
14
19
|
* tools: [weatherTool],
|
|
15
|
-
* agentLoopStrategy: maxIterations(3), // Max 3
|
|
20
|
+
* agentLoopStrategy: maxIterations(3), // Max 3 model turns
|
|
16
21
|
* });
|
|
17
22
|
* ```
|
|
18
23
|
*/
|
|
19
24
|
export declare function maxIterations(max: number): AgentLoopStrategy;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a strategy that continues while `toolCallCount < max`.
|
|
27
|
+
*
|
|
28
|
+
* Unlike {@link maxIterations} (which counts model turns), this bounds
|
|
29
|
+
* **emitted** tool calls counted during the run (including ones skipped by
|
|
30
|
+
* `maxToolCallsPerTurn`). Strategies only run between turns, so the turn that
|
|
31
|
+
* crosses `max` is not truncated — the final count (and executions, unless
|
|
32
|
+
* `maxToolCallsPerTurn` is set) may exceed `max`. Pair with
|
|
33
|
+
* `chat({ maxToolCallsPerTurn })` to also cap parallel fan-out inside a single
|
|
34
|
+
* turn.
|
|
35
|
+
*
|
|
36
|
+
* @param max - Maximum cumulative emitted tool calls before stopping further turns
|
|
37
|
+
* @returns AgentLoopStrategy that returns true while `toolCallCount < max`
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { chat, combineStrategies, maxIterations, maxToolCalls } from '@tanstack/ai'
|
|
42
|
+
*
|
|
43
|
+
* const stream = chat({
|
|
44
|
+
* adapter: openaiText('gpt-4o'),
|
|
45
|
+
* messages: [...],
|
|
46
|
+
* tools: [weatherTool],
|
|
47
|
+
* maxToolCallsPerTurn: 10,
|
|
48
|
+
* agentLoopStrategy: combineStrategies([
|
|
49
|
+
* maxIterations(20),
|
|
50
|
+
* maxToolCalls(20),
|
|
51
|
+
* ]),
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function maxToolCalls(max: number): AgentLoopStrategy;
|
|
20
56
|
/**
|
|
21
57
|
* Creates a strategy that continues until a specific finish reason is encountered
|
|
22
58
|
*
|
|
@@ -51,6 +87,7 @@ export declare function untilFinishReason(stopReasons: Array<string>): AgentLoop
|
|
|
51
87
|
* tools: [weatherTool],
|
|
52
88
|
* agentLoopStrategy: combineStrategies([
|
|
53
89
|
* maxIterations(10),
|
|
90
|
+
* maxToolCalls(20),
|
|
54
91
|
* ({ messages }) => messages.length < 100,
|
|
55
92
|
* ]),
|
|
56
93
|
* });
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
function maxIterations(max) {
|
|
2
2
|
return ({ iterationCount }) => iterationCount < max;
|
|
3
3
|
}
|
|
4
|
+
function maxToolCalls(max) {
|
|
5
|
+
return ({ toolCallCount }) => toolCallCount < max;
|
|
6
|
+
}
|
|
4
7
|
function untilFinishReason(stopReasons) {
|
|
5
8
|
return ({ finishReason, iterationCount }) => {
|
|
6
9
|
if (iterationCount === 0) return true;
|
|
@@ -18,6 +21,7 @@ function combineStrategies(strategies) {
|
|
|
18
21
|
export {
|
|
19
22
|
combineStrategies,
|
|
20
23
|
maxIterations,
|
|
24
|
+
maxToolCalls,
|
|
21
25
|
untilFinishReason
|
|
22
26
|
};
|
|
23
27
|
//# sourceMappingURL=agent-loop-strategies.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loop-strategies.js","sources":["../../../../src/activities/chat/agent-loop-strategies.ts"],"sourcesContent":["import type { AgentLoopStrategy } from '../../types'\n\n/**\n * Creates a strategy that continues for a maximum number of iterations\n *\n * @param max - Maximum number of
|
|
1
|
+
{"version":3,"file":"agent-loop-strategies.js","sources":["../../../../src/activities/chat/agent-loop-strategies.ts"],"sourcesContent":["import type { AgentLoopStrategy } from '../../types'\n\n/**\n * Creates a strategy that continues for a maximum number of **model turns**\n * (iterations), not tool calls.\n *\n * One iteration can still emit many parallel tool calls. Prefer\n * {@link maxToolCalls} (and optionally `maxToolCallsPerTurn` on `chat()`)\n * when you need a tool-call budget.\n *\n * @param max - Maximum number of model turns to allow\n * @returns AgentLoopStrategy that stops after max iterations\n *\n * @example\n * ```typescript\n * const stream = chat({\n * adapter: openaiText(),\n * model: \"gpt-4o\",\n * messages: [...],\n * tools: [weatherTool],\n * agentLoopStrategy: maxIterations(3), // Max 3 model turns\n * });\n * ```\n */\nexport function maxIterations(max: number): AgentLoopStrategy {\n return ({ iterationCount }) => iterationCount < max\n}\n\n/**\n * Creates a strategy that continues while `toolCallCount < max`.\n *\n * Unlike {@link maxIterations} (which counts model turns), this bounds\n * **emitted** tool calls counted during the run (including ones skipped by\n * `maxToolCallsPerTurn`). Strategies only run between turns, so the turn that\n * crosses `max` is not truncated — the final count (and executions, unless\n * `maxToolCallsPerTurn` is set) may exceed `max`. Pair with\n * `chat({ maxToolCallsPerTurn })` to also cap parallel fan-out inside a single\n * turn.\n *\n * @param max - Maximum cumulative emitted tool calls before stopping further turns\n * @returns AgentLoopStrategy that returns true while `toolCallCount < max`\n *\n * @example\n * ```typescript\n * import { chat, combineStrategies, maxIterations, maxToolCalls } from '@tanstack/ai'\n *\n * const stream = chat({\n * adapter: openaiText('gpt-4o'),\n * messages: [...],\n * tools: [weatherTool],\n * maxToolCallsPerTurn: 10,\n * agentLoopStrategy: combineStrategies([\n * maxIterations(20),\n * maxToolCalls(20),\n * ]),\n * })\n * ```\n */\nexport function maxToolCalls(max: number): AgentLoopStrategy {\n return ({ toolCallCount }) => toolCallCount < max\n}\n\n/**\n * Creates a strategy that continues until a specific finish reason is encountered\n *\n * @param stopReasons - Finish reasons that should stop the loop\n * @returns AgentLoopStrategy that stops on specific finish reasons\n *\n * @example\n * ```typescript\n * const stream = chat({\n * adapter: openaiText(),\n * model: \"gpt-4o\",\n * messages: [...],\n * tools: [weatherTool],\n * agentLoopStrategy: untilFinishReason([\"stop\", \"length\"]),\n * });\n * ```\n */\nexport function untilFinishReason(\n stopReasons: Array<string>,\n): AgentLoopStrategy {\n return ({ finishReason, iterationCount }) => {\n // Always allow at least one iteration\n if (iterationCount === 0) return true\n\n // Stop if we hit a stop reason\n if (finishReason && stopReasons.includes(finishReason)) {\n return false\n }\n\n // Otherwise continue\n return true\n }\n}\n\n/**\n * Creates a strategy that combines multiple strategies with AND logic\n * All strategies must return true to continue\n *\n * @param strategies - Array of strategies to combine\n * @returns AgentLoopStrategy that continues only if all strategies return true\n *\n * @example\n * ```typescript\n * const stream = chat({\n * adapter: openaiText(),\n * model: \"gpt-4o\",\n * messages: [...],\n * tools: [weatherTool],\n * agentLoopStrategy: combineStrategies([\n * maxIterations(10),\n * maxToolCalls(20),\n * ({ messages }) => messages.length < 100,\n * ]),\n * });\n * ```\n */\nexport function combineStrategies(\n strategies: Array<AgentLoopStrategy>,\n): AgentLoopStrategy {\n return (state) => {\n return strategies.every((strategy) => strategy(state))\n }\n}\n"],"names":[],"mappings":"AAwBO,SAAS,cAAc,KAAgC;AAC5D,SAAO,CAAC,EAAE,qBAAqB,iBAAiB;AAClD;AAgCO,SAAS,aAAa,KAAgC;AAC3D,SAAO,CAAC,EAAE,oBAAoB,gBAAgB;AAChD;AAmBO,SAAS,kBACd,aACmB;AACnB,SAAO,CAAC,EAAE,cAAc,qBAAqB;AAE3C,QAAI,mBAAmB,EAAG,QAAO;AAGjC,QAAI,gBAAgB,YAAY,SAAS,YAAY,GAAG;AACtD,aAAO;AAAA,IACT;AAGA,WAAO;AAAA,EACT;AACF;AAwBO,SAAS,kBACd,YACmB;AACnB,SAAO,CAAC,UAAU;AAChB,WAAO,WAAW,MAAM,CAAC,aAAa,SAAS,KAAK,CAAC;AAAA,EACvD;AACF;"}
|
|
@@ -93,6 +93,11 @@ export interface TextActivityOptions<TAdapter extends AnyTextAdapter, TSchema ex
|
|
|
93
93
|
abortController?: TextOptions['abortController'];
|
|
94
94
|
/** Strategy for controlling the agent loop */
|
|
95
95
|
agentLoopStrategy?: TextOptions['agentLoopStrategy'];
|
|
96
|
+
/**
|
|
97
|
+
* Cap how many tool calls from a single model turn are executed.
|
|
98
|
+
* Excess calls receive error results. See {@link TextOptions.maxToolCallsPerTurn}.
|
|
99
|
+
*/
|
|
100
|
+
maxToolCallsPerTurn?: TextOptions['maxToolCallsPerTurn'];
|
|
96
101
|
/**
|
|
97
102
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
98
103
|
* Tunes how much of each lazy tool's description appears in the discovery
|
|
@@ -20,6 +20,15 @@ const kind = "text";
|
|
|
20
20
|
function createChatOptions(options) {
|
|
21
21
|
return options;
|
|
22
22
|
}
|
|
23
|
+
function resolveMaxToolCallsPerTurn(cap) {
|
|
24
|
+
if (cap == null) return void 0;
|
|
25
|
+
if (!Number.isFinite(cap) || cap < 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`maxToolCallsPerTurn must be a non-negative finite number, got ${cap}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return Math.floor(cap);
|
|
31
|
+
}
|
|
23
32
|
function combineAbortSignals(a, b) {
|
|
24
33
|
if (!a) return b;
|
|
25
34
|
if (!b) return a;
|
|
@@ -48,6 +57,12 @@ class TextEngine {
|
|
|
48
57
|
effectiveSignal;
|
|
49
58
|
messages;
|
|
50
59
|
iterationCount = 0;
|
|
60
|
+
/** Cumulative tool calls counted in this run (emitted + pending resume). */
|
|
61
|
+
toolCallCount = 0;
|
|
62
|
+
/** Tool calls in the most recent budgeted batch (0 when none). */
|
|
63
|
+
lastTurnToolCallCount = 0;
|
|
64
|
+
/** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
|
|
65
|
+
countedToolCallIds = /* @__PURE__ */ new Set();
|
|
51
66
|
lastFinishReason = null;
|
|
52
67
|
streamStartTime = 0;
|
|
53
68
|
totalChunkCount = 0;
|
|
@@ -62,6 +77,7 @@ class TextEngine {
|
|
|
62
77
|
earlyTermination = false;
|
|
63
78
|
toolPhase = "continue";
|
|
64
79
|
cyclePhase = "processText";
|
|
80
|
+
maxToolCallsPerTurn;
|
|
65
81
|
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
66
82
|
initialApprovals;
|
|
67
83
|
initialClientToolResults;
|
|
@@ -107,6 +123,9 @@ class TextEngine {
|
|
|
107
123
|
this.params = config.params;
|
|
108
124
|
this.systemPrompts = config.params.systemPrompts || [];
|
|
109
125
|
this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
|
|
126
|
+
this.maxToolCallsPerTurn = resolveMaxToolCallsPerTurn(
|
|
127
|
+
config.params.maxToolCallsPerTurn
|
|
128
|
+
);
|
|
110
129
|
this.initialMessageCount = config.params.messages.length;
|
|
111
130
|
const { approvals, clientToolResults } = this.extractClientStateFromOriginalMessages(
|
|
112
131
|
config.params.messages
|
|
@@ -584,8 +603,9 @@ class TextEngine {
|
|
|
584
603
|
return "continue";
|
|
585
604
|
}
|
|
586
605
|
const finishEvent = this.createSyntheticFinishedEvent();
|
|
606
|
+
const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(pendingToolCalls);
|
|
587
607
|
const undiscoveredLazyResults = [];
|
|
588
|
-
const executablePendingCalls =
|
|
608
|
+
const executablePendingCalls = budgetedToolCalls.filter((tc) => {
|
|
589
609
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
590
610
|
undiscoveredLazyResults.push({
|
|
591
611
|
toolCallId: tc.id,
|
|
@@ -601,15 +621,21 @@ class TextEngine {
|
|
|
601
621
|
}
|
|
602
622
|
return true;
|
|
603
623
|
});
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)) {
|
|
609
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
610
|
-
}
|
|
624
|
+
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
|
|
625
|
+
const argsMap = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const tc of pendingToolCalls) {
|
|
627
|
+
argsMap.set(tc.id, tc.function.arguments);
|
|
611
628
|
}
|
|
612
629
|
if (executablePendingCalls.length === 0) {
|
|
630
|
+
if (deferredErrorResults.length > 0) {
|
|
631
|
+
for (const chunk of this.buildToolResultChunks(
|
|
632
|
+
deferredErrorResults,
|
|
633
|
+
finishEvent,
|
|
634
|
+
argsMap
|
|
635
|
+
)) {
|
|
636
|
+
yield* this.pipeThroughMiddleware(chunk);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
613
639
|
return "continue";
|
|
614
640
|
}
|
|
615
641
|
const { approvals, clientToolResults } = this.collectClientState();
|
|
@@ -656,20 +682,17 @@ class TextEngine {
|
|
|
656
682
|
this.setToolPhase("stop");
|
|
657
683
|
return "stop";
|
|
658
684
|
}
|
|
685
|
+
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
659
686
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
660
687
|
toolCalls: pendingToolCalls,
|
|
661
|
-
results:
|
|
688
|
+
results: allResults,
|
|
662
689
|
needsApproval: executionResult.needsApproval,
|
|
663
690
|
needsClientExecution: executionResult.needsClientExecution
|
|
664
691
|
});
|
|
665
|
-
const argsMap = /* @__PURE__ */ new Map();
|
|
666
|
-
for (const tc of pendingToolCalls) {
|
|
667
|
-
argsMap.set(tc.id, tc.function.arguments);
|
|
668
|
-
}
|
|
669
692
|
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
670
|
-
if (
|
|
693
|
+
if (allResults.length > 0) {
|
|
671
694
|
for (const chunk of this.buildToolResultChunks(
|
|
672
|
-
|
|
695
|
+
allResults,
|
|
673
696
|
finishEvent,
|
|
674
697
|
argsMap
|
|
675
698
|
)) {
|
|
@@ -692,7 +715,7 @@ class TextEngine {
|
|
|
692
715
|
return "wait";
|
|
693
716
|
}
|
|
694
717
|
const toolResultChunks = this.buildToolResultChunks(
|
|
695
|
-
|
|
718
|
+
allResults,
|
|
696
719
|
finishEvent,
|
|
697
720
|
argsMap
|
|
698
721
|
);
|
|
@@ -703,18 +726,21 @@ class TextEngine {
|
|
|
703
726
|
}
|
|
704
727
|
async *processToolCalls() {
|
|
705
728
|
if (!this.shouldExecuteToolPhase()) {
|
|
729
|
+
this.lastTurnToolCallCount = 0;
|
|
706
730
|
this.setToolPhase("stop");
|
|
707
731
|
return;
|
|
708
732
|
}
|
|
709
733
|
const toolCalls = this.toolCallManager.getToolCalls();
|
|
710
734
|
const finishEvent = this.finishedEvent;
|
|
711
735
|
if (!finishEvent || toolCalls.length === 0) {
|
|
736
|
+
this.lastTurnToolCallCount = 0;
|
|
712
737
|
this.setToolPhase("stop");
|
|
713
738
|
return;
|
|
714
739
|
}
|
|
740
|
+
const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(toolCalls);
|
|
715
741
|
this.addAssistantToolCallMessage(toolCalls);
|
|
716
742
|
const undiscoveredLazyResults = [];
|
|
717
|
-
const executableToolCalls =
|
|
743
|
+
const executableToolCalls = budgetedToolCalls.filter((tc) => {
|
|
718
744
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
719
745
|
undiscoveredLazyResults.push({
|
|
720
746
|
toolCallId: tc.id,
|
|
@@ -730,15 +756,16 @@ class TextEngine {
|
|
|
730
756
|
}
|
|
731
757
|
return true;
|
|
732
758
|
});
|
|
733
|
-
|
|
734
|
-
for (const chunk of this.buildToolResultChunks(
|
|
735
|
-
undiscoveredLazyResults,
|
|
736
|
-
this.finishedEvent
|
|
737
|
-
)) {
|
|
738
|
-
yield* this.pipeThroughMiddleware(chunk);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
759
|
+
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
|
|
741
760
|
if (executableToolCalls.length === 0) {
|
|
761
|
+
if (deferredErrorResults.length > 0) {
|
|
762
|
+
for (const chunk of this.buildToolResultChunks(
|
|
763
|
+
deferredErrorResults,
|
|
764
|
+
finishEvent
|
|
765
|
+
)) {
|
|
766
|
+
yield* this.pipeThroughMiddleware(chunk);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
742
769
|
this.toolCallManager.clear();
|
|
743
770
|
this.setToolPhase("continue");
|
|
744
771
|
return;
|
|
@@ -789,16 +816,17 @@ class TextEngine {
|
|
|
789
816
|
this.setToolPhase("stop");
|
|
790
817
|
return;
|
|
791
818
|
}
|
|
819
|
+
const allResults = [...executionResult.results, ...deferredErrorResults];
|
|
792
820
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
793
821
|
toolCalls,
|
|
794
|
-
results:
|
|
822
|
+
results: allResults,
|
|
795
823
|
needsApproval: executionResult.needsApproval,
|
|
796
824
|
needsClientExecution: executionResult.needsClientExecution
|
|
797
825
|
});
|
|
798
826
|
if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
|
|
799
|
-
if (
|
|
827
|
+
if (allResults.length > 0) {
|
|
800
828
|
for (const chunk of this.buildToolResultChunks(
|
|
801
|
-
|
|
829
|
+
allResults,
|
|
802
830
|
finishEvent
|
|
803
831
|
)) {
|
|
804
832
|
yield* this.pipeThroughMiddleware(chunk);
|
|
@@ -819,10 +847,7 @@ class TextEngine {
|
|
|
819
847
|
this.setToolPhase("wait");
|
|
820
848
|
return;
|
|
821
849
|
}
|
|
822
|
-
const toolResultChunks = this.buildToolResultChunks(
|
|
823
|
-
executionResult.results,
|
|
824
|
-
finishEvent
|
|
825
|
-
);
|
|
850
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
|
|
826
851
|
for (const chunk of toolResultChunks) {
|
|
827
852
|
yield* this.pipeThroughMiddleware(chunk);
|
|
828
853
|
}
|
|
@@ -1060,9 +1085,53 @@ class TextEngine {
|
|
|
1060
1085
|
return this.loopStrategy({
|
|
1061
1086
|
iterationCount: this.iterationCount,
|
|
1062
1087
|
messages: this.messages,
|
|
1063
|
-
finishReason: this.lastFinishReason
|
|
1088
|
+
finishReason: this.lastFinishReason,
|
|
1089
|
+
toolCallCount: this.toolCallCount,
|
|
1090
|
+
lastTurnToolCallCount: this.lastTurnToolCallCount
|
|
1064
1091
|
}) && this.toolPhase === "continue";
|
|
1065
1092
|
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Record tool calls (deduped by id) and return the subset that should be
|
|
1095
|
+
* executed after applying `maxToolCallsPerTurn`. Excess calls get synthetic
|
|
1096
|
+
* error results so every tool_call still has a matching result.
|
|
1097
|
+
*
|
|
1098
|
+
* Used for both live model turns and pending/resume batches. IDs already
|
|
1099
|
+
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
1100
|
+
* re-added to `toolCallCount`.
|
|
1101
|
+
*/
|
|
1102
|
+
applyToolCallBudget(toolCalls) {
|
|
1103
|
+
this.lastTurnToolCallCount = toolCalls.length;
|
|
1104
|
+
let newlyCounted = 0;
|
|
1105
|
+
for (const tc of toolCalls) {
|
|
1106
|
+
if (!this.countedToolCallIds.has(tc.id)) {
|
|
1107
|
+
this.countedToolCallIds.add(tc.id);
|
|
1108
|
+
newlyCounted++;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
this.toolCallCount += newlyCounted;
|
|
1112
|
+
const cap = this.maxToolCallsPerTurn;
|
|
1113
|
+
if (cap == null || toolCalls.length <= cap) {
|
|
1114
|
+
return { toExecute: toolCalls, skippedResults: [] };
|
|
1115
|
+
}
|
|
1116
|
+
this.logger.agentLoop(
|
|
1117
|
+
`maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
|
|
1118
|
+
{
|
|
1119
|
+
maxToolCallsPerTurn: cap,
|
|
1120
|
+
emitted: toolCalls.length,
|
|
1121
|
+
skipped: toolCalls.length - cap
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
const toExecute = toolCalls.slice(0, cap);
|
|
1125
|
+
const skippedResults = toolCalls.slice(cap).map((tc) => ({
|
|
1126
|
+
toolCallId: tc.id,
|
|
1127
|
+
toolName: tc.function.name,
|
|
1128
|
+
result: {
|
|
1129
|
+
error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`
|
|
1130
|
+
},
|
|
1131
|
+
state: "output-error"
|
|
1132
|
+
}));
|
|
1133
|
+
return { toExecute, skippedResults };
|
|
1134
|
+
}
|
|
1066
1135
|
isAborted() {
|
|
1067
1136
|
return !!this.effectiveSignal?.aborted;
|
|
1068
1137
|
}
|