Skip to content
Merged
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/large-onions-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@langchain/core": patch
"langchain": patch
---

feat(@langchain/core): support of ToolRuntime
100 changes: 97 additions & 3 deletions libs/langchain-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import type {
StringInputToolSchema,
ToolInterface,
ToolOutputType,
ToolRuntime,
} from "./types.js";
import { type JSONSchema, validatesOnlyStrings } from "../utils/json_schema.js";

Expand All @@ -79,6 +80,7 @@ export {
isRunnableToolLike,
isStructuredTool,
isStructuredToolParams,
type ToolRuntime,
} from "./types.js";

export { ToolInputParsingException };
Expand Down Expand Up @@ -635,6 +637,98 @@ export function tool<
>(
func: RunnableFunc<SchemaOutputT, ToolOutputT, ToolRunnableConfig>,
fields: ToolWrapperParams<SchemaT>
):
| DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
| DynamicTool<ToolOutputT>;

// Overloads with ToolRuntime as CallOptions
export function tool<
Copy link
Member

Choose a reason for hiding this comment

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

what's the purpose of these overloads if TState + TContext is only used in the runtime arg? Would say something different if we were recommending the usage of this should be by passing in TState + TContext as generic args, but I don't think that's what we want either. E.g. this should be possible w/o overloads?

const myTool = tool((state, runtime: ToolRuntime<...>) => {}, {})

Copy link
Member Author

Choose a reason for hiding this comment

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

This are unfortunately required because the runtime type is otherwise defined as:

  options:
    | CallOptions
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    | Record<string, any>
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    | (Record<string, any> & CallOptions)
) => RunOutput | Promise<RunOutput>;

which fails because of:

Type 'Record<string, any>' is missing the following properties from type '{ state: { userId: string; }; toolCallId: string; config: ToolRunnableConfig<Record<string, any>, any>; context: { db: { foo: string; }; }; store: BaseStore<string, unknown> | null; writer: ((chunk: unknown) => void) | null; }': state, toolCallId, config, context, and 2 more.ts(2769)

SchemaT extends ZodStringV3,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: InferInteropZodOutput<SchemaT>,
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
): DynamicTool<ToolOutputT>;

export function tool<
SchemaT extends ZodStringV4,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: InferInteropZodOutput<SchemaT>,
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
): DynamicTool<ToolOutputT>;

export function tool<
SchemaT extends ZodObjectV3,
SchemaOutputT = InferInteropZodOutput<SchemaT>,
SchemaInputT = InferInteropZodInput<SchemaT>,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: SchemaOutputT,
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;

export function tool<
SchemaT extends ZodObjectV4,
SchemaOutputT = InferInteropZodOutput<SchemaT>,
SchemaInputT = InferInteropZodInput<SchemaT>,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: SchemaOutputT,
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;

export function tool<
SchemaT extends JSONSchema,
SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
SchemaInputT = ToolInputSchemaInputType<SchemaT>,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: Parameters<DynamicStructuredToolInput<SchemaT>["func"]>[0],
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;

export function tool<
SchemaT extends
| InteropZodObject
| InteropZodType<string>
| JSONSchema = InteropZodObject,
SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
SchemaInputT = ToolInputSchemaInputType<SchemaT>,
ToolOutputT = ToolOutputType,
TState = unknown,
TContext = unknown
>(
func: (
input: SchemaOutputT,
runtime: ToolRuntime<TState, TContext>
) => ToolOutputT | Promise<ToolOutputT>,
fields: ToolWrapperParams<SchemaT>
):
| DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
| DynamicTool<ToolOutputT> {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions libs/langchain-core/src/tools/tests/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<typeof stateSchema>;
type Context = z.infer<typeof contextSchema>;

tool(
(
input,
runtime: ToolRuntime<typeof stateSchema, typeof contextSchema>
) => {
expectTypeOf(input).toEqualTypeOf<{
some: string;
}>();
expectTypeOf(runtime.state).toEqualTypeOf<State>();
expectTypeOf(runtime.context).toEqualTypeOf<Context>();
expectTypeOf(runtime.toolCallId).toEqualTypeOf<string>();
expectTypeOf(runtime.config).toMatchTypeOf<RunnableConfig>();
return `Hello, ${runtime.state.userId}!`;
},
{
name: "test",
description: "test",
schema: z.object({
some: z.string(),
}),
}
);

tool(
(input, runtime: ToolRuntime<State, Context>) => {
expectTypeOf(input).toEqualTypeOf<{
some: string;
}>();
expectTypeOf(runtime.state).toEqualTypeOf<State>();
expectTypeOf(runtime.context).toEqualTypeOf<Context>();
expectTypeOf(runtime.toolCallId).toEqualTypeOf<string>();
expectTypeOf(runtime.config).toMatchTypeOf<RunnableConfig>();
return `Hello, ${runtime.state.userId}!`;
},
{
name: "test",
description: "test",
schema: z.object({
some: z.string(),
}),
}
);
});
});
106 changes: 106 additions & 0 deletions libs/langchain-core/src/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -425,3 +428,106 @@ 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>
: TState extends Record<string, unknown>
? TState
: unknown;
/**
* The ID of the current tool call.
*/
toolCallId: string;
/**
* The current tool call.
*/
toolCall?: ToolCall;
/**
* RunnableConfig for the current execution.
*/
config: ToolRunnableConfig;
/**
* Runtime context (from langgraph `Runtime`).
*/
context: TContext extends InteropZodObject
? InferInteropZodOutput<TContext>
: TContext extends Record<string, unknown>
? TContext
: unknown;
/**
* BaseStore instance for persistent storage (from langgraph `Runtime`).
*/
store: BaseStore<string, unknown> | null;
/**
* Stream writer for streaming output (from langgraph `Runtime`).
*/
writer: ((chunk: unknown) => void) | null;
};
6 changes: 6 additions & 0 deletions libs/langchain/src/agents/nodes/ToolNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
);
Expand Down
Loading
Loading