langchain-hs-graph-0.0.5.0: src/Langchain/Graph/StateGraph.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Langchain.Graph.StateGraph
Description : Core StateGraph engine and pure state reducer combinators
Copyright : (c) 2025-2026 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
Provides explicit, inspectable, stateful agent graph workflow primitives following graph laws.
-}
module Langchain.Graph.StateGraph
( NodeId
, startNodeId
, endNodeId
, StateReducer
, Node (..)
, Edge (..)
, StateGraph (..)
, emptyStateGraph
, addNode
, addEdge
, addConditionalEdge
, compileGraph
, runGraph
, appendMessagesReducer
, replaceFieldReducer
) where
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.IO.Class (MonadIO)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Langchain.Core.Error (LangchainError, internalError)
import Langchain.Core.Model (Message)
-- | Unique node identifier in a StateGraph
type NodeId = Text
-- | Reserved start node constant
startNodeId :: NodeId
startNodeId = "__start__"
-- | Reserved end node constant
endNodeId :: NodeId
endNodeId = "__end__"
-- | Pure state merge function reducer satisfying associative algebra laws
type StateReducer s = s -> s -> s
-- | Node execution action in a StateGraph
data Node s m = Node
{ nodeId :: NodeId
, nodeAction :: s -> m (Either LangchainError s)
}
instance Show (Node s m) where
show n = "Node { nodeId = " ++ show (nodeId n) ++ " }"
-- | Edge representation for transition between graph nodes
data Edge s m
= StaticEdge NodeId
| ConditionalEdge (s -> m (Either LangchainError NodeId))
-- | Declarative StateGraph specification
data StateGraph s m = StateGraph
{ graphNodes :: Map NodeId (Node s m)
, graphEdges :: Map NodeId (Edge s m)
, graphReducer :: StateReducer s
}
-- | Construct an empty StateGraph with a given pure StateReducer
emptyStateGraph :: StateReducer s -> StateGraph s m
emptyStateGraph reducer =
StateGraph
{ graphNodes = Map.empty
, graphEdges = Map.empty
, graphReducer = reducer
}
-- | Add an execution node to the StateGraph
addNode :: NodeId -> (s -> m (Either LangchainError s)) -> StateGraph s m -> StateGraph s m
addNode name action g =
let n = Node {nodeId = name, nodeAction = action}
in g {graphNodes = Map.insert name n (graphNodes g)}
-- | Add a static transition edge between two nodes
addEdge :: NodeId -> NodeId -> StateGraph s m -> StateGraph s m
addEdge fromNode toNode g =
g {graphEdges = Map.insert fromNode (StaticEdge toNode) (graphEdges g)}
-- | Add a dynamic conditional transition edge
addConditionalEdge ::
NodeId -> (s -> m (Either LangchainError NodeId)) -> StateGraph s m -> StateGraph s m
addConditionalEdge fromNode condFn g =
g {graphEdges = Map.insert fromNode (ConditionalEdge condFn) (graphEdges g)}
-- | Validate state graph invariants. Returns the validated StateGraph or an error.
compileGraph :: StateGraph s m -> Either LangchainError (StateGraph s m)
compileGraph sg =
if Map.null (graphNodes sg)
then Left $ internalError "StateGraph must contain at least one node" (Just "compileGraph") Nothing
else Right sg
-- | Execute a StateGraph from start node or specified currentId to endNodeId
runGraph ::
(MonadIO m, MonadError LangchainError m) =>
StateGraph s m ->
NodeId ->
s ->
m s
runGraph sg@StateGraph {..} currentId state
| currentId == endNodeId = pure state
| otherwise = case Map.lookup currentId graphNodes of
Just node -> do
eNextState <- nodeAction node state
case eNextState of
Left err -> throwError err
Right stepState -> do
let mergedState = graphReducer state stepState
case Map.lookup currentId graphEdges of
Nothing -> pure mergedState
Just (StaticEdge nextId) -> runGraph sg nextId mergedState
Just (ConditionalEdge cond) -> do
eNextId <- cond mergedState
case eNextId of
Left err -> throwError err
Right nextId -> runGraph sg nextId mergedState
Nothing -> case Map.lookup currentId graphEdges of
Just (StaticEdge nextId) -> runGraph sg nextId state
Just (ConditionalEdge cond) -> do
eNextId <- cond state
case eNextId of
Left err -> throwError err
Right nextId -> runGraph sg nextId state
Nothing ->
throwError $
internalError ("Node not found in compiled graph: " <> currentId) (Just currentId) Nothing
{- | Standard pure reducer concatenating 'Message' lists.
__Note:__ This reducer expects node actions to return only the /new/ messages (the delta)
to append to the existing history. If your node actions return the full accumulated message
list, use 'replaceFieldReducer' instead to avoid duplicating previous messages.
-}
appendMessagesReducer :: StateReducer [Message]
appendMessagesReducer old new = old ++ new
-- | Standard pure reducer replacing previous state field with new state
replaceFieldReducer :: StateReducer a
replaceFieldReducer _ new = new