@librechat/agents 3.1.88 → 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.
@@ -145,6 +145,14 @@ export abstract class Graph<
145
145
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
146
146
  invokedToolIds?: Set<string>;
147
147
  handlerRegistry: HandlerRegistry | undefined;
148
+ /**
149
+ * True when event-driven tool execution can be routed through callbacks even
150
+ * though this graph intentionally does not own the full handler registry.
151
+ * Self-spawned subagent graphs use this shape: their callback forwarder sends
152
+ * `ON_TOOL_EXECUTE` to the parent's handler, while child run-step events stay
153
+ * wrapped as `ON_SUBAGENT_UPDATE` instead of leaking as parent events.
154
+ */
155
+ eventToolExecutionAvailable: boolean = false;
148
156
  hookRegistry: HookRegistry | undefined;
149
157
  /**
150
158
  * Run-scoped HITL configuration. When `humanInTheLoop?.enabled` is
@@ -167,10 +175,8 @@ export abstract class Graph<
167
175
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
168
176
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
169
177
  eagerEventToolUsageCount: Map<string, number> = new Map();
170
- private eagerEventToolUsageCountsByAgentId: Map<
171
- string,
172
- Map<string, number>
173
- > = new Map();
178
+ private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
179
+ new Map();
174
180
  eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
175
181
  new Map();
176
182
  /**
@@ -1554,7 +1560,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1554
1560
  parentAgentId: agentContext.agentId,
1555
1561
  tokenCounter: agentContext.tokenCounter,
1556
1562
  maxDepth: effectiveSubagentDepth,
1557
- createChildGraph: (input): StandardGraph => new StandardGraph(input),
1563
+ createChildGraph: (input): StandardGraph => {
1564
+ const childGraph = new StandardGraph(input);
1565
+ childGraph.toolOutputReferences = this.toolOutputReferences;
1566
+ childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
1567
+ childGraph.toolExecution = this.toolExecution;
1568
+ childGraph.eventToolExecutionAvailable =
1569
+ this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
1570
+ null;
1571
+ return childGraph;
1572
+ },
1558
1573
  });
1559
1574
 
1560
1575
  const subagentTool = tool(async (rawInput, config) => {
@@ -1,4 +1,4 @@
1
- import { HumanMessage } from '@langchain/core/messages';
1
+ import { AIMessage, HumanMessage } from '@langchain/core/messages';
2
2
  import { FakeListChatModel } from '@langchain/core/utils/testing';
3
3
  import type { ToolCall } from '@langchain/core/messages/tool';
4
4
  import type { RunnableConfig } from '@langchain/core/runnables';
@@ -220,6 +220,92 @@ describe('Subagent Integration', () => {
220
220
  expect(subagentTool).toBeDefined();
221
221
  });
222
222
 
223
+ it('inherits eager event-tool settings into self-spawn child graphs', async () => {
224
+ const originalCreateWorkflow = StandardGraph.prototype.createWorkflow;
225
+ const observedChildGraphs: Array<{
226
+ eagerEventToolExecution: StandardGraph['eagerEventToolExecution'];
227
+ toolOutputReferences: StandardGraph['toolOutputReferences'];
228
+ eventToolExecutionAvailable: boolean;
229
+ }> = [];
230
+ const createWorkflowSpy = jest
231
+ .spyOn(StandardGraph.prototype, 'createWorkflow')
232
+ .mockImplementation(function (this: StandardGraph) {
233
+ if (this.runId?.includes('_sub_') === true) {
234
+ observedChildGraphs.push({
235
+ eagerEventToolExecution: this.eagerEventToolExecution,
236
+ toolOutputReferences: this.toolOutputReferences,
237
+ eventToolExecutionAvailable: this.eventToolExecutionAvailable,
238
+ });
239
+ return {
240
+ invoke: jest.fn(async () => ({
241
+ messages: [new AIMessage('child done')],
242
+ })),
243
+ } as unknown as ReturnType<StandardGraph['createWorkflow']>;
244
+ }
245
+ return originalCreateWorkflow.call(this);
246
+ });
247
+
248
+ const agentWithSelfSpawn: t.AgentInputs = {
249
+ agentId: 'self-parent',
250
+ provider: Providers.OPENAI,
251
+ clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
252
+ instructions: 'Agent with self-spawn for context isolation.',
253
+ maxContextTokens: 8000,
254
+ toolDefinitions: [{ name: 'mcp_lookup' }],
255
+ subagentConfigs: [
256
+ {
257
+ type: 'isolated',
258
+ name: 'Isolated Worker',
259
+ description: 'Runs a task with isolated context',
260
+ self: true,
261
+ },
262
+ ],
263
+ };
264
+
265
+ const run = await Run.create<t.IState>({
266
+ runId: `self-spawn-eager-${Date.now()}`,
267
+ graphConfig: {
268
+ type: 'standard',
269
+ agents: [agentWithSelfSpawn],
270
+ },
271
+ customHandlers: {
272
+ [GraphEvents.ON_TOOL_EXECUTE]: {
273
+ handle: async () => undefined,
274
+ },
275
+ },
276
+ eagerEventToolExecution: { enabled: true },
277
+ toolOutputReferences: { enabled: true },
278
+ returnContent: true,
279
+ skipCleanup: true,
280
+ });
281
+
282
+ const context = (run.Graph as StandardGraph).agentContexts.get(
283
+ 'self-parent'
284
+ );
285
+ const subagentTool = (context?.graphTools as t.GenericTool[]).find(
286
+ (tool) => 'name' in tool && tool.name === Constants.SUBAGENT
287
+ );
288
+ expect(subagentTool).toBeDefined();
289
+
290
+ await subagentTool!.invoke(
291
+ {
292
+ description: 'Use your MCP tool.',
293
+ subagent_type: 'isolated',
294
+ },
295
+ callerConfig
296
+ );
297
+
298
+ expect(observedChildGraphs).toEqual([
299
+ {
300
+ eagerEventToolExecution: { enabled: true },
301
+ toolOutputReferences: { enabled: true },
302
+ eventToolExecutionAvailable: true,
303
+ },
304
+ ]);
305
+
306
+ createWorkflowSpy.mockRestore();
307
+ });
308
+
223
309
  it('should not create subagent tool when maxSubagentDepth is 0', async () => {
224
310
  const agentWithZeroDepth: t.AgentInputs = {
225
311
  ...createParentAgent(),
package/src/stream.ts CHANGED
@@ -27,11 +27,16 @@ import {
27
27
  coerceRecordArgs,
28
28
  normalizeError,
29
29
  } from '@/tools/eagerEventExecution';
30
+ import {
31
+ calculateMaxToolResultChars,
32
+ truncateToolResultContent,
33
+ } from '@/utils/truncation';
30
34
  import {
31
35
  getStreamedToolCallSeal,
32
36
  getStreamedToolCallAdapter,
33
37
  type StreamedToolCallSeal,
34
38
  } from '@/tools/streamedToolCallSeals';
39
+ import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
35
40
 
36
41
  const LOCAL_CODING_BUNDLE_NAME_SET: ReadonlySet<string> = new Set(
37
42
  LOCAL_CODING_BUNDLE_NAMES
@@ -98,11 +103,22 @@ function getNonEmptyValue(possibleValues: string[]): string | undefined {
98
103
  }
99
104
 
100
105
  function isBatchSensitiveToolExecution(graph: StandardGraph): boolean {
101
- return (
102
- graph.hookRegistry != null ||
103
- graph.humanInTheLoop?.enabled === true ||
104
- graph.toolOutputReferences?.enabled === true
105
- );
106
+ return graph.hookRegistry != null || graph.humanInTheLoop?.enabled === true;
107
+ }
108
+
109
+ function hasToolOutputReference(value: unknown): boolean {
110
+ if (typeof value === 'string') {
111
+ return TOOL_OUTPUT_REF_PATTERN.test(value);
112
+ }
113
+ if (Array.isArray(value)) {
114
+ return value.some((item) => hasToolOutputReference(item));
115
+ }
116
+ if (value !== null && typeof value === 'object') {
117
+ return Object.values(value as Record<string, unknown>).some((item) =>
118
+ hasToolOutputReference(item)
119
+ );
120
+ }
121
+ return false;
106
122
  }
107
123
 
108
124
  function isDirectGraphTool(
@@ -194,7 +210,10 @@ function isEagerToolExecutionEnabledForBatch(args: {
194
210
  ) {
195
211
  return false;
196
212
  }
197
- if (graph.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) == null) {
213
+ if (
214
+ graph.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) == null &&
215
+ graph.eventToolExecutionAvailable !== true
216
+ ) {
198
217
  return false;
199
218
  }
200
219
  return true;
@@ -257,13 +276,49 @@ function hasPotentialDirectToolInStreamContext(args: {
257
276
  if ((agentContext?.graphTools?.length ?? 0) > 0) {
258
277
  return true;
259
278
  }
279
+ return false;
280
+ }
281
+
282
+ function hasDirectToolCallChunkInBatch(args: {
283
+ graph: StandardGraph;
284
+ agentContext?: AgentContext;
285
+ toolCallChunks?: ToolCallChunk[];
286
+ }): boolean {
287
+ const { graph, agentContext, toolCallChunks } = args;
260
288
  return (
261
- agentContext?.toolDefinitions?.some((toolDefinition) =>
262
- toolDefinition.name.startsWith(Constants.LC_TRANSFER_TO_)
289
+ toolCallChunks?.some(
290
+ (toolCallChunk) =>
291
+ toolCallChunk.name != null &&
292
+ toolCallChunk.name !== '' &&
293
+ (isDirectGraphTool(toolCallChunk.name, agentContext) ||
294
+ isDirectLocalTool(toolCallChunk.name, graph))
263
295
  ) === true
264
296
  );
265
297
  }
266
298
 
299
+ function hasDirectToolCallChunkStateInStep(args: {
300
+ graph: StandardGraph;
301
+ agentContext?: AgentContext;
302
+ stepKey: string;
303
+ }): boolean {
304
+ const { graph, agentContext, stepKey } = args;
305
+ const prefix = `${stepKey}\u0000`;
306
+ for (const [key, state] of graph.eagerEventToolCallChunks) {
307
+ if (!key.startsWith(prefix)) {
308
+ continue;
309
+ }
310
+ const name = state.name;
311
+ if (
312
+ name != null &&
313
+ name !== '' &&
314
+ (isDirectGraphTool(name, agentContext) || isDirectLocalTool(name, graph))
315
+ ) {
316
+ return true;
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
267
322
  type EagerToolExecutionEntry = {
268
323
  id: string;
269
324
  toolName: string;
@@ -298,6 +353,12 @@ function createEagerToolExecutionPlan(args: {
298
353
  if (hasDirectToolCallInBatch({ graph, agentContext, toolCalls })) {
299
354
  return undefined;
300
355
  }
356
+ if (
357
+ graph.toolOutputReferences?.enabled === true &&
358
+ toolCalls.some((toolCall) => hasToolOutputReference(toolCall.args))
359
+ ) {
360
+ return undefined;
361
+ }
301
362
 
302
363
  const candidateToolCalls = skipExisting
303
364
  ? toolCalls.filter((toolCall) => {
@@ -369,6 +430,7 @@ function startEagerToolExecutions(args: {
369
430
  return;
370
431
  }
371
432
 
433
+ const records: t.EagerEventToolExecution[] = [];
372
434
  const promise: Promise<t.EagerEventToolExecutionOutcome> = new Promise<
373
435
  t.ToolExecuteResult[]
374
436
  >((resolve, reject) => {
@@ -407,20 +469,104 @@ function startEagerToolExecutions(args: {
407
469
  })
408
470
  .catch(reject);
409
471
  }).then(
410
- (results): t.EagerEventToolExecutionOutcome => ({ results }),
472
+ async (results): Promise<t.EagerEventToolExecutionOutcome> => {
473
+ await dispatchEagerToolCompletions({
474
+ graph,
475
+ agentContext,
476
+ records,
477
+ results,
478
+ });
479
+ return { results };
480
+ },
411
481
  (error): t.EagerEventToolExecutionOutcome => ({
412
482
  error: normalizeError(error),
413
483
  })
414
484
  );
415
485
 
416
486
  for (const entry of entries) {
417
- graph.eagerEventToolExecutions.set(entry.id, {
487
+ const record: t.EagerEventToolExecution = {
418
488
  toolCallId: entry.id,
419
489
  toolName: entry.toolName,
420
490
  args: entry.coercedArgs,
421
491
  request: entry.request,
422
492
  promise,
423
- });
493
+ };
494
+ records.push(record);
495
+ graph.eagerEventToolExecutions.set(entry.id, record);
496
+ }
497
+ }
498
+
499
+ async function dispatchEagerToolCompletions(args: {
500
+ graph: StandardGraph;
501
+ agentContext?: AgentContext;
502
+ records: t.EagerEventToolExecution[];
503
+ results: t.ToolExecuteResult[];
504
+ }): Promise<void> {
505
+ const { graph, agentContext, records, results } = args;
506
+ const recordById = new Map(
507
+ records.map((record) => [record.toolCallId, record])
508
+ );
509
+ const maxToolResultChars =
510
+ agentContext?.maxToolResultChars ??
511
+ calculateMaxToolResultChars(agentContext?.maxContextTokens);
512
+
513
+ for (const result of results) {
514
+ const record = recordById.get(result.toolCallId);
515
+ if (record == null) {
516
+ continue;
517
+ }
518
+ if (graph.eagerEventToolExecutions.get(result.toolCallId) !== record) {
519
+ continue;
520
+ }
521
+ const stepId =
522
+ record.request.stepId ??
523
+ graph.toolCallStepIds.get(result.toolCallId) ??
524
+ '';
525
+ if (stepId === '') {
526
+ continue;
527
+ }
528
+ const output =
529
+ result.status === 'error'
530
+ ? `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`
531
+ : truncateToolResultContent(
532
+ typeof result.content === 'string'
533
+ ? result.content
534
+ : JSON.stringify(result.content),
535
+ maxToolResultChars
536
+ );
537
+
538
+ try {
539
+ const dispatched = await safeDispatchCustomEvent(
540
+ GraphEvents.ON_RUN_STEP_COMPLETED,
541
+ {
542
+ result: {
543
+ id: stepId,
544
+ index: record.request.turn ?? 0,
545
+ type: 'tool_call' as const,
546
+ eager: true,
547
+ tool_call: {
548
+ args: JSON.stringify(record.request.args),
549
+ name: record.toolName,
550
+ id: result.toolCallId,
551
+ output,
552
+ progress: 1,
553
+ } as t.ProcessedToolCall,
554
+ },
555
+ },
556
+ graph.config
557
+ );
558
+ if (dispatched === false) {
559
+ continue;
560
+ }
561
+ record.completionDispatched = true;
562
+ } catch (error) {
563
+ // Let ToolNode dispatch the completion through the normal path later.
564
+
565
+ console.warn(
566
+ `[stream] eager completion dispatch failed for toolCallId=${result.toolCallId}:`,
567
+ error instanceof Error ? error.message : error
568
+ );
569
+ }
424
570
  }
425
571
  }
426
572
 
@@ -699,6 +845,8 @@ function startReadyStreamedEagerToolExecutions(args: {
699
845
  } = args;
700
846
  if (
701
847
  hasPotentialDirectToolInStreamContext({ graph, agentContext }) ||
848
+ hasDirectToolCallChunkInBatch({ graph, agentContext, toolCallChunks }) ||
849
+ hasDirectToolCallChunkStateInStep({ graph, agentContext, stepKey }) ||
702
850
  !isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext })
703
851
  ) {
704
852
  return;
@@ -1265,9 +1413,12 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1265
1413
 
1266
1414
  const existingContent = contentParts[index] as
1267
1415
  | (Omit<t.ToolCallContent, 'tool_call'> & {
1268
- tool_call?: t.ToolCallPart;
1416
+ tool_call?: t.ToolCallPart & t.PartMetadata;
1269
1417
  })
1270
1418
  | undefined;
1419
+ if (!finalUpdate && existingContent?.tool_call?.progress === 1) {
1420
+ return;
1421
+ }
1271
1422
 
1272
1423
  /** When args are a valid object, they are likely already invoked */
1273
1424
  let args =