packages feed

langchain-hs-graph 0.0.5.0 → 0.0.6.0

raw patch · 12 files changed

+391/−307 lines, 12 filesdep −asyncdep −timedep ~langchain-hs-corePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: async, time

Dependency ranges changed: langchain-hs-core

API changes (from Hackage documentation)

- Langchain.Graph.MultiAgent: embedSubGraphNode :: forall (m :: Type -> Type) subState parentState. (MonadIO m, MonadError LangchainError m) => NodeId -> StateGraph subState m -> (parentState -> subState) -> (parentState -> subState -> parentState) -> Node parentState m
- Langchain.Graph.MultiAgent: embedSubGraphNodeWithStart :: forall (m :: Type -> Type) subState parentState. (MonadIO m, MonadError LangchainError m) => NodeId -> NodeId -> StateGraph subState m -> (parentState -> subState) -> (parentState -> subState -> parentState) -> Node parentState m
- Langchain.Graph.MultiAgent: supervisorNode :: forall model (m :: Type -> Type) s. (ChatModel model, MonadIO m, MonadError LangchainError m) => model -> NodeId -> [(Text, NodeId)] -> (s -> Text) -> (Text -> s -> s) -> Node s m
- Langchain.Graph.Parallel: addParallelNodes :: forall (m :: Type -> Type) s. MonadIO m => NodeId -> [s -> IO (Either LangchainError s)] -> (s -> [s] -> s) -> StateGraph s m -> StateGraph s m
- Langchain.Graph.Parallel: parallelNode :: forall (m :: Type -> Type) s. MonadIO m => NodeId -> [s -> IO (Either LangchainError s)] -> (s -> [s] -> s) -> Node s m
+ Langchain.Graph.StateGraph: CompiledGraph :: !Map NodeId (Node s m) -> !Map NodeId (Edge s m) -> !StateReducer s -> !GraphConfig -> CompiledGraph s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: GraphConfig :: !Int -> !Bool -> !Text -> IO () -> GraphConfig
+ Langchain.Graph.StateGraph: [compiledConfig] :: CompiledGraph s (m :: Type -> Type) -> !GraphConfig
+ Langchain.Graph.StateGraph: [compiledEdges] :: CompiledGraph s (m :: Type -> Type) -> !Map NodeId (Edge s m)
+ Langchain.Graph.StateGraph: [compiledNodes] :: CompiledGraph s (m :: Type -> Type) -> !Map NodeId (Node s m)
+ Langchain.Graph.StateGraph: [compiledReducer] :: CompiledGraph s (m :: Type -> Type) -> !StateReducer s
+ Langchain.Graph.StateGraph: [graphConfig] :: StateGraph s (m :: Type -> Type) -> !GraphConfig
+ Langchain.Graph.StateGraph: [logStepTransitions] :: GraphConfig -> !Bool
+ Langchain.Graph.StateGraph: [recursionLimit] :: GraphConfig -> !Int
+ Langchain.Graph.StateGraph: [stepLogger] :: GraphConfig -> !Text -> IO ()
+ Langchain.Graph.StateGraph: addChatNode :: forall c (m :: Type -> Type) s. (ChatModel c, MonadIO m) => NodeId -> c -> (s -> Text) -> (s -> Text -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addChatNodeWithConfig :: forall c (m :: Type -> Type) s. (ChatModel c, MonadIO m) => NodeId -> c -> ModelConfig c -> (s -> Text) -> (s -> Text -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addModelNode :: forall c (m :: Type -> Type) s. (ChatModel c, MonadIO m) => NodeId -> c -> (s -> [Message]) -> (s -> Message -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addModelNodeWithConfig :: forall c (m :: Type -> Type) s. (ChatModel c, MonadIO m) => NodeId -> c -> ModelConfig c -> (s -> [Message]) -> (s -> Message -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addRunnableNode :: forall r (m :: Type -> Type) s. (Runnable r m, Monad m) => NodeId -> r -> (s -> RunnableInput r) -> (s -> RunnableOutput r -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addRunnableTreeNode :: forall (m :: Type -> Type) i o s. MonadIO m => NodeId -> RunnableTree (ExceptT LangchainError IO) i o -> (s -> i) -> (s -> o -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addSubGraphNode :: forall (m :: Type -> Type) subState s. (MonadIO m, MonadError LangchainError m) => NodeId -> NodeId -> CompiledGraph subState m -> (s -> subState) -> (s -> subState -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: addTransformNode :: forall (m :: Type -> Type) s. Monad m => NodeId -> (s -> s) -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: data CompiledGraph s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: data GraphConfig
+ Langchain.Graph.StateGraph: defaultGraphConfig :: GraphConfig
+ Langchain.Graph.StateGraph: runGraphWithConfig :: (MonadIO m, MonadError LangchainError m) => GraphConfig -> CompiledGraph s m -> NodeId -> s -> m s
+ Langchain.Graph.StateGraph: setRecursionLimit :: forall s (m :: Type -> Type). Int -> StateGraph s m -> StateGraph s m
+ Langchain.Graph.StateGraph: setStepLogger :: forall s (m :: Type -> Type). (Text -> IO ()) -> StateGraph s m -> StateGraph s m
- Langchain.Graph.HITL: resumeGraph :: (Checkpointer cp m, FromJSON s, ToJSON s, MonadIO m, MonadError LangchainError m) => StateGraph s m -> cp -> Text -> NodeId -> NodeId -> (s -> s) -> m s
+ Langchain.Graph.HITL: resumeGraph :: (Checkpointer cp m, FromJSON s, ToJSON s, MonadIO m, MonadError LangchainError m) => CompiledGraph s m -> cp -> Text -> NodeId -> NodeId -> (s -> s) -> m s
- Langchain.Graph.StateGraph: ConditionalEdge :: (s -> m (Either LangchainError NodeId)) -> Edge s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: ConditionalEdge :: !s -> m (Either LangchainError NodeId) -> Edge s (m :: Type -> Type)
- Langchain.Graph.StateGraph: Node :: NodeId -> (s -> m (Either LangchainError s)) -> Node s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: Node :: !NodeId -> !s -> m (Either LangchainError s) -> Node s (m :: Type -> Type)
- Langchain.Graph.StateGraph: StateGraph :: Map NodeId (Node s m) -> Map NodeId (Edge s m) -> StateReducer s -> StateGraph s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: StateGraph :: !Map NodeId (Node s m) -> !Map NodeId (Edge s m) -> !StateReducer s -> !GraphConfig -> StateGraph s (m :: Type -> Type)
- Langchain.Graph.StateGraph: StaticEdge :: NodeId -> Edge s (m :: Type -> Type)
+ Langchain.Graph.StateGraph: StaticEdge :: !NodeId -> Edge s (m :: Type -> Type)
- Langchain.Graph.StateGraph: [graphEdges] :: StateGraph s (m :: Type -> Type) -> Map NodeId (Edge s m)
+ Langchain.Graph.StateGraph: [graphEdges] :: StateGraph s (m :: Type -> Type) -> !Map NodeId (Edge s m)
- Langchain.Graph.StateGraph: [graphNodes] :: StateGraph s (m :: Type -> Type) -> Map NodeId (Node s m)
+ Langchain.Graph.StateGraph: [graphNodes] :: StateGraph s (m :: Type -> Type) -> !Map NodeId (Node s m)
- Langchain.Graph.StateGraph: [graphReducer] :: StateGraph s (m :: Type -> Type) -> StateReducer s
+ Langchain.Graph.StateGraph: [graphReducer] :: StateGraph s (m :: Type -> Type) -> !StateReducer s
- Langchain.Graph.StateGraph: [nodeAction] :: Node s (m :: Type -> Type) -> s -> m (Either LangchainError s)
+ Langchain.Graph.StateGraph: [nodeAction] :: Node s (m :: Type -> Type) -> !s -> m (Either LangchainError s)
- Langchain.Graph.StateGraph: [nodeId] :: Node s (m :: Type -> Type) -> NodeId
+ Langchain.Graph.StateGraph: [nodeId] :: Node s (m :: Type -> Type) -> !NodeId
- Langchain.Graph.StateGraph: compileGraph :: forall s (m :: Type -> Type). StateGraph s m -> Either LangchainError (StateGraph s m)
+ Langchain.Graph.StateGraph: compileGraph :: forall s (m :: Type -> Type). StateGraph s m -> Either LangchainError (CompiledGraph s m)
- Langchain.Graph.StateGraph: runGraph :: (MonadIO m, MonadError LangchainError m) => StateGraph s m -> NodeId -> s -> m s
+ Langchain.Graph.StateGraph: runGraph :: (MonadIO m, MonadError LangchainError m) => CompiledGraph s m -> NodeId -> s -> m s

Files

CHANGELOG.md view
@@ -2,6 +2,11 @@  All notable changes to this package will be documented in this file. +## 0.0.6.0 - 2026-10-02++- Updated bounds for GHC 9.12 and modern Stackage LTS releases.+- Standalone stack configuration and verified test suite.+ ## 0.0.5.0 - 2026-09-10  - Initial standalone Hackage release of `langchain-hs-graph`.
README.md view
@@ -18,7 +18,7 @@ ## Installation  ```cabal-build-depends: langchain-hs-graph >= 0.0.5 && < 0.0.6+build-depends: langchain-hs-graph >= 0.0.6 && < 0.0.7 ```  ## License
langchain-hs-graph.cabal view
@@ -5,9 +5,9 @@ -- see: https://github.com/sol/hpack  name:           langchain-hs-graph-version:        0.0.5.0+version:        0.0.6.0 synopsis:       Graph-based stateful agent orchestration engine for langchain-hs-description:    StateGraph, StateReducer, Checkpointer, HITL interrupt/resume, and Multi-Agent supervisor patterns.+description:    StateGraph, StateReducer, Checkpointer, and HITL interrupt/resume workflow patterns. category:       Web, AI, Control homepage:       https://github.com/tusharad/langchain-hs#readme bug-reports:    https://github.com/tusharad/langchain-hs/issues@@ -34,8 +34,6 @@       Langchain.Graph.StateGraph       Langchain.Graph.Checkpointer       Langchain.Graph.HITL-      Langchain.Graph.MultiAgent-      Langchain.Graph.Parallel   other-modules:       Paths_langchain_hs_graph   hs-source-dirs:@@ -55,16 +53,14 @@       ScopedTypeVariables   build-depends:       aeson >=2.0 && <3-    , async ==2.2.*     , base >=4.17 && <5     , bytestring >=0.10 && <0.13     , containers >=0.6 && <0.9-    , langchain-hs-core ==0.0.5.*+    , langchain-hs-core ==0.0.6.*     , mtl >=2.2 && <2.4     , sqlite-simple >=0.4.18 && <0.5     , stm ==2.5.*     , text >=1.2 && <3-    , time >=1.9 && <1.15   default-language: Haskell2010  test-suite langchain-hs-graph-test@@ -73,7 +69,6 @@   other-modules:       Test.Langchain.Graph.CheckpointerSpec       Test.Langchain.Graph.HITLSpec-      Test.Langchain.Graph.MultiAgentSpec       Test.Langchain.Graph.StateGraphSpec       Paths_langchain_hs_graph   hs-source-dirs:@@ -95,12 +90,11 @@   build-depends:       QuickCheck >=2.14     , aeson >=2.0 && <3-    , async ==2.2.*     , base >=4.17 && <5     , bytestring >=0.10 && <0.13     , containers >=0.6 && <0.9     , filepath-    , langchain-hs-core ==0.0.5.*+    , langchain-hs-core ==0.0.6.*     , langchain-hs-graph     , mtl >=2.2 && <2.4     , sqlite-simple >=0.4.18 && <0.5@@ -110,5 +104,4 @@     , tasty-quickcheck >=0.10     , temporary     , text >=1.2 && <3-    , time >=1.9 && <1.15   default-language: Haskell2010
src/Langchain/Graph/Checkpointer.hs view
@@ -32,7 +32,7 @@ import qualified Data.Text.Encoding as TE import Database.SQLite.Simple -import Langchain.Core.Error (LangchainError, internalError)+import Langchain.Error (LangchainError, internalError) import Langchain.Graph.StateGraph (NodeId)  -- | Effect-polymorphic Checkpointer typeclass
src/Langchain/Graph/HITL.hs view
@@ -24,9 +24,9 @@ import Data.Text (Text) import qualified Data.Text as T -import Langchain.Core.Error (LangchainError (..), agentError, errorMessage)+import Langchain.Error (LangchainError (..), agentError, errorMessage) import Langchain.Graph.Checkpointer (Checkpointer (..))-import Langchain.Graph.StateGraph (Node (..), NodeId, StateGraph, runGraph)+import Langchain.Graph.StateGraph (CompiledGraph, Node (..), NodeId, runGraph)  -- | Construct a special HITL Interrupt LangchainError hitlInterruptError :: NodeId -> LangchainError@@ -64,14 +64,14 @@ -- | Resume an interrupted graph execution after human modification of checkpoint state resumeGraph ::   (Checkpointer cp m, FromJSON s, ToJSON s, MonadIO m, MonadError LangchainError m) =>-  StateGraph s m ->+  CompiledGraph s m ->   cp ->   Text ->   NodeId ->   NodeId ->   (s -> s) ->   m s-resumeGraph stateGraph cp threadId checkpointNodeId resumeStartNodeId modifier = do+resumeGraph compiledGraph cp threadId checkpointNodeId resumeStartNodeId modifier = do   mbState <- loadCheckpoint cp threadId checkpointNodeId   case mbState of     Left err -> throwError err@@ -84,4 +84,4 @@     Right (Just savedState) -> do       let modifiedState = modifier savedState       _ <- saveCheckpoint cp threadId resumeStartNodeId modifiedState-      runGraph stateGraph resumeStartNodeId modifiedState+      runGraph compiledGraph resumeStartNodeId modifiedState
− src/Langchain/Graph/MultiAgent.hs
@@ -1,96 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}--{- |-Module      : Langchain.Graph.MultiAgent-Description : Multi-Agent supervisor routing and sub-graph embedding nodes-Copyright   : (c) 2025-2026 Tushar Adhatrao-License     : MIT-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>-Stability   : experimental--Provides multi-agent routing supervisor nodes and sub-graph composition primitives.--}-module Langchain.Graph.MultiAgent-  ( supervisorNode-  , embedSubGraphNode-  , embedSubGraphNodeWithStart-  ) where--import Control.Applicative ((<|>))-import Control.Monad.Except (MonadError, throwError)-import Control.Monad.IO.Class (MonadIO)-import Data.Text (Text)-import qualified Data.Text as T--import Langchain.Core.Error (LangchainError, agentError)-import Langchain.Core.Model (ChatModel (..), extractMessageText, userMessage)-import Langchain.Graph.StateGraph (Node (..), NodeId, StateGraph, runGraph, startNodeId)---- | Construct a supervisor routing node that uses an LLM to select the target sub-agent NodeId-supervisorNode ::-  (ChatModel model, MonadIO m, MonadError LangchainError m) =>-  model ->-  NodeId ->-  [(Text, NodeId)] ->-  (s -> Text) ->-  (Text -> s -> s) ->-  Node s m-supervisorNode model name routes extractPrompt updateState =-  Node-    { nodeId = name-    , nodeAction = \state -> do-        let prompt =-              "You are a supervisor delegating tasks to sub-agents. Available routes:\n"-                <> T.unlines [rName <> " -> " <> targetId | (rName, targetId) <- routes]-                <> "\nTask context: "-                <> extractPrompt state-                <> "\nReply with ONLY the route name to execute."-        msg <- invoke model [userMessage prompt] Nothing-        let rawRoute = T.strip (extractMessageText msg)-            cleanRoute = T.filter (\c -> c /= '.' && c /= '"' && c /= '\'' && c /= '`') (T.toLower rawRoute)-            matchExact =-              lookup rawRoute routes-                <|> lookup (T.toLower rawRoute) [(T.toLower r, target) | (r, target) <- routes]-                <|> lookup-                  cleanRoute-                  [(T.toLower (T.filter (\c -> c /= '.' && c /= '"' && c /= '\'') r), target) | (r, target) <- routes]-            matchSubstring =-              case filter (\(r, _) -> T.toLower r `T.isInfixOf` cleanRoute) routes of-                ((_, target) : _) -> Just target-                [] -> Nothing-        case matchExact <|> matchSubstring of-          Just targetId -> pure $ Right $ updateState targetId state-          Nothing -> case routes of-            ((_, fallbackId) : _) -> pure $ Right $ updateState fallbackId state-            [] ->-              throwError $ agentError "No routes configured in supervisor node" (Just "supervisorNode") Nothing-    }---- | Embed a compiled sub-graph into a parent graph node starting at a specific entry node-embedSubGraphNodeWithStart ::-  (MonadIO m, MonadError LangchainError m) =>-  NodeId ->-  NodeId ->-  StateGraph subState m ->-  (parentState -> subState) ->-  (parentState -> subState -> parentState) ->-  Node parentState m-embedSubGraphNodeWithStart name entryId subGraph toSubState mergeState =-  Node-    { nodeId = name-    , nodeAction = \parentSt -> do-        let initSubSt = toSubState parentSt-        finalSubSt <- runGraph subGraph entryId initSubSt-        pure $ Right $ mergeState parentSt finalSubSt-    }---- | Embed a compiled sub-graph into a parent graph node starting at 'startNodeId'-embedSubGraphNode ::-  (MonadIO m, MonadError LangchainError m) =>-  NodeId ->-  StateGraph subState m ->-  (parentState -> subState) ->-  (parentState -> subState -> parentState) ->-  Node parentState m-embedSubGraphNode name = embedSubGraphNodeWithStart name startNodeId
− src/Langchain/Graph/Parallel.hs
@@ -1,52 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}--{- |-Module      : Langchain.Graph.Parallel-Description : Parallel concurrent node execution with state merging-Copyright   : (c) 2025-2026 Tushar Adhatrao-License     : MIT-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>-Stability   : experimental--Executes multiple independent graph nodes concurrently in parallel threads using async,-and merges their resulting sub-states into the parent state via a deterministic reducer.--}-module Langchain.Graph.Parallel-  ( parallelNode-  , addParallelNodes-  ) where--import Control.Concurrent.Async (mapConcurrently)-import Control.Monad.IO.Class (MonadIO, liftIO)--import Langchain.Core.Error (LangchainError)-import Langchain.Graph.StateGraph---- | Construct a parallel composite node that executes worker actions concurrently-parallelNode ::-  MonadIO m =>-  NodeId ->-  [s -> IO (Either LangchainError s)] ->-  (s -> [s] -> s) ->-  Node s m-parallelNode name workerActions mergeFn =-  Node-    { nodeId = name-    , nodeAction = \initState -> liftIO $ do-        results <- mapConcurrently (\action -> action initState) workerActions-        case sequence results of-          Left err -> pure $ Left err-          Right states -> pure $ Right $ mergeFn initState states-    }---- | Helper to register a parallel execution step into a StateGraph-addParallelNodes ::-  MonadIO m =>-  NodeId ->-  [s -> IO (Either LangchainError s)] ->-  (s -> [s] -> s) ->-  StateGraph s m ->-  StateGraph s m-addParallelNodes name workerActions mergeFn graph =-  let pNode = parallelNode name workerActions mergeFn-   in addNode name (nodeAction pNode) graph
src/Langchain/Graph/StateGraph.hs view
@@ -1,93 +1,273 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  {- | Module      : Langchain.Graph.StateGraph-Description : Core StateGraph engine and pure state reducer combinators+Description : Production StateGraph engine with Runnable & LLM node 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.+Production-grade stateful agent orchestration engine. Supports:+  * First-class LLM ChatModel nodes ('addModelNode', 'addChatNode')+  * First-class Runnable & RunnableTree AST nodes ('addRunnableNode', 'addRunnableTreeNode')+  * Pure state transformation nodes ('addTransformNode')+  * Hierarchical sub-graph embedding ('addSubGraphNode')+  * Static and dynamic conditional edge transitions+  * Recursion limits to prevent runaway loops in cyclic graphs+  * Configurable execution tracing and step logging -} module Langchain.Graph.StateGraph-  ( NodeId+  ( -- * Core Types+    NodeId   , startNodeId   , endNodeId   , StateReducer   , Node (..)   , Edge (..)+  , GraphConfig (..)+  , defaultGraphConfig   , StateGraph (..)+  , CompiledGraph (..)++    -- * Graph Construction   , emptyStateGraph-  , addNode+  , setRecursionLimit+  , setStepLogger   , addEdge   , addConditionalEdge++    -- * Node Combinators+  , addNode+  , addTransformNode+  , addRunnableNode+  , addRunnableTreeNode+  , addModelNode+  , addModelNodeWithConfig+  , addChatNode+  , addChatNodeWithConfig+  , addSubGraphNode++    -- * Compilation & Execution   , compileGraph   , runGraph+  , runGraphWithConfig++    -- * Pure State Reducers   , appendMessagesReducer   , replaceFieldReducer   ) where -import Control.Monad.Except (MonadError, throwError)-import Control.Monad.IO.Class (MonadIO)+import Control.Monad (when)+import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)+import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Text (Text)+import qualified Data.Text as T -import Langchain.Core.Error (LangchainError, internalError)-import Langchain.Core.Model (Message)+import Langchain.Error (LangchainError, agentError, internalError)+import Langchain.Model (ChatModel, Message, ModelConfig, extractMessageText, userMessage)+import qualified Langchain.Model as M (invoke)+import Langchain.Runnable (Runnable (..), RunnableTree, interpret)  -- | Unique node identifier in a StateGraph type NodeId = Text --- | Reserved start node constant+-- | Reserved start node identifier startNodeId :: NodeId startNodeId = "__start__" --- | Reserved end node constant+-- | Reserved end node identifier endNodeId :: NodeId endNodeId = "__end__" --- | Pure state merge function reducer satisfying associative algebra laws+-- | Pure state merge reducer function 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)+  { 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+-- | Edge transition between graph nodes data Edge s m-  = StaticEdge NodeId-  | ConditionalEdge (s -> m (Either LangchainError NodeId))+  = StaticEdge !NodeId+  | ConditionalEdge !(s -> m (Either LangchainError NodeId)) +-- | Execution configuration for a StateGraph+data GraphConfig = GraphConfig+  { recursionLimit :: !Int+  , logStepTransitions :: !Bool+  , stepLogger :: !(Text -> IO ())+  }++-- | Default configuration with 25-step recursion limit and disabled logging+defaultGraphConfig :: GraphConfig+defaultGraphConfig =+  GraphConfig+    { recursionLimit = 25+    , logStepTransitions = False+    , stepLogger = const (pure ())+    }+ -- | Declarative StateGraph specification data StateGraph s m = StateGraph-  { graphNodes :: Map NodeId (Node s m)-  , graphEdges :: Map NodeId (Edge s m)-  , graphReducer :: StateReducer s+  { graphNodes :: !(Map NodeId (Node s m))+  , graphEdges :: !(Map NodeId (Edge s m))+  , graphReducer :: !(StateReducer s)+  , graphConfig :: !GraphConfig   } --- | Construct an empty StateGraph with a given pure StateReducer+-- | A validated, compiled StateGraph ready for execution+data CompiledGraph s m = CompiledGraph+  { compiledNodes :: !(Map NodeId (Node s m))+  , compiledEdges :: !(Map NodeId (Edge s m))+  , compiledReducer :: !(StateReducer s)+  , compiledConfig :: !GraphConfig+  }++-- | Construct an empty StateGraph with a pure StateReducer emptyStateGraph :: StateReducer s -> StateGraph s m emptyStateGraph reducer =   StateGraph     { graphNodes = Map.empty     , graphEdges = Map.empty     , graphReducer = reducer+    , graphConfig = defaultGraphConfig     } --- | Add an execution node to the StateGraph+-- | Set the maximum step limit to prevent infinite loops in cyclic graphs+setRecursionLimit :: Int -> StateGraph s m -> StateGraph s m+setRecursionLimit limit g =+  g {graphConfig = (graphConfig g) {recursionLimit = limit}}++-- | Configure a custom step transition logger function+setStepLogger :: (Text -> IO ()) -> StateGraph s m -> StateGraph s m+setStepLogger logger g =+  g {graphConfig = (graphConfig g) {logStepTransitions = True, stepLogger = logger}}++-- | Add a raw 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 pure synchronous state transformation node (no error handling boilerplate)+addTransformNode :: Monad m => NodeId -> (s -> s) -> StateGraph s m -> StateGraph s m+addTransformNode name f =+  addNode name (pure . Right . f)++-- | Add a node backed by any Langchain 'Runnable' (e.g. Tools, ModelRunnable)+addRunnableNode ::+  (Runnable r m, Monad m) =>+  NodeId ->+  r ->+  (s -> RunnableInput r) ->+  (s -> RunnableOutput r -> s) ->+  StateGraph s m ->+  StateGraph s m+addRunnableNode name runnable toInput mergeOutput =+  addNode name $ \state -> do+    eRes <- invoke runnable (toInput state)+    pure $ fmap (mergeOutput state) eRes++-- | Add a node backed by an algebraic 'RunnableTree' pipeline AST+addRunnableTreeNode ::+  (MonadIO m) =>+  NodeId ->+  RunnableTree (ExceptT LangchainError IO) i o ->+  (s -> i) ->+  (s -> o -> s) ->+  StateGraph s m ->+  StateGraph s m+addRunnableTreeNode name tree toInput mergeOutput =+  addNode name $ \state -> liftIO $ do+    eRes <- runExceptT $ interpret tree (toInput state)+    pure $ fmap (mergeOutput state) eRes++-- | Add a dedicated ChatModel node taking and producing rich 'Message' lists+addModelNode ::+  (ChatModel c, MonadIO m) =>+  NodeId ->+  c ->+  (s -> [Message]) ->+  (s -> Message -> s) ->+  StateGraph s m ->+  StateGraph s m+addModelNode name chatModel toMessages mergeMessage =+  addNode name $ \state -> do+    eMsg <- liftIO $ runExceptT $ M.invoke chatModel (toMessages state) Nothing+    pure $ fmap (mergeMessage state) eMsg++-- | Add a dedicated ChatModel node with explicit model configuration+addModelNodeWithConfig ::+  (ChatModel c, MonadIO m) =>+  NodeId ->+  c ->+  ModelConfig c ->+  (s -> [Message]) ->+  (s -> Message -> s) ->+  StateGraph s m ->+  StateGraph s m+addModelNodeWithConfig name chatModel cfg toMessages mergeMessage =+  addNode name $ \state -> do+    eMsg <- liftIO $ runExceptT $ M.invoke chatModel (toMessages state) (Just cfg)+    pure $ fmap (mergeMessage state) eMsg++-- | Add a simple text-in, text-out ChatModel node+addChatNode ::+  (ChatModel c, MonadIO m) =>+  NodeId ->+  c ->+  (s -> Text) ->+  (s -> Text -> s) ->+  StateGraph s m ->+  StateGraph s m+addChatNode name chatModel toPrompt mergeText =+  addNode name $ \state -> do+    let msgs = [userMessage (toPrompt state)]+    eMsg <- liftIO $ runExceptT $ M.invoke chatModel msgs Nothing+    pure $ fmap (mergeText state . extractMessageText) eMsg++-- | Add a simple text-in, text-out ChatModel node with explicit model configuration+addChatNodeWithConfig ::+  (ChatModel c, MonadIO m) =>+  NodeId ->+  c ->+  ModelConfig c ->+  (s -> Text) ->+  (s -> Text -> s) ->+  StateGraph s m ->+  StateGraph s m+addChatNodeWithConfig name chatModel cfg toPrompt mergeText =+  addNode name $ \state -> do+    let msgs = [userMessage (toPrompt state)]+    eMsg <- liftIO $ runExceptT $ M.invoke chatModel msgs (Just cfg)+    pure $ fmap (mergeText state . extractMessageText) eMsg++-- | Embed a compiled sub-graph as a single node in a parent StateGraph+addSubGraphNode ::+  (MonadIO m, MonadError LangchainError m) =>+  NodeId ->+  NodeId ->+  CompiledGraph subState m ->+  (s -> subState) ->+  (s -> subState -> s) ->+  StateGraph s m ->+  StateGraph s m+addSubGraphNode name entryNode subGraph toSubState mergeSubState =+  addNode name $ \parentState -> do+    let initialSub = toSubState parentState+    finalSub <- runGraph subGraph entryNode initialSub+    pure $ Right $ mergeSubState parentState finalSub+ -- | Add a static transition edge between two nodes addEdge :: NodeId -> NodeId -> StateGraph s m -> StateGraph s m addEdge fromNode toNode g =@@ -95,61 +275,115 @@  -- | Add a dynamic conditional transition edge addConditionalEdge ::-  NodeId -> (s -> m (Either LangchainError NodeId)) -> StateGraph s m -> StateGraph s m+  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+-- | Validate state graph invariants and compile to an executable 'CompiledGraph'+compileGraph :: StateGraph s m -> Either LangchainError (CompiledGraph s m)+compileGraph StateGraph {..}+  | Map.null graphNodes =+      Left $ internalError "StateGraph must contain at least one node" (Just "compileGraph") Nothing+  | otherwise =+      Right+        CompiledGraph+          { compiledNodes = graphNodes+          , compiledEdges = graphEdges+          , compiledReducer = graphReducer+          , compiledConfig = graphConfig+          } --- | Execute a StateGraph from start node or specified currentId to endNodeId+-- | Execute a CompiledGraph starting from the given entry NodeId runGraph ::   (MonadIO m, MonadError LangchainError m) =>-  StateGraph s m ->+  CompiledGraph 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+runGraph cg = runGraphWithConfig (compiledConfig cg) cg -{- | Standard pure reducer concatenating 'Message' lists.+-- | Execute a CompiledGraph with an explicit configuration override+runGraphWithConfig ::+  (MonadIO m, MonadError LangchainError m) =>+  GraphConfig ->+  CompiledGraph s m ->+  NodeId ->+  s ->+  m s+runGraphWithConfig cfg cg =+  executeStep 0+  where+    CompiledGraph {..} = cg+    GraphConfig {..} = cfg -__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.--}+    logMsg msg =+      when logStepTransitions $ liftIO $ stepLogger msg++    executeStep stepCount currentId state+      | stepCount >= recursionLimit = do+          let errMsg =+                "StateGraph recursion limit exceeded (max "+                  <> T.pack (show recursionLimit)+                  <> " steps at node: "+                  <> currentId+                  <> ")"+          logMsg $ "[StateGraph ERROR] " <> errMsg+          throwError $ agentError errMsg (Just "runGraph") (Just currentId)+      | currentId == endNodeId = do+          logMsg "[StateGraph] Reached terminal node (__end__)"+          pure state+      | otherwise = case Map.lookup currentId compiledNodes of+          Just node -> do+            logMsg $+              "[StateGraph] Step " <> T.pack (show (stepCount + 1)) <> ": Entering node '" <> currentId <> "'"+            eNextState <- nodeAction node state+            case eNextState of+              Left err -> do+                logMsg $ "[StateGraph ERROR] Node '" <> currentId <> "' failed: " <> T.pack (show err)+                throwError err+              Right stepState -> do+                let mergedState = compiledReducer state stepState+                logMsg $ "[StateGraph] Node '" <> currentId <> "' executed successfully"+                case Map.lookup currentId compiledEdges of+                  Nothing -> do+                    logMsg $ "[StateGraph] No outgoing edge from '" <> currentId <> "', terminating"+                    pure mergedState+                  Just (StaticEdge nextId) -> do+                    logMsg $ "[StateGraph] Static edge: '" <> currentId <> "' -> '" <> nextId <> "'"+                    executeStep (stepCount + 1) nextId mergedState+                  Just (ConditionalEdge cond) -> do+                    eNextId <- cond mergedState+                    case eNextId of+                      Left err -> do+                        logMsg $+                          "[StateGraph ERROR] Conditional edge from '" <> currentId <> "' failed: " <> T.pack (show err)+                        throwError err+                      Right nextId -> do+                        logMsg $ "[StateGraph] Conditional edge: '" <> currentId <> "' -> '" <> nextId <> "'"+                        executeStep (stepCount + 1) nextId mergedState+          Nothing -> case Map.lookup currentId compiledEdges of+            Just (StaticEdge nextId) -> do+              logMsg $ "[StateGraph] Passthrough edge: '" <> currentId <> "' -> '" <> nextId <> "'"+              executeStep (stepCount + 1) nextId state+            Just (ConditionalEdge cond) -> do+              eNextId <- cond state+              case eNextId of+                Left err -> throwError err+                Right nextId -> do+                  logMsg $ "[StateGraph] Passthrough conditional edge: '" <> currentId <> "' -> '" <> nextId <> "'"+                  executeStep (stepCount + 1) nextId state+            Nothing -> do+              let errText = "Node not found in compiled graph: " <> currentId+              logMsg $ "[StateGraph ERROR] " <> errText+              throwError $ internalError errText (Just currentId) Nothing++-- | Standard pure reducer concatenating Message lists appendMessagesReducer :: StateReducer [Message] appendMessagesReducer old new = old ++ new --- | Standard pure reducer replacing previous state field with new state+-- | Standard pure reducer replacing previous state with the newly returned state replaceFieldReducer :: StateReducer a replaceFieldReducer _ new = new
test/Spec.hs view
@@ -4,7 +4,6 @@  import qualified Test.Langchain.Graph.CheckpointerSpec as CheckpointerSpec import qualified Test.Langchain.Graph.HITLSpec as HITLSpec-import qualified Test.Langchain.Graph.MultiAgentSpec as MultiAgentSpec import qualified Test.Langchain.Graph.StateGraphSpec as StateGraphSpec  main :: IO ()@@ -17,5 +16,4 @@     [ StateGraphSpec.tests     , CheckpointerSpec.tests     , HITLSpec.tests-    , MultiAgentSpec.tests     ]
test/Test/Langchain/Graph/HITLSpec.hs view
@@ -4,7 +4,7 @@  import Control.Monad.Except (ExceptT, runExceptT) import Data.Text (Text)-import Langchain.Core.Error (LangchainError)+import Langchain.Error (LangchainError) import Langchain.Graph.Checkpointer import Langchain.Graph.HITL import Langchain.Graph.StateGraph
− test/Test/Langchain/Graph/MultiAgentSpec.hs
@@ -1,53 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Test.Langchain.Graph.MultiAgentSpec (tests) where--import Control.Monad.Except (ExceptT, runExceptT)-import Data.Text (Text)-import Langchain.Core.Error (LangchainError)-import Langchain.Core.Model (ChatModel (..), assistantMessage)-import Langchain.Graph.MultiAgent-import Langchain.Graph.StateGraph-import Test.Tasty-import Test.Tasty.HUnit--newtype MockModel = MockModel Text--instance ChatModel MockModel where-  type ModelConfig MockModel = ()-  invoke (MockModel resp) _ _ = pure $ assistantMessage resp-  stream = undefined--newMockModel :: Text -> MockModel-newMockModel = MockModel--tests :: TestTree-tests =-  testGroup-    "Langchain.Graph.MultiAgentSpec"-    [ testCase "supervisorNode routes based on mock LLM response" $ do-        let mockModel = newMockModel "researcher"-            node =-              supervisorNode-                mockModel-                "supervisor"-                [("researcher", "researchNode")]-                id-                (\target s -> s <> ":" <> target)-        res <- runExceptT $ nodeAction node ("task-input" :: Text)-        res @?= Right (Right ("task-input:researchNode" :: Text))-    , testCase "embedSubGraphNode executes nested graph" $ do-        let action :: Text -> ExceptT LangchainError IO (Either LangchainError Text)-            action s = pure $ Right (s <> " [sub-processed]")-            subG =-              addEdge startNodeId "sub1" $-                addEdge "sub1" endNodeId $-                  addNode "sub1" action $-                    emptyStateGraph replaceFieldReducer-        case compileGraph subG of-          Left err -> assertFailure $ "Failed to compile sub-graph: " ++ show err-          Right compiledSub -> do-            let parentN = embedSubGraphNode "subGraphNode" compiledSub id (\p s -> p <> " | " <> s)-            res <- runExceptT $ nodeAction parentN ("parent-input" :: Text)-            res @?= Right (Right ("parent-input | parent-input [sub-processed]" :: Text))-    ]
test/Test/Langchain/Graph/StateGraphSpec.hs view
@@ -2,26 +2,20 @@  module Test.Langchain.Graph.StateGraphSpec (tests) where +import Control.Concurrent.STM (atomically, modifyTVar', newTVarIO, readTVarIO) import Control.Monad.Except (ExceptT, runExceptT) import Data.Text (Text)-import Langchain.Core.Error (LangchainError)+import qualified Data.Text as T+import Langchain.Error (LangchainError, errorMessage) import Langchain.Graph.StateGraph+import Langchain.Runnable (RunnableTree (..), runPure, (|>>)) import Test.Tasty import Test.Tasty.HUnit-import Test.Tasty.QuickCheck  tests :: TestTree tests =   testGroup     "Langchain.Graph.StateGraphSpec"-    [ unitTests-    , propertyTests-    ]--unitTests :: TestTree-unitTests =-  testGroup-    "Unit Tests"     [ testCase "compileGraph succeeds on valid node graph" $ do         let action :: Text -> ExceptT LangchainError IO (Either LangchainError Text)             action s = pure $ Right (s <> " world")@@ -41,16 +35,77 @@           Right compiled -> do             res <- runExceptT $ runGraph compiled "node1" ("hello" :: Text)             res @?= Right "hello world"-    ]--propertyTests :: TestTree-propertyTests =-  testGroup-    "Property Tests (Laws)"-    [ testProperty "Reducer Associativity Law: (a <> b) <> c == a <> (b <> c)" $ \a b c ->-        let s1 = (a :: String) ++ (b :: String)-            s2 = s1 ++ (c :: String)-            s3 = b ++ c-            s4 = a ++ s3-         in s2 == s4+    , testCase "addTransformNode updates state purely" $ do+        let g =+              addEdge "step1" endNodeId $+                addTransformNode "step1" (<> " [transformed]") $+                  emptyStateGraph replaceFieldReducer+        case compileGraph g of+          Left err -> assertFailure $ "Compilation failed: " ++ show err+          Right compiled -> do+            res <- runExceptT $ runGraph compiled "step1" ("input" :: Text)+            res @?= Right "input [transformed]"+    , testCase "addRunnableTreeNode executes pipeline AST within graph node" $ do+        let pipeline :: RunnableTree (ExceptT LangchainError IO) Text Text+            pipeline = runPure (<> " -> p1") |>> runPure (<> " -> p2")+            g =+              addEdge "runnableNode" endNodeId $+                addRunnableTreeNode "runnableNode" pipeline id (\_ out -> out) $+                  emptyStateGraph replaceFieldReducer+        case compileGraph g of+          Left err -> assertFailure $ "Compilation failed: " ++ show err+          Right compiled -> do+            res <- runExceptT $ runGraph compiled "runnableNode" ("start" :: Text)+            res @?= Right "start -> p1 -> p2"+    , testCase "recursionLimit terminates cyclic graph with error" $ do+        let g =+              setRecursionLimit 5 $+                addEdge "loop" "loop" $+                  addTransformNode "loop" (\n -> (n + 1 :: Int)) $+                    emptyStateGraph replaceFieldReducer+        case compileGraph g of+          Left err -> assertFailure $ "Compilation failed: " ++ show err+          Right compiled -> do+            res <- runExceptT $ runGraph compiled "loop" (0 :: Int)+            case res of+              Left err ->+                assertBool+                  "Contains recursion limit message"+                  ("recursion limit exceeded" `T.isInfixOf` errorMessage err)+              Right val -> assertFailure $ "Expected recursion error, but got: " ++ show val+    , testCase "addSubGraphNode embeds and executes sub-graph cleanly" $ do+        let subG =+              addEdge "sub1" endNodeId $+                addTransformNode "sub1" (<> "_sub") $+                  emptyStateGraph replaceFieldReducer+        case compileGraph subG of+          Left err -> assertFailure $ "Sub-graph compile failed: " ++ show err+          Right compiledSub -> do+            let parentG =+                  addEdge "parent1" endNodeId $+                    addSubGraphNode "parent1" "sub1" compiledSub id (\_ subOut -> "parent:" <> subOut) $+                      emptyStateGraph replaceFieldReducer+            case compileGraph parentG of+              Left err -> assertFailure $ "Parent compile failed: " ++ show err+              Right compiledParent -> do+                res <- runExceptT $ runGraph compiledParent "parent1" ("init" :: Text)+                res @?= Right "parent:init_sub"+    , testCase "setStepLogger logs transition messages" $ do+        logsVar <- newTVarIO []+        let logger msg = atomically $ modifyTVar' logsVar (msg :)+            g =+              setStepLogger logger $+                addEdge "a" "b" $+                  addEdge "b" endNodeId $+                    addTransformNode "a" (<> "_A") $+                      addTransformNode "b" (<> "_B") $+                        emptyStateGraph replaceFieldReducer+        case compileGraph g of+          Left err -> assertFailure $ "Compile failed: " ++ show err+          Right compiled -> do+            res <- runExceptT $ runGraph compiled "a" ("start" :: Text)+            res @?= Right "start_A_B"+            logs <- readTVarIO logsVar+            assertBool "Recorded log messages" (length logs >= 4)+            assertBool "Logged entering node 'a'" (any ("Entering node 'a'" `T.isInfixOf`) logs)     ]