diff --git a/libs/langchain-core/src/tools/index.ts b/libs/langchain-core/src/tools/index.ts index 43d2b9d94de6..a6845cf1b82e 100644 --- a/libs/langchain-core/src/tools/index.ts +++ b/libs/langchain-core/src/tools/index.ts @@ -389,12 +389,15 @@ export class DynamicTool< func: DynamicToolInput["func"]; + providerToolDefinition?: Record; + constructor(fields: DynamicToolInput) { super(fields); this.name = fields.name; this.description = fields.description; this.func = fields.func; this.returnDirect = fields.returnDirect ?? this.returnDirect; + this.providerToolDefinition = fields.providerToolDefinition; } /** @@ -453,6 +456,8 @@ export class DynamicStructuredTool< schema: SchemaT; + providerToolDefinition?: Record; + constructor( fields: DynamicStructuredToolInput ) { @@ -462,6 +467,7 @@ export class DynamicStructuredTool< this.func = fields.func; this.returnDirect = fields.returnDirect ?? this.returnDirect; this.schema = fields.schema; + this.providerToolDefinition = fields.providerToolDefinition; } /** @@ -552,6 +558,33 @@ interface ToolWrapperParams * an agent should stop looping. */ returnDirect?: boolean; + /** + * Provider-specific tool definition to override the tool definition sent to the provider. + * + * This allows you to define a tool with client-side execution while using provider-specific + * built-in tool formats. For example, with Anthropic's memory tool: + * + * ```ts + * tool( + * ({ content }, config) => { ... }, + * { + * name: "memory", + * description: "Store or retrieve information", + * schema: z.object({ content: z.string() }), + * providerToolDefinition: { + * type: "memory_20250818", + * name: "memory", + * }, + * } + * ); + * ``` + * + * When this field is present: + * - The tool will be treated as a client tool that requires local execution + * - The provider-specific definition will be sent to the API instead of auto-generated definition + * - Your handler function will be called when the model uses the tool + */ + providerToolDefinition?: Record; } /** diff --git a/libs/langchain-core/src/tools/tests/tools.test.ts b/libs/langchain-core/src/tools/tests/tools.test.ts index 96eb5ca62e04..10919f178935 100644 --- a/libs/langchain-core/src/tools/tests/tools.test.ts +++ b/libs/langchain-core/src/tools/tests/tools.test.ts @@ -180,6 +180,38 @@ test("Does not double wrap a returned tool message even if a tool call with id i expect(toolResult.name).toBe("baz"); }); +test("tool retains providerToolDefinition metadata when provided", () => { + const providerDefinition = { type: "memory_20250818", name: "memory" }; + + const memoryTool = tool( + (input: string) => input, + { + name: "memory", + providerToolDefinition: providerDefinition, + } + ); + + expect(memoryTool.providerToolDefinition).toBe(providerDefinition); + + const structuredProviderDefinition = { + type: "memory_structured_20250818", + name: "memory_structured", + }; + + const structuredTool = tool( + (input: { content: string }) => input.content, + { + name: "memory_structured", + schema: z.object({ content: z.string() }), + providerToolDefinition: structuredProviderDefinition, + } + ); + + expect(structuredTool.providerToolDefinition).toBe( + structuredProviderDefinition + ); +}); + test("Tool can accept single string input", async () => { const toolCall = { id: "testid", diff --git a/libs/langchain-core/src/tools/types.ts b/libs/langchain-core/src/tools/types.ts index 5eb31e866598..ecf31c9ed06d 100644 --- a/libs/langchain-core/src/tools/types.ts +++ b/libs/langchain-core/src/tools/types.ts @@ -311,6 +311,33 @@ export interface BaseDynamicToolInput extends ToolParams { * an agent should stop looping. */ returnDirect?: boolean; + /** + * Provider-specific tool definition to override the tool definition sent to the provider. + * + * This allows you to define a tool with client-side execution while using provider-specific + * built-in tool formats. For example, with Anthropic's memory tool: + * + * ```ts + * tool( + * ({ content }, config) => { ... }, + * { + * name: "memory", + * description: "Store or retrieve information", + * schema: z.object({ content: z.string() }), + * providerToolDefinition: { + * type: "memory_20250818", + * name: "memory", + * }, + * } + * ); + * ``` + * + * When this field is present: + * - The tool will be treated as a client tool that requires local execution + * - The provider-specific definition will be sent to the API instead of auto-generated definition + * - Your handler function will be called when the model uses the tool + */ + providerToolDefinition?: Record; } /** diff --git a/libs/providers/langchain-anthropic/src/chat_models.ts b/libs/providers/langchain-anthropic/src/chat_models.ts index d413cb7661e4..7033bb484118 100644 --- a/libs/providers/langchain-anthropic/src/chat_models.ts +++ b/libs/providers/langchain-anthropic/src/chat_models.ts @@ -796,6 +796,11 @@ export class ChatAnthropicMessages< return undefined; } return tools.map((tool) => { + // Check if tool has providerToolDefinition FIRST (built-in client tools) + // This must come before isBuiltinTool check because LangChain tools are not plain objects + if (isLangChainTool(tool) && tool.providerToolDefinition) { + return tool.providerToolDefinition as Anthropic.Messages.ToolUnion; + } if (isBuiltinTool(tool)) { return tool; } diff --git a/libs/providers/langchain-anthropic/src/tests/chat_models-tools.int.test.ts b/libs/providers/langchain-anthropic/src/tests/chat_models-tools.int.test.ts index d09e7a0128e6..1899703906db 100644 --- a/libs/providers/langchain-anthropic/src/tests/chat_models-tools.int.test.ts +++ b/libs/providers/langchain-anthropic/src/tests/chat_models-tools.int.test.ts @@ -260,7 +260,7 @@ test("Can bind & stream AnthropicTools", async () => { if (!Array.isArray(finalMessage.content)) { throw new Error("Content is not an array"); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const toolCall = finalMessage.tool_calls?.[0]; if (toolCall === undefined) { throw new Error("No tool call found"); @@ -869,6 +869,42 @@ test("calling tool with no args should work", async () => { // }); // https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool +test("memory tool via LangChain provider definition", async () => { + const llm = new ChatAnthropic({ + model: "claude-sonnet-4-5-20250929", + temperature: 0, + clientOptions: { + defaultHeaders: { + "anthropic-beta": "context-management-2025-06-27", + }, + }, + }); + + const memoryTool = tool(({ content }: { content: string }) => content, { + name: "memory", + description: "Anthropic memory tool (client-side)", + schema: z.object({ content: z.string() }), + providerToolDefinition: { + type: "memory_20250818", + name: "memory", + }, + }); + + const llmWithTools = llm.bindTools([memoryTool]); + const response = await llmWithTools.invoke( + "Please remember that I enjoy hiking in the mountains." + ); + expect(response).toBeInstanceOf(AIMessage); + expect(response.tool_calls).toBeDefined(); + expect(response.tool_calls?.[0].name).toBe("memory"); + + // Memory tool always views existing memories before writing new ones + expect(response.tool_calls?.[0].args).toEqual({ + command: "view", + path: "/memories", + }); +}, 60000); + test("memory tool", async () => { const llm = new ChatAnthropic({ model: "claude-sonnet-4-5-20250929", diff --git a/libs/providers/langchain-anthropic/src/tests/chat_models.test.ts b/libs/providers/langchain-anthropic/src/tests/chat_models.test.ts index 17c59a642716..29d6cdf19093 100644 --- a/libs/providers/langchain-anthropic/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-anthropic/src/tests/chat_models.test.ts @@ -7,6 +7,7 @@ import { } from "@langchain/core/messages"; import { z } from "zod"; import { OutputParserException } from "@langchain/core/output_parsers"; +import { tool } from "@langchain/core/tools"; import { ChatAnthropic } from "../chat_models.js"; import { _convertMessagesToAnthropicPayload } from "../utils/message_inputs.js"; @@ -111,6 +112,32 @@ test("withStructuredOutput with proper output", async () => { }); }); +test("formatStructuredToolToAnthropic forwards provider-specific tool definition", () => { + const model = new ChatAnthropic({ + modelName: "claude-3-haiku-20240307", + temperature: 0, + anthropicApiKey: "testing", + }); + + const providerDefinition = { + type: "memory_20250818", + name: "memory", + }; + + const memoryTool = tool( + ({ content }: { content: string }) => content, + { + name: "memory", + schema: z.object({ content: z.string() }), + providerToolDefinition: providerDefinition, + } + ); + + const formattedTools = model.formatStructuredToolToAnthropic([memoryTool]); + + expect(formattedTools).toEqual([providerDefinition]); +}); + test("Can properly format anthropic messages when given two tool results", async () => { const messageHistory = [ new HumanMessage("What is the weather in SF? Also, what is 2 + 2?"),