@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.
@@ -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;
@@ -215,10 +234,11 @@ function hasFinalToolCallSignal(chunk: Partial<AIMessageChunk>): boolean {
215
234
  function canPrestartSequentialStreamedToolChunks(
216
235
  agentContext: AgentContext | undefined
217
236
  ): boolean {
218
- return (
219
- agentContext?.provider === Providers.ANTHROPIC ||
220
- agentContext?.provider === Providers.MOONSHOT
221
- );
237
+ // Anthropic seals each prior streamed tool-use block when the next indexed
238
+ // tool-use block begins. Live Kimi/Moonshot streams can still revise prior
239
+ // args after advancing to the next index, so keep those on the final
240
+ // tool-call path unless they grow an explicit adapter seal.
241
+ return agentContext?.provider === Providers.ANTHROPIC;
222
242
  }
223
243
 
224
244
  function hasExplicitStreamedToolCallSeals(
@@ -256,13 +276,49 @@ function hasPotentialDirectToolInStreamContext(args: {
256
276
  if ((agentContext?.graphTools?.length ?? 0) > 0) {
257
277
  return true;
258
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;
259
288
  return (
260
- agentContext?.toolDefinitions?.some((toolDefinition) =>
261
- 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))
262
295
  ) === true
263
296
  );
264
297
  }
265
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
+
266
322
  type EagerToolExecutionEntry = {
267
323
  id: string;
268
324
  toolName: string;
@@ -297,6 +353,12 @@ function createEagerToolExecutionPlan(args: {
297
353
  if (hasDirectToolCallInBatch({ graph, agentContext, toolCalls })) {
298
354
  return undefined;
299
355
  }
356
+ if (
357
+ graph.toolOutputReferences?.enabled === true &&
358
+ toolCalls.some((toolCall) => hasToolOutputReference(toolCall.args))
359
+ ) {
360
+ return undefined;
361
+ }
300
362
 
301
363
  const candidateToolCalls = skipExisting
302
364
  ? toolCalls.filter((toolCall) => {
@@ -368,6 +430,7 @@ function startEagerToolExecutions(args: {
368
430
  return;
369
431
  }
370
432
 
433
+ const records: t.EagerEventToolExecution[] = [];
371
434
  const promise: Promise<t.EagerEventToolExecutionOutcome> = new Promise<
372
435
  t.ToolExecuteResult[]
373
436
  >((resolve, reject) => {
@@ -406,20 +469,104 @@ function startEagerToolExecutions(args: {
406
469
  })
407
470
  .catch(reject);
408
471
  }).then(
409
- (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
+ },
410
481
  (error): t.EagerEventToolExecutionOutcome => ({
411
482
  error: normalizeError(error),
412
483
  })
413
484
  );
414
485
 
415
486
  for (const entry of entries) {
416
- graph.eagerEventToolExecutions.set(entry.id, {
487
+ const record: t.EagerEventToolExecution = {
417
488
  toolCallId: entry.id,
418
489
  toolName: entry.toolName,
419
490
  args: entry.coercedArgs,
420
491
  request: entry.request,
421
492
  promise,
422
- });
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
+ }
423
570
  }
424
571
  }
425
572
 
@@ -698,6 +845,8 @@ function startReadyStreamedEagerToolExecutions(args: {
698
845
  } = args;
699
846
  if (
700
847
  hasPotentialDirectToolInStreamContext({ graph, agentContext }) ||
848
+ hasDirectToolCallChunkInBatch({ graph, agentContext, toolCallChunks }) ||
849
+ hasDirectToolCallChunkStateInStep({ graph, agentContext, stepKey }) ||
701
850
  !isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext })
702
851
  ) {
703
852
  return;
@@ -1264,9 +1413,12 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1264
1413
 
1265
1414
  const existingContent = contentParts[index] as
1266
1415
  | (Omit<t.ToolCallContent, 'tool_call'> & {
1267
- tool_call?: t.ToolCallPart;
1416
+ tool_call?: t.ToolCallPart & t.PartMetadata;
1268
1417
  })
1269
1418
  | undefined;
1419
+ if (!finalUpdate && existingContent?.tool_call?.progress === 1) {
1420
+ return;
1421
+ }
1270
1422
 
1271
1423
  /** When args are a valid object, they are likely already invoked */
1272
1424
  let args =