Discussion about why LangGraph JS ToolNode doesn’t inject ToolRuntime.state like Python does, and what the correct workaround or intended design pattern is.

hi @Idrees , it’s an architectural split between the two stacks today - not a bug in your code. In Python the same ToolNode does double duty (low-level graph node and the agent’s tool runner), so it injects state. In JS those two roles live in two different ToolNode classes - the one in @langchain/langgraph/prebuilt is intentionally minimal, and the one used by createAgent is what actually populates ToolRuntime.

  1. Is the difference intentional? Yes, today. Python’s ToolNode calls _inject_tool_args() which fills state/store/runtime from tool_runtime (source). Python’s create_agent just reuses that same ToolNode (source - from langgraph.prebuilt.tool_node import ToolNode). In JS, the ToolNode you imported from @langchain/langgraph/prebuilt only forwards RunnableConfig to tool.invoke - it never builds a ToolRuntime (source). Note that there’s no InjectedState/InjectedStore in JS at all - that whole annotation surface doesn’t exist there.

  2. What actually populates ToolRuntime.state in JS - it’s a different ToolNode. createAgent in the langchain package uses its own ToolNode (langchain/src/agents/nodes/ToolNode.ts):

const output = await invokableTool.invoke(
  { ...toolCall, type: "tool_call" },
  {
    ...config,
    config,
    toolCallId: toolCall.id!,
    state: config.configurable?.__pregel_scratchpad?.currentTaskInput,
    signal: mergeAbortSignals(this.signal, config.signal),
  }
);

There’s an end-to-end test in langchain/src/agents/tests/tools.test.ts that asserts runtime.state, runtime.context, runtime.toolCallId, runtime.config, runtime.writer, runtime.store all work when you use createAgent. So the docs page is right about createAgent - but the prebuilt ToolNode doesn’t go through that path.

  1. Three patterns that actually work in JS today:

A - preferred - use createAgent:

import { createAgent } from "langchain";
import { tool, type ToolRuntime } from "@langchain/core/tools";

const greet = tool(
  async ({ name }, runtime: ToolRuntime<typeof stateSchema>) => {
    return `Hello ${name}! User: ${runtime.state.userId ?? "unknown"}`;
  },
  { name: "greet", description: "...", schema: z.object({ name: z.string() }) }
);

const agent = createAgent({ model, tools: [greet], stateSchema });

B - if you must keep a bare StateGraph + prebuilt ToolNode - don’t try to make the prebuilt node inject. Read the current node’s input from inside the tool using the official helper getCurrentTaskInput exported from @langchain/langgraph (source):

import { getCurrentTaskInput, type LangGraphRunnableConfig } from "@langchain/langgraph";

const greet = tool(
  async ({ name }, config: LangGraphRunnableConfig) => {
    const state = getCurrentTaskInput<MyState>(config);
    return `Hello ${name}! User: ${state.userId ?? "unknown"}`;
  },
  { /* ... */ }
);

That’s the same source the agent ToolNode reads from - config.configurable.__pregel_scratchpad.currentTaskInput - just exposed via the supported helper.

C - subclass ToolNode and put state + toolCallId + config on the invoke options yourself if you really want the ToolRuntime-shaped second argument under a bare graph. That’s exactly what langchain’s agent ToolNode does, so the pattern is supported.

  1. About tool.invoke(toolCall, { configurable: { state } }) - it works in isolation but it’s the least portable choice. Nothing else in the pipeline reads configurable.state; the actual scratchpad key is configurable.__pregel_scratchpad.currentTaskInput. So you’d have to read it back yourself, and getCurrentTaskInput(config) won’t see it. Prefer A → B → C → only this as a last resort.

  2. Is injection planned for the prebuilt JS ToolNode? I can’t speak for the roadmap, but the direction of travel is clear: createAgent is the high-level path in JS (mirroring Python’s create_agent), and the JS prebuilt ToolNode has stayed deliberately minimal. Related threads landed on the same guidance - see #2178, #1995, #2344. I wouldn’t wait on the prebuilt ToolNode to grow state injection - use one of the three patterns above.

Hope this helps!