LangChain.js

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against langchain 1.5.14, @langchain/core 1.2.13, @langchain/langgraph 1.4.18, @langchain/ollama 1.3.0, @langchain/mcp-adapters 1.1.4, @langchain/react 1.2.0, @langchain/langgraph-checkpoint-postgres 1.0.5, deepagents 1.14.1 and @langchain/langgraph-cli 1.5.1 on Node.js 24.20 with TypeScript 7.0.2, Zod 4.6.5 and React 19, as verified on 2026-09-29. The Python pages of this sub-section were written against langchain 1.4.3 and langgraph 1.2.12; the JavaScript packages are versioned independently, so the numbers do not line up. The running example is DocsAssistant, which answers questions over this site’s documentation with Ollama’s llama3.1:8b chat model and nomic-embed-text embedding model over the pgvector store built in Integrating with LangChain. Ollama was not available while the snippets were checked, so every agent run below uses fakeModel, a scripted chat model that ships with langchain, and a stub docs_search tool instead of the pgvector store; the tools, agents, middleware, graphs, checkpointers, the MCP client and the Agent Server are the real packages, and no output comes from a live model. Every snippet was type-checked with tsc --noEmit in strict mode against the packages above, and every snippet that has printed output was executed with them.

LangChain.js is the same framework as the Python one, written in TypeScript: createAgent builds a LangGraph graph, tools are described by a schema, middleware hooks into the agent loop, and a checkpointer plus thread_id gives an agent memory. The concepts and the DocsAssistant scenario carry over unchanged, so this page does not re-teach them; it shows the TypeScript spelling, then gathers everything that is genuinely different in Differences from Python. It also covers the one thing that only exists on the JavaScript side: the frontend SDKs, which connect a React, Vue, Svelte or Angular UI directly to an agent.

The package map

The JavaScript ecosystem is split into scoped npm packages, one per job. The diagram below shows how the packages relate: a solid arrow means "lists as a dependency", a dashed arrow means "lists as a peer dependency", which the application must install itself.

flowchart TD DA["deepagents
batteries-included harness"] LC["langchain
createAgent, middleware, initChatModel, fakeModel"] LG["@langchain/langgraph
StateGraph, checkpointers, interrupt"] CO["@langchain/core
messages, tools, base model classes"] P["partner packages
@langchain/ollama, @langchain/anthropic, @langchain/openai"] M["@langchain/mcp-adapters
MultiServerMCPClient"] PG["@langchain/langgraph-checkpoint-postgres
PostgresSaver"] R["@langchain/react, vue, svelte, angular
useStream, injectStream"] SDK["@langchain/langgraph-sdk
Agent Server client"] LS["langsmith
tracing client"] LC --> LG LC --> LS LG --> SDK R --> SDK LC -.-> CO LG -.-> CO P -.-> CO M -.-> CO M -.-> LG PG -.-> CO DA -.-> LC DA -.-> LG DA -.-> CO R -.-> CO
Figure 1. How the JavaScript packages depend on each other (solid: dependency, dashed: peer dependency)

The edges come from each package’s own package.json. This script prints them for the packages installed in a project, together with the engines field, so you can compare it with any version you have:

versions.mjs — installed versions and the exports this page relies on
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
for (const name of ["langchain", "@langchain/core", "@langchain/langgraph", "@langchain/ollama", "@langchain/mcp-adapters", "@langchain/react", "@langchain/langgraph-checkpoint-postgres", "deepagents"]) {
  const { version, engines } = require(`${name}/package.json`);
  console.log(name.padEnd(42), version.padEnd(8), engines?.node ?? "");
}
const lc = await import("langchain");
const lg = await import("@langchain/langgraph");
console.log("langchain exports:", ["createAgent", "createMiddleware", "tool", "initChatModel", "fakeModel"].map((n) => `${n}=${typeof lc[n]}`).join(" "));
console.log("langgraph exports:", ["StateGraph", "StateSchema", "MessagesValue", "MemorySaver", "InMemoryStore", "Command", "interrupt"].map((n) => `${n}=${typeof lg[n]}`).join(" "));

Executed output:

langchain                                  1.5.14   >=20
@langchain/core                            1.2.13   >=20
@langchain/langgraph                       1.4.18   >=18
@langchain/ollama                          1.3.0    >=20
@langchain/mcp-adapters                    1.1.4    >=20.10.0
@langchain/react                           1.2.0
@langchain/langgraph-checkpoint-postgres   1.0.5    >=18
deepagents                                 1.14.1
langchain exports: createAgent=function createMiddleware=function tool=function initChatModel=function fakeModel=function
langgraph exports: StateGraph=function StateSchema=function MessagesValue=object MemorySaver=function InMemoryStore=function Command=function interrupt=function

And the dependency edges, from deps.mjs (the same idea, restricted to the LangChain family):

deps.mjs — which family packages each package requires
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const family = /^(langchain|deepagents|langsmith|@langchain\/)/;
for (const name of ["langchain", "@langchain/langgraph", "@langchain/ollama", "@langchain/mcp-adapters", "@langchain/react", "@langchain/langgraph-checkpoint-postgres", "deepagents"]) {
  const pkg = require(`${name}/package.json`);
  const show = (o = {}) => Object.keys(o).filter((d) => family.test(d)).sort();
  console.log(name.padEnd(42), "requires", show(pkg.dependencies).join(" ") || "-", "| peer:", show(pkg.peerDependencies).join(" ") || "-");
}

Executed output:

langchain                                  requires @langchain/langgraph @langchain/langgraph-checkpoint langsmith | peer: @langchain/core
@langchain/langgraph                       requires @langchain/langgraph-checkpoint @langchain/langgraph-sdk @langchain/protocol | peer: @langchain/core
@langchain/ollama                          requires - | peer: @langchain/core
@langchain/mcp-adapters                    requires - | peer: @langchain/core @langchain/langgraph
@langchain/react                           requires @langchain/langgraph-sdk | peer: @langchain/core
@langchain/langgraph-checkpoint-postgres   requires - | peer: @langchain/core @langchain/langgraph-checkpoint
deepagents                                 requires - | peer: @langchain/core @langchain/langgraph @langchain/langgraph-checkpoint @langchain/langgraph-sdk langchain langsmith
Table 1. The JavaScript packages and when to install them
Package What it is Install it when

langchain

The agent framework: createAgent, createMiddleware, the built-in middleware, tool, initChatModel, structured output strategies, and re-exports of the message classes and of fakeModel. It depends on @langchain/langgraph.

You are building an agent or an LLM application. This is the default entry point.

@langchain/core

The base abstractions: messages, tools, prompt templates, Runnables and the base chat-model class. It is a peer dependency of langchain and of every partner package.

Always, alongside langchain. Unlike the Python langchain-core, npm does not pull it in for you, so add it to your own package.json.

@langchain/langgraph

The orchestration runtime: StateGraph, StateSchema, MessagesValue, MemorySaver, InMemoryStore, interrupt, Command.

You build graphs yourself or import MemorySaver and Command. langchain installs it, but declare it because your code imports from it directly.

Partner packages (@langchain/ollama, @langchain/anthropic, @langchain/openai, …​)

One package per provider, each with a peer dependency on @langchain/core.

You talk to that provider: @langchain/ollama for DocsAssistant, plus one hosted provider if you also call claude-sonnet-5 or gpt-6-sol.

@langchain/langgraph-checkpoint-postgres

PostgresSaver, a durable checkpointer.

Conversations must survive a restart or be shared between processes.

@langchain/mcp-adapters

MultiServerMCPClient, which turns the tools of MCP servers into LangChain tools.

Your agent uses MCP servers.

@langchain/react, @langchain/vue, @langchain/svelte, @langchain/angular

The frontend SDKs: useStream (injectStream in Angular).

A browser UI talks to an agent served by an Agent Server.

@langchain/langgraph-sdk

The typed client for the Agent Server’s REST and streaming API, and the layer under the frontend SDKs.

A Node.js or browser program calls a deployed agent without a UI framework.

deepagents

createDeepAgent: planning, a virtual filesystem and subagents on top of langchain. Its dependencies are all peers.

The task is long-running and multi-step; see Multi-Agent & Deep Agents.

@langchain/langgraph-cli

The langgraph command line for a local Agent Server (dev) and for building deployment images.

You serve agents to the frontend SDKs or deploy them.

Installing what DocsAssistant needs looks like this; the LangChain.js installation page says Node.js 22 or later is required, while the packages themselves declare engines of 20 or later (see the output above), so treat 22 as the supported floor:

Installing the DocsAssistant dependencies
npm install langchain @langchain/core @langchain/langgraph @langchain/ollama zod
npm install --save-dev typescript @types/node

The examples use ES modules and this tsconfig.json; strict matters, because most of the type inference this page shows (tool arguments, structured responses, useStream state) disappears without it:

tsconfig.json used to type-check every snippet
{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"noEmit":true,"skipLibCheck":true,"lib":["ES2022","DOM","DOM.Iterable"],"types":["node"]},"include":["*.ts","*.tsx"]}

The package split is described in the LangChain.js overview; the Python equivalent is on Ecosystem & Packages.

Models and tools

DocsAssistant’s models are two constructor calls. ChatOllama and OllamaEmbeddings read OLLAMA_BASE_URL from the environment when no baseUrl is passed, and default to http://127.0.0.1:11434:

model.ts — DocsAssistant’s Ollama models
import { ChatOllama, OllamaEmbeddings } from "@langchain/ollama";

// Reads OLLAMA_BASE_URL from the environment when set; defaults to http://127.0.0.1:11434.
export const chatModel = new ChatOllama({ model: "llama3.1:8b", temperature: 0 });
export const embeddings = new OllamaEmbeddings({ model: "nomic-embed-text" });

See LangChain.js — Models for the provider packages and the standard call options. Where a provider-prefixed string is more convenient, for example to switch model from configuration, initChatModel resolves it to the provider’s class. It returns a configurable wrapper rather than the class itself, and it needs the matching partner package installed:

initChatModel with a provider-prefixed model id
import { initChatModel } from "langchain";
const m = await initChatModel("ollama:llama3.1:8b", { temperature: 0 });
console.log(m.constructor.name);

Executed output:

ConfigurableModel

Tools are created with the tool function from langchain, which takes the implementation and an options object whose schema is a Zod object. The schema plays the part of the Python type hints and docstring: its field descriptions are what the model reads. Here is DocsAssistant’s search tool over a small in-memory corpus standing in for the pgvector store:

tools.ts — a Zod-typed tool
import { tool } from "langchain";
import * as z from "zod";

// A stub standing in for the pgvector-backed search built in "Integrating with LangChain".
const CORPUS = [
  { page: "modules/ROOT/pages/database/vector-rag/hnsw.adoc", section: "ef_search", content: "ef_search sets the size of the candidate list HNSW keeps while searching; raise it for recall, lower it for speed." },
  { page: "modules/ROOT/pages/database/vector-rag/hnsw.adoc", section: "m", content: "m is the number of bidirectional links per node in the HNSW graph." },
];

export const docsSearch = tool(
  async ({ query, limit }) => {
    const words = query.toLowerCase().split(/\s+/);
    const hits = CORPUS.filter((d) => words.some((w) => d.content.toLowerCase().includes(w))).slice(0, limit);
    return JSON.stringify({ results: hits.map((h) => ({ page: h.page, section: h.section, snippet: h.content.slice(0, 160) })) });
  },
  {
    name: "docs_search",
    description: "Search this site's documentation and return matching sections (page path, section, snippet).",
    schema: z.object({
      query: z.string().describe("A key phrase from the question, e.g. 'HNSW ef_search'"),
      limit: z.number().int().min(1).max(10).default(3),
    }),
  },
);

The .default(3) on limit makes the argument optional for the model, and .min(1).max(10) becomes part of the JSON Schema the model sees. Tool design advice (narrow tools, concise responses, actionable errors) is the same as in Tool Design; tool itself is covered in the official LangChain.js — Tools page, and the Python decorator on Tools.

createAgent

createAgent takes a model (an instance or a "provider:model" string), tools, a systemPrompt, an optional checkpointer, middleware, a responseFormat and a name. The helper below builds DocsAssistant from any chat model, so the same code runs against Ollama in production and a scripted model in a test:

agent.ts — DocsAssistant as a createAgent agent
import { createAgent } from "langchain";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
import type { BaseCheckpointSaver } from "@langchain/langgraph";
import { docsSearch } from "./tools";

export function buildDocsAssistant(model: BaseChatModel | string, checkpointer?: BaseCheckpointSaver) {
  return createAgent({
    model,
    tools: [docsSearch],
    systemPrompt:
      "You are DocsAssistant. Answer only from documentation returned by docs_search, and cite the page path.",
    checkpointer,
    name: "docs_assistant",
  });
}

The run below uses fakeModel, the chat model LangChain ships for tests: respond queues the next reply, and a reply that carries tool_calls makes the agent call the tool. The first scripted turn asks for docs_search, the second answers from its result. thread_id, passed under configurable, is what ties repeated calls to one conversation:

run-agent.ts — invoking the agent on a thread
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { buildDocsAssistant } from "./agent";

// Two scripted turns: first a tool call, then the final answer. No network, no Ollama.
const model = fakeModel()
  .respond(
    new AIMessage({
      content: "",
      tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }],
    }),
  )
  .respond(new AIMessage("ef_search sets how many candidates HNSW keeps while searching (hnsw.adoc)."));

const agent = buildDocsAssistant(model, new MemorySaver());
const config = { configurable: { thread_id: "t-1" } }; // same thread_id = same conversation
const result = await agent.invoke({ messages: [new HumanMessage("What does ef_search do?")] }, config);

for (const m of result.messages) {
  const calls = AIMessage.isInstance(m) ? m.tool_calls?.map((c) => `${c.name}(${JSON.stringify(c.args)})`) : [];
  console.log(m.type.padEnd(5), calls?.length ? calls.join(", ") : String(m.content).slice(0, 72));
}

Executed output:

human What does ef_search do?
ai    docs_search({"query":"HNSW ef_search"})
tool  {"results":[{"page":"modules/ROOT/pages/database/vector-rag/hnsw.adoc","
ai    ef_search sets how many candidates HNSW keeps while searching (hnsw.adoc

fakeModel also records what the model received (model.calls, model.callCount), so a test can assert on the prompts it was sent; the Python testing approach is on Testing. The agent loop, its options and the Python spelling are on Agents with create_agent; the official page is LangChain.js — Agents, and the testing helpers are on LangChain.js — Unit testing.

Structured output

responseFormat accepts a Zod schema, and toolStrategy or providerStrategy choose how the model is forced into it: providerStrategy uses the provider’s native structured output, toolStrategy adds a hidden tool whose arguments are the answer. The result carries a structuredResponse whose type is inferred from the schema. With toolStrategy the hidden tool is named after the schema’s title, which is why the schema below sets one with .meta; the scripted model "calls" that tool just as a real one would:

run-so.ts — a typed structured response
import { createAgent, fakeModel, toolStrategy, HumanMessage } from "langchain";
import * as z from "zod";

const DocAnswer = z
  .object({
    answer: z.string(),
    pages: z.array(z.string()).describe("Repository paths of the pages the answer is based on"),
    confidence: z.number().min(0).max(1),
  })
  .meta({ title: "DocAnswer" }); // becomes the name of the structured-output tool

// A fake model that "calls" the DocAnswer tool, as a real model would with toolStrategy.
const model = fakeModel().respondWithTools([
  {
    name: "DocAnswer",
    args: {
      answer: "ef_search sets the HNSW candidate list size.",
      pages: ["modules/ROOT/pages/database/vector-rag/hnsw.adoc"],
      confidence: 0.9,
    },
  },
]);

const agent = createAgent({ model, tools: [], responseFormat: toolStrategy(DocAnswer) });
const result = await agent.invoke({ messages: [new HumanMessage("What does ef_search do?")] });
console.log(result.structuredResponse);
console.log(result.structuredResponse.pages.length); // typed: structuredResponse is inferred from the schema

Executed output:

{
  answer: 'ef_search sets the HNSW candidate list size.',
  pages: [ 'modules/ROOT/pages/database/vector-rag/hnsw.adoc' ],
  confidence: 0.9
}
1

Because structuredResponse is inferred, result.structuredResponse.pages.length type-checks and a misspelt field would not. Strategy selection, error handling for invalid output and the Python ToolStrategy are on Structured Output; the JavaScript reference is LangChain.js — Structured output.

Middleware and approval gates

createMiddleware builds a middleware from hooks: node-style beforeModel and afterModel, and wrap-style wrapModelCall and wrapToolCall that receive the request and a handler. The built-in humanInTheLoopMiddleware pauses the run when the model asks for a tool listed in interruptOn, which needs a checkpointer. In the example DocsAssistant may search freely, but opening a GitHub issue needs a human decision (in real code the tool would call the GitHub API with a narrowly scoped token read from an environment variable, never one written into the source):

hitl.ts — a custom middleware and an approval gate
import { AIMessage, HumanMessage, createAgent, createMiddleware, fakeModel, humanInTheLoopMiddleware, tool } from "langchain";
import { Command, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";
import { docsSearch } from "./tools";

// The destructive tool. A real one would call the GitHub API with a fine-grained token read from the environment.
const createGithubIssue = tool(
  async ({ title, body }) => `Created issue #101: ${title} (${body.length} characters)`,
  {
    name: "create_github_issue",
    description: "Open a GitHub issue on the documentation repository. Requires human approval.",
    schema: z.object({ title: z.string().max(120), body: z.string() }),
  },
);

// A custom middleware: log every model call and its number of messages.
const logModelCalls = createMiddleware({
  name: "LogModelCalls",
  wrapModelCall: (request, handler) => {
    console.log(`model call with ${request.messages.length} messages`);
    return handler(request);
  },
});

const model = fakeModel()
  .respond(new AIMessage({ content: "", tool_calls: [{ name: "create_github_issue", args: { title: "ef_search is undocumented", body: "Please document ef_search." }, id: "call_9", type: "tool_call" }] }))
  .respond(new AIMessage("I opened the issue."));

const agent = createAgent({
  model,
  tools: [docsSearch, createGithubIssue],
  middleware: [
    logModelCalls,
    humanInTheLoopMiddleware({
      interruptOn: { create_github_issue: { allowedDecisions: ["approve", "reject"] }, docs_search: false },
    }),
  ],
  checkpointer: new MemorySaver(),
});

const config = { configurable: { thread_id: "t-hitl" } };
const paused = await agent.invoke({ messages: [new HumanMessage("File an issue: ef_search is undocumented")] }, config);
const request = paused.__interrupt__?.[0]?.value as { actionRequests: { name: string; args: unknown }[] };
console.log("paused for approval:", JSON.stringify(request.actionRequests[0]));

const done = await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
console.log(done.messages.at(-2)?.content, "|", done.messages.at(-1)?.content);

Executed output:

model call with 1 messages
paused for approval: {"name":"create_github_issue","args":{"title":"ef_search is undocumented","body":"Please document ef_search."},"description":"Tool execution requires approval\n\nTool: create_github_issue\nArgs: {\n  \"title\": \"ef_search is undocumented\",\n  \"body\": \"Please document ef_search.\"\n}"}
model call with 3 messages
Created issue #101: ef_search is undocumented (26 characters) | I opened the issue.

The first invoke returns with interrupt set, carrying an actionRequests list; the second, given new Command({ resume: { decisions: [{ type: "approve" }] } }) and the same thread_id, executes the tool. reject skips the tool and reports the message to the model, and edit runs it with changed arguments. The model was called once before the pause and once after, with the tool result in its context. The hooks are covered on Middleware and the approval pattern on Human-in-the-Loop and Human-in-the-Loop & Time Travel. The official pages are LangChain.js — Custom middleware and LangChain.js — Human-in-the-loop.

LangGraph.js

createAgent returns a compiled LangGraph graph, so everything in LangGraph applies to it. Building a graph directly is worth it when the control flow is yours rather than the model’s. In LangGraph.js, state is declared with StateSchema: each key is a Zod schema or a special value type, and MessagesValue is the message list with the appending reducer built in. Nodes are plain functions typed as GraphNode<typeof State> that return partial state updates. The graph below retrieves documents, routes on whether it found any, answers when it did, and otherwise pauses with interrupt to ask for approval before reporting a documentation gap:

graph.ts — a StateGraph with a conditional edge, a checkpointer and an interrupt
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import {
  Command, END, GraphNode, InMemoryStore, MemorySaver, MessagesValue, START, StateGraph, StateSchema, interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
import { docsSearch } from "./tools";

const State = new StateSchema({
  messages: MessagesValue,               // built-in reducer: appends messages
  hits: z.array(z.string()).default(() => []),
  issueUrl: z.string().optional(),
});

const model = fakeModel().respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));

const retrieve: GraphNode<typeof State> = async (state) => {
  const question = String(state.messages.at(-1)?.content ?? "");
  const raw = JSON.parse(await docsSearch.invoke({ query: question, limit: 3 })) as { results: { page: string }[] };
  return { hits: raw.results.map((r) => r.page) };
};

const answer: GraphNode<typeof State> = async (state) => {
  const reply = await model.invoke([...state.messages, new HumanMessage(`Sources: ${state.hits.join(", ")}`)]);
  return { messages: [reply] };
};

const escalate: GraphNode<typeof State> = (state) => {
  // Pauses the run; the value is surfaced to the caller under __interrupt__.
  const approved = interrupt({ action: "create_github_issue", title: `Missing docs: ${state.messages.at(-1)?.content}` });
  return approved ? { issueUrl: "https://github.com/example/docs/issues/101" } : {};
};

const graph = new StateGraph(State)
  .addNode("retrieve", retrieve)
  .addNode("answer", answer)
  .addNode("escalate", escalate)
  .addEdge(START, "retrieve")
  .addConditionalEdges("retrieve", (state) => (state.hits.length > 0 ? "answer" : "escalate"), ["answer", "escalate"])
  .addEdge("answer", END)
  .addEdge("escalate", END)
  .compile({ checkpointer: new MemorySaver(), store: new InMemoryStore() });

const found = await graph.invoke({ messages: [new HumanMessage("HNSW ef_search")] }, { configurable: { thread_id: "g1" } });
console.log(found.hits.length, "hit(s);", String(found.messages.at(-1)?.content).slice(0, 40));

const config = { configurable: { thread_id: "g2" } };
const paused = await graph.invoke({ messages: [new HumanMessage("kubernetes operators")] }, config);
if ("__interrupt__" in paused) {
  console.log((paused.__interrupt__ as { value: unknown }[])[0].value);
}
const resumed = await graph.invoke(new Command({ resume: true }), config);
console.log(resumed.issueUrl);

Executed output:

2 hit(s); ef_search controls how many candidates H
{
  action: 'create_github_issue',
  title: 'Missing docs: kubernetes operators'
}
https://github.com/example/docs/issues/101

compile receives the checkpointer and a store, so the graph can be paused and resumed and can share long-term data across threads. Resuming is graph.invoke(new Command({ resume: true }), config) with the same thread_id; the value given to resume becomes the return value of interrupt. The "interrupt" in paused check is needed because the state type of a StateSchema graph does not declare that key, so TypeScript would otherwise reject paused.interrupt. The graph concepts are on LangGraph Fundamentals and the workflow patterns on LangGraph Workflows; the official pages are LangGraph.js — Overview, LangGraph.js — Use the graph API and LangGraph.js — Interrupts.

Persistence

MemorySaver (the JavaScript name for the in-memory checkpointer) loses everything on restart. For durable conversations, PostgresSaver from @langchain/langgraph-checkpoint-postgres is a drop-in replacement; the connection string comes from the environment, and setup() creates its tables on first use:

pg.ts — a durable checkpointer
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import { HumanMessage } from "langchain";
import { buildDocsAssistant } from "./agent";
import { chatModel } from "./model";

const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
await checkpointer.setup(); // creates the checkpoint tables on first use
const agent = buildDocsAssistant(chatModel, checkpointer);
await agent.invoke(
  { messages: [new HumanMessage("What does ef_search do?")] },
  { configurable: { thread_id: "user-42-session-7" } },
);

This snippet was type-checked but not executed, because no PostgreSQL server was available. Keep thread_id values short (a user and session identifier, or a UUID); the persistence documentation warns that very long thread ids overflow the column. Checkpointers, stores and time travel are covered in Python on Short-Term Memory & Checkpointers; the JavaScript pages are LangChain.js — Short-term memory and LangGraph.js — Persistence.

Deep Agents and MCP tools

Deep Agents are on npm as deepagents. createDeepAgent takes the same model, tools and systemPrompt, and returns a graph with planning, a virtual filesystem and subagent tools already wired in:

da.ts — a Deep Agent for DocsAssistant’s research mode
import { createDeepAgent } from "deepagents";
import { docsSearch } from "./tools";
import { chatModel } from "./model";

const agent = createDeepAgent({
  model: chatModel,
  tools: [docsSearch],
  systemPrompt: "You are DocsAssistant's research mode. Plan, search, then write a short cited summary.",
});
console.log(typeof agent.invoke);

Executed output:

function

The snippet builds the agent (the constructor does not call the model) but does not run it. Model-driven behaviour is covered on Multi-Agent & Deep Agents; the official page is Deep Agents — Overview.

MCP servers plug in through @langchain/mcp-adapters. MultiServerMCPClient takes a named map of servers, each with a transport ("stdio" for a local subprocess, "http" with a url for a remote server), and getTools() returns ordinary LangChain tools. The client is stateless by default, and close() releases it. The server here is a real stdio MCP server built with the official TypeScript SDK, so the tool call in the run goes over the protocol:

mcp-server.mjs — a one-tool MCP server over stdio
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod";

const server = new McpServer({ name: "docs-server", version: "1.0.0" });
server.registerTool(
  "docs_lookup",
  { description: "Look up a documentation page by topic.", inputSchema: { topic: z.string() } },
  async ({ topic }) => ({ content: [{ type: "text", text: `modules/ROOT/pages/database/vector-rag/${topic}.adoc` }] }),
);
await server.connect(new StdioServerTransport());
mcp.ts — using its tools in an agent
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { AIMessage, HumanMessage, createAgent, fakeModel } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const client = new MultiServerMCPClient({
  docs: {
    transport: "stdio", // a local subprocess; use "http" with a `url` for a remote server
    command: "node",
    args: ["./mcp-server.mjs"],
  },
});

try {
  const tools = await client.getTools(); // LangChain tools backed by the MCP server
  console.log(tools.map((t) => t.name));

  const model = fakeModel()
    .respond(new AIMessage({ content: "", tool_calls: [{ name: "docs_lookup", args: { topic: "hnsw" }, id: "call_1", type: "tool_call" }] }))
    .respond(new AIMessage("The HNSW page is modules/ROOT/pages/database/vector-rag/hnsw.adoc."));
  const agent = createAgent({ model, tools, checkpointer: new MemorySaver() });
  const result = await agent.invoke({ messages: [new HumanMessage("Where is the HNSW page?")] }, { configurable: { thread_id: "t-mcp" } });
  console.log(result.messages.map((m) => m.type + ": " + JSON.stringify(m.content).slice(0, 70)));
} finally {
  await client.close();
}

Executed output:

[ 'docs_lookup' ]
[
  'human: "Where is the HNSW page?"',
  'ai: ""',
  'tool: "modules/ROOT/pages/database/vector-rag/hnsw.adoc"',
  'ai: "The HNSW page is modules/ROOT/pages/database/vector-rag/hnsw.adoc."'
]

The FastMCP-based MCPAdapter described for Python on MCP Adapters was not found in the JavaScript packages installed for this page, so the JavaScript client to use is MultiServerMCPClient. Writing MCP servers and clients in TypeScript without LangChain is covered on Building MCP Clients. The official page is LangChain.js — Model Context Protocol (MCP).

Streaming

agent.streamEvents(input, { version: "v3" }) returns a run object with typed projections that are consumed independently: messages yields one entry per model call whose text is an async iterable of deltas, toolCalls yields the lifecycle of each tool execution, values yields state snapshots and output resolves to the final state. Because the projections are independent, the loop over them runs concurrently:

run-stream.ts — typed event streaming
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { buildDocsAssistant } from "./agent";
import { MemorySaver } from "@langchain/langgraph";

const model = fakeModel()
  .respond(
    new AIMessage({
      content: "",
      tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }],
    }),
  )
  .respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));

const agent = buildDocsAssistant(model, new MemorySaver());
const stream = await agent.streamEvents(
  { messages: [new HumanMessage("What does ef_search do?")] },
  { version: "v3", configurable: { thread_id: "t-stream" } },
);

await Promise.all([
  (async () => {
    for await (const message of stream.messages) {
      let text = "";
      for await (const delta of message.text) text += delta;
      console.log(`[${message.node}] ${text}`);
    }
  })(),
  (async () => {
    for await (const call of stream.toolCalls) {
      console.log(`tool ${call.name}(${JSON.stringify(call.input)})`);
      console.log("  ->", String(await call.output).slice(0, 80));
    }
  })(),
]);

const finalState = await stream.output;
console.log(finalState.messages.length, "messages in the final state");

Executed output:

[model_request]
tool docs_search({"query":"HNSW ef_search"})
  -> {"results":[{"page":"modules/ROOT/pages/database/vector-rag/hnsw.adoc","section"
[model_request] ef_search controls how many candidates HNSW keeps while searching.
4 messages in the final state

[model_request] is the graph node that made the model call, so the first entry is the tool-call turn (it has no text) and the last is the answer. The scripted model returns each message in a single delta; a live model would emit many short ones, and the same loop would print them as they arrive.

The lower-level stream method takes a streamMode. "updates" emits one entry after every node, keyed by node name, and "messages" and "custom" are the other two modes for agents:

run-modes.ts — streamMode "updates"
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { buildDocsAssistant } from "./agent";
import { MemorySaver } from "@langchain/langgraph";

const model = fakeModel()
  .respond(new AIMessage({ content: "", tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }] }))
  .respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));

const agent = buildDocsAssistant(model, new MemorySaver());
const updates = await agent.stream(
  { messages: [new HumanMessage("What does ef_search do?")] },
  { streamMode: "updates", configurable: { thread_id: "t-modes" } },
);
for await (const chunk of updates) {
  for (const [node, update] of Object.entries(chunk)) {
    const last = (update as { messages: { type: string; content: unknown }[] }).messages.at(-1);
    console.log(node, "->", last?.type, JSON.stringify(last?.content).slice(0, 60));
  }
}

Executed output:

model_request -> ai ""
tools -> tool "{\"results\":[{\"page\":\"modules/ROOT/pages/database/vecto
model_request -> ai "ef_search controls how many candidates HNSW keeps while sea

The official pages are LangChain.js — Event streaming and LangChain.js — Streaming; the Python side, with a FastAPI Server-Sent Events endpoint, is on Streaming. What to show a user while this happens is on Agent UX.

Streaming to React with useStream

The frontend SDKs are the part of LangChain.js with no Python counterpart. On the server, createAgent produces a graph that an Agent Server exposes over HTTP; in the browser, useStream from @langchain/react connects to it and gives the component reactive messages, tool calls, interrupts, a loading flag and thread state. Vue and Svelte export the same name from @langchain/vue and @langchain/svelte, and Angular has injectStream in @langchain/angular.

The server side

The graph is exported from a module, and langgraph.json maps a graph name to that export. The file below builds DocsAssistant on the Ollama model without a checkpointer, because the Agent Server provides persistence:

server.ts — the exported agent
import { chatModel } from "./model";
import { buildDocsAssistant } from "./agent";

// Exported so that langgraph.json can point at it; the Agent Server supplies persistence.
export const docsAssistant = buildDocsAssistant(chatModel);
langgraph.json — the graph name the frontend uses as assistantId
{
  "node_version": "24",
  "graphs": { "docs_assistant": "./server.ts:docsAssistant" },
  "env": ".env"
}

npx @langchain/langgraph-cli dev --no-browser starts the in-memory development server on port 2024; on first run it adds .langgraph_api to .gitignore. To check the server side without a browser, the program below uses the SDK client. It was run against a real langgraph dev process serving a copy of server.ts whose model is the scripted one (Ollama being unavailable), and the output is what the server streamed back:

sdk.ts — calling the Agent Server from Node.js
import { Client } from "@langchain/langgraph-sdk";

const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.create();
const events = client.runs.stream(thread.thread_id, "docs_assistant", {
  input: { messages: [{ role: "user", content: "What does ef_search do?" }] },
  streamMode: "updates",
});
for await (const event of events) {
  if (event.event === "updates") {
    for (const [node, update] of Object.entries(event.data as Record<string, { messages: { type: string; content: string }[] }>)) {
      const last = update.messages.at(-1);
      console.log(node, "->", last?.type, JSON.stringify(last?.content).slice(0, 60));
    }
  } else console.log(event.event);
}

Executed output:

metadata
model_request -> ai ""
tools -> tool "{\"results\":[{\"page\":\"modules/ROOT/pages/database/vecto
model_request -> ai "ef_search controls how many candidates HNSW keeps while sea

The metadata line is the run’s start event. Then come the three node updates of one turn: the model asks for a tool, the tool node returns its result, the model answers. This is exactly the stream a useStream hook consumes. Deploying the same graph, and the production alternatives to the development server, are on Deployment. The official pages are LangGraph.js — Application structure and LangGraph.js — Run a local server.

The React component

useStream takes the server URL (apiUrl) and the graph name (assistantId). Passing typeof docsAssistant as the type parameter lets TypeScript infer the state schema from the compiled graph, so stream.messages and any custom state keys are typed without a hand-written interface. The import is type-only, so none of the server code reaches the browser bundle:

Chat.tsx — a chat component with useStream
import { useState } from "react";
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "langchain";
import type { docsAssistant } from "./server"; // type-only import: no server code reaches the browser bundle

export function Chat() {
  const [draft, setDraft] = useState("");
  const stream = useStream<typeof docsAssistant>({
    apiUrl: "http://localhost:2024", // `langgraph dev` locally; your Agent Server URL in production
    assistantId: "docs_assistant", // a graph name from langgraph.json
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void stream.submit({ messages: [new HumanMessage(draft)] });
        setDraft("");
      }}
    >
      {stream.messages.map((msg) => (
        <p key={msg.id}>
          {HumanMessage.isInstance(msg) ? "You: " : AIMessage.isInstance(msg) ? "DocsAssistant: " : "tool: "}
          {msg.text}
        </p>
      ))}
      {stream.error ? <p role="alert">Something went wrong. Try again.</p> : null}
      <input value={draft} onChange={(e) => setDraft(e.target.value)} disabled={stream.isLoading} />
      {stream.isLoading ? (
        <button type="button" onClick={() => void stream.stop()}>Stop</button>
      ) : (
        <button type="submit">Send</button>
      )}
    </form>
  );
}

The component was type-checked with tsc --noEmit against @langchain/react 1.2.0; it was not rendered in a browser here. stream.submit starts a run and streams into stream.messages, stream.isLoading disables the input while the run is in flight, and stream.stop aborts it. The hook exposes more than this page uses: stream.toolCalls, stream.interrupt for the approval flow shown above, and stream.values. The patterns are catalogued on the LangChain.js — Frontend overview, including approval flows, durable stream rejoin and time travel. The comparison with hand-written fetch and Server-Sent Events is on Streaming, and the UX rules for what such a component should show are on Agent UX.

Differences from Python

The mental model is identical; the differences are in packaging, naming, schemas and a few API shapes. Everything in this table was observed in the packages and pages described at the top of the page.

Table 2. LangChain (Python) and LangChain.js side by side
Topic Python JavaScript / TypeScript

Installation

pip install langchain langchain-ollama; langchain-core is installed for you.

npm install langchain @langchain/core @langchain/ollama; @langchain/core is a peer dependency you add yourself.

Versions

langchain 1.4.3, langgraph 1.2.12, deepagents 0.7.19.

langchain 1.5.14, @langchain/langgraph 1.4.18, deepagents 1.14.1: independent version numbers.

Naming

create_agent, system_prompt, response_format, context_schema, interrupt_on, stream_mode.

createAgent, systemPrompt, responseFormat, contextSchema, interruptOn, streamMode. The run key stays configurable.thread_id.

Tools

@tool decorator; the schema is inferred from type hints and the docstring.

tool(fn, { name, description, schema }); the schema is a Zod object (or another Standard Schema).

State schemas

TypedDict, dataclass or Pydantic model, with reducers via Annotated.

StateSchema with Zod fields, MessagesValue, ReducedValue; Annotation.Root still works but the docs call it legacy.

Structured output

Pydantic model or dataclass in response_format.

Zod schema in responseFormat; with toolStrategy the tool is named after the schema’s title.

In-memory checkpointer

InMemorySaver.

MemorySaver.

Postgres checkpointer

PostgresSaver.from_conn_string(…​), used as a context manager.

PostgresSaver.fromConnString(…​) followed by await setup().

Sync and async

Both: invoke and ainvoke, stream and astream.

Everything returns a promise or an async iterable; use await.

Event streaming

stream_events(…​, version="v3") and astream_events.

streamEvents(input, { version: "v3" }), with for await over messages, toolCalls, values.

Interrupts

interrupt(…​), Command(resume=…​), result key interrupt.

interrupt(…​), new Command({ resume }), interrupt; the human-in-the-loop payload uses actionRequests and args.

Testing

GenericFakeChatModel and friends.

fakeModel() exported from langchain.

MCP

langchain-mcp-adapters and the newer langchain.mcp MCPAdapter.

@langchain/mcp-adapters with MultiServerMCPClient.

Frontend

Not applicable; the browser talks to your own HTTP endpoint.

useStream and injectStream in @langchain/react, @langchain/vue, @langchain/svelte, @langchain/angular.

Runtime

Python 3.10 or later (3.12 here).

Node.js 22 or later per the docs (packages declare 20); langchain also ships a browser entry point.

When a Python page of this sub-section shows an API you cannot find in TypeScript, check the reference documentation before assuming it is missing: the two libraries are released separately, and a feature often lands in one first. For Java the equivalent library is a separate project with its own design; see LangChain4j.