@librechat/agents 3.2.61 → 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.
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +32 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +6 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +62 -1
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +266 -43
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +274 -7
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
- package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
- package/dist/cjs/llm/openrouter/index.cjs +5 -6
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -1
- package/dist/cjs/messages/format.cjs +61 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +31 -19
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +13 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +75 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +111 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +32 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +6 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +62 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +266 -43
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +274 -7
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
- package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
- package/dist/esm/llm/openrouter/index.mjs +5 -6
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/format.mjs +61 -0
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +31 -19
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +13 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +75 -10
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +108 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +3 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -0
- package/dist/types/hooks/index.d.ts +7 -0
- package/dist/types/hooks/types.d.ts +18 -1
- package/dist/types/llm/bedrock/index.d.ts +5 -0
- package/dist/types/llm/openai/index.d.ts +19 -0
- package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
- package/dist/types/messages/prune.d.ts +4 -3
- package/dist/types/tools/ToolNode.d.ts +1 -0
- package/dist/types/types/llm.d.ts +12 -2
- package/dist/types/types/tools.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +30 -0
- package/package.json +6 -6
- package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
- package/src/common/enum.ts +2 -0
- package/src/hooks/HookRegistry.ts +45 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
- package/src/hooks/__tests__/executeHooks.test.ts +85 -2
- package/src/hooks/executeHooks.ts +15 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/types.ts +18 -1
- package/src/langfuse.ts +134 -1
- package/src/llm/bedrock/index.ts +434 -83
- package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
- package/src/llm/custom-chat-models.smoke.test.ts +7 -0
- package/src/llm/openai/index.ts +604 -6
- package/src/llm/openai/managedRequests.test.ts +182 -0
- package/src/llm/openai/streamMetadata.spec.ts +86 -0
- package/src/llm/openai/streamMetadata.ts +95 -0
- package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
- package/src/llm/openrouter/index.ts +9 -5
- package/src/messages/format.ts +96 -3
- package/src/messages/formatAgentMessages.steer.test.ts +326 -0
- package/src/messages/labelContentByAgent.test.ts +75 -0
- package/src/messages/prune.ts +56 -30
- package/src/specs/anthropic.simple.test.ts +4 -2
- package/src/specs/cache.simple.test.ts +17 -4
- package/src/specs/langfuse-callbacks.test.ts +61 -0
- package/src/specs/openai.simple.test.ts +4 -2
- package/src/specs/spec.utils.ts +12 -0
- package/src/specs/summarization.test.ts +9 -13
- package/src/specs/token-accounting-pipeline.test.ts +130 -4
- package/src/specs/tokens.test.ts +214 -0
- package/src/stream.ts +22 -3
- package/src/tools/ToolNode.ts +107 -12
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
- package/src/tools/__tests__/hitl.test.ts +112 -0
- package/src/tools/subagent/SubagentExecutor.ts +2 -0
- package/src/types/llm.ts +12 -2
- package/src/types/tools.ts +1 -1
- package/src/utils/tokens.ts +181 -3
|
@@ -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
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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;
|
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "3.2.62",
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
4519
|
+
hookRegistry: createResultAlteringRegistry(),
|
|
4422
4520
|
});
|
|
4423
4521
|
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
4424
4522
|
|
package/src/common/enum.ts
CHANGED
|
@@ -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 {
|
|
@@ -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
|
});
|
|
@@ -155,7 +155,11 @@ describe('executeHooks', () => {
|
|
|
155
155
|
input: preToolUseInput('Bash'),
|
|
156
156
|
matchQuery: 'Bash',
|
|
157
157
|
});
|
|
158
|
-
expect(result).toEqual({
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
additionalContexts: [],
|
|
160
|
+
injectedMessages: [],
|
|
161
|
+
errors: [],
|
|
162
|
+
});
|
|
159
163
|
});
|
|
160
164
|
|
|
161
165
|
it('returns an empty result when no matcher pattern matches the query', async () => {
|
|
@@ -176,7 +180,11 @@ describe('executeHooks', () => {
|
|
|
176
180
|
matchQuery: 'Bash',
|
|
177
181
|
});
|
|
178
182
|
expect(called).toBe(false);
|
|
179
|
-
expect(result).toEqual({
|
|
183
|
+
expect(result).toEqual({
|
|
184
|
+
additionalContexts: [],
|
|
185
|
+
injectedMessages: [],
|
|
186
|
+
errors: [],
|
|
187
|
+
});
|
|
180
188
|
});
|
|
181
189
|
});
|
|
182
190
|
|
|
@@ -439,6 +447,81 @@ describe('executeHooks', () => {
|
|
|
439
447
|
});
|
|
440
448
|
});
|
|
441
449
|
|
|
450
|
+
describe('injectedMessages accumulation', () => {
|
|
451
|
+
it('accumulates injectedMessages from every hook in registration order', async () => {
|
|
452
|
+
const registry = new HookRegistry();
|
|
453
|
+
registry.register('PreToolUse', {
|
|
454
|
+
hooks: [
|
|
455
|
+
preToolHook(
|
|
456
|
+
async (): Promise<PreToolUseHookOutput> => ({
|
|
457
|
+
injectedMessages: [
|
|
458
|
+
{ role: 'user', content: 'steer one', source: 'steer' },
|
|
459
|
+
],
|
|
460
|
+
})
|
|
461
|
+
),
|
|
462
|
+
preToolHook(
|
|
463
|
+
async (): Promise<PreToolUseHookOutput> => emptyPreOutput
|
|
464
|
+
),
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
registry.register('PreToolUse', {
|
|
468
|
+
hooks: [
|
|
469
|
+
preToolHook(
|
|
470
|
+
async (): Promise<PreToolUseHookOutput> => ({
|
|
471
|
+
injectedMessages: [
|
|
472
|
+
{ role: 'user', content: 'steer two', source: 'steer' },
|
|
473
|
+
{ role: 'system', content: 'hint', source: 'hook' },
|
|
474
|
+
],
|
|
475
|
+
})
|
|
476
|
+
),
|
|
477
|
+
],
|
|
478
|
+
});
|
|
479
|
+
const result = await executeHooks({
|
|
480
|
+
registry,
|
|
481
|
+
input: preToolUseInput('Bash'),
|
|
482
|
+
matchQuery: 'Bash',
|
|
483
|
+
});
|
|
484
|
+
expect(result.injectedMessages).toEqual([
|
|
485
|
+
{ role: 'user', content: 'steer one', source: 'steer' },
|
|
486
|
+
{ role: 'user', content: 'steer two', source: 'steer' },
|
|
487
|
+
{ role: 'system', content: 'hint', source: 'hook' },
|
|
488
|
+
]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('returns an empty array when no hook sets injectedMessages', async () => {
|
|
492
|
+
const registry = new HookRegistry();
|
|
493
|
+
registry.register('PreToolUse', { hooks: [noopPreHook] });
|
|
494
|
+
const result = await executeHooks({
|
|
495
|
+
registry,
|
|
496
|
+
input: preToolUseInput('Bash'),
|
|
497
|
+
matchQuery: 'Bash',
|
|
498
|
+
});
|
|
499
|
+
expect(result.injectedMessages).toEqual([]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('skips injectedMessages from fire-and-forget (async) outputs', async () => {
|
|
503
|
+
const registry = new HookRegistry();
|
|
504
|
+
registry.register('PreToolUse', {
|
|
505
|
+
hooks: [
|
|
506
|
+
preToolHook(
|
|
507
|
+
async (): Promise<PreToolUseHookOutput> => ({
|
|
508
|
+
async: true,
|
|
509
|
+
injectedMessages: [
|
|
510
|
+
{ role: 'user', content: 'ignored', source: 'steer' },
|
|
511
|
+
],
|
|
512
|
+
})
|
|
513
|
+
),
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
const result = await executeHooks({
|
|
517
|
+
registry,
|
|
518
|
+
input: preToolUseInput('Bash'),
|
|
519
|
+
matchQuery: 'Bash',
|
|
520
|
+
});
|
|
521
|
+
expect(result.injectedMessages).toEqual([]);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
442
525
|
describe('updatedInput handling', () => {
|
|
443
526
|
it('last-writer-wins on updatedInput follows registration order', async () => {
|
|
444
527
|
const registry = new HookRegistry();
|