diff options
| author | Ben Sima <ben@bensima.com> | 2025-11-30 00:36:51 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-30 00:36:51 -0500 |
| commit | 5fbcd92ff85bc9cc0b752888f6d3498aafea0b2a (patch) | |
| tree | 4bcae7c97bf81ecae696744cfcd84520e5db639d | |
| parent | d05ca4732710dd9cef7fffd998a03615ad2cb58c (diff) | |
Remove amp dependency entirely
The build and tests pass. Let me provide a summary of the changes made:
Removed the amp dependency entirely from the codebase:
- Removed `runAmp` function (was running amp subprocess) - Removed
`shouldUseEngine` function (env var check `JR_USE_ENGINE`) - Removed
`monitorLog` and `waitForFile` helpers (for amp.log parsing) - Removed
unused imports: `System.IO`, `Data.Text.IO` - Made `runWithEngine`
the default/only path - Updated error messages from "amp" to "engine" -
Renamed `ampOutput` parameter to `agentOutput` in `formatCommitMessage
- Added `Data.IORef` import for `newIORef`, `modifyIORef'`, `readIORef`
- Removed amp.log parsing code: `LogEntry`, `processLogLine`,
`updateFro - Removed unused imports: `Data.Aeson`,
`Data.ByteString.Lazy`, `Data.Te
- Renamed `activityAmpThreadUrl` to `activityThreadUrl`
- Updated field references from `activityAmpThreadUrl` to
`activityThrea - Updated UI label from "Amp Thread:" to "Session:"
- Updated comment from "amp completes" to "engine completes"
- Updated `Amp.execute` to `Engine.runAgent` - Updated logging section
to describe Engine callbacks instead of amp.lo - Updated integration
test guidance to mock Engine instead of amp binary
Task-Id: t-141.6
| -rw-r--r-- | Omni/Agent/DESIGN.md | 8 | ||||
| -rw-r--r-- | Omni/Agent/Log.hs | 42 | ||||
| -rw-r--r-- | Omni/Agent/Worker.hs | 180 | ||||
| -rwxr-xr-x | Omni/Jr.hs | 2 | ||||
| -rw-r--r-- | Omni/Jr/Web.hs | 8 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 4 |
6 files changed, 21 insertions, 223 deletions
diff --git a/Omni/Agent/DESIGN.md b/Omni/Agent/DESIGN.md index 0ee1004..ae1f6b3 100644 --- a/Omni/Agent/DESIGN.md +++ b/Omni/Agent/DESIGN.md @@ -58,7 +58,7 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w - `Task.claim task` - `baseBranch <- Git.determineBaseBranch task` (Check dependencies) - `Git.checkoutTaskBranch task baseBranch` (Force checkout to clean untracked files) - - `Amp.execute prompt` + - `Engine.runAgent prompt` (Native LLM agent via OpenRouter) - `Git.commit` - `Git.checkoutBase` - `Task.submitReview task` @@ -70,8 +70,8 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w - `agent status` checks if PID is alive. ### 4.3 Logging -- Continue writing raw Amp logs to `_/llm/amp.log` in the worker directory. -- `agent log` reads this file and applies the filtering logic (currently in `monitor.sh` jq script) using Haskell (Aeson). +- The Engine module uses callbacks to report activity and costs in real-time. +- `agent log` displays the status bar with worker progress information. - **UI Design**: - **Two-line Status**: The CLI should maintain two reserved lines at the bottom (or top) of the output for each worker: - **Line 1 (Meta)**: `[Worker: omni-worker-1] Task: t-123 | Files: 3 | Credits: $0.45 | Time: 05:23` @@ -109,7 +109,7 @@ The Haskell implementation should replicate the logic of `start-worker.sh` but w ### 6.2 Integration Tests - Create a temporary test repo. - Spawn a worker. -- Mock `amp` binary (simple script that `echo "done"`). +- Mock the Engine LLM calls or use a test API key. - Verify task moves from Open -> InProgress -> Review. ## 7. References diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index 55bc1e2..46ea009 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -6,12 +6,8 @@ module Omni.Agent.Log where import Alpha -import Data.Aeson ((.:), (.:?)) -import qualified Data.Aeson as Aeson -import qualified Data.ByteString.Lazy as BL import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef) import qualified Data.Text as Text -import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Data.Time.Format (defaultTimeLocale, parseTimeOrError) @@ -146,44 +142,6 @@ render = do ANSI.hCursorUp IO.stderr 4 IO.hFlush IO.stderr --- | Log Entry from JSON -data LogEntry = LogEntry - { leMessage :: Text, - leThreadId :: Maybe Text, - leCredits :: Maybe Double, - leTotalCredits :: Maybe Double, - leTimestamp :: Maybe Text - } - deriving (Show, Eq) - -instance Aeson.FromJSON LogEntry where - parseJSON = - Aeson.withObject "LogEntry" <| \v -> - (LogEntry </ (v .: "message")) - <*> v - .:? "threadId" - <*> v - .:? "credits" - <*> v - .:? "totalCredits" - <*> v - .:? "timestamp" - --- | Parse a log line and update status -processLogLine :: Text -> IO () -processLogLine line = do - let bs = BL.fromStrict <| TE.encodeUtf8 line - case Aeson.decode bs of - Just entry -> update (updateFromEntry entry) - Nothing -> pure () -- Ignore invalid JSON - -updateFromEntry :: LogEntry -> Status -> Status -updateFromEntry LogEntry {..} s = - s - { statusThread = leThreadId <|> statusThread s, - statusCredits = maybe (statusCredits s) (/ 100.0) leTotalCredits -- Only update if totalCredits is present - } - -- | Format elapsed time as MM:SS or HH:MM:SS formatElapsed :: NominalDiffTime -> Text formatElapsed elapsed = diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index aa7c5ab..ded4144 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -7,10 +7,10 @@ import Alpha import qualified Data.Aeson as Aeson import qualified Data.Aeson.Key as AesonKey import qualified Data.ByteString.Lazy as BSL +import Data.IORef (modifyIORef', newIORef, readIORef) import qualified Data.List as List import qualified Data.Text as Text import qualified Data.Text.Encoding as TE -import qualified Data.Text.IO as TIO import qualified Data.Time import qualified Omni.Agent.Core as Core import qualified Omni.Agent.Engine as Engine @@ -22,7 +22,6 @@ import qualified System.Directory as Directory import qualified System.Environment as Env import qualified System.Exit as Exit import System.FilePath ((</>)) -import qualified System.IO as IO import qualified System.Process as Process start :: Core.Worker -> Maybe Text -> IO () @@ -89,38 +88,18 @@ processTask worker task = do TaskCore.updateTaskStatus tid TaskCore.InProgress [] say "[worker] Status -> InProgress" - -- Check if we should use native engine - useEngine <- shouldUseEngine - -- Run agent with timing startTime <- Data.Time.getCurrentTime activityId <- TaskCore.logActivityWithMetrics tid TaskCore.Running Nothing Nothing (Just startTime) Nothing Nothing Nothing - (exitCode, output, maybeCost) <- - if useEngine - then do - say "[worker] Starting native engine..." - (code, out, cost) <- runWithEngine repo task - pure (code, out, Just cost) - else do - say "[worker] Starting amp..." - (code, out) <- runAmp repo task - pure (code, out, Nothing) + say "[worker] Starting engine..." + (exitCode, output, costCents) <- runWithEngine repo task endTime <- Data.Time.getCurrentTime say ("[worker] Agent exited with: " <> tshow exitCode) - -- Capture metrics - from engine result or agent log - (threadUrl, costCents) <- case maybeCost of - Just engineCost -> pure (Nothing, Just engineCost) - Nothing -> do - status <- AgentLog.getStatus - let url = ("https://ampcode.com/threads/" <>) </ AgentLog.statusThread status - let cost = Just <| floor (AgentLog.statusCredits status * 100) - pure (url, cost) - -- Update the activity record with metrics - TaskCore.updateActivityMetrics activityId threadUrl (Just endTime) costCents Nothing + TaskCore.updateActivityMetrics activityId Nothing (Just endTime) (Just costCents) Nothing case exitCode of Exit.ExitSuccess -> do @@ -178,10 +157,10 @@ processTask worker task = do say ("[worker] ✓ Task " <> tid <> " -> Review") unless quiet <| AgentLog.update (\s -> s {AgentLog.statusTask = Nothing}) Exit.ExitFailure code -> do - say ("[worker] Amp failed with code " <> tshow code) + say ("[worker] Engine failed with code " <> tshow code) TaskCore.logActivity tid TaskCore.Failed (Just (toMetadata [("exit_code", tshow code)])) -- Don't set back to Open here - leave in InProgress for debugging - say "[worker] Task left in InProgress (amp failure)" + say "[worker] Task left in InProgress (engine failure)" -- | Run lint --fix to format and fix lint issues runFormatters :: FilePath -> IO (Either Text ()) @@ -218,115 +197,7 @@ tryCommit repo msg = do Exit.ExitFailure _ -> pure <| CommitFailed (Text.pack commitErr) Exit.ExitFailure c -> pure <| CommitFailed ("git diff failed with code " <> tshow c) -runAmp :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text) -runAmp repo task = do - -- Check for retry context - maybeRetry <- TaskCore.getRetryContext (TaskCore.taskId task) - - let ns = fromMaybe "." (TaskCore.taskNamespace task) - let basePrompt = - "You are a Worker Agent.\n" - <> "Your goal is to implement the following task:\n\n" - <> formatTask task - <> "\n\nCRITICAL INSTRUCTIONS:\n" - <> "1. Analyze the codebase to understand where to make changes.\n" - <> "2. Implement the changes by editing files.\n" - <> "3. BEFORE finishing, you MUST run: bild --test " - <> ns - <> "\n" - <> "4. Fix ALL errors from bild --test (including hlint suggestions).\n" - <> "5. Keep running bild --test until it passes with no errors.\n" - <> "6. Do NOT update task status or manage git.\n" - <> "7. Only exit after bild --test passes.\n\n" - <> "IMPORTANT: The git commit will fail if hlint finds issues.\n" - <> "You must fix hlint suggestions like:\n" - <> "- 'Use list comprehension' -> use [x | cond] instead of if/else\n" - <> "- 'Avoid lambda' -> use function composition\n" - <> "- 'Redundant bracket' -> remove unnecessary parens\n\n" - <> "Context:\n" - <> "- Working directory: " - <> Text.pack repo - <> "\n" - <> "- Namespace: " - <> ns - <> "\n" - - -- Add retry context if present - let retryPrompt = case maybeRetry of - Nothing -> "" - Just ctx -> - "\n\n## RETRY CONTEXT (IMPORTANT)\n\n" - <> "This task was previously attempted but failed. Attempt: " - <> tshow (TaskCore.retryAttempt ctx) - <> "/3\n" - <> "Reason: " - <> TaskCore.retryReason ctx - <> "\n\n" - <> ( if null (TaskCore.retryConflictFiles ctx) - then "" - else - "Conflicting files from previous attempt:\n" - <> Text.unlines (map (" - " <>) (TaskCore.retryConflictFiles ctx)) - <> "\n" - ) - <> "Original commit: " - <> TaskCore.retryOriginalCommit ctx - <> "\n\n" - <> maybe "" (\notes -> "## HUMAN NOTES/GUIDANCE\n\n" <> notes <> "\n\n") (TaskCore.retryNotes ctx) - <> "INSTRUCTIONS FOR RETRY:\n" - <> "- The codebase has changed since your last attempt\n" - <> "- Re-implement this task on top of the CURRENT codebase\n" - <> "- If there were merge conflicts, the conflicting files may have been modified by others\n" - <> "- Review the current state of those files before making changes\n" - - let prompt = basePrompt <> retryPrompt - - let logFile = repo </> "_/llm/amp.log" - - -- Read AGENTS.md - agentsMd <- - fmap (fromMaybe "") <| do - exists <- Directory.doesFileExist (repo </> "AGENTS.md") - if exists - then Just </ readFile (repo </> "AGENTS.md") - else pure Nothing - - -- Get relevant facts from the knowledge base - relevantFacts <- getRelevantFacts task - let factsSection = formatFacts relevantFacts - - let fullPrompt = - prompt - <> "\n\nREPOSITORY GUIDELINES (AGENTS.md):\n" - <> agentsMd - <> factsSection - - -- Remove old log file - exists <- Directory.doesFileExist logFile - when exists (Directory.removeFile logFile) - - Directory.createDirectoryIfMissing True (repo </> "_/llm") - - -- Assume amp is in PATH - let args = ["--try-opus", "--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack fullPrompt] - - let cp = (Process.proc "amp" args) {Process.cwd = Just repo, Process.std_out = Process.CreatePipe} - (_, Just hOut, _, ph) <- Process.createProcess cp - - tid <- forkIO <| monitorLog logFile ph - - exitCode <- Process.waitForProcess ph - output <- TIO.hGetContents hOut - killThread tid - pure (exitCode, output) - --- | Check if we should use native engine instead of amp subprocess -shouldUseEngine :: IO Bool -shouldUseEngine = do - env <- Env.lookupEnv "JR_USE_ENGINE" - pure <| env == Just "1" - --- | Run task using native Engine instead of amp subprocess +-- | Run task using native Engine -- Returns (ExitCode, output text, cost in cents) runWithEngine :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text, Int) runWithEngine repo task = do @@ -338,7 +209,7 @@ runWithEngine repo task = do -- Check for retry context maybeRetry <- TaskCore.getRetryContext (TaskCore.taskId task) - -- Build the full prompt (same as runAmp) + -- Build the full prompt let ns = fromMaybe "." (TaskCore.taskNamespace task) let basePrompt = buildBasePrompt task ns repo @@ -517,10 +388,10 @@ formatTask t = formatComment c = " [" <> Text.pack (show (TaskCore.commentCreatedAt c)) <> "] " <> TaskCore.commentText c formatCommitMessage :: TaskCore.Task -> Text -> Text -formatCommitMessage task ampOutput = +formatCommitMessage task agentOutput = let tid = TaskCore.taskId task subject = cleanSubject (TaskCore.taskTitle task) - body = cleanBody ampOutput + body = cleanBody agentOutput in if Text.null body then subject <> "\n\nTask-Id: " <> tid else subject <> "\n\n" <> body <> "\n\nTask-Id: " <> tid @@ -573,34 +444,3 @@ formatFact f = then "" else " [" <> Text.intercalate ", " (TaskCore.factRelatedFiles f) <> "]" ) - -monitorLog :: FilePath -> Process.ProcessHandle -> IO () -monitorLog path ph = do - waitForFile path - IO.withFile path IO.ReadMode <| \h -> do - IO.hSetBuffering h IO.LineBuffering - go h - where - go h = do - eof <- IO.hIsEOF h - if eof - then do - mExit <- Process.getProcessExitCode ph - case mExit of - Nothing -> do - threadDelay 100000 -- 0.1s - go h - Just _ -> pure () - else do - line <- TIO.hGetLine h - AgentLog.processLogLine line - go h - -waitForFile :: FilePath -> IO () -waitForFile path = do - exists <- Directory.doesFileExist path - if exists - then pure () - else do - threadDelay 100000 - waitForFile path @@ -155,7 +155,7 @@ runLoop delaySec = do (task : _) -> do putText "" putText ("[loop] === Working on: " <> TaskCore.taskId task <> " ===") - -- Run worker (this blocks until amp completes) + -- Run worker (this blocks until the engine completes) absPath <- Directory.getCurrentDirectory let name = Text.pack (takeFileName absPath) let worker = diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs index befda94..4e55c61 100644 --- a/Omni/Jr/Web.hs +++ b/Omni/Jr/Web.hs @@ -1725,11 +1725,11 @@ instance Lucid.ToHtml TaskDetailPage where renderAttempt totalAttempts (attemptNum, act) = do when (totalAttempts > 1) <| Lucid.div_ [Lucid.class_ "attempt-header"] (Lucid.toHtml ("Attempt " <> tshow attemptNum :: Text)) - case TaskCore.activityAmpThreadUrl act of + case TaskCore.activityThreadUrl act of Nothing -> pure () Just url -> Lucid.div_ [Lucid.class_ "metric-row"] <| do - Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:" + Lucid.span_ [Lucid.class_ "metric-label"] "Session:" Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "amp-thread-btn"] "View in Amp ↗" case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of @@ -2203,11 +2203,11 @@ instance Lucid.ToHtml TaskMetricsPartial where renderAttempt totalAttempts currentTime (attemptNum, act) = do when (totalAttempts > 1) <| Lucid.div_ [Lucid.class_ "attempt-header"] (Lucid.toHtml ("Attempt " <> tshow attemptNum :: Text)) - case TaskCore.activityAmpThreadUrl act of + case TaskCore.activityThreadUrl act of Nothing -> pure () Just url -> Lucid.div_ [Lucid.class_ "metric-row"] <| do - Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:" + Lucid.span_ [Lucid.class_ "metric-label"] "Session:" Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "amp-thread-btn"] "View in Amp ↗" case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 92936bb..49c2247 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -120,7 +120,7 @@ data TaskActivity = TaskActivity activityStage :: ActivityStage, activityMessage :: Maybe Text, activityMetadata :: Maybe Text, -- JSON for extra data - activityAmpThreadUrl :: Maybe Text, -- Link to amp thread + activityThreadUrl :: Maybe Text, -- Link to agent session (unused with native Engine) activityStartedAt :: Maybe UTCTime, -- When work started activityCompletedAt :: Maybe UTCTime, -- When work completed activityCostCents :: Maybe Int, -- API cost in cents @@ -340,7 +340,7 @@ instance SQL.ToRow TaskActivity where SQL.toField (activityStage a), SQL.toField (activityMessage a), SQL.toField (activityMetadata a), - SQL.toField (activityAmpThreadUrl a), + SQL.toField (activityThreadUrl a), SQL.toField (activityStartedAt a), SQL.toField (activityCompletedAt a), SQL.toField (activityCostCents a), |
