LangGraph TypeScript

How to use LangGraph with Flutch SDK.

Overview

LangGraph is a framework for building stateful workflows. See LangGraph docs for framework details.

This guide covers:

  • Wrapping LangGraph in AbstractGraphBuilder
  • Accessing agent configuration from nodes
  • Using Flutch services (ModelInitializer, McpRuntimeClient)
  • Adding interactive callbacks

Builder Integration

Wrap your LangGraph workflow in AbstractGraphBuilder.

Basic Setup

typescript
import { Injectable, Inject } from '@nestjs/common';
import { AbstractGraphBuilder, IGraphRequestPayload } from '@flutch/sdk';
import { StateGraph, START, END, Annotation } from '@langchain/langgraph';
import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb';

// Define your state
export const MyState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => [...current, ...update],
    default: () => []
  })
});

@Injectable()
export class MyGraphV1Builder extends AbstractGraphBuilder<'1.0.0'> {
  readonly version = '1.0.0' as const;

  constructor(
    @Inject('CHECKPOINTER')
    private readonly checkpointer: MongoDBSaver,
    private readonly generateNode: GenerateNode
  ) {
    super();
  }

  async buildGraph(payload?: any): Promise<CompiledGraph> {
    // Build LangGraph workflow
    const workflow = new StateGraph(MyState)
      .addNode('generate', this.generateNode.execute.bind(this.generateNode))
      .addEdge(START, 'generate')
      .addEdge('generate', END);

    // Compile with checkpointer (provided by Flutch)
    return workflow.compile({
      checkpointer: this.checkpointer
    });
  }
}

Key points:

  • ✅ Checkpointer injected by Flutch - automatic state persistence
  • ✅ Use bind(this) for node methods
  • ✅ Return compiled graph from buildGraph()

Configuration Access

Access agent settings from config.configurable.graphSettings in your nodes.

Node with Configuration

typescript
import { Injectable, Logger } from '@nestjs/common';
import { LangGraphRunnableConfig } from '@langchain/langgraph';

@Injectable()
export class GenerateNode {
  private readonly logger = new Logger(GenerateNode.name);

  async execute(
    state: MyStateValues,
    config?: LangGraphRunnableConfig
  ): Promise<Partial<MyStateValues>> {

    // Get agent-specific settings
    const graphSettings = config?.configurable?.graphSettings;
    const systemPrompt = graphSettings?.systemPrompt || 'You are a helpful assistant';
    const modelId = graphSettings?.modelSettings?.modelId || 'gpt-4o';
    const temperature = graphSettings?.modelSettings?.temperature || 0.7;

    this.logger.debug(`Using model: ${modelId}, temp: ${temperature}`);

    // Use settings in your logic
    const messages = [
      new SystemMessage(systemPrompt),
      ...state.messages
    ];

    return { messages };
  }
}

Agent config flows automatically:

Agent Config → payload.graphSettings → config.configurable.graphSettings → Your Node

Using Models

Inject ModelInitializer to create models from agent config.

Model Initialization

typescript
import { Injectable } from '@nestjs/common';
import { ModelInitializer, McpRuntimeClient } from '@flutch/sdk';
import { LangGraphRunnableConfig } from '@langchain/langgraph';

@Injectable()
export class GenerateNode {
  constructor(
    private readonly modelInitializer: ModelInitializer,
    private readonly mcpClient: McpRuntimeClient
  ) {}

  async execute(
    state: MyStateValues,
    config?: LangGraphRunnableConfig
  ): Promise<Partial<MyStateValues>> {

    const graphSettings = config?.configurable?.graphSettings;
    const modelId = graphSettings?.modelSettings?.modelId;
    const enabledTools = graphSettings?.availableTools || [];

    // Initialize model
    const model = await this.modelInitializer.initializeChatModel({
      modelId,
      temperature: graphSettings?.modelSettings?.temperature
    });

    // Add tools if configured
    let modelWithTools = model;
    if (enabledTools.length > 0) {
      const tools = await this.mcpClient.getTools(enabledTools);
      modelWithTools = model.bindTools(tools);
    }

    // Use model
    const result = await modelWithTools.invoke(state.messages, config);

    return { messages: [result] };
  }
}

Benefits:

  • ✅ Models from catalog - no API keys in code
  • ✅ Tools automatically filtered and converted
  • ✅ Each agent has different model/tools

Interactive Callbacks

Add callback buttons from LangGraph nodes.

Callback in Node

typescript
import { Injectable } from '@nestjs/common';
import { Callback, CallbackResult, ExtendedCallbackContext } from '@flutch/sdk';

// 1. Create callback handler
@Injectable()
export class ApprovalCallbacks {
  @Callback('approve-action')
  async handleApproval(context: ExtendedCallbackContext): Promise<CallbackResult> {
    const { action } = context.params;

    // Execute action
    await this.executeAction(action);

    return {
      success: true,
      message: 'Action approved!',
      patch: {
        text: '✅ Action completed',
        disableButtons: true
      }
    };
  }

  private async executeAction(action: string) {
    // Action logic
  }
}

// 2. Register in builder
@Injectable()
@WithCallbacks(ApprovalCallbacks)
export class MyGraphV1Builder extends AbstractGraphBuilder<'1.0.0'> {
  // ...
}

// 3. Issue callback from node
@Injectable()
export class PlanNode {
  constructor(
    private readonly callbackService: CallbackService
  ) {}

  async execute(state: MyStateValues): Promise<Partial<MyStateValues>> {
    const plan = await createPlan(state);

    // Create callback button
    const token = await this.callbackService.issue({
      handler: 'approve-action',
      params: { action: plan }
    });

    return {
      output: {
        text: 'Please approve this plan:',
        buttons: [
          { text: 'Approve', callbackToken: token }
        ]
      }
    };
  }
}

Flow:

Node → issue callback → User clicks → Handler executes → Workflow continues

Streaming

Enable streaming with metadata.

typescript
import { StreamChannel } from '@amelie/graph-service-core';

async buildGraph() {
  const workflow = new StateGraph(MyState)
    .addNode(
      'generate',
      this.generateNode.execute.bind(this.generateNode),
      {
        metadata: {
          stream_channel: StreamChannel.TEXT  // Enable streaming
        }
      }
    );

  return workflow.compile({ checkpointer: this.checkpointer });
}

Stream channels:

  • StreamChannel.TEXT - Text tokens
  • StreamChannel.REASONING - Reasoning

Module Setup

Register everything in NestJS module.

Graph Module

typescript
import { Module } from '@nestjs/common';
import { UniversalGraphModule } from '@flutch/sdk';
import { MyGraphV1Builder } from './versions/v1.0.0/builder';
import { GenerateNode } from './nodes/generate.node';
import { ApprovalCallbacks } from './callbacks/approval.callbacks';

@Module({
  imports: [
    UniversalGraphModule.forRoot({
      builders: [MyGraphV1Builder]
    })
  ],
  providers: [
    GenerateNode,
    ApprovalCallbacks
  ]
})
export class MyGraphModule {}

Bootstrap

typescript
import { bootstrap } from '@flutch/sdk';
import { MyGraphModule } from './my-graph.module';

async function main() {
  await bootstrap(MyGraphModule);
}

main();

Complete Example

Full integration with tool calling.

State Definition

typescript
// src/state.model.ts
import { Annotation } from '@langchain/langgraph';
import { BaseMessage, AIMessage } from '@langchain/core/messages';

export const MyState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => [...current, ...update],
    default: () => []
  }),

  output: Annotation<{
    text: string;
    metadata?: any;
  }>({
    reducer: (current, update) => update ?? current,
    default: () => ({ text: '' })
  })
});

export type MyStateValues = typeof MyState.State;

Node Implementation

typescript
// src/nodes/generate.node.ts
import { Injectable, Logger } from '@nestjs/common';
import { ModelInitializer, McpRuntimeClient } from '@flutch/sdk';
import { MyStateValues } from '../state.model';
import { LangGraphRunnableConfig } from '@langchain/langgraph';
import { SystemMessage } from '@langchain/core/messages';

@Injectable()
export class GenerateNode {
  private readonly logger = new Logger(GenerateNode.name);

  constructor(
    private readonly modelInitializer: ModelInitializer,
    private readonly mcpClient: McpRuntimeClient
  ) {}

  async execute(
    state: MyStateValues,
    config?: LangGraphRunnableConfig
  ): Promise<Partial<MyStateValues>> {

    const graphSettings = config?.configurable?.graphSettings;
    const systemPrompt = graphSettings?.systemPrompt || '';
    const modelId = graphSettings?.modelSettings?.modelId || 'gpt-4o';
    const enabledTools = graphSettings?.availableTools || [];

    // Initialize model with tools
    const model = await this.modelInitializer.initializeChatModel({ modelId });

    let modelWithTools = model;
    if (enabledTools.length > 0) {
      const tools = await this.mcpClient.getTools(enabledTools);
      modelWithTools = model.bindTools(tools);
      this.logger.debug(`Configured ${tools.length} tools`);
    }

    // Prepare messages
    const messages = [
      new SystemMessage(systemPrompt),
      ...state.messages
    ];

    // Invoke model
    const result = await modelWithTools.invoke(messages, config);

    return {
      messages: [result],
      output: {
        text: result.content as string,
        metadata: {
          modelId,
          toolsUsed: (result as any).tool_calls?.length || 0
        }
      }
    };
  }

  async executeTools(
    state: MyStateValues,
    config?: LangGraphRunnableConfig
  ): Promise<Partial<MyStateValues>> {

    const lastMessage = state.messages[state.messages.length - 1];
    const toolCalls = (lastMessage as any)?.tool_calls || [];

    const toolMessages = [];
    for (const toolCall of toolCalls) {
      const result = await this.mcpClient.executeTool(
        toolCall.name,
        toolCall.args
      );

      toolMessages.push({
        type: 'tool',
        tool_call_id: toolCall.id,
        content: result.success ? JSON.stringify(result.data) : result.error,
        name: toolCall.name
      });
    }

    return { messages: toolMessages };
  }
}

Builder

typescript
// src/versions/v1.0.0/builder.ts
import { Injectable, Inject } from '@nestjs/common';
import { AbstractGraphBuilder } from '@flutch/sdk';
import { StateGraph, START, END } from '@langchain/langgraph';
import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb';
import { MyState, MyStateValues } from '../../state.model';
import { GenerateNode } from '../../nodes/generate.node';
import { StreamChannel } from '@amelie/graph-service-core';

@Injectable()
export class MyGraphV1Builder extends AbstractGraphBuilder<'1.0.0'> {
  readonly version = '1.0.0' as const;

  constructor(
    @Inject('CHECKPOINTER')
    private readonly checkpointer: MongoDBSaver,
    private readonly generateNode: GenerateNode
  ) {
    super();
  }

  async buildGraph(): Promise<any> {
    const workflow = new StateGraph(MyState)
      // Generate node with streaming
      .addNode(
        'generate',
        this.generateNode.execute.bind(this.generateNode),
        {
          metadata: {
            stream_channel: StreamChannel.TEXT
          }
        }
      )
      // Tools node
      .addNode(
        'tools',
        this.generateNode.executeTools.bind(this.generateNode)
      );

    // Flow
    workflow.addEdge(START, 'generate');

    // Conditional: check if tools needed
    workflow.addConditionalEdges(
      'generate',
      (state: MyStateValues) => {
        const lastMessage = state.messages[state.messages.length - 1];
        return (lastMessage as any)?.tool_calls?.length > 0 ? 'tools' : END;
      },
      {
        tools: 'tools',
        [END]: END
      }
    );

    // Loop back after tools
    workflow.addEdge('tools', 'generate');

    return workflow.compile({
      checkpointer: this.checkpointer
    });
  }
}

Flutch SDK:

LangGraph:

Other frameworks: