Skip to content

Commit ff8341c

Browse files
feat(@langchain/core): support of ToolRuntime
1 parent 58e664c commit ff8341c

File tree

5 files changed

+342
-3
lines changed

5 files changed

+342
-3
lines changed

libs/langchain-core/src/tools/index.ts

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
StringInputToolSchema,
5656
ToolInterface,
5757
ToolOutputType,
58+
ToolRuntime,
5859
} from "./types.js";
5960
import { type JSONSchema, validatesOnlyStrings } from "../utils/json_schema.js";
6061

@@ -79,6 +80,7 @@ export {
7980
isRunnableToolLike,
8081
isStructuredTool,
8182
isStructuredToolParams,
83+
type ToolRuntime,
8284
} from "./types.js";
8385

8486
export { ToolInputParsingException };
@@ -635,6 +637,98 @@ export function tool<
635637
>(
636638
func: RunnableFunc<SchemaOutputT, ToolOutputT, ToolRunnableConfig>,
637639
fields: ToolWrapperParams<SchemaT>
640+
):
641+
| DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
642+
| DynamicTool<ToolOutputT>;
643+
644+
// Overloads with ToolRuntime as CallOptions
645+
export function tool<
646+
SchemaT extends ZodStringV3,
647+
ToolOutputT = ToolOutputType,
648+
TState = unknown,
649+
TContext = unknown
650+
>(
651+
func: (
652+
input: InferInteropZodOutput<SchemaT>,
653+
runtime: ToolRuntime<TState, TContext>
654+
) => ToolOutputT | Promise<ToolOutputT>,
655+
fields: ToolWrapperParams<SchemaT>
656+
): DynamicTool<ToolOutputT>;
657+
658+
export function tool<
659+
SchemaT extends ZodStringV4,
660+
ToolOutputT = ToolOutputType,
661+
TState = unknown,
662+
TContext = unknown
663+
>(
664+
func: (
665+
input: InferInteropZodOutput<SchemaT>,
666+
runtime: ToolRuntime<TState, TContext>
667+
) => ToolOutputT | Promise<ToolOutputT>,
668+
fields: ToolWrapperParams<SchemaT>
669+
): DynamicTool<ToolOutputT>;
670+
671+
export function tool<
672+
SchemaT extends ZodObjectV3,
673+
SchemaOutputT = InferInteropZodOutput<SchemaT>,
674+
SchemaInputT = InferInteropZodInput<SchemaT>,
675+
ToolOutputT = ToolOutputType,
676+
TState = unknown,
677+
TContext = unknown
678+
>(
679+
func: (
680+
input: SchemaOutputT,
681+
runtime: ToolRuntime<TState, TContext>
682+
) => ToolOutputT | Promise<ToolOutputT>,
683+
fields: ToolWrapperParams<SchemaT>
684+
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;
685+
686+
export function tool<
687+
SchemaT extends ZodObjectV4,
688+
SchemaOutputT = InferInteropZodOutput<SchemaT>,
689+
SchemaInputT = InferInteropZodInput<SchemaT>,
690+
ToolOutputT = ToolOutputType,
691+
TState = unknown,
692+
TContext = unknown
693+
>(
694+
func: (
695+
input: SchemaOutputT,
696+
runtime: ToolRuntime<TState, TContext>
697+
) => ToolOutputT | Promise<ToolOutputT>,
698+
fields: ToolWrapperParams<SchemaT>
699+
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;
700+
701+
export function tool<
702+
SchemaT extends JSONSchema,
703+
SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
704+
SchemaInputT = ToolInputSchemaInputType<SchemaT>,
705+
ToolOutputT = ToolOutputType,
706+
TState = unknown,
707+
TContext = unknown
708+
>(
709+
func: (
710+
input: Parameters<DynamicStructuredToolInput<SchemaT>["func"]>[0],
711+
runtime: ToolRuntime<TState, TContext>
712+
) => ToolOutputT | Promise<ToolOutputT>,
713+
fields: ToolWrapperParams<SchemaT>
714+
): DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>;
715+
716+
export function tool<
717+
SchemaT extends
718+
| InteropZodObject
719+
| InteropZodType<string>
720+
| JSONSchema = InteropZodObject,
721+
SchemaOutputT = ToolInputSchemaOutputType<SchemaT>,
722+
SchemaInputT = ToolInputSchemaInputType<SchemaT>,
723+
ToolOutputT = ToolOutputType,
724+
TState = unknown,
725+
TContext = unknown
726+
>(
727+
func: (
728+
input: SchemaOutputT,
729+
runtime: ToolRuntime<TState, TContext>
730+
) => ToolOutputT | Promise<ToolOutputT>,
731+
fields: ToolWrapperParams<SchemaT>
638732
):
639733
| DynamicStructuredTool<SchemaT, SchemaOutputT, SchemaInputT, ToolOutputT>
640734
| DynamicTool<ToolOutputT> {
@@ -659,9 +753,8 @@ export function tool<
659753
pickRunnableConfigKeys(childConfig),
660754
async () => {
661755
try {
662-
// TS doesn't restrict the type here based on the guard above
663756
// eslint-disable-next-line @typescript-eslint/no-explicit-any
664-
resolve(func(input as any, childConfig));
757+
resolve(func(input as any, childConfig as any));
665758
} catch (e) {
666759
reject(e);
667760
}
@@ -704,7 +797,8 @@ export function tool<
704797
pickRunnableConfigKeys(childConfig),
705798
async () => {
706799
try {
707-
const result = await func(input, childConfig);
800+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
801+
const result = await func(input as any, childConfig as any);
708802

709803
/**
710804
* If the signal is aborted, we don't want to resolve the promise
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { z } from "zod/v3";
2+
import { describe, it, expectTypeOf } from "vitest";
3+
4+
import { tool } from "../index.js";
5+
import type { ToolRuntime } from "../types.js";
6+
import type { RunnableConfig } from "../../runnables/config.js";
7+
8+
describe("ToolRuntime", () => {
9+
it("allows to define runnable config argument as ToolRuntime", () => {
10+
const stateSchema = z.object({
11+
userId: z.string(),
12+
});
13+
const contextSchema = z.object({
14+
db: z.object({
15+
foo: z.string(),
16+
}),
17+
});
18+
19+
type State = z.infer<typeof stateSchema>;
20+
type Context = z.infer<typeof contextSchema>;
21+
22+
tool(
23+
(
24+
input,
25+
runtime: ToolRuntime<typeof stateSchema, typeof contextSchema>
26+
) => {
27+
expectTypeOf(input).toEqualTypeOf<{
28+
some: string;
29+
}>();
30+
expectTypeOf(runtime.state).toEqualTypeOf<State>();
31+
expectTypeOf(runtime.context).toEqualTypeOf<Context>();
32+
expectTypeOf(runtime.toolCallId).toEqualTypeOf<string>();
33+
expectTypeOf(runtime.config).toMatchTypeOf<RunnableConfig>();
34+
return `Hello, ${runtime.state.userId}!`;
35+
},
36+
{
37+
name: "test",
38+
description: "test",
39+
schema: z.object({
40+
some: z.string(),
41+
}),
42+
}
43+
);
44+
45+
tool(
46+
(input, runtime: ToolRuntime<State, Context>) => {
47+
expectTypeOf(input).toEqualTypeOf<{
48+
some: string;
49+
}>();
50+
expectTypeOf(runtime.state).toEqualTypeOf<State>();
51+
expectTypeOf(runtime.context).toEqualTypeOf<Context>();
52+
expectTypeOf(runtime.toolCallId).toEqualTypeOf<string>();
53+
expectTypeOf(runtime.config).toMatchTypeOf<RunnableConfig>();
54+
return `Hello, ${runtime.state.userId}!`;
55+
},
56+
{
57+
name: "test",
58+
description: "test",
59+
schema: z.object({
60+
some: z.string(),
61+
}),
62+
}
63+
);
64+
});
65+
});

libs/langchain-core/src/tools/types.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import {
2121
type InferInteropZodOutput,
2222
type InteropZodType,
2323
isInteropZodSchema,
24+
type InteropZodObject,
2425
} from "../utils/types/zod.js";
26+
2527
import { JSONSchema } from "../utils/json_schema.js";
28+
import type { BaseStore } from "../stores.js";
2629

2730
export type ResponseFormat = "content" | "content_and_artifact" | string;
2831

@@ -425,3 +428,102 @@ export function isLangChainTool(tool?: unknown): tool is StructuredToolParams {
425428
isStructuredTool(tool as any)
426429
);
427430
}
431+
432+
/**
433+
* Runtime context automatically injected into tools.
434+
*
435+
* When a tool function has a parameter named `tool_runtime` with type hint
436+
* `ToolRuntime`, the tool execution system will automatically inject an instance
437+
* containing:
438+
*
439+
* - `state`: The current graph state
440+
* - `toolCallId`: The ID of the current tool call
441+
* - `config`: `RunnableConfig` for the current execution
442+
* - `context`: Runtime context
443+
* - `store`: `BaseStore` instance for persistent storage
444+
* - `writer`: Stream writer for streaming output
445+
*
446+
* No `Annotated` wrapper is needed - just use `runtime: ToolRuntime`
447+
* as a parameter.
448+
*
449+
* @example
450+
* ```typescript
451+
* import { tool, ToolRuntime } from "@langchain/core/tools";
452+
* import { z } from "zod";
453+
*
454+
* const stateSchema = z.object({
455+
* messages: z.array(z.any()),
456+
* userId: z.string().optional(),
457+
* });
458+
*
459+
* const greet = tool(
460+
* async ({ name }, runtime) => {
461+
* // Access state
462+
* const messages = runtime.state.messages;
463+
*
464+
* // Access tool_call_id
465+
* console.log(`Tool call ID: ${runtime.toolCallId}`);
466+
*
467+
* // Access config
468+
* console.log(`Run ID: ${runtime.config.runId}`);
469+
*
470+
* // Access runtime context
471+
* const userId = runtime.context?.userId;
472+
*
473+
* // Access store
474+
* await runtime.store?.mset([["key", "value"]]);
475+
*
476+
* // Stream output
477+
* runtime.writer?.("Processing...");
478+
*
479+
* return `Hello! User ID: ${runtime.state.userId || "unknown"} ${name}`;
480+
* },
481+
* {
482+
* name: "greet",
483+
* description: "Use this to greet the user once you found their info.",
484+
* schema: z.object({ name: z.string() }),
485+
* stateSchema,
486+
* }
487+
* );
488+
* ```
489+
*
490+
* @template StateT - The type of the state schema (inferred from stateSchema)
491+
* @template ContextT - The type of the context schema (inferred from contextSchema)
492+
*/
493+
export type ToolRuntime<
494+
TState = unknown,
495+
TContext = unknown
496+
> = RunnableConfig & {
497+
/**
498+
* The current graph state.
499+
*/
500+
state: TState extends InteropZodObject
501+
? InferInteropZodOutput<TState>
502+
: TState extends Record<string, unknown>
503+
? TState
504+
: unknown;
505+
/**
506+
* The ID of the current tool call.
507+
*/
508+
toolCallId: string;
509+
/**
510+
* RunnableConfig for the current execution.
511+
*/
512+
config: ToolRunnableConfig;
513+
/**
514+
* Runtime context (from langgraph `Runtime`).
515+
*/
516+
context: TContext extends InteropZodObject
517+
? InferInteropZodOutput<TContext>
518+
: TContext extends Record<string, unknown>
519+
? TContext
520+
: unknown;
521+
/**
522+
* BaseStore instance for persistent storage (from langgraph `Runtime`).
523+
*/
524+
store: BaseStore<string, unknown> | null;
525+
/**
526+
* Stream writer for streaming output (from langgraph `Runtime`).
527+
*/
528+
writer: ((chunk: unknown) => void) | null;
529+
};

libs/langchain/src/agents/nodes/ToolNode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ export class ToolNode<
294294
{ ...toolCall, type: "tool_call" },
295295
{
296296
...config,
297+
/**
298+
* extend to match ToolRuntime
299+
*/
300+
config,
301+
toolCallId: toolCall.id!,
302+
state: config.configurable?.__pregel_scratchpad?.currentTaskInput,
297303
signal: mergeAbortSignals(this.signal, config.signal),
298304
}
299305
);

0 commit comments

Comments
 (0)