Create Omni/Agent/Interpreter/Sequential.hs - the basic interpreter for Op.
The free monad Op is just data. To actually run it, we need an interpreter. The sequential interpreter is the simplest - it runs operations one at a time, no parallelism.
This interpreter should produce identical behavior to the current Engine.runAgentWithProvider.
Read Omni/Agent/ARCHITECTURE.md for full design rationale.
Create Omni/Agent/Interpreter/Sequential.hs containing:
data SeqConfig = SeqConfig
{ seqProvider :: Provider -- from Omni/Agent/Provider.hs
, seqTools :: Map Text Tool -- available tools
, seqOnEvent :: Event -> IO () -- optional callback for streaming events
}
defaultSeqConfig :: Provider -> [Tool] -> SeqConfig
runSequential :: SeqConfig -> s -> Op s a -> IO (Either Text (a, Trace, s))
runSequential config initialState program = ...
For each OpF constructor:
interpret :: SeqConfig -> s -> Trace -> Free (OpF s) a -> IO (Either Text (a, Trace, s))
interpret config state trace = \case
Pure a ->
pure (Right (a, trace, state))
Free (Infer model prompt k) -> do
-- Emit start event
t0 <- getCurrentTime
let startEvent = EventInferStart (unModel model) (renderPrompt prompt) t0 0
onEvent config startEvent
let trace' = appendEvent startEvent trace
-- Call provider
result <- Provider.chat (seqProvider config) (seqTools config) (promptToMessages prompt)
case result of
Left err -> pure (Left err)
Right response -> do
-- Emit end event
t1 <- getCurrentTime
let endEvent = EventInferEnd ...
onEvent config endEvent
let trace'' = appendEvent endEvent trace'
-- Continue with response
interpret config state trace'' (k (toResponse response))
Free (Par ops k) -> do
-- Sequential fallback: just run them in order
-- (Parallel.hs will do actual parallelism)
results <- traverse (\op -> runSequential config state op) ops
case sequence results of
Left err -> pure (Left err)
Right rs -> do
let (as, traces, states) = unzip3 rs
-- Merge traces
let mergedTrace = trace <> mconcat traces
-- For sequential, just use last state (CRDT would merge)
let finalState = last (state : states)
interpret config finalState mergedTrace (k as)
Free (Race ops k) -> do
-- Sequential fallback: run first one only
-- (Parallel.hs will race properly)
case ops of
[] -> pure (Left "Race with no branches")
(op:_) -> do
result <- runSequential config state op
case result of
Left err -> pure (Left err)
Right (a, opTrace, newState) -> do
let trace' = trace <> opTrace
interpret config newState trace' (k a)
Free (Get k) -> do
interpret config state trace (k state)
Free (Put newState k) -> do
interpret config newState trace k
Free (Modify f k) -> do
interpret config (f state) trace k
Free (Tool name args k) -> do
-- Emit tool call event
t0 <- getCurrentTime
let callEvent = EventToolCall name args t0 0
onEvent config callEvent
let trace' = appendEvent callEvent trace
case Map.lookup name (seqTools config) of
Nothing -> pure (Left ("Tool not found: " <> name))
Just tool -> do
result <- toolExecute tool args
t1 <- getCurrentTime
let resultEvent = EventToolResult name True (renderValue result) (diffMs t1 t0) t1 0
onEvent config resultEvent
let trace'' = appendEvent resultEvent trace'
interpret config state trace'' (k result)
Free (Emit event k) -> do
onEvent config event
let trace' = appendEvent event trace
interpret config state trace' k
Free (GetTrace k) -> do
interpret config state trace (k trace)
Free (Checkpoint name k) -> do
let event = EventCheckpoint name <current-time>
onEvent config event
let trace' = appendEvent event trace
interpret config state trace' k
Free (Check k) -> do
-- No external steering in basic interpreter
interpret config state trace (k Nothing)
Free (Limit budget op k) -> do
-- Run with budget tracking
result <- runWithBudget config state trace budget op
case result of
Left exhausted -> interpret config state trace (k (Left exhausted))
Right (a, opTrace, newState) ->
interpret config newState (trace <> opTrace) (k (Right a))
Free (Timeout seconds op k) -> do
-- Run with timeout
result <- System.Timeout.timeout (unSeconds seconds * 1000000)
(runSequential config state op)
case result of
Nothing -> interpret config state trace (k (Left TimedOut))
Just (Left err) -> pure (Left err)
Just (Right (a, opTrace, newState)) ->
interpret config newState (trace <> opTrace) (k (Right a))
runWithBudget :: SeqConfig -> s -> Trace -> Budget -> Op s a
-> IO (Either Exhausted (a, Trace, s))
Track cost/tokens as we go, abort if exceeded.
-- Convenience wrapper matching current Engine API
runAgent :: Provider -> [Tool] -> Text -> Op AgentState AgentResult
-> IO (Either Text AgentResult)
runAgent provider tools systemPrompt program = do
let config = defaultSeqConfig provider tools
initialState = AgentState { ... }
result <- runSequential config initialState program
case result of
Left err -> pure (Left err)
Right (a, _, _) -> pure (Right a)