@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.
- package/dist/cjs/agents/AgentContext.cjs +47 -10
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +13 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +121 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +21 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +38 -2
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +6 -0
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +49 -8
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +48 -1
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +19 -0
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/content.cjs +12 -14
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +31 -13
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +20 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +12 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +41 -4
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +30 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +138 -2
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +30 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +47 -10
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +13 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +122 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +22 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +38 -3
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +6 -0
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +49 -8
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +48 -1
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +19 -0
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/content.mjs +12 -15
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +31 -13
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +12 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +41 -4
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +25 -2
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +138 -2
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +30 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +7 -3
- package/dist/types/common/enum.d.ts +13 -0
- package/dist/types/graphs/Graph.d.ts +8 -1
- package/dist/types/llm/bedrock/utils/index.d.ts +1 -1
- package/dist/types/llm/bedrock/utils/message_outputs.d.ts +9 -0
- package/dist/types/llm/invoke.d.ts +1 -1
- package/dist/types/llm/vertexai/index.d.ts +10 -0
- package/dist/types/messages/content.d.ts +5 -0
- package/dist/types/messages/prune.d.ts +4 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +8 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +5 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +11 -1
- package/dist/types/types/graph.d.ts +89 -3
- package/dist/types/types/run.d.ts +13 -0
- package/dist/types/types/tools.d.ts +10 -0
- package/dist/types/utils/tokens.d.ts +7 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +703 -0
- package/src/agents/AgentContext.ts +69 -6
- package/src/agents/__tests__/AgentContext.test.ts +6 -2
- package/src/common/enum.ts +13 -0
- package/src/graphs/Graph.ts +196 -0
- package/src/llm/bedrock/index.ts +40 -0
- package/src/llm/bedrock/streamSealDispatch.test.ts +158 -0
- package/src/llm/bedrock/utils/index.ts +1 -0
- package/src/llm/bedrock/utils/message_outputs.test.ts +85 -0
- package/src/llm/bedrock/utils/message_outputs.ts +43 -0
- package/src/llm/google/utils/common.test.ts +64 -0
- package/src/llm/google/utils/common.ts +18 -0
- package/src/llm/invoke.test.ts +79 -1
- package/src/llm/invoke.ts +58 -4
- package/src/llm/openai/index.ts +95 -1
- package/src/llm/openai/sequentialToolCallSeals.test.ts +199 -0
- package/src/llm/vertexai/index.ts +31 -0
- package/src/llm/vertexai/sealStreamedToolCalls.test.ts +88 -0
- package/src/llm/vertexai/streamSealDispatch.test.ts +148 -0
- package/src/messages/content.ts +24 -32
- package/src/messages/prune.ts +39 -2
- package/src/run.ts +5 -0
- package/src/scripts/subagent-usage-sink.ts +176 -0
- package/src/specs/context-accuracy.live.test.ts +409 -0
- package/src/specs/context-usage-event.test.ts +117 -0
- package/src/specs/context-usage.live.test.ts +297 -0
- package/src/specs/prune.test.ts +51 -1
- package/src/specs/subagent.test.ts +124 -1
- package/src/stream.ts +40 -6
- package/src/summarization/__tests__/node.test.ts +60 -1
- package/src/summarization/node.ts +20 -1
- package/src/tools/ToolNode.ts +85 -3
- package/src/tools/__tests__/SubagentExecutor.test.ts +443 -1
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +368 -0
- package/src/tools/streamedToolCallSeals.ts +37 -9
- package/src/tools/subagent/SubagentExecutor.ts +221 -3
- package/src/types/graph.ts +94 -1
- package/src/types/run.ts +13 -0
- package/src/types/tools.ts +10 -0
- package/src/utils/__tests__/apportion.test.ts +32 -0
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|