← Back to task

Commit 156bdbae

commit 156bdbae1575846f538cf4dfa79384ebb5c24c2f
Author: Ben Sima <ben@bensima.com>
Date:   Thu Jan 1 11:36:29 2026

    Create Harness abstraction for structured agent workflows (t-315)
    
    Added Omni/Agent/Harness.hs with:
    - HarnessConfig: Configuration for harness lifecycle (init/work/verify/commit)
    - HarnessResult: Result type with success, summary, phase, metrics
    - VerifyResult: Success or failed with error message
    - HarnessPhase: Init/Work/Verify/Commit phase tracking
    - runHarness: Placeholder implementation showing the lifecycle
    - simpleHarness: Helper to create basic harness configs
    
    The harness pattern provides a reusable abstraction for agent workflows:
    1. Init: Setup environment, detect state
    2. Work: Agent loop with tools
    3. Verify: Programmatic validation
    4. Commit: Finalize (or rollback on failure)
    
    This is the first step toward generalizing the Coder subagent pattern.
    Future work: Refactor Coder.hs to use this abstraction.
    
    Task-Id: t-315

diff --git a/Omni/Agent/Harness.hs b/Omni/Agent/Harness.hs
new file mode 100644
index 00000000..66ea8895
--- /dev/null
+++ b/Omni/Agent/Harness.hs
@@ -0,0 +1,226 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Harness - A reusable abstraction for structured agent workflows.
+--
+-- A harness provides a programmatic lifecycle for agent work:
+--
+-- @
+-- ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
+-- │  Init   │ ──► │  Work   │ ──► │ Verify  │ ──► │ Commit  │
+-- └─────────┘     └─────────┘     └─────────┘     └─────────┘
+--      │               │               │               │
+--      ▼               ▼               ▼               ▼
+--   Check env      Agent loop     Programmatic     Finalize
+--   Detect state   with tools     validation       or rollback
+-- @
+--
+-- The Coder subagent is the canonical example: it initializes the environment,
+-- runs an agent loop with coding tools, verifies via build/test, and commits.
+--
+-- This pattern can apply to other domains:
+-- - Database migrations: init (backup) → work (write migration) → verify (dry run) → commit (apply)
+-- - Infrastructure: init (plan) → work (generate) → verify (validate) → commit (apply)
+-- - Documents: init (gather context) → work (draft) → verify (constraints) → commit (publish)
+--
+-- : out omni-agent-harness
+-- : dep aeson
+module Omni.Agent.Harness
+  ( -- * Core Types
+    HarnessConfig (..),
+    HarnessResult (..),
+    VerifyResult (..),
+    HarnessPhase (..),
+
+    -- * Running
+    runHarness,
+
+    -- * Helpers
+    simpleHarness,
+
+    -- * Testing
+    main,
+    test,
+  )
+where
+
+import Alpha
+import qualified Data.Aeson as Aeson
+import qualified Data.Text as Text
+import qualified Omni.Agent.Engine as Engine
+import qualified Omni.Test as Test
+
+main :: IO ()
+main = Test.run test
+
+test :: Test.Tree
+test =
+  Test.group
+    "Omni.Agent.Harness"
+    [ Test.unit "VerifyResult equality" <| do
+        VerifySuccess Test.@=? VerifySuccess
+        VerifyFailed "err" Test.@=? VerifyFailed "err",
+      Test.unit "HarnessPhase equality" <| do
+        PhaseInit Test.@=? PhaseInit
+        PhaseWork Test.@=? PhaseWork
+        PhaseVerify Test.@=? PhaseVerify
+        PhaseCommit Test.@=? PhaseCommit,
+      Test.unit "simpleHarness creates config" <| do
+        let h = simpleHarness "test" []
+        harnessName h Test.@=? "test"
+        harnessMaxVerifyRetries h Test.@=? 3
+    ]
+
+-- | Current phase of harness execution
+data HarnessPhase
+  = PhaseInit
+  | PhaseWork
+  | PhaseVerify
+  | PhaseCommit
+  deriving (Show, Eq, Generic)
+
+instance Aeson.ToJSON HarnessPhase
+
+-- | Result of the verify phase
+data VerifyResult
+  = VerifySuccess
+  | VerifyFailed Text  -- ^ Error message for agent to fix
+  deriving (Show, Eq, Generic)
+
+instance Aeson.ToJSON VerifyResult
+
+-- | Result of running a harness
+data HarnessResult = HarnessResult
+  { harnessResultSuccess :: Bool,
+    harnessResultSummary :: Text,
+    harnessResultPhase :: HarnessPhase,  -- ^ Phase where execution ended
+    harnessResultIterations :: Int,
+    harnessResultTokensUsed :: Int,
+    harnessResultCostCents :: Double
+  }
+  deriving (Show, Eq, Generic)
+
+instance Aeson.ToJSON HarnessResult
+
+-- | Configuration for a harness.
+--
+-- The type parameter @ctx@ is the context type produced by init and
+-- consumed by verify/commit/rollback. For example, Coder uses a context
+-- that includes detected project info, work directory, etc.
+data HarnessConfig ctx = HarnessConfig
+  { -- | Human-readable name for the harness
+    harnessName :: Text,
+
+    -- | Initialize the harness, detecting state and preparing the environment.
+    -- Returns Left with error if init fails, Right with context otherwise.
+    harnessInit :: IO (Either Text ctx),
+
+    -- | Verify the work done so far. Called after each work phase.
+    -- Returns VerifySuccess or VerifyFailed with details.
+    harnessVerify :: ctx -> IO VerifyResult,
+
+    -- | Commit the work. Called after verification succeeds.
+    -- The Text parameter is a summary of what was done.
+    harnessCommit :: ctx -> Text -> IO (Either Text ()),
+
+    -- | Rollback/cleanup on failure. Called if work or verify fails repeatedly.
+    harnessRollback :: ctx -> IO (),
+
+    -- | Tools available during the work phase.
+    harnessTools :: [Engine.Tool],
+
+    -- | Generate the system prompt for the work phase.
+    -- Receives the context from init to include detected state.
+    harnessSystemPrompt :: ctx -> Text,
+
+    -- | Maximum number of verify retries before giving up.
+    harnessMaxVerifyRetries :: Int,
+
+    -- | Maximum tokens allowed per work iteration.
+    harnessMaxTokens :: Int,
+
+    -- | Callback for phase transitions (for logging/status updates).
+    harnessOnPhase :: HarnessPhase -> IO ()
+  }
+
+-- | Create a simple harness with default settings.
+--
+-- This is a helper for creating basic harnesses without specifying
+-- every field. You'll typically want to customize the resulting config.
+simpleHarness :: Text -> [Engine.Tool] -> HarnessConfig ()
+simpleHarness name tools =
+  HarnessConfig
+    { harnessName = name,
+      harnessInit = pure (Right ()),
+      harnessVerify = \_ -> pure VerifySuccess,
+      harnessCommit = \_ _ -> pure (Right ()),
+      harnessRollback = \_ -> pure (),
+      harnessTools = tools,
+      harnessSystemPrompt = \_ -> "",
+      harnessMaxVerifyRetries = 3,
+      harnessMaxTokens = 100000,
+      harnessOnPhase = \_ -> pure ()
+    }
+
+-- | Run a harness with the init → work → verify → commit lifecycle.
+--
+-- This is a placeholder implementation. The full implementation would:
+-- 1. Run init phase to get context
+-- 2. Run agent loop with tools and system prompt
+-- 3. After agent signals done, run verify
+-- 4. If verify fails, give error to agent and retry (up to maxRetries)
+-- 5. If verify succeeds, run commit
+-- 6. On any failure, run rollback
+--
+-- The actual Coder implementation in Subagent/Coder.hs has all this logic;
+-- this module provides the abstraction layer.
+runHarness ::
+  Text ->  -- ^ OpenRouter API key for agent
+  HarnessConfig ctx ->
+  Text ->  -- ^ Task description
+  IO (Either Text HarnessResult)
+runHarness _apiKey cfg task = do
+  harnessOnPhase cfg PhaseInit
+  
+  -- Run init
+  initResult <- harnessInit cfg
+  case initResult of
+    Left err ->
+      pure <| Left ("Init failed: " <> err)
+    
+    Right ctx -> do
+      harnessOnPhase cfg PhaseWork
+      
+      -- Placeholder: In the real implementation, this would run the agent loop
+      -- with the tools and system prompt, then verify/retry/commit.
+      -- For now, we just show the structure.
+      
+      let summary = "Harness " <> harnessName cfg <> " completed task: " <> Text.take 50 task
+      
+      harnessOnPhase cfg PhaseVerify
+      verifyResult <- harnessVerify cfg ctx
+      
+      case verifyResult of
+        VerifyFailed err -> do
+          harnessRollback cfg ctx
+          pure <| Left ("Verification failed: " <> err)
+        
+        VerifySuccess -> do
+          harnessOnPhase cfg PhaseCommit
+          commitResult <- harnessCommit cfg ctx summary
+          case commitResult of
+            Left err -> do
+              harnessRollback cfg ctx
+              pure <| Left ("Commit failed: " <> err)
+            Right () ->
+              pure <| Right HarnessResult
+                { harnessResultSuccess = True,
+                  harnessResultSummary = summary,
+                  harnessResultPhase = PhaseCommit,
+                  harnessResultIterations = 1,
+                  harnessResultTokensUsed = 0,
+                  harnessResultCostCents = 0
+                }