commit 483114d458d0828ab24b81fa1f932947cff198c8
Author: Ben Sima <ben@bensima.com>
Date: Fri Jan 2 00:05:02 2026
Omni/Agent/Telegram: Rename Orchestrator.hs to Developer.hs
'Orchestrator' was too vague and overloaded. 'Developer' better
describes its role as the Telegram bot that helps with development tasks.
Task-Id: t-340
diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs
index 6525e3fd..b205e62f 100644
--- a/Omni/Agent/Telegram.hs
+++ b/Omni/Agent/Telegram.hs
@@ -99,10 +99,10 @@ import qualified Omni.Agent.Skills as Skills
import qualified Omni.Agent.Subagent as Subagent
import qualified Omni.Agent.Subagent.Jobs as Jobs
import qualified Omni.Agent.Telegram.Actions as Actions
+import qualified Omni.Agent.Telegram.Developer as Developer
import qualified Omni.Agent.Telegram.IncomingQueue as IncomingQueue
import qualified Omni.Agent.Telegram.Media as Media
import qualified Omni.Agent.Telegram.Messages as Messages
-import qualified Omni.Agent.Telegram.Orchestrator as Orchestrator
import qualified Omni.Agent.Telegram.Reminders as Reminders
import qualified Omni.Agent.Telegram.Types as Types
import qualified Omni.Agent.Tools as Tools
@@ -117,11 +117,11 @@ import qualified Omni.Agent.Tools.Outreach as Outreach
import qualified Omni.Agent.Tools.Pdf as Pdf
import qualified Omni.Agent.Tools.Python as Python
import qualified Omni.Agent.Tools.Tasks as Tasks
-import qualified Omni.Task.Core as Task
import qualified Omni.Agent.Tools.Todos as Todos
import qualified Omni.Agent.Tools.WebReader as WebReader
import qualified Omni.Agent.Tools.WebSearch as WebSearch
import qualified Omni.Ava.Trace as Trace
+import qualified Omni.Task.Core as Task
import qualified Omni.Test as Test
import System.Environment (lookupEnv)
import Text.Printf (printf)
@@ -1772,9 +1772,10 @@ handleReadyCommand :: Int -> Maybe Int -> Text -> IO Bool
handleReadyCommand chatId mThreadId cmd
| cmd == "/ready" || cmd == "/ready " || cmd == "ready" || cmd == "show ready" || cmd == "what's ready" = do
readyTasks <- Task.getReadyTasks
- let msg = if null readyTasks
- then "No tasks ready right now."
- else formatReadyTasks (take 10 readyTasks)
+ let msg =
+ if null readyTasks
+ then "No tasks ready right now."
+ else formatReadyTasks (take 10 readyTasks)
_ <- Messages.enqueueImmediate Nothing chatId mThreadId msg (Just "system") Nothing
pure True
| otherwise = pure False
@@ -1784,10 +1785,14 @@ handleReadyCommand chatId mThreadId cmd
"📋 *Ready tasks:*\n\n"
<> Text.intercalate "\n" (map formatTask tasks)
<> "\n\n_Reply with a task ID to start work._"
-
+
formatTask :: Task.Task -> Text
formatTask t =
- "• `" <> Task.taskId t <> "` [" <> tshow (Task.taskPriority t) <> "] "
+ "• `"
+ <> Task.taskId t
+ <> "` ["
+ <> tshow (Task.taskPriority t)
+ <> "] "
<> Text.take 50 (Task.taskTitle t)
<> if Text.length (Task.taskTitle t) > 50 then "..." else ""
@@ -1795,7 +1800,7 @@ handleReadyCommand chatId mThreadId cmd
handleStatusCommand :: Int -> Maybe Int -> Text -> IO Bool
handleStatusCommand chatId mThreadId cmd
| cmd == "/status" || cmd == "/status " || cmd == "status" || cmd == "what are you working on" = do
- mStatus <- Orchestrator.getOrchestratorStatus chatId
+ mStatus <- Developer.getOrchestratorStatus chatId
let msg = case mStatus of
Nothing -> "I'm not working on anything right now. Send me a task ID to start work."
Just status -> formatOrchestratorStatus status
@@ -1803,38 +1808,46 @@ handleStatusCommand chatId mThreadId cmd
pure True
| otherwise = pure False
where
- formatOrchestratorStatus :: Orchestrator.OrchestratorStatus -> Text
+ formatOrchestratorStatus :: Developer.OrchestratorStatus -> Text
formatOrchestratorStatus status =
- "🔧 *Working on " <> Orchestrator.osTaskId status <> "*\n"
- <> Orchestrator.osTaskTitle status <> "\n\n"
- <> "Phase: " <> formatPhaseSimple (Orchestrator.osPhase status) <> "\n"
- <> "Iteration: " <> tshow (Orchestrator.osIteration status) <> "/" <> tshow (Orchestrator.osMaxIterations status)
-
- formatPhaseSimple :: Orchestrator.OrchestratorPhase -> Text
+ "🔧 *Working on "
+ <> Developer.osTaskId status
+ <> "*\n"
+ <> Developer.osTaskTitle status
+ <> "\n\n"
+ <> "Phase: "
+ <> formatPhaseSimple (Developer.osPhase status)
+ <> "\n"
+ <> "Iteration: "
+ <> tshow (Developer.osIteration status)
+ <> "/"
+ <> tshow (Developer.osMaxIterations status)
+
+ formatPhaseSimple :: Developer.OrchestratorPhase -> Text
formatPhaseSimple = \case
- Orchestrator.PhaseStarting -> "Starting"
- Orchestrator.PhaseCoder _ -> "Coder running"
- Orchestrator.PhaseCoderDone _ -> "Coder done"
- Orchestrator.PhaseVerifyBuild _ -> "Verifying build"
- Orchestrator.PhaseBuildPassed _ -> "Build passed"
- Orchestrator.PhaseBuildFailed _ _ -> "Build failed"
- Orchestrator.PhaseReviewer _ -> "Reviewer running"
- Orchestrator.PhaseReviewerApproved -> "Reviewer approved"
- Orchestrator.PhaseReviewerRejected _ _ -> "Reviewer rejected"
- Orchestrator.PhaseCommitting -> "Committing"
- Orchestrator.PhaseComplete _ -> "Complete"
- Orchestrator.PhaseFailed _ -> "Failed"
+ Developer.PhaseStarting -> "Starting"
+ Developer.PhaseCoder _ -> "Coder running"
+ Developer.PhaseCoderDone _ -> "Coder done"
+ Developer.PhaseVerifyBuild _ -> "Verifying build"
+ Developer.PhaseBuildPassed _ -> "Build passed"
+ Developer.PhaseBuildFailed _ _ -> "Build failed"
+ Developer.PhaseReviewer _ -> "Reviewer running"
+ Developer.PhaseReviewerApproved -> "Reviewer approved"
+ Developer.PhaseReviewerRejected _ _ -> "Reviewer rejected"
+ Developer.PhaseCommitting -> "Committing"
+ Developer.PhaseComplete _ -> "Complete"
+ Developer.PhaseFailed _ -> "Failed"
-- | Handle stop/cancel command - stop current orchestrator work
handleStopCommand :: Types.TelegramConfig -> Int -> Maybe Int -> Text -> IO Bool
handleStopCommand tgConfig chatId mThreadId cmd
| cmd == "stop" || cmd == "/stop" || cmd == "cancel" || cmd == "/cancel" || cmd == "stop work" = do
- result <- Orchestrator.stopOrchestrator chatId
+ result <- Developer.stopOrchestrator chatId
case result of
- Orchestrator.StopNotRunning -> do
+ Developer.StopNotRunning -> do
_ <- Messages.enqueueImmediate Nothing chatId mThreadId "I'm not working on anything right now." (Just "system") Nothing
pure True
- Orchestrator.StopSuccess taskId -> do
+ Developer.StopSuccess taskId -> do
let msg = "⛔ Stopped work on " <> taskId <> ". Changes are uncommitted."
keyboard =
Types.InlineKeyboardMarkup
diff --git a/Omni/Agent/Telegram/Orchestrator.hs b/Omni/Agent/Telegram/Developer.hs
similarity index 79%
rename from Omni/Agent/Telegram/Orchestrator.hs
rename to Omni/Agent/Telegram/Developer.hs
index 3b651239..7acc1935 100644
--- a/Omni/Agent/Telegram/Orchestrator.hs
+++ b/Omni/Agent/Telegram/Developer.hs
@@ -18,7 +18,7 @@
-- : dep aeson
-- : dep async
-- : dep http-conduit
-module Omni.Agent.Telegram.Orchestrator
+module Omni.Agent.Telegram.Developer
( -- * Configuration
OrchestratorConfig (..),
defaultConfig,
@@ -35,7 +35,7 @@ module Omni.Agent.Telegram.Orchestrator
OrchestratorStatus (..),
getActiveOrchestrators,
getOrchestratorStatus,
-
+
-- * Stopping
stopOrchestrator,
StopResult (..),
@@ -48,23 +48,23 @@ where
import Alpha
import qualified Control.Concurrent.Async as Async
+import qualified Control.Concurrent.STM as STM
+import qualified Control.Exception as Exception
+import Data.Aeson ((.:), (.=))
import qualified Data.Aeson as Aeson
-import Data.Aeson ((.=), (.:))
+import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
+import qualified Data.Time as Time
import qualified Network.HTTP.Simple as HTTP
import qualified Omni.Agent.Telegram.Types as Types
import qualified Omni.Task.Core as Task
import qualified Omni.Test as Test
import qualified System.Exit as Exit
-import qualified System.Process as Process
-import qualified System.Timeout as Timeout
import System.IO (hFlush)
-import qualified Control.Concurrent.STM as STM
-import qualified Data.Map.Strict as Map
-import qualified Data.Time as Time
import System.IO.Unsafe (unsafePerformIO)
-import qualified Control.Exception as Exception
+import qualified System.Process as Process
+import qualified System.Timeout as Timeout
-- | Configuration for an orchestrator run
data OrchestratorConfig = OrchestratorConfig
@@ -72,7 +72,7 @@ data OrchestratorConfig = OrchestratorConfig
orchChatId :: Int,
orchThreadId :: Maybe Int,
orchMaxIterations :: Int,
- orchCoderTimeout :: Int, -- seconds
+ orchCoderTimeout :: Int, -- seconds
orchReviewerTimeout :: Int, -- seconds
orchWorkDir :: FilePath
}
@@ -106,8 +106,8 @@ data OrchestratorStatus = OrchestratorStatus
-- | Result of stopping an orchestrator
data StopResult
- = StopSuccess Text -- Task ID that was stopped
- | StopNotRunning -- No orchestrator was running
+ = StopSuccess Text -- Task ID that was stopped
+ | StopNotRunning -- No orchestrator was running
deriving (Show, Eq)
-- | Global registry of active orchestrators (keyed by chatId)
@@ -117,23 +117,27 @@ activeOrchestrators = unsafePerformIO <| STM.newTVarIO Map.empty
-- | Register an orchestrator as active
registerOrchestrator :: OrchestratorStatus -> IO ()
-registerOrchestrator status = STM.atomically <|
- STM.modifyTVar' activeOrchestrators (Map.insert (osChatId status) status)
+registerOrchestrator status =
+ STM.atomically
+ <| STM.modifyTVar' activeOrchestrators (Map.insert (osChatId status) status)
-- | Update the phase of an active orchestrator
updateOrchestratorPhase :: Int -> OrchestratorPhase -> Int -> IO ()
-updateOrchestratorPhase chatId newPhase iteration = STM.atomically <|
- STM.modifyTVar' activeOrchestrators (Map.adjust (\s -> s { osPhase = newPhase, osIteration = iteration }) chatId)
+updateOrchestratorPhase chatId newPhase iteration =
+ STM.atomically
+ <| STM.modifyTVar' activeOrchestrators (Map.adjust (\s -> s {osPhase = newPhase, osIteration = iteration}) chatId)
-- | Update the current process handle for an orchestrator
updateOrchestratorProcess :: Int -> Maybe Process.ProcessHandle -> IO ()
-updateOrchestratorProcess chatId mHandle = STM.atomically <|
- STM.modifyTVar' activeOrchestrators (Map.adjust (\s -> s { osCurrentProcess = mHandle }) chatId)
+updateOrchestratorProcess chatId mHandle =
+ STM.atomically
+ <| STM.modifyTVar' activeOrchestrators (Map.adjust (\s -> s {osCurrentProcess = mHandle}) chatId)
-- | Remove an orchestrator from active registry
unregisterOrchestrator :: Int -> IO ()
-unregisterOrchestrator chatId = STM.atomically <|
- STM.modifyTVar' activeOrchestrators (Map.delete chatId)
+unregisterOrchestrator chatId =
+ STM.atomically
+ <| STM.modifyTVar' activeOrchestrators (Map.delete chatId)
-- | Get all active orchestrators
getActiveOrchestrators :: IO [OrchestratorStatus]
@@ -157,7 +161,7 @@ stopOrchestrator chatId = do
-- Use interruptProcessGroupOf to also kill child processes
Process.interruptProcessGroupOf ph
-- Give it a moment, then force terminate
- threadDelay 100000 -- 100ms
+ threadDelay 100000 -- 100ms
Process.terminateProcess ph
Nothing -> pure ()
-- Unregister the orchestrator
@@ -167,7 +171,7 @@ stopOrchestrator chatId = do
-- | Current phase of the orchestrator
data OrchestratorPhase
= PhaseStarting
- | PhaseCoder Int -- iteration
+ | PhaseCoder Int -- iteration
| PhaseCoderDone Int
| PhaseVerifyBuild Int
| PhaseBuildPassed Int
@@ -176,16 +180,16 @@ data OrchestratorPhase
| PhaseReviewerApproved
| PhaseReviewerRejected Int Text
| PhaseCommitting
- | PhaseComplete Text -- commit hash
+ | PhaseComplete Text -- commit hash
| PhaseFailed Text
deriving (Show, Eq, Generic)
-- | Format a phase as user-friendly status text
formatPhase :: Text -> Int -> OrchestratorPhase -> Text
formatPhase tid maxIter = \case
- PhaseStarting ->
+ PhaseStarting ->
"🔧 " <> tid <> ": Starting orchestrator..."
- PhaseCoder n ->
+ PhaseCoder n ->
"🔧 " <> tid <> ": Running coder... (iteration " <> tshow n <> "/" <> tshow maxIter <> ")"
PhaseCoderDone n ->
"🔧 " <> tid <> ": Coder complete (iteration " <> tshow n <> "/" <> tshow maxIter <> ")"
@@ -218,21 +222,21 @@ spawnOrchestrator :: Types.TelegramConfig -> OrchestratorConfig -> IO (Async.Asy
spawnOrchestrator tgCfg cfg = do
putText <| "spawnOrchestrator: About to spawn async for " <> orchTaskId cfg
hFlush stdout
-
+
-- Look up task title for status display (do this BEFORE spawning)
allTasks <- Task.loadTasks
let mTask = Task.findTask (orchTaskId cfg) allTasks
taskTitle = maybe "Unknown task" Task.taskTitle mTask
chatId = orchChatId cfg
tid = orchTaskId cfg
-
+
Async.async <| do
-- Wrap ENTIRE body in exception handler + finally for cleanup
let cleanup = do
putText <| "Orchestrator cleanup running for " <> tid
hFlush stdout
unregisterOrchestrator chatId
-
+
Exception.finally (runOrchestratorThread tgCfg cfg taskTitle) cleanup
-- | Inner orchestrator thread logic, separated for cleaner exception handling
@@ -241,50 +245,65 @@ runOrchestratorThread tgCfg cfg taskTitle = do
let chatId = orchChatId cfg
tid = orchTaskId cfg
maxIter = orchMaxIterations cfg
-
+
-- Catch ANY exception and report it
- result <- try @SomeException <| do
- putText <| "Orchestrator async thread started for " <> tid
- hFlush stdout
-
- -- Register this orchestrator
- now <- Time.getCurrentTime
- let status = OrchestratorStatus
- { osTaskId = tid,
- osTaskTitle = taskTitle,
- osChatId = chatId,
- osPhase = PhaseStarting,
- osIteration = 0,
- osMaxIterations = maxIter,
- osStartedAt = now,
- osCurrentProcess = Nothing
- }
- registerOrchestrator status
-
- -- Create initial status message
- mMsgId <- createStatusMessage tgCfg chatId (orchThreadId cfg)
- (formatPhase tid maxIter PhaseStarting)
- putText <| "Orchestrator: createStatusMessage returned " <> tshow mMsgId
- hFlush stdout
-
- case mMsgId of
- Nothing -> do
- -- Failed to create status message, send error
- _ <- sendMessage tgCfg chatId (orchThreadId cfg)
- ("❌ Failed to start orchestrator for " <> tid <> ": couldn't create status message")
- pure ()
- Just msgId -> do
- -- Run the main orchestrator loop
- runOrchestrator tgCfg cfg msgId
-
+ result <-
+ try @SomeException <| do
+ putText <| "Orchestrator async thread started for " <> tid
+ hFlush stdout
+
+ -- Register this orchestrator
+ now <- Time.getCurrentTime
+ let status =
+ OrchestratorStatus
+ { osTaskId = tid,
+ osTaskTitle = taskTitle,
+ osChatId = chatId,
+ osPhase = PhaseStarting,
+ osIteration = 0,
+ osMaxIterations = maxIter,
+ osStartedAt = now,
+ osCurrentProcess = Nothing
+ }
+ registerOrchestrator status
+
+ -- Create initial status message
+ mMsgId <-
+ createStatusMessage
+ tgCfg
+ chatId
+ (orchThreadId cfg)
+ (formatPhase tid maxIter PhaseStarting)
+ putText <| "Orchestrator: createStatusMessage returned " <> tshow mMsgId
+ hFlush stdout
+
+ case mMsgId of
+ Nothing -> do
+ -- Failed to create status message, send error
+ _ <-
+ sendMessage
+ tgCfg
+ chatId
+ (orchThreadId cfg)
+ ("❌ Failed to start orchestrator for " <> tid <> ": couldn't create status message")
+ pure ()
+ Just msgId -> do
+ -- Run the main orchestrator loop
+ runOrchestrator tgCfg cfg msgId
+
-- Handle any exception that occurred
case result of
Left err -> do
putText <| "Orchestrator " <> tid <> " crashed with exception: " <> tshow err
hFlush stdout
-- Try to notify user (this might also fail, but at least we try)
- _ <- try @SomeException <| sendMessage tgCfg chatId (orchThreadId cfg)
- ("❌ " <> tid <> " orchestrator crashed: " <> Text.take 200 (tshow err))
+ _ <-
+ try @SomeException
+ <| sendMessage
+ tgCfg
+ chatId
+ (orchThreadId cfg)
+ ("❌ " <> tid <> " orchestrator crashed: " <> Text.take 200 (tshow err))
pure ()
Right () -> do
putText <| "Orchestrator " <> tid <> " completed normally"
@@ -300,13 +319,13 @@ runOrchestrator tgCfg cfg statusMsgId = do
updateStatusMessage tgCfg chatId statusMsgId (formatPhase tid maxIter p)
updateOrchestratorPhase chatId p iter
sendResult msg = sendMessage tgCfg chatId (orchThreadId cfg) msg
-
+
-- Run iterations
result <- runLoop 1
-
+
-- Unregister this orchestrator
unregisterOrchestrator chatId
-
+
-- Send final result as new message
case result of
Left err -> do
@@ -322,28 +341,31 @@ runOrchestrator tgCfg cfg statusMsgId = do
where
runLoop :: Int -> IO (Either Text Text)
runLoop iteration
- | iteration > orchMaxIterations cfg =
+ | iteration > orchMaxIterations cfg =
pure (Left "Max iterations exceeded")
| otherwise = do
let chatId = orchChatId cfg
updatePhase' p = do
- updateStatusMessage tgCfg chatId statusMsgId
+ updateStatusMessage
+ tgCfg
+ chatId
+ statusMsgId
(formatPhase (orchTaskId cfg) (orchMaxIterations cfg) p)
updateOrchestratorPhase chatId p iteration
-
+
-- Phase: Coder
updatePhase' (PhaseCoder iteration)
coderResult <- runCoder cfg
-
+
case coderResult of
Left err -> pure (Left ("Coder failed: " <> err))
Right () -> do
updatePhase' (PhaseCoderDone iteration)
-
+
-- Phase: Verify build
updatePhase' (PhaseVerifyBuild iteration)
buildResult <- runBuild cfg
-
+
case buildResult of
Left err -> do
updatePhase' (PhaseBuildFailed iteration err)
@@ -351,11 +373,11 @@ runOrchestrator tgCfg cfg statusMsgId = do
runLoop (iteration + 1)
Right () -> do
updatePhase' (PhaseBuildPassed iteration)
-
+
-- Phase: Reviewer
updatePhase' (PhaseReviewer iteration)
reviewResult <- runReviewer cfg
-
+
case reviewResult of
Left err -> do
updatePhase' (PhaseReviewerRejected iteration err)
@@ -368,35 +390,36 @@ runOrchestrator tgCfg cfg statusMsgId = do
-- | Run the coder subprocess (pi-code.sh)
runCoder :: OrchestratorConfig -> IO (Either Text ())
runCoder cfg = do
- let proc = (Process.proc "./Omni/Ide/pi-code.sh" [Text.unpack (orchTaskId cfg)])
- { Process.cwd = Just (orchWorkDir cfg),
- -- Inherit stdout/stderr so output goes to parent (visible in logs)
- -- and we don't block on full pipe buffers
- Process.std_out = Process.Inherit,
- Process.std_err = Process.Inherit,
- -- Create new process group so we can kill children
- Process.create_group = True
- }
-
+ let proc =
+ (Process.proc "./Omni/Ide/pi-code.sh" [Text.unpack (orchTaskId cfg)])
+ { Process.cwd = Just (orchWorkDir cfg),
+ -- Inherit stdout/stderr so output goes to parent (visible in logs)
+ -- and we don't block on full pipe buffers
+ Process.std_out = Process.Inherit,
+ Process.std_err = Process.Inherit,
+ -- Create new process group so we can kill children
+ Process.create_group = True
+ }
+
(_, _, _, ph) <- Process.createProcess proc
-
+
-- Register the process handle so it can be stopped
updateOrchestratorProcess (orchChatId cfg) (Just ph)
-
+
-- Wait with timeout
let timeoutMicros = orchCoderTimeout cfg * 1000000
mExit <- Timeout.timeout timeoutMicros (Process.waitForProcess ph)
-
+
-- Clear the process handle
updateOrchestratorProcess (orchChatId cfg) Nothing
-
+
case mExit of
Nothing -> do
-- Timeout - kill process
Process.terminateProcess ph
pure (Left "Coder timeout")
Just Exit.ExitSuccess -> pure (Right ())
- Just (Exit.ExitFailure code) ->
+ Just (Exit.ExitFailure code) ->
pure (Left ("Exit code " <> tshow code))
-- | Run build verification (bild)
@@ -408,23 +431,24 @@ runBuild cfg = do
Nothing -> pure (Left "Task not found")
Just task -> do
let namespace = fromMaybe "Omni/Ava.hs" (Task.taskNamespace task)
- proc = (Process.proc "bild" [Text.unpack namespace])
- { Process.cwd = Just (orchWorkDir cfg),
- Process.std_out = Process.Inherit,
- Process.std_err = Process.Inherit,
- Process.create_group = True
- }
-
+ proc =
+ (Process.proc "bild" [Text.unpack namespace])
+ { Process.cwd = Just (orchWorkDir cfg),
+ Process.std_out = Process.Inherit,
+ Process.std_err = Process.Inherit,
+ Process.create_group = True
+ }
+
(_, _, _, ph) <- Process.createProcess proc
-
+
-- Register the process handle
updateOrchestratorProcess (orchChatId cfg) (Just ph)
-
+
exitCode <- Process.waitForProcess ph
-
+
-- Clear the process handle
updateOrchestratorProcess (orchChatId cfg) Nothing
-
+
case exitCode of
Exit.ExitSuccess -> pure (Right ())
Exit.ExitFailure code ->
@@ -434,36 +458,38 @@ runBuild cfg = do
-- Returns Right commitHash on approval, Left reason on rejection
runReviewer :: OrchestratorConfig -> IO (Either Text Text)
runReviewer cfg = do
- let proc = (Process.proc "./Omni/Ide/pi-review.sh" [Text.unpack (orchTaskId cfg)])
- { Process.cwd = Just (orchWorkDir cfg),
- Process.std_out = Process.Inherit,
- Process.std_err = Process.Inherit,
- Process.create_group = True
- }
-
+ let proc =
+ (Process.proc "./Omni/Ide/pi-review.sh" [Text.unpack (orchTaskId cfg)])
+ { Process.cwd = Just (orchWorkDir cfg),
+ Process.std_out = Process.Inherit,
+ Process.std_err = Process.Inherit,
+ Process.create_group = True
+ }
+
(_, _, _, ph) <- Process.createProcess proc
-
+
-- Register the process handle
updateOrchestratorProcess (orchChatId cfg) (Just ph)
-
+
-- Wait with timeout
let timeoutMicros = orchReviewerTimeout cfg * 1000000
mExit <- Timeout.timeout timeoutMicros (Process.waitForProcess ph)
-
+
-- Clear the process handle
updateOrchestratorProcess (orchChatId cfg) Nothing
-
+
case mExit of
Nothing -> do
Process.terminateProcess ph
pure (Left "Reviewer timeout")
Just Exit.ExitSuccess -> do
-- Reviewer approved - get commit hash from git
- (_, Just hOut, _, ph') <- Process.createProcess
- (Process.proc "git" ["rev-parse", "--short", "HEAD"])
- { Process.cwd = Just (orchWorkDir cfg),
- Process.std_out = Process.CreatePipe
- }
+ (_, Just hOut, _, ph') <-
+ Process.createProcess
+ (Process.proc "git" ["rev-parse", "--short", "HEAD"])
+ { Process.cwd = Just (orchWorkDir cfg),
+ Process.std_out = Process.CreatePipe
+ }
_ <- Process.waitForProcess ph'
commit <- Text.strip <$> TextIO.hGetContents hOut
pure (Right commit)
@@ -483,19 +509,19 @@ createStatusMessage cfg chatId mThreadId text = do
<> Text.unpack (Types.tgBotToken cfg)
<> "/sendMessage"
body =
- Aeson.object <|
- [ "chat_id" .= chatId,
- "text" .= text
- ]
+ Aeson.object
+ <| [ "chat_id" .= chatId,
+ "text" .= text
+ ]
++ maybe [] (\tid -> ["message_thread_id" .= tid]) mThreadId
-
+
req0 <- HTTP.parseRequest url
let req =
HTTP.setRequestMethod "POST"
<| HTTP.setRequestHeader "Content-Type" ["application/json"]
<| HTTP.setRequestBodyLBS (Aeson.encode body)
<| req0
-
+
putText <| "Orchestrator: Creating status message for chat " <> tshow chatId
result <- try @SomeException (HTTP.httpLBS req)
case result of
@@ -528,13 +554,14 @@ data TelegramResponse = TelegramResponse
deriving (Show, Generic)
instance Aeson.FromJSON TelegramResponse where
- parseJSON = Aeson.withObject "TelegramResponse" <| \v -> do
- ok <- v .: "ok"
- mResult <- v Aeson..:? "result"
- msgId <- case mResult of
- Just res -> res Aeson..:? "message_id"
- Nothing -> pure Nothing
- pure TelegramResponse {tgRespOk = ok, tgRespMessageId = msgId}
+ parseJSON =
+ Aeson.withObject "TelegramResponse" <| \v -> do
+ ok <- v .: "ok"
+ mResult <- v Aeson..:? "result"
+ msgId <- case mResult of
+ Just res -> res Aeson..:? "message_id"
+ Nothing -> pure Nothing
+ pure TelegramResponse {tgRespOk = ok, tgRespMessageId = msgId}
-- | Update an existing status message
updateStatusMessage :: Types.TelegramConfig -> Int -> Int -> Text -> IO ()
@@ -550,14 +577,14 @@ updateStatusMessage cfg chatId messageId text = do
"message_id" .= messageId,
"text" .= text
]
-
+
req0 <- HTTP.parseRequest url
let req =
HTTP.setRequestMethod "POST"
<| HTTP.setRequestHeader "Content-Type" ["application/json"]
<| HTTP.setRequestBodyLBS (Aeson.encode body)
<| req0
-
+
result <- try @SomeException (HTTP.httpLBS req)
case result of
Left err -> putText <| "Edit message failed: " <> tshow err
@@ -568,7 +595,7 @@ updateStatusMessage cfg chatId messageId text = do
-- | Send a new message
sendMessage :: Types.TelegramConfig -> Int -> Maybe Int -> Text -> IO (Maybe Int)
-sendMessage = createStatusMessage -- Same implementation
+sendMessage = createStatusMessage -- Same implementation
-- | Main for testing
main :: IO ()
@@ -578,7 +605,7 @@ main = Test.run test
test :: Test.Tree
test =
Test.group
- "Omni.Agent.Telegram.Orchestrator"
+ "Omni.Agent.Telegram.Developer"
[ Test.unit "formatPhase Starting" <| do
formatPhase "t-123" 3 PhaseStarting
Test.@?= "🔧 t-123: Starting orchestrator...",
diff --git a/Omni/Agent/Telegram/Telegram.hs b/Omni/Agent/Telegram/Telegram.hs
index 9c75b307..616b903d 100644
--- a/Omni/Agent/Telegram/Telegram.hs
+++ b/Omni/Agent/Telegram/Telegram.hs
@@ -99,10 +99,10 @@ import qualified Omni.Agent.Skills as Skills
import qualified Omni.Agent.Subagent as Subagent
import qualified Omni.Agent.Subagent.Jobs as Jobs
import qualified Omni.Agent.Telegram.Actions as Actions
+import qualified Omni.Agent.Telegram.Developer as Developer
import qualified Omni.Agent.Telegram.IncomingQueue as IncomingQueue
import qualified Omni.Agent.Telegram.Media as Media
import qualified Omni.Agent.Telegram.Messages as Messages
-import qualified Omni.Agent.Telegram.Orchestrator as Orchestrator
import qualified Omni.Agent.Telegram.Reminders as Reminders
import qualified Omni.Agent.Telegram.Types as Types
import qualified Omni.Agent.Tools as Tools
@@ -117,11 +117,11 @@ import qualified Omni.Agent.Tools.Outreach as Outreach
import qualified Omni.Agent.Tools.Pdf as Pdf
import qualified Omni.Agent.Tools.Python as Python
import qualified Omni.Agent.Tools.Tasks as Tasks
-import qualified Omni.Task.Core as Task
import qualified Omni.Agent.Tools.Todos as Todos
import qualified Omni.Agent.Tools.WebReader as WebReader
import qualified Omni.Agent.Tools.WebSearch as WebSearch
import qualified Omni.Ava.Trace as Trace
+import qualified Omni.Task.Core as Task
import qualified Omni.Test as Test
import System.Environment (lookupEnv)
import Text.Printf (printf)
@@ -1766,9 +1766,10 @@ handleReadyCommand :: Int -> Maybe Int -> Text -> IO Bool
handleReadyCommand chatId mThreadId cmd
| cmd == "/ready" || cmd == "/ready " || cmd == "ready" || cmd == "show ready" || cmd == "what's ready" = do
readyTasks <- Task.getReadyTasks
- let msg = if null readyTasks
- then "No tasks ready right now."
- else formatReadyTasks (take 10 readyTasks)
+ let msg =
+ if null readyTasks
+ then "No tasks ready right now."
+ else formatReadyTasks (take 10 readyTasks)
_ <- Messages.enqueueImmediate Nothing chatId mThreadId msg (Just "system") Nothing
pure True
| otherwise = pure False
@@ -1778,10 +1779,14 @@ handleReadyCommand chatId mThreadId cmd
"📋 *Ready tasks:*\n\n"
<> Text.intercalate "\n" (map formatTask tasks)
<> "\n\n_Reply with a task ID to start work._"
-
+
formatTask :: Task.Task -> Text
formatTask t =
- "• `" <> Task.taskId t <> "` [" <> tshow (Task.taskPriority t) <> "] "
+ "• `"
+ <> Task.taskId t
+ <> "` ["
+ <> tshow (Task.taskPriority t)
+ <> "] "
<> Text.take 50 (Task.taskTitle t)
<> if Text.length (Task.taskTitle t) > 50 then "..." else ""
@@ -1789,7 +1794,7 @@ handleReadyCommand chatId mThreadId cmd
handleStatusCommand :: Int -> Maybe Int -> Text -> IO Bool
handleStatusCommand chatId mThreadId cmd
| cmd == "/status" || cmd == "/status " || cmd == "status" || cmd == "what are you working on" = do
- mStatus <- Orchestrator.getOrchestratorStatus chatId
+ mStatus <- Developer.getOrchestratorStatus chatId
let msg = case mStatus of
Nothing -> "I'm not working on anything right now. Send me a task ID to start work."
Just status -> formatOrchestratorStatus status
@@ -1797,38 +1802,46 @@ handleStatusCommand chatId mThreadId cmd
pure True
| otherwise = pure False
where
- formatOrchestratorStatus :: Orchestrator.OrchestratorStatus -> Text
+ formatOrchestratorStatus :: Developer.OrchestratorStatus -> Text
formatOrchestratorStatus status =
- "🔧 *Working on " <> Orchestrator.osTaskId status <> "*\n"
- <> Orchestrator.osTaskTitle status <> "\n\n"
- <> "Phase: " <> formatPhaseSimple (Orchestrator.osPhase status) <> "\n"
- <> "Iteration: " <> tshow (Orchestrator.osIteration status) <> "/" <> tshow (Orchestrator.osMaxIterations status)
-
- formatPhaseSimple :: Orchestrator.OrchestratorPhase -> Text
+ "🔧 *Working on "
+ <> Developer.osTaskId status
+ <> "*\n"
+ <> Developer.osTaskTitle status
+ <> "\n\n"
+ <> "Phase: "
+ <> formatPhaseSimple (Developer.osPhase status)
+ <> "\n"
+ <> "Iteration: "
+ <> tshow (Developer.osIteration status)
+ <> "/"
+ <> tshow (Developer.osMaxIterations status)
+
+ formatPhaseSimple :: Developer.OrchestratorPhase -> Text
formatPhaseSimple = \case
- Orchestrator.PhaseStarting -> "Starting"
- Orchestrator.PhaseCoder _ -> "Coder running"
- Orchestrator.PhaseCoderDone _ -> "Coder done"
- Orchestrator.PhaseVerifyBuild _ -> "Verifying build"
- Orchestrator.PhaseBuildPassed _ -> "Build passed"
- Orchestrator.PhaseBuildFailed _ _ -> "Build failed"
- Orchestrator.PhaseReviewer _ -> "Reviewer running"
- Orchestrator.PhaseReviewerApproved -> "Reviewer approved"
- Orchestrator.PhaseReviewerRejected _ _ -> "Reviewer rejected"
- Orchestrator.PhaseCommitting -> "Committing"
- Orchestrator.PhaseComplete _ -> "Complete"
- Orchestrator.PhaseFailed _ -> "Failed"
+ Developer.PhaseStarting -> "Starting"
+ Developer.PhaseCoder _ -> "Coder running"
+ Developer.PhaseCoderDone _ -> "Coder done"
+ Developer.PhaseVerifyBuild _ -> "Verifying build"
+ Developer.PhaseBuildPassed _ -> "Build passed"
+ Developer.PhaseBuildFailed _ _ -> "Build failed"
+ Developer.PhaseReviewer _ -> "Reviewer running"
+ Developer.PhaseReviewerApproved -> "Reviewer approved"
+ Developer.PhaseReviewerRejected _ _ -> "Reviewer rejected"
+ Developer.PhaseCommitting -> "Committing"
+ Developer.PhaseComplete _ -> "Complete"
+ Developer.PhaseFailed _ -> "Failed"
-- | Handle stop/cancel command - stop current orchestrator work
handleStopCommand :: Types.TelegramConfig -> Int -> Maybe Int -> Text -> IO Bool
handleStopCommand tgConfig chatId mThreadId cmd
| cmd == "stop" || cmd == "/stop" || cmd == "cancel" || cmd == "/cancel" || cmd == "stop work" = do
- result <- Orchestrator.stopOrchestrator chatId
+ result <- Developer.stopOrchestrator chatId
case result of
- Orchestrator.StopNotRunning -> do
+ Developer.StopNotRunning -> do
_ <- Messages.enqueueImmediate Nothing chatId mThreadId "I'm not working on anything right now." (Just "system") Nothing
pure True
- Orchestrator.StopSuccess taskId -> do
+ Developer.StopSuccess taskId -> do
let msg = "⛔ Stopped work on " <> taskId <> ". Changes are uncommitted."
keyboard =
Types.InlineKeyboardMarkup
diff --git a/Omni/Agent/Tools/Tasks.hs b/Omni/Agent/Tools/Tasks.hs
index 591167c6..06a65cdb 100644
--- a/Omni/Agent/Tools/Tasks.hs
+++ b/Omni/Agent/Tools/Tasks.hs
@@ -39,7 +39,7 @@ import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Text as Text
-- import Data.Time (getCurrentTime)
import qualified Omni.Agent.Engine as Engine
-import qualified Omni.Agent.Telegram.Orchestrator as Orchestrator
+import qualified Omni.Agent.Telegram.Developer as Developer
import qualified Omni.Agent.Telegram.Types as TgTypes
import qualified Omni.Task.Core as Task
import qualified Omni.Test as Test
@@ -126,10 +126,10 @@ executeWorkOnTask ctx v = do
-- Spawn orchestrator with Telegram updates
putText <| "work_on_task: Spawning orchestrator for " <> tid <> " in chat " <> tshow (ttcChatId ctx)
let orchConfig =
- (Orchestrator.defaultConfig tid (ttcChatId ctx))
- { Orchestrator.orchThreadId = ttcThreadId ctx
+ (Developer.defaultConfig tid (ttcChatId ctx))
+ { Developer.orchThreadId = ttcThreadId ctx
}
- _asyncHandle <- Orchestrator.spawnOrchestrator (ttcTelegramConfig ctx) orchConfig
+ _asyncHandle <- Developer.spawnOrchestrator (ttcTelegramConfig ctx) orchConfig
putText <| "work_on_task: Orchestrator spawned, async handle created"
-- Return immediately - orchestrator runs async
pure
@@ -368,15 +368,17 @@ executeCreateTask v = do
deps = case ctaDiscoveredFrom args of
Just dfid -> [Task.Dependency dfid Task.DiscoveredFrom]
Nothing -> []
- result <- try @SomeException <| Task.createTask
- (ctaTitle args)
- Task.WorkTask
- (ctaParentId args)
- (ctaNamespace args)
- priority
- Nothing -- complexity
- deps
- description
+ result <-
+ try @SomeException
+ <| Task.createTask
+ (ctaTitle args)
+ Task.WorkTask
+ (ctaParentId args)
+ (ctaNamespace args)
+ priority
+ Nothing -- complexity
+ deps
+ description
case result of
Left err ->
pure <| Aeson.object ["error" .= tshow err]