LangGraph.js uses the async_hooks
API to more conveniently allow for tracing and callback propagation within
nodes. This API is supported in many environments, such as
Node.js,
Deno,
Cloudflare Workers,
and the
Edge runtime,
but not all, such as within web browsers.
To allow usage of LangGraph.js in environments that do not have the
async_hooks API available, we've added a separate @langchain/langgraph/web
entrypoint. This entrypoint exports everything that the primary
@langchain/langgraph exports, but will not initialize or even import
async_hooks. Here's a simple example:
// Import from "@langchain/langgraph/web"import{END,START,StateGraph,Annotation,}from"@langchain/langgraph/web";import{BaseMessage,HumanMessage}from"@langchain/core/messages";constGraphState=Annotation.Root({messages:Annotation<BaseMessage[]>({reducer:(x,y)=>x.concat(y),}),});constnodeFn=async(_state:typeofGraphState.State)=>{return{messages:[newHumanMessage("Hello from the browser!")]};};// Define a new graphconstworkflow=newStateGraph(GraphState).addNode("node",nodeFn).addEdge(START,"node").addEdge("node",END);constapp=workflow.compile({});// Use the RunnableconstfinalState=awaitapp.invoke({messages:[]},);console.log(finalState.messages[finalState.messages.length-1].content);
Hello from the browser!
Other entrypoints, such as @langchain/langgraph/prebuilt, can be used in
either environment.
Caution
If you are using LangGraph.js on the frontend, make sure you are not exposing any private keys!
For chat models, this means you need to use something like WebLLM
that can run client-side without authentication.
The lack of async_hooks support in web browsers means that if you are calling
a Runnable within a
node (for example, when calling a chat model), you need to manually pass a
config object through to properly support tracing,
.streamEvents()
to stream intermediate steps, and other callback related functionality. This
config object will passed in as the second argument of each node, and should be
used as the second parameter of any Runnable method.
To illustrate this, let's set up our graph again as before, but with a
Runnable within our node. First, we'll avoid passing config through into the
nested function, then try to use .streamEvents() to see the intermediate
results of the nested function:
// Import from "@langchain/langgraph/web"import{END,START,StateGraph,Annotation,}from"@langchain/langgraph/web";import{BaseMessage}from"@langchain/core/messages";import{RunnableLambda}from"@langchain/core/runnables";import{typeStreamEvent}from"@langchain/core/tracers/log_stream";constGraphState2=Annotation.Root({messages:Annotation<BaseMessage[]>({reducer:(x,y)=>x.concat(y),}),});constnodeFn2=async(_state:typeofGraphState2.State)=>{// Note that we do not pass any `config` through hereconstnestedFn=RunnableLambda.from(async(input:string)=>{returnnewHumanMessage(`Hello from ${input}!`);}).withConfig({runName:"nested"});constresponseMessage=awaitnestedFn.invoke("a nested function");return{messages:[responseMessage]};};// Define a new graphconstworkflow2=newStateGraph(GraphState2).addNode("node",nodeFn2).addEdge(START,"node").addEdge("node",END);constapp2=workflow2.compile({});// Stream intermediate steps from the graphconsteventStream2=app2.streamEvents({messages:[]},{version:"v2"},{includeNames:["nested"]},);constevents2:StreamEvent[]=[];forawait(consteventofeventStream2){console.log(event);events2.push(event);}console.log(`Received ${events2.length} events from the nested function`);
Received 0 events from the nested function
We can see that we get no events.
Next, let's try redeclaring the graph with a node that passes config through
correctly:
// Import from "@langchain/langgraph/web"import{END,START,StateGraph,Annotation,}from"@langchain/langgraph/web";import{BaseMessage}from"@langchain/core/messages";import{typeRunnableConfig,RunnableLambda}from"@langchain/core/runnables";import{typeStreamEvent}from"@langchain/core/tracers/log_stream";constGraphState3=Annotation.Root({messages:Annotation<BaseMessage[]>({reducer:(x,y)=>x.concat(y),}),});// Note the second argument here.constnodeFn3=async(_state:typeofGraphState3.State,config?:RunnableConfig)=>{// If you need to nest deeper, remember to pass `_config` when invokingconstnestedFn=RunnableLambda.from(async(input:string,_config?:RunnableConfig)=>{returnnewHumanMessage(`Hello from ${input}!`);},).withConfig({runName:"nested"});constresponseMessage=awaitnestedFn.invoke("a nested function",config);return{messages:[responseMessage]};};// Define a new graphconstworkflow3=newStateGraph(GraphState3).addNode("node",nodeFn3).addEdge(START,"node").addEdge("node",END);constapp3=workflow3.compile({});// Stream intermediate steps from the graphconsteventStream3=app3.streamEvents({messages:[]},{version:"v2"},{includeNames:["nested"]},);constevents3:StreamEvent[]=[];forawait(consteventofeventStream3){console.log(event);events3.push(event);}console.log(`Received ${events3.length} events from the nested function`);