diff options
| author | Ben Sima <ben@bensima.com> | 2025-11-30 21:30:00 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-30 21:30:00 -0500 |
| commit | 9fa7697cd979eaa15a2479819463c3bdd86cc99a (patch) | |
| tree | 0eee4aebe8f99608e1ff3f831797dd0214fe4ed0 /Omni/Agent/Event.hs | |
| parent | 194173619e0e1940284f4d4fa3de49f5197636c1 (diff) | |
Add agent observability: event logging and storage
- Add Omni/Agent/Event.hs with AgentEvent types
- Add agent_events table schema and CRUD functions to Core.hs
- Add new callbacks to Engine.hs: onAssistant, onToolResult, onComplete, onError
- Wire event logging into Worker.hs with session tracking
Events are now persisted to SQLite for each agent work session,
enabling visibility into agent reasoning and tool usage.
Task-Id: t-197.1
Task-Id: t-197.2
Task-Id: t-197.3
Diffstat (limited to 'Omni/Agent/Event.hs')
| -rw-r--r-- | Omni/Agent/Event.hs | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/Omni/Agent/Event.hs b/Omni/Agent/Event.hs new file mode 100644 index 0000000..2b40077 --- /dev/null +++ b/Omni/Agent/Event.hs @@ -0,0 +1,180 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Agent Event types for observability and streaming. +-- +-- Captures all events during agent execution for logging, +-- streaming to web UI, and future interactive chat. +module Omni.Agent.Event + ( AgentEvent (..), + EventType (..), + eventToJSON, + eventFromJSON, + formatEventForTerminal, + ) +where + +import Alpha +import Data.Aeson ((.=)) +import qualified Data.Aeson as Aeson +import qualified Data.Text as Text +import Data.Time (UTCTime, defaultTimeLocale, formatTime) + +-- | Types of agent events +data EventType + = Assistant -- LLM text response + | ToolCall -- Tool invocation with arguments + | ToolResult -- Tool execution result + | UserMessage -- For future interactive chat + | Cost -- Token usage and cost info + | Error -- Failures and errors + | Complete -- Session ended successfully + deriving (Show, Eq, Read) + +-- | A single agent event with timestamp and content +data AgentEvent = AgentEvent + { eventType :: EventType, + eventTimestamp :: UTCTime, + eventContent :: Aeson.Value + } + deriving (Show, Eq) + +-- | Convert event to JSON for storage/streaming +eventToJSON :: AgentEvent -> Aeson.Value +eventToJSON e = + Aeson.object + [ "type" .= show (eventType e), + "timestamp" .= eventTimestamp e, + "content" .= eventContent e + ] + +-- | Parse event from JSON +eventFromJSON :: Aeson.Value -> Maybe AgentEvent +eventFromJSON v = do + obj <- case v of + Aeson.Object o -> Just o + _ -> Nothing + typeStr <- case Aeson.lookup "type" (Aeson.toList obj) of + Just (Aeson.String t) -> Just (Text.unpack t) + _ -> Nothing + eventT <- readMaybe typeStr + ts <- case Aeson.lookup "timestamp" (Aeson.toList obj) of + Just t -> Aeson.parseMaybe Aeson.parseJSON t + _ -> Nothing + content <- Aeson.lookup "content" (Aeson.toList obj) + pure + AgentEvent + { eventType = eventT, + eventTimestamp = ts, + eventContent = content + } + where + Aeson.lookup k pairs = snd </ find (\(k', _) -> k' == k) pairs + Aeson.toList (Aeson.Object o) = map (first Aeson.toText) (Aeson.toList o) + Aeson.toList _ = [] + Aeson.toText = id + first f (a, b) = (f a, b) + +-- | Format event for terminal display +formatEventForTerminal :: AgentEvent -> Text +formatEventForTerminal e = + let ts = Text.pack <| formatTime defaultTimeLocale "%H:%M:%S" (eventTimestamp e) + content = case eventType e of + Assistant -> case eventContent e of + Aeson.String t -> "Assistant: " <> truncate' 100 t + _ -> "Assistant: <message>" + ToolCall -> case eventContent e of + Aeson.Object _ -> + let toolName = getField "tool" (eventContent e) + in "Tool: " <> toolName + _ -> "Tool: <call>" + ToolResult -> case eventContent e of + Aeson.Object _ -> + let toolName = getField "tool" (eventContent e) + success = getField "success" (eventContent e) + in "Result: " <> toolName <> " (" <> success <> ")" + _ -> "Result: <result>" + UserMessage -> case eventContent e of + Aeson.String t -> "User: " <> truncate' 100 t + _ -> "User: <message>" + Cost -> case eventContent e of + Aeson.Object _ -> + let tokens = getField "tokens" (eventContent e) + cents = getField "cents" (eventContent e) + in "Cost: " <> tokens <> " tokens, " <> cents <> " cents" + _ -> "Cost: <info>" + Error -> case eventContent e of + Aeson.String t -> "Error: " <> t + _ -> "Error: <error>" + Complete -> "Complete" + in "[" <> ts <> "] " <> content + where + truncate' n t = if Text.length t > n then Text.take n t <> "..." else t + getField key val = case val of + Aeson.Object o -> case Aeson.lookup key (Aeson.toList o) of + Just (Aeson.String s) -> s + Just (Aeson.Number n) -> Text.pack (show n) + Just (Aeson.Bool b) -> if b then "ok" else "failed" + _ -> "<" <> key <> ">" + _ -> "<" <> key <> ">" + where + Aeson.lookup k pairs = snd </ find (\(k', _) -> k' == k) pairs + Aeson.toList (Aeson.Object o') = map (first' Aeson.toText) (Aeson.toList o') + Aeson.toList _ = [] + Aeson.toText = id + first' f (a, b) = (f a, b) + +-- Helper constructors for common events + +mkAssistantEvent :: UTCTime -> Text -> AgentEvent +mkAssistantEvent ts content = + AgentEvent + { eventType = Assistant, + eventTimestamp = ts, + eventContent = Aeson.String content + } + +mkToolCallEvent :: UTCTime -> Text -> Aeson.Value -> AgentEvent +mkToolCallEvent ts toolName args = + AgentEvent + { eventType = ToolCall, + eventTimestamp = ts, + eventContent = Aeson.object ["tool" .= toolName, "args" .= args] + } + +mkToolResultEvent :: UTCTime -> Text -> Bool -> Text -> AgentEvent +mkToolResultEvent ts toolName success output = + AgentEvent + { eventType = ToolResult, + eventTimestamp = ts, + eventContent = + Aeson.object + [ "tool" .= toolName, + "success" .= success, + "output" .= output + ] + } + +mkCostEvent :: UTCTime -> Int -> Int -> AgentEvent +mkCostEvent ts tokens cents = + AgentEvent + { eventType = Cost, + eventTimestamp = ts, + eventContent = Aeson.object ["tokens" .= tokens, "cents" .= cents] + } + +mkErrorEvent :: UTCTime -> Text -> AgentEvent +mkErrorEvent ts msg = + AgentEvent + { eventType = Error, + eventTimestamp = ts, + eventContent = Aeson.String msg + } + +mkCompleteEvent :: UTCTime -> AgentEvent +mkCompleteEvent ts = + AgentEvent + { eventType = Complete, + eventTimestamp = ts, + eventContent = Aeson.Null + } |
