@librechat/agents 3.7.21 → 3.7.22

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.
Files changed (41) hide show
  1. package/dist/cjs/graphs/Graph.cjs +21 -4
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/invoke.cjs +21 -6
  4. package/dist/cjs/llm/invoke.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +2 -0
  6. package/dist/cjs/stream.cjs +38 -1
  7. package/dist/cjs/stream.cjs.map +1 -1
  8. package/dist/cjs/tools/ToolNode.cjs +95 -18
  9. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  10. package/dist/cjs/tools/ToolSearch.cjs +130 -24
  11. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  12. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +1 -1
  13. package/dist/cjs/tools/preparedSubagents.cjs +129 -0
  14. package/dist/cjs/tools/preparedSubagents.cjs.map +1 -0
  15. package/dist/esm/graphs/Graph.mjs +21 -4
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/llm/invoke.mjs +21 -6
  18. package/dist/esm/llm/invoke.mjs.map +1 -1
  19. package/dist/esm/main.mjs +2 -2
  20. package/dist/esm/stream.mjs +39 -2
  21. package/dist/esm/stream.mjs.map +1 -1
  22. package/dist/esm/tools/ToolNode.mjs +95 -18
  23. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  24. package/dist/esm/tools/ToolSearch.mjs +129 -25
  25. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  26. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +1 -1
  27. package/dist/esm/tools/preparedSubagents.mjs +128 -0
  28. package/dist/esm/tools/preparedSubagents.mjs.map +1 -0
  29. package/dist/types/graphs/Graph.d.ts +6 -1
  30. package/dist/types/tools/ToolNode.d.ts +13 -1
  31. package/dist/types/tools/ToolSearch.d.ts +46 -2
  32. package/dist/types/tools/preparedSubagents.d.ts +26 -0
  33. package/dist/types/types/tools.d.ts +17 -0
  34. package/package.json +1 -1
  35. package/src/graphs/Graph.ts +63 -5
  36. package/src/llm/invoke.ts +35 -8
  37. package/src/stream.ts +70 -1
  38. package/src/tools/ToolNode.ts +189 -43
  39. package/src/tools/ToolSearch.ts +278 -28
  40. package/src/tools/preparedSubagents.ts +228 -0
  41. package/src/types/tools.ts +17 -0
@@ -3,6 +3,7 @@ import type { StructuredToolInterface } from '@langchain/core/tools';
3
3
  import type { ToolCall } from '@langchain/core/messages/tool';
4
4
  import type { MessageContentComplex, RunStepResumeState, ToolErrorData } from './stream';
5
5
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
6
+ import type { PreparedSubagents } from '@/tools/preparedSubagents';
6
7
  import type { RunBreakerScope } from '@/llm/streamLimits';
7
8
  import type { HumanInTheLoopConfig } from './hitl';
8
9
  import type { LangfuseConfig } from './graph';
@@ -42,6 +43,13 @@ export type EagerEventToolExecutionConfig = {
42
43
  * execution with final args.
43
44
  */
44
45
  excludeToolNames?: string[];
46
+ /**
47
+ * Maximum retained early foreground subagent invocations per graph. Defaults
48
+ * to 4; 0 disables them. Checkpoints, parent tool hooks, background calls and
49
+ * control-flow graph tools retain normal batch execution. Once a child has
50
+ * started, a failed/revised model attempt fails instead of automatically retrying.
51
+ */
52
+ maxPendingSubagents?: number;
45
53
  };
46
54
  export type EagerEventToolExecutionOutcome = {
47
55
  results: ToolExecuteResult[];
@@ -273,6 +281,8 @@ export type ToolNodeOptions = {
273
281
  * controller.
274
282
  */
275
283
  getRunScope?: () => RunBreakerScope;
284
+ /** Internal graph-owned prepared invocation registry. */
285
+ preparedSubagents?: PreparedSubagents;
276
286
  /** SDK-owned checkpoint bridge for open run-step lifecycle state. */
277
287
  restoreRunStepResumeState?: (state?: RunStepResumeState, config?: RunnableConfig) => void;
278
288
  /** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
@@ -1160,6 +1170,13 @@ export type ToolSearchArtifact = {
1160
1170
  total_searched: number;
1161
1171
  pattern: string;
1162
1172
  error?: string;
1173
+ /** Servers that had tools to search, emitted when a server filter matched
1174
+ * nothing so the caller can see what it could have asked for. */
1175
+ available_mcp_servers?: string[];
1176
+ /** Requested servers that matched no registered server. */
1177
+ unmatched_mcp_servers?: string[];
1178
+ /** Requested servers that exist but had no tools left to search. */
1179
+ idle_mcp_servers?: string[];
1163
1180
  };
1164
1181
  };
1165
1182
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.7.21",
3
+ "version": "3.7.22",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -146,6 +146,7 @@ import {
146
146
  findCallback,
147
147
  type CallbackEntry,
148
148
  } from '@/utils/callbacks';
149
+ import { PreparedSubagents, PreparedSubagentError } from '@/tools/preparedSubagents';
149
150
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
150
151
  import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
151
152
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
@@ -898,6 +899,7 @@ export abstract class Graph<
898
899
  * Call after a run completes and content has been extracted.
899
900
  */
900
901
  clearHeavyState(): void {
902
+ this.preparedSubagents.clear();
901
903
  this.config = undefined;
902
904
  this.signal = undefined;
903
905
  this.callerSignal = undefined;
@@ -1239,6 +1241,8 @@ export abstract class Graph<
1239
1241
  restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
1240
1242
  }> = new Set();
1241
1243
  private _subagentExecutors = new Set<SubagentExecutor>();
1244
+ readonly preparedSubagents = new PreparedSubagents();
1245
+ protected readonly subagentToolNodes = new Map<string, CustomToolNode<t.BaseGraphState>>();
1242
1246
  public getOrCreateFileCheckpointer(): t.LocalFileCheckpointer | undefined {
1243
1247
  // Return the cached instance unconditionally if one exists. The
1244
1248
  // toolExecution check below decides whether to *create* a new
@@ -1397,16 +1401,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1397
1401
  * paths must not run in that state. */
1398
1402
  protected resolveTrippedBreakerReason(
1399
1403
  breakerSignal: AbortSignal = this.breakerAbort.signal
1400
- ): StreamLimitExceededError | undefined {
1404
+ ): StreamLimitExceededError | PreparedSubagentError | undefined {
1401
1405
  if (
1402
1406
  breakerSignal.aborted &&
1403
- breakerSignal.reason instanceof StreamLimitExceededError
1407
+ (breakerSignal.reason instanceof StreamLimitExceededError || breakerSignal.reason instanceof PreparedSubagentError)
1404
1408
  ) {
1405
1409
  return breakerSignal.reason;
1406
1410
  }
1407
1411
  if (
1408
1412
  this.signal?.aborted === true &&
1409
- this.signal.reason instanceof StreamLimitExceededError
1413
+ (this.signal.reason instanceof StreamLimitExceededError || this.signal.reason instanceof PreparedSubagentError)
1410
1414
  ) {
1411
1415
  return this.signal.reason;
1412
1416
  }
@@ -1581,6 +1585,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1581
1585
  /* Init */
1582
1586
 
1583
1587
  resetValues(keepContent?: boolean, checkpointScope?: string): void {
1588
+ this.preparedSubagents.clear();
1584
1589
  this.resetSubagentCheckpointThreadIds();
1585
1590
  this.messages = [];
1586
1591
  this.hasRestoredCheckpointMessages = false;
@@ -2717,6 +2722,49 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2717
2722
 
2718
2723
  /* Graph */
2719
2724
 
2725
+ canPrestartSubagents(agentContext?: AgentContext): boolean {
2726
+ if (
2727
+ this.eagerEventToolExecution?.enabled !== true ||
2728
+ (this.eagerEventToolExecution.maxPendingSubagents ?? 4) <= 0 ||
2729
+ this.compileOptions?.checkpointer != null ||
2730
+ this.humanInTheLoop?.enabled === true ||
2731
+ (this.interruptingToolNames?.length ?? 0) > 0 ||
2732
+ agentContext == null ||
2733
+ !this.subagentToolNodes.has(agentContext.agentId)
2734
+ ) {
2735
+ return false;
2736
+ }
2737
+ const graphTools = agentContext.graphTools as t.GenericTool[] | undefined;
2738
+ return (
2739
+ graphTools?.length === 1 &&
2740
+ graphTools[0].name === Constants.SUBAGENT &&
2741
+ SUBAGENT_REPLAY_CONTROLLER in graphTools[0]
2742
+ );
2743
+ }
2744
+
2745
+ prestartSubagent(
2746
+ call: ToolCall,
2747
+ attempt: string,
2748
+ agentContext?: AgentContext
2749
+ ): void {
2750
+ const config = this.preparedSubagents.getConfig(attempt);
2751
+ if (
2752
+ !this.canPrestartSubagents(agentContext) ||
2753
+ config == null ||
2754
+ agentContext == null
2755
+ ) {
2756
+ return;
2757
+ }
2758
+ this.subagentToolNodes
2759
+ .get(agentContext.agentId)
2760
+ ?.prestartSubagent(
2761
+ call,
2762
+ attempt,
2763
+ config,
2764
+ this.eagerEventToolExecution?.maxPendingSubagents ?? 4
2765
+ );
2766
+ }
2767
+
2720
2768
  initializeTools({
2721
2769
  currentTools,
2722
2770
  currentToolMap,
@@ -2791,6 +2839,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2791
2839
  hookRegistry: this.hookRegistry,
2792
2840
  humanInTheLoop: this.humanInTheLoop,
2793
2841
  eagerEventToolExecution: this.eagerEventToolExecution,
2842
+ preparedSubagents: this.preparedSubagents,
2794
2843
  codeSessionToolNames: this.codeSessionToolNames,
2795
2844
  eagerEventToolExecutions: this.eagerEventToolExecutions,
2796
2845
  eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
@@ -2816,6 +2865,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2816
2865
  StandardGraph.handleToolCallErrorStatic(this, data, metadata),
2817
2866
  });
2818
2867
  this.registerCompiledToolNode(node);
2868
+ if (agentContext != null) {
2869
+ this.subagentToolNodes.set(agentContext.agentId, node);
2870
+ }
2819
2871
  return node;
2820
2872
  }
2821
2873
 
@@ -4092,7 +4144,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4092
4144
  * succeeding fallback would resolve a run the public contract says
4093
4145
  * must reject. Rethrow before any recovery path.
4094
4146
  */
4095
- if (primaryError instanceof StreamLimitExceededError) {
4147
+ if (
4148
+ primaryError instanceof StreamLimitExceededError ||
4149
+ primaryError instanceof PreparedSubagentError
4150
+ ) {
4096
4151
  /** Tripped before rethrowing so parallel agent nodes' in-flight
4097
4152
  * model calls and subagents stop while the rejection propagates. */
4098
4153
  attemptBreaker.abort(primaryError);
@@ -4342,7 +4397,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4342
4397
  })
4343
4398
  );
4344
4399
  } catch (fallbackError) {
4345
- if (fallbackError instanceof StreamLimitExceededError) {
4400
+ if (
4401
+ fallbackError instanceof StreamLimitExceededError ||
4402
+ fallbackError instanceof PreparedSubagentError
4403
+ ) {
4346
4404
  /** Same treatment as the primary path: a fallback stream that
4347
4405
  * trips the breaker must stop parallel agent nodes' model calls
4348
4406
  * and subagents before the rejection propagates. */
package/src/llm/invoke.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  } from '@/llm/providers';
52
52
  import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
53
53
  import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
54
+ import { PreparedSubagentError } from '@/tools/preparedSubagents';
54
55
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
55
56
  import { resolveClientOptionsModel } from '@/llm/request';
56
57
  import { safeDispatchCustomEvent } from '@/utils/events';
@@ -702,9 +703,8 @@ export async function attemptInvoke(
702
703
  },
703
704
  };
704
705
  const request = resolveAttemptRequest(params, providerStampedConfig);
705
- const configuredModel = providerStampedConfig.metadata?.[
706
- Constants.INVOKED_MODEL
707
- ];
706
+ const configuredModel =
707
+ providerStampedConfig.metadata?.[Constants.INVOKED_MODEL];
708
708
  const modelId =
709
709
  request.modelId ??
710
710
  (typeof configuredModel === 'string' ? configuredModel : undefined);
@@ -745,8 +745,14 @@ export async function attemptInvoke(
745
745
  if (leaseTarget != null && generationKey != null) {
746
746
  registerActiveStreamLimitGeneration(leaseTarget, generationKey);
747
747
  }
748
+ const prepared =
749
+ params.context?.eagerEventToolExecution?.enabled === true
750
+ ? params.context.preparedSubagents
751
+ : undefined;
752
+ const preparedAttempt = resolveGenerationKey(stampedConfig.metadata);
753
+ prepared?.begin(preparedAttempt, stampedConfig);
748
754
  try {
749
- return await attemptInvokeBody(
755
+ const result = await attemptInvokeBody(
750
756
  {
751
757
  request,
752
758
  context: params.context,
@@ -755,7 +761,27 @@ export async function attemptInvoke(
755
761
  },
756
762
  stampedConfig
757
763
  );
764
+ const calls =
765
+ result.messages?.flatMap((message) =>
766
+ 'tool_calls' in message
767
+ ? ((message.tool_calls as ToolCall[] | undefined) ?? [])
768
+ : []
769
+ ) ?? [];
770
+ prepared?.finish(preparedAttempt, calls);
771
+ return result;
772
+ } catch (error) {
773
+ prepared?.finish(preparedAttempt, undefined, error);
774
+ throw error;
758
775
  } finally {
776
+ const chunks = params.context?.eagerEventToolCallChunks;
777
+ if (prepared != null && chunks != null) {
778
+ const prefix = `prepared:${preparedAttempt}\u0000`;
779
+ for (const key of chunks.keys()) {
780
+ if (key.startsWith(prefix)) {
781
+ chunks.delete(key);
782
+ }
783
+ }
784
+ }
759
785
  if (leaseTarget != null && generationKey != null) {
760
786
  releaseStreamLimitGeneration(leaseTarget, generationKey);
761
787
  }
@@ -1053,7 +1079,7 @@ async function attemptInvokeBody(
1053
1079
  const signal = config.signal;
1054
1080
  if (
1055
1081
  signal?.aborted === true &&
1056
- signal.reason instanceof StreamLimitExceededError
1082
+ (signal.reason instanceof StreamLimitExceededError || signal.reason instanceof PreparedSubagentError)
1057
1083
  ) {
1058
1084
  throw signal.reason;
1059
1085
  }
@@ -1290,6 +1316,7 @@ async function attemptInvokeBody(
1290
1316
  const ownAbort =
1291
1317
  restartRoute === 'aborted' &&
1292
1318
  !(error instanceof StreamLimitExceededError) &&
1319
+ !(error instanceof PreparedSubagentError) &&
1293
1320
  config.signal?.aborted !== true;
1294
1321
  if (!ownAbort) {
1295
1322
  throw error;
@@ -1639,7 +1666,7 @@ export async function tryFallbackProviders({
1639
1666
  * a run that must reject. Check before every fallback invocation. */
1640
1667
  if (
1641
1668
  config?.signal?.aborted === true &&
1642
- config.signal.reason instanceof StreamLimitExceededError
1669
+ (config.signal.reason instanceof StreamLimitExceededError || config.signal.reason instanceof PreparedSubagentError)
1643
1670
  ) {
1644
1671
  throw config.signal.reason;
1645
1672
  }
@@ -1673,7 +1700,7 @@ export async function tryFallbackProviders({
1673
1700
  * provider failure. Continuing would try the remaining fallbacks and a
1674
1701
  * succeeding one would resolve a run that must reject.
1675
1702
  */
1676
- if (e instanceof StreamLimitExceededError) {
1703
+ if (e instanceof StreamLimitExceededError || e instanceof PreparedSubagentError) {
1677
1704
  throw e;
1678
1705
  }
1679
1706
  /** A parallel sibling's trip aborts this branch's composed signal, and
@@ -1682,7 +1709,7 @@ export async function tryFallbackProviders({
1682
1709
  * abort. Rethrow the breaker's own reason instead. */
1683
1710
  if (
1684
1711
  config?.signal?.aborted === true &&
1685
- config.signal.reason instanceof StreamLimitExceededError
1712
+ (config.signal.reason instanceof StreamLimitExceededError || config.signal.reason instanceof PreparedSubagentError)
1686
1713
  ) {
1687
1714
  throw config.signal.reason;
1688
1715
  }
package/src/stream.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  requiresStreamLimitAccounting,
16
16
  StreamLimitExceededError,
17
17
  STREAM_LIMIT_EPOCH_KEY,
18
+ resolveGenerationKey,
18
19
  } from '@/llm/streamLimits';
19
20
  import {
20
21
  getStreamedToolCallSeal,
@@ -56,6 +57,7 @@ import {
56
57
  } from '@/utils/truncation';
57
58
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
58
59
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
60
+ import { PreparedSubagentError } from '@/tools/preparedSubagents';
59
61
  import { isReasoningContentBlock } from '@/messages/core';
60
62
  import { safeDispatchCustomEvent } from '@/utils/events';
61
63
  import { composeAbortSignals } from '@/utils/misc';
@@ -1319,6 +1321,65 @@ function startReadyStreamedEagerToolExecutions(args: {
1319
1321
  });
1320
1322
  }
1321
1323
 
1324
+ /** Shares provider sealing/parsing with event tools, but never relaxes their batch guard. */
1325
+ function startPreparedSubagents(
1326
+ graph: StandardGraph,
1327
+ agentContext: AgentContext | undefined,
1328
+ chunk: Partial<AIMessageChunk>,
1329
+ metadata?: Record<string, unknown>
1330
+ ): void {
1331
+ const attempt = resolveGenerationKey(metadata);
1332
+ if (
1333
+ (graph as Partial<StandardGraph>).canPrestartSubagents?.(agentContext) !==
1334
+ true ||
1335
+ graph.preparedSubagents.isOpen(attempt) !== true ||
1336
+ graph.hookRegistry?.hasResultAlteringHooks(
1337
+ graph.preparedSubagents.getConfig(attempt)?.configurable?.run_id
1338
+ ) === true
1339
+ ) {
1340
+ return;
1341
+ }
1342
+ let calls: ToolCall[];
1343
+ if (hasOnArrivalToolCallSeal(chunk) && (chunk.tool_calls?.length ?? 0) > 0) {
1344
+ calls = chunk.tool_calls!;
1345
+ } else {
1346
+ const seal = getStreamedToolCallSeal(chunk.response_metadata);
1347
+ const allowSequentialSeal =
1348
+ canPrestartSequentialStreamedToolChunks(agentContext) ||
1349
+ streamedToolCallAdapterAllowsSequentialSeal(chunk.response_metadata);
1350
+ if (!hasExplicitStreamedToolCallSeals(chunk) && !allowSequentialSeal) {
1351
+ return;
1352
+ }
1353
+ const stepKey = `prepared:${attempt}`;
1354
+ recordEagerToolCallChunks({
1355
+ graph,
1356
+ stepKey,
1357
+ toolCallChunks: chunk.tool_call_chunks ?? [],
1358
+ seal,
1359
+ });
1360
+ calls = getStreamedReadyToolCalls({
1361
+ graph,
1362
+ stepKey,
1363
+ toolCallChunks: chunk.tool_call_chunks,
1364
+ seal,
1365
+ allowSequentialSeal,
1366
+ });
1367
+ pruneEagerToolCallChunkStates({
1368
+ graph,
1369
+ stepKey,
1370
+ toolCallIds: new Set(calls.map((call) => call.id!)),
1371
+ });
1372
+ }
1373
+ for (const call of calls) {
1374
+ if (
1375
+ call.name === Constants.SUBAGENT &&
1376
+ !hasToolOutputReference(call.args)
1377
+ ) {
1378
+ graph.prestartSubagent(call, attempt, agentContext);
1379
+ }
1380
+ }
1381
+ }
1382
+
1322
1383
  export function getChunkContent({
1323
1384
  chunk,
1324
1385
  provider,
@@ -1597,7 +1658,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1597
1658
  if (
1598
1659
  eventBreaker != null &&
1599
1660
  eventBreaker.signal.aborted &&
1600
- eventBreaker.signal.reason instanceof StreamLimitExceededError
1661
+ (eventBreaker.signal.reason instanceof StreamLimitExceededError || eventBreaker.signal.reason instanceof PreparedSubagentError)
1601
1662
  ) {
1602
1663
  throw eventBreaker.signal.reason;
1603
1664
  }
@@ -1743,6 +1804,9 @@ export class ChatModelStreamHandler implements t.EventHandler {
1743
1804
  return;
1744
1805
  }
1745
1806
  throwIfRunBreakerTripped();
1807
+ if (hasOnArrivalToolCallSeal(chunk)) {
1808
+ startPreparedSubagents(graph, agentContext, chunk, metadata);
1809
+ }
1746
1810
  if (hasFinalToolCallSignal(chunk)) {
1747
1811
  startEagerToolExecutions({
1748
1812
  graph,
@@ -1839,6 +1903,11 @@ export class ChatModelStreamHandler implements t.EventHandler {
1839
1903
  }
1840
1904
  }
1841
1905
 
1906
+ if (!hasOnArrivalToolCallSeal(chunk) && !runScopeInvalidated()) {
1907
+ throwIfRunBreakerTripped();
1908
+ startPreparedSubagents(graph, agentContext, chunk, metadata);
1909
+ }
1910
+
1842
1911
  if (isEmptyContent) {
1843
1912
  return;
1844
1913
  }