Skip to content

Conversation

@christian-bromann
Copy link
Member

@christian-bromann christian-bromann commented Oct 30, 2025

Historically LangChain tools don't have a concept of state and context. This is only required when used within LangGraph or with createAgent. This PR attempts to make tools state and context aware. It adds support for automatically injecting runtime context into tool functions when stateSchema or contextSchema are provided. This enables tools to access graph state, runtime context, store, and other runtime information without requiring manual parameter passing.

Trade offs / Alternatives

I think this is the best approach given that the tools function has already several overloads. What I don't like here is that the 2nd argument completely changes now but I think this is fine given type support.

Alternatively I was considering implementing an agenttool function that provides desired / simpler typing of this but it seems like to far off what we do in Python.

Motivation

Currently, tools can only access their input parameters and configuration. However, in many agent workflows, tools need access to:

  • The current graph state (e.g., to read previous messages or state)
  • Runtime context (e.g., user ID, session data)
  • Persistent storage (store)
  • Stream writers for real-time output

This change aligns the TypeScript implementation with the Python ToolRuntime feature, providing a consistent developer experience across both languages.

Usage Example

import { createAgent, tool } from "langchain";
import { z } from "zod";

// Define context schema
const contextSchema = z.object({
  userId: z.string().default("Susanne"),
});

// Tool with contextSchema - runtime is automatically injected
const greet = tool(
  async ({ name }, runtime) => {
    console.log("Runtime state:", runtime.state);
    console.log("Runtime context:", runtime.context);
    console.log("Tool call ID:", runtime.toolCallId);
    console.log("Config:", runtime.config);

    // Access context
    const userId = runtime.context?.userId;

    // Access store
    await runtime.store?.mset([["key", "value"]]);

    // Stream output
    runtime.writer?.("Processing...");

    return `Hello! User ID: ${runtime.context?.userId || "unknown"} ${name}`;
  },
  {
    name: "greet",
    description: "Use this to greet the user once you found their info.",
    schema: z.object({ name: z.string() }),
    contextSchema, // This enables runtime injection
  }
);

const agent = createAgent({
  model: "openai:gpt-4",
  tools: [greet],
  contextSchema,
});

const result = await agent.invoke(
  {
    messages: [{ role: "user", content: "greet the user named Susanne" }],
  },
  {
    context: {
      userId: "Susanne",
    },
  }
);

Todo

I wanted to get some feedback on this before continuing. Missing pieces are:

  • type and unit tests
  • docs update
  • update code examples where needed

Backwards Compatibility

Fully backwards compatible - Existing tools without stateSchema or contextSchema continue to work exactly as before. The runtime parameter is only added when schemas are explicitly provided.

@changeset-bot
Copy link

changeset-bot bot commented Oct 30, 2025

⚠️ No Changeset found

Latest commit: c8fc585

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Member

@hntrl hntrl left a comment

Choose a reason for hiding this comment

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

Few thoughts:

  • for what it's worth, idk if it's totally necessary that we have an addressable ToolRuntime type like python does considering the problem space is so different. Important part is that we can get context about the agent loop from within a tool
  • First gut reaction to introducing a conditional type on tools is it seems like a band aid

Some ideas that I had:

  • Always pass the context to the tool parameters, but only make it type safe if you explicitly cast the second arg to be a type containing the context info
  • introduce a utility similar to getCurrentTaskInput that looks at hidden parameters on call options (e.g. getAgentContext or something like that)

@christian-bromann
Copy link
Member Author

christian-bromann commented Oct 30, 2025

  • Always pass the context to the tool parameters, but only make it type safe if you explicitly cast the second arg to be a type containing the context info

That may work for context (even though IMHO worst DX given you have to do an additional import of RunnableConfig), second: this won't help us to get access to state.

  • introduce a utility similar to getCurrentTaskInput that looks at hidden parameters on call options (e.g. getAgentContext or something like that)

IMHO this doesn't feel like clean DX, bc it would require to import this function from somewhere and it also wouldn't be clear what the function does, except of "magically" giving you the value from somewhere.

  • First gut reaction to introducing a conditional type on tools is it seems like a band aid

I would argue the other way: introducing things like createAgentContext or using getCurrentTaskInput (which is a LangGraph primitive) feel more like a band aid to me. Originally tools were designed without state and context in mind. My DX goal is to make tools aware of these concepts without making user to import additional functions or types. Hence the best solution to this is to extend the ToolWrapperParams to take customState and customContext which aligns with the interface we use in createAgent. This is all we need to make tools type safe by default and keep the compatibility to Python close.

@hntrl
Copy link
Member

hntrl commented Oct 30, 2025

I think the thing I'm more concerned about is how this would be leaking concepts from createAgent into tools which is meant to be ubiquitous across a number of things. Maybe we're fine doing that, but my gut reaction is that bloating the API for tools in pursuit of nice DX for createAgent is awkward.

Some other Q's while I'm thinking about it:

  • What happens if the context schema is different from the context schema of the agent?
  • If a tool gets called without the accompanying context schema, are we just throwing with an error?

@christian-bromann
Copy link
Member Author

  • What happens if the context schema is different from the context schema of the agent?

Great question: I suggest to parse the schema with provided context and state and throw a meaningful error saying that the tool was used within an agent with different stateSchema/contextSchema definition.

  • If a tool gets called without the accompanying context schema, are we just throwing with an error?

No, we are staying backwards compatible: if the user doesn't provide stateSchema or contextSchema to the tool definition, the tool gets executed as usual, the user just won't be able to access state or context (in a type safe manner).

Base automatically changed from cb/middleware-limit-improvements to main October 31, 2025 22:32
@sydney-runkle
Copy link
Contributor

A few notes:

  • The idea that tools can specify their own state schema / context schemas is quite new, not sure if we should introduce that here... I also definitely don't think we should require state schema / context schemas to be defined on a tool in order to access runtime stuff, for ex what if I want to access the store?
  • I like the idea of an injected arg so that we can get away from the magical context manager functions like getConfig etc
  • Are we effectively reserving the kw runtime here? can we inject only if typed?
  • I think it makes sense for this to live alongside ToolNode in LG and be imported in LC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants