@librechat/agents 3.9.3 → 3.9.5

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.
Files changed (125) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/events.cjs +11 -0
  5. package/dist/cjs/events.cjs.map +1 -1
  6. package/dist/cjs/graphs/Graph.cjs +41 -1
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
  9. package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
  10. package/dist/cjs/llm/invoke.cjs +10 -5
  11. package/dist/cjs/llm/invoke.cjs.map +1 -1
  12. package/dist/cjs/llm/streamLimits.cjs +1 -1
  13. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  14. package/dist/cjs/main.cjs +2 -0
  15. package/dist/cjs/messages/fading.cjs +14 -6
  16. package/dist/cjs/messages/fading.cjs.map +1 -1
  17. package/dist/cjs/messages/prune.cjs +95 -36
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/openai/index.cjs +2 -0
  20. package/dist/cjs/openai/index.cjs.map +1 -1
  21. package/dist/cjs/openai/toolProjection.cjs +196 -0
  22. package/dist/cjs/openai/toolProjection.cjs.map +1 -0
  23. package/dist/cjs/run.cjs +8 -1
  24. package/dist/cjs/run.cjs.map +1 -1
  25. package/dist/cjs/session/AgentSession.cjs +1 -1
  26. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  27. package/dist/cjs/stream.cjs +9 -4
  28. package/dist/cjs/stream.cjs.map +1 -1
  29. package/dist/cjs/tools/ArtifactTruncation.cjs +45 -0
  30. package/dist/cjs/tools/ArtifactTruncation.cjs.map +1 -0
  31. package/dist/cjs/tools/BashExecutor.cjs +6 -1
  32. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +6 -1
  34. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  35. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -1
  36. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +2 -1
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
  40. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  41. package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
  42. package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
  43. package/dist/esm/common/enum.mjs +2 -0
  44. package/dist/esm/common/enum.mjs.map +1 -1
  45. package/dist/esm/events.mjs +11 -0
  46. package/dist/esm/events.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +41 -1
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
  50. package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
  51. package/dist/esm/llm/invoke.mjs +10 -5
  52. package/dist/esm/llm/invoke.mjs.map +1 -1
  53. package/dist/esm/llm/streamLimits.mjs +1 -1
  54. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  55. package/dist/esm/main.mjs +3 -3
  56. package/dist/esm/messages/fading.mjs +14 -7
  57. package/dist/esm/messages/fading.mjs.map +1 -1
  58. package/dist/esm/messages/prune.mjs +95 -37
  59. package/dist/esm/messages/prune.mjs.map +1 -1
  60. package/dist/esm/openai/index.mjs +2 -1
  61. package/dist/esm/openai/index.mjs.map +1 -1
  62. package/dist/esm/openai/toolProjection.mjs +196 -0
  63. package/dist/esm/openai/toolProjection.mjs.map +1 -0
  64. package/dist/esm/run.mjs +8 -1
  65. package/dist/esm/run.mjs.map +1 -1
  66. package/dist/esm/session/AgentSession.mjs +1 -1
  67. package/dist/esm/session/AgentSession.mjs.map +1 -1
  68. package/dist/esm/stream.mjs +9 -4
  69. package/dist/esm/stream.mjs.map +1 -1
  70. package/dist/esm/tools/ArtifactTruncation.mjs +44 -0
  71. package/dist/esm/tools/ArtifactTruncation.mjs.map +1 -0
  72. package/dist/esm/tools/BashExecutor.mjs +6 -1
  73. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  74. package/dist/esm/tools/CodeExecutor.mjs +6 -1
  75. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  76. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -1
  77. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  78. package/dist/esm/tools/ToolNode.mjs +2 -1
  79. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  80. package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
  81. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  82. package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
  83. package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
  84. package/dist/types/common/enum.d.ts +4 -0
  85. package/dist/types/graphs/Graph.d.ts +3 -1
  86. package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
  87. package/dist/types/messages/fading.d.ts +14 -2
  88. package/dist/types/messages/prune.d.ts +7 -0
  89. package/dist/types/openai/arguments.d.ts +2 -0
  90. package/dist/types/openai/index.d.ts +2 -0
  91. package/dist/types/openai/toolProjection.d.ts +30 -0
  92. package/dist/types/run.d.ts +1 -0
  93. package/dist/types/tools/ArtifactTruncation.d.ts +3 -0
  94. package/dist/types/tools/ToolNode.d.ts +1 -1
  95. package/dist/types/types/graph.d.ts +5 -3
  96. package/dist/types/types/run.d.ts +5 -0
  97. package/dist/types/types/stream.d.ts +22 -1
  98. package/dist/types/types/tools.d.ts +14 -0
  99. package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
  100. package/package.json +1 -1
  101. package/src/common/enum.ts +5 -0
  102. package/src/events.ts +24 -1
  103. package/src/graphs/Graph.ts +95 -0
  104. package/src/graphs/acceptedModelResponse.ts +307 -0
  105. package/src/llm/invoke.ts +39 -14
  106. package/src/llm/streamLimits.ts +1 -1
  107. package/src/messages/fading.ts +30 -5
  108. package/src/messages/prune.ts +168 -55
  109. package/src/openai/arguments.ts +2 -0
  110. package/src/openai/index.ts +6 -0
  111. package/src/openai/toolProjection.ts +318 -0
  112. package/src/run.ts +21 -0
  113. package/src/session/AgentSession.ts +2 -2
  114. package/src/stream.ts +21 -1
  115. package/src/tools/ArtifactTruncation.ts +85 -0
  116. package/src/tools/BashExecutor.ts +22 -5
  117. package/src/tools/CodeExecutor.ts +18 -1
  118. package/src/tools/ProgrammaticToolCalling.ts +17 -3
  119. package/src/tools/ToolNode.ts +5 -0
  120. package/src/tools/subagent/SubagentReplay.ts +10 -9
  121. package/src/types/graph.ts +15 -9
  122. package/src/types/run.ts +5 -0
  123. package/src/types/stream.ts +26 -0
  124. package/src/types/tools.ts +21 -0
  125. package/src/utils/acceptedToolArguments.ts +204 -0
@@ -0,0 +1,318 @@
1
+ import type {
2
+ OpenAIChatCompletionChunkChoice,
3
+ OpenAIToolCall,
4
+ OpenAIStreamTracker,
5
+ } from './index';
6
+ import type { EventHandler, ModelResponseEvent } from '@/types';
7
+ import { serializeToolArguments } from './arguments';
8
+ import { GraphEvents } from '@/common';
9
+
10
+ interface OpenAIToolCallStreamOptions {
11
+ /** Synchronous framing only. Async transport/backpressure is a separate boundary. */
12
+ emit?: (delta: OpenAIChatCompletionChunkChoice['delta']) => void;
13
+ signal?: AbortSignal;
14
+ /** Bound retained complete-before-publish output, not model execution. Default: 1024. */
15
+ maxToolCalls?: number;
16
+ /** UTF-8 bytes of serialized arguments, names and IDs retained across accepted responses. Default: 4 MiB. */
17
+ maxBufferedBytes?: number;
18
+ }
19
+
20
+ /** Streaming hosts share the finalizer's tracker; map-only hosts collect JSON output. */
21
+ export type OpenAIToolCallStreamConfig = OpenAIToolCallStreamOptions &
22
+ (
23
+ | {
24
+ tracker: OpenAIStreamTracker;
25
+ toolCalls?: never;
26
+ emit: NonNullable<OpenAIToolCallStreamOptions['emit']>;
27
+ }
28
+ | { toolCalls: Map<number, OpenAIToolCall>; tracker?: never }
29
+ );
30
+
31
+ export interface OpenAIToolCallStream {
32
+ /** Pass directly to Run.create({ customHandlers: stream.handlers }). */
33
+ handlers: Record<string, EventHandler>;
34
+ /** Publish only after the host verifies that the entire run completed successfully. */
35
+ finish: () => void;
36
+ abort: () => void;
37
+ }
38
+
39
+ function positiveLimit(value: number | undefined, fallback: number): number {
40
+ const limit = value ?? fallback;
41
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
42
+ throw new Error('Tool projection limits must be positive safe integers');
43
+ }
44
+ return limit;
45
+ }
46
+
47
+ /** Serializes finalized, accepted tool calls. No provider fragments or attempt inference. */
48
+ export function createOpenAIToolCallStream(
49
+ config: OpenAIToolCallStreamConfig
50
+ ): OpenAIToolCallStream {
51
+ const { emit, signal, tracker } = config;
52
+ const suppliedMap: unknown = config.toolCalls;
53
+ const suppliedEmitter: unknown = emit;
54
+ if (tracker != null && typeof suppliedEmitter !== 'function') {
55
+ throw new Error('A streaming tracker requires an emitter');
56
+ }
57
+ if (tracker != null && suppliedMap != null) {
58
+ throw new Error('Provide a tracker or a tool-call map, not both');
59
+ }
60
+ const toolCalls = tracker?.toolCalls ?? config.toolCalls;
61
+ if (toolCalls == null)
62
+ throw new Error('Provide a tracker or a tool-call map');
63
+ if (toolCalls.size !== 0)
64
+ throw new Error('Tool projection requires an empty output map');
65
+ let previousChunkKind: OpenAIStreamTracker['lastChunkKind'];
66
+ let acceptedTerminalKind: OpenAIStreamTracker['lastChunkKind'];
67
+ const maxCalls = positiveLimit(config.maxToolCalls, 1024);
68
+ const maxBytes = positiveLimit(config.maxBufferedBytes, 4 * 1024 * 1024);
69
+ const calls: Array<{
70
+ id: string;
71
+ name: string;
72
+ arguments: string;
73
+ agentId: string;
74
+ messageId?: string;
75
+ acceptedId: string;
76
+ bytes: number;
77
+ }> = [];
78
+ const providerIds = new Set<string>();
79
+ const acceptedIds = new Set<string>();
80
+ let bufferedBytes = 0;
81
+ let phase: 'open' | 'emitting' | 'finished' | 'aborted' = 'open';
82
+
83
+ const release = (): void => {
84
+ calls.length = 0;
85
+ acceptedIds.clear();
86
+ providerIds.clear();
87
+ bufferedBytes = 0;
88
+ };
89
+ const discard = (agentId: string, messageId?: string): void => {
90
+ for (let i = calls.length - 1; i >= 0; i--) {
91
+ const call = calls[i];
92
+ if (
93
+ call.agentId !== agentId ||
94
+ (messageId != null && call.messageId !== messageId)
95
+ )
96
+ continue;
97
+ bufferedBytes -= call.bytes;
98
+ calls.splice(i, 1);
99
+ }
100
+ acceptedIds.clear();
101
+ providerIds.clear();
102
+ for (const call of calls) {
103
+ acceptedIds.add(call.acceptedId);
104
+ if (call.id !== '') providerIds.add(call.id);
105
+ }
106
+ acceptedTerminalKind = calls.length > 0 ? 'tool_call' : 'text';
107
+ };
108
+ const abort = (): void => {
109
+ const phaseBeforeAbort = phase;
110
+ if (phase !== 'finished') {
111
+ phase = 'aborted';
112
+ toolCalls.clear();
113
+ if (tracker != null && phaseBeforeAbort === 'emitting')
114
+ tracker.lastChunkKind = previousChunkKind;
115
+ }
116
+ release();
117
+ };
118
+ const checkCancellation = (): void => {
119
+ if (signal?.aborted === true) abort();
120
+ if (phase === 'aborted') {
121
+ const error = new Error('Agent response aborted');
122
+ error.name = 'AbortError';
123
+ throw error;
124
+ }
125
+ };
126
+ const emitDelta = (delta: OpenAIChatCompletionChunkChoice['delta']): void => {
127
+ const result: unknown = emit?.(delta);
128
+ if (
129
+ result != null &&
130
+ typeof result === 'object' &&
131
+ 'then' in result &&
132
+ typeof result.then === 'function'
133
+ ) {
134
+ // Observe a mistakenly async sink's rejection, then fail closed. Never silently
135
+ // publish a successful terminal response before pending writes settle.
136
+ void Promise.resolve(result).catch(() => undefined);
137
+ throw new Error('Tool projection requires a synchronous emitter');
138
+ }
139
+ };
140
+ const accept = (result: ModelResponseEvent): void => {
141
+ if (phase !== 'open') return;
142
+ try {
143
+ checkCancellation();
144
+ if (result.invalidToolCalls.length > 0) {
145
+ throw new Error('Accepted model response contains invalid tool calls');
146
+ }
147
+ const ownership = result.toolCallDispositions;
148
+ if (
149
+ !Array.isArray(ownership) ||
150
+ ownership.length !== result.toolCalls.length ||
151
+ ownership.some(
152
+ (value) =>
153
+ value !== 'sdk' && value !== 'provider' && value !== 'client'
154
+ )
155
+ ) {
156
+ throw new Error('Accepted tool calls lack trusted execution ownership');
157
+ }
158
+ const delegated = result.toolCalls.filter(
159
+ (_call, index) => ownership[index] === 'client'
160
+ );
161
+ if (delegated.length === 0) {
162
+ // A text or internal-only response supersedes this agent's request,
163
+ // never a sibling's. Neither 'sdk' nor 'provider' enters the wire.
164
+ discard(result.agentId);
165
+ return;
166
+ }
167
+ if (result.id.trim() === '' || acceptedIds.has(result.id)) {
168
+ throw new Error('Missing or repeated accepted model response identity');
169
+ }
170
+ if (calls.length + delegated.length > maxCalls) {
171
+ throw new Error('Tool projection call limit exceeded');
172
+ }
173
+ acceptedIds.add(result.id);
174
+ for (const call of delegated) {
175
+ if (typeof call.name !== 'string' || call.name.trim() === '') {
176
+ throw new Error('Accepted tool call is missing its name');
177
+ }
178
+ const id = call.id ?? '';
179
+ if (typeof id !== 'string')
180
+ throw new Error('Accepted tool call ID must be a string');
181
+ if (id !== '') providerIds.add(id);
182
+ const identityBytes =
183
+ Buffer.byteLength(id, 'utf8') + Buffer.byteLength(call.name, 'utf8');
184
+ const args = serializeToolArguments(
185
+ call.args,
186
+ maxBytes - bufferedBytes - identityBytes
187
+ );
188
+ const bytes = identityBytes + Buffer.byteLength(args, 'utf8');
189
+ bufferedBytes += bytes;
190
+ calls.push({
191
+ id,
192
+ name: call.name,
193
+ arguments: args,
194
+ bytes,
195
+ agentId: result.agentId,
196
+ messageId: result.messageId,
197
+ acceptedId: result.id,
198
+ });
199
+ }
200
+ acceptedTerminalKind = 'tool_call';
201
+ } catch (error) {
202
+ abort();
203
+ throw error;
204
+ }
205
+ };
206
+
207
+ return {
208
+ handlers: {
209
+ [GraphEvents.ON_MODEL_TOOLS_CLAIMED]: {
210
+ handle: (event, data): void => {
211
+ if (
212
+ event !== GraphEvents.ON_MODEL_TOOLS_CLAIMED ||
213
+ data == null ||
214
+ !('type' in data) ||
215
+ data.type !== 'model_tools_claimed'
216
+ )
217
+ return;
218
+ if (phase !== 'open')
219
+ throw new Error('Tool projection is already finalized');
220
+ checkCancellation();
221
+ discard(data.agentId, data.messageId);
222
+ },
223
+ },
224
+ [GraphEvents.ON_MODEL_RESPONSE]: {
225
+ handle: (event, data): void => {
226
+ if (
227
+ event !== GraphEvents.ON_MODEL_RESPONSE ||
228
+ data == null ||
229
+ !('type' in data) ||
230
+ data.type !== 'model_response'
231
+ )
232
+ return;
233
+ accept(data);
234
+ },
235
+ },
236
+ },
237
+ abort,
238
+ finish: (): void => {
239
+ if (phase === 'finished' || phase === 'emitting') return;
240
+ checkCancellation();
241
+ previousChunkKind = tracker?.lastChunkKind;
242
+ phase = 'emitting';
243
+ try {
244
+ if (toolCalls.size !== 0)
245
+ throw new Error(
246
+ 'Tool projection output was modified during collection'
247
+ );
248
+ // Do not allocate synthetic IDs until every accepted response has arrived.
249
+ // This reserves provider IDs even when they occur in a later invocation.
250
+ const usedIds = new Set<string>();
251
+ const ready: OpenAIToolCall[] = [];
252
+ let reservedBytes = bufferedBytes;
253
+ let nextSyntheticId = 0;
254
+ for (const call of calls) {
255
+ let id = call.id;
256
+ if (id === '' || usedIds.has(id)) {
257
+ do {
258
+ id = `call_${nextSyntheticId++}`;
259
+ } while (providerIds.has(id) || usedIds.has(id));
260
+ reservedBytes +=
261
+ Buffer.byteLength(id, 'utf8') -
262
+ Buffer.byteLength(call.id, 'utf8');
263
+ if (reservedBytes > maxBytes)
264
+ throw new Error('Tool projection buffer limit exceeded');
265
+ }
266
+ usedIds.add(id);
267
+ ready.push(
268
+ Object.freeze({
269
+ id,
270
+ type: 'function',
271
+ function: Object.freeze({
272
+ name: call.name,
273
+ arguments: call.arguments,
274
+ }),
275
+ })
276
+ );
277
+ }
278
+ if (tracker != null && ready.length > 0 && !tracker.hasRole) {
279
+ tracker.hasRole = true;
280
+ emitDelta({ role: 'assistant' });
281
+ checkCancellation();
282
+ }
283
+ for (let index = 0; index < ready.length; index++) {
284
+ checkCancellation();
285
+ const call = ready[index];
286
+ toolCalls.set(index, call);
287
+ if (tracker != null) tracker.lastChunkKind = acceptedTerminalKind;
288
+ emitDelta({
289
+ tool_calls: [
290
+ {
291
+ index,
292
+ id: call.id,
293
+ type: 'function',
294
+ function: { name: call.function.name, arguments: '' },
295
+ },
296
+ ],
297
+ });
298
+ checkCancellation();
299
+ emitDelta({
300
+ tool_calls: [
301
+ { index, function: { arguments: call.function.arguments } },
302
+ ],
303
+ });
304
+ checkCancellation();
305
+ }
306
+ if (tracker != null && acceptedTerminalKind != null) {
307
+ tracker.lastChunkKind = acceptedTerminalKind;
308
+ }
309
+ phase = 'finished';
310
+ } catch (error) {
311
+ abort();
312
+ throw error;
313
+ } finally {
314
+ release();
315
+ }
316
+ },
317
+ };
318
+ }
package/src/run.ts CHANGED
@@ -468,6 +468,7 @@ export class Run<_T extends t.BaseGraphState> {
468
468
  private langfuse?: t.LangfuseConfig;
469
469
  private toolOutputReferences?: t.ToolOutputReferencesConfig;
470
470
  private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
471
+ private clientDelegatedToolNames?: readonly string[];
471
472
  private codeSessionToolNames?: string[];
472
473
  private interruptingToolNames?: string[];
473
474
  private toolExecution?: t.ToolExecutionConfig;
@@ -549,12 +550,19 @@ export class Run<_T extends t.BaseGraphState> {
549
550
  }
550
551
  }
551
552
 
553
+ if (
554
+ (config.clientDelegatedToolNames?.length ?? 0) > 0 &&
555
+ handlerRegistry.getHandler(GraphEvents.ON_MODEL_RESPONSE) == null
556
+ ) {
557
+ throw new Error('Client tool delegation requires an accepted-result handler');
558
+ }
552
559
  this.handlerRegistry = handlerRegistry;
553
560
  this.hookRegistry = config.hooks;
554
561
  this.humanInTheLoop = config.humanInTheLoop;
555
562
  this.langfuse = config.langfuse;
556
563
  this.toolOutputReferences = config.toolOutputReferences;
557
564
  this.eagerEventToolExecution = config.eagerEventToolExecution;
565
+ this.clientDelegatedToolNames = config.clientDelegatedToolNames;
558
566
  this.codeSessionToolNames = config.codeSessionToolNames;
559
567
  this.interruptingToolNames = config.interruptingToolNames;
560
568
  this.toolExecution = config.toolExecution;
@@ -573,6 +581,9 @@ export class Run<_T extends t.BaseGraphState> {
573
581
 
574
582
  /** Handle different graph types */
575
583
  if (config.graphConfig.type === 'multi-agent') {
584
+ if (this.clientDelegatedToolNames != null && this.clientDelegatedToolNames.length > 0) {
585
+ throw new Error('Client tool delegation requires a single-agent graph');
586
+ }
576
587
  this.graphRunnable = this.createMultiAgentGraph(config.graphConfig);
577
588
  if (this.Graph) {
578
589
  this.Graph.handlerRegistry = handlerRegistry;
@@ -663,6 +674,7 @@ export class Run<_T extends t.BaseGraphState> {
663
674
  preemption: this.preemption,
664
675
  streamLimits: this.streamLimits,
665
676
  toolExecution: this.toolExecution,
677
+ clientDelegatedToolNames: this.clientDelegatedToolNames,
666
678
  },
667
679
  });
668
680
  /** Propagate compile options from graph config */
@@ -1058,6 +1070,11 @@ export class Run<_T extends t.BaseGraphState> {
1058
1070
  ) {
1059
1071
  return;
1060
1072
  }
1073
+ // Accepted results are graph-owned, never inferred from provider/tool callbacks.
1074
+ if (
1075
+ eventName === GraphEvents.ON_MODEL_RESPONSE ||
1076
+ eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
1077
+ ) return;
1061
1078
  const handler = this.handlerRegistry?.getHandler(eventName);
1062
1079
  /**
1063
1080
  * Tool completions arriving over the custom-event channel are the only
@@ -1554,6 +1571,10 @@ export class Run<_T extends t.BaseGraphState> {
1554
1571
 
1555
1572
  const modelEndAt =
1556
1573
  eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
1574
+ if (
1575
+ eventName === GraphEvents.ON_MODEL_RESPONSE ||
1576
+ eventName === GraphEvents.ON_MODEL_TOOLS_CLAIMED
1577
+ ) continue;
1557
1578
  const handler = this.handlerRegistry?.getHandler(eventName);
1558
1579
  if (handler) {
1559
1580
  await handler.handle(eventName, data, metadata, this.Graph);
@@ -27,6 +27,7 @@ import {
27
27
  deriveSessionMessages,
28
28
  releaseSessionProjection,
29
29
  } from './sessionProjection';
30
+ import { FADING_TIER_VERSION, isFadingTier } from '@/messages/fading';
30
31
  import { createSummarizeNode } from '@/summarization/node';
31
32
  import { resolveStreamLimits } from '@/llm/streamLimits';
32
33
  import { JsonlSessionStore } from './JsonlSessionStore';
@@ -34,7 +35,6 @@ import { AgentContext } from '@/agents/AgentContext';
34
35
  import { ContentTypes, GraphEvents } from '@/common';
35
36
  import { createRunId, createSessionId } from './ids';
36
37
  import { deriveMessages } from './deriveMessages';
37
- import { isFadingTier } from '@/messages/fading';
38
38
  import { createRunHandlers } from './handlers';
39
39
  import { Run } from '@/run';
40
40
 
@@ -420,7 +420,7 @@ function mergeFadingTier(
420
420
  masked !== current.masked ||
421
421
  masked !== incoming.masked;
422
422
  return {
423
- v: 1,
423
+ v: FADING_TIER_VERSION,
424
424
  budgetTokens,
425
425
  masked,
426
426
  ...(latched ? { latched: true } : {}),
package/src/stream.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  truncateToolResultContent,
57
57
  } from '@/utils/truncation';
58
58
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
59
+ import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
59
60
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
60
61
  import { PreparedSubagentError } from '@/tools/preparedSubagents';
61
62
  import { isReasoningContentBlock } from '@/messages/core';
@@ -746,6 +747,10 @@ function createEagerToolExecutionPlan(args: {
746
747
  toolCall.id == null ||
747
748
  toolCall.id === '' ||
748
749
  toolCall.name === '' ||
750
+ // A serialized parsed call is not a prepared executable object;
751
+ // parsing is permitted only on sealed raw tool_call_chunks.
752
+ typeof toolCall.args === 'string' ||
753
+ graph.clientDelegatedToolNames?.has(toolCall.name) === true ||
749
754
  (!skipExisting && graph.eagerEventToolExecutions.has(toolCall.id))
750
755
  )
751
756
  ) {
@@ -794,6 +799,9 @@ function startEagerToolExecutions(args: {
794
799
  skipExisting?: boolean;
795
800
  }): void {
796
801
  const { graph, metadata, agentContext, toolCalls, skipExisting } = args;
802
+ // A later call in the same model turn may be client-delegated. Do not
803
+ // pre-execute an earlier SDK call in a run that rejects mixed batches.
804
+ if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
797
805
  const entries = createEagerToolExecutionPlan({
798
806
  graph,
799
807
  metadata,
@@ -1345,6 +1353,10 @@ function startPreparedSubagents(
1345
1353
  metadata?: Record<string, unknown>
1346
1354
  ): void {
1347
1355
  const attempt = resolveGenerationKey(metadata);
1356
+ if ((graph.clientDelegatedToolNames?.size ?? 0) > 0) return;
1357
+ // A parsed string call is not executable even if the same event carries
1358
+ // sealed raw fragments. Wait for a separately validated complete call.
1359
+ if (chunk.tool_calls?.some((call) => typeof call.args === 'string') === true) return;
1348
1360
  if (
1349
1361
  (graph as Partial<StandardGraph>).canPrestartSubagents?.(agentContext) !==
1350
1362
  true ||
@@ -1389,6 +1401,9 @@ function startPreparedSubagents(
1389
1401
  for (const call of calls) {
1390
1402
  if (
1391
1403
  call.name === Constants.SUBAGENT &&
1404
+ graph.clientDelegatedToolNames?.has(call.name) !== true &&
1405
+ typeof call.args === 'object' &&
1406
+ !Array.isArray(call.args) &&
1392
1407
  !hasToolOutputReference(call.args)
1393
1408
  ) {
1394
1409
  graph.prestartSubagent(call, attempt, agentContext);
@@ -1643,7 +1658,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1643
1658
  return;
1644
1659
  }
1645
1660
 
1646
- const chunk = data.chunk as Partial<AIMessageChunk>;
1661
+ let chunk = data.chunk as Partial<AIMessageChunk>;
1647
1662
 
1648
1663
  /** Attempts stamp their breaker epoch into event metadata; a mismatch
1649
1664
  * marks a straggling chunk from a failed run that outlived
@@ -1681,6 +1696,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
1681
1696
  }
1682
1697
  };
1683
1698
 
1699
+ // Callback delivery can beat the producer's iterator. Validate before
1700
+ // accounting, run steps, or eager dispatch reads raw tool descriptors.
1701
+ chunk = snapshotValidatedModelChunk(chunk as AIMessageChunk);
1702
+
1684
1703
  /**
1685
1704
  * Enforced before every content-specific early return below
1686
1705
  * (server-tool results, deferred mixed reasoning, late OpenRouter
@@ -1885,6 +1904,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1885
1904
  chunk.response_metadata as Record<string, unknown> | undefined
1886
1905
  );
1887
1906
  const canStreamEager =
1907
+ chunk.tool_calls?.some((call) => typeof call.args === 'string') !== true &&
1888
1908
  (allowSequentialSeal || hasExplicitStreamedToolCallSeals(chunk)) &&
1889
1909
  !hasPotentialDirectToolInStreamContext({ graph, agentContext }) &&
1890
1910
  isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext });
@@ -0,0 +1,85 @@
1
+ import type { ArtifactTruncation, ArtifactTruncationReason } from '@/types';
2
+
3
+ const MAX_REPORTED_TRUNCATED_PATHS = 20;
4
+ const ARTIFACT_TRUNCATION_REASONS = new Set<string>([
5
+ 'max_files',
6
+ 'depth',
7
+ 'size',
8
+ 'path',
9
+ 'unreadable',
10
+ ]);
11
+
12
+ function isNonNegativeInteger(value: unknown): value is number {
13
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
14
+ }
15
+
16
+ export function normalizeArtifactTruncation(
17
+ value: unknown
18
+ ): ArtifactTruncation | undefined {
19
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
20
+ return undefined;
21
+ }
22
+
23
+ const candidate = value as Partial<ArtifactTruncation>;
24
+ if (
25
+ candidate.code !== 'artifact_truncated' ||
26
+ !isNonNegativeInteger(candidate.skipped_count) ||
27
+ candidate.skipped_count === 0 ||
28
+ candidate.reasons == null ||
29
+ typeof candidate.reasons !== 'object' ||
30
+ Array.isArray(candidate.reasons) ||
31
+ !Array.isArray(candidate.skipped) ||
32
+ candidate.skipped.length > MAX_REPORTED_TRUNCATED_PATHS ||
33
+ candidate.skipped.length > candidate.skipped_count ||
34
+ candidate.skipped.some((path) => typeof path !== 'string')
35
+ ) {
36
+ return undefined;
37
+ }
38
+
39
+ const reasons: ArtifactTruncation['reasons'] = {};
40
+ let reasonTotal = 0;
41
+ for (const [reason, count] of Object.entries(candidate.reasons)) {
42
+ if (
43
+ !ARTIFACT_TRUNCATION_REASONS.has(reason) ||
44
+ !isNonNegativeInteger(count)
45
+ ) {
46
+ return undefined;
47
+ }
48
+ reasonTotal += count;
49
+ if (reasonTotal > candidate.skipped_count) {
50
+ return undefined;
51
+ }
52
+ reasons[reason as ArtifactTruncationReason] = count;
53
+ }
54
+
55
+ if (reasonTotal !== candidate.skipped_count) {
56
+ return undefined;
57
+ }
58
+
59
+ return {
60
+ code: candidate.code,
61
+ reasons,
62
+ skipped: [...candidate.skipped],
63
+ skipped_count: candidate.skipped_count,
64
+ };
65
+ }
66
+
67
+ export function appendArtifactTruncationWarning(
68
+ output: string,
69
+ truncation: ArtifactTruncation | undefined
70
+ ): string {
71
+ if (truncation == null) {
72
+ return output;
73
+ }
74
+
75
+ const reasons = Object.entries(truncation.reasons)
76
+ .map(([reason, count]) => `${reason}: ${count}`)
77
+ .join(', ');
78
+ const shown = truncation.skipped.length;
79
+ const paths =
80
+ shown > 0
81
+ ? ` Not delivered: ${truncation.skipped.join(', ')}${shown < truncation.skipped_count ? ` (${shown} of ${truncation.skipped_count} shown)` : ''}.`
82
+ : '';
83
+ const warning = `Note: ${truncation.skipped_count} file(s) were omitted from delivery${reasons ? ` (${reasons})` : ''}.${paths} Write fewer files per execution or combine them into an archive. The code itself ran; do not rerun automatically because it may have had side effects.`;
84
+ return `${output.trimEnd()}\n${warning}\n`;
85
+ }
@@ -23,14 +23,18 @@ import {
23
23
  appendArtifactDeliveryWarning,
24
24
  normalizeArtifactDeliveryFailure,
25
25
  } from '@/tools/ArtifactDelivery';
26
- import { logCodeApiDiagnostic } from '@/tools/diagnostics';
26
+ import {
27
+ appendArtifactTruncationWarning,
28
+ normalizeArtifactTruncation,
29
+ } from '@/tools/ArtifactTruncation';
27
30
  import { appendExecutionArtifactFileSummary } from '@/tools/CodeSessionFileSummary';
28
31
  import { resolveAttachedWorkspaceInstanceId } from '@/tools/workspaceIdentity';
32
+ import { prepareBashProgrammaticCode } from './BashProgrammaticToolCalling';
33
+ import { logCodeApiDiagnostic } from '@/tools/diagnostics';
34
+ import { makeRequest } from './ProgrammaticToolCalling';
29
35
  import { resolveFetchProxyAgent } from '@/utils/proxy';
30
36
  import { INTENT_PROPERTY } from '@/tools/intentArg';
31
37
  import { Constants } from '@/common';
32
- import { prepareBashProgrammaticCode } from './BashProgrammaticToolCalling';
33
- import { makeRequest } from './ProgrammaticToolCalling';
34
38
 
35
39
  config();
36
40
 
@@ -407,6 +411,13 @@ function createBashExecutionTool(
407
411
  outputWithReminder,
408
412
  artifactDelivery
409
413
  );
414
+ const artifactTruncation = normalizeArtifactTruncation(
415
+ result.artifact_truncation
416
+ );
417
+ const outputWithWarnings = appendArtifactTruncationWarning(
418
+ outputWithDeliveryWarning,
419
+ artifactTruncation
420
+ );
410
421
  const hasFiles = result.files != null && result.files.length > 0;
411
422
  const deletionEcho =
412
423
  result.deleted_files != null
@@ -422,11 +433,11 @@ function createBashExecutionTool(
422
433
  return [
423
434
  hasWorkspace
424
435
  ? appendExecutionArtifactFileSummary(
425
- outputWithDeliveryWarning,
436
+ outputWithWarnings,
426
437
  result.files
427
438
  )
428
439
  : appendCodeSessionFileSummary(
429
- outputWithDeliveryWarning,
440
+ outputWithWarnings,
430
441
  result.files
431
442
  ),
432
443
  (hasFiles
@@ -436,6 +447,9 @@ function createBashExecutionTool(
436
447
  ...(artifactDelivery != null
437
448
  ? { artifact_delivery: artifactDelivery }
438
449
  : {}),
450
+ ...(artifactTruncation != null
451
+ ? { artifact_truncation: artifactTruncation }
452
+ : {}),
439
453
  ...deletionEcho,
440
454
  ...runtimeEcho,
441
455
  }
@@ -444,6 +458,9 @@ function createBashExecutionTool(
444
458
  ...(artifactDelivery != null
445
459
  ? { artifact_delivery: artifactDelivery }
446
460
  : {}),
461
+ ...(artifactTruncation != null
462
+ ? { artifact_truncation: artifactTruncation }
463
+ : {}),
447
464
  ...deletionEcho,
448
465
  ...runtimeEcho,
449
466
  }) satisfies t.CodeExecutionArtifact,
@@ -9,6 +9,10 @@ import {
9
9
  appendArtifactDeliveryWarning,
10
10
  normalizeArtifactDeliveryFailure,
11
11
  } from '@/tools/ArtifactDelivery';
12
+ import {
13
+ appendArtifactTruncationWarning,
14
+ normalizeArtifactTruncation,
15
+ } from '@/tools/ArtifactTruncation';
12
16
  import {
13
17
  describeCodeApiError,
14
18
  logCodeApiDiagnostic,
@@ -671,6 +675,13 @@ function createCodeExecutionTool(
671
675
  outputWithReminder,
672
676
  artifactDelivery
673
677
  );
678
+ const artifactTruncation = normalizeArtifactTruncation(
679
+ result.artifact_truncation
680
+ );
681
+ const outputWithWarnings = appendArtifactTruncationWarning(
682
+ outputWithDeliveryWarning,
683
+ artifactTruncation
684
+ );
674
685
  const hasFiles = result.files != null && result.files.length > 0;
675
686
  const deletionEcho =
676
687
  result.deleted_files != null
@@ -687,7 +698,7 @@ function createCodeExecutionTool(
687
698
  }
688
699
  : {};
689
700
  return [
690
- appendCodeSessionFileSummary(outputWithDeliveryWarning, result.files),
701
+ appendCodeSessionFileSummary(outputWithWarnings, result.files),
691
702
  (hasFiles
692
703
  ? {
693
704
  session_id: result.session_id,
@@ -695,6 +706,9 @@ function createCodeExecutionTool(
695
706
  ...(artifactDelivery != null
696
707
  ? { artifact_delivery: artifactDelivery }
697
708
  : {}),
709
+ ...(artifactTruncation != null
710
+ ? { artifact_truncation: artifactTruncation }
711
+ : {}),
698
712
  ...deletionEcho,
699
713
  ...runtimeEcho,
700
714
  }
@@ -703,6 +717,9 @@ function createCodeExecutionTool(
703
717
  ...(artifactDelivery != null
704
718
  ? { artifact_delivery: artifactDelivery }
705
719
  : {}),
720
+ ...(artifactTruncation != null
721
+ ? { artifact_truncation: artifactTruncation }
722
+ : {}),
706
723
  ...deletionEcho,
707
724
  ...runtimeEcho,
708
725
  }) satisfies t.CodeExecutionArtifact,