@librechat/agents 3.1.87 → 3.1.89
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/graphs/Graph.cjs +18 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/stream.cjs +120 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +24 -7
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +18 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/stream.mjs +120 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +24 -7
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +8 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/types/tools.d.ts +9 -0
- package/dist/types/utils/events.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +1307 -342
- package/src/graphs/Graph.ts +20 -5
- package/src/specs/subagent.test.ts +87 -1
- package/src/stream.ts +168 -16
- package/src/tools/ToolNode.ts +134 -111
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
- package/src/types/tools.ts +9 -0
- package/src/utils/events.ts +4 -2
|
@@ -117,6 +117,276 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
117
117
|
expect(result.messages[0].content).toBe('eager result');
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it('uses a prestarted event result when final args are canonically equivalent', async () => {
|
|
121
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
122
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
123
|
+
const request: t.ToolCallRequest = {
|
|
124
|
+
id: 'call_weather',
|
|
125
|
+
name: 'weather',
|
|
126
|
+
args: { city: 'NYC', units: 'metric' },
|
|
127
|
+
stepId: 'step_weather',
|
|
128
|
+
turn: 0,
|
|
129
|
+
};
|
|
130
|
+
eagerExecutions.set('call_weather', {
|
|
131
|
+
toolCallId: 'call_weather',
|
|
132
|
+
toolName: 'weather',
|
|
133
|
+
args: { city: 'NYC', units: 'metric' },
|
|
134
|
+
request,
|
|
135
|
+
promise: Promise.resolve({
|
|
136
|
+
results: [
|
|
137
|
+
{
|
|
138
|
+
toolCallId: 'call_weather',
|
|
139
|
+
status: 'success',
|
|
140
|
+
content: 'eager result',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const toolNode = new ToolNode({
|
|
147
|
+
tools: [createDummyTool('weather')],
|
|
148
|
+
eventDrivenMode: true,
|
|
149
|
+
eagerEventToolExecution: { enabled: true },
|
|
150
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
151
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = (await toolNode.invoke({
|
|
155
|
+
messages: [
|
|
156
|
+
createAIMessage('call_weather', 'weather', {
|
|
157
|
+
units: 'metric',
|
|
158
|
+
city: 'NYC',
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
})) as { messages: ToolMessage[] };
|
|
162
|
+
|
|
163
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
164
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
165
|
+
expect(result.messages).toHaveLength(1);
|
|
166
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('uses a prestarted event result when only final planning turn differs', async () => {
|
|
170
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
171
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
172
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
173
|
+
const request: t.ToolCallRequest = {
|
|
174
|
+
id: 'call_weather',
|
|
175
|
+
name: 'weather',
|
|
176
|
+
args: { city: 'NYC' },
|
|
177
|
+
stepId: 'step_weather',
|
|
178
|
+
turn: 7,
|
|
179
|
+
};
|
|
180
|
+
eagerExecutions.set('call_weather', {
|
|
181
|
+
toolCallId: 'call_weather',
|
|
182
|
+
toolName: 'weather',
|
|
183
|
+
args: { city: 'NYC' },
|
|
184
|
+
request,
|
|
185
|
+
promise: Promise.resolve({
|
|
186
|
+
results: [
|
|
187
|
+
{
|
|
188
|
+
toolCallId: 'call_weather',
|
|
189
|
+
status: 'success',
|
|
190
|
+
content: 'eager result',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const toolNode = new ToolNode({
|
|
197
|
+
tools: [createDummyTool('weather')],
|
|
198
|
+
eventDrivenMode: true,
|
|
199
|
+
eagerEventToolExecution: { enabled: true },
|
|
200
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
201
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
202
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = (await toolNode.invoke({
|
|
206
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
207
|
+
})) as { messages: ToolMessage[] };
|
|
208
|
+
|
|
209
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
210
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
211
|
+
expect(eagerUsageCount.get('weather')).toBe(1);
|
|
212
|
+
expect(result.messages).toHaveLength(1);
|
|
213
|
+
expect(result.messages[0].status).toBe('success');
|
|
214
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('redispatches completion when a prestarted event result used a stale turn', async () => {
|
|
218
|
+
const stepCompletions: Array<{
|
|
219
|
+
result?: { index?: number; tool_call?: { output?: string } };
|
|
220
|
+
}> = [];
|
|
221
|
+
jest
|
|
222
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
223
|
+
.mockImplementation(async (event, data): Promise<boolean | void> => {
|
|
224
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
225
|
+
stepCompletions.push(data as (typeof stepCompletions)[number]);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (event === GraphEvents.ON_TOOL_EXECUTE) {
|
|
229
|
+
throw new Error('tool should not redispatch');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
234
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
235
|
+
const request: t.ToolCallRequest = {
|
|
236
|
+
id: 'call_weather',
|
|
237
|
+
name: 'weather',
|
|
238
|
+
args: { city: 'NYC' },
|
|
239
|
+
stepId: 'step_weather',
|
|
240
|
+
turn: 7,
|
|
241
|
+
};
|
|
242
|
+
eagerExecutions.set('call_weather', {
|
|
243
|
+
toolCallId: 'call_weather',
|
|
244
|
+
toolName: 'weather',
|
|
245
|
+
args: { city: 'NYC' },
|
|
246
|
+
request,
|
|
247
|
+
completionDispatched: true,
|
|
248
|
+
promise: Promise.resolve({
|
|
249
|
+
results: [
|
|
250
|
+
{
|
|
251
|
+
toolCallId: 'call_weather',
|
|
252
|
+
status: 'success',
|
|
253
|
+
content: 'eager result',
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const toolNode = new ToolNode({
|
|
260
|
+
tools: [createDummyTool('weather')],
|
|
261
|
+
eventDrivenMode: true,
|
|
262
|
+
eagerEventToolExecution: { enabled: true },
|
|
263
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
264
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
265
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = (await toolNode.invoke({
|
|
269
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
270
|
+
})) as { messages: ToolMessage[] };
|
|
271
|
+
|
|
272
|
+
expect(stepCompletions).toHaveLength(1);
|
|
273
|
+
expect(stepCompletions[0].result?.index).toBe(0);
|
|
274
|
+
expect(stepCompletions[0].result?.tool_call?.output).toBe('eager result');
|
|
275
|
+
expect(result.messages).toHaveLength(1);
|
|
276
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('uses a matching prestarted event result when output references are enabled', async () => {
|
|
280
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
281
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
282
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
283
|
+
const request: t.ToolCallRequest = {
|
|
284
|
+
id: 'call_weather',
|
|
285
|
+
name: 'weather',
|
|
286
|
+
args: { city: 'NYC' },
|
|
287
|
+
stepId: 'step_weather',
|
|
288
|
+
turn: 0,
|
|
289
|
+
};
|
|
290
|
+
eagerExecutions.set('call_weather', {
|
|
291
|
+
toolCallId: 'call_weather',
|
|
292
|
+
toolName: 'weather',
|
|
293
|
+
args: { city: 'NYC' },
|
|
294
|
+
request,
|
|
295
|
+
promise: Promise.resolve({
|
|
296
|
+
results: [
|
|
297
|
+
{
|
|
298
|
+
toolCallId: 'call_weather',
|
|
299
|
+
status: 'success',
|
|
300
|
+
content: 'eager result',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const toolNode = new ToolNode({
|
|
307
|
+
tools: [createDummyTool('weather')],
|
|
308
|
+
eventDrivenMode: true,
|
|
309
|
+
eagerEventToolExecution: { enabled: true },
|
|
310
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
311
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
312
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
313
|
+
toolOutputReferences: { enabled: true },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const result = (await toolNode.invoke(
|
|
317
|
+
{
|
|
318
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
319
|
+
},
|
|
320
|
+
{ configurable: { run_id: 'run_1' } }
|
|
321
|
+
)) as { messages: ToolMessage[] };
|
|
322
|
+
|
|
323
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
324
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
325
|
+
expect(result.messages).toHaveLength(1);
|
|
326
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
327
|
+
expect(result.messages[0].additional_kwargs._refKey).toBe('tool0turn0');
|
|
328
|
+
expect(result.messages[0].additional_kwargs._refScope).toBe('run_1');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('does not redispatch completion when eager path already emitted it', async () => {
|
|
332
|
+
const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
|
|
333
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
334
|
+
jest
|
|
335
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
336
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
337
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
338
|
+
completedEvents.push(data as { result: t.ToolEndEvent });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
348
|
+
const request: t.ToolCallRequest = {
|
|
349
|
+
id: 'call_weather',
|
|
350
|
+
name: 'weather',
|
|
351
|
+
args: { city: 'NYC' },
|
|
352
|
+
stepId: 'step_weather',
|
|
353
|
+
turn: 0,
|
|
354
|
+
};
|
|
355
|
+
eagerExecutions.set('call_weather', {
|
|
356
|
+
toolCallId: 'call_weather',
|
|
357
|
+
toolName: 'weather',
|
|
358
|
+
args: { city: 'NYC' },
|
|
359
|
+
request,
|
|
360
|
+
completionDispatched: true,
|
|
361
|
+
promise: Promise.resolve({
|
|
362
|
+
results: [
|
|
363
|
+
{
|
|
364
|
+
toolCallId: 'call_weather',
|
|
365
|
+
status: 'success',
|
|
366
|
+
content: 'eager result',
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const toolNode = new ToolNode({
|
|
373
|
+
tools: [createDummyTool('weather')],
|
|
374
|
+
eventDrivenMode: true,
|
|
375
|
+
eagerEventToolExecution: { enabled: true },
|
|
376
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
377
|
+
eagerEventToolUsageCount: new Map([['weather', 1]]),
|
|
378
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result = (await toolNode.invoke({
|
|
382
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
383
|
+
})) as { messages: ToolMessage[] };
|
|
384
|
+
|
|
385
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
386
|
+
expect(completedEvents).toHaveLength(0);
|
|
387
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
388
|
+
});
|
|
389
|
+
|
|
120
390
|
it('fails closed without redispatching when final args differ from the prestarted call', async () => {
|
|
121
391
|
const { toolExecuteCalls } = installToolExecuteResponder('fresh result');
|
|
122
392
|
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
@@ -196,7 +466,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
196
466
|
const { toolExecuteCalls } = installToolExecuteResponder('normal result');
|
|
197
467
|
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
198
468
|
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
199
|
-
const
|
|
469
|
+
const prestartedRequest: t.ToolCallRequest = {
|
|
200
470
|
id: 'call_weather_2',
|
|
201
471
|
name: 'weather',
|
|
202
472
|
args: { city: 'Boston' },
|
|
@@ -207,13 +477,13 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
207
477
|
toolCallId: 'call_weather_2',
|
|
208
478
|
toolName: 'weather',
|
|
209
479
|
args: { city: 'Boston' },
|
|
210
|
-
request:
|
|
480
|
+
request: prestartedRequest,
|
|
211
481
|
promise: Promise.resolve({
|
|
212
482
|
results: [
|
|
213
483
|
{
|
|
214
484
|
toolCallId: 'call_weather_2',
|
|
215
485
|
status: 'success',
|
|
216
|
-
content: '
|
|
486
|
+
content: 'prestarted eager result',
|
|
217
487
|
},
|
|
218
488
|
],
|
|
219
489
|
}),
|
|
@@ -263,8 +533,8 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
263
533
|
'call_weather_2',
|
|
264
534
|
]);
|
|
265
535
|
expect(result.messages[0].status).toBe('success');
|
|
266
|
-
expect(result.messages[1].status).toBe('
|
|
267
|
-
expect(result.messages[1].content).
|
|
536
|
+
expect(result.messages[1].status).toBe('success');
|
|
537
|
+
expect(result.messages[1].content).toBe('prestarted eager result');
|
|
268
538
|
});
|
|
269
539
|
|
|
270
540
|
it('returns a per-call error for malformed event tool args without aborting the batch', async () => {
|
|
@@ -314,9 +584,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
314
584
|
'call_weather_good',
|
|
315
585
|
]);
|
|
316
586
|
expect(result.messages[0].status).toBe('error');
|
|
317
|
-
expect(result.messages[0].content).toContain(
|
|
318
|
-
'Invalid tool call arguments'
|
|
319
|
-
);
|
|
587
|
+
expect(result.messages[0].content).toContain('Invalid tool call arguments');
|
|
320
588
|
expect(result.messages[1].status).toBe('success');
|
|
321
589
|
expect(result.messages[1].content).toBe('normal result');
|
|
322
590
|
});
|
|
@@ -328,9 +596,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
328
596
|
const hookRegistry = new HookRegistry();
|
|
329
597
|
hookRegistry.register('PreToolUse', {
|
|
330
598
|
hooks: [
|
|
331
|
-
async (
|
|
332
|
-
input: PreToolUseHookInput
|
|
333
|
-
): Promise<PreToolUseHookOutput> => {
|
|
599
|
+
async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {
|
|
334
600
|
preToolTurns.push(input.turn);
|
|
335
601
|
return { decision: 'allow' };
|
|
336
602
|
},
|
|
@@ -351,9 +617,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
351
617
|
});
|
|
352
618
|
|
|
353
619
|
await toolNode.invoke({
|
|
354
|
-
messages: [
|
|
355
|
-
createAIMessage('call_weather_1', 'weather', { city: 'NYC' }),
|
|
356
|
-
],
|
|
620
|
+
messages: [createAIMessage('call_weather_1', 'weather', { city: 'NYC' })],
|
|
357
621
|
});
|
|
358
622
|
|
|
359
623
|
eagerUsageCount.clear();
|
package/src/types/tools.ts
CHANGED
|
@@ -49,6 +49,13 @@ export type EagerEventToolExecution = {
|
|
|
49
49
|
args: Record<string, unknown>;
|
|
50
50
|
request: ToolCallRequest;
|
|
51
51
|
promise: Promise<EagerEventToolExecutionOutcome>;
|
|
52
|
+
/**
|
|
53
|
+
* True when the streaming eager path already emitted the user-visible
|
|
54
|
+
* ON_RUN_STEP_COMPLETED event for this call. ToolNode still consumes the
|
|
55
|
+
* result later to mutate graph state in provider-safe order, but skips
|
|
56
|
+
* duplicate completion emission.
|
|
57
|
+
*/
|
|
58
|
+
completionDispatched?: boolean;
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
export type EagerEventToolCallChunkState = {
|
|
@@ -172,6 +179,8 @@ export type ToolEndEvent = {
|
|
|
172
179
|
/** The content index of the tool call */
|
|
173
180
|
index: number;
|
|
174
181
|
type?: 'tool_call';
|
|
182
|
+
/** True when the stream eager path surfaced this completion before ToolNode finalized graph state. */
|
|
183
|
+
eager?: boolean;
|
|
175
184
|
};
|
|
176
185
|
|
|
177
186
|
/**
|
package/src/utils/events.ts
CHANGED
|
@@ -13,9 +13,10 @@ export async function safeDispatchCustomEvent(
|
|
|
13
13
|
event: string,
|
|
14
14
|
payload: unknown,
|
|
15
15
|
config?: RunnableConfig
|
|
16
|
-
): Promise<void> {
|
|
16
|
+
): Promise<boolean | void> {
|
|
17
17
|
try {
|
|
18
18
|
await dispatchCustomEvent(event, payload, config);
|
|
19
|
+
return true;
|
|
19
20
|
} catch (e) {
|
|
20
21
|
// Check if this is the known EventStreamCallbackHandler error
|
|
21
22
|
if (
|
|
@@ -26,10 +27,11 @@ export async function safeDispatchCustomEvent(
|
|
|
26
27
|
// Suppress this specific error - it's expected during parallel execution
|
|
27
28
|
// when EventStreamCallbackHandler loses track of run IDs
|
|
28
29
|
// console.debug('Suppressed error dispatching custom event:', e);
|
|
29
|
-
return;
|
|
30
|
+
return false;
|
|
30
31
|
}
|
|
31
32
|
// Log other errors
|
|
32
33
|
console.error('Error dispatching custom event:', e);
|
|
34
|
+
return false;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|