From ff8341cfc03c64b5d4ae5a02c3359d5ad4aa2963 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 3 Nov 2025 16:17:07 -0800 Subject: [PATCH 1/4] feat(@langchain/core): support of ToolRuntime --- libs/langchain-core/src/tools/index.ts | 100 ++++++++++++++++- .../src/tools/tests/types.test-d.ts | 65 +++++++++++ libs/langchain-core/src/tools/types.ts | 102 ++++++++++++++++++ libs/langchain/src/agents/nodes/ToolNode.ts | 6 ++ libs/langchain/src/agents/tests/tools.test.ts | 72 +++++++++++++ 5 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 libs/langchain-core/src/tools/tests/types.test-d.ts create mode 100644 libs/langchain/src/agents/tests/tools.test.ts diff --git a/libs/langchain-core/src/tools/index.ts b/libs/langchain-core/src/tools/index.ts index 1d9f4e95ed6c..1357548318a9 100644 --- a/libs/langchain-core/src/tools/index.ts +++ b/libs/langchain-core/src/tools/index.ts @@ -55,6 +55,7 @@ import type { StringInputToolSchema, ToolInterface, ToolOutputType, + ToolRuntime, } from "./types.js"; import { type JSONSchema, validatesOnlyStrings } from "../utils/json_schema.js"; @@ -79,6 +80,7 @@ export { isRunnableToolLike, isStructuredTool, isStructuredToolParams, + type ToolRuntime, } from "./types.js"; export { ToolInputParsingException }; @@ -635,6 +637,98 @@ export function tool< >( func: RunnableFunc, fields: ToolWrapperParams +): + | DynamicStructuredTool + | DynamicTool; + +// Overloads with ToolRuntime as CallOptions +export function tool< + SchemaT extends ZodStringV3, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: InferInteropZodOutput, + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams +): DynamicTool; + +export function tool< + SchemaT extends ZodStringV4, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: InferInteropZodOutput, + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams +): DynamicTool; + +export function tool< + SchemaT extends ZodObjectV3, + SchemaOutputT = InferInteropZodOutput, + SchemaInputT = InferInteropZodInput, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: SchemaOutputT, + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams +): DynamicStructuredTool; + +export function tool< + SchemaT extends ZodObjectV4, + SchemaOutputT = InferInteropZodOutput, + SchemaInputT = InferInteropZodInput, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: SchemaOutputT, + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams +): DynamicStructuredTool; + +export function tool< + SchemaT extends JSONSchema, + SchemaOutputT = ToolInputSchemaOutputType, + SchemaInputT = ToolInputSchemaInputType, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: Parameters["func"]>[0], + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams +): DynamicStructuredTool; + +export function tool< + SchemaT extends + | InteropZodObject + | InteropZodType + | JSONSchema = InteropZodObject, + SchemaOutputT = ToolInputSchemaOutputType, + SchemaInputT = ToolInputSchemaInputType, + ToolOutputT = ToolOutputType, + TState = unknown, + TContext = unknown +>( + func: ( + input: SchemaOutputT, + runtime: ToolRuntime + ) => ToolOutputT | Promise, + fields: ToolWrapperParams ): | DynamicStructuredTool | DynamicTool { @@ -659,9 +753,8 @@ export function tool< pickRunnableConfigKeys(childConfig), async () => { try { - // TS doesn't restrict the type here based on the guard above // eslint-disable-next-line @typescript-eslint/no-explicit-any - resolve(func(input as any, childConfig)); + resolve(func(input as any, childConfig as any)); } catch (e) { reject(e); } @@ -704,7 +797,8 @@ export function tool< pickRunnableConfigKeys(childConfig), async () => { try { - const result = await func(input, childConfig); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await func(input as any, childConfig as any); /** * If the signal is aborted, we don't want to resolve the promise diff --git a/libs/langchain-core/src/tools/tests/types.test-d.ts b/libs/langchain-core/src/tools/tests/types.test-d.ts new file mode 100644 index 000000000000..0f1abbb242a1 --- /dev/null +++ b/libs/langchain-core/src/tools/tests/types.test-d.ts @@ -0,0 +1,65 @@ +import { z } from "zod/v3"; +import { describe, it, expectTypeOf } from "vitest"; + +import { tool } from "../index.js"; +import type { ToolRuntime } from "../types.js"; +import type { RunnableConfig } from "../../runnables/config.js"; + +describe("ToolRuntime", () => { + it("allows to define runnable config argument as ToolRuntime", () => { + const stateSchema = z.object({ + userId: z.string(), + }); + const contextSchema = z.object({ + db: z.object({ + foo: z.string(), + }), + }); + + type State = z.infer; + type Context = z.infer; + + tool( + ( + input, + runtime: ToolRuntime + ) => { + expectTypeOf(input).toEqualTypeOf<{ + some: string; + }>(); + expectTypeOf(runtime.state).toEqualTypeOf(); + expectTypeOf(runtime.context).toEqualTypeOf(); + expectTypeOf(runtime.toolCallId).toEqualTypeOf(); + expectTypeOf(runtime.config).toMatchTypeOf(); + return `Hello, ${runtime.state.userId}!`; + }, + { + name: "test", + description: "test", + schema: z.object({ + some: z.string(), + }), + } + ); + + tool( + (input, runtime: ToolRuntime) => { + expectTypeOf(input).toEqualTypeOf<{ + some: string; + }>(); + expectTypeOf(runtime.state).toEqualTypeOf(); + expectTypeOf(runtime.context).toEqualTypeOf(); + expectTypeOf(runtime.toolCallId).toEqualTypeOf(); + expectTypeOf(runtime.config).toMatchTypeOf(); + return `Hello, ${runtime.state.userId}!`; + }, + { + name: "test", + description: "test", + schema: z.object({ + some: z.string(), + }), + } + ); + }); +}); diff --git a/libs/langchain-core/src/tools/types.ts b/libs/langchain-core/src/tools/types.ts index 5eb31e866598..cf219cbd55e4 100644 --- a/libs/langchain-core/src/tools/types.ts +++ b/libs/langchain-core/src/tools/types.ts @@ -21,8 +21,11 @@ import { type InferInteropZodOutput, type InteropZodType, isInteropZodSchema, + type InteropZodObject, } from "../utils/types/zod.js"; + import { JSONSchema } from "../utils/json_schema.js"; +import type { BaseStore } from "../stores.js"; export type ResponseFormat = "content" | "content_and_artifact" | string; @@ -425,3 +428,102 @@ export function isLangChainTool(tool?: unknown): tool is StructuredToolParams { isStructuredTool(tool as any) ); } + +/** + * Runtime context automatically injected into tools. + * + * When a tool function has a parameter named `tool_runtime` with type hint + * `ToolRuntime`, the tool execution system will automatically inject an instance + * containing: + * + * - `state`: The current graph state + * - `toolCallId`: The ID of the current tool call + * - `config`: `RunnableConfig` for the current execution + * - `context`: Runtime context + * - `store`: `BaseStore` instance for persistent storage + * - `writer`: Stream writer for streaming output + * + * No `Annotated` wrapper is needed - just use `runtime: ToolRuntime` + * as a parameter. + * + * @example + * ```typescript + * import { tool, ToolRuntime } from "@langchain/core/tools"; + * import { z } from "zod"; + * + * const stateSchema = z.object({ + * messages: z.array(z.any()), + * userId: z.string().optional(), + * }); + * + * const greet = tool( + * async ({ name }, runtime) => { + * // Access state + * const messages = runtime.state.messages; + * + * // Access tool_call_id + * console.log(`Tool call ID: ${runtime.toolCallId}`); + * + * // Access config + * console.log(`Run ID: ${runtime.config.runId}`); + * + * // Access runtime context + * const userId = runtime.context?.userId; + * + * // Access store + * await runtime.store?.mset([["key", "value"]]); + * + * // Stream output + * runtime.writer?.("Processing..."); + * + * return `Hello! User ID: ${runtime.state.userId || "unknown"} ${name}`; + * }, + * { + * name: "greet", + * description: "Use this to greet the user once you found their info.", + * schema: z.object({ name: z.string() }), + * stateSchema, + * } + * ); + * ``` + * + * @template StateT - The type of the state schema (inferred from stateSchema) + * @template ContextT - The type of the context schema (inferred from contextSchema) + */ +export type ToolRuntime< + TState = unknown, + TContext = unknown +> = RunnableConfig & { + /** + * The current graph state. + */ + state: TState extends InteropZodObject + ? InferInteropZodOutput + : TState extends Record + ? TState + : unknown; + /** + * The ID of the current tool call. + */ + toolCallId: string; + /** + * RunnableConfig for the current execution. + */ + config: ToolRunnableConfig; + /** + * Runtime context (from langgraph `Runtime`). + */ + context: TContext extends InteropZodObject + ? InferInteropZodOutput + : TContext extends Record + ? TContext + : unknown; + /** + * BaseStore instance for persistent storage (from langgraph `Runtime`). + */ + store: BaseStore | null; + /** + * Stream writer for streaming output (from langgraph `Runtime`). + */ + writer: ((chunk: unknown) => void) | null; +}; diff --git a/libs/langchain/src/agents/nodes/ToolNode.ts b/libs/langchain/src/agents/nodes/ToolNode.ts index 43896e7bd0af..c011eee4a9f2 100644 --- a/libs/langchain/src/agents/nodes/ToolNode.ts +++ b/libs/langchain/src/agents/nodes/ToolNode.ts @@ -294,6 +294,12 @@ export class ToolNode< { ...toolCall, type: "tool_call" }, { ...config, + /** + * extend to match ToolRuntime + */ + config, + toolCallId: toolCall.id!, + state: config.configurable?.__pregel_scratchpad?.currentTaskInput, signal: mergeAbortSignals(this.signal, config.signal), } ); diff --git a/libs/langchain/src/agents/tests/tools.test.ts b/libs/langchain/src/agents/tests/tools.test.ts new file mode 100644 index 000000000000..4f87d5b9079e --- /dev/null +++ b/libs/langchain/src/agents/tests/tools.test.ts @@ -0,0 +1,72 @@ +import { z } from "zod/v3"; +import { describe, it, expect } from "vitest"; +import { tool, ToolRuntime } from "@langchain/core/tools"; +import { HumanMessage, ToolMessage } from "@langchain/core/messages"; +import { InMemoryStore } from "@langchain/langgraph"; + +import { createAgent } from "../index.js"; +import { FakeToolCallingModel } from "./utils.js"; + +describe("tools", () => { + it("can access state and context", async () => { + const stateSchema = z.object({ bar: z.string().default("baz") }); + const contextSchema = z.object({ foo: z.string().default("bar") }); + const store = new InMemoryStore(); + + const model = new FakeToolCallingModel({ + toolCalls: [ + [ + { + type: "tool_call", + name: "test", + args: { city: "Tokyo" }, + id: "1", + }, + ], + ], + }); + + const getWeather = tool( + async ( + input, + runtime: ToolRuntime + ) => { + expect(runtime.state).toEqual(expect.objectContaining({ bar: "baz" })); + expect(runtime.context).toEqual({ foo: "bar" }); + expect(runtime.toolCallId).toEqual("1"); + expect(runtime.config.recursionLimit).toEqual(25); + expect(typeof runtime.writer).toBe("function"); + expect(runtime.store?.constructor.name).toEqual("AsyncBatchedStore"); + return `The weather in ${input.city} is sunny. The foo is ${runtime.context.foo} and the bar is ${runtime.state.bar}.`; + }, + { + name: "test", + description: "test", + schema: z.object({ city: z.string() }), + } + ); + + const agent = createAgent({ + model, + tools: [getWeather], + store, + contextSchema: z.object({ foo: z.string().default("bar") }), + stateSchema: z.object({ bar: z.string().default("baz") }), + }); + + const result = await agent.invoke( + { + messages: [new HumanMessage("What is the weather in Tokyo?")], + bar: "baz", + }, + { + context: { foo: "bar" }, + } + ); + + expect(ToolMessage.isInstance(result.messages.at(-1))).toBe(true); + expect(result.messages.at(-1)?.content).toBe( + "The weather in Tokyo is sunny. The foo is bar and the bar is baz." + ); + }); +}); From a4f66604f1f0b39769fcfc7356846539bc7d847f Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 5 Nov 2025 17:43:12 -0800 Subject: [PATCH 2/4] cr --- libs/langchain-core/src/tools/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langchain-core/src/tools/types.ts b/libs/langchain-core/src/tools/types.ts index cf219cbd55e4..437fbb3137ce 100644 --- a/libs/langchain-core/src/tools/types.ts +++ b/libs/langchain-core/src/tools/types.ts @@ -493,7 +493,7 @@ export function isLangChainTool(tool?: unknown): tool is StructuredToolParams { export type ToolRuntime< TState = unknown, TContext = unknown -> = RunnableConfig & { +> = ToolRunnableConfig & { /** * The current graph state. */ From ae3878369b145a0bdeb8f80b3c061c22420ea0fc Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 5 Nov 2025 17:56:51 -0800 Subject: [PATCH 3/4] cr --- libs/langchain-core/src/tools/types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/langchain-core/src/tools/types.ts b/libs/langchain-core/src/tools/types.ts index 437fbb3137ce..87a0cd492c1b 100644 --- a/libs/langchain-core/src/tools/types.ts +++ b/libs/langchain-core/src/tools/types.ts @@ -493,7 +493,7 @@ export function isLangChainTool(tool?: unknown): tool is StructuredToolParams { export type ToolRuntime< TState = unknown, TContext = unknown -> = ToolRunnableConfig & { +> = RunnableConfig & { /** * The current graph state. */ @@ -506,6 +506,10 @@ export type ToolRuntime< * The ID of the current tool call. */ toolCallId: string; + /** + * The current tool call. + */ + toolCall?: ToolCall; /** * RunnableConfig for the current execution. */ From 865079ad865626eedf1e44259ac37c10c128ce2a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 5 Nov 2025 18:06:07 -0800 Subject: [PATCH 4/4] Create changeset for ToolRuntime feature Add changeset for ToolRuntime support in langchain. --- .changeset/large-onions-attack.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/large-onions-attack.md diff --git a/.changeset/large-onions-attack.md b/.changeset/large-onions-attack.md new file mode 100644 index 000000000000..9b401dd0eedd --- /dev/null +++ b/.changeset/large-onions-attack.md @@ -0,0 +1,6 @@ +--- +"@langchain/core": patch +"langchain": patch +--- + +feat(@langchain/core): support of ToolRuntime