@librechat/agents 3.2.33 → 3.2.35

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 (133) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +47 -10
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +121 -3
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/llm/bedrock/index.cjs +21 -2
  8. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +38 -2
  10. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +6 -0
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +49 -8
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +48 -1
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/vertexai/index.cjs +19 -0
  18. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +2 -0
  20. package/dist/cjs/messages/content.cjs +12 -14
  21. package/dist/cjs/messages/content.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +31 -13
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +7 -2
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/stream.cjs +20 -2
  27. package/dist/cjs/stream.cjs.map +1 -1
  28. package/dist/cjs/summarization/node.cjs +12 -1
  29. package/dist/cjs/summarization/node.cjs.map +1 -1
  30. package/dist/cjs/tools/ToolNode.cjs +41 -4
  31. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  32. package/dist/cjs/tools/streamedToolCallSeals.cjs +30 -1
  33. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +138 -2
  35. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  36. package/dist/cjs/utils/tokens.cjs +30 -0
  37. package/dist/cjs/utils/tokens.cjs.map +1 -1
  38. package/dist/esm/agents/AgentContext.mjs +47 -10
  39. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  40. package/dist/esm/common/enum.mjs +13 -0
  41. package/dist/esm/common/enum.mjs.map +1 -1
  42. package/dist/esm/graphs/Graph.mjs +122 -4
  43. package/dist/esm/graphs/Graph.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +22 -3
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +38 -3
  47. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  48. package/dist/esm/llm/google/utils/common.mjs +6 -0
  49. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  50. package/dist/esm/llm/invoke.mjs +49 -8
  51. package/dist/esm/llm/invoke.mjs.map +1 -1
  52. package/dist/esm/llm/openai/index.mjs +48 -1
  53. package/dist/esm/llm/openai/index.mjs.map +1 -1
  54. package/dist/esm/llm/vertexai/index.mjs +19 -0
  55. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  56. package/dist/esm/main.mjs +3 -3
  57. package/dist/esm/messages/content.mjs +12 -15
  58. package/dist/esm/messages/content.mjs.map +1 -1
  59. package/dist/esm/messages/prune.mjs +31 -13
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/run.mjs +7 -2
  62. package/dist/esm/run.mjs.map +1 -1
  63. package/dist/esm/stream.mjs +21 -3
  64. package/dist/esm/stream.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +12 -1
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/ToolNode.mjs +41 -4
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/streamedToolCallSeals.mjs +25 -2
  70. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  71. package/dist/esm/tools/subagent/SubagentExecutor.mjs +138 -2
  72. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  73. package/dist/esm/utils/tokens.mjs +30 -1
  74. package/dist/esm/utils/tokens.mjs.map +1 -1
  75. package/dist/types/agents/AgentContext.d.ts +7 -3
  76. package/dist/types/common/enum.d.ts +13 -0
  77. package/dist/types/graphs/Graph.d.ts +8 -1
  78. package/dist/types/llm/bedrock/utils/index.d.ts +1 -1
  79. package/dist/types/llm/bedrock/utils/message_outputs.d.ts +9 -0
  80. package/dist/types/llm/invoke.d.ts +1 -1
  81. package/dist/types/llm/vertexai/index.d.ts +10 -0
  82. package/dist/types/messages/content.d.ts +5 -0
  83. package/dist/types/messages/prune.d.ts +4 -0
  84. package/dist/types/run.d.ts +1 -0
  85. package/dist/types/tools/ToolNode.d.ts +8 -0
  86. package/dist/types/tools/streamedToolCallSeals.d.ts +5 -1
  87. package/dist/types/tools/subagent/SubagentExecutor.d.ts +11 -1
  88. package/dist/types/types/graph.d.ts +89 -3
  89. package/dist/types/types/run.d.ts +13 -0
  90. package/dist/types/types/tools.d.ts +10 -0
  91. package/dist/types/utils/tokens.d.ts +7 -0
  92. package/package.json +1 -1
  93. package/src/__tests__/stream.eagerEventExecution.test.ts +703 -0
  94. package/src/agents/AgentContext.ts +69 -6
  95. package/src/agents/__tests__/AgentContext.test.ts +6 -2
  96. package/src/common/enum.ts +13 -0
  97. package/src/graphs/Graph.ts +196 -0
  98. package/src/llm/bedrock/index.ts +40 -0
  99. package/src/llm/bedrock/streamSealDispatch.test.ts +158 -0
  100. package/src/llm/bedrock/utils/index.ts +1 -0
  101. package/src/llm/bedrock/utils/message_outputs.test.ts +85 -0
  102. package/src/llm/bedrock/utils/message_outputs.ts +43 -0
  103. package/src/llm/google/utils/common.test.ts +64 -0
  104. package/src/llm/google/utils/common.ts +18 -0
  105. package/src/llm/invoke.test.ts +79 -1
  106. package/src/llm/invoke.ts +58 -4
  107. package/src/llm/openai/index.ts +95 -1
  108. package/src/llm/openai/sequentialToolCallSeals.test.ts +199 -0
  109. package/src/llm/vertexai/index.ts +31 -0
  110. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +88 -0
  111. package/src/llm/vertexai/streamSealDispatch.test.ts +148 -0
  112. package/src/messages/content.ts +24 -32
  113. package/src/messages/prune.ts +39 -2
  114. package/src/run.ts +5 -0
  115. package/src/scripts/subagent-usage-sink.ts +176 -0
  116. package/src/specs/context-accuracy.live.test.ts +409 -0
  117. package/src/specs/context-usage-event.test.ts +117 -0
  118. package/src/specs/context-usage.live.test.ts +297 -0
  119. package/src/specs/prune.test.ts +51 -1
  120. package/src/specs/subagent.test.ts +124 -1
  121. package/src/stream.ts +40 -6
  122. package/src/summarization/__tests__/node.test.ts +60 -1
  123. package/src/summarization/node.ts +20 -1
  124. package/src/tools/ToolNode.ts +85 -3
  125. package/src/tools/__tests__/SubagentExecutor.test.ts +443 -1
  126. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +368 -0
  127. package/src/tools/streamedToolCallSeals.ts +37 -9
  128. package/src/tools/subagent/SubagentExecutor.ts +221 -3
  129. package/src/types/graph.ts +94 -1
  130. package/src/types/run.ts +13 -0
  131. package/src/types/tools.ts +10 -0
  132. package/src/utils/__tests__/apportion.test.ts +32 -0
  133. package/src/utils/tokens.ts +33 -0
@@ -0,0 +1,368 @@
1
+ import { z } from 'zod';
2
+ import { tool } from '@langchain/core/tools';
3
+ import { AIMessage, ToolMessage } from '@langchain/core/messages';
4
+ import { describe, it, expect, jest, afterEach } from '@jest/globals';
5
+ import type { StructuredToolInterface } from '@langchain/core/tools';
6
+ import type * as t from '@/types';
7
+ import * as events from '@/utils/events';
8
+ import { GraphEvents } from '@/common';
9
+ import { HookRegistry } from '@/hooks';
10
+ import { ToolNode } from '../ToolNode';
11
+
12
+ function createDummyTool(name: string): StructuredToolInterface {
13
+ return tool(async () => 'direct should not run', {
14
+ name,
15
+ description: 'dummy',
16
+ schema: z.object({}).passthrough(),
17
+ }) as unknown as StructuredToolInterface;
18
+ }
19
+
20
+ function createAIMessageWithToolCalls(
21
+ toolCalls: Array<{ id: string; name: string; args: Record<string, unknown> }>
22
+ ): AIMessage {
23
+ return new AIMessage({
24
+ content: '',
25
+ tool_calls: toolCalls,
26
+ });
27
+ }
28
+
29
+ type CompletionEvent = {
30
+ result: {
31
+ id: string;
32
+ tool_call: { id: string; output: string };
33
+ };
34
+ };
35
+
36
+ function flushAsync(): Promise<void> {
37
+ return new Promise((resolve) => setTimeout(resolve, 0));
38
+ }
39
+
40
+ describe('ToolNode per-call onResult completion emission', () => {
41
+ afterEach(() => {
42
+ jest.restoreAllMocks();
43
+ });
44
+
45
+ it('emits a completion as the host reports each result, before the batch resolves', async () => {
46
+ const timeline: string[] = [];
47
+ const completions: CompletionEvent[] = [];
48
+
49
+ jest
50
+ .spyOn(events, 'safeDispatchCustomEvent')
51
+ .mockImplementation(async (event, data): Promise<void> => {
52
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
53
+ const completion = data as CompletionEvent;
54
+ completions.push(completion);
55
+ timeline.push(`completed:${completion.result.tool_call.id}`);
56
+ return;
57
+ }
58
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
59
+ return;
60
+ }
61
+ const batch = data as t.ToolExecuteBatchRequest;
62
+ expect(batch.onResult).toBeDefined();
63
+
64
+ // Host finishes the fast call first and reports it immediately.
65
+ timeline.push('onResult:call_weather');
66
+ batch.onResult?.({
67
+ toolCallId: 'call_weather',
68
+ status: 'success',
69
+ content: 'sunny',
70
+ });
71
+
72
+ await flushAsync();
73
+ timeline.push('resolving-batch');
74
+ batch.resolve([
75
+ {
76
+ toolCallId: 'call_weather',
77
+ status: 'success',
78
+ content: 'sunny',
79
+ },
80
+ {
81
+ toolCallId: 'call_stock',
82
+ status: 'success',
83
+ content: '42',
84
+ },
85
+ ]);
86
+ });
87
+
88
+ const toolNode = new ToolNode({
89
+ tools: [createDummyTool('weather'), createDummyTool('stock')],
90
+ eventDrivenMode: true,
91
+ toolCallStepIds: new Map([
92
+ ['call_weather', 'step_weather'],
93
+ ['call_stock', 'step_stock'],
94
+ ]),
95
+ });
96
+
97
+ const result = (await toolNode.invoke({
98
+ messages: [
99
+ createAIMessageWithToolCalls([
100
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
101
+ { id: 'call_stock', name: 'stock', args: { ticker: 'CH' } },
102
+ ]),
103
+ ],
104
+ })) as { messages: ToolMessage[] };
105
+
106
+ // The fast call's completion was emitted before the batch resolved.
107
+ expect(timeline.indexOf('completed:call_weather')).toBeGreaterThan(
108
+ timeline.indexOf('onResult:call_weather')
109
+ );
110
+ expect(timeline.indexOf('completed:call_weather')).toBeLessThan(
111
+ timeline.indexOf('resolving-batch')
112
+ );
113
+
114
+ // Exactly one completion per call: early for weather, batch for stock.
115
+ const byId = completions.map((c) => c.result.tool_call.id);
116
+ expect(byId.filter((id) => id === 'call_weather')).toHaveLength(1);
117
+ expect(byId.filter((id) => id === 'call_stock')).toHaveLength(1);
118
+ expect(
119
+ completions.find((c) => c.result.tool_call.id === 'call_weather')?.result
120
+ .tool_call.output
121
+ ).toBe('sunny');
122
+
123
+ expect(result.messages).toHaveLength(2);
124
+ expect(result.messages.map((m) => m.content)).toEqual(['sunny', '42']);
125
+ });
126
+
127
+ it('ignores duplicate and unknown onResult reports', async () => {
128
+ const completions: CompletionEvent[] = [];
129
+
130
+ jest
131
+ .spyOn(events, 'safeDispatchCustomEvent')
132
+ .mockImplementation(async (event, data): Promise<void> => {
133
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
134
+ completions.push(data as CompletionEvent);
135
+ return;
136
+ }
137
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
138
+ return;
139
+ }
140
+ const batch = data as t.ToolExecuteBatchRequest;
141
+ batch.onResult?.({
142
+ toolCallId: 'call_weather',
143
+ status: 'success',
144
+ content: 'sunny',
145
+ });
146
+ batch.onResult?.({
147
+ toolCallId: 'call_weather',
148
+ status: 'success',
149
+ content: 'sunny again',
150
+ });
151
+ batch.onResult?.({
152
+ toolCallId: 'call_unknown',
153
+ status: 'success',
154
+ content: 'never requested',
155
+ });
156
+ await flushAsync();
157
+ batch.resolve([
158
+ {
159
+ toolCallId: 'call_weather',
160
+ status: 'success',
161
+ content: 'sunny',
162
+ },
163
+ ]);
164
+ });
165
+
166
+ const toolNode = new ToolNode({
167
+ tools: [createDummyTool('weather')],
168
+ eventDrivenMode: true,
169
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
170
+ });
171
+
172
+ await toolNode.invoke({
173
+ messages: [
174
+ createAIMessageWithToolCalls([
175
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
176
+ ]),
177
+ ],
178
+ });
179
+
180
+ const byId = completions.map((c) => c.result.tool_call.id);
181
+ expect(byId.filter((id) => id === 'call_weather')).toHaveLength(1);
182
+ expect(byId.filter((id) => id === 'call_unknown')).toHaveLength(0);
183
+ });
184
+
185
+ it('does not offer onResult when batch-sensitive hooks are configured', async () => {
186
+ let observedBatch: t.ToolExecuteBatchRequest | undefined;
187
+
188
+ jest
189
+ .spyOn(events, 'safeDispatchCustomEvent')
190
+ .mockImplementation(async (event, data): Promise<void> => {
191
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
192
+ return;
193
+ }
194
+ const batch = data as t.ToolExecuteBatchRequest;
195
+ observedBatch = batch;
196
+ batch.resolve([
197
+ {
198
+ toolCallId: 'call_weather',
199
+ status: 'success',
200
+ content: 'sunny',
201
+ },
202
+ ]);
203
+ });
204
+
205
+ const toolNode = new ToolNode({
206
+ tools: [createDummyTool('weather')],
207
+ eventDrivenMode: true,
208
+ hookRegistry: new HookRegistry(),
209
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
210
+ });
211
+
212
+ await toolNode.invoke({
213
+ messages: [
214
+ createAIMessageWithToolCalls([
215
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
216
+ ]),
217
+ ],
218
+ });
219
+
220
+ expect(observedBatch).toBeDefined();
221
+ expect(observedBatch?.onResult).toBeUndefined();
222
+ });
223
+
224
+ it('does not offer onResult when human-in-the-loop is enabled', async () => {
225
+ let observedBatch: t.ToolExecuteBatchRequest | undefined;
226
+
227
+ jest
228
+ .spyOn(events, 'safeDispatchCustomEvent')
229
+ .mockImplementation(async (event, data): Promise<void> => {
230
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
231
+ return;
232
+ }
233
+ const batch = data as t.ToolExecuteBatchRequest;
234
+ observedBatch = batch;
235
+ batch.resolve([
236
+ {
237
+ toolCallId: 'call_weather',
238
+ status: 'success',
239
+ content: 'sunny',
240
+ },
241
+ ]);
242
+ });
243
+
244
+ const toolNode = new ToolNode({
245
+ tools: [createDummyTool('weather')],
246
+ eventDrivenMode: true,
247
+ humanInTheLoop: { enabled: true },
248
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
249
+ });
250
+
251
+ await toolNode.invoke({
252
+ messages: [
253
+ createAIMessageWithToolCalls([
254
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
255
+ ]),
256
+ ],
257
+ });
258
+
259
+ expect(observedBatch).toBeDefined();
260
+ expect(observedBatch?.onResult).toBeUndefined();
261
+ });
262
+
263
+ it('falls back to batch emission when the early dispatch is not delivered', async () => {
264
+ const completionAttempts: CompletionEvent[] = [];
265
+ let failNextCompletion = true;
266
+
267
+ jest
268
+ .spyOn(events, 'safeDispatchCustomEvent')
269
+ .mockImplementation(async (event, data): Promise<boolean | void> => {
270
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
271
+ completionAttempts.push(data as CompletionEvent);
272
+ if (failNextCompletion) {
273
+ failNextCompletion = false;
274
+ return false;
275
+ }
276
+ return;
277
+ }
278
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
279
+ return;
280
+ }
281
+ const batch = data as t.ToolExecuteBatchRequest;
282
+ batch.onResult?.({
283
+ toolCallId: 'call_weather',
284
+ status: 'success',
285
+ content: 'sunny',
286
+ });
287
+ await flushAsync();
288
+ batch.resolve([
289
+ {
290
+ toolCallId: 'call_weather',
291
+ status: 'success',
292
+ content: 'sunny',
293
+ },
294
+ ]);
295
+ });
296
+
297
+ const toolNode = new ToolNode({
298
+ tools: [createDummyTool('weather')],
299
+ eventDrivenMode: true,
300
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
301
+ });
302
+
303
+ await toolNode.invoke({
304
+ messages: [
305
+ createAIMessageWithToolCalls([
306
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
307
+ ]),
308
+ ],
309
+ });
310
+
311
+ // First attempt (early) was rejected by the dispatcher; the batch path
312
+ // re-emitted it.
313
+ expect(completionAttempts).toHaveLength(2);
314
+ expect(completionAttempts[0].result.tool_call.id).toBe('call_weather');
315
+ expect(completionAttempts[1].result.tool_call.id).toBe('call_weather');
316
+ });
317
+
318
+ it('emits error-status results with the standard error formatting', async () => {
319
+ const completions: CompletionEvent[] = [];
320
+
321
+ jest
322
+ .spyOn(events, 'safeDispatchCustomEvent')
323
+ .mockImplementation(async (event, data): Promise<void> => {
324
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
325
+ completions.push(data as CompletionEvent);
326
+ return;
327
+ }
328
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
329
+ return;
330
+ }
331
+ const batch = data as t.ToolExecuteBatchRequest;
332
+ batch.onResult?.({
333
+ toolCallId: 'call_weather',
334
+ status: 'error',
335
+ content: '',
336
+ errorMessage: 'city not found',
337
+ });
338
+ await flushAsync();
339
+ batch.resolve([
340
+ {
341
+ toolCallId: 'call_weather',
342
+ status: 'error',
343
+ content: '',
344
+ errorMessage: 'city not found',
345
+ },
346
+ ]);
347
+ });
348
+
349
+ const toolNode = new ToolNode({
350
+ tools: [createDummyTool('weather')],
351
+ eventDrivenMode: true,
352
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
353
+ });
354
+
355
+ await toolNode.invoke({
356
+ messages: [
357
+ createAIMessageWithToolCalls([
358
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
359
+ ]),
360
+ ],
361
+ });
362
+
363
+ expect(completions).toHaveLength(1);
364
+ expect(completions[0].result.tool_call.output).toBe(
365
+ 'Error: city not found\n Please fix your mistakes.'
366
+ );
367
+ });
368
+ });
@@ -3,9 +3,41 @@ export const STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY =
3
3
  export const STREAMED_TOOL_CALL_SEAL_METADATA_KEY =
4
4
  'lc_streamed_tool_call_seal';
5
5
  export const OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER = 'openai_responses';
6
+ export const BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER = 'bedrock_converse';
7
+ export const GOOGLE_STREAMED_TOOL_CALL_ADAPTER = 'google_genai';
8
+ export const OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER =
9
+ 'openai_chat_sequential';
6
10
 
7
11
  export type StreamedToolCallAdapter =
8
- typeof OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER;
12
+ | typeof OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER
13
+ | typeof BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER
14
+ | typeof GOOGLE_STREAMED_TOOL_CALL_ADAPTER
15
+ | typeof OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER;
16
+
17
+ const STREAMED_TOOL_CALL_ADAPTERS: ReadonlySet<string> = new Set([
18
+ OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
19
+ BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER,
20
+ GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
21
+ OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
22
+ ]);
23
+
24
+ /**
25
+ * Adapters whose wire protocol streams tool calls strictly sequentially by
26
+ * index, so a prior call is sealed the moment a later index begins. Used by
27
+ * the stream handler to extend next-index sealing beyond the provider-keyed
28
+ * Anthropic allowlist.
29
+ */
30
+ const SEQUENTIAL_SEAL_STREAMED_TOOL_CALL_ADAPTERS: ReadonlySet<string> =
31
+ new Set([OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER]);
32
+
33
+ export function streamedToolCallAdapterAllowsSequentialSeal(
34
+ metadata: Record<string, unknown> | undefined
35
+ ): boolean {
36
+ const adapter = getStreamedToolCallAdapter(metadata);
37
+ return (
38
+ adapter != null && SEQUENTIAL_SEAL_STREAMED_TOOL_CALL_ADAPTERS.has(adapter)
39
+ );
40
+ }
9
41
 
10
42
  export type StreamedToolCallSeal =
11
43
  | {
@@ -20,11 +52,9 @@ export type StreamedToolCallSeal =
20
52
  export function getStreamedToolCallAdapter(
21
53
  metadata: Record<string, unknown> | undefined
22
54
  ): StreamedToolCallAdapter | undefined {
23
- if (
24
- metadata?.[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY] ===
25
- OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER
26
- ) {
27
- return OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER;
55
+ const adapter = metadata?.[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY];
56
+ if (typeof adapter === 'string' && STREAMED_TOOL_CALL_ADAPTERS.has(adapter)) {
57
+ return adapter as StreamedToolCallAdapter;
28
58
  }
29
59
  return undefined;
30
60
  }
@@ -47,9 +77,7 @@ export function getStreamedToolCallSeal(
47
77
  }
48
78
  const id = 'id' in seal && typeof seal.id === 'string' ? seal.id : undefined;
49
79
  const index =
50
- 'index' in seal && typeof seal.index === 'number'
51
- ? seal.index
52
- : undefined;
80
+ 'index' in seal && typeof seal.index === 'number' ? seal.index : undefined;
53
81
  if (id == null && index == null) {
54
82
  return undefined;
55
83
  }