Allow non-chained addNode calls when initializing graph #82

Closed
opened 2026-02-15 17:15:33 -05:00 by yindo · 4 comments
Owner

Originally created by @mprado271 on GitHub (Sep 4, 2024).

Description

There is a compile error in a script that creates a simple agent.

Reproduction

nvm use 22;
mkdir langgraph-bug-2024-09-04;
cd langgraph-bug-2024-09-04;
pnpm init;
pnpm install @langchain/langgraph@0.2.2;
touch index.ts;

index.ts

import { Annotation, END, START, StateGraph } from "@langchain/langgraph";

const State = Annotation.Root({
  foo: Annotation<number>,
  bar: Annotation<string[]>,
})

const workflow = new StateGraph(State);

workflow.addNode("hello", (state) => {
  console.log("hello");
  return {
    ...state,
    foo: 1,
  }
});

workflow.addEdge(START, "hello");
workflow.addEdge("hello", END);

const app = workflow.compile();
app.invoke({});

Running the script we get a compile error:

npx ts-node index.ts;

Error

/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
index.ts:18:25 - error TS2345: Argument of type '"hello"' is not assignable to parameter of type '"__start__" | "__end__"'.

18 workflow.addEdge(START, "hello");
                           ~~~~~~~
index.ts:19:18 - error TS2345: Argument of type '"hello"' is not assignable to parameter of type '"__start__" | "__start__"[]'.

19 workflow.addEdge("hello", END);
                    ~~~~~~~

    at createTSError (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:859:12)
    at reportTSError (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:863:19)
    at getOutput (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1077:36)
    at Object.compile (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1433:41)
    at Module.m._compile (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1617:30)
    at Module._extensions..js (node:internal/modules/cjs/loader:1588:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1282:32)
    at Function.Module._load (node:internal/modules/cjs/loader:1098:12)
    at TracingChannel.traceSync (node:diagnostics_channel:315:14) {
  diagnosticCodes: [ 2345, 2345 ]
}
Originally created by @mprado271 on GitHub (Sep 4, 2024). ### Description There is a compile error in a script that creates a simple agent. ### Reproduction ```bash nvm use 22; mkdir langgraph-bug-2024-09-04; cd langgraph-bug-2024-09-04; pnpm init; pnpm install @langchain/langgraph@0.2.2; touch index.ts; ``` index.ts ```ts import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; const State = Annotation.Root({ foo: Annotation<number>, bar: Annotation<string[]>, }) const workflow = new StateGraph(State); workflow.addNode("hello", (state) => { console.log("hello"); return { ...state, foo: 1, } }); workflow.addEdge(START, "hello"); workflow.addEdge("hello", END); const app = workflow.compile(); app.invoke({}); ``` Running the script we get a compile error: ```bash npx ts-node index.ts; ``` ### Error ``` /Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:859 return new TSError(diagnosticText, diagnosticCodes, diagnostics); ^ TSError: ⨯ Unable to compile TypeScript: index.ts:18:25 - error TS2345: Argument of type '"hello"' is not assignable to parameter of type '"__start__" | "__end__"'. 18 workflow.addEdge(START, "hello"); ~~~~~~~ index.ts:19:18 - error TS2345: Argument of type '"hello"' is not assignable to parameter of type '"__start__" | "__start__"[]'. 19 workflow.addEdge("hello", END); ~~~~~~~ at createTSError (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:859:12) at reportTSError (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:863:19) at getOutput (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1077:36) at Object.compile (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1433:41) at Module.m._compile (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1617:30) at Module._extensions..js (node:internal/modules/cjs/loader:1588:10) at Object.require.extensions.<computed> [as .ts] (/Users/mprado/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1621:12) at Module.load (node:internal/modules/cjs/loader:1282:32) at Function.Module._load (node:internal/modules/cjs/loader:1098:12) at TracingChannel.traceSync (node:diagnostics_channel:315:14) { diagnosticCodes: [ 2345, 2345 ] } ```
yindo closed this issue 2026-02-15 17:15:33 -05:00
Author
Owner

@jacoblee93 commented on GitHub (Sep 4, 2024):

Hey @mprado-me, you will actually have to chain these to get proper types:

import { Annotation, END, START, StateGraph } from "@langchain/langgraph";

const State = Annotation.Root({
  foo: Annotation<number>,
  bar: Annotation<string[]>,
})

const workflow = new StateGraph(State)
  .addNode("hello", (state) => {
    console.log("hello");
    return {
      ...state,
      foo: 1,
    }
  })
  .addEdge(START, "hello");
  .addEdge("hello", END);

const app = workflow.compile();
app.invoke({});

We could avoid a hard type error here in the future potentially CC @nfcampos so will leave this open.

@jacoblee93 commented on GitHub (Sep 4, 2024): Hey @mprado-me, you will actually have to chain these to get proper types: ```ts import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; const State = Annotation.Root({ foo: Annotation<number>, bar: Annotation<string[]>, }) const workflow = new StateGraph(State) .addNode("hello", (state) => { console.log("hello"); return { ...state, foo: 1, } }) .addEdge(START, "hello"); .addEdge("hello", END); const app = workflow.compile(); app.invoke({}); ``` We could avoid a hard type error here in the future potentially CC @nfcampos so will leave this open.
Author
Owner

@Masstronaut commented on GitHub (Sep 5, 2024):

The way the StateGraph builder works makes this challenging for a couple reasons:

  1. Each time a builder method (.addEdge(), .addNode(), etc.) is called, it returns a new object with a different type that reflects the edge/node that was added. In TypeScript, this means that even using a mutable object with assignments (see below) would result in errors:
let workflow = new StateGraph(State);
// Error: types don't match
workflow = workflow.addNode("hello", (state) => {
    console.log("hello");
    return {
      ...state,
      foo: 1,
    }
  });

This is because the StateGraph type is modified to reflect that the graph now has a hello node. Doing so is necessary to provide nice type errors when trying to add an edge to a node that doesn't exist on the graph. That leads to the second problem

  1. Having each builder method call return an object of the same type would remove the helpful type errors that occur when trying to add an edge that connects to a node which doesn't exist in the graph. Doing so would degrade the developer experience, and likely result in errors being transferred from compile time to runtime.

With that in mind, @mprado-me does the solution @jacoblee93 provided work for you, or do you have a use case you can explain where chaining builder methods doesn't work? If the latter, it would be really helpful if you can share more details about your use case!

@Masstronaut commented on GitHub (Sep 5, 2024): The way the `StateGraph` builder works makes this challenging for a couple reasons: 1. Each time a builder method (`.addEdge()`, `.addNode()`, etc.) is called, it returns a new object with a different type that reflects the edge/node that was added. In TypeScript, this means that even using a mutable object with assignments (see below) would result in errors: ```ts let workflow = new StateGraph(State); // Error: types don't match workflow = workflow.addNode("hello", (state) => { console.log("hello"); return { ...state, foo: 1, } }); ``` This is because the `StateGraph` type is modified to reflect that the graph now has a `hello` node. Doing so is necessary to provide nice type errors when trying to add an edge to a node that doesn't exist on the graph. That leads to the second problem 2. Having each builder method call return an object of the same type would remove the helpful type errors that occur when trying to add an edge that connects to a node which doesn't exist in the graph. Doing so would degrade the developer experience, and likely result in errors being transferred from compile time to runtime. With that in mind, @mprado-me does the solution @jacoblee93 provided work for you, or do you have a use case you can explain where chaining builder methods doesn't work? If the latter, it would be really helpful if you can share more details about your use case!
Author
Owner

@mprado271 commented on GitHub (Sep 5, 2024):

Hi @jacoblee93 and @Masstronaut , thanks for the response.

@Masstronaut the solution proposed by @jacoblee93 works 100%.

My problem is solved by now.

@mprado271 commented on GitHub (Sep 5, 2024): Hi @jacoblee93 and @Masstronaut , thanks for the response. @Masstronaut the solution proposed by @jacoblee93 works 100%. My problem is solved by now.
Author
Owner

@Masstronaut commented on GitHub (Sep 5, 2024):

Awesome, glad to hear it!

For anyone who may discover this thread later, if you do happen to have a need for separating out each builder method call, you can assign each change to a new variable and it will work. Adapting the code from @mprado-me to do this:

import { Annotation, END, START, StateGraph } from "@langchain/langgraph";

const State = Annotation.Root({
  foo: Annotation<number>,
  bar: Annotation<string[]>,
})

const workflow = new StateGraph(State);
const withNode = workflow.addNode("hello", (state) => {
    console.log("hello");
    return {
      ...state,
      foo: 1,
    }
  });
const withStart = withNode.addEdge(START, "hello");
const finalGraph = withStart.addEdge("hello", END);

const app = finalGraph.compile();
app.invoke({});

Since each result is stored in a new variable, it avoids the issue of not being able to assign a value to a variable with a different type.

@Masstronaut commented on GitHub (Sep 5, 2024): Awesome, glad to hear it! For anyone who may discover this thread later, if you do happen to have a need for separating out each builder method call, you can assign each change to a new variable and it will work. Adapting the code from @mprado-me to do this: ```ts import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; const State = Annotation.Root({ foo: Annotation<number>, bar: Annotation<string[]>, }) const workflow = new StateGraph(State); const withNode = workflow.addNode("hello", (state) => { console.log("hello"); return { ...state, foo: 1, } }); const withStart = withNode.addEdge(START, "hello"); const finalGraph = withStart.addEdge("hello", END); const app = finalGraph.compile(); app.invoke({}); ``` Since each result is stored in a new variable, it avoids the issue of not being able to assign a value to a variable with a different type.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#82