Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions libs/langchain-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,12 +389,15 @@ export class DynamicTool<

func: DynamicToolInput<ToolOutputT>["func"];

providerToolDefinition?: Record<string, unknown>;

constructor(fields: DynamicToolInput<ToolOutputT>) {
super(fields);
this.name = fields.name;
this.description = fields.description;
this.func = fields.func;
this.returnDirect = fields.returnDirect ?? this.returnDirect;
this.providerToolDefinition = fields.providerToolDefinition;
}

/**
Expand Down Expand Up @@ -453,6 +456,8 @@ export class DynamicStructuredTool<

schema: SchemaT;

providerToolDefinition?: Record<string, unknown>;

constructor(
fields: DynamicStructuredToolInput<SchemaT, SchemaOutputT, ToolOutputT>
) {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -552,6 +558,33 @@ interface ToolWrapperParams<RunInput = ToolInputSchemaBase | undefined>
* 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<string, unknown>;
}

/**
Expand Down
32 changes: 32 additions & 0 deletions libs/langchain-core/src/tools/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions libs/langchain-core/src/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions libs/providers/langchain-anthropic/src/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions libs/providers/langchain-anthropic/src/tests/chat_models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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?"),
Expand Down