@prestyj/agent 5.10.0 → 5.11.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/index.cjs +102 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +105 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -20,6 +20,13 @@ interface AgentTool<T extends z.ZodType = z.ZodType> extends Tool {
|
|
|
20
20
|
* batch runs in source order so stateful mutations cannot race each other.
|
|
21
21
|
*/
|
|
22
22
|
executionMode?: ToolExecutionMode;
|
|
23
|
+
/**
|
|
24
|
+
* Overrides the loop's default per-tool timeout. A tool that owns a longer
|
|
25
|
+
* internal budget than the default must declare it here, or the loop cancels
|
|
26
|
+
* it first and the tool's own timeout — with its specific, actionable error
|
|
27
|
+
* message — becomes unreachable.
|
|
28
|
+
*/
|
|
29
|
+
timeoutMs?: number;
|
|
23
30
|
execute: (args: z.infer<T>, context: ToolContext) => ToolExecuteResult | Promise<ToolExecuteResult>;
|
|
24
31
|
}
|
|
25
32
|
interface AgentTextDeltaEvent {
|
|
@@ -70,6 +77,21 @@ interface AgentTurnEndEvent {
|
|
|
70
77
|
usage: Usage;
|
|
71
78
|
timing: AgentTurnTiming;
|
|
72
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A safe point between steps: the assistant message and every tool result for
|
|
82
|
+
* this turn are now in the message array, and no provider call is in flight.
|
|
83
|
+
*
|
|
84
|
+
* Hosts that persist a transcript flush here. Without it a crash mid-run loses
|
|
85
|
+
* the WHOLE turn — including tool results whose side effects already landed on
|
|
86
|
+
* disk — because the only flush happens after the loop returns.
|
|
87
|
+
*
|
|
88
|
+
* Yielded immediately after tool results are appended, so it pairs with
|
|
89
|
+
* `turn_end` (which covers the assistant half) to cover every message.
|
|
90
|
+
*/
|
|
91
|
+
interface AgentCheckpointEvent {
|
|
92
|
+
type: "checkpoint";
|
|
93
|
+
turn: number;
|
|
94
|
+
}
|
|
73
95
|
interface AgentDoneEvent {
|
|
74
96
|
type: "agent_done";
|
|
75
97
|
totalTurns: number;
|
|
@@ -87,6 +109,21 @@ interface AgentMaxTurnsEvent {
|
|
|
87
109
|
totalTurns: number;
|
|
88
110
|
maxTurns: number;
|
|
89
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Emitted when the loop was about to stop on an exhausted turn budget but the
|
|
114
|
+
* host granted an extension instead. The effective budget is raised and the
|
|
115
|
+
* loop continues with a continuation prompt, so this is NOT terminal — unlike
|
|
116
|
+
* `max_turns`, which still fires if the extended budget is also spent.
|
|
117
|
+
*/
|
|
118
|
+
interface AgentTurnBudgetExtendedEvent {
|
|
119
|
+
type: "turn_budget_extended";
|
|
120
|
+
/** Turn number at which the budget was exhausted. */
|
|
121
|
+
turn: number;
|
|
122
|
+
/** New effective `maxTurns` after the extension. */
|
|
123
|
+
grantedTurns: number;
|
|
124
|
+
/** 1-based extension count for this run. */
|
|
125
|
+
extension: number;
|
|
126
|
+
}
|
|
90
127
|
/**
|
|
91
128
|
* Warning signal emitted when a turn ended on a non-clean stop reason —
|
|
92
129
|
* `max_tokens` (output clipped at the model's output-token limit), `refusal`,
|
|
@@ -148,7 +185,7 @@ interface AgentFollowUpMessageEvent {
|
|
|
148
185
|
type: "follow_up_message";
|
|
149
186
|
content: Message["content"];
|
|
150
187
|
}
|
|
151
|
-
type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTruncatedEvent | AgentErrorEvent;
|
|
188
|
+
type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
|
|
152
189
|
interface TransformContextOptions {
|
|
153
190
|
/** Force a transform after the provider reports context overflow. */
|
|
154
191
|
force?: boolean;
|
|
@@ -168,6 +205,12 @@ interface AgentOptions {
|
|
|
168
205
|
/** Control whether tools may/must be called, or select a named tool when supported. */
|
|
169
206
|
toolChoice?: StreamOptions["toolChoice"];
|
|
170
207
|
maxTurns?: number;
|
|
208
|
+
/**
|
|
209
|
+
* How many times `onTurnBudgetExhausted` may grant extra turns in one run.
|
|
210
|
+
* Each grant raises the effective budget by the original `maxTurns`.
|
|
211
|
+
* Default: 2. Set 0 to disable extensions entirely.
|
|
212
|
+
*/
|
|
213
|
+
maxTurnExtensions?: number;
|
|
171
214
|
maxTokens?: number;
|
|
172
215
|
temperature?: number;
|
|
173
216
|
thinking?: StreamOptions["thinking"];
|
|
@@ -234,6 +277,18 @@ interface AgentOptions {
|
|
|
234
277
|
* on read.
|
|
235
278
|
*/
|
|
236
279
|
getFollowUpMessages?: () => Promise<Message[] | null> | Message[] | null;
|
|
280
|
+
/**
|
|
281
|
+
* Consulted when a tool-running turn exhausts the turn budget mid-task,
|
|
282
|
+
* before the loop emits the terminal `max_turns` event. Return true to grant
|
|
283
|
+
* another `maxTurns` worth of turns; false (the default when unset) keeps
|
|
284
|
+
* today's hard cut-off. Hosts should only grant on evidence of progress —
|
|
285
|
+
* extending a spinning agent just buys it more tokens to spin with.
|
|
286
|
+
*/
|
|
287
|
+
onTurnBudgetExhausted?: (ctx: {
|
|
288
|
+
turn: number;
|
|
289
|
+
maxTurns: number;
|
|
290
|
+
extension: number;
|
|
291
|
+
}) => Promise<boolean> | boolean;
|
|
237
292
|
}
|
|
238
293
|
interface AgentResult {
|
|
239
294
|
message: AssistantMessage;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ interface AgentTool<T extends z.ZodType = z.ZodType> extends Tool {
|
|
|
20
20
|
* batch runs in source order so stateful mutations cannot race each other.
|
|
21
21
|
*/
|
|
22
22
|
executionMode?: ToolExecutionMode;
|
|
23
|
+
/**
|
|
24
|
+
* Overrides the loop's default per-tool timeout. A tool that owns a longer
|
|
25
|
+
* internal budget than the default must declare it here, or the loop cancels
|
|
26
|
+
* it first and the tool's own timeout — with its specific, actionable error
|
|
27
|
+
* message — becomes unreachable.
|
|
28
|
+
*/
|
|
29
|
+
timeoutMs?: number;
|
|
23
30
|
execute: (args: z.infer<T>, context: ToolContext) => ToolExecuteResult | Promise<ToolExecuteResult>;
|
|
24
31
|
}
|
|
25
32
|
interface AgentTextDeltaEvent {
|
|
@@ -70,6 +77,21 @@ interface AgentTurnEndEvent {
|
|
|
70
77
|
usage: Usage;
|
|
71
78
|
timing: AgentTurnTiming;
|
|
72
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A safe point between steps: the assistant message and every tool result for
|
|
82
|
+
* this turn are now in the message array, and no provider call is in flight.
|
|
83
|
+
*
|
|
84
|
+
* Hosts that persist a transcript flush here. Without it a crash mid-run loses
|
|
85
|
+
* the WHOLE turn — including tool results whose side effects already landed on
|
|
86
|
+
* disk — because the only flush happens after the loop returns.
|
|
87
|
+
*
|
|
88
|
+
* Yielded immediately after tool results are appended, so it pairs with
|
|
89
|
+
* `turn_end` (which covers the assistant half) to cover every message.
|
|
90
|
+
*/
|
|
91
|
+
interface AgentCheckpointEvent {
|
|
92
|
+
type: "checkpoint";
|
|
93
|
+
turn: number;
|
|
94
|
+
}
|
|
73
95
|
interface AgentDoneEvent {
|
|
74
96
|
type: "agent_done";
|
|
75
97
|
totalTurns: number;
|
|
@@ -87,6 +109,21 @@ interface AgentMaxTurnsEvent {
|
|
|
87
109
|
totalTurns: number;
|
|
88
110
|
maxTurns: number;
|
|
89
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Emitted when the loop was about to stop on an exhausted turn budget but the
|
|
114
|
+
* host granted an extension instead. The effective budget is raised and the
|
|
115
|
+
* loop continues with a continuation prompt, so this is NOT terminal — unlike
|
|
116
|
+
* `max_turns`, which still fires if the extended budget is also spent.
|
|
117
|
+
*/
|
|
118
|
+
interface AgentTurnBudgetExtendedEvent {
|
|
119
|
+
type: "turn_budget_extended";
|
|
120
|
+
/** Turn number at which the budget was exhausted. */
|
|
121
|
+
turn: number;
|
|
122
|
+
/** New effective `maxTurns` after the extension. */
|
|
123
|
+
grantedTurns: number;
|
|
124
|
+
/** 1-based extension count for this run. */
|
|
125
|
+
extension: number;
|
|
126
|
+
}
|
|
90
127
|
/**
|
|
91
128
|
* Warning signal emitted when a turn ended on a non-clean stop reason —
|
|
92
129
|
* `max_tokens` (output clipped at the model's output-token limit), `refusal`,
|
|
@@ -148,7 +185,7 @@ interface AgentFollowUpMessageEvent {
|
|
|
148
185
|
type: "follow_up_message";
|
|
149
186
|
content: Message["content"];
|
|
150
187
|
}
|
|
151
|
-
type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTruncatedEvent | AgentErrorEvent;
|
|
188
|
+
type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
|
|
152
189
|
interface TransformContextOptions {
|
|
153
190
|
/** Force a transform after the provider reports context overflow. */
|
|
154
191
|
force?: boolean;
|
|
@@ -168,6 +205,12 @@ interface AgentOptions {
|
|
|
168
205
|
/** Control whether tools may/must be called, or select a named tool when supported. */
|
|
169
206
|
toolChoice?: StreamOptions["toolChoice"];
|
|
170
207
|
maxTurns?: number;
|
|
208
|
+
/**
|
|
209
|
+
* How many times `onTurnBudgetExhausted` may grant extra turns in one run.
|
|
210
|
+
* Each grant raises the effective budget by the original `maxTurns`.
|
|
211
|
+
* Default: 2. Set 0 to disable extensions entirely.
|
|
212
|
+
*/
|
|
213
|
+
maxTurnExtensions?: number;
|
|
171
214
|
maxTokens?: number;
|
|
172
215
|
temperature?: number;
|
|
173
216
|
thinking?: StreamOptions["thinking"];
|
|
@@ -234,6 +277,18 @@ interface AgentOptions {
|
|
|
234
277
|
* on read.
|
|
235
278
|
*/
|
|
236
279
|
getFollowUpMessages?: () => Promise<Message[] | null> | Message[] | null;
|
|
280
|
+
/**
|
|
281
|
+
* Consulted when a tool-running turn exhausts the turn budget mid-task,
|
|
282
|
+
* before the loop emits the terminal `max_turns` event. Return true to grant
|
|
283
|
+
* another `maxTurns` worth of turns; false (the default when unset) keeps
|
|
284
|
+
* today's hard cut-off. Hosts should only grant on evidence of progress —
|
|
285
|
+
* extending a spinning agent just buys it more tokens to spin with.
|
|
286
|
+
*/
|
|
287
|
+
onTurnBudgetExhausted?: (ctx: {
|
|
288
|
+
turn: number;
|
|
289
|
+
maxTurns: number;
|
|
290
|
+
extension: number;
|
|
291
|
+
}) => Promise<boolean> | boolean;
|
|
237
292
|
}
|
|
238
293
|
interface AgentResult {
|
|
239
294
|
message: AssistantMessage;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
EventStream,
|
|
9
9
|
EZCoderAIError,
|
|
10
10
|
isHardBillingMessage,
|
|
11
|
-
redactValue
|
|
11
|
+
redactValue,
|
|
12
|
+
sliceHead,
|
|
13
|
+
sliceTail
|
|
12
14
|
} from "@prestyj/ai";
|
|
13
15
|
|
|
14
16
|
// src/local-backend.ts
|
|
@@ -35,6 +37,7 @@ function isLocalBackendUrl(baseUrl) {
|
|
|
35
37
|
|
|
36
38
|
// src/agent-loop.ts
|
|
37
39
|
var DEFAULT_MAX_TURNS = 300;
|
|
40
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
|
|
38
41
|
var _diagFn = null;
|
|
39
42
|
function setStreamDiagnostic(fn) {
|
|
40
43
|
_diagFn = fn;
|
|
@@ -237,6 +240,9 @@ function abortablePromise(promise, signal) {
|
|
|
237
240
|
promise.then(resolveOnce, rejectOnce);
|
|
238
241
|
});
|
|
239
242
|
}
|
|
243
|
+
function turnBudgetContinuationPrompt() {
|
|
244
|
+
return "[You reached the turn limit for this segment but the work is not finished, so you have been granted more turns. Before continuing, state in one or two sentences what is already done and what remains, then keep going from there \u2014 do not restart work that is already complete.]";
|
|
245
|
+
}
|
|
240
246
|
function abortableSleep(ms, signal) {
|
|
241
247
|
if (signal?.aborted) return Promise.reject(createAbortError());
|
|
242
248
|
return new Promise((resolve, reject) => {
|
|
@@ -258,6 +264,9 @@ function closeIterator(iterator) {
|
|
|
258
264
|
}
|
|
259
265
|
async function* agentLoop(messages, options) {
|
|
260
266
|
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
267
|
+
let effectiveMaxTurns = maxTurns;
|
|
268
|
+
const maxTurnExtensions = Math.max(0, options.maxTurnExtensions ?? 2);
|
|
269
|
+
let turnExtensions = 0;
|
|
261
270
|
const maxContinuations = options.maxContinuations ?? 5;
|
|
262
271
|
let toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
|
|
263
272
|
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
@@ -305,12 +314,12 @@ async function* agentLoop(messages, options) {
|
|
|
305
314
|
const firstEventTimeoutMs = localBackend ? Number.POSITIVE_INFINITY : usesSilentReasoningBudget ? STREAM_THINKING_IDLE_TIMEOUT_MS : STREAM_FIRST_EVENT_TIMEOUT_MS;
|
|
306
315
|
const initialHardTimeoutMs = localBackend || usesSilentReasoningBudget ? STREAM_THINKING_HARD_TIMEOUT_MS : STREAM_HARD_TIMEOUT_MS;
|
|
307
316
|
const MAX_TOOLCALL_DELTA_CHARS = 1e6;
|
|
308
|
-
const
|
|
317
|
+
const MAX_TOOLCALL_NO_PROGRESS_EVENTS = 2e4;
|
|
309
318
|
let logicalTurnStartedAt = 0;
|
|
310
319
|
let firstProviderEventAt;
|
|
311
320
|
let providerDurationMs = 0;
|
|
312
321
|
try {
|
|
313
|
-
while (turn <
|
|
322
|
+
while (turn < effectiveMaxTurns) {
|
|
314
323
|
options.signal?.throwIfAborted();
|
|
315
324
|
turn++;
|
|
316
325
|
if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
|
|
@@ -381,6 +390,7 @@ async function* agentLoop(messages, options) {
|
|
|
381
390
|
let lastEventType = "";
|
|
382
391
|
let toolcallDeltaChars = 0;
|
|
383
392
|
let toolcallDeltaCount = 0;
|
|
393
|
+
let toolcallNoProgressCount = 0;
|
|
384
394
|
let runawayDetected = null;
|
|
385
395
|
let attemptText = "";
|
|
386
396
|
let lastYieldEndTime = Date.now();
|
|
@@ -535,11 +545,13 @@ async function* agentLoop(messages, options) {
|
|
|
535
545
|
const chunkChars = event.argsJson?.length ?? 0;
|
|
536
546
|
toolcallDeltaChars += chunkChars;
|
|
537
547
|
toolcallDeltaCount++;
|
|
538
|
-
|
|
548
|
+
toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
|
|
549
|
+
if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
|
|
539
550
|
runawayDetected = {
|
|
540
551
|
kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
|
|
541
552
|
chars: toolcallDeltaChars,
|
|
542
|
-
events: toolcallDeltaCount
|
|
553
|
+
events: toolcallDeltaCount,
|
|
554
|
+
noProgressEvents: toolcallNoProgressCount
|
|
543
555
|
};
|
|
544
556
|
diag("runaway_toolcall_detected", {
|
|
545
557
|
...runawayDetected,
|
|
@@ -721,11 +733,15 @@ async function* agentLoop(messages, options) {
|
|
|
721
733
|
turn--;
|
|
722
734
|
continue;
|
|
723
735
|
}
|
|
724
|
-
const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.
|
|
736
|
+
const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.noProgressEvents} consecutive tool-call delta events without argument progress`;
|
|
725
737
|
yield {
|
|
726
738
|
type: "error",
|
|
727
|
-
error: new
|
|
728
|
-
`The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}).
|
|
739
|
+
error: new EZCoderAIError(
|
|
740
|
+
`The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}). Your conversation is preserved.`,
|
|
741
|
+
{
|
|
742
|
+
source: "provider",
|
|
743
|
+
hint: "Retry once. If it keeps happening, switch models and continue the preserved conversation."
|
|
744
|
+
}
|
|
729
745
|
)
|
|
730
746
|
};
|
|
731
747
|
break;
|
|
@@ -752,7 +768,15 @@ async function* agentLoop(messages, options) {
|
|
|
752
768
|
role: "assistant",
|
|
753
769
|
content: [{ type: "text", text: attemptText }]
|
|
754
770
|
});
|
|
755
|
-
messages.push({
|
|
771
|
+
messages.push({
|
|
772
|
+
role: "user",
|
|
773
|
+
content: PARTIAL_CONTINUATION_PROMPT,
|
|
774
|
+
provenance: {
|
|
775
|
+
source: "runtime",
|
|
776
|
+
kind: "continuation",
|
|
777
|
+
visibility: "hidden"
|
|
778
|
+
}
|
|
779
|
+
});
|
|
756
780
|
preservedChars = attemptText.length;
|
|
757
781
|
}
|
|
758
782
|
diag("retry", {
|
|
@@ -871,6 +895,9 @@ async function* agentLoop(messages, options) {
|
|
|
871
895
|
useNonStreamingFallback = false;
|
|
872
896
|
totalUsage.inputTokens += response.usage.inputTokens;
|
|
873
897
|
totalUsage.outputTokens += response.usage.outputTokens;
|
|
898
|
+
if (response.usage.reasoningTokens) {
|
|
899
|
+
totalUsage.reasoningTokens = (totalUsage.reasoningTokens ?? 0) + response.usage.reasoningTokens;
|
|
900
|
+
}
|
|
874
901
|
if (response.usage.cacheRead) {
|
|
875
902
|
totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + response.usage.cacheRead;
|
|
876
903
|
}
|
|
@@ -922,7 +949,15 @@ async function* agentLoop(messages, options) {
|
|
|
922
949
|
model: options.model
|
|
923
950
|
});
|
|
924
951
|
yield { type: "truncated", reason: "max_tokens", continued: true };
|
|
925
|
-
messages.push({
|
|
952
|
+
messages.push({
|
|
953
|
+
role: "user",
|
|
954
|
+
content: MAX_TOKENS_CONTINUATION_PROMPT,
|
|
955
|
+
provenance: {
|
|
956
|
+
source: "runtime",
|
|
957
|
+
kind: "continuation",
|
|
958
|
+
visibility: "hidden"
|
|
959
|
+
}
|
|
960
|
+
});
|
|
926
961
|
continue;
|
|
927
962
|
}
|
|
928
963
|
yield { type: "truncated", reason: "max_tokens", continued: false };
|
|
@@ -998,6 +1033,7 @@ async function* agentLoop(messages, options) {
|
|
|
998
1033
|
);
|
|
999
1034
|
const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
1000
1035
|
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
1036
|
+
yield { type: "checkpoint", turn };
|
|
1001
1037
|
const toolsAborted = executionResult.aborted;
|
|
1002
1038
|
if (fatalToolArgumentError) {
|
|
1003
1039
|
if (fatalToolArgumentRecoverable && !toolArgumentAutoContinueUsed) {
|
|
@@ -1029,8 +1065,51 @@ async function* agentLoop(messages, options) {
|
|
|
1029
1065
|
}
|
|
1030
1066
|
}
|
|
1031
1067
|
}
|
|
1032
|
-
if (turn >=
|
|
1033
|
-
|
|
1068
|
+
if (turn >= effectiveMaxTurns) {
|
|
1069
|
+
let extended = false;
|
|
1070
|
+
if (options.onTurnBudgetExhausted && turnExtensions < maxTurnExtensions) {
|
|
1071
|
+
const extension = turnExtensions + 1;
|
|
1072
|
+
let granted = false;
|
|
1073
|
+
try {
|
|
1074
|
+
granted = await options.onTurnBudgetExhausted({
|
|
1075
|
+
turn,
|
|
1076
|
+
maxTurns: effectiveMaxTurns,
|
|
1077
|
+
extension
|
|
1078
|
+
});
|
|
1079
|
+
} catch {
|
|
1080
|
+
granted = false;
|
|
1081
|
+
}
|
|
1082
|
+
if (granted) {
|
|
1083
|
+
turnExtensions = extension;
|
|
1084
|
+
effectiveMaxTurns += maxTurns;
|
|
1085
|
+
extended = true;
|
|
1086
|
+
diag("turn_budget_extended", {
|
|
1087
|
+
turn,
|
|
1088
|
+
grantedTurns: effectiveMaxTurns,
|
|
1089
|
+
extension,
|
|
1090
|
+
provider: options.provider,
|
|
1091
|
+
model: options.model
|
|
1092
|
+
});
|
|
1093
|
+
yield {
|
|
1094
|
+
type: "turn_budget_extended",
|
|
1095
|
+
turn,
|
|
1096
|
+
grantedTurns: effectiveMaxTurns,
|
|
1097
|
+
extension
|
|
1098
|
+
};
|
|
1099
|
+
messages.push({
|
|
1100
|
+
role: "user",
|
|
1101
|
+
content: turnBudgetContinuationPrompt(),
|
|
1102
|
+
provenance: {
|
|
1103
|
+
source: "runtime",
|
|
1104
|
+
kind: "continuation",
|
|
1105
|
+
visibility: "hidden"
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (!extended) {
|
|
1111
|
+
hitMaxTurns = true;
|
|
1112
|
+
}
|
|
1034
1113
|
}
|
|
1035
1114
|
}
|
|
1036
1115
|
} finally {
|
|
@@ -1046,14 +1125,15 @@ async function* agentLoop(messages, options) {
|
|
|
1046
1125
|
if (hitMaxTurns) {
|
|
1047
1126
|
diag("max_turns_reached", {
|
|
1048
1127
|
turn,
|
|
1049
|
-
maxTurns,
|
|
1128
|
+
maxTurns: effectiveMaxTurns,
|
|
1129
|
+
extensions: turnExtensions,
|
|
1050
1130
|
provider: options.provider,
|
|
1051
1131
|
model: options.model
|
|
1052
1132
|
});
|
|
1053
1133
|
yield {
|
|
1054
1134
|
type: "max_turns",
|
|
1055
1135
|
totalTurns: turn,
|
|
1056
|
-
maxTurns
|
|
1136
|
+
maxTurns: effectiveMaxTurns
|
|
1057
1137
|
};
|
|
1058
1138
|
}
|
|
1059
1139
|
yield {
|
|
@@ -1089,7 +1169,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1089
1169
|
try {
|
|
1090
1170
|
const parsed = tool.parameters.parse(toolCall.args);
|
|
1091
1171
|
const callerSignal = options.signal;
|
|
1092
|
-
const toolTimeout = AbortSignal.timeout(
|
|
1172
|
+
const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
|
|
1093
1173
|
const ctx = {
|
|
1094
1174
|
signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
|
|
1095
1175
|
toolCallId: toolCall.id,
|
|
@@ -1302,9 +1382,9 @@ function capToolResults(toolResults, maxToolResultChars) {
|
|
|
1302
1382
|
const originalChars = toolResult.content.length;
|
|
1303
1383
|
const headChars = Math.floor(max * 0.7);
|
|
1304
1384
|
const tailChars = max - headChars;
|
|
1305
|
-
const head = toolResult.content
|
|
1306
|
-
const tail = toolResult.content
|
|
1307
|
-
const omitted = originalChars -
|
|
1385
|
+
const head = sliceHead(toolResult.content, headChars);
|
|
1386
|
+
const tail = sliceTail(toolResult.content, tailChars);
|
|
1387
|
+
const omitted = originalChars - head.length - tail.length;
|
|
1308
1388
|
toolResult.content = head + `
|
|
1309
1389
|
|
|
1310
1390
|
[... ${omitted} characters omitted ...]
|
|
@@ -1339,11 +1419,11 @@ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
|
|
|
1339
1419
|
const headChars = Math.floor(fairShare * 0.7);
|
|
1340
1420
|
const tailChars = fairShare - headChars;
|
|
1341
1421
|
const omitted = originalChars - fairShare;
|
|
1342
|
-
toolResult.content = toolResult.content
|
|
1422
|
+
toolResult.content = sliceHead(toolResult.content, headChars) + `
|
|
1343
1423
|
|
|
1344
1424
|
[... ${omitted} characters trimmed: this turn's combined tool results exceeded the per-turn budget. Re-run this call alone with narrower filters or offset/limit if you need the omitted content ...]
|
|
1345
1425
|
|
|
1346
|
-
` + (
|
|
1426
|
+
` + sliceTail(toolResult.content, tailChars);
|
|
1347
1427
|
toolResult.capped = {
|
|
1348
1428
|
originalChars: toolResult.capped?.originalChars ?? originalChars,
|
|
1349
1429
|
keptChars: toolResult.content.length,
|
|
@@ -1362,12 +1442,14 @@ function truncateToolResultText(text, maxChars) {
|
|
|
1362
1442
|
if (text.length <= maxChars) return text;
|
|
1363
1443
|
const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
|
|
1364
1444
|
const headChars = Math.max(maxChars - tailChars, 0);
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1445
|
+
const head = sliceHead(text, headChars);
|
|
1446
|
+
const tail = sliceTail(text, tailChars);
|
|
1447
|
+
const omitted = text.length - head.length - tail.length;
|
|
1448
|
+
return `${head}
|
|
1367
1449
|
|
|
1368
1450
|
[... ${omitted} characters omitted after context overflow ...]
|
|
1369
1451
|
|
|
1370
|
-
${
|
|
1452
|
+
${tail}`;
|
|
1371
1453
|
}
|
|
1372
1454
|
function truncateOversizedToolResults(messages, maxChars) {
|
|
1373
1455
|
if (maxChars <= 0) return false;
|