@prestyj/agent 5.6.0 → 5.8.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, StopReason, Usage, AssistantMessage } from '@prestyj/ai';
1
+ import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@prestyj/ai';
2
2
  import { z } from 'zod';
3
3
 
4
4
  interface StructuredToolResult {
@@ -49,11 +49,26 @@ interface AgentToolCallEndEvent {
49
49
  isError: boolean;
50
50
  durationMs: number;
51
51
  }
52
+ interface AgentTurnTiming {
53
+ /** Logical turn start, before context transforms or provider retries. Unix epoch milliseconds. */
54
+ startedAt: number;
55
+ /** First provider event, or full-response arrival for non-streaming fallback. */
56
+ firstProviderEventAt?: number;
57
+ /** Successful provider response completion. Unix epoch milliseconds. */
58
+ completedAt: number;
59
+ /** Time spent awaiting provider attempts, including failed attempts but excluding retry backoff. */
60
+ providerDurationMs: number;
61
+ /** Time from logical turn start to the first provider event. */
62
+ ttftMs?: number;
63
+ /** Output tokens divided by total provider duration. Omitted when no rate is measurable. */
64
+ outputTokensPerSecond?: number;
65
+ }
52
66
  interface AgentTurnEndEvent {
53
67
  type: "turn_end";
54
68
  turn: number;
55
69
  stopReason: StopReason;
56
70
  usage: Usage;
71
+ timing: AgentTurnTiming;
57
72
  }
58
73
  interface AgentDoneEvent {
59
74
  type: "agent_done";
@@ -121,6 +136,14 @@ interface AgentFollowUpMessageEvent {
121
136
  content: Message["content"];
122
137
  }
123
138
  type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentErrorEvent;
139
+ interface TransformContextOptions {
140
+ /** Force a transform after the provider reports context overflow. */
141
+ force?: boolean;
142
+ /** Latest successful provider usage, anchored at its assistant message. */
143
+ usage?: Usage;
144
+ /** Messages appended after that usage sample and not yet seen by the provider. */
145
+ pendingMessages: Message[];
146
+ }
124
147
  interface AgentOptions {
125
148
  provider: StreamOptions["provider"];
126
149
  model: string;
@@ -129,6 +152,8 @@ interface AgentOptions {
129
152
  priorMessages?: Message[];
130
153
  tools?: AgentTool[];
131
154
  serverTools?: ServerToolDefinition[];
155
+ /** Control whether tools may/must be called, or select a named tool when supported. */
156
+ toolChoice?: StreamOptions["toolChoice"];
132
157
  maxTurns?: number;
133
158
  maxTokens?: number;
134
159
  temperature?: number;
@@ -163,6 +188,10 @@ interface AgentOptions {
163
188
  clearToolUses?: boolean;
164
189
  /** Max characters for a single tool result. Results exceeding this are truncated with a notice. */
165
190
  maxToolResultChars?: number;
191
+ /** Aggregate budget for ALL tool results in one assistant turn. Protects
192
+ * against parallel fan-outs injecting huge uncached context in one turn;
193
+ * the largest results are trimmed (water-filling) with a re-run notice. */
194
+ maxTurnToolResultChars?: number;
166
195
  /** Max consecutive pause_turn continuations before stopping (default: 5).
167
196
  * Prevents infinite loops when server-side tools keep pausing. */
168
197
  maxContinuations?: number;
@@ -171,12 +200,12 @@ interface AgentOptions {
171
200
  * the messages array (e.g. compaction, truncation). Return the same array
172
201
  * for no-op, or a new array to replace the conversation context.
173
202
  *
203
+ * The latest provider usage is authoritative for the history through its
204
+ * assistant response. `pendingMessages` contains context appended afterward.
174
205
  * When `options.force` is true, the caller should compact unconditionally
175
206
  * (e.g. after a context overflow error from the API).
176
207
  */
177
- transformContext?: (messages: Message[], options?: {
178
- force?: boolean;
179
- }) => Message[] | Promise<Message[]>;
208
+ transformContext?: (messages: Message[], options: TransformContextOptions) => Message[] | Promise<Message[]>;
180
209
  /**
181
210
  * Polled after tool execution completes each turn. Returns user messages
182
211
  * to inject into the conversation before the next LLM call (steering).
@@ -282,4 +311,4 @@ declare function isBillingError(err: unknown): boolean;
282
311
  declare function isUsageLimitError(err: unknown): boolean;
283
312
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
284
313
 
285
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, agentLoop, isAbortError, isBillingError, isContextOverflow, isUsageLimitError, setStreamDiagnostic };
314
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isUsageLimitError, setStreamDiagnostic };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, StopReason, Usage, AssistantMessage } from '@prestyj/ai';
1
+ import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@prestyj/ai';
2
2
  import { z } from 'zod';
3
3
 
4
4
  interface StructuredToolResult {
@@ -49,11 +49,26 @@ interface AgentToolCallEndEvent {
49
49
  isError: boolean;
50
50
  durationMs: number;
51
51
  }
52
+ interface AgentTurnTiming {
53
+ /** Logical turn start, before context transforms or provider retries. Unix epoch milliseconds. */
54
+ startedAt: number;
55
+ /** First provider event, or full-response arrival for non-streaming fallback. */
56
+ firstProviderEventAt?: number;
57
+ /** Successful provider response completion. Unix epoch milliseconds. */
58
+ completedAt: number;
59
+ /** Time spent awaiting provider attempts, including failed attempts but excluding retry backoff. */
60
+ providerDurationMs: number;
61
+ /** Time from logical turn start to the first provider event. */
62
+ ttftMs?: number;
63
+ /** Output tokens divided by total provider duration. Omitted when no rate is measurable. */
64
+ outputTokensPerSecond?: number;
65
+ }
52
66
  interface AgentTurnEndEvent {
53
67
  type: "turn_end";
54
68
  turn: number;
55
69
  stopReason: StopReason;
56
70
  usage: Usage;
71
+ timing: AgentTurnTiming;
57
72
  }
58
73
  interface AgentDoneEvent {
59
74
  type: "agent_done";
@@ -121,6 +136,14 @@ interface AgentFollowUpMessageEvent {
121
136
  content: Message["content"];
122
137
  }
123
138
  type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentErrorEvent;
139
+ interface TransformContextOptions {
140
+ /** Force a transform after the provider reports context overflow. */
141
+ force?: boolean;
142
+ /** Latest successful provider usage, anchored at its assistant message. */
143
+ usage?: Usage;
144
+ /** Messages appended after that usage sample and not yet seen by the provider. */
145
+ pendingMessages: Message[];
146
+ }
124
147
  interface AgentOptions {
125
148
  provider: StreamOptions["provider"];
126
149
  model: string;
@@ -129,6 +152,8 @@ interface AgentOptions {
129
152
  priorMessages?: Message[];
130
153
  tools?: AgentTool[];
131
154
  serverTools?: ServerToolDefinition[];
155
+ /** Control whether tools may/must be called, or select a named tool when supported. */
156
+ toolChoice?: StreamOptions["toolChoice"];
132
157
  maxTurns?: number;
133
158
  maxTokens?: number;
134
159
  temperature?: number;
@@ -163,6 +188,10 @@ interface AgentOptions {
163
188
  clearToolUses?: boolean;
164
189
  /** Max characters for a single tool result. Results exceeding this are truncated with a notice. */
165
190
  maxToolResultChars?: number;
191
+ /** Aggregate budget for ALL tool results in one assistant turn. Protects
192
+ * against parallel fan-outs injecting huge uncached context in one turn;
193
+ * the largest results are trimmed (water-filling) with a re-run notice. */
194
+ maxTurnToolResultChars?: number;
166
195
  /** Max consecutive pause_turn continuations before stopping (default: 5).
167
196
  * Prevents infinite loops when server-side tools keep pausing. */
168
197
  maxContinuations?: number;
@@ -171,12 +200,12 @@ interface AgentOptions {
171
200
  * the messages array (e.g. compaction, truncation). Return the same array
172
201
  * for no-op, or a new array to replace the conversation context.
173
202
  *
203
+ * The latest provider usage is authoritative for the history through its
204
+ * assistant response. `pendingMessages` contains context appended afterward.
174
205
  * When `options.force` is true, the caller should compact unconditionally
175
206
  * (e.g. after a context overflow error from the API).
176
207
  */
177
- transformContext?: (messages: Message[], options?: {
178
- force?: boolean;
179
- }) => Message[] | Promise<Message[]>;
208
+ transformContext?: (messages: Message[], options: TransformContextOptions) => Message[] | Promise<Message[]>;
180
209
  /**
181
210
  * Polled after tool execution completes each turn. Returns user messages
182
211
  * to inject into the conversation before the next LLM call (steering).
@@ -282,4 +311,4 @@ declare function isBillingError(err: unknown): boolean;
282
311
  declare function isUsageLimitError(err: unknown): boolean;
283
312
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
284
313
 
285
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, agentLoop, isAbortError, isBillingError, isContextOverflow, isUsageLimitError, setStreamDiagnostic };
314
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isUsageLimitError, setStreamDiagnostic };
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  stream,
8
8
  EventStream,
9
9
  EZCoderAIError,
10
- isHardBillingMessage
10
+ isHardBillingMessage,
11
+ redactValue
11
12
  } from "@prestyj/ai";
12
13
  var DEFAULT_MAX_TURNS = 300;
13
14
  var _diagFn = null;
@@ -126,7 +127,7 @@ function classifyOverload(err) {
126
127
  if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
127
128
  return "overloaded";
128
129
  }
129
- if (statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504 || msg.includes("api_error") || msg.includes("server_error") || msg.includes("internal server error") || msg.includes("bad gateway") || msg.includes("service unavailable") || msg.includes("gateway timeout")) {
130
+ if (statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504 || statusCode === 507 || msg.includes("api_error") || msg.includes("server_error") || msg.includes("internal server error") || msg.includes("bad gateway") || msg.includes("service unavailable") || msg.includes("gateway timeout") || msg.includes("exceeded request buffer limit while retrying upstream")) {
130
131
  return "provider_error";
131
132
  }
132
133
  if (isOpaqueProviderMessage(err.message)) {
@@ -232,6 +233,8 @@ async function* agentLoop(messages, options) {
232
233
  const maxContinuations = options.maxContinuations ?? 5;
233
234
  let toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
234
235
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
236
+ let latestProviderUsage;
237
+ let usageAnchorIndex;
235
238
  let turn = 0;
236
239
  let hitMaxTurns = false;
237
240
  let firstTurn = true;
@@ -268,10 +271,14 @@ async function* agentLoop(messages, options) {
268
271
  const initialHardTimeoutMs = isSakana ? STREAM_THINKING_HARD_TIMEOUT_MS : STREAM_HARD_TIMEOUT_MS;
269
272
  const MAX_TOOLCALL_DELTA_CHARS = 1e6;
270
273
  const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
274
+ let logicalTurnStartedAt = 0;
275
+ let firstProviderEventAt;
276
+ let providerDurationMs = 0;
271
277
  try {
272
278
  while (turn < maxTurns) {
273
279
  options.signal?.throwIfAborted();
274
280
  turn++;
281
+ if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
275
282
  toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
276
283
  if (_diagFn) {
277
284
  let msgChars = 0;
@@ -304,7 +311,11 @@ async function* agentLoop(messages, options) {
304
311
  firstTurn = false;
305
312
  if (options.transformContext) {
306
313
  diag("transform_start");
307
- const transformed = await options.transformContext(messages);
314
+ const pendingMessages = usageAnchorIndex === void 0 ? [] : messages.slice(usageAnchorIndex + 1);
315
+ const transformed = await options.transformContext(messages, {
316
+ usage: latestProviderUsage,
317
+ pendingMessages
318
+ });
308
319
  if (transformed !== messages) {
309
320
  diag("transform_compacted", {
310
321
  before: messages.length,
@@ -312,6 +323,8 @@ async function* agentLoop(messages, options) {
312
323
  });
313
324
  messages.length = 0;
314
325
  messages.push(...transformed);
326
+ latestProviderUsage = void 0;
327
+ usageAnchorIndex = void 0;
315
328
  }
316
329
  diag("transform_end");
317
330
  }
@@ -321,6 +334,7 @@ async function* agentLoop(messages, options) {
321
334
  let idleTimer = null;
322
335
  let hardTimer = null;
323
336
  let idleTimedOut = false;
337
+ let providerAttemptStartedAt;
324
338
  let streamEventCount = 0;
325
339
  let lastEventTime = Date.now();
326
340
  let streamCallStart = Date.now();
@@ -366,12 +380,14 @@ async function* agentLoop(messages, options) {
366
380
  try {
367
381
  diag("stream_call", { nonStreaming: useNonStreamingFallback });
368
382
  streamCallStart = Date.now();
383
+ providerAttemptStartedAt = streamCallStart;
369
384
  const result = stream({
370
385
  provider: options.provider,
371
386
  model: options.model,
372
387
  messages,
373
388
  tools: options.tools,
374
389
  serverTools: options.serverTools,
390
+ toolChoice: options.toolChoice,
375
391
  webSearch: options.webSearch,
376
392
  maxTokens: options.maxTokens,
377
393
  temperature: options.temperature,
@@ -414,6 +430,7 @@ async function* agentLoop(messages, options) {
414
430
  maxConsumerLagMs = consumerLag;
415
431
  }
416
432
  streamEventCount++;
433
+ if (firstProviderEventAt === void 0) firstProviderEventAt = pullTime;
417
434
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
418
435
  lastEventType = event.type;
419
436
  if ((event.type === "text_delta" || event.type === "server_toolcall" || event.type === "toolcall_delta") && !hasReceivedEvent) {
@@ -506,6 +523,7 @@ async function* agentLoop(messages, options) {
506
523
  eventTypes: eventTypeCounts
507
524
  });
508
525
  response = await abortablePromise(result.response, streamController.signal);
526
+ if (firstProviderEventAt === void 0) firstProviderEventAt = Date.now();
509
527
  } catch (err) {
510
528
  if (streamController.signal.aborted) closeIterator(streamIterator);
511
529
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -567,10 +585,17 @@ async function* agentLoop(messages, options) {
567
585
  ...overflowDetails
568
586
  });
569
587
  try {
570
- const compacted = await options.transformContext(messages, { force: true });
588
+ const pendingMessages = usageAnchorIndex === void 0 ? [] : messages.slice(usageAnchorIndex + 1);
589
+ const compacted = await options.transformContext(messages, {
590
+ force: true,
591
+ usage: latestProviderUsage,
592
+ pendingMessages
593
+ });
571
594
  if (compacted !== messages && compacted.length < messages.length) {
572
595
  messages.length = 0;
573
596
  messages.push(...compacted);
597
+ latestProviderUsage = void 0;
598
+ usageAnchorIndex = void 0;
574
599
  diag("overflow_compact_success", {
575
600
  attempt: overflowCompactionAttempts,
576
601
  messages: messages.length,
@@ -730,6 +755,9 @@ async function* agentLoop(messages, options) {
730
755
  });
731
756
  throw err;
732
757
  } finally {
758
+ if (providerAttemptStartedAt !== void 0) {
759
+ providerDurationMs += Date.now() - providerAttemptStartedAt;
760
+ }
733
761
  if (idleTimer) clearTimeout(idleTimer);
734
762
  if (hardTimer) clearTimeout(hardTimer);
735
763
  options.signal?.removeEventListener("abort", forwardAbort);
@@ -773,11 +801,29 @@ async function* agentLoop(messages, options) {
773
801
  totalUsage.cacheWrite = (totalUsage.cacheWrite ?? 0) + response.usage.cacheWrite;
774
802
  }
775
803
  messages.push(response.message);
804
+ latestProviderUsage = response.usage;
805
+ usageAnchorIndex = messages.length - 1;
806
+ const completedAt = Date.now();
807
+ const outputTokensPerSecond = providerDurationMs > 0 && response.usage.outputTokens > 0 ? response.usage.outputTokens / (providerDurationMs / 1e3) : void 0;
808
+ const timing = {
809
+ startedAt: logicalTurnStartedAt,
810
+ ...firstProviderEventAt !== void 0 ? {
811
+ firstProviderEventAt,
812
+ ttftMs: Math.max(0, firstProviderEventAt - logicalTurnStartedAt)
813
+ } : {},
814
+ completedAt,
815
+ providerDurationMs,
816
+ ...outputTokensPerSecond !== void 0 ? { outputTokensPerSecond } : {}
817
+ };
818
+ logicalTurnStartedAt = 0;
819
+ firstProviderEventAt = void 0;
820
+ providerDurationMs = 0;
776
821
  yield {
777
822
  type: "turn_end",
778
823
  turn,
779
824
  stopReason: response.stopReason,
780
- usage: response.usage
825
+ usage: response.usage,
826
+ timing
781
827
  };
782
828
  if (response.stopReason === "pause_turn") {
783
829
  consecutivePauses++;
@@ -844,6 +890,7 @@ async function* agentLoop(messages, options) {
844
890
  const executionOptions = {
845
891
  signal: options.signal,
846
892
  maxToolResultChars: options.maxToolResultChars,
893
+ maxTurnToolResultChars: options.maxTurnToolResultChars,
847
894
  toolMap,
848
895
  invalidToolArgumentCounts,
849
896
  markFatalToolArgumentError
@@ -961,8 +1008,8 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
961
1008
  ctx.signal
962
1009
  );
963
1010
  const normalized = normalizeToolResult(raw);
964
- resultContent = normalized.content;
965
- details = normalized.details;
1011
+ resultContent = redactValue(normalized.content);
1012
+ details = redactValue(normalized.details);
966
1013
  for (const key of options.invalidToolArgumentCounts.keys()) {
967
1014
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
968
1015
  }
@@ -990,10 +1037,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
990
1037
  );
991
1038
  }
992
1039
  } else {
993
- resultContent = err instanceof Error ? err.message : String(err);
1040
+ resultContent = redactValue(err instanceof Error ? err.message : String(err));
994
1041
  }
995
1042
  }
996
1043
  }
1044
+ resultContent = redactValue(resultContent);
1045
+ details = redactValue(details);
997
1046
  const durationMs = Date.now() - startTime;
998
1047
  pushEvent({
999
1048
  type: "tool_call_end",
@@ -1081,6 +1130,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1081
1130
  }
1082
1131
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1083
1132
  capToolResults(toolResults, options.maxToolResultChars);
1133
+ capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1084
1134
  return { toolResults, aborted };
1085
1135
  }
1086
1136
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
@@ -1120,6 +1170,7 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1120
1170
  }
1121
1171
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1122
1172
  capToolResults(toolResults, options.maxToolResultChars);
1173
+ capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1123
1174
  return { toolResults, aborted };
1124
1175
  }
1125
1176
  function buildToolResults(initialToolResults, toolCalls, resultsById) {
@@ -1162,6 +1213,34 @@ function capToolResults(toolResults, maxToolResultChars) {
1162
1213
  ` + tail;
1163
1214
  }
1164
1215
  }
1216
+ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
1217
+ if (!maxTurnToolResultChars) return;
1218
+ const textResults = toolResults.filter(
1219
+ (toolResult) => typeof toolResult.content === "string"
1220
+ );
1221
+ const total = textResults.reduce((sum, toolResult) => sum + toolResult.content.length, 0);
1222
+ if (total <= maxTurnToolResultChars) return;
1223
+ const bySize = [...textResults].sort((a, b) => a.content.length - b.content.length);
1224
+ let remaining = maxTurnToolResultChars;
1225
+ let left = bySize.length;
1226
+ for (const toolResult of bySize) {
1227
+ const fairShare = Math.floor(remaining / left);
1228
+ left--;
1229
+ if (toolResult.content.length <= fairShare) {
1230
+ remaining -= toolResult.content.length;
1231
+ continue;
1232
+ }
1233
+ remaining -= fairShare;
1234
+ const headChars = Math.floor(fairShare * 0.7);
1235
+ const tailChars = fairShare - headChars;
1236
+ const omitted = toolResult.content.length - fairShare;
1237
+ toolResult.content = toolResult.content.slice(0, headChars) + `
1238
+
1239
+ [... ${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 ...]
1240
+
1241
+ ` + (tailChars > 0 ? toolResult.content.slice(-tailChars) : "");
1242
+ }
1243
+ }
1165
1244
  function normalizeToolResult(raw) {
1166
1245
  return typeof raw === "string" ? { content: raw } : raw;
1167
1246
  }