@librechat/agents 3.2.60 → 3.2.62

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 (116) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +32 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +6 -0
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs +12 -0
  8. package/dist/cjs/hooks/index.cjs.map +1 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/langfuse.cjs +62 -1
  11. package/dist/cjs/langfuse.cjs.map +1 -1
  12. package/dist/cjs/llm/bedrock/index.cjs +266 -43
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/index.cjs +274 -7
  15. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
  17. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
  18. package/dist/cjs/llm/openrouter/index.cjs +5 -6
  19. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  20. package/dist/cjs/main.cjs +6 -1
  21. package/dist/cjs/messages/format.cjs +61 -0
  22. package/dist/cjs/messages/format.cjs.map +1 -1
  23. package/dist/cjs/messages/prune.cjs +31 -19
  24. package/dist/cjs/messages/prune.cjs.map +1 -1
  25. package/dist/cjs/stream.cjs +13 -3
  26. package/dist/cjs/stream.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolNode.cjs +75 -10
  28. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/utils/tokens.cjs +111 -0
  32. package/dist/cjs/utils/tokens.cjs.map +1 -1
  33. package/dist/esm/common/enum.mjs +2 -0
  34. package/dist/esm/common/enum.mjs.map +1 -1
  35. package/dist/esm/hooks/HookRegistry.mjs +32 -0
  36. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  37. package/dist/esm/hooks/executeHooks.mjs +6 -0
  38. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  39. package/dist/esm/hooks/index.mjs +12 -1
  40. package/dist/esm/hooks/index.mjs.map +1 -0
  41. package/dist/esm/hooks/types.mjs.map +1 -1
  42. package/dist/esm/langfuse.mjs +62 -1
  43. package/dist/esm/langfuse.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +266 -43
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/llm/openai/index.mjs +274 -7
  47. package/dist/esm/llm/openai/index.mjs.map +1 -1
  48. package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
  49. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
  50. package/dist/esm/llm/openrouter/index.mjs +5 -6
  51. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  52. package/dist/esm/main.mjs +3 -3
  53. package/dist/esm/messages/format.mjs +61 -0
  54. package/dist/esm/messages/format.mjs.map +1 -1
  55. package/dist/esm/messages/prune.mjs +31 -19
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/stream.mjs +13 -3
  58. package/dist/esm/stream.mjs.map +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +75 -10
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  63. package/dist/esm/utils/tokens.mjs +108 -1
  64. package/dist/esm/utils/tokens.mjs.map +1 -1
  65. package/dist/types/common/enum.d.ts +3 -1
  66. package/dist/types/hooks/HookRegistry.d.ts +10 -0
  67. package/dist/types/hooks/index.d.ts +7 -0
  68. package/dist/types/hooks/types.d.ts +18 -1
  69. package/dist/types/llm/bedrock/index.d.ts +5 -0
  70. package/dist/types/llm/openai/index.d.ts +19 -0
  71. package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
  72. package/dist/types/messages/prune.d.ts +4 -3
  73. package/dist/types/tools/ToolNode.d.ts +1 -0
  74. package/dist/types/types/llm.d.ts +12 -2
  75. package/dist/types/types/tools.d.ts +1 -1
  76. package/dist/types/utils/tokens.d.ts +30 -0
  77. package/package.json +9 -9
  78. package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
  79. package/src/common/enum.ts +2 -0
  80. package/src/hooks/HookRegistry.ts +45 -0
  81. package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
  82. package/src/hooks/__tests__/executeHooks.test.ts +85 -2
  83. package/src/hooks/executeHooks.ts +15 -0
  84. package/src/hooks/index.ts +7 -0
  85. package/src/hooks/types.ts +18 -1
  86. package/src/langfuse.ts +134 -1
  87. package/src/llm/bedrock/index.ts +434 -83
  88. package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
  89. package/src/llm/custom-chat-models.smoke.test.ts +7 -0
  90. package/src/llm/openai/index.ts +604 -6
  91. package/src/llm/openai/managedRequests.test.ts +182 -0
  92. package/src/llm/openai/streamMetadata.spec.ts +86 -0
  93. package/src/llm/openai/streamMetadata.ts +95 -0
  94. package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
  95. package/src/llm/openrouter/index.ts +9 -5
  96. package/src/messages/format.ts +96 -3
  97. package/src/messages/formatAgentMessages.steer.test.ts +326 -0
  98. package/src/messages/labelContentByAgent.test.ts +75 -0
  99. package/src/messages/prune.ts +56 -30
  100. package/src/specs/anthropic.simple.test.ts +4 -2
  101. package/src/specs/cache.simple.test.ts +17 -4
  102. package/src/specs/langfuse-callbacks.test.ts +61 -0
  103. package/src/specs/openai.simple.test.ts +4 -2
  104. package/src/specs/spec.utils.ts +12 -0
  105. package/src/specs/summarization.test.ts +9 -13
  106. package/src/specs/token-accounting-pipeline.test.ts +130 -4
  107. package/src/specs/tokens.test.ts +214 -0
  108. package/src/stream.ts +22 -3
  109. package/src/tools/ToolNode.ts +107 -12
  110. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
  111. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
  112. package/src/tools/__tests__/hitl.test.ts +112 -0
  113. package/src/tools/subagent/SubagentExecutor.ts +2 -0
  114. package/src/types/llm.ts +12 -2
  115. package/src/types/tools.ts +1 -1
  116. package/src/utils/tokens.ts +181 -3
@@ -822,10 +822,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
822
822
  private recordEventToolPlanningTurn(
823
823
  toolName: string,
824
824
  turn: number,
825
- callId?: string
825
+ callId?: string,
826
+ runId?: string
826
827
  ): void {
827
828
  this.recordToolUsageTurn(toolName, turn, callId);
828
- if (this.canConsumeEagerEventExecution()) {
829
+ if (this.canConsumeEagerEventExecution(runId)) {
829
830
  this.eagerEventToolUsageCount?.set(
830
831
  toolName,
831
832
  Math.max(this.eagerEventToolUsageCount.get(toolName) ?? 0, turn + 1)
@@ -2091,6 +2092,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2091
2092
  const postToolBatchEntryByCallId = new Map<string, PostToolBatchEntry>();
2092
2093
  const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
2093
2094
  additionalContexts: [] as string[],
2095
+ injectedMessages: [] as t.InjectedMessage[],
2094
2096
  errors: [] as string[],
2095
2097
  });
2096
2098
 
@@ -2101,6 +2103,32 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2101
2103
  * to defensively `?.` it across every reference inside.
2102
2104
  */
2103
2105
  const hookRegistry = this.hookRegistry;
2106
+ /**
2107
+ * Pull each call's prestarted eager record BEFORE awaiting the hooks:
2108
+ * an async deny must never race the eager host promise into emitting a
2109
+ * successful completion (`dispatchEagerToolCompletions`' map-identity
2110
+ * check skips deleted records). Allowed entries get their record
2111
+ * restored in the decision loop below so consumption still works;
2112
+ * denied entries stay deleted. Ask/interrupt configs never have eager
2113
+ * records (HITL disables the reservation gate).
2114
+ */
2115
+ const preemptedEagerRecords = new Map<
2116
+ string,
2117
+ t.EagerEventToolExecution
2118
+ >();
2119
+ if (this.eagerEventToolExecutions != null) {
2120
+ for (const entry of preToolCalls) {
2121
+ const callId = entry.call.id;
2122
+ if (callId == null) {
2123
+ continue;
2124
+ }
2125
+ const record = this.eagerEventToolExecutions.get(callId);
2126
+ if (record != null) {
2127
+ preemptedEagerRecords.set(callId, record);
2128
+ this.eagerEventToolExecutions.delete(callId);
2129
+ }
2130
+ }
2131
+ }
2104
2132
  const preResults = await Promise.all(
2105
2133
  preToolCalls.map((entry) =>
2106
2134
  executeHooks({
@@ -2183,6 +2211,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2183
2211
  contentString,
2184
2212
  reason,
2185
2213
  });
2214
+ /**
2215
+ * A prestarted eager execution for a now-blocked call must neither be
2216
+ * consumed nor emit its completion — the run reports this call as
2217
+ * blocked. Deleting the record makes `dispatchEagerToolCompletions`'
2218
+ * map-identity check skip the pending emission (same pattern as the
2219
+ * rejected-results cleanup below).
2220
+ */
2221
+ this.eagerEventToolExecutions?.delete(entry.call.id!);
2186
2222
  };
2187
2223
 
2188
2224
  const flushDeferredBlockedSideEffects = async (): Promise<void> => {
@@ -2312,6 +2348,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2312
2348
  if (hookResult.updatedInput != null) {
2313
2349
  applyInputOverride(entry, hookResult.updatedInput);
2314
2350
  }
2351
+ if (entry.call.id != null) {
2352
+ const preempted = preemptedEagerRecords.get(entry.call.id);
2353
+ if (preempted != null) {
2354
+ this.eagerEventToolExecutions?.set(entry.call.id, preempted);
2355
+ }
2356
+ }
2315
2357
  approvedEntries.push(entry);
2316
2358
  }
2317
2359
 
@@ -2592,7 +2634,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2592
2634
  usageCount: this.toolUsageCount,
2593
2635
  invalidArgsBehavior: 'error-result',
2594
2636
  recordTurn: (toolName, reservedTurn, callId) => {
2595
- this.recordEventToolPlanningTurn(toolName, reservedTurn, callId);
2637
+ this.recordEventToolPlanningTurn(
2638
+ toolName,
2639
+ reservedTurn,
2640
+ callId,
2641
+ runId
2642
+ );
2596
2643
  },
2597
2644
  });
2598
2645
  if (plan == null) {
@@ -2637,7 +2684,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2637
2684
  * released if the dispatch fails, letting the batch path re-emit.
2638
2685
  */
2639
2686
  const canEmitEarlyCompletions =
2640
- this.hookRegistry == null && this.humanInTheLoop?.enabled !== true;
2687
+ this.hookRegistry?.hasResultAlteringHooks(runId) !== true &&
2688
+ this.humanInTheLoop?.enabled !== true;
2689
+ /**
2690
+ * Snapshot the post-hook gates at the same instant as the early-emission
2691
+ * gate: a result-altering hook registered while this batch is in flight
2692
+ * applies from the NEXT batch. Evaluating these after results settle
2693
+ * would let a late hook rewrite the ToolMessage AFTER an early
2694
+ * completion already emitted the pre-hook output — two views of the
2695
+ * same call.
2696
+ */
2697
+ const hasPostHook =
2698
+ this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
2699
+ const hasFailureHook =
2700
+ this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
2641
2701
  const earlyCompletionDispatchedIds = new Set<string>();
2642
2702
  const earlyCompletionDispatches: Array<Promise<void>> = [];
2643
2703
  const dispatchRequestById = new Map(
@@ -2753,11 +2813,6 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2753
2813
 
2754
2814
  this.storeCodeSessionFromResults(results, requestMap);
2755
2815
 
2756
- const hasPostHook =
2757
- this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
2758
- const hasFailureHook =
2759
- this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
2760
-
2761
2816
  for (const result of results) {
2762
2817
  if (result.injectedMessages && result.injectedMessages.length > 0) {
2763
2818
  try {
@@ -2895,6 +2950,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2895
2950
  this.maxToolResultChars
2896
2951
  );
2897
2952
  finalToolOutput = hookResult.updatedOutput;
2953
+ /**
2954
+ * The hook ACTUALLY rewrote this output: any completion the
2955
+ * stream already emitted for the consumed eager result showed
2956
+ * the raw content, so un-mark it and let the dispatch below
2957
+ * re-emit the corrected version. Hooks that merely observe or
2958
+ * add context leave the original emission final — no
2959
+ * duplicates.
2960
+ */
2961
+ eagerCompletionDispatchedIds.delete(result.toolCallId);
2898
2962
  }
2899
2963
  }
2900
2964
 
@@ -2971,11 +3035,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2971
3035
  return { toolMessages, injected };
2972
3036
  }
2973
3037
 
2974
- private canConsumeEagerEventExecution(): boolean {
3038
+ /** Run-scoped so another run's session hooks can't flip this run's gate. */
3039
+ private canConsumeEagerEventExecution(runId?: string): boolean {
2975
3040
  return (
2976
3041
  this.eventDrivenMode &&
2977
3042
  this.eagerEventToolExecution?.enabled === true &&
2978
- this.hookRegistry == null &&
3043
+ this.hookRegistry?.hasResultAlteringHooks(runId) !== true &&
2979
3044
  this.humanInTheLoop?.enabled !== true
2980
3045
  );
2981
3046
  }
@@ -2983,7 +3048,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2983
3048
  private takeMatchingEagerEventExecution(
2984
3049
  request: t.ToolCallRequest
2985
3050
  ): t.EagerEventToolExecution | undefined {
2986
- if (!this.canConsumeEagerEventExecution()) {
3051
+ // Static enablement only: a stored record means the reservation-time
3052
+ // gates passed and the host already dispatched the execution. Re-checking
3053
+ // the dynamic hook gate here could decline consumption after a mid-run
3054
+ // registration and send the same call through normal dispatch — executing
3055
+ // the tool twice. PostToolUse hooks still process consumed results below.
3056
+ if (
3057
+ !this.eventDrivenMode ||
3058
+ this.eagerEventToolExecution?.enabled !== true
3059
+ ) {
2987
3060
  return undefined;
2988
3061
  }
2989
3062
 
@@ -3098,6 +3171,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3098
3171
  orderedBatchEntries.push(entry);
3099
3172
  }
3100
3173
  }
3174
+ const hookInjectedMessages: t.InjectedMessage[] = [];
3101
3175
  if (
3102
3176
  this.hookRegistry?.hasHookFor('PostToolBatch', runId) === true &&
3103
3177
  orderedBatchEntries.length > 0
@@ -3118,6 +3192,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3118
3192
  for (const ctx of batchHookResult.additionalContexts) {
3119
3193
  batchAdditionalContexts.push(ctx);
3120
3194
  }
3195
+ for (const msg of batchHookResult.injectedMessages) {
3196
+ hookInjectedMessages.push(msg);
3197
+ }
3121
3198
  }
3122
3199
  }
3123
3200
 
@@ -3137,6 +3214,24 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3137
3214
  })
3138
3215
  );
3139
3216
  }
3217
+
3218
+ /**
3219
+ * Hook-returned `injectedMessages` land AFTER the consolidated context
3220
+ * message: one converted `HumanMessage` per entry (role/source kept in
3221
+ * `additional_kwargs`), preserving per-message identity so verbatim
3222
+ * user speech (e.g. steering) sits closest to the next model call.
3223
+ */
3224
+ if (hookInjectedMessages.length > 0) {
3225
+ try {
3226
+ injected.push(...this.convertInjectedMessages(hookInjectedMessages));
3227
+ } catch (e) {
3228
+ // eslint-disable-next-line no-console
3229
+ console.warn(
3230
+ '[ToolNode] Failed to convert PostToolBatch injectedMessages:',
3231
+ e instanceof Error ? e.message : e
3232
+ );
3233
+ }
3234
+ }
3140
3235
  }
3141
3236
 
3142
3237
  private async dispatchStepCompleted(