summaryrefslogtreecommitdiff
path: root/Omni/Agent/Worker.hs
diff options
context:
space:
mode:
Diffstat (limited to 'Omni/Agent/Worker.hs')
-rw-r--r--Omni/Agent/Worker.hs139
1 files changed, 139 insertions, 0 deletions
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
new file mode 100644
index 0000000..be971cf
--- /dev/null
+++ b/Omni/Agent/Worker.hs
@@ -0,0 +1,139 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : out omni-agent-worker
+module Omni.Agent.Worker where
+
+import Alpha
+import qualified Data.Text as Text
+import qualified Omni.Agent.Core as Core
+import qualified Omni.Agent.Git as Git
+import qualified Omni.Log as Log
+import qualified Omni.Task.Core as TaskCore
+import qualified System.Directory as Directory
+import qualified System.Process as Process
+import qualified System.Exit as Exit
+import System.FilePath ((</>))
+
+start :: Core.Worker -> IO ()
+start worker = do
+ Log.info ["worker", "starting loop for", Core.workerName worker]
+ loop worker
+
+loop :: Core.Worker -> IO ()
+loop worker = do
+ let repo = Core.workerPath worker
+
+ Log.info ["worker", "syncing tasks"]
+ -- Sync with live first to get latest code and tasks
+ -- We ignore errors here to keep the loop alive, but syncWithLive panics on conflict.
+ -- Ideally we should catch exceptions, but for now let it fail and restart (via supervisor or manual).
+ Git.syncWithLive repo
+
+ -- Sync tasks database (import from live)
+ -- Since we rebased, .tasks/tasks.jsonl should be up to date with live.
+ -- But we might need to consolidate if there are merge artifacts (not likely with rebase).
+ -- The bash script calls ./Omni/Agent/sync-tasks.sh which calls 'task import'.
+ -- Here we rely on 'task loadTasks' reading the file.
+ -- But 'syncWithLive' already updated the file from git.
+
+ -- Find ready work
+ readyTasks <- TaskCore.getReadyTasks
+ case readyTasks of
+ [] -> do
+ Log.info ["worker", "no work found, sleeping"]
+ threadDelay (60 * 1000000) -- 60 seconds
+ loop worker
+
+ (task : _) -> do
+ processTask worker task
+ loop worker
+
+processTask :: Core.Worker -> TaskCore.Task -> IO ()
+processTask worker task = do
+ let repo = Core.workerPath worker
+ let tid = TaskCore.taskId task
+
+ Log.info ["worker", "claiming task", tid]
+
+ -- Claim task
+ TaskCore.updateTaskStatus tid TaskCore.InProgress
+
+ -- Commit claim locally
+ Git.commit repo ("task: claim " <> tid)
+
+ -- Prepare branch
+ let taskBranch = "task/" <> tid
+ currentBranch <- Git.getCurrentBranch repo
+ if currentBranch == taskBranch
+ then Log.info ["worker", "resuming branch", taskBranch]
+ else Git.createBranch repo taskBranch
+
+ -- Run Amp
+ exitCode <- runAmp repo task
+
+ case exitCode of
+ Exit.ExitSuccess -> do
+ Log.info ["worker", "agent finished successfully"]
+
+ -- Commit changes
+ -- We should check if there are changes, but 'git add .' is safe.
+ Git.commit repo ("feat: implement " <> tid)
+
+ -- Submit for review
+ Log.info ["worker", "submitting for review"]
+
+ -- Switch back to worker base
+ let base = Core.workerName worker
+ Git.checkout repo base
+
+ -- Sync again
+ Git.syncWithLive repo
+
+ -- Update status to Review
+ TaskCore.updateTaskStatus tid TaskCore.Review
+ Git.commit repo ("task: review " <> tid)
+
+ Exit.ExitFailure code -> do
+ Log.warn ["worker", "agent failed with code", Text.pack (show code)]
+ threadDelay (10 * 1000000) -- Sleep 10s
+
+runAmp :: FilePath -> TaskCore.Task -> IO Exit.ExitCode
+runAmp repo task = do
+ let prompt = "You are a Worker Agent.\n"
+ <> "Your goal is to implement the following task:\n\n"
+ <> formatTask task
+ <> "\n\nINSTRUCTIONS:\n"
+ <> "1. Analyze the codebase (use finder/Grep) to understand where to make changes.\n"
+ <> "2. Implement the changes by editing files.\n"
+ <> "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. When finished and tested, exit.\n\n"
+ <> "Context:\n"
+ <> "- You are working in '" <> Text.pack repo <> "'.\n"
+ <> "- The task is in namespace '" <> maybe "root" (\x -> x) (TaskCore.taskNamespace task) <> "'.\n"
+
+ 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 prompt]
+
+ let cp = (Process.proc "amp" args) {Process.cwd = Just repo}
+ (_, _, _, ph) <- Process.createProcess cp
+ Process.waitForProcess ph
+
+formatTask :: TaskCore.Task -> Text
+formatTask t =
+ "Task: " <> TaskCore.taskId t <> "\n"
+ <> "Title: " <> TaskCore.taskTitle t <> "\n"
+ <> "Type: " <> Text.pack (show (TaskCore.taskType t)) <> "\n"
+ <> "Status: " <> Text.pack (show (TaskCore.taskStatus t)) <> "\n"
+ <> "Priority: " <> Text.pack (show (TaskCore.taskPriority t)) <> "\n"
+ <> maybe "" (\p -> "Parent: " <> p <> "\n") (TaskCore.taskParent t)
+ <> maybe "" (\ns -> "Namespace: " <> ns <> "\n") (TaskCore.taskNamespace t)
+ <> "Created: " <> Text.pack (show (TaskCore.taskCreatedAt t)) <> "\n"
+ <> "Updated: " <> Text.pack (show (TaskCore.taskUpdatedAt t)) <> "\n"
+ <> (if null (TaskCore.taskDependencies t) then "" else "\nDependencies:\n" <> Text.unlines (map formatDep (TaskCore.taskDependencies t)))
+ where
+ formatDep dep = " - " <> TaskCore.depId dep <> " [" <> Text.pack (show (TaskCore.depType dep)) <> "]"