summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-30 00:36:51 -0500
committerBen Sima <ben@bensima.com>2025-11-30 00:36:51 -0500
commit5fbcd92ff85bc9cc0b752888f6d3498aafea0b2a (patch)
tree4bcae7c97bf81ecae696744cfcd84520e5db639d /Omni
parentd05ca4732710dd9cef7fffd998a03615ad2cb58c (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
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Agent/DESIGN.md8
-rw-r--r--Omni/Agent/Log.hs42
-rw-r--r--Omni/Agent/Worker.hs180
-rwxr-xr-xOmni/Jr.hs2
-rw-r--r--Omni/Jr/Web.hs8
-rw-r--r--Omni/Task/Core.hs4
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
diff --git a/Omni/Jr.hs b/Omni/Jr.hs
index 49f94c8..0690970 100755
--- a/Omni/Jr.hs
+++ b/Omni/Jr.hs
@@ -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),