packages feed

langchain-hs-core 0.0.6.0 → 0.0.6.2

raw patch · 15 files changed

+2092/−243 lines, 15 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Langchain.Agent.PlanAndExecute: class StreamingStepExecutor e (m :: Type -> Type)
+ Langchain.Agent.PlanAndExecute: executeStepStream :: StreamingStepExecutor e m => e -> Text -> ConduitT () StreamEvent m Text
+ Langchain.Agent.PlanAndExecute: instance (Langchain.Model.ChatModel model, Control.Monad.IO.Class.MonadIO m, Control.Monad.Error.Class.MonadError Langchain.Error.LangchainError m) => Langchain.Agent.PlanAndExecute.StreamingStepExecutor model m
+ Langchain.Agent.PlanAndExecute: instance (Langchain.Tool.Binding.ToolBinder model n, Control.Monad.IO.Class.MonadIO n, Control.Monad.Error.Class.MonadError Langchain.Error.LangchainError n) => Langchain.Agent.PlanAndExecute.StreamingStepExecutor (Langchain.Agent.ReAct.ReActAgent model n) n
+ Langchain.Agent.PlanAndExecute: instance (Langchain.Tool.Binding.ToolBinder model n, Control.Monad.IO.Class.MonadIO n, Control.Monad.Error.Class.MonadError Langchain.Error.LangchainError n) => Langchain.Agent.PlanAndExecute.StreamingStepExecutor (model, [Langchain.Tool.Tool n]) n
+ Langchain.Agent.PlanAndExecute: instance (m GHC.Types.~ n, GHC.Base.Monad m) => Langchain.Agent.PlanAndExecute.StreamingStepExecutor (Data.Text.Internal.Text -> m Data.Text.Internal.Text) n
+ Langchain.Agent.PlanAndExecute: runPlanAndExecuteStream :: (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> (StreamEvent -> m ()) -> m Text
+ Langchain.Agent.PlanAndExecute: streamPlanAndExecute :: forall planner executor (m :: Type -> Type). (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> EventStream m
+ Langchain.Agent.PlanAndExecute: streamPlanAndExecuteWithResult :: forall planner executor (m :: Type -> Type). (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> ConduitT () StreamEvent m Text
+ Langchain.Agent.ReAct: AbortOnRepeat :: !Int -> LoopDetectionStrategy
+ Langchain.Agent.ReAct: AllowRepeats :: LoopDetectionStrategy
+ Langchain.Agent.ReAct: Cancelled :: !Message -> InterruptibleResult
+ Langchain.Agent.ReAct: Completed :: !Message -> InterruptibleResult
+ Langchain.Agent.ReAct: WarnOnRepeat :: !Int -> LoopDetectionStrategy
+ Langchain.Agent.ReAct: [agentLoopDetection] :: ReActAgent model (m :: Type -> Type) -> !LoopDetectionStrategy
+ Langchain.Agent.ReAct: [agentMaxDelegationDepth] :: ReActAgent model (m :: Type -> Type) -> !Int
+ Langchain.Agent.ReAct: [agentStreamingTools] :: ReActAgent model (m :: Type -> Type) -> [StreamingTool m]
+ Langchain.Agent.ReAct: [traceIterations] :: AgentTrace -> !Int
+ Langchain.Agent.ReAct: asAgentTool :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => Text -> Text -> ReActAgent model m -> Tool m
+ Langchain.Agent.ReAct: asStreamingAgentTool :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => Text -> Text -> ReActAgent model m -> StreamingTool m
+ Langchain.Agent.ReAct: data InterruptibleResult
+ Langchain.Agent.ReAct: data LoopDetectionStrategy
+ Langchain.Agent.ReAct: instance GHC.Classes.Eq Langchain.Agent.ReAct.InterruptibleResult
+ Langchain.Agent.ReAct: instance GHC.Classes.Eq Langchain.Agent.ReAct.LoopDetectionStrategy
+ Langchain.Agent.ReAct: instance GHC.Show.Show Langchain.Agent.ReAct.InterruptibleResult
+ Langchain.Agent.ReAct: instance GHC.Show.Show Langchain.Agent.ReAct.LoopDetectionStrategy
+ Langchain.Agent.ReAct: reactStepStream :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => model -> [Tool m] -> [Message] -> Maybe (ModelConfig model) -> ConduitT () StreamEvent m AgentStep
+ Langchain.Agent.ReAct: runReActAgentStream :: (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> (StreamEvent -> m ()) -> m Message
+ Langchain.Agent.ReAct: runReActAgentStreamInterruptible :: (ToolBinder model m, MonadIO m, MonadError LangchainError m) => CancelToken -> ReActAgent model m -> [Message] -> (StreamEvent -> m ()) -> m InterruptibleResult
+ Langchain.Agent.ReAct: streamReActAgent :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> EventStream m
+ Langchain.Agent.ReAct: streamReActAgentWithTrace :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> ConduitT () StreamEvent m AgentTrace
+ Langchain.Agent.ReAct: withLoopDetection :: forall model (m :: Type -> Type). LoopDetectionStrategy -> ReActAgent model m -> ReActAgent model m
+ Langchain.Agent.ReAct: withMaxConsecutiveIdenticalToolCalls :: forall model (m :: Type -> Type). Int -> ReActAgent model m -> ReActAgent model m
+ Langchain.Agent.ReAct: withMaxDelegationDepth :: forall model (m :: Type -> Type). Int -> ReActAgent model m -> ReActAgent model m
+ Langchain.Agent.ReAct: withStreamingTools :: forall (m :: Type -> Type) model. [StreamingTool m] -> ReActAgent model m -> ReActAgent model m
+ Langchain.Eval: CaseResult :: !Value -> !Maybe Value -> !Either Text Value -> ![CaseScoreResult] -> !Int -> !Bool -> CaseResult
+ Langchain.Eval: CaseScoreResult :: !Text -> !EvalScore -> CaseScoreResult
+ Langchain.Eval: EvalCase :: !i -> !Maybe o -> !Maybe (Map Text Text) -> EvalCase i o
+ Langchain.Eval: EvalReport :: !Int -> !Int -> !Double -> !Double -> !Int -> ![CaseResult] -> EvalReport
+ Langchain.Eval: EvalScore :: !Double -> !Bool -> !Maybe Text -> EvalScore
+ Langchain.Eval: Scorer :: !Text -> (EvalCase i o -> o -> m EvalScore) -> Scorer i o (m :: Type -> Type)
+ Langchain.Eval: [caseActual] :: CaseResult -> !Either Text Value
+ Langchain.Eval: [caseExpected] :: CaseResult -> !Maybe Value
+ Langchain.Eval: [caseInput] :: CaseResult -> !Value
+ Langchain.Eval: [caseLatencyMicros] :: CaseResult -> !Int
+ Langchain.Eval: [casePassed] :: CaseResult -> !Bool
+ Langchain.Eval: [caseScoreName] :: CaseScoreResult -> !Text
+ Langchain.Eval: [caseScore] :: CaseScoreResult -> !EvalScore
+ Langchain.Eval: [caseScores] :: CaseResult -> ![CaseScoreResult]
+ Langchain.Eval: [evalExpected] :: EvalCase i o -> !Maybe o
+ Langchain.Eval: [evalInput] :: EvalCase i o -> !i
+ Langchain.Eval: [evalMetadata] :: EvalCase i o -> !Maybe (Map Text Text)
+ Langchain.Eval: [reportCases] :: EvalReport -> ![CaseResult]
+ Langchain.Eval: [reportMeanLatencyMicros] :: EvalReport -> !Int
+ Langchain.Eval: [reportMeanScore] :: EvalReport -> !Double
+ Langchain.Eval: [reportPassRate] :: EvalReport -> !Double
+ Langchain.Eval: [reportPassedCases] :: EvalReport -> !Int
+ Langchain.Eval: [reportTotalCases] :: EvalReport -> !Int
+ Langchain.Eval: [scoreCase] :: Scorer i o (m :: Type -> Type) -> EvalCase i o -> o -> m EvalScore
+ Langchain.Eval: [scorePassed] :: EvalScore -> !Bool
+ Langchain.Eval: [scoreReason] :: EvalScore -> !Maybe Text
+ Langchain.Eval: [scoreValue] :: EvalScore -> !Double
+ Langchain.Eval: [scorerName] :: Scorer i o (m :: Type -> Type) -> !Text
+ Langchain.Eval: containsScorer :: forall (m :: Type -> Type). Monad m => Scorer Text Text m
+ Langchain.Eval: createScorer :: Text -> (EvalCase i o -> o -> m EvalScore) -> Scorer i o m
+ Langchain.Eval: data CaseResult
+ Langchain.Eval: data CaseScoreResult
+ Langchain.Eval: data EvalCase i o
+ Langchain.Eval: data EvalReport
+ Langchain.Eval: data EvalScore
+ Langchain.Eval: data Scorer i o (m :: Type -> Type)
+ Langchain.Eval: exactMatchScorer :: forall o (m :: Type -> Type) i. (Eq o, Show o, Monad m) => Scorer i o m
+ Langchain.Eval: instance (Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON o) => Data.Aeson.Types.FromJSON.FromJSON (Langchain.Eval.EvalCase i o)
+ Langchain.Eval: instance (Data.Aeson.Types.ToJSON.ToJSON o, Data.Aeson.Types.ToJSON.ToJSON i) => Data.Aeson.Types.ToJSON.ToJSON (Langchain.Eval.EvalCase i o)
+ Langchain.Eval: instance (GHC.Classes.Eq i, GHC.Classes.Eq o) => GHC.Classes.Eq (Langchain.Eval.EvalCase i o)
+ Langchain.Eval: instance (GHC.Show.Show i, GHC.Show.Show o) => GHC.Show.Show (Langchain.Eval.EvalCase i o)
+ Langchain.Eval: instance Data.Aeson.Types.FromJSON.FromJSON Langchain.Eval.CaseResult
+ Langchain.Eval: instance Data.Aeson.Types.FromJSON.FromJSON Langchain.Eval.CaseScoreResult
+ Langchain.Eval: instance Data.Aeson.Types.FromJSON.FromJSON Langchain.Eval.EvalReport
+ Langchain.Eval: instance Data.Aeson.Types.FromJSON.FromJSON Langchain.Eval.EvalScore
+ Langchain.Eval: instance Data.Aeson.Types.ToJSON.ToJSON Langchain.Eval.CaseResult
+ Langchain.Eval: instance Data.Aeson.Types.ToJSON.ToJSON Langchain.Eval.CaseScoreResult
+ Langchain.Eval: instance Data.Aeson.Types.ToJSON.ToJSON Langchain.Eval.EvalReport
+ Langchain.Eval: instance Data.Aeson.Types.ToJSON.ToJSON Langchain.Eval.EvalScore
+ Langchain.Eval: instance GHC.Classes.Eq Langchain.Eval.CaseResult
+ Langchain.Eval: instance GHC.Classes.Eq Langchain.Eval.CaseScoreResult
+ Langchain.Eval: instance GHC.Classes.Eq Langchain.Eval.EvalReport
+ Langchain.Eval: instance GHC.Classes.Eq Langchain.Eval.EvalScore
+ Langchain.Eval: instance GHC.Generics.Generic (Langchain.Eval.EvalCase i o)
+ Langchain.Eval: instance GHC.Generics.Generic Langchain.Eval.CaseResult
+ Langchain.Eval: instance GHC.Generics.Generic Langchain.Eval.CaseScoreResult
+ Langchain.Eval: instance GHC.Generics.Generic Langchain.Eval.EvalReport
+ Langchain.Eval: instance GHC.Generics.Generic Langchain.Eval.EvalScore
+ Langchain.Eval: instance GHC.Show.Show Langchain.Eval.CaseResult
+ Langchain.Eval: instance GHC.Show.Show Langchain.Eval.CaseScoreResult
+ Langchain.Eval: instance GHC.Show.Show Langchain.Eval.EvalReport
+ Langchain.Eval: instance GHC.Show.Show Langchain.Eval.EvalScore
+ Langchain.Eval: llmJudgeScorer :: forall model (m :: Type -> Type). (ChatModel model, MonadIO m, MonadError LangchainError m) => model -> Text -> Scorer Text Text m
+ Langchain.Eval: runEvalSuite :: forall i o m. (MonadIO m, ToJSON i, ToJSON o) => (i -> m (Either LangchainError o)) -> [Scorer i o m] -> [EvalCase i o] -> m EvalReport
+ Langchain.Eval: toolCallExactScorer :: forall (m :: Type -> Type) i. Monad m => [Text] -> Scorer i AgentTrace m
+ Langchain.Prelude: AbortOnRepeat :: !Int -> LoopDetectionStrategy
+ Langchain.Prelude: AllowRepeats :: LoopDetectionStrategy
+ Langchain.Prelude: Cancelled :: !Message -> InterruptibleResult
+ Langchain.Prelude: CaseResult :: !Value -> !Maybe Value -> !Either Text Value -> ![CaseScoreResult] -> !Int -> !Bool -> CaseResult
+ Langchain.Prelude: CaseScoreResult :: !Text -> !EvalScore -> CaseScoreResult
+ Langchain.Prelude: Completed :: !Message -> InterruptibleResult
+ Langchain.Prelude: EvalCase :: !i -> !Maybe o -> !Maybe (Map Text Text) -> EvalCase i o
+ Langchain.Prelude: EvalReport :: !Int -> !Int -> !Double -> !Double -> !Int -> ![CaseResult] -> EvalReport
+ Langchain.Prelude: EvalScore :: !Double -> !Bool -> !Maybe Text -> EvalScore
+ Langchain.Prelude: Scorer :: !Text -> (EvalCase i o -> o -> m EvalScore) -> Scorer i o (m :: Type -> Type)
+ Langchain.Prelude: StreamingTool :: Tool m -> (Value -> ConduitT () Text m (Either LangchainError Text)) -> StreamingTool (m :: Type -> Type)
+ Langchain.Prelude: ToolChunk :: Text -> Text -> Text -> StreamEvent
+ Langchain.Prelude: WarnOnRepeat :: !Int -> LoopDetectionStrategy
+ Langchain.Prelude: [caseActual] :: CaseResult -> !Either Text Value
+ Langchain.Prelude: [caseExpected] :: CaseResult -> !Maybe Value
+ Langchain.Prelude: [caseInput] :: CaseResult -> !Value
+ Langchain.Prelude: [caseLatencyMicros] :: CaseResult -> !Int
+ Langchain.Prelude: [casePassed] :: CaseResult -> !Bool
+ Langchain.Prelude: [caseScoreName] :: CaseScoreResult -> !Text
+ Langchain.Prelude: [caseScore] :: CaseScoreResult -> !EvalScore
+ Langchain.Prelude: [caseScores] :: CaseResult -> ![CaseScoreResult]
+ Langchain.Prelude: [evalExpected] :: EvalCase i o -> !Maybe o
+ Langchain.Prelude: [evalInput] :: EvalCase i o -> !i
+ Langchain.Prelude: [evalMetadata] :: EvalCase i o -> !Maybe (Map Text Text)
+ Langchain.Prelude: [reportCases] :: EvalReport -> ![CaseResult]
+ Langchain.Prelude: [reportMeanLatencyMicros] :: EvalReport -> !Int
+ Langchain.Prelude: [reportMeanScore] :: EvalReport -> !Double
+ Langchain.Prelude: [reportPassRate] :: EvalReport -> !Double
+ Langchain.Prelude: [reportPassedCases] :: EvalReport -> !Int
+ Langchain.Prelude: [reportTotalCases] :: EvalReport -> !Int
+ Langchain.Prelude: [scoreCase] :: Scorer i o (m :: Type -> Type) -> EvalCase i o -> o -> m EvalScore
+ Langchain.Prelude: [scorePassed] :: EvalScore -> !Bool
+ Langchain.Prelude: [scoreReason] :: EvalScore -> !Maybe Text
+ Langchain.Prelude: [scoreValue] :: EvalScore -> !Double
+ Langchain.Prelude: [scorerName] :: Scorer i o (m :: Type -> Type) -> !Text
+ Langchain.Prelude: [streamingToolDef] :: StreamingTool (m :: Type -> Type) -> Tool m
+ Langchain.Prelude: [streamingToolExecute] :: StreamingTool (m :: Type -> Type) -> Value -> ConduitT () Text m (Either LangchainError Text)
+ Langchain.Prelude: [traceIterations] :: AgentTrace -> !Int
+ Langchain.Prelude: asAgentTool :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => Text -> Text -> ReActAgent model m -> Tool m
+ Langchain.Prelude: asStreamingAgentTool :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => Text -> Text -> ReActAgent model m -> StreamingTool m
+ Langchain.Prelude: class StreamingStepExecutor e (m :: Type -> Type)
+ Langchain.Prelude: consumeStreamWith :: Monad m => (StreamEvent -> m ()) -> ConduitT StreamEvent Void m Message
+ Langchain.Prelude: containsScorer :: forall (m :: Type -> Type). Monad m => Scorer Text Text m
+ Langchain.Prelude: createScorer :: Text -> (EvalCase i o -> o -> m EvalScore) -> Scorer i o m
+ Langchain.Prelude: createStreamingTool :: forall (m :: Type -> Type). Monad m => Text -> Text -> Value -> (Value -> ConduitT () Text m (Either LangchainError Text)) -> StreamingTool m
+ Langchain.Prelude: data CancelToken
+ Langchain.Prelude: data CaseResult
+ Langchain.Prelude: data CaseScoreResult
+ Langchain.Prelude: data EvalCase i o
+ Langchain.Prelude: data EvalReport
+ Langchain.Prelude: data EvalScore
+ Langchain.Prelude: data InterruptibleResult
+ Langchain.Prelude: data LoopDetectionStrategy
+ Langchain.Prelude: data Scorer i o (m :: Type -> Type)
+ Langchain.Prelude: data StreamingTool (m :: Type -> Type)
+ Langchain.Prelude: exactMatchScorer :: forall o (m :: Type -> Type) i. (Eq o, Show o, Monad m) => Scorer i o m
+ Langchain.Prelude: executeStepStream :: StreamingStepExecutor e m => e -> Text -> ConduitT () StreamEvent m Text
+ Langchain.Prelude: fromTool :: forall (m :: Type -> Type). Monad m => Tool m -> StreamingTool m
+ Langchain.Prelude: isCancelled :: MonadIO m => CancelToken -> m Bool
+ Langchain.Prelude: liftChatStream :: forall (m :: Type -> Type). (MonadIO m, MonadError LangchainError m) => ChatStream -> EventStream m
+ Langchain.Prelude: liftChatStreamResource :: forall (m :: Type -> Type). (MonadResource m, MonadError LangchainError m) => ChatStream -> EventStream m
+ Langchain.Prelude: liftChatStreamWithCancel :: forall (m :: Type -> Type). (MonadIO m, MonadError LangchainError m) => CancelToken -> ChatStream -> EventStream m
+ Langchain.Prelude: llmJudgeScorer :: forall model (m :: Type -> Type). (ChatModel model, MonadIO m, MonadError LangchainError m) => model -> Text -> Scorer Text Text m
+ Langchain.Prelude: newCancelToken :: MonadIO m => m CancelToken
+ Langchain.Prelude: reactStepStream :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => model -> [Tool m] -> [Message] -> Maybe (ModelConfig model) -> ConduitT () StreamEvent m AgentStep
+ Langchain.Prelude: runEvalSuite :: forall i o m. (MonadIO m, ToJSON i, ToJSON o) => (i -> m (Either LangchainError o)) -> [Scorer i o m] -> [EvalCase i o] -> m EvalReport
+ Langchain.Prelude: runPlanAndExecuteStream :: (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> (StreamEvent -> m ()) -> m Text
+ Langchain.Prelude: runReActAgentStream :: (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> (StreamEvent -> m ()) -> m Message
+ Langchain.Prelude: runReActAgentStreamInterruptible :: (ToolBinder model m, MonadIO m, MonadError LangchainError m) => CancelToken -> ReActAgent model m -> [Message] -> (StreamEvent -> m ()) -> m InterruptibleResult
+ Langchain.Prelude: runStreamWith :: Monad m => (StreamEvent -> m ()) -> EventStream m -> m Message
+ Langchain.Prelude: streamPlanAndExecute :: forall planner executor (m :: Type -> Type). (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> EventStream m
+ Langchain.Prelude: streamPlanAndExecuteWithResult :: forall planner executor (m :: Type -> Type). (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) => PlanAndExecuteAgent planner executor -> Text -> ConduitT () StreamEvent m Text
+ Langchain.Prelude: streamReActAgent :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> EventStream m
+ Langchain.Prelude: streamReActAgentWithTrace :: forall model (m :: Type -> Type). (ToolBinder model m, MonadIO m, MonadError LangchainError m) => ReActAgent model m -> [Message] -> ConduitT () StreamEvent m AgentTrace
+ Langchain.Prelude: streamStreamingToolCall :: forall (m :: Type -> Type). Monad m => Text -> StreamingTool m -> Value -> ConduitT () StreamEvent m (Either LangchainError Text)
+ Langchain.Prelude: streamToolCall :: forall (m :: Type -> Type). Monad m => Text -> Tool m -> Value -> ConduitT () StreamEvent m (Either LangchainError Text)
+ Langchain.Prelude: streamingShellTool :: forall (m :: Type -> Type). MonadIO m => SecurityPolicy -> StreamingTool m
+ Langchain.Prelude: streamingShellToolWithCancel :: forall (m :: Type -> Type). MonadIO m => CancelToken -> SecurityPolicy -> StreamingTool m
+ Langchain.Prelude: tapStreamMessage :: forall (m :: Type -> Type). Monad m => ConduitT StreamEvent StreamEvent m Message
+ Langchain.Prelude: toolCallExactScorer :: forall (m :: Type -> Type) i. Monad m => [Text] -> Scorer i AgentTrace m
+ Langchain.Prelude: triggerCancel :: MonadIO m => CancelToken -> m ()
+ Langchain.Prelude: type ChatStream = EventStream StreamM
+ Langchain.Prelude: type StreamM = ExceptT LangchainError ResourceT IO
+ Langchain.Prelude: withLoopDetection :: forall model (m :: Type -> Type). LoopDetectionStrategy -> ReActAgent model m -> ReActAgent model m
+ Langchain.Prelude: withMaxConsecutiveIdenticalToolCalls :: forall model (m :: Type -> Type). Int -> ReActAgent model m -> ReActAgent model m
+ Langchain.Prelude: withMaxDelegationDepth :: forall model (m :: Type -> Type). Int -> ReActAgent model m -> ReActAgent model m
+ Langchain.Prelude: withStreamingSecurity :: forall (m :: Type -> Type). MonadIO m => ToolTier -> SecurityPolicy -> StreamingTool m -> StreamingTool m
+ Langchain.Prelude: withStreamingTools :: forall (m :: Type -> Type) model. [StreamingTool m] -> ReActAgent model m -> ReActAgent model m
+ Langchain.Stream: ToolChunk :: Text -> Text -> Text -> StreamEvent
+ Langchain.Stream: [toolChunkText] :: StreamEvent -> Text
+ Langchain.Stream: consumeStreamWith :: Monad m => (StreamEvent -> m ()) -> ConduitT StreamEvent Void m Message
+ Langchain.Stream: data CancelToken
+ Langchain.Stream: instance GHC.Classes.Eq Langchain.Stream.CancelToken
+ Langchain.Stream: isCancelled :: MonadIO m => CancelToken -> m Bool
+ Langchain.Stream: liftChatStream :: forall (m :: Type -> Type). (MonadIO m, MonadError LangchainError m) => ChatStream -> EventStream m
+ Langchain.Stream: liftChatStreamResource :: forall (m :: Type -> Type). (MonadResource m, MonadError LangchainError m) => ChatStream -> EventStream m
+ Langchain.Stream: liftChatStreamWithCancel :: forall (m :: Type -> Type). (MonadIO m, MonadError LangchainError m) => CancelToken -> ChatStream -> EventStream m
+ Langchain.Stream: newCancelToken :: MonadIO m => m CancelToken
+ Langchain.Stream: runStreamWith :: Monad m => (StreamEvent -> m ()) -> EventStream m -> m Message
+ Langchain.Stream: tapStreamMessage :: forall (m :: Type -> Type). Monad m => ConduitT StreamEvent StreamEvent m Message
+ Langchain.Stream: triggerCancel :: MonadIO m => CancelToken -> m ()
+ Langchain.Stream: waitCancel :: CancelToken -> STM ()
+ Langchain.Tool: StreamingTool :: Tool m -> (Value -> ConduitT () Text m (Either LangchainError Text)) -> StreamingTool (m :: Type -> Type)
+ Langchain.Tool: [streamingToolDef] :: StreamingTool (m :: Type -> Type) -> Tool m
+ Langchain.Tool: [streamingToolExecute] :: StreamingTool (m :: Type -> Type) -> Value -> ConduitT () Text m (Either LangchainError Text)
+ Langchain.Tool: createStreamingTool :: forall (m :: Type -> Type). Monad m => Text -> Text -> Value -> (Value -> ConduitT () Text m (Either LangchainError Text)) -> StreamingTool m
+ Langchain.Tool: data StreamingTool (m :: Type -> Type)
+ Langchain.Tool: fromTool :: forall (m :: Type -> Type). Monad m => Tool m -> StreamingTool m
+ Langchain.Tool: instance GHC.Show.Show (Langchain.Tool.StreamingTool m)
+ Langchain.Tool: streamStreamingToolCall :: forall (m :: Type -> Type). Monad m => Text -> StreamingTool m -> Value -> ConduitT () StreamEvent m (Either LangchainError Text)
+ Langchain.Tool: streamToolCall :: forall (m :: Type -> Type). Monad m => Text -> Tool m -> Value -> ConduitT () StreamEvent m (Either LangchainError Text)
+ Langchain.Tool.Security: withStreamingSecurity :: forall (m :: Type -> Type). MonadIO m => ToolTier -> SecurityPolicy -> StreamingTool m -> StreamingTool m
+ Langchain.Tool.Shell: streamingShellTool :: forall (m :: Type -> Type). MonadIO m => SecurityPolicy -> StreamingTool m
+ Langchain.Tool.Shell: streamingShellToolWithCancel :: forall (m :: Type -> Type). MonadIO m => CancelToken -> SecurityPolicy -> StreamingTool m
- Langchain.Agent.ReAct: AgentTrace :: [(AgentStep, [Message])] -> Message -> AgentTrace
+ Langchain.Agent.ReAct: AgentTrace :: [(AgentStep, [Message])] -> Message -> !Int -> AgentTrace
- Langchain.Agent.ReAct: ReActAgent :: model -> [Tool m] -> Int -> Maybe Text -> Maybe (ModelConfig model) -> Maybe SomeMemory -> Maybe CallbackManager -> Maybe (Message -> Bool) -> ToolErrorStrategy -> ReActAgent model (m :: Type -> Type)
+ Langchain.Agent.ReAct: ReActAgent :: model -> [Tool m] -> Int -> Maybe Text -> Maybe (ModelConfig model) -> Maybe SomeMemory -> Maybe CallbackManager -> Maybe (Message -> Bool) -> ToolErrorStrategy -> [StreamingTool m] -> !LoopDetectionStrategy -> !Int -> ReActAgent model (m :: Type -> Type)
- Langchain.Prelude: AgentTrace :: [(AgentStep, [Message])] -> Message -> AgentTrace
+ Langchain.Prelude: AgentTrace :: [(AgentStep, [Message])] -> Message -> !Int -> AgentTrace
- Langchain.Prelude: ReActAgent :: model -> [Tool m] -> Int -> Maybe Text -> Maybe (ModelConfig model) -> Maybe SomeMemory -> Maybe CallbackManager -> Maybe (Message -> Bool) -> ToolErrorStrategy -> ReActAgent model (m :: Type -> Type)
+ Langchain.Prelude: ReActAgent :: model -> [Tool m] -> Int -> Maybe Text -> Maybe (ModelConfig model) -> Maybe SomeMemory -> Maybe CallbackManager -> Maybe (Message -> Bool) -> ToolErrorStrategy -> [StreamingTool m] -> !LoopDetectionStrategy -> !Int -> ReActAgent model (m :: Type -> Type)

Files

CHANGELOG.md view
@@ -2,6 +2,29 @@  All notable changes to this package will be documented in this file. +## 0.0.6.2 - 2026-11-14++> **Note**: `0.0.6.1` was skipped globally after `langchain-hs-ollama-v0.0.6.1`.++- **Tool-Call & Agent Streaming**:+  - Extended `StreamEvent` in `Langchain.Stream` with `ToolStart`, `ToolChunk`, `ToolEnd`, and `ToolErrorEvent`, plus `liftChatStream` and `tapStreamMessage`.+  - Added cancellation-safe streaming combinators `liftChatStreamResource` and `liftChatStreamWithCancel` in `Langchain.Stream`.+  - Added `StreamingTool`, `createStreamingTool`, `fromTool`, `streamToolCall`, and `streamStreamingToolCall` to `Langchain.Tool`.+  - Added `streamingShellTool` (`Langchain.Tool.Shell`) and `withStreamingSecurity` (`Langchain.Tool.Security`).+  - Added `withStreamingTools`, `reactStepStream`, `streamReActAgent`, `streamReActAgentWithTrace`, and `runReActAgentStream` to `Langchain.Agent.ReAct`.+  - Added `streamPlanAndExecute` and `runPlanAndExecuteStream` to `Langchain.Agent.PlanAndExecute`.+  - Updated `Langchain.Prelude` exports for streaming tools and agents.+- **Multi-Agent Primitives & ReAct Loop Detection**:+  - Added `LoopDetectionStrategy` (`NoLoopDetection`, `WarnOnDuplicateAction`, `FailOnLoop`, `BreakOnLoop`) and iteration telemetry (`traceIterations`, `traceTerminatedBy`).+  - Added multi-agent handoff tools `asAgentTool` and `asStreamingAgentTool`.+- **Evaluation Harness (`Langchain.Eval`)**:+  - Added `EvalCase`, `EvalScore`, `Scorer`, and `EvalReport` with Aeson support.+  - Implemented `exactMatchScorer`, `containsScorer`, `toolCallExactScorer`, `llmJudgeScorer`, and `runEvalSuite`.+- **Security, Checkpointing, Memory & Parsers (consolidated post-`0.0.6.0` alignment)**:+  - `SecurityPolicy`, `FileSystem`, `Process`, and `Checkpoint` tool suites (`Langchain.Tool.*`).+  - `InVariableMemory`, `SessionMemory`, `EntityMemory`, `CombinedMemory`, `FileMemory`, and buffer reset helpers.+  - Updated `sqlite-simple` bounds to `>= 0.4.18 && < 0.5`.+ ## 0.0.6.0 - 2026-10-02  - Updated bounds for GHC 9.12 and modern Stackage LTS releases.
README.md view
@@ -32,7 +32,7 @@  ```cabal build-depends:-    langchain-hs-core >= 0.0.6.0 && < 0.0.7.0+    langchain-hs-core >= 0.0.6.2 && < 0.0.7.0 ```  To connect concrete LLMs, combine with satellite provider packages:
langchain-hs-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           langchain-hs-core-version:        0.0.6.0+version:        0.0.6.2 synopsis:       Core typeclasses and pure AST primitives for langchain-hs description:    Zero HTTP dependency core package containing ChatModel, RunnableTree, ContentBlock, Tool, and StreamEvent primitives. category:       Web, AI, Control@@ -91,6 +91,7 @@       Langchain.Agent.PlanAndExecute       Langchain.Guardrail       Langchain.Guardrail.Core+      Langchain.Eval       Langchain.Prelude   other-modules:       Paths_langchain_hs_core@@ -127,7 +128,7 @@     , sqlite-simple >=0.4.18 && <0.5     , stm ==2.5.*     , text >=1.2 && <3-    , time >=1.9 && <1.15+    , time >=1.9 && <1.16   default-language: Haskell2010  test-suite langchain-hs-core-test@@ -135,6 +136,7 @@   main-is: Spec.hs   other-modules:       Test.Langchain.Callback+      Test.Langchain.Eval       Test.Langchain.Model       Test.Langchain.Monad       Test.Langchain.Observability@@ -185,5 +187,5 @@     , tasty-hunit >=0.10     , tasty-quickcheck >=0.10     , text >=1.2 && <3-    , time >=1.9 && <1.15+    , time >=1.9 && <1.16   default-language: Haskell2010
src/Langchain/Agent/PlanAndExecute.hs view
@@ -27,22 +27,36 @@   ( PlanStep (..)   , Plan (..)   , StepExecutor (..)+  , StreamingStepExecutor (..)   , PlanAndExecuteAgent (..)   , newPlanAndExecuteAgent   , newPlanAndExecuteAgentWithTools   , runPlanAndExecute+  , streamPlanAndExecute+  , streamPlanAndExecuteWithResult+  , runPlanAndExecuteStream   ) where  import Control.Applicative ((<|>))+import Control.Monad (void) import Control.Monad.Except (MonadError, throwError) import Control.Monad.IO.Class (MonadIO)-import Data.Aeson (FromJSON (..), ToJSON, Value (..), withObject, (.!=), (.:), (.:?))+import Control.Monad.Trans (lift)+import Data.Aeson (FromJSON (..), ToJSON (toJSON), Value (..), withObject, (.!=), (.:), (.:?)) import Data.Aeson.Types (prependFailure, typeMismatch)+import Data.Conduit (ConduitT, fuseUpstream, runConduit, yield, (.|))+import qualified Data.Conduit.List as CL import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) -import Langchain.Agent.ReAct (ReActAgent, defaultReActAgent, runReActAgent)+import Langchain.Agent.ReAct+  ( AgentTrace (traceFinalAnswer)+  , ReActAgent+  , defaultReActAgent+  , runReActAgent+  , streamReActAgentWithTrace+  ) import Langchain.Error (LangchainError, agentError) import Langchain.Model   ( ChatModel (..)@@ -50,6 +64,7 @@   , userMessage   ) import Langchain.OutputParser.Structured (StructuredOutput, TypeSchema, structuredInvoke)+import Langchain.Stream (EventStream, StreamEvent (..), liftChatStream, tapStreamMessage) import Langchain.Tool (Tool) import Langchain.Tool.Binding (ToolBinder (..)) @@ -129,6 +144,45 @@     msg <- invoke model [userMessage prompt] Nothing     pure $ extractMessageText msg +-- | Streaming abstraction for executing individual steps of a plan.+class StreamingStepExecutor e m where+  executeStepStream ::+    -- | Step executor instance (agent, model with tools, function, or model)+    e ->+    -- | Step instruction prompt to execute+    Text ->+    -- | Stream of execution events returning the textual step result+    ConduitT () StreamEvent m Text++instance+  {-# OVERLAPPING #-}+  (ToolBinder model n, MonadIO n, MonadError LangchainError n) =>+  StreamingStepExecutor (ReActAgent model n) n+  where+  executeStepStream agent prompt = do+    trace <- streamReActAgentWithTrace agent [userMessage prompt]+    pure $ extractMessageText (traceFinalAnswer trace)++instance+  {-# OVERLAPPING #-}+  (ToolBinder model n, MonadIO n, MonadError LangchainError n) =>+  StreamingStepExecutor (model, [Tool n]) n+  where+  executeStepStream (model, tools) =+    executeStepStream (defaultReActAgent model tools)++instance {-# OVERLAPPING #-} (m ~ n, Monad m) => StreamingStepExecutor (Text -> m Text) n where+  executeStepStream f prompt = lift (f prompt)++instance+  {-# OVERLAPPABLE #-}+  (ChatModel model, MonadIO m, MonadError LangchainError m) =>+  StreamingStepExecutor model m+  where+  executeStepStream model prompt = do+    msg <- liftChatStream (stream model [userMessage prompt] Nothing) .| tapStreamMessage+    pure $ extractMessageText msg+ -- | Plan-and-Execute agent container data PlanAndExecuteAgent planner executor = PlanAndExecuteAgent   { plannerModel :: planner@@ -163,6 +217,41 @@ newPlanAndExecuteAgentWithTools planner model tools =   PlanAndExecuteAgent planner (defaultReActAgent model tools) +buildPlanPrompt :: Maybe Text -> Text -> Text+buildPlanPrompt (Just p) userGoal = p <> "\nGoal: " <> userGoal+buildPlanPrompt Nothing userGoal =+  "You are an expert planner. For the following goal, generate a concise step-by-step execution plan.\n"+    <> "Output JSON format: {\"planSteps\": [{\"stepNumber\": 1, \"stepDescription\": \"...\"}]}\n"+    <> "Keep the plan focused and minimal (between 2 to 3 distinct, actionable steps).\n"+    <> "Goal: "+    <> userGoal++buildStepPrompt :: Text -> [(PlanStep, Text)] -> PlanStep -> Text+buildStepPrompt userGoal prevOutputs currStep =+  "User Goal: "+    <> userGoal+    <> ( if null prevOutputs+           then ""+           else+             "\n\nCompleted Steps So Far:\n"+               <> T.unlines+                 [T.pack (show num) <> ". " <> desc <> " -> " <> out | (PlanStep num desc, out) <- prevOutputs]+       )+    <> "\n\nCurrent Task To Execute (Step "+    <> T.pack (show (stepNumber currStep))+    <> "): "+    <> stepDescription currStep+    <> "\nExecute this task using any appropriate tools available and provide the outcome:"++buildSynthesisPrompt :: Text -> [(PlanStep, Text)] -> Text+buildSynthesisPrompt userGoal stepOutputs =+  "User Goal: "+    <> userGoal+    <> "\n\nStep Execution History:\n"+    <> T.unlines+      [T.pack (show num) <> ". " <> desc <> " -> " <> out | (PlanStep num desc, out) <- stepOutputs]+    <> "\n\nProvide the final synthesized answer satisfying the goal:"+ -- | Execute a goal using the Plan-and-Execute workflow with structured JSON planning runPlanAndExecute ::   (ChatModel planner, StepExecutor executor m, MonadIO m, MonadError LangchainError m) =>@@ -173,43 +262,60 @@   -- | Final synthesized answer solving the user goal   m Text runPlanAndExecute PlanAndExecuteAgent {..} userGoal = do-  let planPrompt = case planPromptTemplate of-        Just p -> p <> "\nGoal: " <> userGoal-        Nothing ->-          "You are an expert planner. For the following goal, generate a concise step-by-step execution plan.\n"-            <> "Output JSON format: {\"planSteps\": [{\"stepNumber\": 1, \"stepDescription\": \"...\"}]}\n"-            <> "Keep the plan focused and minimal (between 2 to 3 distinct, actionable steps).\n"-            <> "Goal: "-            <> userGoal-  plan <- structuredInvoke plannerModel [userMessage planPrompt]+  plan <- structuredInvoke plannerModel [userMessage (buildPlanPrompt planPromptTemplate userGoal)]   if null (planSteps plan)     then throwError $ agentError "Planner generated an empty plan" (Just "PlanAndExecuteAgent") Nothing     else executeSteps (planSteps plan) []   where-    executeSteps [] stepOutputs = do-      let synthesisPrompt =-            "User Goal: "-              <> userGoal-              <> "\n\nStep Execution History:\n"-              <> T.unlines-                [T.pack (show num) <> ". " <> desc <> " -> " <> out | (PlanStep num desc, out) <- stepOutputs]-              <> "\n\nProvide the final synthesized answer satisfying the goal:"-      executeStep stepExecutor synthesisPrompt+    executeSteps [] stepOutputs =+      executeStep stepExecutor (buildSynthesisPrompt userGoal stepOutputs)     executeSteps (currStep : restSteps) prevOutputs = do-      let stepPrompt =-            "User Goal: "-              <> userGoal-              <> ( if null prevOutputs-                     then ""-                     else-                       "\n\nCompleted Steps So Far:\n"-                         <> T.unlines-                           [T.pack (show num) <> ". " <> desc <> " -> " <> out | (PlanStep num desc, out) <- prevOutputs]-                 )-              <> "\n\nCurrent Task To Execute (Step "-              <> T.pack (show (stepNumber currStep))-              <> "): "-              <> stepDescription currStep-              <> "\nExecute this task using any appropriate tools available and provide the outcome:"-      stepOut <- executeStep stepExecutor stepPrompt+      stepOut <- executeStep stepExecutor (buildStepPrompt userGoal prevOutputs currStep)       executeSteps restSteps (prevOutputs ++ [(currStep, stepOut)])++{- | Execute a goal using the Plan-and-Execute workflow while streaming 'StreamEvent's+and returning the final synthesized answer.+-}+streamPlanAndExecuteWithResult ::+  (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) =>+  PlanAndExecuteAgent planner executor ->+  Text ->+  ConduitT () StreamEvent m Text+streamPlanAndExecuteWithResult PlanAndExecuteAgent {..} userGoal = do+  let pRunId = "plan-and-execute-run"+  yield $ ChainStart pRunId "PlanAndExecuteAgent" (toJSON userGoal)+  plan <-+    lift $ structuredInvoke plannerModel [userMessage (buildPlanPrompt planPromptTemplate userGoal)]+  if null (planSteps plan)+    then throwError $ agentError "Planner generated an empty plan" (Just "PlanAndExecuteAgent") Nothing+    else do+      finalOut <- executeSteps pRunId (planSteps plan) []+      yield $ ChainEnd pRunId "PlanAndExecuteAgent" (toJSON finalOut)+      pure finalOut+  where+    executeSteps _ [] stepOutputs =+      executeStepStream stepExecutor (buildSynthesisPrompt userGoal stepOutputs)+    executeSteps pRunId (currStep : restSteps) prevOutputs = do+      let sNodeId = "step-" <> T.pack (show (stepNumber currStep))+      yield $ NodeStart pRunId sNodeId (toJSON currStep)+      stepOut <- executeStepStream stepExecutor (buildStepPrompt userGoal prevOutputs currStep)+      yield $ NodeEnd pRunId sNodeId (toJSON stepOut)+      executeSteps pRunId restSteps (prevOutputs ++ [(currStep, stepOut)])++-- | Execute a goal using the Plan-and-Execute workflow as an 'EventStream'.+streamPlanAndExecute ::+  (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) =>+  PlanAndExecuteAgent planner executor ->+  Text ->+  EventStream m+streamPlanAndExecute agent userGoal = void (streamPlanAndExecuteWithResult agent userGoal)++-- | Execute a Plan-and-Execute agent in streaming mode with an event callback.+runPlanAndExecuteStream ::+  (ChatModel planner, StreamingStepExecutor executor m, MonadIO m, MonadError LangchainError m) =>+  PlanAndExecuteAgent planner executor ->+  Text ->+  (StreamEvent -> m ()) ->+  m Text+runPlanAndExecuteStream agent userGoal onEvent =+  runConduit (streamPlanAndExecuteWithResult agent userGoal `fuseUpstream` CL.mapM_ onEvent)
src/Langchain/Agent/ReAct.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -49,6 +50,7 @@   , AgentTrace (..)   , ReActAgent (..)   , ToolErrorStrategy (..)+  , LoopDetectionStrategy (..)      -- * Construction   , defaultReActAgent@@ -62,26 +64,67 @@   , withCallbackManager   , withStopCondition   , withToolErrorStrategy+  , withStreamingTools+  , withLoopDetection+  , withMaxConsecutiveIdenticalToolCalls+  , withMaxDelegationDepth +    -- * Multi-Agent Handoff & Delegation Tools+  , asAgentTool+  , asStreamingAgentTool+     -- * Execution   , reactStep   , runReActAgent   , runReActAgentWithTrace++    -- * Streaming Execution+  , InterruptibleResult (..)+  , reactStepStream+  , streamReActAgent+  , streamReActAgentWithTrace+  , runReActAgentStream+  , runReActAgentStreamInterruptible   ) where -import Control.Monad (forM, when)+import Control.Monad (unless, when) import Control.Monad.Except (MonadError, catchError, throwError) import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans (lift)+import Data.Aeson (Value (..), object, toJSON, (.=))+import Data.Aeson.Types (parseEither, (.:?))+import Data.Conduit (ConduitT, await, fuseUpstream, mapOutput, runConduit, yield, (.|))+import qualified Data.Conduit.List as CL+import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef) import Data.List (find) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (diffUTCTime, getCurrentTime)  import Langchain.Callback.Manager (CallbackEvent (..), CallbackManager, dispatchEvent)-import Langchain.Error (LangchainError, agentError, errorMessage)+import Langchain.Error (LangchainError, agentError, errorMessage, toolError) import Langchain.Memory.Core (BaseMemory (..), SomeMemory (..)) import Langchain.Model import qualified Langchain.Model.Types as M+import Langchain.Stream+  ( CancelToken+  , EventStream+  , StreamEvent+    ( ChainEnd+    , ChainStart+    , LLMChunk+    , LLMEnd+    , LLMStart+    , ToolChunk+    , ToolEnd+    , ToolErrorEvent+    , ToolStart+    )+  , isCancelled+  , liftChatStream+  , liftChatStreamWithCancel+  , tapStreamMessage+  ) import Langchain.Tool import Langchain.Tool.Binding (ToolBinder (..)) @@ -93,6 +136,16 @@     FailOnError   deriving (Eq, Show) +-- | Strategy for detecting runaway loops where the model repeatedly invokes identical tools with identical arguments.+data LoopDetectionStrategy+  = -- | Allow consecutive repeat tool calls without intervention (default for backwards compatibility).+    AllowRepeats+  | -- | After @n@ consecutive identical tool call signatures, skip tool execution and inject an observation warning to the LLM.+    WarnOnRepeat !Int+  | -- | After @n@ consecutive identical tool call signatures, abort execution immediately with an 'AgentError' and notify 'CallbackManager'.+    AbortOnRepeat !Int+  deriving (Eq, Show)+ -- | A single step in the ReAct reasoning chain. data AgentStep   = -- | The model decided to call one or more tools.@@ -107,6 +160,8 @@   -- ^ Each pair is (the step, observation messages produced)   , traceFinalAnswer :: Message   -- ^ The final answer message+  , traceIterations :: !Int+  -- ^ Total reasoning iterations executed   }   deriving (Eq, Show) @@ -135,6 +190,12 @@   -- ^ Optional early-stop predicate checked after each LLM response   , agentToolErrorStrategy :: ToolErrorStrategy   -- ^ How to handle tool execution errors (default: 'ContinueOnError')+  , agentStreamingTools :: [StreamingTool m]+  -- ^ Optional streaming tools capable of emitting incremental 'ToolChunk' events+  , agentLoopDetection :: !LoopDetectionStrategy+  -- ^ Strategy for runaway consecutive duplicate tool-call loop detection (default: 'AllowRepeats')+  , agentMaxDelegationDepth :: !Int+  -- ^ Maximum allowed delegation depth for nested sub-agents (default: 5)   }  {- | Construct a ReAct agent with sensible defaults.@@ -159,6 +220,9 @@     , agentCallbackManager = Nothing     , agentStopCondition = Nothing     , agentToolErrorStrategy = ContinueOnError+    , agentStreamingTools = []+    , agentLoopDetection = AllowRepeats+    , agentMaxDelegationDepth = 5     }  {- | Attach a 'BaseMemory' instance for persistent conversation history.@@ -263,6 +327,49 @@   ReActAgent model m withToolErrorStrategy strategy agent = agent {agentToolErrorStrategy = strategy} +-- | Attach 'StreamingTool's that emit incremental 'ToolChunk' events during streaming execution.+withStreamingTools ::+  [StreamingTool m] ->+  ReActAgent model m ->+  ReActAgent model m+withStreamingTools sTools agent = agent {agentStreamingTools = sTools}++{- | Set the runaway loop detection strategy (default: 'AllowRepeats').++* 'AllowRepeats': no intervention on repeated identical tool calls.+* 'WarnOnRepeat': skip execution and inject a warning observation once consecutive duplicates reach @n@.+* 'AbortOnRepeat': immediately terminate the reasoning loop and throw 'AgentError' once consecutive duplicates reach @n@.+-}+withLoopDetection ::+  LoopDetectionStrategy ->+  ReActAgent model m ->+  ReActAgent model m+withLoopDetection strat agent = agent {agentLoopDetection = strat}++-- | Set runaway loop detection to abort after @n@ consecutive duplicate tool calls.+withMaxConsecutiveIdenticalToolCalls ::+  Int ->+  ReActAgent model m ->+  ReActAgent model m+withMaxConsecutiveIdenticalToolCalls n = withLoopDetection (AbortOnRepeat n)++-- | Set the maximum allowed agent delegation depth (default: 5).+withMaxDelegationDepth ::+  Int ->+  ReActAgent model m ->+  ReActAgent model m+withMaxDelegationDepth d agent = agent {agentMaxDelegationDepth = d}++-- | Classify a model response 'Message' as 'AgentAction' or 'AgentFinish'.+classifyStepResponse :: Message -> AgentStep+classifyStepResponse responseMsg =+  case messageToolCalls responseMsg of+    Just tcs@(_ : _) -> AgentAction responseMsg tcs+    _ -> AgentFinish responseMsg++allAgentTools :: ReActAgent model m -> [Tool m]+allAgentTools ReActAgent {..} = agentTools ++ map streamingToolDef agentStreamingTools+ {- | Run a single step of ReAct reasoning.  Sends the current message history (with tools bound) to the LLM and@@ -285,51 +392,51 @@ reactStep model tools history userCfg = do   let toolCfg = bindToolsConfig @model tools userCfg   responseMsg <- invoke model history toolCfg-  case messageToolCalls responseMsg of-    Just tcs@(_ : _) -> pure $ AgentAction responseMsg tcs-    _ -> pure $ AgentFinish responseMsg--{- | Execute the full ReAct reasoning loop, returning only the final answer.--For the full reasoning trace, use 'runReActAgentWithTrace'.--When the agent has a 'BaseMemory' attached:+  pure $ classifyStepResponse responseMsg -1. Existing conversation history is loaded from memory.-2. The user's input messages are appended.-3. After the loop finishes, both the user input and final answer are saved to memory.--}-runReActAgent ::+reactStepStreamWith ::   forall model m.   (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>-  -- | Configured ReAct agent instance-  ReActAgent model m ->-  -- | Input user messages for this turn+  Maybe CancelToken ->+  model ->+  [Tool m] ->   [Message] ->-  -- | The final assistant answer message-  m Message-runReActAgent agent userMsgs = traceFinalAnswer <$> runReActAgentWithTrace agent userMsgs--{- | Execute the full ReAct reasoning loop, returning both the final answer-and the complete intermediate reasoning trace.+  Maybe (ModelConfig model) ->+  ConduitT () StreamEvent m AgentStep+reactStepStreamWith mbTok model tools history userCfg = do+  let toolCfg = bindToolsConfig @model tools userCfg+      liftFn = maybe liftChatStream liftChatStreamWithCancel mbTok+  responseMsg <- liftFn (stream model history toolCfg) .| tapStreamMessage+  pure $ classifyStepResponse responseMsg -Tool calls within a single step are executed in sequence within the @m@ monad,-and the observations are fed back to the LLM for the next reasoning step.+{- | Run a single step of ReAct reasoning using streaming LLM invocation. -The trace includes every 'AgentStep' and the observation messages produced-at each iteration, which is invaluable for debugging and observability.+Forwards all 'StreamEvent's from the underlying 'ChatModel' 'stream'+(including 'LLMStart', 'LLMChunk' with text and 'toolCallDelta', and 'LLMEnd')+downstream in real time, and returns the resulting 'AgentStep'. -}-runReActAgentWithTrace ::+reactStepStream ::   forall model m.   (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>-  -- | Configured ReAct agent instance+  -- | The chat model to stream+  model ->+  -- | Available tools to bind to the model+  [Tool m] ->+  -- | Current conversation message history+  [Message] ->+  -- | Optional provider-specific model configuration+  Maybe (ModelConfig model) ->+  -- | Stream of LLM events returning the step classification+  ConduitT () StreamEvent m AgentStep+reactStepStream = reactStepStreamWith Nothing++buildInitialHistory ::+  (MonadIO m, MonadError LangchainError m) =>   ReActAgent model m ->-  -- | Input user messages for this turn   [Message] ->-  -- | Full reasoning trace containing all intermediate steps and final answer-  m AgentTrace-runReActAgentWithTrace ReActAgent {..} userMsgs = do-  -- 1. Build initial history: system prompt + memory + user messages+  Text ->+  m [Message]+buildInitialHistory ReActAgent {..} userMsgs caller = do   memoryMsgs <- case agentMemory of     Nothing -> pure []     Just (SomeMemory mem) -> messages mem@@ -347,135 +454,541 @@       agentError         "Cannot invoke ReAct agent with empty message history (no user query, memory, or system prompt)"         (Just "ReActAgent")-        (Just "runReActAgentWithTrace")+        (Just caller) -  -- 2. Run the reasoning loop-  trace <- go initialHistory agentMaxIterations []+  pure initialHistory -  -- 3. Persist to memory if available+persistTurnMemory ::+  (MonadIO m, MonadError LangchainError m) =>+  ReActAgent model m ->+  [Message] ->+  Message ->+  m ()+persistTurnMemory ReActAgent {..} userMsgs finalMsg =   case agentMemory of     Nothing -> pure ()     Just (SomeMemory mem) -> do-      -- Save each user message       mapM_ (addMessage mem) userMsgs-      -- Save the final answer-      addMessage mem (traceFinalAnswer trace)+      addMessage mem finalMsg -  pure trace+toolObservationMessage :: Text -> Text -> Text -> Message+toolObservationMessage tName tcId txt =+  (textMessage M.Tool txt)+    { M.messageName = Just tName+    , M.messageToolId = Just tcId+    }++dispatchCb :: MonadIO m => Maybe CallbackManager -> CallbackEvent -> m ()+dispatchCb Nothing _ = pure ()+dispatchCb (Just mgr) ev = dispatchEvent mgr ev++reactRunId :: Text+reactRunId = "react-agent-run"++-- | Execute a ReAct loop step-by-step, parameterized over the LLM step and tool executors.+runLoopWith ::+  forall model m n.+  (MonadIO n, MonadError LangchainError n) =>+  Maybe CancelToken ->+  ReActAgent model m ->+  ([Message] -> n AgentStep) ->+  (ToolCall -> n Message) ->+  [Message] ->+  Int ->+  [(AgentStep, [Message])] ->+  n AgentTrace+runLoopWith mbTok agent@ReActAgent {..} runStep execTool = go Nothing 0   where-    go :: [Message] -> Int -> [(AgentStep, [Message])] -> m AgentTrace-    go history maxIter accSteps+    checkCancelled = maybe (pure False) isCancelled mbTok++    execToolsWithCancel [] acc = pure (reverse acc)+    execToolsWithCancel (tc : rest) acc = do+      cancelled <- checkCancelled+      if cancelled+        then pure (reverse acc)+        else do+          obs <- execTool tc+          execToolsWithCancel rest (obs : acc)++    toolSignature tcs = [(toolCallName tc, toolCallArguments tc) | tc <- tcs]++    go mbLastSig repeatCount history maxIter accSteps       | maxIter <= 0 = do           let maxErr =                 "ReAct Agent exceeded maximum iterations ("                   <> T.pack (show agentMaxIterations)                   <> ")"-          case agentCallbackManager of-            Nothing -> pure ()-            Just mgr -> do-              now <- liftIO getCurrentTime-              dispatchEvent mgr (OnError "ReActAgent" maxErr now)+          now <- liftIO getCurrentTime+          dispatchCb agentCallbackManager (OnError "ReActAgent" maxErr now)           throwError $             agentError               maxErr               (Just "ReActAgent")               (Just "runReActAgent")       | otherwise = do-          -- Dispatch OnLLMStart callback before calling model-          llmStartTime <- liftIO getCurrentTime-          case agentCallbackManager of-            Nothing -> pure ()-            Just mgr ->-              dispatchEvent mgr (OnLLMStart "ReActAgent" (map extractMessageText history) llmStartTime)+          cancelledBefore <- checkCancelled+          if cancelledBefore+            then pure $ AgentTrace accSteps (assistantMessage "") (length accSteps)+            else do+              llmStartTime <- liftIO getCurrentTime+              dispatchCb+                agentCallbackManager+                (OnLLMStart "ReActAgent" (map extractMessageText history) llmStartTime) -          -- Invoke the model with error interception for observability-          eStep <--            (Right <$> reactStep agentModel agentTools history agentModelConfig)-              `catchError` (pure . Left)-          llmEndTime <- liftIO getCurrentTime-          let llmLatency = round (realToFrac (diffUTCTime llmEndTime llmStartTime) * 1000000 :: Double) :: Int-          case eStep of-            Left err -> do-              case agentCallbackManager of-                Nothing -> pure ()-                Just mgr ->-                  dispatchEvent mgr (OnError "ReActAgent" (errorMessage err) llmEndTime)-              throwError err-            Right step -> case step of-              AgentFinish finalMsg -> do-                case agentCallbackManager of-                  Nothing -> pure ()-                  Just mgr ->-                    dispatchEvent mgr (OnLLMEnd "ReActAgent" (extractMessageText finalMsg) llmLatency llmEndTime)-                let allSteps = accSteps ++ [(step, [])]-                pure $ AgentTrace allSteps finalMsg-              AgentAction respMsg tcs -> do-                case agentCallbackManager of-                  Nothing -> pure ()-                  Just mgr ->-                    dispatchEvent mgr (OnLLMEnd "ReActAgent" (extractMessageText respMsg) llmLatency llmEndTime)+              eStep <- (Right <$> runStep history) `catchError` (pure . Left)+              llmEndTime <- liftIO getCurrentTime+              let llmLatency = round (realToFrac (diffUTCTime llmEndTime llmStartTime) * 1000000 :: Double) :: Int+              case eStep of+                Left err -> do+                  dispatchCb agentCallbackManager (OnError "ReActAgent" (errorMessage err) llmEndTime)+                  throwError err+                Right step -> case step of+                  AgentFinish finalMsg -> do+                    dispatchCb+                      agentCallbackManager+                      (OnLLMEnd "ReActAgent" (extractMessageText finalMsg) llmLatency llmEndTime)+                    let finalSteps = accSteps ++ [(step, [])]+                    pure $ AgentTrace finalSteps finalMsg (length finalSteps)+                  AgentAction respMsg tcs -> do+                    dispatchCb+                      agentCallbackManager+                      (OnLLMEnd "ReActAgent" (extractMessageText respMsg) llmLatency llmEndTime)+                    cancelledAfterStep <- checkCancelled+                    if cancelledAfterStep+                      then do+                        let finalSteps = accSteps ++ [(step, [])]+                        pure $ AgentTrace finalSteps respMsg (length finalSteps)+                      else case agentStopCondition of+                        Just cond+                          | cond respMsg -> do+                              let finalSteps = accSteps ++ [(step, [])]+                              pure $ AgentTrace finalSteps respMsg (length finalSteps)+                        _ -> do+                          let currentSig = toolSignature tcs+                              nextRepeatCount =+                                case mbLastSig of+                                  Just prevSig | prevSig == currentSig -> repeatCount + 1+                                  _ -> 1+                          case agentLoopDetection of+                            AbortOnRepeat maxR+                              | nextRepeatCount >= max 1 maxR -> do+                                  let abortErr =+                                        "Runaway loop detected: consecutive identical tool calls ("+                                          <> T.pack (show nextRepeatCount)+                                          <> " repetitions)"+                                  now <- liftIO getCurrentTime+                                  dispatchCb agentCallbackManager (OnError "ReActAgent" abortErr now)+                                  throwError $+                                    agentError+                                      abortErr+                                      (Just "ReActAgent")+                                      (Just "runLoopWith:loopDetection")+                            WarnOnRepeat maxR+                              | nextRepeatCount >= max 1 maxR -> do+                                  let warnText tc =+                                        "Loop warning: tool '"+                                          <> toolCallName tc+                                          <> "' called with identical arguments "+                                          <> T.pack (show nextRepeatCount)+                                          <> " times consecutively. Please change your approach, adjust arguments, or provide your final answer."+                                      warnMsgs =+                                        [ toolObservationMessage+                                            (toolCallName tc)+                                            (toolCallId tc)+                                            (warnText tc)+                                        | tc <- tcs+                                        ]+                                      newHistory = history ++ [respMsg] ++ warnMsgs+                                      newSteps = accSteps ++ [(step, warnMsgs)]+                                  go (Just currentSig) nextRepeatCount newHistory (maxIter - 1) newSteps+                            _ -> do+                              obsMsgs <- execToolsWithCancel tcs []+                              let newHistory = history ++ [respMsg] ++ obsMsgs+                                  newSteps = accSteps ++ [(step, obsMsgs)]+                              let _ = agent+                              go (Just currentSig) nextRepeatCount newHistory (maxIter - 1) newSteps -                -- Check custom stop condition-                case agentStopCondition of-                  Just cond | cond respMsg -> do-                    let allSteps = accSteps ++ [(step, [])]-                    pure $ AgentTrace allSteps respMsg-                  _ -> do-                    obsMsgs <- forM tcs executeTool-                    let newHistory = history ++ [respMsg] ++ obsMsgs-                        newSteps = accSteps ++ [(step, obsMsgs)]-                    go newHistory (maxIter - 1) newSteps+handleToolOutcome ::+  (MonadIO n, MonadError LangchainError n) =>+  ReActAgent model m ->+  ToolCall ->+  (StreamEvent -> n ()) ->+  Either LangchainError Text ->+  Int ->+  n Message+handleToolOutcome ReActAgent {..} tc emitStream eRes durationMicros = do+  let tName = toolCallName tc+  endTime <- liftIO getCurrentTime+  case eRes of+    Left err -> do+      let errTxt = "Error executing tool " <> tName <> ": " <> errorMessage err+      dispatchCb agentCallbackManager (OnError tName errTxt endTime)+      emitStream $ ToolErrorEvent reactRunId tName err+      when (agentToolErrorStrategy == FailOnError) $+        throwError $+          agentError errTxt (Just "ReActAgent") (Just $ "executeTool:" <> tName)+      pure $ toolObservationMessage tName (toolCallId tc) errTxt+    Right outTxt -> do+      dispatchCb agentCallbackManager (OnToolEnd tName outTxt durationMicros endTime)+      emitStream $ ToolEnd reactRunId tName (toJSON outTxt)+      pure $ toolObservationMessage tName (toolCallId tc) outTxt -    executeTool :: ToolCall -> m Message-    executeTool tc = do-      let tName = toolCallName tc-      case agentCallbackManager of-        Nothing -> pure ()-        Just mgr -> do-          now <- liftIO getCurrentTime-          dispatchEvent mgr (OnToolStart tName (toolCallArguments tc) now)+executeToolSync ::+  forall model m.+  (MonadIO m, MonadError LangchainError m) =>+  ReActAgent model m ->+  ToolCall ->+  m Message+executeToolSync agent@ReActAgent {..} tc = do+  let tName = toolCallName tc+      tools = allAgentTools agent+  now <- liftIO getCurrentTime+  dispatchCb agentCallbackManager (OnToolStart tName (toolCallArguments tc) now)+  case find (\t -> toolName t == tName) tools of+    Nothing -> do+      let notFoundMsg = "Error: Tool not found: " <> tName+          err = agentError notFoundMsg (Just "ReActAgent") (Just $ "executeTool:" <> tName)+      handleToolOutcome agent tc (const (pure ())) (Left err) 0+    Just tool -> do+      startTime <- liftIO getCurrentTime+      eRes <- toolExecute tool (toolCallArguments tc)+      endTime <- liftIO getCurrentTime+      let durationMicros = round (realToFrac (diffUTCTime endTime startTime) * 1000000 :: Double) :: Int+      handleToolOutcome agent tc (const (pure ())) eRes durationMicros +executeToolStream ::+  forall model m.+  (MonadIO m, MonadError LangchainError m) =>+  ReActAgent model m ->+  ToolCall ->+  ConduitT () StreamEvent m Message+executeToolStream agent@ReActAgent {..} tc = do+  let tName = toolCallName tc+  now <- liftIO getCurrentTime+  dispatchCb agentCallbackManager (OnToolStart tName (toolCallArguments tc) now)+  yield $ ToolStart reactRunId tName (toolCallArguments tc)+  case find (\st -> toolName (streamingToolDef st) == tName) agentStreamingTools of+    Just sTool -> do+      startTime <- liftIO getCurrentTime+      eRes <- mapOutput (ToolChunk reactRunId tName) (streamingToolExecute sTool (toolCallArguments tc))+      endTime <- liftIO getCurrentTime+      let durationMicros = round (realToFrac (diffUTCTime endTime startTime) * 1000000 :: Double) :: Int+      handleToolOutcome agent tc yield eRes durationMicros+    Nothing ->       case find (\t -> toolName t == tName) agentTools of         Nothing -> do           let notFoundMsg = "Error: Tool not found: " <> tName-          case agentCallbackManager of-            Nothing -> pure ()-            Just mgr -> do-              now <- liftIO getCurrentTime-              dispatchEvent mgr (OnError tName notFoundMsg now)-          when (agentToolErrorStrategy == FailOnError) $-            throwError $-              agentError notFoundMsg (Just "ReActAgent") (Just $ "executeTool:" <> tName)-          pure $-            (textMessage M.Tool notFoundMsg)-              { M.messageName = Just tName-              , M.messageToolId = Just (toolCallId tc)-              }+              err = agentError notFoundMsg (Just "ReActAgent") (Just $ "executeTool:" <> tName)+          handleToolOutcome agent tc yield (Left err) 0         Just tool -> do           startTime <- liftIO getCurrentTime-          eRes <- toolExecute tool (toolCallArguments tc)+          eRes <- lift $ toolExecute tool (toolCallArguments tc)           endTime <- liftIO getCurrentTime           let durationMicros = round (realToFrac (diffUTCTime endTime startTime) * 1000000 :: Double) :: Int-          case eRes of-            Left err -> do-              let errTxt = "Error executing tool " <> tName <> ": " <> errorMessage err-              case agentCallbackManager of-                Nothing -> pure ()-                Just mgr -> dispatchEvent mgr (OnError tName errTxt endTime)-              when (agentToolErrorStrategy == FailOnError) $-                throwError $-                  agentError errTxt (Just "ReActAgent") (Just $ "executeTool:" <> tName)-              pure $-                (textMessage M.Tool errTxt)-                  { M.messageName = Just tName-                  , M.messageToolId = Just (toolCallId tc)-                  }-            Right outTxt -> do-              case agentCallbackManager of-                Nothing -> pure ()-                Just mgr -> dispatchEvent mgr (OnToolEnd tName outTxt durationMicros endTime)-              pure $-                (textMessage M.Tool outTxt)-                  { M.messageName = Just tName-                  , M.messageToolId = Just (toolCallId tc)-                  }+          handleToolOutcome agent tc yield eRes durationMicros++{- | Execute the full ReAct reasoning loop, returning only the final answer.++For the full reasoning trace, use 'runReActAgentWithTrace'.+For streaming execution, use 'streamReActAgent' or 'runReActAgentStream'.++When the agent has a 'BaseMemory' attached:++1. Existing conversation history is loaded from memory.+2. The user's input messages are appended.+3. After the loop finishes, both the user input and final answer are saved to memory.+-}+runReActAgent ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Configured ReAct agent instance+  ReActAgent model m ->+  -- | Input user messages for this turn+  [Message] ->+  -- | The final assistant answer message+  m Message+runReActAgent agent userMsgs = traceFinalAnswer <$> runReActAgentWithTrace agent userMsgs++{- | Execute the full ReAct reasoning loop, returning both the final answer+and the complete intermediate reasoning trace.++Tool calls within a single step are executed in sequence within the @m@ monad,+and the observations are fed back to the LLM for the next reasoning step.++The trace includes every 'AgentStep' and the observation messages produced+at each iteration, which is invaluable for debugging and observability.+-}+runReActAgentWithTrace ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Configured ReAct agent instance+  ReActAgent model m ->+  -- | Input user messages for this turn+  [Message] ->+  -- | Full reasoning trace containing all intermediate steps and final answer+  m AgentTrace+runReActAgentWithTrace agent@ReActAgent {..} userMsgs = do+  initialHistory <- buildInitialHistory agent userMsgs "runReActAgentWithTrace"+  let tools = allAgentTools agent+  trace <-+    runLoopWith+      Nothing+      agent+      (\hist -> reactStep agentModel tools hist agentModelConfig)+      (executeToolSync agent)+      initialHistory+      agentMaxIterations+      []+  persistTurnMemory agent userMsgs (traceFinalAnswer trace)+  pure trace++streamReActAgentTraceWith ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  Maybe CancelToken ->+  ReActAgent model m ->+  [Message] ->+  ConduitT () StreamEvent m AgentTrace+streamReActAgentTraceWith mbTok agent@ReActAgent {..} userMsgs = do+  initialHistory <- lift $ buildInitialHistory agent userMsgs "streamReActAgentWithTrace"+  let tools = allAgentTools agent+  yield $ ChainStart reactRunId "ReActAgent" (toJSON initialHistory)+  trace <-+    runLoopWith+      mbTok+      agent+      (\hist -> reactStepStreamWith mbTok agentModel tools hist agentModelConfig)+      (executeToolStream agent)+      initialHistory+      agentMaxIterations+      []+  cancelled <- maybe (pure False) isCancelled mbTok+  unless cancelled $ do+    lift $ persistTurnMemory agent userMsgs (traceFinalAnswer trace)+    yield $ ChainEnd reactRunId "ReActAgent" (toJSON (traceFinalAnswer trace))+  pure trace++{- | Execute the full ReAct reasoning loop as a Conduit stream of 'StreamEvent's,+returning the complete 'AgentTrace'.++Emits 'ChainStart', followed by 'LLMStart' / 'LLMChunk' / 'LLMEnd' for each+reasoning turn and 'ToolStart' / 'ToolChunk' / 'ToolEnd' / 'ToolErrorEvent' for+each tool invocation, concluding with 'ChainEnd'.+-}+streamReActAgentWithTrace ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Configured ReAct agent instance+  ReActAgent model m ->+  -- | Input user messages for this turn+  [Message] ->+  -- | Event stream returning the full reasoning trace+  ConduitT () StreamEvent m AgentTrace+streamReActAgentWithTrace = streamReActAgentTraceWith Nothing++{- | Execute the full ReAct reasoning loop as an 'EventStream'.++Suitable for composition with 'collectEvents', 'runStreamWith', or any Conduit sink.+-}+streamReActAgent ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Configured ReAct agent instance+  ReActAgent model m ->+  -- | Input user messages for this turn+  [Message] ->+  -- | Stream of agent lifecycle, LLM, and tool events+  EventStream m+streamReActAgent agent userMsgs = do+  _ <- streamReActAgentWithTrace agent userMsgs+  pure ()++-- | Outcome of an interruptible streaming ReAct turn.+data InterruptibleResult+  = -- | Turn completed normally with the final assistant message.+    Completed !Message+  | -- | Turn was cancelled via 'CancelToken', returning any partial assistant text emitted.+    Cancelled !Message+  deriving (Eq, Show)++{- | Execute an interruptible streaming ReAct turn using a cooperative 'CancelToken'.++Safe for @LangchainT () IO@ callers without @ResourceT@. When cancelled mid-turn,+upstream streaming stops promptly, conversation memory is left unmodified, and+'Cancelled' returns the partial assistant message streamed so far.+-}+runReActAgentStreamInterruptible ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  CancelToken ->+  ReActAgent model m ->+  [Message] ->+  (StreamEvent -> m ()) ->+  m InterruptibleResult+runReActAgentStreamInterruptible tok agent userMsgs onEvent = do+  cancelledStart <- isCancelled tok+  if cancelledStart+    then pure $ Cancelled (assistantMessage "")+    else do+      chunksRef <- liftIO $ newIORef ([] :: [Text])+      let trackAndForward ev = do+            liftIO $ case ev of+              LLMStart {} -> writeIORef chunksRef []+              LLMChunk _ txt _ | not (T.null txt) -> modifyIORef' chunksRef (txt :)+              LLMEnd _ msg _ -> writeIORef chunksRef [extractMessageText msg]+              _ -> pure ()+            onEvent ev+      trace <-+        runConduit+          ( streamReActAgentTraceWith (Just tok) agent userMsgs+              `fuseUpstream` CL.mapM_ trackAndForward+          )+      cancelledEnd <- isCancelled tok+      if cancelledEnd+        then do+          partialTxt <- liftIO (T.concat . reverse <$> readIORef chunksRef)+          pure $ Cancelled (assistantMessage partialTxt)+        else pure $ Completed (traceFinalAnswer trace)++{- | Execute the ReAct agent in streaming mode, invoking a callback for every+'StreamEvent' as it arrives and returning the final assistant 'Message'.+-}+runReActAgentStream ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Configured ReAct agent instance+  ReActAgent model m ->+  -- | Input user messages for this turn+  [Message] ->+  -- | Callback invoked on each 'StreamEvent' in real time+  (StreamEvent -> m ()) ->+  -- | The final assistant answer message+  m Message+runReActAgentStream agent userMsgs onEvent =+  traceFinalAnswer+    <$> runConduit (streamReActAgentWithTrace agent userMsgs `fuseUpstream` CL.mapM_ onEvent)++-- | JSON Schema describing the input expected by agent handoff tools.+handoffSchema :: Value+handoffSchema =+  object+    [ "type" .= ("object" :: Text)+    , "properties"+        .= object+          [ "task"+              .= object+                [ "type" .= ("string" :: Text)+                , "description" .= ("The task or query for the specialist agent" :: Text)+                ]+          ]+    , "required" .= (["task"] :: [Text])+    ]++-- | Extract the task query string from tool arguments.+extractAgentTask :: Text -> Value -> Either LangchainError Text+extractAgentTask toolNm = \case+  String s | not (T.null (T.strip s)) -> Right s+  Object o -> case parseEither (.:? "task") o of+    Right (Just task) | not (T.null (T.strip task)) -> Right task+    _ -> case parseEither (.:? "query") o of+      Right (Just q) | not (T.null (T.strip q)) -> Right q+      _ -> case parseEither (.:? "prompt") o of+        Right (Just p) | not (T.null (T.strip p)) -> Right p+        _ ->+          Left $+            toolError+              ("Missing or invalid 'task' parameter for agent tool: " <> toolNm)+              (Just toolNm)+              Nothing+  _ ->+    Left $+      toolError+        ("Invalid arguments object for agent tool: " <> toolNm)+        (Just toolNm)+        Nothing++{- | Wrap a 'ReActAgent' as a callable 'Tool' that delegates tasks to the sub-agent.++Accepts @{ "task": "<text>" }@ (or a raw JSON string) and returns the sub-agent's+final answer text. Bounded by 'agentMaxDelegationDepth' to prevent unbounded recursion.+-}+asAgentTool ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Tool name (e.g., "researcher")+  Text ->+  -- | Tool description explaining the specialist agent's role+  Text ->+  -- | Specialist sub-agent to delegate tasks to+  ReActAgent model m ->+  -- | Wrapped tool ready to be attached to a supervisor agent+  Tool m+asAgentTool name desc subAgent =+  createTool name desc handoffSchema $ \args -> do+    case extractAgentTask name args of+      Left err -> pure (Left err)+      Right task+        | agentMaxDelegationDepth subAgent <= 0 ->+            pure $+              Left $+                agentError+                  ("Maximum agent delegation depth exceeded for sub-agent '" <> name <> "'")+                  (Just "ReActAgent")+                  (Just "asAgentTool")+        | otherwise -> do+            let delegatedAgent =+                  subAgent {agentMaxDelegationDepth = agentMaxDelegationDepth subAgent - 1}+            eRes <- (Right <$> runReActAgent delegatedAgent [userMessage task]) `catchError` (pure . Left)+            pure $ fmap extractMessageText eRes++{- | Wrap a 'ReActAgent' as a 'StreamingTool' forwarding intermediate progress chunks to the caller.++Emits incremental sub-agent text and tool chunks downstream wrapped as 'ToolChunk' events.+-}+asStreamingAgentTool ::+  forall model m.+  (ToolBinder model m, MonadIO m, MonadError LangchainError m) =>+  -- | Tool name (e.g., "researcher")+  Text ->+  -- | Tool description explaining the specialist agent's role+  Text ->+  -- | Specialist sub-agent to delegate tasks to+  ReActAgent model m ->+  -- | Wrapped streaming tool emitting incremental sub-agent chunks+  StreamingTool m+asStreamingAgentTool name desc subAgent =+  createStreamingTool name desc handoffSchema $ \args -> do+    case extractAgentTask name args of+      Left err -> pure (Left err)+      Right task+        | agentMaxDelegationDepth subAgent <= 0 ->+            pure $+              Left $+                agentError+                  ("Maximum agent delegation depth exceeded for sub-agent '" <> name <> "'")+                  (Just "ReActAgent")+                  (Just "asStreamingAgentTool")+        | otherwise -> do+            let delegatedAgent =+                  subAgent {agentMaxDelegationDepth = agentMaxDelegationDepth subAgent - 1}+            ansRef <- liftIO $ newIORef ""+            let captureSub = do+                  trace <- streamReActAgentWithTrace delegatedAgent [userMessage task]+                  liftIO $ writeIORef ansRef (extractMessageText (traceFinalAnswer trace))+                forwardEvents = do+                  mEv <- await+                  case mEv of+                    Nothing -> pure ()+                    Just ev -> do+                      case ev of+                        LLMChunk _ txt Nothing | not (T.null txt) -> yield txt+                        ToolChunk _ _ chunk | not (T.null chunk) -> yield chunk+                        _ -> pure ()+                      forwardEvents+            catchError+              ( do+                  captureSub .| forwardEvents+                  ans <- liftIO $ readIORef ansRef+                  pure $ Right ans+              )+              (pure . Left)
+ src/Langchain/Eval.hs view
@@ -0,0 +1,308 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++{- |+Module      : Langchain.Eval+Description : Lightweight evaluation harness for chains, agents, and models+Copyright   : (c) 2025-2026 Tushar Adhatrao+License     : MIT+Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>+Stability   : experimental++Quantitative evaluation and regression testing framework for LLM applications.+Provides typed datasets ('EvalCase'), deterministic scorers (exact match, substring,+tool call trace accuracy), LLM-as-a-judge scoring, and JSON evaluation report generation.+-}+module Langchain.Eval+  ( -- * Core Types+    EvalCase (..)+  , EvalScore (..)+  , Scorer (..)+  , createScorer+  , CaseResult (..)+  , CaseScoreResult (..)+  , EvalReport (..)++    -- * Deterministic Scorers+  , exactMatchScorer+  , containsScorer+  , toolCallExactScorer++    -- * Model-based Scorers+  , llmJudgeScorer++    -- * Evaluation Runner+  , runEvalSuite+  ) where++import Control.Monad.Except (MonadError, catchError)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Aeson+  ( FromJSON (..)+  , ToJSON (..)+  , Value (..)+  , decode+  , withObject+  , (.:)+  , (.:?)+  )+import Data.Aeson.Types (parseEither)+import qualified Data.ByteString.Lazy as LBS+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Time.Clock (diffUTCTime, getCurrentTime)+import GHC.Generics (Generic)++import Langchain.Agent.ReAct (AgentStep (..), AgentTrace (..))+import Langchain.Error (LangchainError, errorMessage)+import Langchain.Model (ChatModel (..))+import Langchain.Model.Types (extractMessageText, toolCallName, userMessage)++-- | A single evaluation test case with input, optional reference target, and metadata.+data EvalCase i o = EvalCase+  { evalInput :: !i+  -- ^ Input supplied to the subject under test+  , evalExpected :: !(Maybe o)+  -- ^ Optional ground-truth or expected reference output+  , evalMetadata :: !(Maybe (Map Text Text))+  -- ^ Optional contextual tags or metadata+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- | Assessment produced by a 'Scorer' on an individual test case.+data EvalScore = EvalScore+  { scoreValue :: !Double+  -- ^ Normalized score between 0.0 and 1.0+  , scorePassed :: !Bool+  -- ^ Whether the case satisfied the threshold or assertion+  , scoreReason :: !(Maybe Text)+  -- ^ Diagnostic or qualitative reasoning for the score+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- | Named evaluation metric evaluating actual output against a test case.+data Scorer i o m = Scorer+  { scorerName :: !Text+  -- ^ Unique identifier for this evaluation metric+  , scoreCase :: EvalCase i o -> o -> m EvalScore+  -- ^ Evaluation function producing 'EvalScore'+  }++-- | Construct a custom 'Scorer'.+createScorer :: Text -> (EvalCase i o -> o -> m EvalScore) -> Scorer i o m+createScorer = Scorer++-- | Score result produced by a specific named scorer.+data CaseScoreResult = CaseScoreResult+  { caseScoreName :: !Text+  , caseScore :: !EvalScore+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- | Outcome of running all scorers on a single test case.+data CaseResult = CaseResult+  { caseInput :: !Value+  , caseExpected :: !(Maybe Value)+  , caseActual :: !(Either Text Value)+  , caseScores :: ![CaseScoreResult]+  , caseLatencyMicros :: !Int+  , casePassed :: !Bool+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- | Aggregated summary report of an evaluation suite run.+data EvalReport = EvalReport+  { reportTotalCases :: !Int+  -- ^ Total number of evaluated cases+  , reportPassedCases :: !Int+  -- ^ Number of cases that passed all scorers+  , reportPassRate :: !Double+  -- ^ Ratio of passed cases (0.0 to 1.0)+  , reportMeanScore :: !Double+  -- ^ Average score value across all evaluations (0.0 to 1.0)+  , reportMeanLatencyMicros :: !Int+  -- ^ Average execution latency per case in microseconds+  , reportCases :: ![CaseResult]+  -- ^ Detailed per-case scoring breakdown+  }+  deriving (Show, Eq, Generic, ToJSON, FromJSON)++-- | Deterministic scorer verifying exact equality between actual and expected output.+exactMatchScorer :: (Eq o, Show o, Monad m) => Scorer i o m+exactMatchScorer = createScorer "exact_match" $ \c actual ->+  case evalExpected c of+    Nothing -> pure $ EvalScore 1.0 True (Just "No expected output specified")+    Just expected ->+      if actual == expected+        then pure $ EvalScore 1.0 True (Just "Exact match")+        else+          pure $+            EvalScore+              0.0+              False+              (Just $ "Expected: " <> T.pack (show expected) <> ", Actual: " <> T.pack (show actual))++-- | Deterministic scorer checking if expected text is contained within actual text.+containsScorer :: Monad m => Scorer Text Text m+containsScorer = createScorer "contains" $ \c actual ->+  case evalExpected c of+    Nothing -> pure $ EvalScore 1.0 True (Just "No expected substring specified")+    Just expectedSub ->+      if expectedSub `T.isInfixOf` actual+        then pure $ EvalScore 1.0 True (Just "Substring found")+        else+          pure $+            EvalScore+              0.0+              False+              (Just $ "Substring '" <> expectedSub <> "' not found in actual output")++-- | Deterministic scorer verifying that an 'AgentTrace' invoked an exact sequence of tools.+toolCallExactScorer :: Monad m => [Text] -> Scorer i AgentTrace m+toolCallExactScorer expectedTools = createScorer "tool_call_exact" $ \_ trace -> do+  let actualTools =+        [ toolCallName tc+        | (AgentAction _ tcs, _) <- traceSteps trace+        , tc <- tcs+        ]+  if actualTools == expectedTools+    then pure $ EvalScore 1.0 True (Just "Tool call sequence matched exactly")+    else+      pure $+        EvalScore+          0.0+          False+          ( Just $+              "Expected tool sequence: "+                <> T.pack (show expectedTools)+                <> ", but got: "+                <> T.pack (show actualTools)+          )++-- | LLM-as-a-judge scorer evaluating output against a custom qualitative rubric.+llmJudgeScorer ::+  forall model m.+  (ChatModel model, MonadIO m, MonadError LangchainError m) =>+  model ->+  Text ->+  Scorer Text Text m+llmJudgeScorer model rubric = createScorer "llm_judge" $ \c actual -> do+  let inputTxt = evalInput c+      expectedTxt = fromMaybe "" (evalExpected c)+      prompt =+        "You are an expert AI evaluator. Assess the following actual output against the rubric.\n\n"+          <> "Evaluation Rubric: "+          <> rubric+          <> "\n\n"+          <> "Test Input: "+          <> inputTxt+          <> "\n"+          <> "Reference Target: "+          <> expectedTxt+          <> "\n"+          <> "Actual Output: "+          <> actual+          <> "\n\n"+          <> "Reply with a JSON object: {\"passed\": true/false, \"score\": 0.0-1.0, \"reason\": \"<explanation>\"}"+  eResp <- (Right <$> invoke model [userMessage prompt] Nothing) `catchError` (pure . Left)+  case eResp of+    Left err -> pure $ EvalScore 0.0 False (Just $ "LLM judge invocation failed: " <> errorMessage err)+    Right resp -> do+      let outTxt = extractMessageText resp+      case parseJudgeJson outTxt of+        Just (passed, sc, rsn) -> pure $ EvalScore sc passed (Just rsn)+        Nothing ->+          let passed = not ("fail" `T.isInfixOf` T.toLower outTxt)+              sc = if passed then 1.0 else 0.0+           in pure $ EvalScore sc passed (Just outTxt)++parseJudgeJson :: Text -> Maybe (Bool, Double, Text)+parseJudgeJson txt = do+  val <- decode (LBS.fromStrict (TE.encodeUtf8 txt))+  case parseEither parser val of+    Right res -> Just res+    Left _ -> Nothing+  where+    parser = withObject "JudgeResponse" $ \o -> do+      passed <- o .: "passed"+      sc <-+        o .:? "score" >>= \case+          Just s -> pure s+          Nothing -> pure (if passed then 1.0 else 0.0)+      rsn <-+        o .:? "reason" >>= \case+          Just r -> pure r+          Nothing -> pure ""+      pure (passed, sc, rsn)++-- | Execute an evaluation suite over a set of test cases and scorers.+runEvalSuite ::+  forall i o m.+  (MonadIO m, ToJSON i, ToJSON o) =>+  (i -> m (Either LangchainError o)) ->+  [Scorer i o m] ->+  [EvalCase i o] ->+  m EvalReport+runEvalSuite runner scorers cases = do+  caseResults <- mapM evaluateCase cases+  let total = length caseResults+      passedCount = length (filter casePassed caseResults)+      passRate = if total == 0 then 1.0 else fromIntegral passedCount / fromIntegral total+      allScores = [scoreValue (caseScore sr) | cr <- caseResults, sr <- caseScores cr]+      meanScore =+        if null allScores+          then if total == 0 then 1.0 else 0.0+          else sum allScores / fromIntegral (length allScores)+      allLatencies = map caseLatencyMicros caseResults+      meanLatency =+        if null allLatencies+          then 0+          else sum allLatencies `div` total+  pure+    EvalReport+      { reportTotalCases = total+      , reportPassedCases = passedCount+      , reportPassRate = passRate+      , reportMeanScore = meanScore+      , reportMeanLatencyMicros = meanLatency+      , reportCases = caseResults+      }+  where+    evaluateCase c = do+      startTime <- liftIO getCurrentTime+      eActual <- runner (evalInput c)+      endTime <- liftIO getCurrentTime+      let latencyMicros =+            round (realToFrac (diffUTCTime endTime startTime) * 1000000 :: Double) :: Int+      case eActual of+        Left err -> do+          let failedScore = EvalScore 0.0 False (Just $ errorMessage err)+              scores = [CaseScoreResult (scorerName s) failedScore | s <- scorers]+          pure+            CaseResult+              { caseInput = toJSON (evalInput c)+              , caseExpected = fmap toJSON (evalExpected c)+              , caseActual = Left (errorMessage err)+              , caseScores = scores+              , caseLatencyMicros = latencyMicros+              , casePassed = False+              }+        Right actual -> do+          scores <- mapM (\s -> CaseScoreResult (scorerName s) <$> scoreCase s c actual) scorers+          let caseOk = all (scorePassed . caseScore) scores+          pure+            CaseResult+              { caseInput = toJSON (evalInput c)+              , caseExpected = fmap toJSON (evalExpected c)+              , caseActual = Right (toJSON actual)+              , caseScores = scores+              , caseLatencyMicros = latencyMicros+              , casePassed = caseOk+              }
src/Langchain/Prelude.hs view
@@ -56,6 +56,7 @@     , LLMChunk     , LLMEnd     , ToolStart+    , ToolChunk     , ToolEnd     , ToolErrorEvent     , ChainStart@@ -65,6 +66,18 @@     )   , TokenUsage (..)   , EventStream+  , StreamM+  , ChatStream+  , CancelToken+  , newCancelToken+  , triggerCancel+  , isCancelled+  , liftChatStream+  , liftChatStreamResource+  , liftChatStreamWithCancel+  , tapStreamMessage+  , consumeStreamWith+  , runStreamWith   , collectEvents   , printEvents @@ -100,6 +113,11 @@   , Tool (..)   , createTool   , toolToValue+  , StreamingTool (..)+  , createStreamingTool+  , fromTool+  , streamToolCall+  , streamStreamingToolCall   , ToolBinder (..)   , DeriveToolSchema (..)   , deriveToolParametersSchema@@ -108,10 +126,14 @@   , PlanStep (..)   , Plan (..)   , StepExecutor (..)+  , StreamingStepExecutor (..)   , PlanAndExecuteAgent (..)   , newPlanAndExecuteAgent   , newPlanAndExecuteAgentWithTools   , runPlanAndExecute+  , streamPlanAndExecute+  , streamPlanAndExecuteWithResult+  , runPlanAndExecuteStream      -- * Guardrails & Safety   , GuardrailResult (..)@@ -293,6 +315,7 @@   , AgentStep (..)   , AgentTrace (..)   , ToolErrorStrategy (..)+  , LoopDetectionStrategy (..)   , defaultReActAgent   , withMemory   , withoutMemory@@ -302,9 +325,21 @@   , withCallbackManager   , withStopCondition   , withToolErrorStrategy+  , withStreamingTools+  , withLoopDetection+  , withMaxConsecutiveIdenticalToolCalls+  , withMaxDelegationDepth+  , asAgentTool+  , asStreamingAgentTool   , reactStep   , runReActAgent   , runReActAgentWithTrace+  , reactStepStream+  , streamReActAgent+  , streamReActAgentWithTrace+  , runReActAgentStream+  , InterruptibleResult (..)+  , runReActAgentStreamInterruptible      -- * Tool Suite & Security   , ToolTier (..)@@ -313,6 +348,7 @@   , readOnlySecurityPolicy   , workspaceSandboxPolicy   , withSecurity+  , withStreamingSecurity   , newFileSystemSuite   , findFilesTool   , grepSearchTool@@ -325,6 +361,8 @@   , commandStatusTool   , commandKillTool   , shellTool+  , streamingShellTool+  , streamingShellToolWithCancel      -- * Hybrid Retrieval & BM25   , BM25Index (..)@@ -339,6 +377,20 @@   , searchHybrid   , searchHybridWithScores   , reciprocalRankFusion++    -- * Evaluation & Regression Testing+  , EvalCase (..)+  , EvalScore (..)+  , Scorer (..)+  , createScorer+  , CaseResult (..)+  , CaseScoreResult (..)+  , EvalReport (..)+  , exactMatchScorer+  , containsScorer+  , toolCallExactScorer+  , llmJudgeScorer+  , runEvalSuite   ) where  import Langchain.Agent.PlanAndExecute@@ -351,6 +403,7 @@ import Langchain.DocumentLoader.FileLoader import Langchain.Embeddings.Core import Langchain.Error+import Langchain.Eval import Langchain.Guardrail.Core import Langchain.Memory import Langchain.Model@@ -377,6 +430,6 @@ import Langchain.Tool.GenericSchema import Langchain.Tool.Process import Langchain.Tool.Security-import Langchain.Tool.Shell (shellTool)+import Langchain.Tool.Shell (shellTool, streamingShellTool, streamingShellToolWithCancel) import Langchain.VectorStore.Core import Langchain.VectorStore.InMemory
src/Langchain/Stream.hs view
@@ -21,32 +21,60 @@   , StreamCallback   , StreamSource   , callbackSource+  , liftChatStream+  , liftChatStreamResource+  , CancelToken+  , newCancelToken+  , triggerCancel+  , isCancelled+  , waitCancel+  , liftChatStreamWithCancel+  , tapStreamMessage+  , consumeStreamWith+  , runStreamWith   , collectEvents   , printEvents   ) where -import Control.Concurrent.Async (async, cancel)+import Control.Concurrent.Async (Async, AsyncCancelled (..), async, cancel, wait) import Control.Concurrent.STM-  ( atomically+  ( STM+  , TBQueue+  , TMVar+  , atomically   , newEmptyTMVarIO   , newTBQueueIO   , orElse   , putTMVar   , readTBQueue   , readTMVar+  , tryPutTMVar+  , tryReadTMVar   , writeTBQueue   )-import Control.Exception (finally)-import Control.Monad.Except (ExceptT, runExceptT)-import Control.Monad.IO.Class (liftIO)-import Control.Monad.Trans.Resource (ResourceT, runResourceT)+import Control.Exception+  ( Exception (displayException)+  , SomeException+  , catch+  , finally+  , fromException+  , onException+  , throwIO+  )+import Control.Monad (void)+import Control.Monad.Except (ExceptT, MonadError (throwError), runExceptT)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Resource (MonadResource, ResourceT, runResourceT) import Data.Aeson (FromJSON, ToJSON, Value)-import Data.Conduit (ConduitT, bracketP, runConduit, yield, (.|))+import Data.Conduit (ConduitT, Void, await, bracketP, fuseUpstream, runConduit, yield, (.|)) import qualified Data.Conduit.List as CL+import Data.IORef (newIORef, readIORef, writeIORef)+import Data.Maybe (isJust) import Data.Text (Text)+import qualified Data.Text as T import GHC.Generics (Generic)-import Langchain.Error (LangchainError)-import Langchain.Model.Types (Message, ToolCall)+import Langchain.Error (LangchainError, llmError)+import Langchain.Model.Types (Message (..), ToolCall, assistantMessage)  -- | Token usage accounting for LLM execution. data TokenUsage = TokenUsage@@ -84,6 +112,12 @@       , toolName :: Text       , toolInput :: Value       }+  | -- | Tool incremental streaming output chunk+    ToolChunk+      { runId :: Text+      , toolName :: Text+      , toolChunkText :: Text+      }   | -- | Tool execution completion     ToolEnd       { runId :: Text@@ -159,6 +193,176 @@             Nothing -> pure ()      third (_, _, worker) = worker++-- | Cooperative single-shot cancellation token backed by an STM 'TMVar'.+newtype CancelToken = CancelToken (TMVar ())+  deriving (Eq)++-- | Allocate a new untriggered 'CancelToken'.+newCancelToken :: MonadIO m => m CancelToken+newCancelToken = CancelToken <$> liftIO newEmptyTMVarIO++-- | Signal cancellation on a 'CancelToken' (idempotent).+triggerCancel :: MonadIO m => CancelToken -> m ()+triggerCancel (CancelToken var) = liftIO . atomically . void $ tryPutTMVar var ()++-- | Check whether a 'CancelToken' has been triggered.+isCancelled :: MonadIO m => CancelToken -> m Bool+isCancelled (CancelToken var) = liftIO . atomically $ isJust <$> tryReadTMVar var++waitCancel :: CancelToken -> STM ()+waitCancel (CancelToken var) = readTMVar var++startChatWorker ::+  ChatStream ->+  IO (TBQueue StreamEvent, TMVar (Maybe LangchainError), Async ())+startChatWorker chatStream = do+  q <- newTBQueueIO 64+  done <- newEmptyTMVarIO+  errRef <- newIORef Nothing+  w <-+    async $+      ( do+          eRes <-+            runResourceT $+              runExceptT $+                runConduit $+                  chatStream .| CL.mapM_ (liftIO . atomically . writeTBQueue q)+          case eRes of+            Right () -> pure ()+            Left err -> writeIORef errRef (Just err)+      )+        `catch` ( \ex -> case fromException ex of+                    Just AsyncCancelled -> throwIO (ex :: SomeException)+                    Nothing ->+                      writeIORef+                        errRef+                        (Just $ llmError (T.pack (displayException ex)) (Just "Stream") Nothing)+                )+        `finally` (readIORef errRef >>= atomically . void . tryPutTMVar done)+  pure (q, done, w)++consumeChatWorker ::+  (MonadIO m, MonadError LangchainError m) =>+  Maybe CancelToken ->+  (TBQueue StreamEvent, TMVar (Maybe LangchainError), Async ()) ->+  EventStream m+consumeChatWorker mbTok (queue, doneVar, worker) = loop+  where+    loop = do+      next <-+        liftIO $+          atomically+            ( case mbTok of+                Just tok ->+                  (Left Nothing <$ waitCancel tok)+                    `orElse` (Right <$> readTBQueue queue)+                    `orElse` (Left . Just <$> readTMVar doneVar)+                Nothing ->+                  (Right <$> readTBQueue queue)+                    `orElse` (Left . Just <$> readTMVar doneVar)+            )+            `onException` cancel worker+      case next of+        Right ev -> do+          yield ev+          loop+        Left Nothing ->+          liftIO $ cancel worker+        Left (Just Nothing) ->+          liftIO $ wait worker+        Left (Just (Just err)) -> do+          liftIO $ cancel worker+          throwError err++{- | Lift a resource-safe 'ChatStream' into any 'MonadIO' and 'MonadError LangchainError'+'EventStream'. Events are forwarded incrementally through a bounded STM queue,+and cancelling or throwing an exception cancels the upstream stream worker promptly.+-}+liftChatStream ::+  (MonadIO m, MonadError LangchainError m) =>+  ChatStream ->+  EventStream m+liftChatStream chatStream = do+  st <- liftIO $ startChatWorker chatStream+  consumeChatWorker Nothing st++{- | Lift a resource-safe 'ChatStream' into any 'MonadResource' and+'MonadError LangchainError' 'EventStream'. Uses 'bracketP' so stopping+consumption early (e.g. @src .| CL.take 1@) cancels the upstream producer+immediately.+-}+liftChatStreamResource ::+  (MonadResource m, MonadError LangchainError m) =>+  ChatStream ->+  EventStream m+liftChatStreamResource chatStream =+  bracketP (startChatWorker chatStream) (\(_, _, w) -> cancel w) (consumeChatWorker Nothing)++{- | Lift a resource-safe 'ChatStream' with cooperative cancellation via+'CancelToken'. When cancelled, the upstream stream worker is terminated promptly+and the Conduit exits cleanly so downstream accumulators preserve partial text.+-}+liftChatStreamWithCancel ::+  (MonadIO m, MonadError LangchainError m) =>+  CancelToken ->+  ChatStream ->+  EventStream m+liftChatStreamWithCancel tok chatStream = do+  st <- liftIO $ startChatWorker chatStream+  consumeChatWorker (Just tok) st++{- | Pass every 'StreamEvent' downstream unchanged while accumulating the final 'Message'+from the last 'LLMEnd' event (or synthesizing one from accumulated 'LLMChunk's if+no 'LLMEnd' event was emitted).+-}+tapStreamMessage :: Monad m => ConduitT StreamEvent StreamEvent m Message+tapStreamMessage = loop [] [] Nothing+  where+    loop accText accTools mbFinal = do+      mEv <- await+      case mEv of+        Nothing ->+          pure $ case mbFinal of+            Just msg ->+              case (messageToolCalls msg, reverse accTools) of+                (Nothing, ts@(_ : _)) -> msg {messageToolCalls = Just ts}+                _ -> msg+            Nothing ->+              let fullText = T.concat (reverse accText)+                  ts = reverse accTools+               in (assistantMessage fullText)+                    { messageToolCalls = if null ts then Nothing else Just ts+                    }+        Just ev -> do+          yield ev+          case ev of+            LLMStart {} -> loop [] [] Nothing+            LLMChunk {..} ->+              let nextText = if T.null chunkText then accText else chunkText : accText+                  nextTools = maybe accTools (: accTools) toolCallDelta+               in loop nextText nextTools mbFinal+            LLMEnd {..} ->+              loop accText accTools (Just finalMessage)+            _ -> loop accText accTools mbFinal++{- | Consume a 'StreamEvent' pipeline, invoking a handler for every event, and+return the final 'Message' from the last 'LLMEnd' event (or synthesize one from+accumulated 'LLMChunk's if no 'LLMEnd' event was emitted).+-}+consumeStreamWith ::+  Monad m =>+  (StreamEvent -> m ()) ->+  ConduitT StreamEvent Void m Message+consumeStreamWith onEvent = tapStreamMessage `fuseUpstream` CL.mapM_ onEvent++-- | Run an 'EventStream' with an event handler and return the resulting 'Message'.+runStreamWith ::+  Monad m =>+  (StreamEvent -> m ()) ->+  EventStream m ->+  m Message+runStreamWith onEvent src = runConduit (src .| consumeStreamWith onEvent)  -- | Collect all emitted events from a stream into a list. collectEvents :: Monad m => EventStream m -> m [StreamEvent]
src/Langchain/Tool.hs view
@@ -14,12 +14,20 @@   ( Tool (..)   , createTool   , toolToValue+  , StreamingTool (..)+  , createStreamingTool+  , fromTool+  , streamToolCall+  , streamStreamingToolCall   ) where +import Control.Monad.Trans (lift) import Data.Aeson+import Data.Conduit (ConduitT, await, fuseUpstream, mapOutput, runConduit, yield) import Data.Text (Text)  import Langchain.Error (LangchainError)+import Langchain.Stream (StreamEvent (ToolChunk, ToolEnd, ToolErrorEvent, ToolStart))  -- | Effect-polymorphic Tool abstraction data Tool m = Tool@@ -63,3 +71,90 @@           , "parameters" .= toolSchema           ]     ]++-- | Effect-polymorphic streaming Tool abstraction supporting incremental 'Text' output chunks.+data StreamingTool m = StreamingTool+  { streamingToolDef :: Tool m+  -- ^ Underlying tool specification and synchronous fallback executor+  , streamingToolExecute :: Value -> ConduitT () Text m (Either LangchainError Text)+  -- ^ Monadic streaming execution yielding output chunks and returning final text output+  }++instance Show (StreamingTool m) where+  show st = "StreamingTool { toolName = " ++ show (toolName (streamingToolDef st)) ++ " }"++-- | Construct a 'StreamingTool' from a name, description, schema, and streaming executor.+createStreamingTool ::+  Monad m =>+  Text ->+  Text ->+  Value ->+  (Value -> ConduitT () Text m (Either LangchainError Text)) ->+  StreamingTool m+createStreamingTool name desc schema streamExec =+  StreamingTool+    { streamingToolDef =+        createTool name desc schema $ \args ->+          runConduit (streamExec args `fuseUpstream` drainSink)+    , streamingToolExecute = streamExec+    }+  where+    drainSink = do+      mItem <- await+      case mItem of+        Nothing -> pure ()+        Just _ -> drainSink++-- | Lift a standard 'Tool' into a 'StreamingTool' that emits its full output as a single chunk.+fromTool :: Monad m => Tool m -> StreamingTool m+fromTool t =+  StreamingTool+    { streamingToolDef = t+    , streamingToolExecute = \args -> do+        res <- lift $ toolExecute t args+        case res of+          Right out -> yield out >> pure (Right out)+          Left err -> pure (Left err)+    }++{- | Execute a 'Tool' within a 'StreamEvent' pipeline, emitting 'ToolStart'+followed by 'ToolEnd' or 'ToolErrorEvent'.+-}+streamToolCall ::+  Monad m =>+  Text ->+  Tool m ->+  Value ->+  ConduitT () StreamEvent m (Either LangchainError Text)+streamToolCall rId tool args = do+  let tName = toolName tool+  yield $ ToolStart rId tName args+  eRes <- lift $ toolExecute tool args+  case eRes of+    Left err -> do+      yield $ ToolErrorEvent rId tName err+      pure (Left err)+    Right out -> do+      yield $ ToolEnd rId tName (toJSON out)+      pure (Right out)++{- | Execute a 'StreamingTool' within a 'StreamEvent' pipeline, emitting+'ToolStart', 'ToolChunk' for each streamed text chunk, and 'ToolEnd' or 'ToolErrorEvent'.+-}+streamStreamingToolCall ::+  Monad m =>+  Text ->+  StreamingTool m ->+  Value ->+  ConduitT () StreamEvent m (Either LangchainError Text)+streamStreamingToolCall rId sTool args = do+  let tName = toolName (streamingToolDef sTool)+  yield $ ToolStart rId tName args+  eRes <- mapOutput (ToolChunk rId tName) (streamingToolExecute sTool args)+  case eRes of+    Left err -> do+      yield $ ToolErrorEvent rId tName err+      pure (Left err)+    Right out -> do+      yield $ ToolEnd rId tName (toJSON out)+      pure (Right out)
src/Langchain/Tool/Security.hs view
@@ -38,6 +38,7 @@      -- * Tool Security Combinator   , withSecurity+  , withStreamingSecurity   ) where  import Control.Monad.IO.Class (MonadIO, liftIO)@@ -51,8 +52,9 @@ import System.Directory (canonicalizePath, doesDirectoryExist, doesFileExist) import System.FilePath (isAbsolute, normalise, pathSeparator, takeDirectory, takeFileName, (</>)) +import Control.Monad.Trans (lift) import Langchain.Error (LangchainError, toolError)-import Langchain.Tool (Tool (..))+import Langchain.Tool (StreamingTool (..), Tool (..))  -- | Tier levels representing the risk and side-effects of tool execution. data ToolTier@@ -278,6 +280,26 @@                   Nothing                   Nothing +checkPolicyApproval ::+  MonadIO m =>+  ToolTier ->+  SecurityPolicy ->+  Text ->+  Text ->+  Value ->+  m (Either LangchainError ())+checkPolicyApproval tier policy tName tDesc args =+  case checkTierPermission policy tier of+    Left err -> pure $ Left err+    Right () -> do+      mApproval <- case policyApprovalHook policy of+        Nothing -> pure Approved+        Just hook -> liftIO $ hook (ApprovalRequest tName tier args tDesc)+      case mApproval of+        Denied reason ->+          pure (Left (toolError ("Execution rejected by approval policy: " <> reason) (Just tName) Nothing))+        Approved -> pure $ Right ()+ -- | Wrap a 'Tool m' with permission tier enforcement, sandboxing, and optional HITL approval. withSecurity ::   MonadIO m =>@@ -288,17 +310,27 @@ withSecurity tier policy tool@Tool {..} =   tool     { toolExecute = \args -> do-        case checkTierPermission policy tier of+        eOk <- checkPolicyApproval tier policy toolName toolDescription args+        case eOk of           Left err -> pure $ Left err-          Right () -> do-            -- Check HITL approval if hook is defined-            mApproval <- case policyApprovalHook policy of-              Nothing -> pure Approved-              Just hook -> liftIO $ hook (ApprovalRequest toolName tier args toolDescription)-            case mApproval of-              Denied reason ->-                pure $-                  Left $-                    toolError ("Execution rejected by approval policy: " <> reason) (Just toolName) Nothing-              Approved -> toolExecute args+          Right () -> toolExecute args     }++-- | Wrap a 'StreamingTool m' with permission tier enforcement, sandboxing, and optional HITL approval.+withStreamingSecurity ::+  MonadIO m =>+  ToolTier ->+  SecurityPolicy ->+  StreamingTool m ->+  StreamingTool m+withStreamingSecurity tier policy st@StreamingTool {..} =+  let securedDef = withSecurity tier policy streamingToolDef+   in st+        { streamingToolDef = securedDef+        , streamingToolExecute = \args -> do+            eOk <-+              lift $ checkPolicyApproval tier policy (toolName securedDef) (toolDescription securedDef) args+            case eOk of+              Left err -> pure $ Left err+              Right () -> streamingToolExecute args+        }
src/Langchain/Tool/Shell.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}  {- | Module      : Langchain.Tool.Shell@@ -15,27 +16,96 @@ module Langchain.Tool.Shell   ( shellTool   , secureShellTool+  , streamingShellTool+  , streamingShellToolWithCancel   ) where -import Control.Exception (SomeException, try)+import Control.Concurrent.Async (AsyncCancelled (..), async, cancel, concurrently, wait)+import Control.Concurrent.STM+  ( TBQueue+  , atomically+  , newEmptyTMVarIO+  , newTBQueueIO+  , orElse+  , readTBQueue+  , readTMVar+  , tryPutTMVar+  , writeTBQueue+  )+import Control.Exception (SomeException, catch, finally, fromException, onException, throwIO, try)+import Control.Monad (void) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (Value (..), object, (.=)) import Data.Aeson.Types (parseEither, (.:?))+import Data.Conduit (ConduitT, yield)+import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef) import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.IO as TIO import System.Exit (ExitCode (..))-import System.Process (readProcessWithExitCode)+import System.IO (BufferMode (LineBuffering), Handle, hClose, hIsEOF, hSetBuffering)+import System.Process+  ( CreateProcess (..)+  , StdStream (..)+  , createProcess+  , interruptProcessGroupOf+  , proc+  , readProcessWithExitCode+  , terminateProcess+  , waitForProcess+  ) -import Langchain.Error (toolError)-import Langchain.Tool (Tool (..), createTool)+import Langchain.Error (LangchainError, toolError)+import Langchain.Stream (CancelToken, isCancelled, waitCancel)+import Langchain.Tool (StreamingTool, Tool (..), createStreamingTool, createTool) import Langchain.Tool.Security   ( SecurityPolicy   , ToolTier (..)   , defaultSecurityPolicy   , validateCommandSafety   , withSecurity+  , withStreamingSecurity   ) +shellToolDescription :: Text+shellToolDescription =+  "Execute a shell command line (e.g. bash/sh) and return stdout and stderr output."++shellToolSchema :: Value+shellToolSchema =+  object+    [ "type" .= ("object" :: Text)+    , "properties"+        .= object+          [ "command"+              .= object+                [ "type" .= ("string" :: Text)+                , "description" .= ("The shell command line to execute" :: Text)+                ]+          ]+    , "required" .= (["command"] :: [Text])+    ]++extractShellCommand :: Value -> Either LangchainError Text+extractShellCommand = \case+  Object o -> case parseEither (.:? "command") o of+    Right (Just cmd) -> Right cmd+    _ -> Left $ toolError "Missing 'command' parameter" (Just "shell_command") Nothing+  _ -> Left $ toolError "Invalid arguments object" (Just "shell_command") Nothing++formatShellResult :: ExitCode -> Text -> Text -> Text+formatShellResult ExitSuccess stdoutTxt stderrTxt =+  let out = T.strip stdoutTxt+      err = T.strip stderrTxt+   in if T.null out+        then if T.null err then "Command completed with no output." else err+        else out+formatShellResult (ExitFailure code) stdoutTxt stderrTxt =+  let combined = T.strip (stdoutTxt <> "\n" <> stderrTxt)+   in "Command exited with code "+        <> T.pack (show code)+        <> (if T.null combined then "" else ": " <> combined)+ -- | Tool that executes a shell command with default security policy. shellTool :: MonadIO m => Tool m shellTool = secureShellTool defaultSecurityPolicy@@ -46,42 +116,158 @@   withSecurity TierExecution policy $     createTool       "shell_command"-      "Execute a shell command line (e.g. bash/sh) and return stdout and stderr output."-      ( object-          [ "type" .= ("object" :: Text)-          , "properties"-              .= object-                [ "command"-                    .= object-                      [ "type" .= ("string" :: Text)-                      , "description" .= ("The shell command line to execute" :: Text)-                      ]-                ]-          , "required" .= (["command"] :: [Text])-          ]+      shellToolDescription+      shellToolSchema+      ( \args -> case extractShellCommand args of+          Left err -> pure $ Left err+          Right cmd -> case validateCommandSafety policy cmd of+            Left err -> pure $ Left err+            Right () -> do+              eRes <- liftIO $ try (readProcessWithExitCode "sh" ["-c", T.unpack cmd] "")+              case eRes of+                Left err ->+                  pure $ Left $ toolError (T.pack $ show (err :: SomeException)) (Just "shell_command") Nothing+                Right (code, stdoutStr, stderrStr) ->+                  pure $ Right $ formatShellResult code (T.pack stdoutStr) (T.pack stderrStr)       )-      ( \case-          Object o -> case parseEither (.:? "command") o of-            Right (Just cmd) -> case validateCommandSafety policy cmd of-              Left err -> pure $ Left err-              Right () -> do-                eRes <- liftIO $ try (readProcessWithExitCode "sh" ["-c", T.unpack cmd] "")-                case eRes of-                  Left err ->-                    pure $ Left $ toolError (T.pack $ show (err :: SomeException)) (Just "shell_command") Nothing-                  Right (ExitSuccess, stdoutStr, stderrStr) ->-                    let out = T.strip (T.pack stdoutStr)-                        err = T.strip (T.pack stderrStr)-                     in if T.null out-                          then if T.null err then pure $ Right "Command completed with no output." else pure $ Right err-                          else pure $ Right out-                  Right (ExitFailure code, stdoutStr, stderrStr) ->-                    let combined = T.strip (T.pack (stdoutStr <> "\n" <> stderrStr))-                     in pure $-                          Right $-                            "Command exited with code "-                              <> T.pack (show code)-                              <> (if T.null combined then "" else ": " <> combined)-            _ -> pure $ Left $ toolError "Missing 'command' parameter" (Just "shell_command") Nothing-          _ -> pure $ Left $ toolError "Invalid arguments object" (Just "shell_command") Nothing++-- | Streaming shell tool that yields stdout and stderr lines incrementally while enforcing 'SecurityPolicy'.+streamingShellTool :: MonadIO m => SecurityPolicy -> StreamingTool m+streamingShellTool = makeStreamingShellTool Nothing++{- | Streaming shell tool that monitors a 'CancelToken' and terminates the+spawned shell child process immediately on cancellation.+-}+streamingShellToolWithCancel :: MonadIO m => CancelToken -> SecurityPolicy -> StreamingTool m+streamingShellToolWithCancel tok = makeStreamingShellTool (Just tok)++makeStreamingShellTool :: MonadIO m => Maybe CancelToken -> SecurityPolicy -> StreamingTool m+makeStreamingShellTool mbTok policy =+  withStreamingSecurity TierExecution policy $+    createStreamingTool+      "shell_command"+      shellToolDescription+      shellToolSchema+      ( \args -> case extractShellCommand args of+          Left err -> pure $ Left err+          Right cmd -> case validateCommandSafety policy cmd of+            Left err -> pure $ Left err+            Right () -> streamShellCommand mbTok cmd       )++streamShellCommand ::+  MonadIO m =>+  Maybe CancelToken ->+  Text ->+  ConduitT () Text m (Either LangchainError Text)+streamShellCommand mbTok cmd = do+  cancelledNow <- maybe (pure False) isCancelled mbTok+  if cancelledNow+    then pure $ Left $ toolError "Shell command cancelled" (Just "shell_command") Nothing+    else startShellProcess mbTok cmd++startShellProcess ::+  MonadIO m =>+  Maybe CancelToken ->+  Text ->+  ConduitT () Text m (Either LangchainError Text)+startShellProcess mbTok cmd = do+  eProc <-+    liftIO $+      try $+        createProcess+          (proc "sh" ["-c", T.unpack cmd])+            { std_in = NoStream+            , std_out = CreatePipe+            , std_err = CreatePipe+            , create_group = True+            }+  case eProc of+    Left ex ->+      pure $ Left $ toolError (T.pack $ show (ex :: SomeException)) (Just "shell_command") Nothing+    Right (_, Just hOut, Just hErr, ph) -> do+      (queue, doneVar, worker) <- liftIO $ do+        hSetBuffering hOut LineBuffering+        hSetBuffering hErr LineBuffering+        q <- newTBQueueIO 128+        done <- newEmptyTMVarIO+        outRef <- newIORef ([] :: [Text])+        errRef <- newIORef ([] :: [Text])+        resRef <- newIORef (Right "" :: Either LangchainError Text)+        w <-+          async $+            ( do+                _ <- concurrently (pumpHandle hOut q outRef) (pumpHandle hErr q errRef)+                code <- waitForProcess ph+                outChunks <- T.concat . reverse <$> readIORef outRef+                errChunks <- T.concat . reverse <$> readIORef errRef+                writeIORef resRef (Right $ formatShellResult code outChunks errChunks)+            )+              `catch` ( \ex -> case fromException ex of+                          Just AsyncCancelled -> do+                            void (try @SomeException (interruptProcessGroupOf ph))+                            void (try @SomeException (terminateProcess ph))+                            void (try @SomeException (waitForProcess ph))+                            throwIO (ex :: SomeException)+                          Nothing ->+                            writeIORef+                              resRef+                              (Left $ toolError (T.pack $ show (ex :: SomeException)) (Just "shell_command") Nothing)+                      )+              `finally` ( do+                            void (try @SomeException (hClose hOut))+                            void (try @SomeException (hClose hErr))+                            readIORef resRef >>= atomically . void . tryPutTMVar done+                        )+        pure (q, done, w)+      drainLoop queue doneVar worker+    Right (_, mbOut, mbErr, ph) -> do+      liftIO $ do+        mapM_ (try @SomeException . hClose) mbOut+        mapM_ (try @SomeException . hClose) mbErr+        void (try @SomeException (interruptProcessGroupOf ph))+        void (try @SomeException (terminateProcess ph))+      pure $+        Left $+          toolError "Failed to open stdout/stderr pipes for shell_command" (Just "shell_command") Nothing+  where+    pumpHandle :: Handle -> TBQueue Text -> IORef [Text] -> IO ()+    pumpHandle h q ref = loop+      where+        loop = do+          eof <- hIsEOF h+          if eof+            then pure ()+            else do+              line <- TIO.hGetLine h+              let chunk = line <> "\n"+              modifyIORef' ref (chunk :)+              atomically $ writeTBQueue q chunk+              loop++    drainLoop queue doneVar worker = loop+      where+        loop = do+          next <-+            liftIO $+              atomically+                ( case mbTok of+                    Just tok ->+                      (Left Nothing <$ waitCancel tok)+                        `orElse` (Right <$> readTBQueue queue)+                        `orElse` (Left . Just <$> readTMVar doneVar)+                    Nothing ->+                      (Right <$> readTBQueue queue)+                        `orElse` (Left . Just <$> readTMVar doneVar)+                )+                `onException` cancel worker+          case next of+            Right chunk -> do+              yield chunk+              loop+            Left Nothing -> do+              liftIO $ cancel worker+              pure $ Left $ toolError "Shell command cancelled" (Just "shell_command") Nothing+            Left (Just res) -> do+              void $ liftIO $ wait worker+              pure res
test/Spec.hs view
@@ -3,6 +3,7 @@ import Test.Tasty  import qualified Test.Langchain.Callback as CallbackTest+import qualified Test.Langchain.Eval as EvalTest import qualified Test.Langchain.Model as ModelTest import qualified Test.Langchain.Monad as MonadTest import qualified Test.Langchain.Observability as ObservabilityTest@@ -30,4 +31,5 @@     , CallbackTest.tests     , RetrieverTest.tests     , ResilienceTest.tests+    , EvalTest.tests     ]
+ test/Test/Langchain/Eval.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}++module Test.Langchain.Eval (tests) where++import Control.Monad.Except (runExceptT)+import qualified Data.Aeson as Aeson+import Data.Text (Text)+import Test.Tasty+import Test.Tasty.HUnit++import Langchain.Agent.ReAct (AgentStep (..), AgentTrace (..))+import Langchain.Error (LangchainError)+import Langchain.Eval+import Langchain.Model.Types (Role (..), ToolCall (..), assistantMessage, textMessage)+import Test.Langchain.TestModel (TestChatModel (..))++tests :: TestTree+tests =+  testGroup+    "Langchain.Eval"+    [ testCase "runEvalSuite computes pass rate, mean score, and latency across mixed cases" $ do+        let cases :: [EvalCase Text Text]+            cases =+              [ EvalCase "query-1" (Just "answer-1") Nothing+              , EvalCase "query-2" (Just "answer-2") Nothing+              ]+            mockRunner :: Text -> IO (Either LangchainError Text)+            mockRunner "query-1" = pure $ Right "answer-1"+            mockRunner "query-2" = pure $ Right "wrong-answer"+            mockRunner _ = pure $ Right ""+        report <- runEvalSuite mockRunner [exactMatchScorer] cases+        reportTotalCases report @?= 2+        reportPassedCases report @?= 1+        reportPassRate report @?= 0.5+        reportMeanScore report @?= 0.5+        assertBool "Non-negative latency" (reportMeanLatencyMicros report >= 0)+        length (reportCases report) @?= 2+    , testCase "containsScorer verifies substring presence" $ do+        let cPass :: EvalCase Text Text+            cPass = EvalCase "in" (Just "needle") Nothing+            cFail :: EvalCase Text Text+            cFail = EvalCase "in" (Just "missing") Nothing+        scPass <- scoreCase containsScorer cPass ("haystack needle haystack" :: Text)+        scorePassed scPass @?= True+        scoreValue scPass @?= 1.0+        scFail <- scoreCase containsScorer cFail ("haystack other haystack" :: Text)+        scorePassed scFail @?= False+        scoreValue scFail @?= 0.0+    , testCase "toolCallExactScorer verifies sequence of invoked tool names in AgentTrace" $ do+        let tc1 = ToolCall "c1" "function" "search" (Aeson.object [])+            tc2 = ToolCall "c2" "function" "calculator" (Aeson.object [])+            action1 = AgentAction (assistantMessage "") [tc1]+            action2 = AgentAction (assistantMessage "") [tc2]+            finish = AgentFinish (assistantMessage "Done")+            obs = textMessage Tool "obs"+            trace =+              AgentTrace+                { traceSteps = [(action1, [obs]), (action2, [obs]), (finish, [])]+                , traceFinalAnswer = assistantMessage "Done"+                , traceIterations = 3+                }+            matchingScorer = toolCallExactScorer ["search", "calculator"]+            mismatchScorer = toolCallExactScorer ["calculator", "search"]+            evalCase = EvalCase ("test" :: Text) Nothing Nothing+        resMatch <- scoreCase matchingScorer evalCase trace+        scorePassed resMatch @?= True+        scoreValue resMatch @?= 1.0+        resMismatch <- scoreCase mismatchScorer evalCase trace+        scorePassed resMismatch @?= False+        scoreValue resMismatch @?= 0.0+    , testCase "llmJudgeScorer parses JSON judge evaluation and score" $ do+        let judgeModel =+              TestChatModel+                "{\"passed\": true, \"score\": 0.95, \"reason\": \"Accurate and concise.\"}"+                "judge-mock"+            scorer = llmJudgeScorer judgeModel "Evaluate answer accuracy"+            evalCase :: EvalCase Text Text+            evalCase = EvalCase "What is 2+2?" (Just "4") Nothing+        eScore <- runExceptT $ scoreCase scorer evalCase "4"+        case eScore of+          Left err -> assertFailure ("Judge scorer failed: " ++ show err)+          Right score -> do+            scorePassed score @?= True+            scoreValue score @?= 0.95+            scoreReason score @?= Just "Accurate and concise."+    , testCase "EvalReport round-trips cleanly via Aeson encode and decode" $ do+        let score = EvalScore 1.0 True (Just "Matched perfectly")+            caseRes =+              CaseResult+                { caseInput = Aeson.String "input-1"+                , caseExpected = Just (Aeson.String "expected-1")+                , caseActual = Right (Aeson.String "actual-1")+                , caseScores = [CaseScoreResult "exact_match" score]+                , caseLatencyMicros = 1200+                , casePassed = True+                }+            report =+              EvalReport+                { reportTotalCases = 1+                , reportPassedCases = 1+                , reportPassRate = 1.0+                , reportMeanScore = 1.0+                , reportMeanLatencyMicros = 1200+                , reportCases = [caseRes]+                }+            encoded = Aeson.encode report+            decoded = Aeson.decode encoded+        decoded @?= Just report+    ]
test/Test/Langchain/Stream.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}  module Test.Langchain.Stream (tests) where@@ -5,10 +6,21 @@ import Test.Tasty import Test.Tasty.HUnit -import Control.Monad.Except (runExceptT)+import Control.Concurrent (threadDelay)+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, tryTakeMVar)+import Control.Exception (finally)+import Control.Monad.Except (runExceptT, throwError)+import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Resource (runResourceT)+import Data.Aeson (object, (.=))+import Data.Conduit (runConduit, yield, (.|))+import qualified Data.Conduit.List as CL+import Data.IORef (modifyIORef', newIORef, readIORef)+import Data.Time.Clock (diffUTCTime, getCurrentTime) +import Langchain.Error (llmError) import Langchain.Model+import Langchain.Monad (runLangchainT) import Langchain.Stream import Test.Langchain.TestModel (TestChatModel (..)) @@ -30,4 +42,94 @@                 chunkText c @?= "Streamed content"                 extractMessageText (finalMessage e) @?= "Streamed content"               _ -> assertFailure $ "Unexpected event sequence: " ++ show events+    , testCase "liftChatStream forwards events into LangchainT () IO" $ do+        let model = TestChatModel "Bridged stream" "mock-gpt"+            input = [userMessage "Bridge test"]+        res <- runLangchainT () $ collectEvents $ liftChatStream (stream model input Nothing)+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right events -> do+            length events @?= 3+            case reverse events of+              LLMEnd _ msg _ : _ -> extractMessageText msg @?= "Bridged stream"+              _ -> assertFailure $ "Expected LLMEnd, got: " ++ show events+    , testCase "liftChatStream propagates LangchainError mid-stream" $ do+        let errStream :: ChatStream+            errStream = do+              yield $ LLMStart "r1" "mock" [userMessage "Hi"]+              yield $ LLMChunk "r1" "partial" Nothing+              throwError $ llmError "mid-stream boom" Nothing Nothing+        res <- runExceptT $ collectEvents $ liftChatStream errStream+        case res of+          Left _ -> pure ()+          Right events -> assertFailure $ "Expected failure, got: " ++ show events+    , testCase "runStreamWith invokes callback and resets across LLMStart turns" $ do+        let tc = ToolCall "call_1" "function" "calc" (object ["x" .= (1 :: Int)])+            multiTurnStream :: EventStream IO+            multiTurnStream = do+              yield $ LLMStart "r1" "mock" []+              yield $ LLMChunk "r1" "Calling tool..." Nothing+              yield $ LLMChunk "r1" "" (Just tc)+              yield $ LLMEnd "r1" ((assistantMessage "Calling tool...") {messageToolCalls = Just [tc]}) Nothing+              yield $ ToolStart "r1" "calc" (object ["x" .= (1 :: Int)])+              yield $ ToolEnd "r1" "calc" (object ["res" .= (2 :: Int)])+              yield $ LLMStart "r2" "mock" []+              yield $ LLMChunk "r2" "Final " Nothing+              yield $ LLMChunk "r2" "answer" Nothing+        seenRef <- newIORef ([] :: [StreamEvent])+        finalMsg <- runStreamWith (\ev -> liftIO $ modifyIORef' seenRef (ev :)) multiTurnStream+        seen <- readIORef seenRef+        length seen @?= 9+        extractMessageText finalMsg @?= "Final answer"+        messageToolCalls finalMsg @?= Nothing+    , testCase "liftChatStreamResource cancels upstream within 200ms on take 1" $ do+        cleanedUp <- newEmptyMVar+        let slowUpstream :: ChatStream+            slowUpstream = do+              yield $ LLMStart "slow" "mock" []+              liftIO $+                threadDelay 5000000+                  `finally` putMVar cleanedUp ()+              yield $ LLMChunk "slow" "late" Nothing+        t0 <- getCurrentTime+        res <-+          runResourceT $+            runExceptT $+              runConduit $+                liftChatStreamResource slowUpstream .| CL.take 1+        t1 <- getCurrentTime+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right evs -> length evs @?= 1+        threadDelay 50000+        mbDone <- tryTakeMVar cleanedUp+        mbDone @?= Just ()+        let elapsed = realToFrac (diffUTCTime t1 t0) :: Double+        assertBool ("Expected cancellation < 0.2s, got: " ++ show elapsed) (elapsed < 0.2)+    , testCase "liftChatStreamWithCancel stops on CancelToken and keeps partial text" $ do+        tok <- newCancelToken+        cleanedUp <- newEmptyMVar+        gate <- newEmptyMVar :: IO (MVar ())+        let partialUpstream :: ChatStream+            partialUpstream = do+              yield $ LLMStart "c1" "mock" []+              yield $ LLMChunk "c1" "partial-" Nothing+              liftIO $+                takeMVar gate+                  `finally` putMVar cleanedUp ()+              yield $ LLMChunk "c1" "never" Nothing+        res <-+          runLangchainT () $+            runStreamWith+              ( \case+                  LLMChunk {} -> triggerCancel tok+                  _ -> pure ()+              )+              (liftChatStreamWithCancel tok partialUpstream)+        case res of+          Left err -> assertFailure $ "Unexpected error: " ++ show err+          Right msg -> extractMessageText msg @?= "partial-"+        threadDelay 50000+        mbDone <- tryTakeMVar cleanedUp+        mbDone @?= Just ()     ]
test/Test/Langchain/Tool.hs view
@@ -3,11 +3,25 @@  module Test.Langchain.Tool (tests) where +import Control.Concurrent (threadDelay)+import Control.Concurrent.Async (async, cancel, wait) import Data.Aeson import Data.Aeson.Types (parseEither)+import Data.Conduit (fuseUpstream, runConduit, yield)+import qualified Data.Conduit.List as CL+import Data.IORef (modifyIORef', newIORef, readIORef) import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Clock (diffUTCTime, getCurrentTime) import Langchain.Error+import Langchain.Stream+  ( StreamEvent (ToolChunk, ToolEnd, ToolErrorEvent, ToolStart)+  , newCancelToken+  , triggerCancel+  ) import Langchain.Tool+import Langchain.Tool.Security (readOnlySecurityPolicy, unrestrictedSecurityPolicy)+import Langchain.Tool.Shell (streamingShellTool, streamingShellToolWithCancel) import Test.Tasty import Test.Tasty.HUnit @@ -36,4 +50,104 @@         let t = createTool "calculator" "Performs math" (object []) calcExec         res <- toolExecute t (object [])         res @?= Right "42"+    , testCase "streamToolCall emits ToolStart and ToolEnd on success" $ do+        let t = createTool "calculator" "Performs math" (object []) calcExec+        eventsRef <- newIORef ([] :: [StreamEvent])+        res <-+          runConduit $+            streamToolCall "run-1" t (object [])+              `fuseUpstream` CL.mapM_ (\ev -> modifyIORef' eventsRef (++ [ev]))+        res @?= Right "42"+        events <- readIORef eventsRef+        events+          @?= [ ToolStart "run-1" "calculator" (object [])+              , ToolEnd "run-1" "calculator" (toJSON ("42" :: Text))+              ]+    , testCase "streamToolCall emits ToolStart and ToolErrorEvent on failure" $ do+        let t = createTool "calculator" "Performs math" (object []) calcExec+        eventsRef <- newIORef ([] :: [StreamEvent])+        res <-+          runConduit $+            streamToolCall "run-2" t (Number 1)+              `fuseUpstream` CL.mapM_ (\ev -> modifyIORef' eventsRef (++ [ev]))+        case res of+          Left _ -> pure ()+          Right out -> assertFailure $ "Expected error, got: " ++ show out+        events <- readIORef eventsRef+        case events of+          [ToolStart "run-2" "calculator" (Number 1), ToolErrorEvent "run-2" "calculator" _] -> pure ()+          _ -> assertFailure $ "Unexpected events: " ++ show events+    , testCase "streamStreamingToolCall emits incremental ToolChunk events" $ do+        let sTool =+              createStreamingTool "streamer" "Streams chunks" (object []) $ \_ -> do+                yield "chunk-1 "+                yield "chunk-2"+                pure (Right "chunk-1 chunk-2")+        eventsRef <- newIORef ([] :: [StreamEvent])+        res <-+          runConduit $+            streamStreamingToolCall "run-3" sTool (object [])+              `fuseUpstream` CL.mapM_ (\ev -> modifyIORef' eventsRef (++ [ev]))+        res @?= Right "chunk-1 chunk-2"+        events <- readIORef eventsRef+        events+          @?= [ ToolStart "run-3" "streamer" (object [])+              , ToolChunk "run-3" "streamer" "chunk-1 "+              , ToolChunk "run-3" "streamer" "chunk-2"+              , ToolEnd "run-3" "streamer" (toJSON ("chunk-1 chunk-2" :: Text))+              ]+    , testCase "streamingShellTool streams stdout lines and enforces SecurityPolicy" $ do+        let sShell = streamingShellTool unrestrictedSecurityPolicy+        eventsRef <- newIORef ([] :: [StreamEvent])+        res <-+          runConduit $+            streamStreamingToolCall+              "run-sh"+              sShell+              (object ["command" .= ("printf 'line1\\nline2\\n'" :: Text)])+              `fuseUpstream` CL.mapM_ (\ev -> modifyIORef' eventsRef (++ [ev]))+        res @?= Right "line1\nline2"+        events <- readIORef eventsRef+        let chunks = [txt | ToolChunk _ _ txt <- events]+        chunks @?= ["line1\n", "line2\n"]++        let roShell = streamingShellTool readOnlySecurityPolicy+        roRes <- toolExecute (streamingToolDef roShell) (object ["command" .= ("echo hi" :: Text)])+        case roRes of+          Left err -> assertBool "Mentions permission" ("permission" `T.isInfixOf` T.toLower (errorMessage err))+          Right ok -> assertFailure $ "Expected security denial, got: " ++ show ok+    , testCase "cancelling streamingShellTool via Async.cancel terminates sleep child promptly" $ do+        let sShell = streamingShellTool unrestrictedSecurityPolicy+        t0 <- getCurrentTime+        a <-+          async $+            runConduit $+              streamStreamingToolCall+                "run-cancel-async"+                sShell+                (object ["command" .= ("sleep 10" :: Text)])+                `fuseUpstream` CL.sinkNull+        threadDelay 100000+        cancel a+        t1 <- getCurrentTime+        let elapsed = realToFrac (diffUTCTime t1 t0) :: Double+        assertBool ("Expected prompt Async cancellation (< 2s), got: " ++ show elapsed) (elapsed < 2.0)+    , testCase "cancelling streamingShellToolWithCancel via CancelToken terminates promptly" $ do+        tok <- newCancelToken+        let sShell = streamingShellToolWithCancel tok unrestrictedSecurityPolicy+        t0 <- getCurrentTime+        a <-+          async $+            runConduit $+              streamStreamingToolCall+                "run-cancel-tok"+                sShell+                (object ["command" .= ("sleep 10" :: Text)])+                `fuseUpstream` CL.sinkNull+        threadDelay 100000+        triggerCancel tok+        _ <- wait a+        t1 <- getCurrentTime+        let elapsed = realToFrac (diffUTCTime t1 t0) :: Double+        assertBool ("Expected prompt CancelToken termination (< 2s), got: " ++ show elapsed) (elapsed < 2.0)     ]