commit 00b3042ed8ede6d5bf8f98c74a299bb118570750
Author: Coder Agent <coder@agents.omni>
Date: Wed Apr 15 22:05:09 2026
feat(agents): replace agentd HTTP calls with local runtime APIs
Rework Omni.Agents.Client to call agentd runtime modules directly.
Migrate Ava web and Telegram orchestrator agent control to Client.
Task-Id: t-802
diff --git a/Omni/Agents/Client.hs b/Omni/Agents/Client.hs
index 409187c3..a30f303d 100644
--- a/Omni/Agents/Client.hs
+++ b/Omni/Agents/Client.hs
@@ -3,19 +3,15 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
--- | HTTP client for the Agentd daemon API.
+-- | Agent lifecycle client backed by the local agentd CLI/runtime modules.
--
--- The web server calls these functions to manage agents via the daemon
--- running at localhost:8400 (configurable via AGENTD_URL env var).
+-- This module intentionally avoids the removed agentd HTTP daemon API.
--
-- : dep aeson
-- : dep bytestring
--- : dep http-conduit
+-- : dep uuid
module Omni.Agents.Client
- ( -- * Configuration
- daemonUrl,
-
- -- * Types
+ ( -- * Types
AgentInfo (..),
AgentStatus (..),
SpawnRequest (..),
@@ -38,18 +34,11 @@ import qualified Control.Monad.Fail as Fail
import Data.Aeson ((.:), (.:?), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Text as Text
-import qualified Network.HTTP.Simple as HTTP
-import qualified System.Environment as Env
-
--- ============================================================================
--- Configuration
--- ============================================================================
-
--- | Get the daemon base URL from env or use default.
-daemonUrl :: IO Text
-daemonUrl = do
- mUrl <- Env.lookupEnv "AGENTD_URL"
- pure (maybe "http://localhost:8400" Text.pack mUrl)
+import qualified Data.Time as Time
+import qualified Data.UUID as UUID
+import qualified Data.UUID.V4 as UUID
+import qualified Omni.Agent.Models as Models
+import qualified Omni.Agentd.Daemon as Daemon
-- ============================================================================
-- Types
@@ -105,8 +94,8 @@ instance Aeson.FromJSON AgentInfo where
AgentInfo
</ (o .: "run_id")
<*> (o .: "status")
- <*> (o .: "prompt")
- <*> (o .: "workspace")
+ <*> (o .:? "prompt" Aeson..!= "")
+ <*> (o .:? "workspace" Aeson..!= "")
<*> (o .:? "pid")
<*> (o .:? "started_at")
<*> (o .:? "completed_at")
@@ -168,115 +157,158 @@ instance Aeson.FromJSON ChatMessage where
<*> (o .: "content")
<*> (o .: "created_at")
+-- ============================================================================
+-- Helpers
+-- ============================================================================
+
+daemonStatusToClient :: Daemon.AgentStatus -> AgentStatus
+daemonStatusToClient = \case
+ Daemon.StatusPending -> StatusPending
+ Daemon.StatusRunning -> StatusRunning
+ Daemon.StatusIdle -> StatusIdle
+ Daemon.StatusCompleted -> StatusCompleted
+ Daemon.StatusFailed -> StatusFailed
+ Daemon.StatusStopped -> StatusStopped
+
+formatTimestamp :: Time.UTCTime -> Text
+formatTimestamp = Text.pack <. Time.formatTime Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S UTC"
+
+persistentToAgentInfo :: Daemon.PersistentAgent -> AgentInfo
+persistentToAgentInfo pa =
+ let cfg = Daemon.paConfig pa
+ mInfo = Daemon.paDbInfo pa
+ in AgentInfo
+ { aiRunId = Daemon.acName cfg,
+ aiStatus = maybe StatusStopped (daemonStatusToClient <. Daemon.aiStatus) mInfo,
+ aiPrompt = fromMaybe "" (Daemon.aiPrompt </ mInfo),
+ aiWorkspace = fromMaybe (Text.pack (Daemon.acCwd cfg)) (Daemon.aiWorkspace </ mInfo),
+ aiPid = Daemon.aiPid =<< mInfo,
+ aiStartedAt = formatTimestamp </ (Daemon.aiStartedAt =<< mInfo),
+ aiCompletedAt = formatTimestamp </ (Daemon.aiCompletedAt =<< mInfo),
+ aiError = Daemon.aiError =<< mInfo,
+ aiSummary = Daemon.aiSummary =<< mInfo,
+ aiCostCents = Daemon.aiCostCents =<< mInfo,
+ aiTitle = Daemon.aiTitle =<< mInfo
+ }
+
+nonEmptyText :: Maybe Text -> Maybe Text
+nonEmptyText = \case
+ Nothing -> Nothing
+ Just txt ->
+ let stripped = Text.strip txt
+ in if Text.null stripped then Nothing else Just stripped
+
+generateAgentName :: IO Text
+generateAgentName = do
+ uuid <- UUID.nextRandom
+ pure ("agent-" <> UUID.toText uuid)
+
+sendPromptWithRetry :: Text -> Text -> Int -> IO (Either Text ())
+sendPromptWithRetry runId prompt retries = do
+ result <- Daemon.sendPersistentAgent Nothing runId prompt
+ case result of
+ Left err
+ | retries > 0,
+ "FIFO not found" `Text.isInfixOf` err -> do
+ threadDelay 200000
+ sendPromptWithRetry runId prompt (retries - 1)
+ Left err -> pure (Left err)
+ Right () -> pure (Right ())
+
-- ============================================================================
-- API Calls
-- ============================================================================
--- | List all agents from the daemon.
+-- | List all persistent agents.
listAgents :: IO (Either Text [AgentInfo])
listAgents = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents"))
- resp <- HTTP.httpJSON req
- pure (HTTP.getResponseBody resp :: [AgentInfo])
+ result <- try @SomeException <| Daemon.listPersistentAgents Nothing
pure <| case result of
Left err -> Left ("Failed to list agents: " <> tshow err)
- Right agents -> Right agents
+ Right agents -> Right (map persistentToAgentInfo agents)
--- | Get a single agent's info.
+-- | Get a single persistent agent's info.
getAgent :: Text -> IO (Either Text AgentInfo)
getAgent runId = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents/" <> runId))
- resp <- HTTP.httpJSON req
- pure (HTTP.getResponseBody resp :: AgentInfo)
+ result <- try @SomeException <| Daemon.getPersistentAgent Nothing runId
pure <| case result of
Left err -> Left ("Failed to get agent: " <> tshow err)
- Right agent -> Right agent
+ Right Nothing -> Left ("Agent not found: " <> runId)
+ Right (Just agent) -> Right (persistentToAgentInfo agent)
--- | Get conversation messages for an agent.
+-- | Get conversation messages for a persistent agent.
getMessages :: Text -> IO (Either Text [ChatMessage])
getMessages runId = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents/" <> runId <> "/messages"))
- resp <- HTTP.httpJSON req
- pure (HTTP.getResponseBody resp :: [ChatMessage])
+ result <- try @SomeException <| Daemon.getPersistentMessages Nothing runId
pure <| case result of
Left err -> Left ("Failed to get messages: " <> tshow err)
- Right msgs -> Right msgs
+ Right values ->
+ let messages = mapMaybe decodeMessage values
+ in Right messages
+ where
+ decodeMessage value =
+ case Aeson.fromJSON value of
+ Aeson.Success msg -> Just msg
+ Aeson.Error _ -> Nothing
--- | Spawn a new agent.
+-- | Create/start a persistent agent and optionally send initial prompt.
spawnAgent :: SpawnRequest -> IO (Either Text SpawnResponse)
spawnAgent spawnReq = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents"))
- let req' =
- HTTP.setRequestBodyJSON spawnReq
- <| HTTP.setRequestMethod "POST" req
- resp <- HTTP.httpJSON req'
- pure (HTTP.getResponseBody resp :: SpawnResponse)
- pure <| case result of
- Left err -> Left ("Failed to spawn agent: " <> tshow err)
- Right resp -> Right resp
+ runId <- maybe generateAgentName pure (nonEmptyText (srName spawnReq))
+ let cfg =
+ Daemon.AgentConfig
+ { Daemon.acName = runId,
+ Daemon.acProvider = fromMaybe "auto" (nonEmptyText (srProvider spawnReq)),
+ Daemon.acModel = fromMaybe Models.defaultModel (nonEmptyText (srModel spawnReq)),
+ Daemon.acCwd = Text.unpack (srCwd spawnReq),
+ Daemon.acThinking = fromMaybe "high" (nonEmptyText (srThinking spawnReq)),
+ Daemon.acExtraArgs = nonEmptyText (srExtraArgs spawnReq),
+ Daemon.acExtraEnv = mempty
+ }
+
+ createResult <- Daemon.createAgent Nothing cfg
+ case createResult of
+ Left err
+ | "Agent already exists:" `Text.isPrefixOf` err -> startAndPrime runId
+ Left err -> pure (Left ("Failed to spawn agent: " <> err))
+ Right _ -> startAndPrime runId
+ where
+ startAndPrime runId = do
+ startResult <- Daemon.startPersistentAgent Nothing runId
+ case startResult of
+ Left err -> pure (Left ("Failed to start agent: " <> err))
+ Right _ -> do
+ sendResult <-
+ case nonEmptyText (srPrompt spawnReq) of
+ Nothing -> pure (Right ())
+ Just prompt -> sendPromptWithRetry runId prompt 10
+ case sendResult of
+ Left err -> pure (Left ("Failed to send initial prompt: " <> err))
+ Right () -> pure (Right (SpawnResponse runId "started"))
--- | Send a message to a running/idle agent.
+-- | Send a message to a running/idle persistent agent.
sendMessage :: Text -> Text -> IO (Either Text ())
sendMessage runId message = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents/" <> runId <> "/send"))
- let body = Aeson.object ["message" .= message]
- req' =
- HTTP.setRequestBodyJSON body
- <| HTTP.setRequestMethod "POST" req
- resp <- HTTP.httpLBS req'
- let code = HTTP.getResponseStatusCode resp
- if code >= 200 && code < 300
- then pure ()
- else panic ("HTTP " <> tshow code)
+ result <- try @SomeException <| Daemon.sendPersistentAgent Nothing runId message
pure <| case result of
Left err -> Left ("Failed to send: " <> tshow err)
- Right () -> Right ()
+ Right (Left err) -> Left err
+ Right (Right ()) -> Right ()
--- | Stop a running agent.
+-- | Stop a persistent agent.
stopAgent :: Text -> IO (Either Text ())
stopAgent runId = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents/" <> runId <> "/stop"))
- let req' = HTTP.setRequestMethod "POST" req
- resp <- HTTP.httpLBS req'
- let code = HTTP.getResponseStatusCode resp
- if code >= 200 && code < 300
- then pure ()
- else panic ("HTTP " <> tshow code)
+ result <- try @SomeException <| Daemon.stopPersistentAgent Nothing runId
pure <| case result of
Left err -> Left ("Failed to stop: " <> tshow err)
- Right () -> Right ()
+ Right (Left err) -> Left err
+ Right (Right _) -> Right ()
--- | Remove an agent and its config/state.
+-- | Remove a persistent agent and archive its DB history.
removeAgent :: Text -> IO (Either Text ())
removeAgent runId = do
- url <- daemonUrl
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (url <> "/agents/" <> runId))
- let req' = HTTP.setRequestMethod "DELETE" req
- resp <- HTTP.httpLBS req'
- let code = HTTP.getResponseStatusCode resp
- if code >= 200 && code < 300
- then pure ()
- else panic ("HTTP " <> tshow code)
+ result <- try @SomeException <| Daemon.removePersistentAgent Nothing runId
pure <| case result of
Left err -> Left ("Failed to remove agent: " <> tshow err)
- Right () -> Right ()
+ Right (Left err) -> Left err
+ Right (Right ()) -> Right ()
diff --git a/Omni/Ava/Telegram/Developer.hs b/Omni/Ava/Telegram/Developer.hs
index ffdd279b..82f5bc03 100644
--- a/Omni/Ava/Telegram/Developer.hs
+++ b/Omni/Ava/Telegram/Developer.hs
@@ -51,17 +51,19 @@ import qualified Control.Concurrent.STM as STM
import qualified Control.Exception as Exception
import Data.Aeson ((.:), (.=))
import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Key as Key
+import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.IORef as IORef
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.Agents.Client as Client
import qualified Omni.Ava.Telegram.Types as Types
import qualified Omni.Task.Core as Task
import qualified Omni.Test as Test
import qualified System.Directory as Dir
-import qualified System.Environment as Env
import System.IO (hFlush)
import System.IO.Unsafe (unsafePerformIO)
@@ -205,17 +207,6 @@ formatPhase tid _maxIter = \case
PhaseFailed err ->
"❌ " <> tid <> " failed: " <> Text.take 200 err
--- | Agentd spawn response
-newtype AgentdSpawnResponse = AgentdSpawnResponse
- { asrRunId :: Text
- }
- deriving (Show, Eq, Generic)
-
-instance Aeson.FromJSON AgentdSpawnResponse where
- parseJSON =
- Aeson.withObject "AgentdSpawnResponse" <| \v ->
- AgentdSpawnResponse </ (v Aeson..: "run_id")
-
-- | Agent status info from agentd
data AgentdInfo = AgentdInfo
{ adiRunId :: Text,
@@ -236,66 +227,76 @@ instance Aeson.FromJSON AgentdInfo where
<*> (v Aeson..:? "error")
<*> (v Aeson..:? "cost_cents")
--- | Base URL for agentd HTTP API
+-- | Legacy base-url hook retained for call-site compatibility.
agentdBaseUrl :: IO Text
-agentdBaseUrl = do
- mUrl <- Env.lookupEnv "AGENTD_URL"
- pure <| maybe "http://127.0.0.1:8400" Text.pack mUrl
+agentdBaseUrl = pure "cli://agentd"
--- | Webhook URL for agentd -> Ava notifications
+-- | Webhook URL is unused in CLI mode.
agentdWebhookUrl :: IO (Maybe Text)
-agentdWebhookUrl = do
- mUrl <- Env.lookupEnv "AGENTD_WEBHOOK_URL"
- pure <| Just (maybe "http://127.0.0.1:8079/agents/webhook" Text.pack mUrl)
+agentdWebhookUrl = pure Nothing
+
+lookupPayloadText :: Text -> Aeson.Object -> Maybe Text
+lookupPayloadText key obj =
+ case KeyMap.lookup (Key.fromText key) obj of
+ Just (Aeson.String txt) -> Just txt
+ _ -> Nothing
+
+nonEmptyPayloadText :: Maybe Text -> Maybe Text
+nonEmptyPayloadText = \case
+ Nothing -> Nothing
+ Just txt ->
+ let stripped = Text.strip txt
+ in if Text.null stripped then Nothing else Just stripped
agentdSpawnAgent :: Text -> Aeson.Value -> IO (Either Text Text)
-agentdSpawnAgent baseUrl payload = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (baseUrl <> "/agents"))
- let req' =
- HTTP.setRequestBodyJSON payload
- <| HTTP.setRequestMethod "POST" req
- resp <- HTTP.httpJSON req' :: IO (HTTP.Response AgentdSpawnResponse)
- pure <| asrRunId (HTTP.getResponseBody resp)
- case result of
- Left err -> pure (Left (tshow err))
- Right runId -> pure (Right runId)
+agentdSpawnAgent _ payload =
+ case payload of
+ Aeson.Object obj -> do
+ let spawnReq =
+ Client.SpawnRequest
+ { Client.srName = nonEmptyPayloadText (lookupPayloadText "name" obj),
+ Client.srProvider = nonEmptyPayloadText (lookupPayloadText "provider" obj),
+ Client.srModel = nonEmptyPayloadText (lookupPayloadText "model" obj),
+ Client.srCwd = fromMaybe "/home/ben/omni/ava" (lookupPayloadText "cwd" obj),
+ Client.srThinking = nonEmptyPayloadText (lookupPayloadText "thinking" obj),
+ Client.srExtraArgs = nonEmptyPayloadText (lookupPayloadText "extra_args" obj),
+ Client.srPrompt = nonEmptyPayloadText (lookupPayloadText "prompt" obj)
+ }
+ result <- Client.spawnAgent spawnReq
+ pure <| case result of
+ Left err -> Left err
+ Right resp -> Right (Client.spRunId resp)
+ _ -> pure (Left "Invalid spawn payload")
+
+agentdStatusText :: Client.AgentStatus -> Text
+agentdStatusText = \case
+ Client.StatusPending -> "pending"
+ Client.StatusRunning -> "running"
+ Client.StatusIdle -> "idle"
+ Client.StatusCompleted -> "completed"
+ Client.StatusFailed -> "failed"
+ Client.StatusStopped -> "stopped"
agentdGetInfo :: Text -> Text -> IO (Either Text AgentdInfo)
-agentdGetInfo baseUrl runId = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (baseUrl <> "/agents/" <> runId))
- resp <- HTTP.httpJSON req :: IO (HTTP.Response AgentdInfo)
- pure <| HTTP.getResponseBody resp
- case result of
- Left err -> pure (Left (tshow err))
- Right info -> pure (Right info)
+agentdGetInfo _ runId = do
+ result <- Client.getAgent runId
+ pure <| case result of
+ Left err -> Left err
+ Right info ->
+ Right
+ AgentdInfo
+ { adiRunId = Client.aiRunId info,
+ adiStatus = agentdStatusText (Client.aiStatus info),
+ adiSummary = Client.aiSummary info,
+ adiError = Client.aiError info,
+ adiCostCents = Client.aiCostCents info
+ }
agentdStop :: Text -> Text -> IO (Either Text ())
-agentdStop baseUrl runId = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (baseUrl <> "/agents/" <> runId <> "/stop"))
- let req' = HTTP.setRequestMethod "POST" req
- _ <- HTTP.httpNoBody req'
- pure ()
- case result of
- Left err -> pure (Left (tshow err))
- Right () -> pure (Right ())
+agentdStop _ = Client.stopAgent
agentdDelete :: Text -> Text -> IO (Either Text ())
-agentdDelete baseUrl runId = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (baseUrl <> "/agents/" <> runId))
- let req' = HTTP.setRequestMethod "DELETE" req
- _ <- HTTP.httpNoBody req'
- pure ()
- case result of
- Left err -> pure (Left (tshow err))
- Right () -> pure (Right ())
+agentdDelete _ = Client.removeAgent
cleanupAgentdRun :: Text -> Text -> IO ()
cleanupAgentdRun baseUrl runId = do
diff --git a/Omni/Ava/Web.hs b/Omni/Ava/Web.hs
index c056ab9b..d651bb71 100644
--- a/Omni/Ava/Web.hs
+++ b/Omni/Ava/Web.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE NoImplicitPrelude #-}
@@ -43,16 +44,15 @@ import qualified Data.Text as Text
import qualified Data.Text.Encoding as TE
import qualified Database.SQLite.Simple as SQL
import qualified Lucid
-import qualified Network.HTTP.Simple as HTTP
import qualified Network.Wai.Handler.Warp as Warp
import qualified Omni.Agent.Trace.Storage as Trace
+import qualified Omni.Agents.Client as Client
import qualified Omni.Ava.Agent.Backend as Agent
import qualified Omni.Task.Web.Handlers as TaskWeb
import Omni.Task.Web.Pages ()
import Omni.Task.Web.Partials ()
import Servant
import qualified Servant.HTML.Lucid as Lucid
-import qualified System.Environment as Env
main :: IO ()
main = putText "Use Omni.Ava for the main entry point"
@@ -186,66 +186,82 @@ instance Aeson.FromJSON AgentdSpawnResponse where
defaultAgentPrompt :: Text
defaultAgentPrompt = "Initialize and wait for tasks."
--- | Agentd base URL (override with AGENTD_URL)
+-- | Legacy base-url hook retained for call-site compatibility.
agentdBaseUrl :: IO Text
-agentdBaseUrl = do
- mUrl <- Env.lookupEnv "AGENTD_URL"
- pure <| maybe "http://127.0.0.1:8400" Text.pack mUrl
+agentdBaseUrl = pure "cli://agentd"
agentdWebhookUrl :: Text
-agentdWebhookUrl = "http://127.0.0.1:8079/agents/webhook"
+agentdWebhookUrl = "cli://agentd-webhook-disabled"
+
+lookupPayloadText :: Text -> Aeson.Object -> Maybe Text
+lookupPayloadText key obj =
+ case KeyMap.lookup (Key.fromText key) obj of
+ Just (Aeson.String txt) -> Just txt
+ _ -> Nothing
+
+nonEmptyPayloadText :: Maybe Text -> Maybe Text
+nonEmptyPayloadText = \case
+ Nothing -> Nothing
+ Just txt ->
+ let stripped = Text.strip txt
+ in if Text.null stripped then Nothing else Just stripped
+
+parseAgentPathWithTail :: Text -> Text -> Maybe Text
+parseAgentPathWithTail tailSegment path =
+ case Text.splitOn "/" (Text.strip path) of
+ ["", "agents", rid, tailValue]
+ | tailValue == tailSegment,
+ not (Text.null rid) ->
+ Just rid
+ _ -> Nothing
+
+parseAgentPath :: Text -> Maybe Text
+parseAgentPath path =
+ case Text.splitOn "/" (Text.strip path) of
+ ["", "agents", rid]
+ | not (Text.null rid) -> Just rid
+ _ -> Nothing
agentdSpawnAgent :: Text -> Aeson.Value -> IO (Either Text AgentdSpawnResponse)
-agentdSpawnAgent base payload = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (base <> "/agents"))
- let req' =
- HTTP.setRequestBodyJSON payload
- <| HTTP.setRequestMethod "POST" req
- resp <- HTTP.httpJSON req'
- pure (HTTP.getResponseBody resp :: AgentdSpawnResponse)
- case result of
- Left err -> pure (Left (tshow err))
- Right resp -> pure (Right resp)
+agentdSpawnAgent _ payload =
+ case payload of
+ Aeson.Object obj -> do
+ let spawnReq =
+ Client.SpawnRequest
+ { Client.srName = nonEmptyPayloadText (lookupPayloadText "name" obj),
+ Client.srProvider = nonEmptyPayloadText (lookupPayloadText "provider" obj),
+ Client.srModel = nonEmptyPayloadText (lookupPayloadText "model" obj),
+ Client.srCwd = fromMaybe "/home/ben/omni/live" (lookupPayloadText "cwd" obj),
+ Client.srThinking = nonEmptyPayloadText (lookupPayloadText "thinking" obj),
+ Client.srExtraArgs = nonEmptyPayloadText (lookupPayloadText "extra_args" obj),
+ Client.srPrompt = nonEmptyPayloadText (lookupPayloadText "prompt" obj)
+ }
+ result <- Client.spawnAgent spawnReq
+ pure <| case result of
+ Left err -> Left err
+ Right resp -> Right (AgentdSpawnResponse (Client.spRunId resp))
+ _ -> pure (Left "Invalid spawn payload")
agentdPostJson :: Text -> Text -> Aeson.Value -> IO (Either Text ())
-agentdPostJson base path payload = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (base <> path))
- let req' =
- HTTP.setRequestBodyJSON payload
- <| HTTP.setRequestMethod "POST" req
- _ <- HTTP.httpNoBody req'
- pure ()
- case result of
- Left err -> pure (Left (tshow err))
- Right () -> pure (Right ())
+agentdPostJson _ path payload =
+ case (parseAgentPathWithTail "send" path, payload) of
+ (Just runId, Aeson.Object obj) ->
+ case nonEmptyPayloadText (lookupPayloadText "message" obj) of
+ Nothing -> pure (Left "Missing message")
+ Just msg -> Client.sendMessage runId msg
+ _ -> pure (Left ("Unsupported agentd CLI route: " <> path))
agentdPostEmpty :: Text -> Text -> IO (Either Text ())
-agentdPostEmpty base path = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (base <> path))
- let req' = HTTP.setRequestMethod "POST" req
- _ <- HTTP.httpNoBody req'
- pure ()
- case result of
- Left err -> pure (Left (tshow err))
- Right () -> pure (Right ())
+agentdPostEmpty _ path =
+ case parseAgentPathWithTail "stop" path of
+ Just runId -> Client.stopAgent runId
+ Nothing -> pure (Left ("Unsupported agentd CLI route: " <> path))
agentdDelete :: Text -> Text -> IO (Either Text ())
-agentdDelete base path = do
- result <-
- try @SomeException <| do
- req <- HTTP.parseRequest (Text.unpack (base <> path))
- let req' = HTTP.setRequestMethod "DELETE" req
- _ <- HTTP.httpNoBody req'
- pure ()
- case result of
- Left err -> pure (Left (tshow err))
- Right () -> pure (Right ())
+agentdDelete _ path =
+ case parseAgentPath path of
+ Just runId -> Client.removeAgent runId
+ Nothing -> pure (Left ("Unsupported agentd CLI route: " <> path))
-- | Webhook payload from agents
data AgentWebhookPayload = AgentWebhookPayload