langchain-hs-0.0.3.0: src/Langchain/Agent/Executor.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{- |
Module : Langchain.Agent.Executor
Description : Agent execution loop and orchestration
Copyright : (c) 2025 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
This module provides the execution engine for agents. It orchestrates the
agent planning loop, tool execution, and result collection.
The executor handles:
- The main agent loop (plan -> execute -> observe)
- Error handling and recovery
- Iteration limits and timeouts
- Callbacks and logging
- State management
-}
module Langchain.Agent.Executor
( -- * Main Execution Functions
runAgentExecutor
-- * Result Types
, AgentExecutionResult (..)
, ExecutionMetrics (..)
-- * Utilities
, createInitialState
)
where
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans.Except
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time (UTCTime, diffUTCTime, getCurrentTime)
import Langchain.Agent.Core
import Langchain.Agent.Middleware
import Langchain.Error
( LangchainResult
, agentError
)
import Langchain.LLM.Core
import Langchain.Memory.Core
data AgentExecutionResult = AgentExecutionResult
{ executionFinish :: AgentFinish
-- ^ The final result of the agent execution
, executionSteps :: [AgentStep]
-- ^ All tool calls made and their results
, executionMetrics :: ExecutionMetrics
-- ^ Performance metrics
}
deriving (Show, Eq)
data ExecutionMetrics = ExecutionMetrics
{ metricsIterations :: Int
-- ^ Number of agent iterations
, metricsExecutionTime :: Double
-- ^ Total time in seconds
, metricsToolCalls :: Int
-- ^ Number of tool calls made
, metricsSuccess :: Bool
-- ^ Whether execution completed successfully
}
deriving (Show, Eq)
-- | Create the initial state of the agent with default memory.
createInitialState :: Maybe SomeMemory -> Text -> AgentState
createInitialState mbSomeMemory input =
AgentState
{ agentMemory = fromMaybe (SomeMemory defaultMemory) mbSomeMemory
, agentInput = input
, agentIterations = 0
}
where
defaultMemory =
WindowBufferMemory
{ maxWindowSize = 100
, windowBufferMessages = initialChatMessage "You are a helpful AI assistant."
}
{-
Returns False if:
- Max iterations reached
- Max execution time exceeded
-}
shouldContinue :: AgentConfig -> AgentState -> Double -> Bool
shouldContinue AgentConfig {..} state elapsedSeconds =
iterationsOk && timeOk
where
iterationsOk = agentIterations state < maxIterations
timeOk = case maxExecutionTime of
Nothing -> True
Just maxTime -> elapsedSeconds < fromIntegral maxTime
-- | Helper function to add an action to the state's memory
addActionToState :: AgentState -> AgentAction -> IO (LangchainResult AgentState)
addActionToState state action =
case agentMemory state of
SomeMemory mem -> do
eMemWithAction <- addMessage mem (actionToMsg action)
case eMemWithAction of
Left err -> pure $ Left err
Right memWithAction -> pure $ Right $ state {agentMemory = SomeMemory memWithAction}
where
actionToMsg act =
defaultMessage
{ role = Assistant
, content = actionLog act
, messageData =
defaultMessageData
{ toolCalls = Just (actionToolCall act)
}
}
-- | Helper function to add observations to the state's memory
addObservationsToState :: AgentState -> [Text] -> IO (LangchainResult AgentState)
addObservationsToState state observations =
case agentMemory state of
SomeMemory mem -> do
eMemsWithObs <- sequenceA <$> traverse (addMessage mem . toolResultToMsg) observations
case eMemsWithObs of
Left err -> pure $ Left err
Right mems -> pure $ Right $ state {agentMemory = SomeMemory (last mems)}
where
toolResultToMsg res =
defaultMessage
{ role = Tool
, content = res
}
executeAgentLoop ::
Agent a =>
a ->
AgentConfig ->
AgentCallbacks ->
[AgentMiddleware a] ->
AgentState ->
UTCTime ->
IO (LangchainResult AgentExecutionResult)
executeAgentLoop agent config callbacks middlewares initialState startTime =
loop agent initialState []
where
loop agent0 state0 steps = runExceptT $ do
currentTime <- liftIO getCurrentTime
let elapsedSeconds = realToFrac $ diffUTCTime currentTime startTime
-- Check termination conditions
if not (shouldContinue config state0 elapsedSeconds)
then do
let err = agentError "Agent execution exceeded limits" Nothing Nothing
ExceptT . pure $ Left err
else do
-- Plan next action
when (verboseLogging config) $
liftIO $
putStrLn $
"[Agent] Planning iteration " <> show (agentIterations state0)
(state1, agent1) <-
ExceptT $
applyMiddlewares beforeModelCall middlewares (state0, agent0)
plan_ <- ExceptT $ plan agent1 state1
(state2, agent2) <-
ExceptT $
applyMiddlewares afterModelCall middlewares (state1, agent1)
case plan_ of
(Done finish) -> do
-- Agent has finished
let metrics =
ExecutionMetrics
{ metricsIterations = agentIterations state2
, metricsExecutionTime = elapsedSeconds
, metricsToolCalls = length steps
, metricsSuccess = True
}
return $ AgentExecutionResult finish steps metrics
(Continue action) -> do
-- add toolCalls in state memory
state3 <- ExceptT $ addActionToState state2 action
-- Execute action
liftIO $ onAgentAction callbacks action
(state4, agent4) <-
ExceptT $
applyMiddlewares beforeToolCall middlewares (state3, agent2)
when (verboseLogging config) $
liftIO $
putStrLn $
"[Agent] Executing: " <> show (actionToolCall action)
observations <-
ExceptT $
sequenceA <$> traverse (executeTool agent4) (actionToolCall action)
mapM_ (liftIO . onAgentObservation callbacks) observations
when (verboseLogging config) $
liftIO $
putStrLn $
"[Agent] Observation: " <> mconcat (T.unpack <$> observations)
-- Record step
timestamp <- liftIO getCurrentTime
let newSteps = map (\obs -> AgentStep action obs timestamp) observations
mapM_ (liftIO . onAgentStep callbacks) newSteps
-- Update state memory with tool results and continue
state5 <-
ExceptT $
addObservationsToState state4 observations
(state6, agent6) <-
ExceptT $
applyMiddlewares afterToolCall middlewares (state5, agent4)
let newState =
state6
{ agentIterations = agentIterations state6 + 1
}
ExceptT (loop agent6 newState (steps ++ newSteps))
{- |
Runs the agent executor.
This function initializes the agent, runs the agent loop, and returns the final result.
Arguments:
- agent: The agent to run
- config: The agent configuration
- callbacks: The agent callbacks
- input: The input to the agent
Returns:
- The final result of the agent execution
- The execution metrics
- The execution steps
-}
runAgentExecutor ::
Agent a =>
a ->
AgentConfig ->
AgentCallbacks ->
[AgentMiddleware a] ->
Text ->
IO (LangchainResult AgentExecutionResult)
runAgentExecutor agent0 config callbacks middlewares input = do
startTime <- getCurrentTime
onAgentStart callbacks input
runExceptT $ do
let initialState = createInitialState (stateMemory config) input
state0 <- ExceptT $ initialize agent0 initialState
(state1, agent1) <-
ExceptT $
applyMiddlewares beforeAgent middlewares (state0, agent0)
result <-
ExceptT $
executeAgentLoop agent1 config callbacks middlewares state1 startTime
(state2, agent2) <-
ExceptT $
applyMiddlewares afterAgent middlewares (state1, agent1)
liftIO $ finalize agent2 state2
liftIO $ onAgentFinish callbacks (executionFinish result)
return result