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 +10/−0
- README.md +6/−6
- langchain-hs.cabal +6/−5
- test/Test/Langchain/Agent/AdvancedAgentsSpec.hs +23/−0
- test/Test/Langchain/Agent/ReAct.hs +364/−4
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. -[](https://hackage.haskell.org/package/langchain-hs)+[](https://hackage.haskell.org/package/langchain-hs) [](https://www.haskell.org/ghc/) [](LICENSE) [](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 ]