diff options
| author | Ben Sima <ben@bensima.com> | 2025-11-22 17:07:32 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-22 17:08:11 -0500 |
| commit | 832a0a7d88d0553e7edf055addb2c3a6f9f492ab (patch) | |
| tree | 805e0aa0e174f2a3c61cb44fb3fe71562d77af2b /Omni | |
| parent | 3e232940a6769cc2a238dc7b41b7c7b215295963 (diff) | |
| parent | bb15513a94140c22aa3aea510314f60c94df4d97 (diff) | |
task: complete t-1o2bxd11zv9 (Merge)
https: //ampcode.com/threads/T-ca3b086b-5a85-422a-b13d-256784c04221
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID:
https://ampcode.com/threads/T-ca3b086b-5a85-422a-b13d-256784c04221
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Agent/Log.hs | 213 | ||||
| -rw-r--r-- | Omni/Agent/Worker.hs | 99 |
2 files changed, 130 insertions, 182 deletions
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index dd66abc..07770d0 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-} @@ -7,34 +6,22 @@ module Omni.Agent.Log where import Alpha -import Data.Aeson (Value (..), decode) -import qualified Data.Aeson.KeyMap as KM +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.Encoding as TextEnc +import qualified Data.Text as Text +import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO -import qualified Data.Vector as V import qualified System.Console.ANSI as ANSI import qualified System.IO as IO import System.IO.Unsafe (unsafePerformIO) -import Text.Printf (printf) - --- | Parsed log entry -data LogEntry = LogEntry - { leMessage :: Maybe Text, - leLevel :: Maybe Text, - leToolName :: Maybe Text, - leBatches :: Maybe [[Text]], - leMethod :: Maybe Text, - lePath :: Maybe Text - } - deriving (Show, Eq) -- | Status of the agent for the UI data Status = Status { statusWorker :: Text, statusTask :: Maybe Text, - statusThreadId :: Maybe Text, + statusThread :: Maybe Text, statusFiles :: Int, statusCredits :: Double, statusTime :: Text, -- formatted time string @@ -47,7 +34,7 @@ emptyStatus workerName = Status { statusWorker = workerName, statusTask = Nothing, - statusThreadId = Nothing, + statusThread = Nothing, statusFiles = 0, statusCredits = 0.0, statusTime = "00:00", @@ -64,9 +51,10 @@ init :: Text -> IO () init workerName = do IO.hSetBuffering IO.stderr IO.LineBuffering writeIORef currentStatus (emptyStatus workerName) - -- Reserve 5 lines at bottom - replicateM_ 5 (IO.hPutStrLn IO.stderr "") - ANSI.hCursorUp IO.stderr 5 + -- Reserve 2 lines at bottom + IO.hPutStrLn IO.stderr "" + IO.hPutStrLn IO.stderr "" + ANSI.hCursorUp IO.stderr 2 -- | Update the status update :: (Status -> Status) -> IO () @@ -78,96 +66,14 @@ update f = do updateActivity :: Text -> IO () updateActivity msg = update (\s -> s {statusActivity = msg}) --- | Process a log line from the agent and update status if relevant -processLogLine :: Text -> IO () -processLogLine line = do - let entry = parseLine line - for_ (entry +> formatLogEntry) updateActivity - --- | Parse a JSON log line into a LogEntry -parseLine :: Text -> Maybe LogEntry -parseLine line = do - let lbs = BL.fromStrict (TextEnc.encodeUtf8 line) - obj <- decode lbs - case obj of - Object o -> - Just - LogEntry - { leMessage = getString "message" o, - leLevel = getString "level" o, - leToolName = getString "toolName" o, - leBatches = getBatches o, - leMethod = getString "method" o, - lePath = getString "path" o - } - _ -> Nothing - where - getString k o = - case KM.lookup k o of - Just (String s) -> Just s - _ -> Nothing - - getBatches o = - case KM.lookup "batches" o of - Just (Array b) -> - Just - <| mapMaybe - ( \case - Array b0 -> - Just - <| mapMaybe - ( \case - String s -> Just s - _ -> Nothing - ) - (V.toList b0) - _ -> Nothing - ) - (V.toList b) - _ -> Nothing - --- | Format a log entry into a user-friendly status message (NO EMOJIS) -formatLogEntry :: LogEntry -> Maybe Text -formatLogEntry LogEntry {..} = - case leMessage of - Just "executing 1 tools in 1 batch(es)" -> do - let tools = fromMaybe [] leBatches - let firstTool = case tools of - ((t : _) : _) -> t - _ -> "unknown" - Just ("THOUGHT: Planning tool execution (" <> firstTool <> ")") - Just "Tool Bash permitted - action: allow" -> - Just "TOOL: Bash command executed" - Just "Processing tool completion for ledger" - | isJust leToolName -> - Just ("TOOL: " <> fromMaybe "unknown" leToolName <> " completed") - Just "ide-fs" | leMethod == Just "readFile" -> - case lePath of - Just p -> Just ("READ: " <> p) - _ -> Nothing - Just "System prompt build complete (no changes)" -> - Just "THINKING..." - Just "System prompt build complete (first build)" -> - Just "STARTING new task context" - Just msg - | leLevel == Just "error" -> - Just ("ERROR: " <> msg) - _ -> Nothing - -- | Log a scrolling message (appears above status bars) log :: Text -> IO () log msg = do - -- Clear status bars (5 lines) - ANSI.hClearLine IO.stderr - ANSI.hCursorDown IO.stderr 1 - ANSI.hClearLine IO.stderr - ANSI.hCursorDown IO.stderr 1 - ANSI.hClearLine IO.stderr - ANSI.hCursorDown IO.stderr 1 + -- Clear status bars ANSI.hClearLine IO.stderr ANSI.hCursorDown IO.stderr 1 ANSI.hClearLine IO.stderr - ANSI.hCursorUp IO.stderr 4 + ANSI.hCursorUp IO.stderr 1 -- Print message (scrolls screen) TIO.hPutStrLn IO.stderr msg @@ -176,43 +82,90 @@ log msg = do -- (Since we scrolled, we are now on the line above where the first status line should be) render --- | Render the 5 status lines (Vertical Layout) +-- | Render the two status lines render :: IO () render = do Status {..} <- readIORef currentStatus + -- Line 1: Meta + -- [Worker: name] Task: t-123 | Thread: T-abc | Files: 3 | Credits: $0.45 | Time: 05:23 let taskStr = maybe "None" identity statusTask - threadStr = maybe "None" identity statusThreadId + threadStr = maybe "None" identity statusThread + meta = + "[Worker: " + <> statusWorker + <> "] Task: " + <> taskStr + <> " | Thread: " + <> threadStr + <> " | Files: " + <> tshow statusFiles + <> " | Credits: $" + <> tshow statusCredits + <> " | Time: " + <> statusTime - -- Line 1: Worker + Time ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr <| "Worker: " <> statusWorker <> " | Time: " <> statusTime + TIO.hPutStr IO.stderr meta - -- Line 2: Task + -- Line 2: Activity + -- [14:05:22] > Thinking... ANSI.hCursorDown IO.stderr 1 ANSI.hSetCursorColumn IO.stderr 0 ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr <| "Task: " <> taskStr + TIO.hPutStr IO.stderr ("> " <> statusActivity) - -- Line 3: Thread - ANSI.hCursorDown IO.stderr 1 - ANSI.hSetCursorColumn IO.stderr 0 - ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr <| "Thread: " <> threadStr + -- Return cursor to line 1 + ANSI.hCursorUp IO.stderr 1 + IO.hFlush IO.stderr - -- Line 4: Credits - ANSI.hCursorDown IO.stderr 1 - ANSI.hSetCursorColumn IO.stderr 0 - ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr <| "Credits: $" <> str (printf "%.2f" statusCredits :: String) +-- | Log Entry from JSON +data LogEntry = LogEntry + { leMessage :: Text, + leThreadId :: Maybe Text, + leCredits :: Maybe Double, + leTotalCredits :: Maybe Double, + leTimestamp :: Maybe Text + } + deriving (Show, Eq) - -- Line 5: Activity - ANSI.hCursorDown IO.stderr 1 - ANSI.hSetCursorColumn IO.stderr 0 - ANSI.hClearLine IO.stderr - TIO.hPutStr IO.stderr ("> " <> statusActivity) +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 = fromMaybe (statusCredits s) (leTotalCredits <|> leCredits), + statusTime = maybe (statusTime s) formatTime leTimestamp + } - -- Return cursor to Line 1 - ANSI.hCursorUp IO.stderr 4 - IO.hFlush IO.stderr +formatTime :: Text -> Text +formatTime ts = + -- "2025-11-22T21:24:02.512Z" -> "21:24" + case Text.splitOn "T" ts of + [_, time] -> case Text.splitOn ":" time of + (h : m : _) -> h <> ":" <> m + _ -> ts + _ -> ts diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs index 1cc0b8d..9f9e3bb 100644 --- a/Omni/Agent/Worker.hs +++ b/Omni/Agent/Worker.hs @@ -89,7 +89,7 @@ processTask worker task = do -- Run Amp AgentLog.updateActivity "Running Amp agent..." - (exitCode, output) <- runAmp repo task + exitCode <- runAmp repo task case exitCode of Exit.ExitSuccess -> do @@ -99,10 +99,8 @@ processTask worker task = do TaskCore.updateTaskStatus tid TaskCore.Review [] -- Commit changes - -- We use the agent's output as the extended commit description - let summary = Text.strip output - let commitMsg = "feat: implement " <> tid <> "\n\n" <> summary - Git.commit repo commitMsg + -- We should check if there are changes, but 'git add .' is safe. + Git.commit repo ("feat: implement " <> tid) -- Submit for review AgentLog.updateActivity "Submitting for review..." @@ -125,7 +123,7 @@ processTask worker task = do AgentLog.updateActivity "Agent failed, retrying..." threadDelay (10 * 1000000) -- Sleep 10s -runAmp :: FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text) +runAmp :: FilePath -> TaskCore.Task -> IO Exit.ExitCode runAmp repo task = do let prompt = "You are a Worker Agent.\n" @@ -137,8 +135,7 @@ runAmp repo task = do <> "3. Run tests to verify your work (e.g., 'bild --test Omni/Namespace').\n" <> "4. Fix any errors found during testing.\n" <> "5. Do NOT update the task status or manage git branches (the system handles that).\n" - <> "6. Do NOT run 'git commit'. The system will commit your changes automatically.\n" - <> "7. When finished and tested, exit.\n\n" + <> "6. When finished and tested, exit.\n\n" <> "Context:\n" <> "- You are working in '" <> Text.pack repo @@ -147,38 +144,25 @@ runAmp repo task = do <> fromMaybe "root" (TaskCore.taskNamespace task) <> "'.\n" - Directory.createDirectoryIfMissing True (repo </> "_/llm") - let logPath = repo </> "_/llm/amp.log" - - -- Ensure log file is empty/exists - IO.writeFile logPath "" - - -- Read AGENTS.md - agentsMd <- - fmap (fromMaybe "") <| do - exists <- Directory.doesFileExist (repo </> "AGENTS.md") - if exists - then Just </ readFile (repo </> "AGENTS.md") - else pure Nothing + let logFile = repo </> "_/llm/amp.log" - let fullPrompt = - prompt - <> "\n\nREPOSITORY GUIDELINES (AGENTS.md):\n" - <> agentsMd + -- Remove old log file + exists <- Directory.doesFileExist logFile + when exists (Directory.removeFile logFile) - -- Monitor log file - tidLog <- forkIO (monitorLog logPath) + Directory.createDirectoryIfMissing True (repo </> "_/llm") -- Assume amp is in PATH - let args = ["--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack fullPrompt] + let args = ["--log-level", "debug", "--log-file", "_/llm/amp.log", "--dangerously-allow-all", "-x", Text.unpack prompt] let cp = (Process.proc "amp" args) {Process.cwd = Just repo} - (exitCode, out, _err) <- Process.readCreateProcessWithExitCode cp "" + (_, _, _, ph) <- Process.createProcess cp - -- Cleanup - killThread tidLog + tid <- forkIO <| monitorLog logFile ph - pure (exitCode, Text.pack out) + exitCode <- Process.waitForProcess ph + killThread tid + pure exitCode formatTask :: TaskCore.Task -> Text formatTask t = @@ -210,6 +194,37 @@ formatTask t = where formatDep dep = " - " <> TaskCore.depId dep <> " [" <> Text.pack (show (TaskCore.depType dep)) <> "]" +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 + findBaseBranch :: FilePath -> TaskCore.Task -> IO Text findBaseBranch repo task = do let deps = TaskCore.taskDependencies task @@ -230,23 +245,3 @@ findBaseBranch repo task = do case candidates of (candidate : _) -> pure ("task/" <> TaskCore.depId candidate) [] -> pure "live" - -monitorLog :: FilePath -> IO () -monitorLog path = do - -- Wait for file to exist - waitForFile path - - IO.withFile path IO.ReadMode <| \h -> do - IO.hSetBuffering h IO.LineBuffering - forever <| do - eof <- IO.hIsEOF h - if eof - then threadDelay 100000 -- 0.1s - else do - line <- TIO.hGetLine h - AgentLog.processLogLine line - -waitForFile :: FilePath -> IO () -waitForFile p = do - e <- Directory.doesFileExist p - if e then pure () else threadDelay 100000 >> waitForFile p |
