← Back to task

Commit f3779cd9

commit f3779cd9a556defd5ce8c0bd0c92865437e9dba8
Author: Ben Sima <ben@bensima.com>
Date:   Wed Dec 31 21:31:24 2025

    Integrate Telegram Orchestrator with work_on_task tool
    
    - Modified workOnTaskTool to take TaskToolContext with TelegramConfig, chatId, threadId
    - Updated Telegram.hs to pass context to the tool
    - Orchestrator now spawns as async thread and sends progress via Telegram
    
    When work_on_task is called, it:
    1. Validates the task exists and is workable
    2. Spawns the orchestrator async (doesn't block chat)
    3. Returns immediately with confirmation
    4. Orchestrator posts status updates as it runs
    
    Task-Id: t-298.2

diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs
index 94533c8a..a08eb530 100644
--- a/Omni/Agent/Telegram.hs
+++ b/Omni/Agent/Telegram.hs
@@ -1322,7 +1322,14 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe
           <> [AvaLogs.searchChatHistoryTool]
       taskTools =
         if isBenAuthorized userName
-          then [Tasks.workOnTaskTool, Tasks.listReadyTasksTool]
+          then
+            let taskCtx =
+                  Tasks.TaskToolContext
+                    { Tasks.ttcTelegramConfig = tgConfig,
+                      Tasks.ttcChatId = chatId,
+                      Tasks.ttcThreadId = Types.tmThreadId msg
+                    }
+             in [Tasks.workOnTaskTool taskCtx, Tasks.listReadyTasksTool]
           else []
       tools = memoryTools <> searchTools <> webReaderTools <> pdfTools <> notesTools <> calendarTools <> todoTools <> messageTools <> hledgerTools <> emailTools <> pythonTools <> httpTools <> outreachTools <> feedbackTools <> fileTools <> skillsTools <> subagentToolList <> auditLogTools <> taskTools
 
diff --git a/Omni/Agent/Tools/Tasks.hs b/Omni/Agent/Tools/Tasks.hs
index c84a7e23..7faf3f15 100644
--- a/Omni/Agent/Tools/Tasks.hs
+++ b/Omni/Agent/Tools/Tasks.hs
@@ -5,16 +5,20 @@
 -- | Task tools for the coding orchestrator.
 --
 -- Allows Ava to work on tasks from the task database by spawning
--- the pi-orchestrate coder/reviewer loop.
+-- the pi-orchestrate coder/reviewer loop with Telegram status updates.
 --
 -- : out omni-agent-tools-tasks
 -- : dep aeson
+-- : dep async
 -- : dep process
 module Omni.Agent.Tools.Tasks
   ( -- * Tools
     workOnTaskTool,
     listReadyTasksTool,
 
+    -- * Context for tools
+    TaskToolContext (..),
+
     -- * Testing
     main,
     test,
@@ -22,15 +26,22 @@ module Omni.Agent.Tools.Tasks
 where
 
 import Alpha
-import qualified Control.Concurrent as Concurrent
 import Data.Aeson ((.:), (.=))
 import qualified Data.Aeson as Aeson
 import qualified Data.Text as Text
 import qualified Omni.Agent.Engine as Engine
+import qualified Omni.Agent.Telegram.Orchestrator as Orchestrator
+import qualified Omni.Agent.Telegram.Types as TgTypes
 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
+
+-- | Context needed for task tools to send Telegram updates
+data TaskToolContext = TaskToolContext
+  { ttcTelegramConfig :: TgTypes.TelegramConfig,
+    ttcChatId :: Int,
+    ttcThreadId :: Maybe Int
+  }
+  deriving (Show)
 
 -- | Arguments for work_on_task tool
 newtype WorkOnTaskArgs = WorkOnTaskArgs
@@ -53,14 +64,16 @@ test =
     [ Test.unit "placeholder" <| pure ()
     ]
 
--- | Tool to start working on a task
-workOnTaskTool :: Engine.Tool
-workOnTaskTool =
+-- | Tool to start working on a task.
+-- Takes a context so it can send Telegram status updates.
+workOnTaskTool :: TaskToolContext -> Engine.Tool
+workOnTaskTool ctx =
   Engine.Tool
     { Engine.toolName = "work_on_task",
       Engine.toolDescription =
         "Start working on a coding task. This spawns the coder/reviewer loop "
           <> "which will make changes, review them, and commit if approved. "
+          <> "Progress updates will be posted to the chat. "
           <> "Use this when the user asks you to work on, implement, or fix a task. "
           <> "The task ID should be in format 't-123' or 't-280.2.1'.",
       Engine.toolJsonSchema =
@@ -76,11 +89,11 @@ workOnTaskTool =
                 ],
             "required" .= (["task_id"] :: [Text])
           ],
-      Engine.toolExecute = executeWorkOnTask
+      Engine.toolExecute = executeWorkOnTask ctx
     }
 
-executeWorkOnTask :: Aeson.Value -> IO Aeson.Value
-executeWorkOnTask v = do
+executeWorkOnTask :: TaskToolContext -> Aeson.Value -> IO Aeson.Value
+executeWorkOnTask ctx v = do
   case Aeson.fromJSON v of
     Aeson.Error e ->
       pure <| Aeson.object ["error" .= Text.pack e]
@@ -101,55 +114,20 @@ executeWorkOnTask v = do
               if status `notElem` [Task.Open, Task.InProgress, Task.Draft]
                 then pure <| Aeson.object ["error" .= ("Task " <> tid <> " is not in a workable state (status: " <> tshow status <> ")")]
                 else do
-                  -- Spawn orchestrator and check for early failures
-                  spawnResult <- spawnOrchestrator tid
-                  case spawnResult of
-                    Left err ->
-                      pure <| Aeson.object ["error" .= err]
-                    Right () ->
-                      pure
-                        <| Aeson.object
-                          [ "success" .= True,
-                            "task_id" .= tid,
-                            "title" .= Task.taskTitle task,
-                            "message" .= ("Started coder/reviewer loop for " <> tid <> ": " <> Task.taskTitle task)
-                          ]
-
--- | Spawn the orchestrator process and check for early failures.
--- Returns Left with error message if spawn fails or process exits immediately.
--- Returns Right () if process is still running after startup delay.
-spawnOrchestrator :: Text -> IO (Either Text ())
-spawnOrchestrator tid = do
-  let scriptPath = "/home/ben/omni/ava/Omni/Ide/pi-orchestrate.sh"
-      createProc =
-        (Process.proc scriptPath [Text.unpack tid])
-          { Process.std_out = Process.NoStream,
-            Process.std_err = Process.NoStream,
-            -- Detach from controlling terminal so it keeps running
-            Process.new_session = True,
-            Process.cwd = Just "/home/ben/omni/ava"
-          }
-
-  -- Try to create the process
-  result <- try @SomeException (Process.createProcess createProc)
-  case result of
-    Left err ->
-      pure <| Left ("Failed to spawn orchestrator: " <> tshow err)
-    Right (_, _, _, ph) -> do
-      -- Wait 500ms to catch immediate failures (missing script, permission errors, etc)
-      Concurrent.threadDelay 500000
-
-      -- Check if process exited early
-      maybeExit <- Process.getProcessExitCode ph
-      case maybeExit of
-        Nothing ->
-          -- Process still running - success!
-          pure (Right ())
-        Just Exit.ExitSuccess ->
-          -- Exited immediately with success - suspicious but ok
-          pure (Right ())
-        Just (Exit.ExitFailure code) ->
-          pure <| Left ("Orchestrator failed to start (exit code " <> tshow code <> ")")
+                  -- Spawn orchestrator with Telegram updates
+                  let orchConfig =
+                        (Orchestrator.defaultConfig tid (ttcChatId ctx))
+                          { Orchestrator.orchThreadId = ttcThreadId ctx
+                          }
+                  _ <- Orchestrator.spawnOrchestrator (ttcTelegramConfig ctx) orchConfig
+                  -- Return immediately - orchestrator runs async
+                  pure
+                    <| Aeson.object
+                      [ "success" .= True,
+                        "task_id" .= tid,
+                        "title" .= Task.taskTitle task,
+                        "message" .= ("Started coder/reviewer loop for " <> tid <> ": " <> Task.taskTitle task <> ". Progress updates will appear in chat.")
+                      ]
 
 -- | Tool to list tasks ready for work
 listReadyTasksTool :: Engine.Tool