@librechat/agents 3.1.81 → 3.1.82

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 (47) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +102 -35
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +13 -0
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/openai/index.cjs +50 -13
  6. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  7. package/dist/cjs/llm/openrouter/index.cjs +17 -7
  8. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  9. package/dist/cjs/llm/openrouter/toolCache.cjs +55 -0
  10. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -0
  11. package/dist/cjs/tools/ToolNode.cjs +70 -12
  12. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  13. package/dist/esm/agents/AgentContext.mjs +101 -34
  14. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  15. package/dist/esm/graphs/Graph.mjs +13 -0
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/llm/openai/index.mjs +50 -14
  18. package/dist/esm/llm/openai/index.mjs.map +1 -1
  19. package/dist/esm/llm/openrouter/index.mjs +17 -7
  20. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  21. package/dist/esm/llm/openrouter/toolCache.mjs +53 -0
  22. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -0
  23. package/dist/esm/tools/ToolNode.mjs +70 -12
  24. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  25. package/dist/types/agents/AgentContext.d.ts +6 -1
  26. package/dist/types/llm/openrouter/index.d.ts +1 -0
  27. package/dist/types/llm/openrouter/toolCache.d.ts +2 -0
  28. package/dist/types/tools/ToolNode.d.ts +5 -0
  29. package/dist/types/types/run.d.ts +2 -0
  30. package/package.json +2 -1
  31. package/src/agents/AgentContext.ts +146 -38
  32. package/src/agents/__tests__/AgentContext.test.ts +198 -0
  33. package/src/graphs/Graph.ts +24 -0
  34. package/src/llm/custom-chat-models.smoke.test.ts +76 -0
  35. package/src/llm/openai/deepseek.test.ts +14 -1
  36. package/src/llm/openai/index.ts +38 -12
  37. package/src/llm/openrouter/index.ts +22 -7
  38. package/src/llm/openrouter/reasoning.test.ts +33 -0
  39. package/src/llm/openrouter/toolCache.test.ts +83 -0
  40. package/src/llm/openrouter/toolCache.ts +89 -0
  41. package/src/messages/cache.test.ts +127 -0
  42. package/src/scripts/openrouter_prompt_cache_live.ts +310 -0
  43. package/src/specs/agent-handoffs.live.test.ts +140 -0
  44. package/src/specs/agent-handoffs.test.ts +266 -2
  45. package/src/specs/openrouter.simple.test.ts +15 -8
  46. package/src/tools/ToolNode.ts +92 -13
  47. package/src/types/run.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../../../../src/llm/openrouter/index.ts"],"sourcesContent":["import { ChatOpenAI } from '@/llm/openai';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { ChatGenerationChunk } from '@langchain/core/outputs';\nimport type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';\nimport type {\n ChatOpenAICallOptions,\n OpenAIChatInput,\n OpenAIClient,\n} from '@langchain/openai';\n\nexport type OpenRouterReasoningEffort =\n | 'xhigh'\n | 'high'\n | 'medium'\n | 'low'\n | 'minimal'\n | 'none';\n\nexport interface OpenRouterReasoning {\n effort?: OpenRouterReasoningEffort;\n max_tokens?: number;\n exclude?: boolean;\n enabled?: boolean;\n}\n\nexport interface ChatOpenRouterCallOptions\n extends Omit<ChatOpenAICallOptions, 'reasoning'> {\n /** @deprecated Use `reasoning` object instead */\n include_reasoning?: boolean;\n reasoning?: OpenRouterReasoning;\n modelKwargs?: OpenAIChatInput['modelKwargs'];\n}\n\nexport type ChatOpenRouterInput = Partial<\n ChatOpenRouterCallOptions & OpenAIChatInput\n>;\n\n/** invocationParams return type extended with OpenRouter reasoning */\nexport type OpenRouterInvocationParams = Omit<\n OpenAIClient.Chat.ChatCompletionCreateParams,\n 'messages'\n> & {\n reasoning?: OpenRouterReasoning;\n};\n\ntype InvocationParamsExtra = {\n streaming?: boolean;\n};\n\ninterface OpenRouterReasoningTextDetail {\n type: 'reasoning.text';\n text?: string;\n format?: string;\n index?: number;\n}\n\ninterface OpenRouterReasoningEncryptedDetail {\n type: 'reasoning.encrypted';\n id?: string;\n data?: string;\n format?: string;\n index?: number;\n}\n\ntype OpenRouterReasoningDetail =\n | OpenRouterReasoningTextDetail\n | OpenRouterReasoningEncryptedDetail;\n\nfunction isReasoningTextDetail(\n value: unknown\n): value is OpenRouterReasoningTextDetail {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n value.type === 'reasoning.text'\n );\n}\n\nfunction isReasoningEncryptedDetail(\n value: unknown\n): value is OpenRouterReasoningEncryptedDetail {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n value.type === 'reasoning.encrypted'\n );\n}\n\nfunction getReasoningDetails(value: unknown): OpenRouterReasoningDetail[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value.filter(\n (detail): detail is OpenRouterReasoningDetail =>\n isReasoningTextDetail(detail) || isReasoningEncryptedDetail(detail)\n );\n}\n\nexport class ChatOpenRouter extends ChatOpenAI {\n private openRouterReasoning?: OpenRouterReasoning;\n /** @deprecated Use `reasoning` object instead */\n private includeReasoning?: boolean;\n\n constructor(_fields: ChatOpenRouterInput) {\n const {\n include_reasoning,\n reasoning: openRouterReasoning,\n modelKwargs = {},\n ...fields\n } = _fields;\n\n // Extract reasoning from modelKwargs if provided there (e.g., from LLMConfig)\n const { reasoning: mkReasoning, ...restModelKwargs } = modelKwargs as {\n reasoning?: OpenRouterReasoning;\n } & Record<string, unknown>;\n\n super({\n ...fields,\n modelKwargs: restModelKwargs,\n includeReasoningDetails: true,\n convertReasoningDetailsToContent: true,\n });\n\n // Merge reasoning config: modelKwargs.reasoning < constructor reasoning\n if (mkReasoning != null || openRouterReasoning != null) {\n this.openRouterReasoning = {\n ...mkReasoning,\n ...openRouterReasoning,\n };\n }\n\n this.includeReasoning = include_reasoning;\n }\n static lc_name(): 'LibreChatOpenRouter' {\n return 'LibreChatOpenRouter';\n }\n\n // @ts-expect-error - OpenRouter reasoning extends OpenAI Reasoning with additional\n // effort levels ('xhigh' | 'none' | 'minimal') not in ReasoningEffort.\n // The parent's generic conditional return type cannot be widened in an override.\n override invocationParams(\n options?: this['ParsedCallOptions'],\n extra?: InvocationParamsExtra\n ): OpenRouterInvocationParams {\n type MutableParams = Omit<\n OpenAIClient.Chat.ChatCompletionCreateParams,\n 'messages'\n > & { reasoning_effort?: string; reasoning?: OpenRouterReasoning };\n\n const optionsWithDefaults = this._combineCallOptions(options);\n const params = (\n this._useResponsesApi(options)\n ? this.responses.invocationParams(optionsWithDefaults)\n : this.completions.invocationParams(optionsWithDefaults, extra)\n ) as MutableParams;\n\n // Remove the OpenAI-native reasoning_effort that the parent sets;\n // OpenRouter uses a `reasoning` object instead\n delete params.reasoning_effort;\n\n // Build the OpenRouter reasoning config\n const reasoning = this.buildOpenRouterReasoning(optionsWithDefaults);\n if (reasoning != null) {\n params.reasoning = reasoning;\n } else {\n delete params.reasoning;\n }\n\n return params;\n }\n\n private buildOpenRouterReasoning(\n options?: this['ParsedCallOptions']\n ): OpenRouterReasoning | undefined {\n let reasoning: OpenRouterReasoning | undefined;\n\n // 1. Instance-level reasoning config (from constructor)\n if (this.openRouterReasoning != null) {\n reasoning = { ...this.openRouterReasoning };\n }\n\n // 2. LangChain-style reasoning params (from parent's `this.reasoning`)\n const lcReasoning = this.getReasoningParams(options);\n if (lcReasoning?.effort != null) {\n reasoning = {\n ...reasoning,\n effort: lcReasoning.effort as OpenRouterReasoningEffort,\n };\n }\n\n // 3. Call-level reasoning override\n const callReasoning = (options as ChatOpenRouterCallOptions | undefined)\n ?.reasoning;\n if (callReasoning != null) {\n reasoning = { ...reasoning, ...callReasoning };\n }\n\n // 4. Legacy include_reasoning backward compatibility\n if (reasoning == null && this.includeReasoning === true) {\n reasoning = { enabled: true };\n }\n\n return reasoning;\n }\n\n override async *_streamResponseChunks(\n messages: BaseMessage[],\n options: this['ParsedCallOptions'],\n runManager?: CallbackManagerForLLMRun\n ): AsyncGenerator<ChatGenerationChunk> {\n const reasoningTextByIndex = new Map<\n number,\n OpenRouterReasoningTextDetail\n >();\n const reasoningEncryptedById = new Map<\n string,\n OpenRouterReasoningEncryptedDetail\n >();\n\n for await (const generationChunk of super._streamResponseChunks(\n messages,\n options,\n runManager\n )) {\n let currentReasoningText = '';\n const reasoningDetails = getReasoningDetails(\n generationChunk.message.additional_kwargs.reasoning_details\n );\n\n for (const detail of reasoningDetails) {\n if (detail.type === 'reasoning.text') {\n currentReasoningText += detail.text ?? '';\n const index = detail.index ?? 0;\n const existing = reasoningTextByIndex.get(index);\n if (existing != null) {\n existing.text = `${existing.text ?? ''}${detail.text ?? ''}`;\n continue;\n }\n reasoningTextByIndex.set(index, {\n ...detail,\n text: detail.text ?? '',\n });\n continue;\n }\n if (detail.id != null) {\n reasoningEncryptedById.set(detail.id, { ...detail });\n }\n }\n\n if (\n currentReasoningText.length > 0 &&\n generationChunk.message.additional_kwargs.reasoning == null\n ) {\n generationChunk.message.additional_kwargs.reasoning =\n currentReasoningText;\n }\n\n if (generationChunk.generationInfo?.finish_reason != null) {\n const finalReasoningDetails = [\n ...reasoningTextByIndex.values(),\n ...reasoningEncryptedById.values(),\n ];\n if (finalReasoningDetails.length > 0) {\n generationChunk.message.additional_kwargs.reasoning_details =\n finalReasoningDetails;\n } else {\n delete generationChunk.message.additional_kwargs.reasoning_details;\n }\n yield generationChunk;\n continue;\n }\n\n delete generationChunk.message.additional_kwargs.reasoning_details;\n yield generationChunk;\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../../../../src/llm/openrouter/index.ts"],"sourcesContent":["import { ChatOpenAI } from '@/llm/openai';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { ChatGenerationChunk } from '@langchain/core/outputs';\nimport type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';\nimport type {\n ChatOpenAICallOptions,\n OpenAIChatInput,\n OpenAIClient,\n} from '@langchain/openai';\n\nexport type OpenRouterReasoningEffort =\n | 'xhigh'\n | 'high'\n | 'medium'\n | 'low'\n | 'minimal'\n | 'none';\n\nexport interface OpenRouterReasoning {\n effort?: OpenRouterReasoningEffort;\n max_tokens?: number;\n exclude?: boolean;\n enabled?: boolean;\n}\n\nexport interface ChatOpenRouterCallOptions\n extends Omit<ChatOpenAICallOptions, 'reasoning'> {\n /** @deprecated Use `reasoning` object instead */\n include_reasoning?: boolean;\n reasoning?: OpenRouterReasoning;\n modelKwargs?: OpenAIChatInput['modelKwargs'];\n promptCache?: boolean;\n}\n\nexport type ChatOpenRouterInput = Partial<\n ChatOpenRouterCallOptions & OpenAIChatInput\n>;\n\n/** invocationParams return type extended with OpenRouter reasoning */\nexport type OpenRouterInvocationParams = Omit<\n OpenAIClient.Chat.ChatCompletionCreateParams,\n 'messages'\n> & {\n reasoning?: OpenRouterReasoning;\n};\n\ntype InvocationParamsExtra = {\n streaming?: boolean;\n};\n\ninterface OpenRouterReasoningTextDetail {\n type: 'reasoning.text';\n text?: string;\n format?: string;\n index?: number;\n}\n\ninterface OpenRouterReasoningEncryptedDetail {\n type: 'reasoning.encrypted';\n id?: string;\n data?: string;\n format?: string;\n index?: number;\n}\n\ntype OpenRouterReasoningDetail =\n | OpenRouterReasoningTextDetail\n | OpenRouterReasoningEncryptedDetail;\n\nfunction isReasoningTextDetail(\n value: unknown\n): value is OpenRouterReasoningTextDetail {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n value.type === 'reasoning.text'\n );\n}\n\nfunction isReasoningEncryptedDetail(\n value: unknown\n): value is OpenRouterReasoningEncryptedDetail {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n value.type === 'reasoning.encrypted'\n );\n}\n\nfunction getReasoningDetails(value: unknown): OpenRouterReasoningDetail[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value.filter(\n (detail): detail is OpenRouterReasoningDetail =>\n isReasoningTextDetail(detail) || isReasoningEncryptedDetail(detail)\n );\n}\n\nexport class ChatOpenRouter extends ChatOpenAI {\n private openRouterReasoning?: OpenRouterReasoning;\n /** @deprecated Use `reasoning` object instead */\n private includeReasoning?: boolean;\n\n constructor(_fields: ChatOpenRouterInput) {\n const fieldsWithoutPromptCache: ChatOpenRouterInput = { ..._fields };\n delete fieldsWithoutPromptCache.promptCache;\n\n const {\n include_reasoning,\n reasoning: openRouterReasoning,\n modelKwargs = {},\n ...fields\n } = fieldsWithoutPromptCache;\n\n // Extract reasoning from modelKwargs if provided there (e.g., from LLMConfig)\n const { reasoning: mkReasoning, ...restModelKwargs } = modelKwargs as {\n reasoning?: OpenRouterReasoning;\n } & Record<string, unknown>;\n const mergedReasoning =\n mkReasoning != null || openRouterReasoning != null\n ? {\n ...mkReasoning,\n ...openRouterReasoning,\n }\n : undefined;\n const runtimeReasoning =\n mergedReasoning ??\n (include_reasoning === true ? { enabled: true } : undefined);\n const parentModelKwargs =\n runtimeReasoning == null\n ? restModelKwargs\n : { ...restModelKwargs, reasoning: runtimeReasoning };\n\n super({\n ...fields,\n modelKwargs: parentModelKwargs,\n includeReasoningDetails: true,\n convertReasoningDetailsToContent: true,\n });\n\n // Merge reasoning config: modelKwargs.reasoning < constructor reasoning\n if (mergedReasoning != null) {\n this.openRouterReasoning = mergedReasoning;\n }\n\n this.includeReasoning = include_reasoning;\n }\n static lc_name(): 'LibreChatOpenRouter' {\n return 'LibreChatOpenRouter';\n }\n\n // @ts-expect-error - OpenRouter reasoning extends OpenAI Reasoning with additional\n // effort levels ('xhigh' | 'none' | 'minimal') not in ReasoningEffort.\n // The parent's generic conditional return type cannot be widened in an override.\n override invocationParams(\n options?: this['ParsedCallOptions'],\n extra?: InvocationParamsExtra\n ): OpenRouterInvocationParams {\n type MutableParams = Omit<\n OpenAIClient.Chat.ChatCompletionCreateParams,\n 'messages'\n > & { reasoning_effort?: string; reasoning?: OpenRouterReasoning };\n\n const optionsWithDefaults = this._combineCallOptions(options);\n const params = (\n this._useResponsesApi(options)\n ? this.responses.invocationParams(optionsWithDefaults)\n : this.completions.invocationParams(optionsWithDefaults, extra)\n ) as MutableParams;\n\n // Remove the OpenAI-native reasoning_effort that the parent sets;\n // OpenRouter uses a `reasoning` object instead\n delete params.reasoning_effort;\n\n // Build the OpenRouter reasoning config\n const reasoning = this.buildOpenRouterReasoning(optionsWithDefaults);\n if (reasoning != null) {\n params.reasoning = reasoning;\n } else {\n delete params.reasoning;\n }\n\n return params;\n }\n\n private buildOpenRouterReasoning(\n options?: this['ParsedCallOptions']\n ): OpenRouterReasoning | undefined {\n let reasoning: OpenRouterReasoning | undefined;\n\n // 1. Instance-level reasoning config (from constructor)\n if (this.openRouterReasoning != null) {\n reasoning = { ...this.openRouterReasoning };\n }\n\n // 2. LangChain-style reasoning params (from parent's `this.reasoning`)\n const lcReasoning = this.getReasoningParams(options);\n if (lcReasoning?.effort != null) {\n reasoning = {\n ...reasoning,\n effort: lcReasoning.effort as OpenRouterReasoningEffort,\n };\n }\n\n // 3. Call-level reasoning override\n const callReasoning = (options as ChatOpenRouterCallOptions | undefined)\n ?.reasoning;\n if (callReasoning != null) {\n reasoning = { ...reasoning, ...callReasoning };\n }\n\n // 4. Legacy include_reasoning backward compatibility\n if (reasoning == null && this.includeReasoning === true) {\n reasoning = { enabled: true };\n }\n\n return reasoning;\n }\n\n override async *_streamResponseChunks(\n messages: BaseMessage[],\n options: this['ParsedCallOptions'],\n runManager?: CallbackManagerForLLMRun\n ): AsyncGenerator<ChatGenerationChunk> {\n const reasoningTextByIndex = new Map<\n number,\n OpenRouterReasoningTextDetail\n >();\n const reasoningEncryptedById = new Map<\n string,\n OpenRouterReasoningEncryptedDetail\n >();\n\n for await (const generationChunk of super._streamResponseChunks(\n messages,\n options,\n runManager\n )) {\n let currentReasoningText = '';\n const reasoningDetails = getReasoningDetails(\n generationChunk.message.additional_kwargs.reasoning_details\n );\n\n for (const detail of reasoningDetails) {\n if (detail.type === 'reasoning.text') {\n currentReasoningText += detail.text ?? '';\n const index = detail.index ?? 0;\n const existing = reasoningTextByIndex.get(index);\n if (existing != null) {\n existing.text = `${existing.text ?? ''}${detail.text ?? ''}`;\n continue;\n }\n reasoningTextByIndex.set(index, {\n ...detail,\n text: detail.text ?? '',\n });\n continue;\n }\n if (detail.id != null) {\n reasoningEncryptedById.set(detail.id, { ...detail });\n }\n }\n\n if (\n currentReasoningText.length > 0 &&\n generationChunk.message.additional_kwargs.reasoning == null\n ) {\n generationChunk.message.additional_kwargs.reasoning =\n currentReasoningText;\n }\n\n if (generationChunk.generationInfo?.finish_reason != null) {\n const finalReasoningDetails = [\n ...reasoningTextByIndex.values(),\n ...reasoningEncryptedById.values(),\n ];\n if (finalReasoningDetails.length > 0) {\n generationChunk.message.additional_kwargs.reasoning_details =\n finalReasoningDetails;\n } else {\n delete generationChunk.message.additional_kwargs.reasoning_details;\n }\n yield generationChunk;\n continue;\n }\n\n delete generationChunk.message.additional_kwargs.reasoning_details;\n yield generationChunk;\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { _convertToOpenAITool } from '../openai/index.mjs';
2
+
3
+ const CACHE_CONTROL = { type: 'ephemeral' };
4
+ function getToolName(tool) {
5
+ const candidate = tool;
6
+ if (typeof candidate.name === 'string') {
7
+ return candidate.name;
8
+ }
9
+ if (typeof candidate.function?.name === 'string') {
10
+ return candidate.function.name;
11
+ }
12
+ return undefined;
13
+ }
14
+ function hasDeferredMarker(tool) {
15
+ return tool.defer_loading === true;
16
+ }
17
+ function toOpenRouterTool(tool) {
18
+ const converted = _convertToOpenAITool(tool);
19
+ if (hasDeferredMarker(tool)) {
20
+ return { ...converted, defer_loading: true };
21
+ }
22
+ return converted;
23
+ }
24
+ function markCacheControl(tool) {
25
+ return {
26
+ ...tool,
27
+ cache_control: CACHE_CONTROL,
28
+ };
29
+ }
30
+ function partitionAndMarkOpenRouterToolCache(tools, isDeferred) {
31
+ if (tools == null || tools.length === 0) {
32
+ return tools;
33
+ }
34
+ const staticTools = [];
35
+ const deferredTools = [];
36
+ for (const tool of tools) {
37
+ const converted = toOpenRouterTool(tool);
38
+ const name = getToolName(converted) ?? getToolName(tool);
39
+ if (name != null && isDeferred(name)) {
40
+ deferredTools.push(converted);
41
+ continue;
42
+ }
43
+ staticTools.push(converted);
44
+ }
45
+ if (staticTools.length === 0) {
46
+ return [...deferredTools];
47
+ }
48
+ staticTools[staticTools.length - 1] = markCacheControl(staticTools[staticTools.length - 1]);
49
+ return [...staticTools, ...deferredTools];
50
+ }
51
+
52
+ export { partitionAndMarkOpenRouterToolCache };
53
+ //# sourceMappingURL=toolCache.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolCache.mjs","sources":["../../../../src/llm/openrouter/toolCache.ts"],"sourcesContent":["import type { BindToolsInput } from '@langchain/core/language_models/chat_models';\nimport type { OpenAIClient } from '@langchain/openai';\nimport type { GraphTools } from '@/types';\nimport { _convertToOpenAITool } from '@/llm/openai';\n\nconst CACHE_CONTROL = { type: 'ephemeral' as const };\n\ntype OpenRouterToolWithCacheControl = OpenAIClient.ChatCompletionTool & {\n cache_control?: typeof CACHE_CONTROL;\n defer_loading?: boolean;\n};\n\ntype ToolNameCandidate = {\n name?: unknown;\n function?: {\n name?: unknown;\n };\n defer_loading?: unknown;\n};\n\nfunction getToolName(tool: unknown): string | undefined {\n const candidate = tool as ToolNameCandidate;\n if (typeof candidate.name === 'string') {\n return candidate.name;\n }\n if (typeof candidate.function?.name === 'string') {\n return candidate.function.name;\n }\n return undefined;\n}\n\nfunction hasDeferredMarker(tool: unknown): boolean {\n return (tool as ToolNameCandidate).defer_loading === true;\n}\n\nfunction toOpenRouterTool(tool: unknown): OpenRouterToolWithCacheControl {\n const converted = _convertToOpenAITool(\n tool as BindToolsInput\n ) as OpenRouterToolWithCacheControl;\n\n if (hasDeferredMarker(tool)) {\n return { ...converted, defer_loading: true };\n }\n\n return converted;\n}\n\nfunction markCacheControl(\n tool: OpenRouterToolWithCacheControl\n): OpenRouterToolWithCacheControl {\n return {\n ...tool,\n cache_control: CACHE_CONTROL,\n };\n}\n\nexport function partitionAndMarkOpenRouterToolCache(\n tools: GraphTools | undefined,\n isDeferred: (toolName: string) => boolean\n): GraphTools | undefined {\n if (tools == null || tools.length === 0) {\n return tools;\n }\n\n const staticTools: OpenRouterToolWithCacheControl[] = [];\n const deferredTools: OpenRouterToolWithCacheControl[] = [];\n\n for (const tool of tools as readonly unknown[]) {\n const converted = toOpenRouterTool(tool);\n const name = getToolName(converted) ?? getToolName(tool);\n\n if (name != null && isDeferred(name)) {\n deferredTools.push(converted);\n continue;\n }\n\n staticTools.push(converted);\n }\n\n if (staticTools.length === 0) {\n return [...deferredTools] as GraphTools;\n }\n\n staticTools[staticTools.length - 1] = markCacheControl(\n staticTools[staticTools.length - 1]\n );\n\n return [...staticTools, ...deferredTools] as GraphTools;\n}\n"],"names":[],"mappings":";;AAKA,MAAM,aAAa,GAAG,EAAE,IAAI,EAAE,WAAoB,EAAE;AAepD,SAAS,WAAW,CAAC,IAAa,EAAA;IAChC,MAAM,SAAS,GAAG,IAAyB;AAC3C,IAAA,IAAI,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,EAAE;QACtC,OAAO,SAAS,CAAC,IAAI;IACvB;IACA,IAAI,OAAO,SAAS,CAAC,QAAQ,EAAE,IAAI,KAAK,QAAQ,EAAE;AAChD,QAAA,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI;IAChC;AACA,IAAA,OAAO,SAAS;AAClB;AAEA,SAAS,iBAAiB,CAAC,IAAa,EAAA;AACtC,IAAA,OAAQ,IAA0B,CAAC,aAAa,KAAK,IAAI;AAC3D;AAEA,SAAS,gBAAgB,CAAC,IAAa,EAAA;AACrC,IAAA,MAAM,SAAS,GAAG,oBAAoB,CACpC,IAAsB,CACW;AAEnC,IAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE;QAC3B,OAAO,EAAE,GAAG,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE;IAC9C;AAEA,IAAA,OAAO,SAAS;AAClB;AAEA,SAAS,gBAAgB,CACvB,IAAoC,EAAA;IAEpC,OAAO;AACL,QAAA,GAAG,IAAI;AACP,QAAA,aAAa,EAAE,aAAa;KAC7B;AACH;AAEM,SAAU,mCAAmC,CACjD,KAA6B,EAC7B,UAAyC,EAAA;IAEzC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACvC,QAAA,OAAO,KAAK;IACd;IAEA,MAAM,WAAW,GAAqC,EAAE;IACxD,MAAM,aAAa,GAAqC,EAAE;AAE1D,IAAA,KAAK,MAAM,IAAI,IAAI,KAA2B,EAAE;AAC9C,QAAA,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC;QACxC,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC;QAExD,IAAI,IAAI,IAAI,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE;AACpC,YAAA,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;YAC7B;QACF;AAEA,QAAA,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC;IAC7B;AAEA,IAAA,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE;AAC5B,QAAA,OAAO,CAAC,GAAG,aAAa,CAAe;IACzC;AAEA,IAAA,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,gBAAgB,CACpD,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CACpC;AAED,IAAA,OAAO,CAAC,GAAG,WAAW,EAAE,GAAG,aAAa,CAAe;AACzD;;;;"}
@@ -32,6 +32,9 @@ import './local/attachments.mjs';
32
32
  function isSend(value) {
33
33
  return value instanceof Send;
34
34
  }
35
+ function isHandoffToolName(name) {
36
+ return name.startsWith(Constants.LC_TRANSFER_TO_);
37
+ }
35
38
  /**
36
39
  * Format a fail-closed diagnostic for malformed approval-decision
37
40
  * fields. Hosts deserialize resume payloads from untyped JSON, so
@@ -394,6 +397,58 @@ class ToolNode extends RunnableCallable {
394
397
  getFileCheckpointer() {
395
398
  return this.fileCheckpointer;
396
399
  }
400
+ *getRegisteredHandoffNames() {
401
+ if (this.directToolNames != null) {
402
+ for (const toolName of this.directToolNames) {
403
+ yield toolName;
404
+ }
405
+ }
406
+ for (const toolName of this.toolMap.keys()) {
407
+ if (this.directToolNames?.has(toolName) === true) {
408
+ continue;
409
+ }
410
+ yield toolName;
411
+ }
412
+ }
413
+ hasRegisteredHandoffTool() {
414
+ for (const toolName of this.getRegisteredHandoffNames()) {
415
+ if (isHandoffToolName(toolName)) {
416
+ return true;
417
+ }
418
+ }
419
+ return false;
420
+ }
421
+ getHandoffToolNameSuggestion(callName) {
422
+ if (!isHandoffToolName(callName)) {
423
+ return undefined;
424
+ }
425
+ let suggestion;
426
+ for (const toolName of this.getRegisteredHandoffNames()) {
427
+ if (!isHandoffToolName(toolName) ||
428
+ toolName.length >= callName.length ||
429
+ !callName.startsWith(toolName)) {
430
+ continue;
431
+ }
432
+ if (suggestion == null || toolName.length > suggestion.length) {
433
+ suggestion = toolName;
434
+ }
435
+ }
436
+ return suggestion;
437
+ }
438
+ shouldHandleUnknownHandoffLocally(callName, hasRegisteredHandoffTool) {
439
+ if (!isHandoffToolName(callName) || this.toolMap.has(callName)) {
440
+ return false;
441
+ }
442
+ return hasRegisteredHandoffTool ?? this.hasRegisteredHandoffTool();
443
+ }
444
+ getUnknownToolErrorMessage(callName) {
445
+ const suggestion = this.getHandoffToolNameSuggestion(callName);
446
+ if (suggestion == null) {
447
+ return `Tool "${callName}" not found.`;
448
+ }
449
+ return (`Tool "${callName}" not found. Did you mean "${suggestion}"? ` +
450
+ 'Handoff tool names must match exactly.');
451
+ }
397
452
  /**
398
453
  * Flush the per-Run direct-path turn cache. Called by the Graph at
399
454
  * end-of-Run via `clearHeavyState`. The map intentionally survives
@@ -479,7 +534,7 @@ class ToolNode extends RunnableCallable {
479
534
  const runId = batchScopeId ?? config.configurable?.run_id;
480
535
  try {
481
536
  if (tool === undefined) {
482
- throw new Error(`Tool "${call.name}" not found.`);
537
+ throw new Error(this.getUnknownToolErrorMessage(call.name));
483
538
  }
484
539
  /**
485
540
  * `usageCount` is the per-tool-name invocation index that
@@ -2121,8 +2176,9 @@ class ToolNode extends RunnableCallable {
2121
2176
  const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
2122
2177
  let outputs;
2123
2178
  if (this.isSendInput(input)) {
2124
- const isDirectTool = this.directToolNames?.has(input.lg_tool_call.name);
2125
- if (this.eventDrivenMode && isDirectTool !== true) {
2179
+ const isLocalTool = this.directToolNames?.has(input.lg_tool_call.name) === true ||
2180
+ this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
2181
+ if (this.eventDrivenMode && !isLocalTool) {
2126
2182
  return this.executeViaEvent([input.lg_tool_call], config, input, {
2127
2183
  batchIndices: [0],
2128
2184
  turn,
@@ -2205,26 +2261,28 @@ class ToolNode extends RunnableCallable {
2205
2261
  false));
2206
2262
  }) ?? [];
2207
2263
  if (this.eventDrivenMode && filteredCalls.length > 0) {
2208
- const filteredIndices = filteredCalls.map((_, idx) => idx);
2209
- if (!this.directToolNames || this.directToolNames.size === 0) {
2210
- return this.executeViaEvent(filteredCalls, config, input, {
2211
- batchIndices: filteredIndices,
2212
- turn,
2213
- batchScopeId,
2214
- });
2215
- }
2264
+ const directToolNames = this.directToolNames;
2265
+ const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
2216
2266
  const directEntries = [];
2217
2267
  const eventEntries = [];
2218
2268
  for (let i = 0; i < filteredCalls.length; i++) {
2219
2269
  const call = filteredCalls[i];
2220
2270
  const entry = { call, batchIndex: i };
2221
- if (this.directToolNames.has(call.name)) {
2271
+ if (directToolNames?.has(call.name) === true ||
2272
+ this.shouldHandleUnknownHandoffLocally(call.name, hasRegisteredHandoffTool)) {
2222
2273
  directEntries.push(entry);
2223
2274
  }
2224
2275
  else {
2225
2276
  eventEntries.push(entry);
2226
2277
  }
2227
2278
  }
2279
+ if (directEntries.length === 0) {
2280
+ return this.executeViaEvent(filteredCalls, config, input, {
2281
+ batchIndices: eventEntries.map((entry) => entry.batchIndex),
2282
+ turn,
2283
+ batchScopeId,
2284
+ });
2285
+ }
2228
2286
  const directCalls = directEntries.map((e) => e.call);
2229
2287
  const directIndices = directEntries.map((e) => e.batchIndex);
2230
2288
  const eventCalls = eventEntries.map((e) => e.call);