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
+ }