diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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`.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/langchain-hs-graph.cabal b/langchain-hs-graph.cabal
--- a/langchain-hs-graph.cabal
+++ b/langchain-hs-graph.cabal
@@ -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
diff --git a/src/Langchain/Graph/Checkpointer.hs b/src/Langchain/Graph/Checkpointer.hs
--- a/src/Langchain/Graph/Checkpointer.hs
+++ b/src/Langchain/Graph/Checkpointer.hs
@@ -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
diff --git a/src/Langchain/Graph/HITL.hs b/src/Langchain/Graph/HITL.hs
--- a/src/Langchain/Graph/HITL.hs
+++ b/src/Langchain/Graph/HITL.hs
@@ -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
diff --git a/src/Langchain/Graph/MultiAgent.hs b/src/Langchain/Graph/MultiAgent.hs
deleted file mode 100644
--- a/src/Langchain/Graph/MultiAgent.hs
+++ /dev/null
@@ -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
diff --git a/src/Langchain/Graph/Parallel.hs b/src/Langchain/Graph/Parallel.hs
deleted file mode 100644
--- a/src/Langchain/Graph/Parallel.hs
+++ /dev/null
@@ -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
diff --git a/src/Langchain/Graph/StateGraph.hs b/src/Langchain/Graph/StateGraph.hs
--- a/src/Langchain/Graph/StateGraph.hs
+++ b/src/Langchain/Graph/StateGraph.hs
@@ -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
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -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
     ]
diff --git a/test/Test/Langchain/Graph/HITLSpec.hs b/test/Test/Langchain/Graph/HITLSpec.hs
--- a/test/Test/Langchain/Graph/HITLSpec.hs
+++ b/test/Test/Langchain/Graph/HITLSpec.hs
@@ -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
diff --git a/test/Test/Langchain/Graph/MultiAgentSpec.hs b/test/Test/Langchain/Graph/MultiAgentSpec.hs
deleted file mode 100644
--- a/test/Test/Langchain/Graph/MultiAgentSpec.hs
+++ /dev/null
@@ -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))
-    ]
diff --git a/test/Test/Langchain/Graph/StateGraphSpec.hs b/test/Test/Langchain/Graph/StateGraphSpec.hs
--- a/test/Test/Langchain/Graph/StateGraphSpec.hs
+++ b/test/Test/Langchain/Graph/StateGraphSpec.hs
@@ -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)
     ]
