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
6 changes: 6 additions & 0 deletions .changeset/weak-kings-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@langchain/core": patch
"langchain": minor
---

feat(langchain): support for browser tools
2 changes: 1 addition & 1 deletion libs/langchain-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ export abstract class BaseToolkit {
* @template {ToolInputSchemaBase} RunInput The input schema for the tool.
* @template {string} NameT The literal name type for discriminated union support.
*/
interface ToolWrapperParams<
export interface ToolWrapperParams<
RunInput = ToolInputSchemaBase | undefined,
NameT extends string = string,
> extends ToolParams {
Expand Down
2 changes: 1 addition & 1 deletion libs/langchain-core/src/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export type ToolReturnType<TInput, TConfig, TOutput> =
* Base type that establishes the types of input schemas that can be used for LangChain tool
* definitions.
*/
export type ToolInputSchemaBase = z3.ZodTypeAny | JSONSchema;
export type ToolInputSchemaBase = InteropZodType | JSONSchema;

/**
* Parameters for the Tool classes.
Expand Down
2 changes: 1 addition & 1 deletion libs/langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,4 @@
"./package.json": "./package.json"
},
"module": "./dist/index.js"
}
}
25 changes: 25 additions & 0 deletions libs/langchain/src/agents/tests/tools.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod/v3";
import { describe, it, expectTypeOf } from "vitest";
import { tool, type DynamicStructuredTool } from "@langchain/core/tools";

import { tool as headlessTool } from "../../tools/headless.js";
import { createMiddleware } from "../middleware.js";
import { createAgent } from "../index.js";
import type { InferAgentTools } from "../types.js";
Expand Down Expand Up @@ -111,4 +112,28 @@ describe("tools", () => {
>
>();
});

it("should allow to infer tool types from headless tool primitive", () => {
const tool = headlessTool({
name: "test",
description: "Test",
schema: z.object({
message: z.string(),
}),
});

const agent = createAgent({
tools: [tool],
model: "gpt-4",
responseFormat: z.object({
output: z.string(),
}),
});

type AgentTools = InferAgentTools<typeof agent>;

// Verify individual tool types are preserved at specific indices
type FirstTool = AgentTools[0];
expectTypeOf<FirstTool>().toEqualTypeOf<typeof tool>();
});
});
11 changes: 10 additions & 1 deletion libs/langchain/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,23 @@ export { initChatModel } from "./chat_models/universal.js";
* LangChain Tools
*/
export {
tool,
Tool,
type ToolRuntime,
DynamicTool,
StructuredTool,
DynamicStructuredTool,
} from "@langchain/core/tools";

/**
* LangChain tool primitive (supports both normal and headless tools)
*/
export {
tool,
type HeadlessTool,
type HeadlessToolFields,
type HeadlessToolImplementation,
} from "./tools/headless.js";

/**
* LangChain utilities
*/
Expand Down
11 changes: 10 additions & 1 deletion libs/langchain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@ export {
export { initChatModel } from "./chat_models/universal.js";

/**
* LangChain Tools
* LangChain Tools with support for headless tools
*/
export {
tool,
type HeadlessTool,
type HeadlessToolFields,
type HeadlessToolImplementation,
} from "./tools/headless.js";

/**
* LangChain Core Tool Primitives
*/
export {
Tool,
type ToolRuntime,
DynamicTool,
Expand Down
222 changes: 222 additions & 0 deletions libs/langchain/src/tools/headless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* Unified Tool Primitive for LangChain Agents
*
* This module re-exports the `tool` primitive from `@langchain/core/tools` with
* an additional overload: when called without an implementation function, it
* creates a **headless tool** that interrupts agent execution and delegates the
* implementation to the client (e.g. via `useStream({ tools: [...] })`).
*
* @module
*/

import {
tool as coreTool,
DynamicStructuredTool,
type ToolRunnableConfig,
} from "@langchain/core/tools";
import type {
InteropZodObject,
InferInteropZodInput,
InferInteropZodOutput,
} from "@langchain/core/utils/types";

/**
* Configuration fields for creating a headless tool.
*/
export type HeadlessToolFields<
SchemaT extends InteropZodObject,
NameT extends string = string,
> = {
/** The name of the tool. Used by the client to match implementations. */
name: NameT;
/** Description of what the tool does. */
description: string;
/** The Zod schema defining the tool's input. */
schema: SchemaT;
};

/**
* A tool implementation that pairs a headless tool with its execution function.
*
* Created by calling `.implement()` on a {@link HeadlessTool}.
* Pass to `useStream({ tools: [...] })` on the client side.
*/
export type HeadlessToolImplementation<
SchemaT extends InteropZodObject = InteropZodObject,
OutputT = unknown,
NameT extends string = string,
> = {
tool: HeadlessTool<SchemaT, NameT>;
execute: (args: InferInteropZodOutput<SchemaT>) => Promise<OutputT>;
};
Comment on lines +38 to +51

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this need to be its own type?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's consumed by useStream and represents a tool definition with implementation. This is NOT a traditional DynamicStructuredTool as it represents the pairing between a tool definition and its implementation.


/**
* A headless tool that always interrupts agent execution on the server.
*
* The implementation is provided separately on the client via
* `useStream({ tools: [...] })` using `.implement()`.
*/
export type HeadlessTool<

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we should call this ToolDefinition 🙈

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already put it on the v2 wish list. Happy to clean this up when the time is right 😉

SchemaT extends InteropZodObject = InteropZodObject,
NameT extends string = string,
> = DynamicStructuredTool<
SchemaT,
InferInteropZodOutput<SchemaT>,
InferInteropZodInput<SchemaT>,
unknown,
unknown,
NameT
> & {
/**
* Pairs this headless tool with a client-side implementation.
*
* The returned object should be passed to `useStream({ tools: [...] })`.
* The SDK matches the implementation to the tool by name and calls
* `execute` with the typed arguments from the interrupt payload.
*
* @param execute - The function that implements the tool on the client
*/
implement: <OutputT>(
execute: (args: InferInteropZodOutput<SchemaT>) => Promise<OutputT>
) => HeadlessToolImplementation<SchemaT, OutputT, NameT>;
};

function createHeadlessTool<
SchemaT extends InteropZodObject,
NameT extends string,
>(fields: HeadlessToolFields<SchemaT, NameT>): HeadlessTool<SchemaT, NameT> {
const { name, description, schema } = fields;

const wrappedTool = coreTool(
async (
args: InferInteropZodOutput<SchemaT>,
config?: ToolRunnableConfig
) => {
const { interrupt } = await import("@langchain/langgraph");
return interrupt({
type: "tool",
toolCall: {
id: config?.toolCall?.id,
name,
args,
},
});
},
{
name,
description,
schema,
metadata: {
headlessTool: true,
},
}
);

const headlessTool: HeadlessTool<SchemaT, NameT> = Object.assign(
wrappedTool,
{
implement: <OutputT>(
execute: (args: InferInteropZodOutput<SchemaT>) => Promise<OutputT>
): HeadlessToolImplementation<SchemaT, OutputT, NameT> => ({
tool: headlessTool,
execute,
}),
}
) as HeadlessTool<SchemaT, NameT>;

return headlessTool;
}

/**
* The headless overload signature added to the core `tool` function.
*
* When called **without** an implementation function — just `tool({ name, description, schema })` —
* returns a {@link HeadlessTool} that interrupts on every agent invocation.
* The client provides the implementation via `useStream({ tools: [...] })`.
*/
type HeadlessToolOverload = {
<SchemaT extends InteropZodObject, NameT extends string>(
fields: HeadlessToolFields<SchemaT, NameT>
): HeadlessTool<SchemaT, NameT>;
};

/**
* Unified tool primitive for LangChain agents.
*
* Enhances the `tool` function from `@langchain/core/tools` with a headless
* overload: when called **without** an implementation function, the tool
* interrupts agent execution and lets the client supply the implementation.
*
* ---
*
* **Normal tool** — pass an implementation function as the first argument:
*
* ```typescript
* import { tool } from "langchain/tools";
* import { z } from "zod";
*
* const getWeather = tool(
* async ({ city }) => `The weather in ${city} is sunny.`,
* {
* name: "get_weather",
* description: "Get the weather for a city",
* schema: z.object({ city: z.string() }),
* }
* );
* ```
*
* ---
*
* **Headless tool** — omit the implementation; the client provides it later:
*
* ```typescript
* import { tool } from "langchain/tools";
* import { z } from "zod";
*
* // Server: define the tool shape — no implementation needed
* export const getLocation = tool({
* name: "get_location",
* description: "Get the user's current GPS location",
* schema: z.object({
* highAccuracy: z.boolean().optional().describe("Request high accuracy GPS"),
* }),
* });
*
* // Server: register with the agent
* const agent = createAgent({
* model: "openai:gpt-4o",
* tools: [getLocation],
* });
*
* // Client: provide the implementation in useStream
* const stream = useStream({
* assistantId: "agent",
* tools: [
* getLocation.implement(async ({ highAccuracy }) => {
* return new Promise((resolve, reject) => {
* navigator.geolocation.getCurrentPosition(
* (pos) => resolve({
* latitude: pos.coords.latitude,
* longitude: pos.coords.longitude,
* }),
* (err) => reject(new Error(err.message)),
* { enableHighAccuracy: highAccuracy }
* );
* });
* }),
* ],
* });
* ```
*/
export const tool: HeadlessToolOverload & typeof coreTool = ((
funcOrFields: unknown,
fields?: unknown
) => {
if (typeof funcOrFields !== "function") {
return createHeadlessTool(
funcOrFields as HeadlessToolFields<InteropZodObject, string>
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (coreTool as any)(funcOrFields, fields);
}) as HeadlessToolOverload & typeof coreTool;
14 changes: 14 additions & 0 deletions libs/langchain/src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* LangChain Tools
*
* This module provides tool utilities for LangChain agents.
*
* @module
*/

export {
tool,
type HeadlessTool,
type HeadlessToolFields,
type HeadlessToolImplementation,
} from "./headless.js";
Loading
Loading