packages feed

langchain-hs 0.0.6.0 → 0.0.6.2

raw patch · 5 files changed

+409/−15 lines, 5 filesdep ~langchain-hs-coredep ~langchain-hs-graphPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: langchain-hs-core, langchain-hs-graph

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Changelog for `langchain-hs` +## 0.0.6.2 - 2026-11-14++> **Note**: Version `0.0.6.1` was skipped globally because `langchain-hs-ollama` already claimed `0.0.6.1` (`langchain-hs-ollama-v0.0.6.1`).++- Aligned all 8 monorepo packages (`langchain-hs`, `langchain-hs-core`, `langchain-hs-graph`, `langchain-hs-mcp`, `langchain-hs-ollama`, `langchain-hs-openai`, `langchain-hs-gemini`, `langchain-hs-anthropic`) on `0.0.6.2`.+- Updated `langchain-hs-core` and `langchain-hs-graph` dependency bounds to `>= 0.0.6.2 && < 0.0.7`.+- Surfaced tool-call streaming, `StreamingTool`, `streamingShellTool`, `withStreamingSecurity`, `streamReActAgent`, `runReActAgentStream`, `streamPlanAndExecute`, and `runPlanAndExecuteStream` via `Langchain.Prelude` and module re-exports.+- Surfaced StateGraph event streaming (`streamGraphEvents`, `GraphStreamEvent`) via `langchain-hs-graph` re-exports.+- Re-exported `Langchain.Eval` evaluation harness and multi-agent handoff tools (`asAgentTool`, `asStreamingAgentTool`).+ ## 0.0.6.0 - 2026-10-15  ### Highlights
README.md view
@@ -4,7 +4,7 @@  A functional, effect-polymorphic Haskell ecosystem for building production AI applications, featuring pure AST pipelines (`RunnableTree`), cyclic state graphs (`StateGraph`), Model Context Protocol (MCP), and OpenTelemetry observability. -[![Hackage](https://img.shields.io/badge/hackage-0.0.6.0-blue.svg)](https://hackage.haskell.org/package/langchain-hs)+[![Hackage](https://img.shields.io/badge/hackage-0.0.6.2-blue.svg)](https://hackage.haskell.org/package/langchain-hs) [![GHC](https://img.shields.io/badge/GHC-9.8%2B-purple.svg)](https://www.haskell.org/ghc/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Documentation](https://img.shields.io/badge/docs-website-blueviolet.svg)](https://tusharad.github.io/langchain-hs/)@@ -24,11 +24,11 @@  ```yaml extra-deps:-  - langchain-hs-0.0.6.0-  - langchain-hs-core-0.0.6.0-  - langchain-hs-graph-0.0.6.0-  - langchain-hs-ollama-0.0.6.0-  - langchain-hs-openai-0.0.6.0+  - langchain-hs-0.0.6.2+  - langchain-hs-core-0.0.6.2+  - langchain-hs-graph-0.0.6.2+  - langchain-hs-ollama-0.0.6.2+  - langchain-hs-openai-0.0.6.2 ```  Package reference on Hackage: [https://hackage.haskell.org/package/langchain-hs](https://hackage.haskell.org/package/langchain-hs)
langchain-hs.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           langchain-hs-version:        0.0.6.0+version:        0.0.6.2 synopsis:       Pure functional LLM agent framework and multi-agent graph engine in Haskell description:    Build LLM-powered applications, ReAct agents, and LangGraph-style stateful workflows                 in Haskell. Provider-specific packages:@@ -98,6 +98,7 @@     , Langchain.Agent.PlanAndExecute     , Langchain.Guardrail     , Langchain.Guardrail.Core+    , Langchain.Eval     , Langchain.Prelude     , Langchain.Graph.StateGraph     , Langchain.Graph.Checkpointer@@ -105,8 +106,8 @@   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:       base >=4.17 && <5-    , langchain-hs-core ==0.0.6.*-    , langchain-hs-graph ==0.0.6.*+    , langchain-hs-core >=0.0.6.2 && <0.0.7+    , langchain-hs-graph >=0.0.6.2 && <0.0.7   default-language: Haskell2010  test-suite langchain-hs-test@@ -156,8 +157,8 @@     , directory     , filepath     , langchain-hs-    , langchain-hs-core ==0.0.6.*-    , langchain-hs-graph ==0.0.6.*+    , langchain-hs-core >=0.0.6.2 && <0.0.7+    , langchain-hs-graph >=0.0.6.2 && <0.0.7     , mtl     , resourcet     , stm
test/Test/Langchain/Agent/AdvancedAgentsSpec.hs view
@@ -1,14 +1,18 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}  module Test.Langchain.Agent.AdvancedAgentsSpec (tests) where  import Control.Monad.Except (ExceptT, runExceptT)+import Control.Monad.IO.Class (liftIO)+import Data.IORef (modifyIORef', newIORef, readIORef) import qualified Data.Text as T import Test.Tasty import Test.Tasty.HUnit  import Langchain.Agent.PlanAndExecute import Langchain.Error (LangchainError)+import Langchain.Stream (StreamEvent (ChainEnd, ChainStart, NodeEnd, NodeStart)) import Test.Langchain.Provider.Mock (newMockModel)  tests :: TestTree@@ -34,4 +38,23 @@         case res of           Left err -> assertFailure ("PlanAndExecute failed: " ++ show err)           Right ans -> ans @?= "Result: 42"+    , testCase "runPlanAndExecuteStream emits Chain and Node lifecycle events" $ do+        let planner =+              newMockModel+                "{\"planSteps\": [{\"stepNumber\": 1, \"stepDescription\": \"Step one\"}]}"+            executor = newMockModel "Streamed plan result."+            agent = newPlanAndExecuteAgent planner executor Nothing+        eventsRef <- newIORef ([] :: [StreamEvent])+        res <- runExceptT $+          runPlanAndExecuteStream agent "Solve goal" $ \ev ->+            liftIO $ modifyIORef' eventsRef (++ [ev])+        case res of+          Left err -> assertFailure ("Stream PlanAndExecute failed: " ++ show err)+          Right ans -> do+            ans @?= "Streamed plan result."+            events <- readIORef eventsRef+            assertBool "Has ChainStart" (any (\case ChainStart {} -> True; _ -> False) events)+            assertBool "Has NodeStart" (any (\case NodeStart {} -> True; _ -> False) events)+            assertBool "Has NodeEnd" (any (\case NodeEnd {} -> True; _ -> False) events)+            assertBool "Has ChainEnd" (any (\case ChainEnd {} -> True; _ -> False) events)     ]
test/Test/Langchain/Agent/ReAct.hs view
@@ -1,24 +1,43 @@ {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-}  module Test.Langchain.Agent.ReAct (tests) where +import Control.Concurrent (threadDelay)+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, tryTakeMVar)+import Control.Exception (finally)+import Control.Monad (forM_, unless, when) import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (Value (..), object, (.=))+import Data.Conduit (yield) import Data.IORef-import Data.Maybe (listToMaybe)+import Data.Maybe (fromMaybe, listToMaybe) import qualified Data.Text as T import Test.Tasty import Test.Tasty.HUnit  import Langchain.Agent.ReAct-import Langchain.Error (LangchainError)+import Langchain.Callback.Manager+  ( CallbackEvent (..)+  , CallbackHandler (..)+  , newCallbackManager+  , registerHandler+  )+import Langchain.Error (LangchainError, errorMessage, toolError) import Langchain.Memory.Core (BaseMemory (..), newWindowBufferMemory) import Langchain.Model-import Langchain.Tool (Tool, createTool)+import Langchain.Monad (LangchainT, runLangchainT)+import Langchain.Stream+  ( StreamEvent (..)+  , collectEvents+  , newCancelToken+  , triggerCancel+  )+import Langchain.Tool (StreamingTool, Tool, createStreamingTool, createTool, toolExecute) import Langchain.Tool.Binding (ToolBinder (..)) import Test.Langchain.Provider.Mock (newMockModel) @@ -58,11 +77,45 @@       (m : rest) -> do         writeIORef stepsRef rest         pure m-  stream = error "stream not supported in StepSequenceModel"+  stream (StepSequenceModel stepsRef histRef) history _ = do+    let rId = "seq-stream-run"+    yield $ LLMStart rId "step-sequence-model" history+    resp <- liftIO $ do+      modifyIORef histRef (++ [history])+      steps <- readIORef stepsRef+      case steps of+        [] -> pure $ assistantMessage "Default response"+        (m : rest) -> do+          writeIORef stepsRef rest+          pure m+    let txt = extractMessageText resp+    unless (T.null txt) $+      forM_ (T.chunksOf 4 txt) $ \chunk ->+        yield $ LLMChunk rId chunk Nothing+    forM_ (fromMaybe [] (messageToolCalls resp)) $ \tc ->+      yield $ LLMChunk rId "" (Just tc)+    yield $ LLMEnd rId resp Nothing  instance ToolBinder StepSequenceModel m where   bindToolsConfig _ _ = Nothing +data InterruptMockModel = InterruptMockModel (MVar ()) (MVar ())++instance ChatModel InterruptMockModel where+  type ModelConfig InterruptMockModel = ()+  invoke _ _ _ = pure $ assistantMessage "sync"+  stream (InterruptMockModel gate cleaned) msgs _ = do+    yield $ LLMStart "int-1" "interrupt-mock" msgs+    yield $ LLMChunk "int-1" "partial reply" Nothing+    liftIO $+      takeMVar gate+        `finally` putMVar cleaned ()+    yield $ LLMChunk "int-1" " trailing" Nothing+    yield $ LLMEnd "int-1" (assistantMessage "partial reply trailing") Nothing++instance ToolBinder InterruptMockModel m where+  bindToolsConfig _ cfg = cfg+ tests :: TestTree tests =   testGroup@@ -228,4 +281,311 @@             case captured of               Nothing -> assertFailure "Expected config to be passed to model"               Just _ -> pure () -- Config was passed (exact value depends on ToolBinder instance)+    , testCase "streamReActAgent emits ChainStart, LLM events, Tool events, and ChainEnd across turns" $ do+        let tc1 = ToolCall "call_1" "function" "calculator" (object ["expression" .= ("2+2" :: T.Text)])+            respWithTools = (assistantMessage "Thinking...") {messageToolCalls = Just [tc1]}+            finalResp = assistantMessage "Answer is 4"+        sRef <- newIORef [respWithTools, finalResp]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent = defaultReActAgent model [calculatorTool]+        res <- runExceptT $ collectEvents $ streamReActAgent agent [userMessage "Calculate 2+2"]+        case res of+          Left err -> assertFailure $ "Unexpected stream error: " ++ show err+          Right events -> do+            case (events, reverse events) of+              (ChainStart _ "ReActAgent" _ : _, ChainEnd _ "ReActAgent" _ : _) -> pure ()+              _ -> assertFailure $ "Expected ChainStart/ChainEnd wrapping events: " ++ show events+            let toolStarts = [tName | ToolStart _ tName _ <- events]+                toolEnds = [tName | ToolEnd _ tName _ <- events]+                toolDeltas = [tc | LLMChunk _ _ (Just tc) <- events]+                textChunks = T.concat [txt | LLMChunk _ txt Nothing <- events]+            toolStarts @?= ["calculator"]+            toolEnds @?= ["calculator"]+            toolDeltas @?= [tc1]+            textChunks @?= "Thinking...Answer is 4"+    , testCase "runReActAgentStream works in LangchainT () IO with memory and callbacks" $ do+        mem <- newWindowBufferMemory 20 []+        cbMgr <- newCallbackManager+        cbLog <- newIORef ([] :: [T.Text])+        registerHandler cbMgr $ CallbackHandler "test-cb" $ \ev ->+          let tag = case ev of+                OnLLMStart {} -> "llm_start"+                OnLLMEnd {} -> "llm_end"+                OnToolStart {} -> "tool_start"+                OnToolEnd {} -> "tool_end"+                _ -> "other"+           in modifyIORef cbLog (++ [tag])+        let tc1 = ToolCall "call_1" "function" "calculator" (object ["x" .= (1 :: Int)])+            respWithTools = (assistantMessage "") {messageToolCalls = Just [tc1]}+            finalResp = assistantMessage "Done 4"+        sRef <- newIORef [respWithTools, finalResp]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withCallbackManager cbMgr $+                withMemory mem $+                  defaultReActAgent model [calculatorTool :: Tool (LangchainT () IO)]+        seenChunks <- newIORef ([] :: [T.Text])+        res <-+          runLangchainT () $+            runReActAgentStream agent [userMessage "Go"] $+              \case+                LLMChunk _ txt Nothing | not (T.null txt) -> liftIO $ modifyIORef seenChunks (++ [txt])+                _ -> pure ()+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right finalMsg -> do+            extractMessageText finalMsg @?= "Done 4"+            chunks <- readIORef seenChunks+            T.concat chunks @?= "Done 4"+            cbs <- readIORef cbLog+            cbs @?= ["llm_start", "llm_end", "tool_start", "tool_end", "llm_start", "llm_end"]+            memRes <- runExceptT $ messages mem+            case memRes of+              Left err -> assertFailure $ show err+              Right saved -> length saved @?= 2+    , testCase "streamReActAgent streams ToolChunk events from withStreamingTools" $ do+        let sTool =+              createStreamingTool "stream_echo" "Echoes chunks" (object []) $ \_ -> do+                yield "part1-"+                yield "part2"+                pure (Right "part1-part2")+            tc1 = ToolCall "call_s" "function" "stream_echo" (object [])+            respWithTools = (assistantMessage "") {messageToolCalls = Just [tc1]}+            finalResp = assistantMessage "Streamed tool ok"+        sRef <- newIORef [respWithTools, finalResp]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withStreamingTools [sTool] $+                defaultReActAgent model []+        res <- runExceptT $ collectEvents $ streamReActAgent agent [userMessage "Run stream tool"]+        case res of+          Left err -> assertFailure $ "Unexpected stream error: " ++ show err+          Right events -> do+            let toolChunks = [(nm, ch) | ToolChunk _ nm ch <- events]+            toolChunks @?= [("stream_echo", "part1-"), ("stream_echo", "part2")]+    , testCase "streamReActAgent emits ToolErrorEvent and respects FailOnError" $ do+        let failingTool :: Tool (ExceptT LangchainError IO)+            failingTool =+              createTool "boom" "Always fails" (object []) $ \_ ->+                pure $ Left $ toolError "exploded" (Just "boom") Nothing+            tc1 = ToolCall "call_err" "function" "boom" (object [])+            respWithTools = (assistantMessage "") {messageToolCalls = Just [tc1]}+        sRef <- newIORef [respWithTools]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withToolErrorStrategy FailOnError $+                defaultReActAgent model [failingTool]+        seenRef <- newIORef ([] :: [StreamEvent])+        res <- runExceptT $+          runReActAgentStream agent [userMessage "Trigger error"] $ \ev ->+            liftIO $ modifyIORef seenRef (++ [ev])+        case res of+          Left _ -> pure ()+          Right msg -> assertFailure $ "Expected FailOnError abort, got: " ++ show msg+        seen <- readIORef seenRef+        assertBool "Saw ToolStart" (any (\case ToolStart _ "boom" _ -> True; _ -> False) seen)+        assertBool "Saw ToolErrorEvent" (any (\case ToolErrorEvent _ "boom" _ -> True; _ -> False) seen)+    , testCase "runReActAgentStreamInterruptible cancels turn and keeps partial text" $ do+        tok <- newCancelToken+        gate <- newEmptyMVar+        cleaned <- newEmptyMVar+        mem <- newWindowBufferMemory 20 []+        let model = InterruptMockModel gate cleaned+            agent = withMemory mem $ defaultReActAgent model []+        res <-+          runLangchainT () $+            runReActAgentStreamInterruptible tok agent [userMessage "Interrupt me"] $ \case+              LLMChunk _ txt Nothing | not (T.null txt) -> triggerCancel tok+              _ -> pure ()+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right (Completed msg) -> assertFailure $ "Expected Cancelled, got Completed: " ++ show msg+          Right (Cancelled partialMsg) -> extractMessageText partialMsg @?= "partial reply"+        threadDelay 50000+        mbCleaned <- tryTakeMVar cleaned+        mbCleaned @?= Just ()+        memRes <- runExceptT $ messages mem+        case memRes of+          Left err -> assertFailure $ show err+          Right saved -> saved @?= []+    , testCase "AbortOnRepeat 3 halts loop on 3rd consecutive duplicate tool call in runReActAgent" $ do+        cbMgr <- newCallbackManager+        errSeenRef <- newIORef False+        registerHandler cbMgr $ CallbackHandler "loop-err-handler" $ \case+          OnError "ReActAgent" msg _ ->+            when ("Runaway loop detected" `T.isInfixOf` msg) $+              writeIORef errSeenRef True+          _ -> pure ()+        let tc = ToolCall "call_1" "function" "calculator" (object ["expression" .= ("2+2" :: T.Text)])+            respWithTool = (assistantMessage "") {messageToolCalls = Just [tc]}+        sRef <- newIORef [respWithTool, respWithTool, respWithTool, respWithTool]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withCallbackManager cbMgr $+                withLoopDetection (AbortOnRepeat 3) (defaultReActAgent model [calculatorTool])+        res <- runExceptT $ runReActAgent agent [userMessage "Loop test"]+        case res of+          Left err -> do+            assertBool "Mentions runaway loop" ("Runaway loop detected" `T.isInfixOf` errorMessage err)+            sawCb <- readIORef errSeenRef+            assertBool "Dispatched OnError callback" sawCb+          Right msg -> assertFailure $ "Expected loop abort, got success: " ++ show msg+    , testCase "AbortOnRepeat 3 halts loop in streamReActAgent" $ do+        let tc = ToolCall "call_1" "function" "calculator" (object ["expression" .= ("2+2" :: T.Text)])+            respWithTool = (assistantMessage "") {messageToolCalls = Just [tc]}+        sRef <- newIORef [respWithTool, respWithTool, respWithTool, respWithTool]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withLoopDetection (AbortOnRepeat 3) $+                defaultReActAgent model [calculatorTool]+        res <- runExceptT $ collectEvents $ streamReActAgent agent [userMessage "Stream loop test"]+        case res of+          Left err ->+            assertBool "Mentions runaway loop" ("Runaway loop detected" `T.isInfixOf` errorMessage err)+          Right evs -> assertFailure $ "Expected stream loop abort, got events: " ++ show evs+    , testCase "WarnOnRepeat 2 injects observation warning allowing recovery on step 3" $ do+        execCountRef <- newIORef (0 :: Int)+        let countingCalcTool :: Tool (ExceptT LangchainError IO)+            countingCalcTool =+              createTool "calculator" "Mock calculator tool" (object []) $ \_ -> do+                liftIO $ modifyIORef' execCountRef (+ 1)+                pure $ Right "4"+            tc = ToolCall "call_1" "function" "calculator" (object ["expression" .= ("2+2" :: T.Text)])+            respWithTool = (assistantMessage "") {messageToolCalls = Just [tc]}+            recoveredResp = assistantMessage "I noticed the loop warning and resolved 4."+        sRef <- newIORef [respWithTool, respWithTool, recoveredResp]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withLoopDetection (WarnOnRepeat 2) $+                defaultReActAgent model [countingCalcTool]+        res <- runExceptT $ runReActAgentWithTrace agent [userMessage "Calculate"]+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right trace -> do+            traceIterations trace @?= 3+            extractMessageText (traceFinalAnswer trace)+              @?= "I noticed the loop warning and resolved 4."+            execCount <- readIORef execCountRef+            -- Tool was executed on step 1, but skipped on step 2 due to WarnOnRepeat+            execCount @?= 1+            histories <- readIORef hRef+            case histories of+              [_, _, step3Hist] -> do+                let toolMsgs = filter (\m -> messageRole m == Tool) step3Hist+                case reverse toolMsgs of+                  (warnMsg : _) ->+                    assertBool+                      "Step 3 received loop warning"+                      ("Loop warning" `T.isInfixOf` extractMessageText warnMsg)+                  [] -> assertFailure "Expected tool message in step 3 history"+              _ -> assertFailure $ "Expected 3 history snapshots, got: " ++ show (length histories)+    , testCase "distinct tool arguments do not trigger AbortOnRepeat" $ do+        let tc1 = ToolCall "call_1" "function" "calculator" (object ["expression" .= ("1+1" :: T.Text)])+            tc2 = ToolCall "call_2" "function" "calculator" (object ["expression" .= ("2+2" :: T.Text)])+            tc3 = ToolCall "call_3" "function" "calculator" (object ["expression" .= ("3+3" :: T.Text)])+            r1 = (assistantMessage "") {messageToolCalls = Just [tc1]}+            r2 = (assistantMessage "") {messageToolCalls = Just [tc2]}+            r3 = (assistantMessage "") {messageToolCalls = Just [tc3]}+            finalResp = assistantMessage "All three distinct calculations finished"+        sRef <- newIORef [r1, r2, r3, finalResp]+        hRef <- newIORef []+        let model = StepSequenceModel sRef hRef+            agent =+              withLoopDetection (AbortOnRepeat 2) $+                defaultReActAgent model [calculatorTool]+        res <- runExceptT $ runReActAgent agent [userMessage "Calculate distinct"]+        case res of+          Left err -> assertFailure $ "Distinct arguments should not abort: " ++ show err+          Right finalMsg ->+            extractMessageText finalMsg @?= "All three distinct calculations finished"+    , testCase "asAgentTool delegates task from supervisor to specialist agent" $ do+        let specModel = newMockModel "4"+            specialistAgent = defaultReActAgent specModel [calculatorTool]+            specialistTool :: Tool (ExceptT LangchainError IO)+            specialistTool =+              asAgentTool "math_specialist" "Specialist math agent" specialistAgent+            callSpecialist =+              ToolCall+                "call_spec"+                "function"+                "math_specialist"+                (object ["task" .= ("compute 2+2" :: T.Text)])+            supResp1 = (assistantMessage "") {messageToolCalls = Just [callSpecialist]}+            supResp2 = assistantMessage "Supervisor final answer: 4"+        sRef <- newIORef [supResp1, supResp2]+        hRef <- newIORef []+        let supModel = StepSequenceModel sRef hRef+            supervisor = defaultReActAgent supModel [specialistTool]+        res <- runExceptT $ runReActAgent supervisor [userMessage "Solve 2+2"]+        case res of+          Left err -> assertFailure $ "Supervisor delegation failed: " ++ show err+          Right finalMsg -> extractMessageText finalMsg @?= "Supervisor final answer: 4"+    , testCase "asStreamingAgentTool forwards specialist chunks as ToolChunk in streamReActAgent" $ do+        let specStreamingTool =+              createStreamingTool "stream_echo" "Echoes chunks" (object []) $ \_ -> do+                yield "spec-chunk-1 "+                yield "spec-chunk-2"+                pure (Right "spec-chunk-1 spec-chunk-2")+            specCall =+              ToolCall "call_echo" "function" "stream_echo" (object [])+            specResp1 = (assistantMessage "") {messageToolCalls = Just [specCall]}+            specResp2 = assistantMessage "specialist finished"+        specStepsRef <- newIORef [specResp1, specResp2]+        specHistRef <- newIORef []+        let specModel = StepSequenceModel specStepsRef specHistRef+            specialist =+              withStreamingTools [specStreamingTool] (defaultReActAgent specModel [])+            streamSpecialistTool :: StreamingTool (ExceptT LangchainError IO)+            streamSpecialistTool =+              asStreamingAgentTool "specialist_streamer" "Specialist streaming agent" specialist+            callSpec =+              ToolCall+                "call_stream_spec"+                "function"+                "specialist_streamer"+                (object ["task" .= ("stream this" :: T.Text)])+            supResp1 = (assistantMessage "") {messageToolCalls = Just [callSpec]}+            supResp2 = assistantMessage "Supervisor completed streaming delegation"+        sRef <- newIORef [supResp1, supResp2]+        hRef <- newIORef []+        let supModel = StepSequenceModel sRef hRef+            supervisor =+              withStreamingTools [streamSpecialistTool] (defaultReActAgent supModel [])+        res <- runExceptT $ collectEvents $ streamReActAgent supervisor [userMessage "Delegate stream"]+        case res of+          Left err -> assertFailure $ "Streaming delegation failed: " ++ show err+          Right evs -> do+            let tStarts = [nm | ToolStart _ nm _ <- evs]+                tChunks = [(nm, ch) | ToolChunk _ nm ch <- evs]+                tEnds = [nm | ToolEnd _ nm _ <- evs]+            assertBool "Saw specialist ToolStart" ("specialist_streamer" `elem` tStarts)+            assertBool "Saw specialist ToolEnd" ("specialist_streamer" `elem` tEnds)+            assertBool "Saw streamed chunks from specialist" (not (null tChunks))+            map snd tChunks+              @?= ["spec-chunk-1 ", "spec-chunk-2", "spec", "iali", "st f", "inis", "hed"]+    , testCase "asAgentTool respects maxDelegationDepth" $ do+        let specModel = newMockModel "depth"+            specialist =+              withMaxDelegationDepth 0 (defaultReActAgent specModel [calculatorTool])+            specTool :: Tool (ExceptT LangchainError IO)+            specTool = asAgentTool "deep_agent" "Deep agent" specialist+        res <- runExceptT $ toolExecute specTool (object ["task" .= ("run" :: T.Text)])+        case res of+          Right (Left err) ->+            assertBool+              "Mentions delegation depth exceeded"+              ("delegation depth exceeded" `T.isInfixOf` errorMessage err)+          Left err ->+            assertBool+              "Mentions delegation depth exceeded"+              ("delegation depth exceeded" `T.isInfixOf` errorMessage err)+          Right (Right val) ->+            assertFailure $ "Expected delegation depth failure, got: " ++ show val     ]