@librechat/agents 3.2.33 → 3.2.34
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/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/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/stream.cjs +20 -2
- package/dist/cjs/stream.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/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/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/stream.mjs +21 -3
- package/dist/esm/stream.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/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/vertexai/index.d.ts +10 -0
- package/dist/types/tools/ToolNode.d.ts +8 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +5 -1
- package/dist/types/types/tools.d.ts +10 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +703 -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/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/stream.ts +40 -6
- package/src/tools/ToolNode.ts +85 -3
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +368 -0
- package/src/tools/streamedToolCallSeals.ts +37 -9
- package/src/types/tools.ts +10 -0
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
7
7
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
8
8
|
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
9
|
+
BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER,
|
|
10
|
+
GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
|
|
11
|
+
OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
|
|
9
12
|
} from '@/tools/streamedToolCallSeals';
|
|
10
13
|
import {
|
|
11
14
|
Constants,
|
|
@@ -139,6 +142,18 @@ const openAIResponsesToolCallMetadata = {
|
|
|
139
142
|
[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
|
|
140
143
|
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
141
144
|
};
|
|
145
|
+
const bedrockConverseToolCallMetadata = {
|
|
146
|
+
[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
|
|
147
|
+
BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER,
|
|
148
|
+
};
|
|
149
|
+
const googleOnArrivalToolCallMetadata = {
|
|
150
|
+
[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]: GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
|
|
151
|
+
[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'all' },
|
|
152
|
+
};
|
|
153
|
+
const openAIChatSequentialToolCallMetadata = {
|
|
154
|
+
[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
|
|
155
|
+
OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
|
|
156
|
+
};
|
|
142
157
|
|
|
143
158
|
describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
144
159
|
afterEach(() => {
|
|
@@ -4023,4 +4038,692 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
4023
4038
|
});
|
|
4024
4039
|
expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
|
|
4025
4040
|
});
|
|
4041
|
+
|
|
4042
|
+
it('prestarts Bedrock Converse streamed tool calls on content block stop seals', async () => {
|
|
4043
|
+
const graph = createGraph({
|
|
4044
|
+
getAgentContext: jest.fn(
|
|
4045
|
+
(): Partial<AgentContext> => ({
|
|
4046
|
+
provider: Providers.BEDROCK,
|
|
4047
|
+
reasoningKey: 'reasoning_content',
|
|
4048
|
+
toolDefinitions: [{ name: 'weather' }],
|
|
4049
|
+
graphTools: [],
|
|
4050
|
+
agentId: 'agent_1',
|
|
4051
|
+
})
|
|
4052
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4053
|
+
});
|
|
4054
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4055
|
+
jest
|
|
4056
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4057
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4058
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4062
|
+
toolExecuteCalls.push(batch);
|
|
4063
|
+
batch.resolve([
|
|
4064
|
+
{
|
|
4065
|
+
toolCallId: 'call_weather',
|
|
4066
|
+
status: 'success',
|
|
4067
|
+
content: 'sunny',
|
|
4068
|
+
},
|
|
4069
|
+
]);
|
|
4070
|
+
});
|
|
4071
|
+
|
|
4072
|
+
const handler = new ChatModelStreamHandler();
|
|
4073
|
+
const metadata = { langgraph_node: 'agent' };
|
|
4074
|
+
|
|
4075
|
+
await handler.handle(
|
|
4076
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4077
|
+
{
|
|
4078
|
+
chunk: {
|
|
4079
|
+
content: '',
|
|
4080
|
+
tool_call_chunks: [
|
|
4081
|
+
{
|
|
4082
|
+
id: 'call_weather',
|
|
4083
|
+
name: 'weather',
|
|
4084
|
+
index: 1,
|
|
4085
|
+
},
|
|
4086
|
+
],
|
|
4087
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4088
|
+
} as unknown as t.StreamChunk,
|
|
4089
|
+
},
|
|
4090
|
+
metadata,
|
|
4091
|
+
graph
|
|
4092
|
+
);
|
|
4093
|
+
await handler.handle(
|
|
4094
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4095
|
+
{
|
|
4096
|
+
chunk: {
|
|
4097
|
+
content: '',
|
|
4098
|
+
tool_call_chunks: [
|
|
4099
|
+
{
|
|
4100
|
+
args: '{"city":"N',
|
|
4101
|
+
index: 1,
|
|
4102
|
+
},
|
|
4103
|
+
],
|
|
4104
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4105
|
+
} as unknown as t.StreamChunk,
|
|
4106
|
+
},
|
|
4107
|
+
metadata,
|
|
4108
|
+
graph
|
|
4109
|
+
);
|
|
4110
|
+
await handler.handle(
|
|
4111
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4112
|
+
{
|
|
4113
|
+
chunk: {
|
|
4114
|
+
content: '',
|
|
4115
|
+
tool_call_chunks: [
|
|
4116
|
+
{
|
|
4117
|
+
args: 'YC"}',
|
|
4118
|
+
index: 1,
|
|
4119
|
+
},
|
|
4120
|
+
],
|
|
4121
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4122
|
+
} as unknown as t.StreamChunk,
|
|
4123
|
+
},
|
|
4124
|
+
metadata,
|
|
4125
|
+
graph
|
|
4126
|
+
);
|
|
4127
|
+
|
|
4128
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4129
|
+
|
|
4130
|
+
await handler.handle(
|
|
4131
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4132
|
+
{
|
|
4133
|
+
chunk: {
|
|
4134
|
+
content: '',
|
|
4135
|
+
tool_call_chunks: [
|
|
4136
|
+
{
|
|
4137
|
+
args: '',
|
|
4138
|
+
index: 1,
|
|
4139
|
+
},
|
|
4140
|
+
],
|
|
4141
|
+
response_metadata: {
|
|
4142
|
+
...bedrockConverseToolCallMetadata,
|
|
4143
|
+
[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: {
|
|
4144
|
+
kind: 'single',
|
|
4145
|
+
index: 1,
|
|
4146
|
+
},
|
|
4147
|
+
},
|
|
4148
|
+
} as unknown as t.StreamChunk,
|
|
4149
|
+
},
|
|
4150
|
+
metadata,
|
|
4151
|
+
graph
|
|
4152
|
+
);
|
|
4153
|
+
|
|
4154
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4155
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
4156
|
+
id: 'call_weather',
|
|
4157
|
+
name: 'weather',
|
|
4158
|
+
args: { city: 'NYC' },
|
|
4159
|
+
stepId: expect.stringMatching(/^step_/),
|
|
4160
|
+
turn: 0,
|
|
4161
|
+
});
|
|
4162
|
+
expect(
|
|
4163
|
+
graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 1))
|
|
4164
|
+
).toBe(false);
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
it('prestarts each Bedrock tool call at its own stop seal in multi-call turns', async () => {
|
|
4168
|
+
const graph = createGraph({
|
|
4169
|
+
getAgentContext: jest.fn(
|
|
4170
|
+
(): Partial<AgentContext> => ({
|
|
4171
|
+
provider: Providers.BEDROCK,
|
|
4172
|
+
reasoningKey: 'reasoning_content',
|
|
4173
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
4174
|
+
graphTools: [],
|
|
4175
|
+
agentId: 'agent_1',
|
|
4176
|
+
})
|
|
4177
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4178
|
+
});
|
|
4179
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4180
|
+
jest
|
|
4181
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4182
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4183
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4187
|
+
toolExecuteCalls.push(batch);
|
|
4188
|
+
batch.resolve(
|
|
4189
|
+
batch.toolCalls.map((call) => ({
|
|
4190
|
+
toolCallId: call.id,
|
|
4191
|
+
status: 'success',
|
|
4192
|
+
content: `ok ${call.name}`,
|
|
4193
|
+
}))
|
|
4194
|
+
);
|
|
4195
|
+
});
|
|
4196
|
+
|
|
4197
|
+
const handler = new ChatModelStreamHandler();
|
|
4198
|
+
const metadata = { langgraph_node: 'agent' };
|
|
4199
|
+
const sendChunk = async (chunk: Record<string, unknown>): Promise<void> =>
|
|
4200
|
+
handler.handle(
|
|
4201
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4202
|
+
{ chunk: chunk as unknown as t.StreamChunk },
|
|
4203
|
+
metadata,
|
|
4204
|
+
graph
|
|
4205
|
+
);
|
|
4206
|
+
|
|
4207
|
+
await sendChunk({
|
|
4208
|
+
content: '',
|
|
4209
|
+
tool_call_chunks: [{ id: 'call_weather', name: 'weather', index: 1 }],
|
|
4210
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4211
|
+
});
|
|
4212
|
+
await sendChunk({
|
|
4213
|
+
content: '',
|
|
4214
|
+
tool_call_chunks: [{ args: '{"city":"NYC"}', index: 1 }],
|
|
4215
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4216
|
+
});
|
|
4217
|
+
await sendChunk({
|
|
4218
|
+
content: '',
|
|
4219
|
+
tool_call_chunks: [{ args: '', index: 1 }],
|
|
4220
|
+
response_metadata: {
|
|
4221
|
+
...bedrockConverseToolCallMetadata,
|
|
4222
|
+
[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'single', index: 1 },
|
|
4223
|
+
},
|
|
4224
|
+
});
|
|
4225
|
+
|
|
4226
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4227
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
4228
|
+
id: 'call_weather',
|
|
4229
|
+
args: { city: 'NYC' },
|
|
4230
|
+
});
|
|
4231
|
+
|
|
4232
|
+
await sendChunk({
|
|
4233
|
+
content: '',
|
|
4234
|
+
tool_call_chunks: [{ id: 'call_stock', name: 'stock', index: 2 }],
|
|
4235
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4236
|
+
});
|
|
4237
|
+
await sendChunk({
|
|
4238
|
+
content: '',
|
|
4239
|
+
tool_call_chunks: [{ args: '{"ticker":"CH"}', index: 2 }],
|
|
4240
|
+
response_metadata: bedrockConverseToolCallMetadata,
|
|
4241
|
+
});
|
|
4242
|
+
|
|
4243
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4244
|
+
|
|
4245
|
+
await sendChunk({
|
|
4246
|
+
content: '',
|
|
4247
|
+
tool_call_chunks: [{ args: '', index: 2 }],
|
|
4248
|
+
response_metadata: {
|
|
4249
|
+
...bedrockConverseToolCallMetadata,
|
|
4250
|
+
[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'single', index: 2 },
|
|
4251
|
+
},
|
|
4252
|
+
});
|
|
4253
|
+
|
|
4254
|
+
expect(toolExecuteCalls).toHaveLength(2);
|
|
4255
|
+
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
4256
|
+
id: 'call_stock',
|
|
4257
|
+
args: { ticker: 'CH' },
|
|
4258
|
+
turn: 0,
|
|
4259
|
+
});
|
|
4260
|
+
});
|
|
4261
|
+
|
|
4262
|
+
it('prestarts Google tool calls sealed on arrival without a final tool-call signal', async () => {
|
|
4263
|
+
const graph = createGraph({
|
|
4264
|
+
getAgentContext: jest.fn(
|
|
4265
|
+
(): Partial<AgentContext> => ({
|
|
4266
|
+
provider: Providers.GOOGLE,
|
|
4267
|
+
reasoningKey: 'reasoning',
|
|
4268
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
4269
|
+
graphTools: [],
|
|
4270
|
+
agentId: 'agent_1',
|
|
4271
|
+
})
|
|
4272
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4273
|
+
});
|
|
4274
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4275
|
+
jest
|
|
4276
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4277
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4278
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4282
|
+
toolExecuteCalls.push(batch);
|
|
4283
|
+
batch.resolve(
|
|
4284
|
+
batch.toolCalls.map((call) => ({
|
|
4285
|
+
toolCallId: call.id,
|
|
4286
|
+
status: 'success',
|
|
4287
|
+
content: `ok ${call.name}`,
|
|
4288
|
+
}))
|
|
4289
|
+
);
|
|
4290
|
+
});
|
|
4291
|
+
|
|
4292
|
+
const handler = new ChatModelStreamHandler();
|
|
4293
|
+
const metadata = { langgraph_node: 'agent' };
|
|
4294
|
+
|
|
4295
|
+
await handler.handle(
|
|
4296
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4297
|
+
{
|
|
4298
|
+
chunk: {
|
|
4299
|
+
content: '',
|
|
4300
|
+
tool_calls: [
|
|
4301
|
+
{
|
|
4302
|
+
id: 'call_weather',
|
|
4303
|
+
name: 'weather',
|
|
4304
|
+
args: { city: 'NYC' },
|
|
4305
|
+
},
|
|
4306
|
+
],
|
|
4307
|
+
tool_call_chunks: [
|
|
4308
|
+
{
|
|
4309
|
+
id: 'call_weather',
|
|
4310
|
+
name: 'weather',
|
|
4311
|
+
args: '{"city":"NYC"}',
|
|
4312
|
+
},
|
|
4313
|
+
],
|
|
4314
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4315
|
+
} as unknown as t.StreamChunk,
|
|
4316
|
+
},
|
|
4317
|
+
metadata,
|
|
4318
|
+
graph
|
|
4319
|
+
);
|
|
4320
|
+
|
|
4321
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4322
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
4323
|
+
id: 'call_weather',
|
|
4324
|
+
name: 'weather',
|
|
4325
|
+
args: { city: 'NYC' },
|
|
4326
|
+
stepId: expect.stringMatching(/^step_/),
|
|
4327
|
+
turn: 0,
|
|
4328
|
+
});
|
|
4329
|
+
|
|
4330
|
+
// A later chunk with another sealed call prestarts independently.
|
|
4331
|
+
await handler.handle(
|
|
4332
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4333
|
+
{
|
|
4334
|
+
chunk: {
|
|
4335
|
+
content: '',
|
|
4336
|
+
tool_calls: [
|
|
4337
|
+
{
|
|
4338
|
+
id: 'call_stock',
|
|
4339
|
+
name: 'stock',
|
|
4340
|
+
args: { ticker: 'CH' },
|
|
4341
|
+
},
|
|
4342
|
+
],
|
|
4343
|
+
tool_call_chunks: [
|
|
4344
|
+
{
|
|
4345
|
+
id: 'call_stock',
|
|
4346
|
+
name: 'stock',
|
|
4347
|
+
args: '{"ticker":"CH"}',
|
|
4348
|
+
},
|
|
4349
|
+
],
|
|
4350
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4351
|
+
} as unknown as t.StreamChunk,
|
|
4352
|
+
},
|
|
4353
|
+
metadata,
|
|
4354
|
+
graph
|
|
4355
|
+
);
|
|
4356
|
+
|
|
4357
|
+
expect(toolExecuteCalls).toHaveLength(2);
|
|
4358
|
+
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
4359
|
+
id: 'call_stock',
|
|
4360
|
+
name: 'stock',
|
|
4361
|
+
args: { ticker: 'CH' },
|
|
4362
|
+
turn: 0,
|
|
4363
|
+
});
|
|
4364
|
+
expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
|
|
4365
|
+
expect(graph.eagerEventToolExecutions.has('call_stock')).toBe(true);
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4368
|
+
it('keeps Google tool calls lazy without an on-arrival seal', async () => {
|
|
4369
|
+
const graph = createGraph({
|
|
4370
|
+
getAgentContext: jest.fn(
|
|
4371
|
+
(): Partial<AgentContext> => ({
|
|
4372
|
+
provider: Providers.GOOGLE,
|
|
4373
|
+
reasoningKey: 'reasoning',
|
|
4374
|
+
toolDefinitions: [{ name: 'weather' }],
|
|
4375
|
+
graphTools: [],
|
|
4376
|
+
agentId: 'agent_1',
|
|
4377
|
+
})
|
|
4378
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4379
|
+
});
|
|
4380
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4381
|
+
jest
|
|
4382
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4383
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4384
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
|
|
4388
|
+
});
|
|
4389
|
+
|
|
4390
|
+
await new ChatModelStreamHandler().handle(
|
|
4391
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4392
|
+
{
|
|
4393
|
+
chunk: {
|
|
4394
|
+
content: '',
|
|
4395
|
+
tool_calls: [
|
|
4396
|
+
{
|
|
4397
|
+
id: 'call_weather',
|
|
4398
|
+
name: 'weather',
|
|
4399
|
+
args: { city: 'NYC' },
|
|
4400
|
+
},
|
|
4401
|
+
],
|
|
4402
|
+
tool_call_chunks: [
|
|
4403
|
+
{
|
|
4404
|
+
id: 'call_weather',
|
|
4405
|
+
name: 'weather',
|
|
4406
|
+
args: '{"city":"NYC"}',
|
|
4407
|
+
},
|
|
4408
|
+
],
|
|
4409
|
+
} as unknown as t.StreamChunk,
|
|
4410
|
+
},
|
|
4411
|
+
{ langgraph_node: 'agent' },
|
|
4412
|
+
graph
|
|
4413
|
+
);
|
|
4414
|
+
|
|
4415
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4416
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4417
|
+
});
|
|
4418
|
+
|
|
4419
|
+
it('does not prestart on-arrival sealed calls when batch-sensitive hooks are configured', async () => {
|
|
4420
|
+
const graph = createGraph({
|
|
4421
|
+
hookRegistry: {} as StandardGraph['hookRegistry'],
|
|
4422
|
+
});
|
|
4423
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
4424
|
+
|
|
4425
|
+
await new ChatModelStreamHandler().handle(
|
|
4426
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4427
|
+
{
|
|
4428
|
+
chunk: {
|
|
4429
|
+
content: '',
|
|
4430
|
+
tool_calls: [
|
|
4431
|
+
{
|
|
4432
|
+
id: 'call_weather',
|
|
4433
|
+
name: 'weather',
|
|
4434
|
+
args: { city: 'NYC' },
|
|
4435
|
+
},
|
|
4436
|
+
],
|
|
4437
|
+
tool_call_chunks: [
|
|
4438
|
+
{
|
|
4439
|
+
id: 'call_weather',
|
|
4440
|
+
name: 'weather',
|
|
4441
|
+
args: '{"city":"NYC"}',
|
|
4442
|
+
},
|
|
4443
|
+
],
|
|
4444
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4445
|
+
} as unknown as t.StreamChunk,
|
|
4446
|
+
},
|
|
4447
|
+
{ langgraph_node: 'agent' },
|
|
4448
|
+
graph
|
|
4449
|
+
);
|
|
4450
|
+
|
|
4451
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
4452
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
4453
|
+
expect.anything(),
|
|
4454
|
+
expect.anything()
|
|
4455
|
+
);
|
|
4456
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4457
|
+
});
|
|
4458
|
+
|
|
4459
|
+
it('does not prestart on-arrival sealed calls for programmatic tool calling batches', async () => {
|
|
4460
|
+
const graph = createGraph();
|
|
4461
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
4462
|
+
|
|
4463
|
+
await new ChatModelStreamHandler().handle(
|
|
4464
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4465
|
+
{
|
|
4466
|
+
chunk: {
|
|
4467
|
+
content: '',
|
|
4468
|
+
tool_calls: [
|
|
4469
|
+
{
|
|
4470
|
+
id: 'call_weather',
|
|
4471
|
+
name: 'weather',
|
|
4472
|
+
args: { city: 'NYC' },
|
|
4473
|
+
},
|
|
4474
|
+
],
|
|
4475
|
+
tool_call_chunks: [
|
|
4476
|
+
{
|
|
4477
|
+
id: 'call_weather',
|
|
4478
|
+
name: 'weather',
|
|
4479
|
+
args: '{"city":"NYC"}',
|
|
4480
|
+
},
|
|
4481
|
+
],
|
|
4482
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4483
|
+
} as unknown as t.StreamChunk,
|
|
4484
|
+
},
|
|
4485
|
+
{
|
|
4486
|
+
langgraph_node: 'agent',
|
|
4487
|
+
[Constants.PROGRAMMATIC_TOOL_CALLING]: true,
|
|
4488
|
+
},
|
|
4489
|
+
graph
|
|
4490
|
+
);
|
|
4491
|
+
|
|
4492
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
4493
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
4494
|
+
expect.anything(),
|
|
4495
|
+
expect.anything()
|
|
4496
|
+
);
|
|
4497
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4498
|
+
});
|
|
4499
|
+
|
|
4500
|
+
it('does not prestart on-arrival sealed calls when eager execution is disabled', async () => {
|
|
4501
|
+
const graph = createGraph({
|
|
4502
|
+
eagerEventToolExecution: { enabled: false },
|
|
4503
|
+
} as Partial<StandardGraph>);
|
|
4504
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
4505
|
+
|
|
4506
|
+
await new ChatModelStreamHandler().handle(
|
|
4507
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4508
|
+
{
|
|
4509
|
+
chunk: {
|
|
4510
|
+
content: '',
|
|
4511
|
+
tool_calls: [
|
|
4512
|
+
{
|
|
4513
|
+
id: 'call_weather',
|
|
4514
|
+
name: 'weather',
|
|
4515
|
+
args: { city: 'NYC' },
|
|
4516
|
+
},
|
|
4517
|
+
],
|
|
4518
|
+
tool_call_chunks: [
|
|
4519
|
+
{
|
|
4520
|
+
id: 'call_weather',
|
|
4521
|
+
name: 'weather',
|
|
4522
|
+
args: '{"city":"NYC"}',
|
|
4523
|
+
},
|
|
4524
|
+
],
|
|
4525
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4526
|
+
} as unknown as t.StreamChunk,
|
|
4527
|
+
},
|
|
4528
|
+
{ langgraph_node: 'agent' },
|
|
4529
|
+
graph
|
|
4530
|
+
);
|
|
4531
|
+
|
|
4532
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
4533
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
4534
|
+
expect.anything(),
|
|
4535
|
+
expect.anything()
|
|
4536
|
+
);
|
|
4537
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4538
|
+
});
|
|
4539
|
+
|
|
4540
|
+
it('prestarts official OpenAI sequential streamed tool calls when a later index begins', async () => {
|
|
4541
|
+
const graph = createGraph({
|
|
4542
|
+
getAgentContext: jest.fn(
|
|
4543
|
+
(): Partial<AgentContext> => ({
|
|
4544
|
+
provider: Providers.OPENAI,
|
|
4545
|
+
reasoningKey: 'reasoning_content',
|
|
4546
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
4547
|
+
graphTools: [],
|
|
4548
|
+
agentId: 'agent_1',
|
|
4549
|
+
})
|
|
4550
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4551
|
+
});
|
|
4552
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4553
|
+
jest
|
|
4554
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4555
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4556
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4557
|
+
return;
|
|
4558
|
+
}
|
|
4559
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4560
|
+
toolExecuteCalls.push(batch);
|
|
4561
|
+
batch.resolve(
|
|
4562
|
+
batch.toolCalls.map((call) => ({
|
|
4563
|
+
toolCallId: call.id,
|
|
4564
|
+
status: 'success',
|
|
4565
|
+
content: `ok ${call.name}`,
|
|
4566
|
+
}))
|
|
4567
|
+
);
|
|
4568
|
+
});
|
|
4569
|
+
|
|
4570
|
+
const handler = new ChatModelStreamHandler();
|
|
4571
|
+
const metadata = { langgraph_node: 'agent' };
|
|
4572
|
+
|
|
4573
|
+
await handler.handle(
|
|
4574
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4575
|
+
{
|
|
4576
|
+
chunk: {
|
|
4577
|
+
content: '',
|
|
4578
|
+
tool_call_chunks: [
|
|
4579
|
+
{
|
|
4580
|
+
id: 'call_weather',
|
|
4581
|
+
name: 'weather',
|
|
4582
|
+
args: '{"city":"NYC"}',
|
|
4583
|
+
index: 0,
|
|
4584
|
+
},
|
|
4585
|
+
],
|
|
4586
|
+
response_metadata: openAIChatSequentialToolCallMetadata,
|
|
4587
|
+
} as unknown as t.StreamChunk,
|
|
4588
|
+
},
|
|
4589
|
+
metadata,
|
|
4590
|
+
graph
|
|
4591
|
+
);
|
|
4592
|
+
|
|
4593
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4594
|
+
|
|
4595
|
+
await handler.handle(
|
|
4596
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4597
|
+
{
|
|
4598
|
+
chunk: {
|
|
4599
|
+
content: '',
|
|
4600
|
+
tool_call_chunks: [
|
|
4601
|
+
{
|
|
4602
|
+
id: 'call_stock',
|
|
4603
|
+
name: 'stock',
|
|
4604
|
+
index: 1,
|
|
4605
|
+
},
|
|
4606
|
+
],
|
|
4607
|
+
response_metadata: openAIChatSequentialToolCallMetadata,
|
|
4608
|
+
} as unknown as t.StreamChunk,
|
|
4609
|
+
},
|
|
4610
|
+
metadata,
|
|
4611
|
+
graph
|
|
4612
|
+
);
|
|
4613
|
+
|
|
4614
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4615
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
4616
|
+
id: 'call_weather',
|
|
4617
|
+
name: 'weather',
|
|
4618
|
+
args: { city: 'NYC' },
|
|
4619
|
+
stepId: expect.stringMatching(/^step_/),
|
|
4620
|
+
turn: 0,
|
|
4621
|
+
});
|
|
4622
|
+
|
|
4623
|
+
await handler.handle(
|
|
4624
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4625
|
+
{
|
|
4626
|
+
chunk: {
|
|
4627
|
+
content: '',
|
|
4628
|
+
tool_call_chunks: [
|
|
4629
|
+
{
|
|
4630
|
+
args: '{"ticker":"CH"}',
|
|
4631
|
+
index: 1,
|
|
4632
|
+
},
|
|
4633
|
+
],
|
|
4634
|
+
response_metadata: openAIChatSequentialToolCallMetadata,
|
|
4635
|
+
} as unknown as t.StreamChunk,
|
|
4636
|
+
},
|
|
4637
|
+
metadata,
|
|
4638
|
+
graph
|
|
4639
|
+
);
|
|
4640
|
+
|
|
4641
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
4642
|
+
|
|
4643
|
+
// The final cumulative tool_calls chunk dispatches only the unstarted call.
|
|
4644
|
+
await handler.handle(
|
|
4645
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4646
|
+
{
|
|
4647
|
+
chunk: {
|
|
4648
|
+
content: '',
|
|
4649
|
+
tool_calls: [
|
|
4650
|
+
{
|
|
4651
|
+
id: 'call_weather',
|
|
4652
|
+
name: 'weather',
|
|
4653
|
+
args: { city: 'NYC' },
|
|
4654
|
+
},
|
|
4655
|
+
{
|
|
4656
|
+
id: 'call_stock',
|
|
4657
|
+
name: 'stock',
|
|
4658
|
+
args: { ticker: 'CH' },
|
|
4659
|
+
},
|
|
4660
|
+
],
|
|
4661
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
4662
|
+
} as unknown as t.StreamChunk,
|
|
4663
|
+
},
|
|
4664
|
+
metadata,
|
|
4665
|
+
graph
|
|
4666
|
+
);
|
|
4667
|
+
|
|
4668
|
+
expect(toolExecuteCalls).toHaveLength(2);
|
|
4669
|
+
expect(toolExecuteCalls[1].toolCalls).toHaveLength(1);
|
|
4670
|
+
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
4671
|
+
id: 'call_stock',
|
|
4672
|
+
name: 'stock',
|
|
4673
|
+
args: { ticker: 'CH' },
|
|
4674
|
+
turn: 0,
|
|
4675
|
+
});
|
|
4676
|
+
});
|
|
4677
|
+
|
|
4678
|
+
it('does not prestart on-arrival sealed calls when direct tools may stream', async () => {
|
|
4679
|
+
const graph = createGraph({
|
|
4680
|
+
getAgentContext: jest.fn(
|
|
4681
|
+
(): Partial<AgentContext> => ({
|
|
4682
|
+
provider: Providers.GOOGLE,
|
|
4683
|
+
reasoningKey: 'reasoning',
|
|
4684
|
+
toolDefinitions: [{ name: 'weather' }],
|
|
4685
|
+
graphTools: [{ name: 'lookup' }],
|
|
4686
|
+
agentId: 'agent_1',
|
|
4687
|
+
})
|
|
4688
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
4689
|
+
});
|
|
4690
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4691
|
+
jest
|
|
4692
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4693
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4694
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
|
|
4698
|
+
});
|
|
4699
|
+
|
|
4700
|
+
await new ChatModelStreamHandler().handle(
|
|
4701
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4702
|
+
{
|
|
4703
|
+
chunk: {
|
|
4704
|
+
content: '',
|
|
4705
|
+
tool_calls: [
|
|
4706
|
+
{
|
|
4707
|
+
id: 'call_weather',
|
|
4708
|
+
name: 'weather',
|
|
4709
|
+
args: { city: 'NYC' },
|
|
4710
|
+
},
|
|
4711
|
+
],
|
|
4712
|
+
tool_call_chunks: [
|
|
4713
|
+
{
|
|
4714
|
+
id: 'call_weather',
|
|
4715
|
+
name: 'weather',
|
|
4716
|
+
args: '{"city":"NYC"}',
|
|
4717
|
+
},
|
|
4718
|
+
],
|
|
4719
|
+
response_metadata: googleOnArrivalToolCallMetadata,
|
|
4720
|
+
} as unknown as t.StreamChunk,
|
|
4721
|
+
},
|
|
4722
|
+
{ langgraph_node: 'agent' },
|
|
4723
|
+
graph
|
|
4724
|
+
);
|
|
4725
|
+
|
|
4726
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4727
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4728
|
+
});
|
|
4026
4729
|
});
|