@librechat/agents 3.2.61 → 3.2.63

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 (125) 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/graphs/Graph.cjs +6 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/HookRegistry.cjs +32 -0
  6. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  7. package/dist/cjs/hooks/executeHooks.cjs +6 -0
  8. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  9. package/dist/cjs/hooks/index.cjs +12 -0
  10. package/dist/cjs/hooks/index.cjs.map +1 -0
  11. package/dist/cjs/hooks/types.cjs.map +1 -1
  12. package/dist/cjs/langfuse.cjs +62 -1
  13. package/dist/cjs/langfuse.cjs.map +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs +266 -43
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/index.cjs +274 -7
  17. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
  19. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
  20. package/dist/cjs/llm/openrouter/index.cjs +5 -6
  21. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  22. package/dist/cjs/main.cjs +6 -1
  23. package/dist/cjs/messages/format.cjs +61 -0
  24. package/dist/cjs/messages/format.cjs.map +1 -1
  25. package/dist/cjs/messages/prune.cjs +31 -19
  26. package/dist/cjs/messages/prune.cjs.map +1 -1
  27. package/dist/cjs/stream.cjs +13 -3
  28. package/dist/cjs/stream.cjs.map +1 -1
  29. package/dist/cjs/tools/ToolNode.cjs +76 -11
  30. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +2 -0
  32. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  33. package/dist/cjs/utils/tokens.cjs +111 -0
  34. package/dist/cjs/utils/tokens.cjs.map +1 -1
  35. package/dist/esm/common/enum.mjs +2 -0
  36. package/dist/esm/common/enum.mjs.map +1 -1
  37. package/dist/esm/graphs/Graph.mjs +6 -2
  38. package/dist/esm/graphs/Graph.mjs.map +1 -1
  39. package/dist/esm/hooks/HookRegistry.mjs +32 -0
  40. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  41. package/dist/esm/hooks/executeHooks.mjs +6 -0
  42. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  43. package/dist/esm/hooks/index.mjs +12 -1
  44. package/dist/esm/hooks/index.mjs.map +1 -0
  45. package/dist/esm/hooks/types.mjs.map +1 -1
  46. package/dist/esm/langfuse.mjs +62 -1
  47. package/dist/esm/langfuse.mjs.map +1 -1
  48. package/dist/esm/llm/bedrock/index.mjs +266 -43
  49. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  50. package/dist/esm/llm/openai/index.mjs +274 -7
  51. package/dist/esm/llm/openai/index.mjs.map +1 -1
  52. package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
  53. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
  54. package/dist/esm/llm/openrouter/index.mjs +5 -6
  55. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  56. package/dist/esm/main.mjs +3 -3
  57. package/dist/esm/messages/format.mjs +61 -0
  58. package/dist/esm/messages/format.mjs.map +1 -1
  59. package/dist/esm/messages/prune.mjs +31 -19
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +13 -3
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/tools/ToolNode.mjs +76 -11
  64. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  65. package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -0
  66. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  67. package/dist/esm/utils/tokens.mjs +108 -1
  68. package/dist/esm/utils/tokens.mjs.map +1 -1
  69. package/dist/types/common/enum.d.ts +3 -1
  70. package/dist/types/graphs/Graph.d.ts +3 -1
  71. package/dist/types/hooks/HookRegistry.d.ts +10 -0
  72. package/dist/types/hooks/index.d.ts +7 -0
  73. package/dist/types/hooks/types.d.ts +18 -1
  74. package/dist/types/llm/bedrock/index.d.ts +5 -0
  75. package/dist/types/llm/openai/index.d.ts +19 -0
  76. package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
  77. package/dist/types/messages/prune.d.ts +4 -3
  78. package/dist/types/tools/ToolNode.d.ts +1 -0
  79. package/dist/types/types/graph.d.ts +10 -0
  80. package/dist/types/types/llm.d.ts +12 -2
  81. package/dist/types/types/tools.d.ts +1 -1
  82. package/dist/types/utils/tokens.d.ts +30 -0
  83. package/package.json +6 -6
  84. package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
  85. package/src/common/enum.ts +2 -0
  86. package/src/graphs/Graph.ts +12 -4
  87. package/src/hooks/HookRegistry.ts +45 -0
  88. package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
  89. package/src/hooks/__tests__/executeHooks.test.ts +85 -2
  90. package/src/hooks/executeHooks.ts +15 -0
  91. package/src/hooks/index.ts +7 -0
  92. package/src/hooks/types.ts +18 -1
  93. package/src/langfuse.ts +134 -1
  94. package/src/llm/bedrock/index.ts +434 -83
  95. package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
  96. package/src/llm/custom-chat-models.smoke.test.ts +7 -0
  97. package/src/llm/openai/index.ts +604 -6
  98. package/src/llm/openai/managedRequests.test.ts +182 -0
  99. package/src/llm/openai/streamMetadata.spec.ts +86 -0
  100. package/src/llm/openai/streamMetadata.ts +95 -0
  101. package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
  102. package/src/llm/openrouter/index.ts +9 -5
  103. package/src/messages/format.ts +96 -3
  104. package/src/messages/formatAgentMessages.steer.test.ts +326 -0
  105. package/src/messages/labelContentByAgent.test.ts +75 -0
  106. package/src/messages/prune.ts +56 -30
  107. package/src/specs/anthropic.simple.test.ts +4 -2
  108. package/src/specs/cache.simple.test.ts +17 -4
  109. package/src/specs/langfuse-callbacks.test.ts +61 -0
  110. package/src/specs/openai.simple.test.ts +4 -2
  111. package/src/specs/spec.utils.ts +12 -0
  112. package/src/specs/summarization.test.ts +9 -13
  113. package/src/specs/token-accounting-pipeline.test.ts +130 -4
  114. package/src/specs/tokens.test.ts +214 -0
  115. package/src/stream.ts +22 -3
  116. package/src/tools/ToolNode.ts +112 -13
  117. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
  118. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
  119. package/src/tools/__tests__/hitl.test.ts +112 -0
  120. package/src/tools/__tests__/subagentHooks.test.ts +124 -0
  121. package/src/tools/subagent/SubagentExecutor.ts +3 -0
  122. package/src/types/graph.ts +10 -0
  123. package/src/types/llm.ts +12 -2
  124. package/src/types/tools.ts +1 -1
  125. package/src/utils/tokens.ts +181 -3
@@ -108,6 +108,16 @@ export declare class HookRegistry {
108
108
  * halt state. No-op when no signal exists for that session.
109
109
  */
110
110
  clearHaltSignal(sessionId: string): void;
111
+ /**
112
+ * True when any registered hook can alter a tool result before or after
113
+ * execution (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`). Eager
114
+ * tool execution and early completion emission gate on this instead of
115
+ * registry presence, so observation-only registries (e.g. a
116
+ * `PostToolBatch` steering drain) keep those fast paths. With
117
+ * `sessionId`, checks global + that session; without it, conservatively
118
+ * scans every session bucket.
119
+ */
120
+ hasResultAlteringHooks(sessionId?: string): boolean;
111
121
  /** True if at least one matcher exists for `event` (global + session). */
112
122
  hasHookFor(event: HookEvent, sessionId?: string): boolean;
113
123
  private ensureSessionBucket;
@@ -1,6 +1,13 @@
1
1
  export { HookRegistry } from './HookRegistry';
2
2
  export type { HookHaltSignal } from './HookRegistry';
3
3
  export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
4
+ /**
5
+ * Feature probe for hosts: hook outputs support `injectedMessages`
6
+ * (per-message graph-state injection at the `PostToolBatch` boundary).
7
+ * Hosts must gate drain-style hooks on this so a queued message can never
8
+ * be consumed by an SDK version that would silently drop it.
9
+ */
10
+ export declare const HOOK_INJECTED_MESSAGES_CAPABLE = true;
4
11
  export { matchesQuery, hasNestedQuantifier, MAX_PATTERN_LENGTH, MAX_CACHE_SIZE, } from './matchers';
5
12
  export { createToolPolicyHook } from './createToolPolicyHook';
6
13
  export type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';
@@ -1,4 +1,5 @@
1
1
  import type { BaseMessage } from '@langchain/core/messages';
2
+ import type { InjectedMessage } from '@/types/tools';
2
3
  /**
3
4
  * Closed set of hook lifecycle events supported by the hooks system.
4
5
  *
@@ -107,7 +108,9 @@ export interface PostToolBatchEntry {
107
108
  *
108
109
  * Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks
109
110
  * for the same batch have completed, BEFORE the next model call. Pass an
110
- * `additionalContext` to inject context for that next model turn.
111
+ * `additionalContext` to inject context for that next model turn, or
112
+ * `injectedMessages` to inject standalone per-message user speech (e.g.
113
+ * mid-run steering) that must not be consolidated with hook context.
111
114
  */
112
115
  export interface PostToolBatchHookInput extends BaseHookInput {
113
116
  hook_event_name: 'PostToolBatch';
@@ -191,6 +194,18 @@ export type HookInputByEvent = {
191
194
  export interface BaseHookOutput {
192
195
  /** Context string to inject into the conversation. Accumulated across hooks. */
193
196
  additionalContext?: string;
197
+ /**
198
+ * Messages to inject into graph state, one `HumanMessage` per entry
199
+ * (converted via `ToolNode.convertInjectedMessages`, which preserves
200
+ * `role`/`source`/`isMeta` in `additional_kwargs`). Unlike
201
+ * `additionalContext` — which is consolidated across hooks into a single
202
+ * system-flavored message — each entry keeps its own identity and role,
203
+ * making this the channel for injecting verbatim user speech (e.g. a
204
+ * mid-run steering message). Accumulated across hooks in registration
205
+ * order. Currently consumed only at the `PostToolBatch` dispatch site;
206
+ * other events ignore the field.
207
+ */
208
+ injectedMessages?: InjectedMessage[];
194
209
  /** True to prevent the next model turn. Any hook can set this. */
195
210
  preventContinuation?: boolean;
196
211
  /** Reason reported alongside `preventContinuation`. */
@@ -418,6 +433,8 @@ export interface AggregatedHookResult {
418
433
  updatedOutput?: unknown;
419
434
  /** Accumulated `additionalContext` strings from every hook, in order. */
420
435
  additionalContexts: string[];
436
+ /** Accumulated `injectedMessages` from every hook, in registration order. */
437
+ injectedMessages: InjectedMessage[];
421
438
  /** True if any hook returned `preventContinuation`. */
422
439
  preventContinuation?: boolean;
423
440
  /**
@@ -52,6 +52,10 @@ export interface CustomChatBedrockConverseInput extends ChatBedrockConverseInput
52
52
  * on; use `'5m'` for any model that rejects it.
53
53
  */
54
54
  promptCacheTtl?: PromptCacheTtl;
55
+ /**
56
+ * Minimum delay in milliseconds between visible streamed content deltas.
57
+ */
58
+ _lc_stream_delay?: number;
55
59
  /**
56
60
  * Guardrail configuration for Converse and ConverseStream invocations.
57
61
  * `streamProcessingMode` is only used by ConverseStream.
@@ -89,6 +93,7 @@ export interface CustomChatBedrockConverseCallOptions {
89
93
  guardrailConfig?: CustomGuardrailConfiguration;
90
94
  }
91
95
  export declare class CustomChatBedrockConverse extends ChatBedrockConverse {
96
+ _lc_stream_delay: number;
92
97
  /**
93
98
  * Whether to insert Bedrock prompt cache checkpoints when available.
94
99
  */
@@ -20,10 +20,23 @@ type LibreChatOpenAIFields = t.ChatOpenAIFields & {
20
20
  includeReasoningContent?: boolean;
21
21
  includeReasoningDetails?: boolean;
22
22
  convertReasoningDetailsToContent?: boolean;
23
+ promptCacheExplicit?: boolean;
24
+ safety_identifier?: string;
23
25
  };
24
26
  type LibreChatAzureOpenAIFields = t.AzureOpenAIInput & {
25
27
  _lc_stream_delay?: number;
28
+ promptCacheExplicit?: boolean;
29
+ safety_identifier?: string;
26
30
  };
31
+ /** @internal */
32
+ export declare function addChatCacheBreakpoints(messages: OpenAIClient.Chat.Completions.ChatCompletionMessageParam[]): OpenAIClient.Chat.Completions.ChatCompletionMessageParam[];
33
+ /** @internal */
34
+ export declare function addResponseCacheBreakpoints(input: OpenAIClient.Responses.ResponseCreateParams['input']): OpenAIClient.Responses.ResponseCreateParams['input'];
35
+ /** @internal */
36
+ export declare function shouldIncludeEncryptedReasoning(model: string, params: {
37
+ store?: boolean | null;
38
+ reasoning?: unknown;
39
+ }): boolean;
27
40
  export declare function emitStreamChunkCallback(chunk: ChatGenerationChunk, runManager?: CallbackManagerForLLMRun): Promise<void>;
28
41
  /**
29
42
  * Formats a tool in either OpenAI format, or LangChain structured tool format
@@ -64,6 +77,12 @@ export declare class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptio
64
77
  getReasoningParams(options?: this['ParsedCallOptions']): OpenAIClient.Reasoning | undefined;
65
78
  protected _getReasoningParams(options?: this['ParsedCallOptions']): OpenAIClient.Reasoning | undefined;
66
79
  _streamResponseChunks(messages: BaseMessage[], options: this['ParsedCallOptions'], runManager?: CallbackManagerForLLMRun): AsyncGenerator<ChatGenerationChunk>;
80
+ /**
81
+ * Raw variant that skips scalar-metadata de-duplication. Used by subclasses
82
+ * (e.g. `ChatOpenRouter`) that read `finish_reason` as a control signal and
83
+ * must de-duplicate only after their own finalization.
84
+ */
85
+ protected _streamRawResponseChunks(messages: BaseMessage[], options: this['ParsedCallOptions'], runManager?: CallbackManagerForLLMRun): AsyncGenerator<ChatGenerationChunk>;
67
86
  }
68
87
  export declare class AzureChatOpenAI extends OriginalAzureChatOpenAI {
69
88
  _lc_stream_delay?: number;
@@ -0,0 +1,16 @@
1
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
2
+ /** Per-completion-index set of scalar fields already emitted this stream. */
3
+ export type SeenScalarMetadata = Map<number, Set<string>>;
4
+ /**
5
+ * Strip scalar `response_metadata`/`generationInfo` fields that have already
6
+ * appeared on an earlier chunk of the same completion (tracked in `seen`),
7
+ * keeping the first occurrence so the aggregated message still carries the
8
+ * value once. Scoped per completion index so multi-choice (`n > 1`) responses,
9
+ * which `_generate` aggregates separately, keep each choice's own metadata.
10
+ *
11
+ * Deletes from a shallow clone rather than in place: providers such as
12
+ * `ChatDeepSeek` split one raw chunk into several synthetic pieces that share
13
+ * the same `response_metadata`/`generationInfo` objects, so mutating in place
14
+ * would strip fields from sibling chunks that were already emitted.
15
+ */
16
+ export declare function dropRepeatedScalarMetadata(chunk: ChatGenerationChunk, seen: SeenScalarMetadata): void;
@@ -61,9 +61,10 @@ export type PruneMessagesParams = {
61
61
  usageMetadata?: Partial<UsageMetadata>;
62
62
  startType?: ReturnType<BaseMessage['getType']>;
63
63
  /**
64
- * Usage from the most recent LLM call only (not accumulated).
65
- * When provided, calibration uses this instead of usageMetadata
66
- * to avoid inflated ratios from N×cacheRead accumulation.
64
+ * Fallback usage from the most recent LLM call only (not accumulated).
65
+ * Calibration prefers the provider's raw `usageMetadata.input_tokens`
66
+ * when available, because cache detail fields may use non-window
67
+ * accounting units.
67
68
  */
68
69
  lastCallUsage?: {
69
70
  totalTokens: number;
@@ -369,6 +369,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
369
369
  * ToolMessages (appended AFTER to respect provider ordering).
370
370
  */
371
371
  private dispatchToolEvents;
372
+ /** Run-scoped so another run's session hooks can't flip this run's gate. */
372
373
  private canConsumeEagerEventExecution;
373
374
  private takeMatchingEagerEventExecution;
374
375
  private resolveEagerEventExecution;
@@ -233,6 +233,16 @@ export type StandardGraphInput = {
233
233
  * they already flow through the registry's `CHAT_MODEL_END` handler.
234
234
  */
235
235
  subagentUsageSink?: SubagentUsageSink;
236
+ /**
237
+ * True when this graph IS a subagent child run (set by `SubagentExecutor`
238
+ * when it constructs the child graph). Drives the hook-input `agentId`
239
+ * subagent-scope marker: hook dispatches from this graph's tool nodes
240
+ * carry `agentId` so run-scoped host hooks — which fire for child scopes
241
+ * too, because children inherit the parent's `run_id` — can tell child
242
+ * scope from the top level. Top-level graphs leave this unset and their
243
+ * hook inputs carry only `executingAgentId`.
244
+ */
245
+ subagentScope?: boolean;
236
246
  };
237
247
  export type GraphEdge = {
238
248
  /** Agent ID, use a list for multiple sources */
@@ -28,7 +28,7 @@ export type AzureClientOptions = Partial<OpenAIChatInput> & Partial<AzureOpenAII
28
28
  deploymentName?: string;
29
29
  } & BaseChatModelParams & {
30
30
  configuration?: OAIClientOptions;
31
- };
31
+ } & ManagedRequestOptions;
32
32
  /**
33
33
  * Controls whether Claude's reasoning content is returned in adaptive
34
34
  * thinking responses. Added for Claude Opus 4.7, which omits thinking by
@@ -52,7 +52,13 @@ export type GoogleThinkingConfig = {
52
52
  includeThoughts?: boolean;
53
53
  thinkingLevel?: 'THINKING_LEVEL_UNSPECIFIED' | 'LOW' | 'MEDIUM' | 'HIGH';
54
54
  };
55
- export type OpenAIClientOptions = ChatOpenAIFields;
55
+ /** GPT-5.6 managed-request passthrough fields, shared by the OpenAI and
56
+ * Azure wrappers that both read them. */
57
+ export type ManagedRequestOptions = {
58
+ promptCacheExplicit?: boolean;
59
+ safety_identifier?: string;
60
+ };
61
+ export type OpenAIClientOptions = ChatOpenAIFields & ManagedRequestOptions;
56
62
  export type AnthropicClientOptions = Omit<AnthropicInput, 'thinking'> & {
57
63
  thinking?: ThinkingConfig;
58
64
  promptCache?: boolean;
@@ -78,6 +84,10 @@ export type BedrockAnthropicInput = ChatBedrockConverseInput & {
78
84
  * server-side, so the default is safe to leave on.
79
85
  */
80
86
  promptCacheTtl?: PromptCacheTtl;
87
+ /**
88
+ * Minimum delay in milliseconds between visible streamed content deltas.
89
+ */
90
+ _lc_stream_delay?: number;
81
91
  };
82
92
  export type BedrockConverseClientOptions = BedrockAnthropicInput;
83
93
  export type BedrockAnthropicClientOptions = BedrockAnthropicInput;
@@ -482,7 +482,7 @@ export type InjectedMessage = {
482
482
  /** When true, the message is framework-internal: not shown in UI, not counted as a user turn */
483
483
  isMeta?: boolean;
484
484
  /** Origin tag for downstream consumers (UI, pruner, compaction) */
485
- source?: 'skill' | 'hook' | 'system';
485
+ source?: 'skill' | 'hook' | 'system' | 'steer';
486
486
  /** Only set when source is 'skill', for compaction preservation */
487
487
  skillName?: string;
488
488
  };
@@ -1,5 +1,7 @@
1
1
  import type { BaseMessage } from '@langchain/core/messages';
2
2
  export type EncodingName = 'o200k_base' | 'claude';
3
+ /** Safety margin for image and document token estimates (5% overestimate). */
4
+ export declare const IMAGE_TOKEN_SAFETY_MARGIN = 1.05;
3
5
  /**
4
6
  * Extracts image dimensions from the first bytes of a base64-encoded
5
7
  * PNG, JPEG, GIF, or WebP without decoding the full image.
@@ -13,6 +15,34 @@ export declare function extractImageDimensions(base64Data: string): {
13
15
  export declare function estimateAnthropicImageTokens(width: number, height: number): number;
14
16
  /** Estimates image token cost for OpenAI (high detail). */
15
17
  export declare function estimateOpenAIImageTokens(width: number, height: number, detail?: string): number;
18
+ /**
19
+ * Estimates token cost for an image content block.
20
+ * Extracts dimensions from base64 header when available.
21
+ * Falls back to Anthropic minimum (1024) when dimensions can't be determined.
22
+ */
23
+ export declare function estimateImageBlockTokens(block: Record<string, unknown>, encoding: EncodingName): number;
24
+ /**
25
+ * Estimates token cost for a document/file content block.
26
+ * Handles both LangChain standard format (`type: 'file'` with `source_type`)
27
+ * and Anthropic format (`type: 'document'` with `source`).
28
+ *
29
+ * - Plain text: tokenized directly via `getTokenCount`.
30
+ * - Base64 PDF: page count estimated from base64 length × per-page cost.
31
+ * - URL reference: conservative flat estimate.
32
+ */
33
+ export declare function estimateDocumentBlockTokens(block: Record<string, unknown>, encoding: EncodingName, getTokenCount: (text: string) => number): number;
34
+ /**
35
+ * Estimates token cost for a timed-media block (video/audio). Handles Google
36
+ * `{ type: 'media', mimeType, data|url|fileUri }`, OpenRouter
37
+ * `{ type: 'video_url' }` / `{ type: 'input_audio' }`, and standard
38
+ * `{ type: 'video' }` / `{ type: 'audio' }` blocks (payload as `data` base64 or
39
+ * `Uint8Array`, base64 `url`, or `fileId`). Duration is inferred from encoded
40
+ * size (providers price by duration, which the block does not carry) at Gemini's
41
+ * rates; a payload with no size (bare URL / file id) falls back to a ~30s
42
+ * estimate. Returns 0 for non-timed media (e.g. an image/document `media` block)
43
+ * so it is never mispriced as video.
44
+ */
45
+ export declare function estimateTimedMediaBlockTokens(block: Record<string, unknown>): number;
16
46
  export declare function encodingForModel(model: string): EncodingName;
17
47
  export declare function getTokenCountForMessage(message: BaseMessage, getTokenCount: (text: string) => number, encoding?: EncodingName): number;
18
48
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.61",
3
+ "version": "3.2.63",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -114,7 +114,7 @@
114
114
  "scripts": {
115
115
  "prepare": "node husky-setup.js",
116
116
  "prepublishOnly": "npm run build",
117
- "build": "rm -rf ./dist && tsdown && tsc -p tsconfig.build.json",
117
+ "build": "tsdown && tsc -p tsconfig.build.json",
118
118
  "build:dev": "tsdown",
119
119
  "sort-imports": "node scripts/sort-imports.ts",
120
120
  "sort-imports:check": "node scripts/sort-imports.ts --check",
@@ -200,7 +200,7 @@
200
200
  "format": "prettier --write ."
201
201
  },
202
202
  "overrides": {
203
- "@langchain/openai": "1.5.3",
203
+ "@langchain/openai": "1.5.5",
204
204
  "@browserbasehq/stagehand": {
205
205
  "openai": "$openai"
206
206
  },
@@ -216,7 +216,7 @@
216
216
  "@aws-sdk/client-bedrock-runtime": "^3.1075.0",
217
217
  "@langchain/anthropic": "^1.5.1",
218
218
  "@langchain/aws": "^1.4.2",
219
- "@langchain/core": "^1.2.1",
219
+ "@langchain/core": "^1.2.2",
220
220
  "@langchain/deepseek": "^1.1.3",
221
221
  "@langchain/google-common": "2.2.0",
222
222
  "@langchain/google-gauth": "2.2.0",
@@ -224,7 +224,7 @@
224
224
  "@langchain/google-vertexai": "2.2.0",
225
225
  "@langchain/langgraph": "^1.4.6",
226
226
  "@langchain/mistralai": "^1.2.0",
227
- "@langchain/openai": "1.5.3",
227
+ "@langchain/openai": "1.5.5",
228
228
  "@langchain/textsplitters": "^1.0.1",
229
229
  "@langchain/xai": "^1.4.3",
230
230
  "@langfuse/langchain": "^5.4.1",
@@ -243,7 +243,7 @@
243
243
  "mathjs": "^15.2.0",
244
244
  "nanoid": "^3.3.7",
245
245
  "okapibm25": "^1.4.1",
246
- "openai": "^6.35.0",
246
+ "openai": "^6.46.0",
247
247
  "uuid": "^11.1.1"
248
248
  },
249
249
  "peerDependencies": {
@@ -21,6 +21,13 @@ import {
21
21
  import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
22
22
  import { HandlerRegistry } from '@/events';
23
23
  import * as events from '@/utils/events';
24
+ import { HookRegistry } from '@/hooks';
25
+
26
+ function createResultAlteringRegistry(): HookRegistry {
27
+ const registry = new HookRegistry();
28
+ registry.register('PreToolUse', { hooks: [async () => ({})] });
29
+ return registry;
30
+ }
24
31
 
25
32
  function createGraph(overrides: Partial<StandardGraph> = {}): StandardGraph {
26
33
  const runSteps = new Map<string, t.RunStep>();
@@ -3221,9 +3228,50 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
3221
3228
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
3222
3229
  });
3223
3230
 
3231
+ it('does not prestart when parent-run session hooks alter results', async () => {
3232
+ // A subagent child graph has its own runId, but ToolNode executes hooks
3233
+ // under the PARENT run id from `configurable.run_id` ONLY — the prestart
3234
+ // gate must read the same source (a differing metadata run id here is a
3235
+ // deliberate distractor pinning the precedence).
3236
+ const sessionRegistry = new HookRegistry();
3237
+ sessionRegistry.registerSession('parent-run', 'PreToolUse', {
3238
+ hooks: [async () => ({ decision: 'allow' as const })],
3239
+ });
3240
+ const graph = createGraph({
3241
+ runId: 'child-run',
3242
+ hookRegistry: sessionRegistry,
3243
+ config: {
3244
+ configurable: { user_id: 'user_1', run_id: 'parent-run' },
3245
+ metadata: { run_id: 'run_1' },
3246
+ },
3247
+ } as Partial<StandardGraph>);
3248
+ const sessionDispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3249
+
3250
+ await new ChatModelStreamHandler().handle(
3251
+ GraphEvents.CHAT_MODEL_STREAM,
3252
+ {
3253
+ chunk: {
3254
+ content: '',
3255
+ tool_calls: [
3256
+ { id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
3257
+ ],
3258
+ } as unknown as t.StreamChunk,
3259
+ },
3260
+ { langgraph_node: 'agent', run_id: 'some-other-run' },
3261
+ graph
3262
+ );
3263
+
3264
+ expect(sessionDispatchSpy).not.toHaveBeenCalledWith(
3265
+ GraphEvents.ON_TOOL_EXECUTE,
3266
+ expect.anything(),
3267
+ expect.anything()
3268
+ );
3269
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
3270
+ });
3271
+
3224
3272
  it('does not prestart when batch-sensitive hooks are configured', async () => {
3225
3273
  const graph = createGraph({
3226
- hookRegistry: {} as StandardGraph['hookRegistry'],
3274
+ hookRegistry: createResultAlteringRegistry(),
3227
3275
  });
3228
3276
  const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3229
3277
 
@@ -3253,6 +3301,56 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
3253
3301
  expect(graph.eagerEventToolExecutions.size).toBe(0);
3254
3302
  });
3255
3303
 
3304
+ it('prestarts when the registry only has observation hooks (PostToolBatch)', async () => {
3305
+ const observationRegistry = new HookRegistry();
3306
+ observationRegistry.register('PostToolBatch', {
3307
+ hooks: [async () => ({})],
3308
+ });
3309
+ const graph = createGraph({ hookRegistry: observationRegistry });
3310
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
3311
+ jest
3312
+ .spyOn(events, 'safeDispatchCustomEvent')
3313
+ .mockImplementation(async (event, data): Promise<void> => {
3314
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
3315
+ return;
3316
+ }
3317
+ const batch = data as t.ToolExecuteBatchRequest;
3318
+ toolExecuteCalls.push(batch);
3319
+ batch.resolve([
3320
+ {
3321
+ toolCallId: 'call_weather',
3322
+ status: 'success',
3323
+ content: 'sunny',
3324
+ },
3325
+ ]);
3326
+ });
3327
+
3328
+ await new ChatModelStreamHandler().handle(
3329
+ GraphEvents.CHAT_MODEL_STREAM,
3330
+ {
3331
+ chunk: {
3332
+ content: '',
3333
+ tool_calls: [
3334
+ {
3335
+ id: 'call_weather',
3336
+ name: 'weather',
3337
+ args: { city: 'NYC' },
3338
+ },
3339
+ ],
3340
+ response_metadata: finalToolCallResponseMetadata,
3341
+ } as unknown as t.StreamChunk,
3342
+ },
3343
+ { langgraph_node: 'agent' },
3344
+ graph
3345
+ );
3346
+
3347
+ expect(toolExecuteCalls).toHaveLength(1);
3348
+ expect(graph.eagerEventToolExecutions.get('call_weather')).toMatchObject({
3349
+ toolCallId: 'call_weather',
3350
+ toolName: 'weather',
3351
+ });
3352
+ });
3353
+
3256
3354
  it('does not buffer streamed chunks when eager execution is disabled', async () => {
3257
3355
  const graph = createGraph({
3258
3356
  eagerEventToolExecution: { enabled: false },
@@ -4418,7 +4516,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
4418
4516
 
4419
4517
  it('does not prestart on-arrival sealed calls when batch-sensitive hooks are configured', async () => {
4420
4518
  const graph = createGraph({
4421
- hookRegistry: {} as StandardGraph['hookRegistry'],
4519
+ hookRegistry: createResultAlteringRegistry(),
4422
4520
  });
4423
4521
  const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
4424
4522
 
@@ -140,6 +140,8 @@ export enum ContentTypes {
140
140
  SUMMARY = 'summary',
141
141
  /** Bedrock */
142
142
  REASONING_CONTENT = 'reasoning_content',
143
+ /** Mid-run user steer persisted inline in an assistant message; replayed as a user turn */
144
+ STEER = 'steer',
143
145
  }
144
146
 
145
147
  export enum ToolCallTypes {
@@ -870,6 +870,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
870
870
  * {@link t.StandardGraphInput.subagentUsageSink}.
871
871
  */
872
872
  subagentUsageSink?: t.SubagentUsageSink;
873
+ /** See {@link t.StandardGraphInput.subagentScope}. */
874
+ subagentScope: boolean;
873
875
 
874
876
  constructor({
875
877
  runId,
@@ -880,12 +882,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
880
882
  indexTokenCountMap,
881
883
  calibrationRatio,
882
884
  subagentUsageSink,
885
+ subagentScope,
883
886
  }: t.StandardGraphInput) {
884
887
  super();
885
888
  this.runId = runId;
886
889
  this.signal = signal;
887
890
  this.langfuse = langfuse;
888
891
  this.subagentUsageSink = subagentUsageSink;
892
+ this.subagentScope = subagentScope === true;
889
893
 
890
894
  if (agents.length === 0) {
891
895
  throw new Error('At least one agent configuration is required');
@@ -1272,7 +1276,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1272
1276
  eventDrivenMode: true,
1273
1277
  sessions: this.sessions,
1274
1278
  toolDefinitions: toolDefMap,
1275
- agentId: agentContext?.agentId,
1279
+ // `agentId` is the subagent-scope marker — set ONLY for child-run
1280
+ // graphs (hooks fire for child scopes too, via the inherited
1281
+ // run_id); `executingAgentId` always identifies the owning agent.
1282
+ agentId: this.subagentScope ? agentContext?.agentId : undefined,
1276
1283
  executingAgentId: agentContext?.agentId,
1277
1284
  toolCallStepIds: this.toolCallStepIds,
1278
1285
  toolRegistry: agentContext?.toolRegistry,
@@ -1339,9 +1346,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1339
1346
  trace: traceToolNode,
1340
1347
  runLangfuse: this.langfuse,
1341
1348
  agentLangfuse: agentContext?.langfuse,
1342
- // `agentId` is intentionally left unset on this path (it is the
1343
- // subagent-scope marker); `executingAgentId` always identifies the owning
1344
- // agent so hooks can attribute the batch even at the top level.
1349
+ // `agentId` is the subagent-scope marker set ONLY for child-run
1350
+ // graphs; `executingAgentId` always identifies the owning agent so
1351
+ // hooks can attribute the batch even at the top level.
1352
+ agentId: this.subagentScope ? agentContext?.agentId : undefined,
1345
1353
  executingAgentId: agentContext?.agentId,
1346
1354
  toolCallStepIds: this.toolCallStepIds,
1347
1355
  errorHandler: (data, metadata): Promise<boolean> =>
@@ -13,6 +13,17 @@ import type { HookEvent, HookMatcher } from './types';
13
13
  */
14
14
  type MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;
15
15
 
16
+ /**
17
+ * Events whose hooks can change a tool call's input or output. Presence of
18
+ * any of these disables eager tool execution and early completion emission;
19
+ * observation-only events (`PostToolBatch`, `Stop`, telemetry hooks) do not.
20
+ */
21
+ const RESULT_ALTERING_HOOK_EVENTS = [
22
+ 'PreToolUse',
23
+ 'PostToolUse',
24
+ 'PostToolUseFailure',
25
+ ] as const satisfies readonly HookEvent[];
26
+
16
27
  /**
17
28
  * Snapshot of a halt request raised by a hook returning
18
29
  * `preventContinuation: true`. The SDK's run loop polls for this between
@@ -196,6 +207,31 @@ export class HookRegistry {
196
207
  this.haltSignals.delete(sessionId);
197
208
  }
198
209
 
210
+ /**
211
+ * True when any registered hook can alter a tool result before or after
212
+ * execution (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`). Eager
213
+ * tool execution and early completion emission gate on this instead of
214
+ * registry presence, so observation-only registries (e.g. a
215
+ * `PostToolBatch` steering drain) keep those fast paths. With
216
+ * `sessionId`, checks global + that session; without it, conservatively
217
+ * scans every session bucket.
218
+ */
219
+ hasResultAlteringHooks(sessionId?: string): boolean {
220
+ if (hasResultAlteringInBucket(this.global)) {
221
+ return true;
222
+ }
223
+ if (sessionId !== undefined) {
224
+ const bucket = this.sessions.get(sessionId);
225
+ return bucket !== undefined && hasResultAlteringInBucket(bucket);
226
+ }
227
+ for (const bucket of this.sessions.values()) {
228
+ if (hasResultAlteringInBucket(bucket)) {
229
+ return true;
230
+ }
231
+ }
232
+ return false;
233
+ }
234
+
199
235
  /** True if at least one matcher exists for `event` (global + session). */
200
236
  hasHookFor(event: HookEvent, sessionId?: string): boolean {
201
237
  if (readList(this.global, event).length > 0) {
@@ -242,6 +278,15 @@ function readList(
242
278
  return bucket[event] ?? [];
243
279
  }
244
280
 
281
+ function hasResultAlteringInBucket(bucket: MatcherBucket): boolean {
282
+ for (const event of RESULT_ALTERING_HOOK_EVENTS) {
283
+ if (readList(bucket, event).length > 0) {
284
+ return true;
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+
245
290
  function removeFromList<E extends HookEvent>(
246
291
  list: HookMatcher<HookEvent>[],
247
292
  matcher: HookMatcher<E>
@@ -187,4 +187,52 @@ describe('HookRegistry', () => {
187
187
  }
188
188
  });
189
189
  });
190
+
191
+ describe('hasResultAlteringHooks', () => {
192
+ it('returns false for an empty registry', () => {
193
+ const registry = new HookRegistry();
194
+ expect(registry.hasResultAlteringHooks()).toBe(false);
195
+ expect(registry.hasResultAlteringHooks('run-1')).toBe(false);
196
+ });
197
+
198
+ it('returns false when only observation hooks are registered', () => {
199
+ const registry = new HookRegistry();
200
+ registry.register('PostToolBatch', { hooks: [async () => ({})] });
201
+ registry.register('Stop', { hooks: [async () => ({})] });
202
+ expect(registry.hasResultAlteringHooks()).toBe(false);
203
+ expect(registry.hasResultAlteringHooks('run-1')).toBe(false);
204
+ });
205
+
206
+ it('returns true for each result-altering event registered globally', () => {
207
+ for (const event of [
208
+ 'PreToolUse',
209
+ 'PostToolUse',
210
+ 'PostToolUseFailure',
211
+ ] as const) {
212
+ const registry = new HookRegistry();
213
+ registry.register(event, { hooks: [async () => ({})] });
214
+ expect(registry.hasResultAlteringHooks()).toBe(true);
215
+ expect(registry.hasResultAlteringHooks('any-session')).toBe(true);
216
+ }
217
+ });
218
+
219
+ it('scopes session lookups to the given sessionId', () => {
220
+ const registry = new HookRegistry();
221
+ registry.registerSession('run-a', 'PreToolUse', makePreToolUseMatcher());
222
+ expect(registry.hasResultAlteringHooks('run-a')).toBe(true);
223
+ expect(registry.hasResultAlteringHooks('run-b')).toBe(false);
224
+ });
225
+
226
+ it('conservatively scans all sessions when no sessionId is given', () => {
227
+ const registry = new HookRegistry();
228
+ registry.registerSession(
229
+ 'run-a',
230
+ 'PostToolUse',
231
+ makePostToolUseMatcher()
232
+ );
233
+ expect(registry.hasResultAlteringHooks()).toBe(true);
234
+ registry.clearSession('run-a');
235
+ expect(registry.hasResultAlteringHooks()).toBe(false);
236
+ });
237
+ });
190
238
  });