Async_hooks cannot be used in browser (@langchain/core version ^1.0.0-alpha.x ) #361

Closed
opened 2026-02-15 18:16:12 -05:00 by yindo · 8 comments
Owner

Originally created by @SvetlaGeorgieva on GitHub (Sep 24, 2025).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).

Example Code

Define some test code:

// Import from "@langchain/langgraph/web"
import {
  END,
  START,
  StateGraph,
  Annotation,
} from '@langchain/langgraph/web';
import { BaseMessage, HumanMessage } from '@langchain/core/messages';

const GraphState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
  }),
});

export async function runAiAgent(): Promise<string> {
  const nodeFn = async (_state: typeof GraphState.State) => ({
    messages: [new HumanMessage('Hello from the browser!')]
  });

  // Define a new graph
  const workflow = new StateGraph(GraphState)
    .addNode('node', nodeFn)
    .addEdge(START, 'node')
    .addEdge('node', END);

  const app = workflow.compile({});

  // Use the Runnable
  const finalState = await app.invoke({
    messages: [],
  });

  const lastMessage = finalState.messages[finalState.messages.length - 1];
  return typeof lastMessage.content === 'string'
    ? lastMessage.content
    : JSON.stringify(lastMessage.content);
}

Then use it somewhere in the app (in my case from an Angular component):

import { runAiAgent } from '../../ai/ai-agent'; // or your path

// some other code


  public async testAI() {
    // Simple usage
    const message = await runAiAgent();
    console.log(message); // "Hello from the browser!"
  }

Error Message and Stack Trace (if applicable)

context.js:15 Uncaught TypeError: import_node_async_hooks.AsyncLocalStorage is not a constructor
at context.js:15:61

Description

I am using langgraph and langchain when creating an agent in Electron app (with Angular).
I use the integrations with OpenaAI and Anthropic provider (via the @langchain/openaiand @langchain/anthropic).
To make it work I am using imports from @langchain/langgraph/web.

import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';
import { AIMessage, HumanMessage, BaseMessage, SystemMessage, isAIMessageChunk } from '@langchain/core/messages';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import {
    END,
    START,
    StateGraph,
    Annotation,
} from '@langchain/langgraph/web';

When using the latest 0.x.x versions of the libraries, I have no issues running the agent. However, using the 1.0.0-alpha.x versions cause an exception:

context.js:15 Uncaught TypeError: import_node_async_hooks.AsyncLocalStorage is not a constructor
    at context.js:15:61

The specific versions when there are no errors are:

    "@langchain/anthropic": "^0.3.27",
    "@langchain/core": "^0.3.75",
    "@langchain/langgraph": "^0.4.9",
    "@langchain/openai": "^0.6.11",

The versions when there are errors:

    "@langchain/anthropic": "^1.0.0-alpha.1",
    "@langchain/core": "^1.0.0-alpha.1",
    "@langchain/langgraph": "^1.0.0-alpha.1",
    "@langchain/openai": "^1.0.0-alpha.1",

The issue seems to be coming from @langchain/core version 1.0.0-alpha.1, which uses Node.js's AsyncLocalStorage from the node:async_hooks module. However, in an Electron renderer process, Node.js APIs like async_hooks are not available by default.

P.S. For the 0.x.x versions I found a similar issue, which was resolved by using imports from @langchain/langgraph/web:

It would be great if such a fix can be applied for the upcoming 1.0.0 version of @langchain/core

System Info

  • Electron app with Angular and .NET
  • Current machine: MacOS
  • Node version: v22.12.0
  • NPM version 10.9.0
Originally created by @SvetlaGeorgieva on GitHub (Sep 24, 2025). ### Checked other resources - [x] I added a very descriptive title to this issue. - [x] I searched the LangGraph.js documentation with the integrated search. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangGraph.js rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package). ### Example Code Define some test code: ```typescript // Import from "@langchain/langgraph/web" import { END, START, StateGraph, Annotation, } from '@langchain/langgraph/web'; import { BaseMessage, HumanMessage } from '@langchain/core/messages'; const GraphState = Annotation.Root({ messages: Annotation<BaseMessage[]>({ reducer: (x, y) => x.concat(y), }), }); export async function runAiAgent(): Promise<string> { const nodeFn = async (_state: typeof GraphState.State) => ({ messages: [new HumanMessage('Hello from the browser!')] }); // Define a new graph const workflow = new StateGraph(GraphState) .addNode('node', nodeFn) .addEdge(START, 'node') .addEdge('node', END); const app = workflow.compile({}); // Use the Runnable const finalState = await app.invoke({ messages: [], }); const lastMessage = finalState.messages[finalState.messages.length - 1]; return typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content); } ``` Then use it somewhere in the app (in my case from an Angular component): ``` typescript import { runAiAgent } from '../../ai/ai-agent'; // or your path // some other code public async testAI() { // Simple usage const message = await runAiAgent(); console.log(message); // "Hello from the browser!" } ``` ### Error Message and Stack Trace (if applicable) context.js:15 Uncaught TypeError: import_node_async_hooks.AsyncLocalStorage is not a constructor at context.js:15:61 ### Description I am using langgraph and langchain when creating an agent in Electron app (with Angular). I use the integrations with OpenaAI and Anthropic provider (via the `@langchain/openai`and `@langchain/anthropic`). To make it work I am using imports from `@langchain/langgraph/web`. ``` import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai'; import { ChatAnthropic } from '@langchain/anthropic'; import { AIMessage, HumanMessage, BaseMessage, SystemMessage, isAIMessageChunk } from '@langchain/core/messages'; import { ToolNode } from '@langchain/langgraph/prebuilt'; import { END, START, StateGraph, Annotation, } from '@langchain/langgraph/web'; ``` When using the latest 0.x.x versions of the libraries, I have no issues running the agent. However, using the 1.0.0-alpha.x versions cause an exception: ``` context.js:15 Uncaught TypeError: import_node_async_hooks.AsyncLocalStorage is not a constructor at context.js:15:61 ``` The specific versions when there are no errors are: ``` "@langchain/anthropic": "^0.3.27", "@langchain/core": "^0.3.75", "@langchain/langgraph": "^0.4.9", "@langchain/openai": "^0.6.11", ``` The versions when there are errors: ``` "@langchain/anthropic": "^1.0.0-alpha.1", "@langchain/core": "^1.0.0-alpha.1", "@langchain/langgraph": "^1.0.0-alpha.1", "@langchain/openai": "^1.0.0-alpha.1", ``` The issue seems to be coming from @langchain/core version 1.0.0-alpha.1, which uses Node.js's `AsyncLocalStorage` from the node:async_hooks module. However, in an Electron renderer process, Node.js APIs like async_hooks are not available by default. P.S. For the 0.x.x versions I found a similar issue, which was resolved by using imports from `@langchain/langgraph/web`: - https://github.com/langchain-ai/langgraphjs/issues/81 - https://langchain-ai.github.io/langgraphjs/how-tos/use-in-web-environments/ It would be great if such a fix can be applied for the upcoming 1.0.0 version of `@langchain/core` ### System Info - Electron app with Angular and .NET - Current machine: MacOS - Node version: v22.12.0 - NPM version 10.9.0
yindo closed this issue 2026-02-15 18:16:12 -05:00
Author
Owner

@dhrumil83 commented on GitHub (Oct 20, 2025):

+1 – same issue here.

Browser build (Vite / esbuild)
@langchain/core@1.0.0-alpha.x
Worked fine in 0.3.x, now breaks due to node:async_hooks import.

Currently pinned back to 0.3.x until fixed.

@dhrumil83 commented on GitHub (Oct 20, 2025): +1 – same issue here. Browser build (Vite / esbuild) @langchain/core@1.0.0-alpha.x Worked fine in 0.3.x, now breaks due to `node:async_hooks` import. Currently pinned back to 0.3.x until fixed.
Author
Owner

@lidianhao123 commented on GitHub (Nov 7, 2025):

+1 same issue here

Is there a plan to support the browser environment? When is it expected to be available?
@dqbd @jacoblee93

@lidianhao123 commented on GitHub (Nov 7, 2025): +1 same issue here Is there a plan to support the browser environment? When is it expected to be available? @dqbd @jacoblee93
Author
Owner

@jacoblee93 commented on GitHub (Nov 7, 2025):

I believe @hntrl is looking at adding support back!

@jacoblee93 commented on GitHub (Nov 7, 2025): I believe @hntrl is looking at adding support back!
Author
Owner

@xkcm commented on GitHub (Nov 8, 2025):

For everyone struggling with this issue, I have a temporary fix which seems to have worked for me. I've added a simple shim generated by chatgpt for async_hooks in my project, here's the shim:

async_hooks-zone.ts

import "zone.js";

type ZoneLike = {
  get(key: string): any;
  fork(spec: { name?: string; properties?: Record<string, any> }): ZoneLike;
  run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
};
declare const Zone: { current: ZoneLike };

export class AsyncLocalStorage<T> {
    private _key: string;
    private _disabled = false;

    constructor() {
        this._key = `ALS_${Math.random().toString(36).slice(2)}`;
    }

    disable(): void {
        this._disabled = true;
    }

    getStore(): T | undefined {
        return Zone.current.get(this._key) as T | undefined;
    }

    run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R {
        if (this._disabled) return callback(...args);
        const zone = Zone.current.fork({
            name: "async-local-storage",
            properties: { [this._key]: store },
        });
        return zone.run(callback, undefined, args);
    }

    enterWith(store: T): void {
        if (this._disabled) return;
        Zone.current.fork({
            name: "async-local-enter",
            properties: { [this._key]: store },
        }).run(() => void 0);
    }
}

export default { AsyncLocalStorage };

You need to also install zone.js package.
Then I've added aliases to the configuration, in my case I'm using vite for frontend bundling:

vite.config.ts

import { resolve } from "node:path";

export default defineConfig({
        // ...
        resolve: {
            alias: {
                // temp fix until langchain fixes support for browser environments,
                // related issue on github: https://github.com/langchain-ai/langgraphjs/issues/1699
                "async_hooks": resolve("<path to the shim>"),
                "node:async_hooks": resolve("<path to the shim>"),
            },
        },
        // ...
});

This seems to have resolved the issue, I'm using a simple react agent with tools, so I'm not sure if the rest will work correctly.

@xkcm commented on GitHub (Nov 8, 2025): For everyone struggling with this issue, I have a temporary fix which seems to have worked for me. I've added a simple shim generated by chatgpt for async_hooks in my project, here's the shim: `async_hooks-zone.ts` ```ts import "zone.js"; type ZoneLike = { get(key: string): any; fork(spec: { name?: string; properties?: Record<string, any> }): ZoneLike; run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T; }; declare const Zone: { current: ZoneLike }; export class AsyncLocalStorage<T> { private _key: string; private _disabled = false; constructor() { this._key = `ALS_${Math.random().toString(36).slice(2)}`; } disable(): void { this._disabled = true; } getStore(): T | undefined { return Zone.current.get(this._key) as T | undefined; } run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R { if (this._disabled) return callback(...args); const zone = Zone.current.fork({ name: "async-local-storage", properties: { [this._key]: store }, }); return zone.run(callback, undefined, args); } enterWith(store: T): void { if (this._disabled) return; Zone.current.fork({ name: "async-local-enter", properties: { [this._key]: store }, }).run(() => void 0); } } export default { AsyncLocalStorage }; ``` You need to also install `zone.js` package. Then I've added aliases to the configuration, in my case I'm using vite for frontend bundling: `vite.config.ts` ```ts import { resolve } from "node:path"; export default defineConfig({ // ... resolve: { alias: { // temp fix until langchain fixes support for browser environments, // related issue on github: https://github.com/langchain-ai/langgraphjs/issues/1699 "async_hooks": resolve("<path to the shim>"), "node:async_hooks": resolve("<path to the shim>"), }, }, // ... }); ``` This seems to have resolved the issue, I'm using a simple react agent with tools, so I'm not sure if the rest will work correctly.
Author
Owner

@Kingtous commented on GitHub (Jan 15, 2026):

Any updates here?

@Kingtous commented on GitHub (Jan 15, 2026): Any updates here?
Author
Owner

@thedomeffm commented on GitHub (Jan 19, 2026):

I am building a internal desktop application with tauri + vue.js and I am running into the same problem here. Would be great to get some feedback if the support is coming back for not-node.js runtimes.

I would need to switch away from langchain.

@thedomeffm commented on GitHub (Jan 19, 2026): I am building a internal desktop application with tauri + vue.js and I am running into the same problem here. Would be great to get some feedback if the support is coming back for not-node.js runtimes. I would need to switch away from langchain.
Author
Owner

@liwei-link commented on GitHub (Jan 21, 2026):

+1

@liwei-link commented on GitHub (Jan 21, 2026): +1
Author
Owner

@hntrl commented on GitHub (Jan 23, 2026):

Hi all! Believe this was fixed with https://github.com/langchain-ai/langchainjs/pull/9229/changes. Just to confirm I have this MRE when using StateGraph in a web environment, and I'm able to run this without issues.

I'm going to close this for now since the original issue was opened against an alpha version of langchain, but if people are still encountering this it would be much appreciated if you could open a new issue that has specifics about your environment setup:

import {
  StateGraph,
  START,
  END,
  GraphNode,
  ConditionalEdgeRouter,
} from "@langchain/langgraph/web";
import { z } from "zod";
import { StateSchema } from "@langchain/langgraph/web";

const GraphState = new StateSchema({
  // Input text from user
  input: z.string().default(""),

  // Validated/cleaned input
  validated: z.string().default(""),

  // Processed text (transformations applied)
  processed: z.string().default(""),

  // Analysis results
  analysis: z
    .object({
      wordCount: z.number(),
      charCount: z.number(),
      hasNumbers: z.boolean(),
      avgWordLength: z.number(),
    })
    .default({
      wordCount: 0,
      charCount: 0,
      hasNumbers: false,
      avgWordLength: 0,
    }),

  // Final formatted output
  output: z.string().default(""),

  // Track which nodes were executed
  executionPath: z.array(z.string()).default([]),
});

type State = z.infer<typeof GraphState>;

// =============================================================================
// 2. Define Node Functions
// =============================================================================

/**
 * Validate and clean the input
 */
const validateNode: GraphNode<typeof GraphState> = async (state) => {
  // Simulate async operation (like you would with real API calls)
  await delay(300);
  const cleaned = state.input.trim();

  return {
    validated: cleaned,
    executionPath: ["validate"],
  };
};

/**
 * Process the text with transformations
 */
const processNode: GraphNode<typeof GraphState> = async (state) => {
  await delay(400);

  // Apply some transformations
  const processed = state.validated
    .split(" ")
    .map((word) => {
      // Capitalize first letter of each word
      return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
    })
    .join(" ");

  return {
    processed,
    executionPath: ["process"],
  };
};

/**
 * Analyze the processed text
 */
const analyzeNode: GraphNode<typeof GraphState> = async (state) => {
  await delay(350);

  const words = state.processed.split(/\s+/).filter(Boolean);
  const wordCount = words.length;
  const charCount = state.processed.length;
  const hasNumbers = /\d/.test(state.processed);
  const avgWordLength =
    wordCount > 0
      ? Math.round(
          (words.reduce((sum, w) => sum + w.length, 0) / wordCount) * 10,
        ) / 10
      : 0;

  return {
    analysis: {
      wordCount,
      charCount,
      hasNumbers,
      avgWordLength,
    },
    executionPath: ["analyze"],
  };
};

/**
 * Format node - for shorter texts
 */
const formatNode: GraphNode<typeof GraphState> = async (state) => {
  await delay(250);

  const formatted = `📝 "${state.processed}"

📊 Statistics:
  • Words: ${state.analysis.wordCount}
  • Characters: ${state.analysis.charCount}
  • Contains numbers: ${state.analysis.hasNumbers ? "Yes" : "No"}
  • Avg word length: ${state.analysis.avgWordLength}`;

  return {
    output: formatted,
    executionPath: ["format"],
  };
};

/**
 * Enhance node - for longer texts (adds summary)
 */
const enhanceNode: GraphNode<typeof GraphState> = async (state) => {
  await delay(400);

  // Create a simple "summary" by taking first few words
  const words = state.processed.split(/\s+/);
  const summary = words.slice(0, 5).join(" ") + (words.length > 5 ? "..." : "");

  const enhanced = `📝 Full text: "${state.processed}"

📋 Summary: ${summary}

📊 Analysis:
  • Total words: ${state.analysis.wordCount}
  • Total characters: ${state.analysis.charCount}
  • Contains numbers: ${state.analysis.hasNumbers ? "Yes ✓" : "No ✗"}
  • Average word length: ${state.analysis.avgWordLength} chars
  • Text complexity: ${state.analysis.avgWordLength > 5 ? "Higher" : "Normal"}`;

  return {
    output: enhanced,
    executionPath: ["enhance"],
  };
};

// =============================================================================
// 3. Define Conditional Edge Router
// =============================================================================

/**
 * Route to different output nodes based on text length
 */
const routeByLength: ConditionalEdgeRouter<{
  InputSchema: typeof GraphState;
  Nodes: "format" | "enhance";
}> = (state) => {
  // If text has more than 5 words, use enhance node
  if (state.analysis.wordCount > 5) {
    return "enhance";
  }
  return "format";
};

// =============================================================================
// 4. Build the StateGraph
// =============================================================================

const workflow = new StateGraph(GraphState)
  // Add all nodes
  .addNode("validate", validateNode)
  .addNode("process", processNode)
  .addNode("analyze", analyzeNode)
  .addNode("format", formatNode)
  .addNode("enhance", enhanceNode)

  // Define edges
  .addEdge(START, "validate")
  .addEdge("validate", "process")
  .addEdge("process", "analyze")

  // Conditional edge: route based on analysis
  .addConditionalEdges("analyze", routeByLength, {
    format: "format",
    enhance: "enhance",
  })

  // Both output nodes lead to END
  .addEdge("format", END)
  .addEdge("enhance", END);

// Compile the graph
const graph = workflow.compile();

// =============================================================================
// 5. UI Integration
// =============================================================================

// Helper function for delays
function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// Display the graph structure
function displayGraphStructure(): void {
  const visual = document.getElementById("graph-visual");
  if (visual) {
    visual.textContent = `
┌─────────────────────────────────────────────────────────────┐
│                    StateGraph Structure                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   START                                                     │
│     │                                                       │
│     ▼                                                       │
│  ┌──────────┐                                               │
│  │ validate │  ← Clean and validate input                   │
│  └────┬─────┘                                               │
│       │                                                     │
│       ▼                                                     │
│  ┌──────────┐                                               │
│  │ process  │  ← Transform text (capitalize words)          │
│  └────┬─────┘                                               │
│       │                                                     │
│       ▼                                                     │
│  ┌──────────┐                                               │
│  │ analyze  │  ← Compute statistics                         │
│  └────┬─────┘                                               │
│       │                                                     │
│       ▼  (conditional edge)                                 │
│   wordCount > 5?                                            │
│      /    \\                                                 │
│     /      \\                                                │
│    ▼        ▼                                               │
│ ┌────────┐  ┌─────────┐                                     │
│ │ format │  │ enhance │  ← Different output formatting      │
│ └───┬────┘  └────┬────┘                                     │
│     │            │                                          │
│     └──────┬─────┘                                          │
│            ▼                                                │
│          END                                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
`;
  }
}

// Format state update for display
function formatStateUpdate(
  nodeName: string,
  state: typeof GraphState.Update,
): string {
  const relevantState: Record<string, unknown> = {};

  // Only show the fields that were updated
  if ("validated" in state) relevantState.validated = state.validated;
  if ("processed" in state) relevantState.processed = state.processed;
  if ("analysis" in state) relevantState.analysis = state.analysis;
  if ("output" in state) relevantState.output = state.output;
  if ("executionPath" in state)
    relevantState.executionPath = state.executionPath;

  return JSON.stringify(relevantState, null, 2);
}

// Run the graph with streaming
async function runGraph(inputText: string): Promise<void> {
  const output = document.getElementById("output");
  const runBtn = document.getElementById("run-btn") as HTMLButtonElement;

  if (!output || !runBtn) return;

  // Clear output and disable button
  output.innerHTML = "";
  runBtn.disabled = true;

  try {
    // Stream the execution
    const stream = await graph.stream(
      { input: inputText },
      { streamMode: "updates" },
    );

    for await (const update of stream) {
      // update is a record of { nodeName: stateUpdate }
      for (const [nodeName, stateUpdate] of Object.entries(update)) {
        const div = document.createElement("div");
        div.className = "node-update";

        const nameDiv = document.createElement("div");
        nameDiv.className = "node-name";
        nameDiv.textContent = `Node: ${nodeName}`;

        const stateDiv = document.createElement("div");
        stateDiv.className = "state-change";
        stateDiv.textContent = formatStateUpdate(
          nodeName,
          stateUpdate as Partial<State>,
        );

        div.appendChild(nameDiv);
        div.appendChild(stateDiv);
        output.appendChild(div);

        // Scroll to bottom
        output.scrollTop = output.scrollHeight;
      }
    }

    // Get final state
    const finalState = await graph.invoke({ input: inputText });

    const finalDiv = document.createElement("div");
    finalDiv.className = "node-update final-state";

    const finalNameDiv = document.createElement("div");
    finalNameDiv.className = "node-name";
    finalNameDiv.textContent = "✓ Final Output";

    const finalStateDiv = document.createElement("div");
    finalStateDiv.className = "state-change";
    finalStateDiv.style.whiteSpace = "pre-wrap";
    finalStateDiv.textContent = finalState.output;

    finalDiv.appendChild(finalNameDiv);
    finalDiv.appendChild(finalStateDiv);
    output.appendChild(finalDiv);
  } catch (error) {
    output.innerHTML = `<div style="color: red;">Error: ${error instanceof Error ? error.message : String(error)}</div>`;
  } finally {
    runBtn.disabled = false;
  }
}

// Initialize
document.addEventListener("DOMContentLoaded", () => {
  displayGraphStructure();

  const runBtn = document.getElementById("run-btn");
  const inputEl = document.getElementById("input") as HTMLInputElement;

  runBtn?.addEventListener("click", () => {
    const inputText = inputEl?.value || "Hello World";
    runGraph(inputText);
  });

  // Also run on Enter key
  inputEl?.addEventListener("keypress", (e) => {
    if (e.key === "Enter") {
      const inputText = inputEl?.value || "Hello World";
      runGraph(inputText);
    }
  });
});

@hntrl commented on GitHub (Jan 23, 2026): Hi all! Believe this was fixed with https://github.com/langchain-ai/langchainjs/pull/9229/changes. Just to confirm I have this MRE when using StateGraph in a web environment, and I'm able to run this without issues. I'm going to close this for now since the original issue was opened against an alpha version of langchain, but if people are still encountering this it would be much appreciated if you could open a new issue that has specifics about your environment setup: ```ts import { StateGraph, START, END, GraphNode, ConditionalEdgeRouter, } from "@langchain/langgraph/web"; import { z } from "zod"; import { StateSchema } from "@langchain/langgraph/web"; const GraphState = new StateSchema({ // Input text from user input: z.string().default(""), // Validated/cleaned input validated: z.string().default(""), // Processed text (transformations applied) processed: z.string().default(""), // Analysis results analysis: z .object({ wordCount: z.number(), charCount: z.number(), hasNumbers: z.boolean(), avgWordLength: z.number(), }) .default({ wordCount: 0, charCount: 0, hasNumbers: false, avgWordLength: 0, }), // Final formatted output output: z.string().default(""), // Track which nodes were executed executionPath: z.array(z.string()).default([]), }); type State = z.infer<typeof GraphState>; // ============================================================================= // 2. Define Node Functions // ============================================================================= /** * Validate and clean the input */ const validateNode: GraphNode<typeof GraphState> = async (state) => { // Simulate async operation (like you would with real API calls) await delay(300); const cleaned = state.input.trim(); return { validated: cleaned, executionPath: ["validate"], }; }; /** * Process the text with transformations */ const processNode: GraphNode<typeof GraphState> = async (state) => { await delay(400); // Apply some transformations const processed = state.validated .split(" ") .map((word) => { // Capitalize first letter of each word return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .join(" "); return { processed, executionPath: ["process"], }; }; /** * Analyze the processed text */ const analyzeNode: GraphNode<typeof GraphState> = async (state) => { await delay(350); const words = state.processed.split(/\s+/).filter(Boolean); const wordCount = words.length; const charCount = state.processed.length; const hasNumbers = /\d/.test(state.processed); const avgWordLength = wordCount > 0 ? Math.round( (words.reduce((sum, w) => sum + w.length, 0) / wordCount) * 10, ) / 10 : 0; return { analysis: { wordCount, charCount, hasNumbers, avgWordLength, }, executionPath: ["analyze"], }; }; /** * Format node - for shorter texts */ const formatNode: GraphNode<typeof GraphState> = async (state) => { await delay(250); const formatted = `📝 "${state.processed}" 📊 Statistics: • Words: ${state.analysis.wordCount} • Characters: ${state.analysis.charCount} • Contains numbers: ${state.analysis.hasNumbers ? "Yes" : "No"} • Avg word length: ${state.analysis.avgWordLength}`; return { output: formatted, executionPath: ["format"], }; }; /** * Enhance node - for longer texts (adds summary) */ const enhanceNode: GraphNode<typeof GraphState> = async (state) => { await delay(400); // Create a simple "summary" by taking first few words const words = state.processed.split(/\s+/); const summary = words.slice(0, 5).join(" ") + (words.length > 5 ? "..." : ""); const enhanced = `📝 Full text: "${state.processed}" 📋 Summary: ${summary} 📊 Analysis: • Total words: ${state.analysis.wordCount} • Total characters: ${state.analysis.charCount} • Contains numbers: ${state.analysis.hasNumbers ? "Yes ✓" : "No ✗"} • Average word length: ${state.analysis.avgWordLength} chars • Text complexity: ${state.analysis.avgWordLength > 5 ? "Higher" : "Normal"}`; return { output: enhanced, executionPath: ["enhance"], }; }; // ============================================================================= // 3. Define Conditional Edge Router // ============================================================================= /** * Route to different output nodes based on text length */ const routeByLength: ConditionalEdgeRouter<{ InputSchema: typeof GraphState; Nodes: "format" | "enhance"; }> = (state) => { // If text has more than 5 words, use enhance node if (state.analysis.wordCount > 5) { return "enhance"; } return "format"; }; // ============================================================================= // 4. Build the StateGraph // ============================================================================= const workflow = new StateGraph(GraphState) // Add all nodes .addNode("validate", validateNode) .addNode("process", processNode) .addNode("analyze", analyzeNode) .addNode("format", formatNode) .addNode("enhance", enhanceNode) // Define edges .addEdge(START, "validate") .addEdge("validate", "process") .addEdge("process", "analyze") // Conditional edge: route based on analysis .addConditionalEdges("analyze", routeByLength, { format: "format", enhance: "enhance", }) // Both output nodes lead to END .addEdge("format", END) .addEdge("enhance", END); // Compile the graph const graph = workflow.compile(); // ============================================================================= // 5. UI Integration // ============================================================================= // Helper function for delays function delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } // Display the graph structure function displayGraphStructure(): void { const visual = document.getElementById("graph-visual"); if (visual) { visual.textContent = ` ┌─────────────────────────────────────────────────────────────┐ │ StateGraph Structure │ ├─────────────────────────────────────────────────────────────┤ │ │ │ START │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ validate │ ← Clean and validate input │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ process │ ← Transform text (capitalize words) │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ analyze │ ← Compute statistics │ │ └────┬─────┘ │ │ │ │ │ ▼ (conditional edge) │ │ wordCount > 5? │ │ / \\ │ │ / \\ │ │ ▼ ▼ │ │ ┌────────┐ ┌─────────┐ │ │ │ format │ │ enhance │ ← Different output formatting │ │ └───┬────┘ └────┬────┘ │ │ │ │ │ │ └──────┬─────┘ │ │ ▼ │ │ END │ │ │ └─────────────────────────────────────────────────────────────┘ `; } } // Format state update for display function formatStateUpdate( nodeName: string, state: typeof GraphState.Update, ): string { const relevantState: Record<string, unknown> = {}; // Only show the fields that were updated if ("validated" in state) relevantState.validated = state.validated; if ("processed" in state) relevantState.processed = state.processed; if ("analysis" in state) relevantState.analysis = state.analysis; if ("output" in state) relevantState.output = state.output; if ("executionPath" in state) relevantState.executionPath = state.executionPath; return JSON.stringify(relevantState, null, 2); } // Run the graph with streaming async function runGraph(inputText: string): Promise<void> { const output = document.getElementById("output"); const runBtn = document.getElementById("run-btn") as HTMLButtonElement; if (!output || !runBtn) return; // Clear output and disable button output.innerHTML = ""; runBtn.disabled = true; try { // Stream the execution const stream = await graph.stream( { input: inputText }, { streamMode: "updates" }, ); for await (const update of stream) { // update is a record of { nodeName: stateUpdate } for (const [nodeName, stateUpdate] of Object.entries(update)) { const div = document.createElement("div"); div.className = "node-update"; const nameDiv = document.createElement("div"); nameDiv.className = "node-name"; nameDiv.textContent = `Node: ${nodeName}`; const stateDiv = document.createElement("div"); stateDiv.className = "state-change"; stateDiv.textContent = formatStateUpdate( nodeName, stateUpdate as Partial<State>, ); div.appendChild(nameDiv); div.appendChild(stateDiv); output.appendChild(div); // Scroll to bottom output.scrollTop = output.scrollHeight; } } // Get final state const finalState = await graph.invoke({ input: inputText }); const finalDiv = document.createElement("div"); finalDiv.className = "node-update final-state"; const finalNameDiv = document.createElement("div"); finalNameDiv.className = "node-name"; finalNameDiv.textContent = "✓ Final Output"; const finalStateDiv = document.createElement("div"); finalStateDiv.className = "state-change"; finalStateDiv.style.whiteSpace = "pre-wrap"; finalStateDiv.textContent = finalState.output; finalDiv.appendChild(finalNameDiv); finalDiv.appendChild(finalStateDiv); output.appendChild(finalDiv); } catch (error) { output.innerHTML = `<div style="color: red;">Error: ${error instanceof Error ? error.message : String(error)}</div>`; } finally { runBtn.disabled = false; } } // Initialize document.addEventListener("DOMContentLoaded", () => { displayGraphStructure(); const runBtn = document.getElementById("run-btn"); const inputEl = document.getElementById("input") as HTMLInputElement; runBtn?.addEventListener("click", () => { const inputText = inputEl?.value || "Hello World"; runGraph(inputText); }); // Also run on Enter key inputEl?.addEventListener("keypress", (e) => { if (e.key === "Enter") { const inputText = inputEl?.value || "Hello World"; runGraph(inputText); } }); }); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#361