summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-01 18:59:04 -0500
committerBen Sima <ben@bensima.com>2025-12-01 18:59:04 -0500
commit11ee0b44397ff5f58a11a105883c07a39d49bfa3 (patch)
tree7f29283b0ab4808e8b1d9fb5a5a0500a360e392d /Omni
parentd78a9a5038fb95f4035812b4ef5b0c25036aae4a (diff)
Fix timeline partial to include cost/token metrics and controls
The HTMX-refreshed AgentEventsPartial was missing: - Cost/token summary in header - Live toggle button - Autoscroll toggle button - Comment form Now matches the full page renderUnifiedTimeline output.
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Agent/Worker.hs43
-rw-r--r--Omni/Jr/Web/Handlers.hs2
-rw-r--r--Omni/Jr/Web/Partials.hs33
-rw-r--r--Omni/Jr/Web/Types.hs2
4 files changed, 60 insertions, 20 deletions
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index 8adb7c2..a8bbfc9 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -107,16 +107,20 @@ processTask worker task = do
activityId <- TaskCore.logActivityWithMetrics tid TaskCore.Running Nothing Nothing (Just startTime) Nothing Nothing Nothing
say "[worker] Starting engine..."
- (exitCode, output, costCents) <- runWithEngine worker repo task
+ engineResult <- runWithEngine worker repo task
endTime <- Data.Time.getCurrentTime
- say ("[worker] Agent exited with: " <> tshow exitCode)
-- Update the activity record with metrics (convert Double to Int by rounding)
+ let costCents = case engineResult of
+ EngineSuccess _ c -> c
+ EngineGuardrailViolation _ c -> c
+ EngineError _ c -> c
TaskCore.updateActivityMetrics activityId Nothing (Just endTime) (Just (round costCents)) Nothing
- case exitCode of
- Exit.ExitSuccess -> do
+ case engineResult of
+ EngineSuccess output _ -> do
+ say "[worker] Agent completed successfully"
TaskCore.logActivity tid TaskCore.Reviewing Nothing
say "[worker] Running formatters..."
_ <- runFormatters repo
@@ -170,9 +174,18 @@ processTask worker task = do
TaskCore.updateTaskStatusWithActor tid TaskCore.Review [] TaskCore.Junior
say ("[worker] ✓ Task " <> tid <> " -> Review")
unless quiet <| AgentLog.update (\s -> s {AgentLog.statusTask = Nothing})
- Exit.ExitFailure code -> do
- say ("[worker] Engine failed with code " <> tshow code)
- TaskCore.logActivity tid TaskCore.Failed (Just (toMetadata [("exit_code", tshow code)]))
+ EngineGuardrailViolation errMsg _ -> do
+ say ("[worker] Guardrail violation: " <> errMsg)
+ TaskCore.logActivity tid TaskCore.Failed (Just (toMetadata [("reason", "guardrail_violation")]))
+ -- Add comment with guardrail details
+ _ <- TaskCore.addComment tid errMsg TaskCore.Junior
+ -- Set to NeedsHelp so human can review
+ TaskCore.updateTaskStatusWithActor tid TaskCore.NeedsHelp [] TaskCore.Junior
+ say ("[worker] Task " <> tid <> " -> NeedsHelp (guardrail violation)")
+ unless quiet <| AgentLog.update (\s -> s {AgentLog.statusTask = Nothing})
+ EngineError errMsg _ -> do
+ say ("[worker] Engine error: " <> errMsg)
+ TaskCore.logActivity tid TaskCore.Failed (Just (toMetadata [("reason", "engine_error")]))
-- Don't set back to Open here - leave in InProgress for debugging
say "[worker] Task left in InProgress (engine failure)"
@@ -211,9 +224,14 @@ tryCommit repo msg = do
Exit.ExitFailure _ -> pure <| CommitFailed (Text.pack commitErr)
Exit.ExitFailure c -> pure <| CommitFailed ("git diff failed with code " <> tshow c)
+data EngineResult
+ = EngineSuccess Text Double -- output, cost
+ | EngineGuardrailViolation Text Double -- error message, cost
+ | EngineError Text Double -- error message, cost
+
-- | Run task using native Engine
--- Returns (ExitCode, output text, cost in cents)
-runWithEngine :: Core.Worker -> FilePath -> TaskCore.Task -> IO (Exit.ExitCode, Text, Double)
+-- Returns engine result with output/error and cost
+runWithEngine :: Core.Worker -> FilePath -> TaskCore.Task -> IO EngineResult
runWithEngine worker repo task = do
-- Read API key from environment
maybeApiKey <- Env.lookupEnv "OPENROUTER_API_KEY"
@@ -338,10 +356,13 @@ runWithEngine worker repo task = do
totalCost <- readIORef totalCostRef
case result of
- Left err -> pure (Exit.ExitFailure 1, "Engine error: " <> err, totalCost)
+ Left err ->
+ if "Guardrail: " `Text.isPrefixOf` err
+ then pure (EngineGuardrailViolation err totalCost)
+ else pure (EngineError ("Engine error: " <> err) totalCost)
Right agentResult -> do
let output = Engine.resultFinalMessage agentResult
- pure (Exit.ExitSuccess, output, totalCost)
+ pure (EngineSuccess output totalCost)
-- | Build the base prompt for the agent
buildBasePrompt :: TaskCore.Task -> Text -> FilePath -> Text
diff --git a/Omni/Jr/Web/Handlers.hs b/Omni/Jr/Web/Handlers.hs
index 463c9f7..da8a2e6 100644
--- a/Omni/Jr/Web/Handlers.hs
+++ b/Omni/Jr/Web/Handlers.hs
@@ -435,7 +435,7 @@ server =
let isInProgress = case TaskCore.findTask tid tasks of
Nothing -> False
Just task -> TaskCore.taskStatus task == TaskCore.InProgress
- pure (AgentEventsPartial events isInProgress now)
+ pure (AgentEventsPartial tid events isInProgress now)
taskEventsStreamHandler :: Text -> Servant.Handler (SourceIO ByteString)
taskEventsStreamHandler tid = do
diff --git a/Omni/Jr/Web/Partials.hs b/Omni/Jr/Web/Partials.hs
index 79c997e..9c0e870 100644
--- a/Omni/Jr/Web/Partials.hs
+++ b/Omni/Jr/Web/Partials.hs
@@ -15,8 +15,15 @@ import qualified Lucid
import qualified Lucid.Base as Lucid
import Numeric (showFFloat)
import Omni.Jr.Web.Components
- ( priorityBadgeWithForm,
+ ( aggregateCostMetrics,
+ commentForm,
+ formatCostHeader,
+ formatTokensHeader,
+ metaSep,
+ priorityBadgeWithForm,
+ renderAutoscrollToggle,
renderListGroupItem,
+ renderLiveToggle,
renderMarkdown,
renderRelativeTimestamp,
renderTimelineEvent,
@@ -235,13 +242,25 @@ instance Lucid.ToHtml DescriptionEditPartial where
instance Lucid.ToHtml AgentEventsPartial where
toHtmlRaw = Lucid.toHtml
- toHtml (AgentEventsPartial events isInProgress now) = do
+ toHtml (AgentEventsPartial tid events isInProgress now) = do
+ let nonCostEvents = filter (\e -> TaskCore.storedEventType e /= "Cost") events
+ eventCount = length nonCostEvents
+ (totalCents, totalTokens) = aggregateCostMetrics events
Lucid.h3_ <| do
- Lucid.toHtml ("Timeline (" <> tshow (length events) <> ")")
- when isInProgress <| Lucid.span_ [Lucid.class_ "timeline-live"] " LIVE"
- if null events
+ Lucid.toHtml ("Timeline (" <> tshow eventCount <> ")")
+ when (totalCents > 0 || totalTokens > 0) <| do
+ Lucid.span_ [Lucid.class_ "timeline-cost-summary"] <| do
+ metaSep
+ when (totalCents > 0) <| Lucid.toHtml (formatCostHeader totalCents)
+ when (totalCents > 0 && totalTokens > 0) <| metaSep
+ when (totalTokens > 0) <| Lucid.toHtml (formatTokensHeader totalTokens <> " tokens")
+ when isInProgress <| do
+ renderLiveToggle
+ renderAutoscrollToggle
+ if null nonCostEvents
then Lucid.p_ [Lucid.class_ "empty-msg"] "No activity yet."
else do
Lucid.div_ [Lucid.class_ "timeline-events"] <| do
- traverse_ (renderTimelineEvent now) events
- timelineScrollScript
+ traverse_ (renderTimelineEvent now) nonCostEvents
+ when isInProgress <| timelineScrollScript
+ commentForm tid
diff --git a/Omni/Jr/Web/Types.hs b/Omni/Jr/Web/Types.hs
index c463bfa..025f3a6 100644
--- a/Omni/Jr/Web/Types.hs
+++ b/Omni/Jr/Web/Types.hs
@@ -287,7 +287,7 @@ newtype TaskListPartial = TaskListPartial [TaskCore.Task]
data TaskMetricsPartial = TaskMetricsPartial Text [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) UTCTime
-data AgentEventsPartial = AgentEventsPartial [TaskCore.StoredEvent] Bool UTCTime
+data AgentEventsPartial = AgentEventsPartial Text [TaskCore.StoredEvent] Bool UTCTime
data DescriptionViewPartial = DescriptionViewPartial Text Text Bool