Create Sequential Interpreter

t-369.3·WorkTask·
·
·
·Omni/Agent.hs
Parent:t-369·Created1 month ago·Updated1 month ago

Dependencies

Description

Edit

Create Omni/Agent/Interpreter/Sequential.hs - the basic interpreter for Op.

Context

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.

Deliverables

Create Omni/Agent/Interpreter/Sequential.hs containing:

1. Interpreter Configuration

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

2. Main Interpreter Function

runSequential :: SeqConfig -> s -> Op s a -> IO (Either Text (a, Trace, s))
runSequential config initialState program = ...

3. Interpretation Logic

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))

4. Budget Tracking

runWithBudget :: SeqConfig -> s -> Trace -> Budget -> Op s a 
              -> IO (Either Exhausted (a, Trace, s))

Track cost/tokens as we go, abort if exceeded.

5. Helper to Run Agent-Like Programs

-- 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)

Notes

  • Use Provider.hs for actual LLM calls
  • Use Tool type from Engine.hs or Tools.hs
  • Par/Race should work but just run sequentially (parallel interpreter will optimize)
  • Budget tracking should match current guardrails behavior
  • Event callback allows streaming to UI during execution

Testing

  • Pure operations (Get/Put/Modify) work correctly
  • Infer calls Provider.chat
  • Tool calls execute tools
  • Budget exhaustion aborts correctly
  • Timeout aborts correctly
  • Trace captures all events
  • Matches Engine.runAgentWithProvider behavior for simple cases

Files to Read First

  • Omni/Agent/ARCHITECTURE.md (design)
  • Omni/Agent/Op.hs (the Op type to interpret)
  • Omni/Agent/Trace.hs (Event/Trace types)
  • Omni/Agent/Engine.hs (current implementation to match)
  • Omni/Agent/Provider.hs (for LLM calls)

Timeline (2)

🔄[human]Open → InProgress1 month ago
🔄[human]InProgress → Done1 month ago