summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-01 10:43:59 -0500
committerBen Sima <ben@bensima.com>2025-12-01 10:43:59 -0500
commit046e6d1ca55651379f938b4481570bcb1b122e1e (patch)
tree511a0bfaa9f06c58964da24509bb59cc89c954e1
parent661beb3802e3d827febcb163bfb90e4f18ad8127 (diff)
Add actor column to agent_events table
- Add 'actor' column to agent_events table (human/junior/system) - Add System to CommentAuthor type (reused for actor) - Add SQL FromField/ToField instances for CommentAuthor - Update insertAgentEvent to accept actor parameter - Update all SELECT queries to include actor column - Update Worker.hs to pass actor for all event types - Guardrail events logged with System actor Migration: ALTER TABLE adds column with default 'junior' for existing rows. Task-Id: t-213.1
-rw-r--r--Omni/Agent/Worker.hs22
-rw-r--r--Omni/Jr/Web.hs2
-rw-r--r--Omni/Task/Core.hs60
3 files changed, 50 insertions, 34 deletions
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index bbdba9d..5fa169f 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -267,10 +267,11 @@ runWithEngine worker repo task = do
-- Helper to log events to DB
-- For text content, store as-is; for structured data, JSON-encode
- let logEventText = TaskCore.insertAgentEvent tid sessionId
- logEventJson eventType value = do
+ let logJuniorEvent eventType content = TaskCore.insertAgentEvent tid sessionId eventType content TaskCore.Junior
+ logJuniorJson eventType value = do
let contentJson = TE.decodeUtf8 (BSL.toStrict (Aeson.encode value))
- TaskCore.insertAgentEvent tid sessionId eventType contentJson
+ TaskCore.insertAgentEvent tid sessionId eventType contentJson TaskCore.Junior
+ logSystemEvent eventType content = TaskCore.insertAgentEvent tid sessionId eventType content TaskCore.System
-- Build Engine config with callbacks
totalCostRef <- newIORef (0 :: Double)
@@ -285,29 +286,30 @@ runWithEngine worker repo task = do
Engine.engineOnCost = \tokens cost -> do
modifyIORef' totalCostRef (+ cost)
sayLog <| "Cost: " <> tshow cost <> " cents (" <> tshow tokens <> " tokens)"
- logEventJson "Cost" (Aeson.object [("tokens", Aeson.toJSON tokens), ("cents", Aeson.toJSON cost)]),
+ logJuniorJson "Cost" (Aeson.object [("tokens", Aeson.toJSON tokens), ("cents", Aeson.toJSON cost)]),
Engine.engineOnActivity = \activity -> do
sayLog <| "[engine] " <> activity,
Engine.engineOnToolCall = \toolName args -> do
sayLog <| "[tool] " <> toolName
- logEventText "ToolCall" (toolName <> ": " <> args),
+ logJuniorEvent "ToolCall" (toolName <> ": " <> args),
Engine.engineOnAssistant = \msg -> do
sayLog <| "[assistant] " <> Text.take 200 msg
- logEventText "Assistant" msg,
+ logJuniorEvent "Assistant" msg,
Engine.engineOnToolResult = \toolName success output -> do
let statusStr = if success then "ok" else "failed"
sayLog <| "[result] " <> toolName <> " (" <> statusStr <> "): " <> Text.take 100 output
- logEventText "ToolResult" output,
+ logJuniorEvent "ToolResult" output,
Engine.engineOnComplete = do
sayLog "[engine] Complete"
- logEventText "Complete" "",
+ logJuniorEvent "Complete" "",
Engine.engineOnError = \err -> do
sayLog <| "[error] " <> err
- logEventText "Error" err,
+ logJuniorEvent "Error" err,
Engine.engineOnGuardrail = \guardrailResult -> do
let guardrailMsg = formatGuardrailResult guardrailResult
+ contentJson = TE.decodeUtf8 (BSL.toStrict (Aeson.encode guardrailResult))
sayLog <| "[guardrail] " <> guardrailMsg
- logEventJson "Guardrail" (Aeson.toJSON guardrailResult)
+ logSystemEvent "Guardrail" contentJson
}
-- Build Agent config with guardrails
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index 7555e22..f7a2219 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -1632,9 +1632,11 @@ instance Lucid.ToHtml TaskDetailPage where
authorClass = case TaskCore.commentAuthor c of
TaskCore.Human -> "author-human"
TaskCore.Junior -> "author-junior"
+ TaskCore.System -> "author-system"
authorLabel author = case author of
TaskCore.Human -> "Human" :: Text
TaskCore.Junior -> "Junior" :: Text
+ TaskCore.System -> "System" :: Text
commentForm :: (Monad m) => Text -> Lucid.HtmlT m ()
commentForm tid =
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 1212a56..41fcae4 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -140,8 +140,8 @@ data Fact = Fact
}
deriving (Show, Eq, Generic)
--- Comment author
-data CommentAuthor = Human | Junior
+-- Comment/event author (also used as Actor for timeline events)
+data CommentAuthor = Human | Junior | System
deriving (Show, Eq, Read, Generic)
-- Comment for task notes/context
@@ -266,16 +266,6 @@ instance SQL.FromField ActivityStage where
instance SQL.ToField ActivityStage where
toField x = SQL.toField (show x :: String)
-instance SQL.FromField CommentAuthor where
- fromField f = do
- t <- SQL.fromField f :: SQLOk.Ok String
- case readMaybe t of
- Just x -> pure x
- Nothing -> SQL.returnError SQL.ConversionFailed f "Invalid CommentAuthor"
-
-instance SQL.ToField CommentAuthor where
- toField x = SQL.toField (show x :: String)
-
-- Store dependencies as JSON text
instance SQL.FromField [Dependency] where
fromField f = do
@@ -540,10 +530,16 @@ createAgentEventsTable conn = do
\ session_id TEXT NOT NULL, \
\ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, \
\ event_type TEXT NOT NULL, \
- \ content TEXT NOT NULL \
+ \ content TEXT NOT NULL, \
+ \ actor TEXT NOT NULL DEFAULT 'junior' \
\)"
SQL.execute_ conn "CREATE INDEX IF NOT EXISTS idx_agent_events_task ON agent_events(task_id)"
SQL.execute_ conn "CREATE INDEX IF NOT EXISTS idx_agent_events_session ON agent_events(session_id)"
+ -- Add actor column to existing tables (migration)
+ SQL.execute_ conn "ALTER TABLE agent_events ADD COLUMN actor TEXT NOT NULL DEFAULT 'junior'" `catch` ignoreAlterError
+ where
+ ignoreAlterError :: SQL.SQLError -> IO ()
+ ignoreAlterError _ = pure () -- Column already exists
-- | Expected columns for task_activity table (name, type, nullable)
taskActivityColumns :: [(Text, Text)]
@@ -1609,6 +1605,20 @@ deleteFact fid =
-- Agent Events (for observability)
-- ============================================================================
+instance SQL.FromField CommentAuthor where
+ fromField f = do
+ t <- SQL.fromField f :: SQLOk.Ok String
+ case t of
+ "human" -> pure Human
+ "junior" -> pure Junior
+ "system" -> pure System
+ _ -> SQL.returnError SQL.ConversionFailed f "Invalid CommentAuthor"
+
+instance SQL.ToField CommentAuthor where
+ toField Human = SQL.toField ("human" :: String)
+ toField Junior = SQL.toField ("junior" :: String)
+ toField System = SQL.toField ("system" :: String)
+
-- | Stored agent event record
data StoredEvent = StoredEvent
{ storedEventId :: Int,
@@ -1616,7 +1626,8 @@ data StoredEvent = StoredEvent
storedEventSessionId :: Text,
storedEventTimestamp :: UTCTime,
storedEventType :: Text,
- storedEventContent :: Text
+ storedEventContent :: Text,
+ storedEventActor :: CommentAuthor
}
deriving (Show, Eq, Generic)
@@ -1633,6 +1644,7 @@ instance SQL.FromRow StoredEvent where
<*> SQL.field
<*> SQL.field
<*> SQL.field
+ <*> SQL.field
-- | Generate a new session ID (timestamp-based for simplicity)
generateSessionId :: IO Text
@@ -1640,14 +1652,14 @@ generateSessionId = do
now <- getCurrentTime
pure <| "s-" <> T.pack (show now)
--- | Insert an agent event
-insertAgentEvent :: Text -> Text -> Text -> Text -> IO ()
-insertAgentEvent taskId sessionId eventType content =
+-- | Insert an agent event with actor
+insertAgentEvent :: Text -> Text -> Text -> Text -> CommentAuthor -> IO ()
+insertAgentEvent taskId sessionId eventType content actor =
withDb <| \conn ->
SQL.execute
conn
- "INSERT INTO agent_events (task_id, session_id, event_type, content) VALUES (?, ?, ?, ?)"
- (taskId, sessionId, eventType, content)
+ "INSERT INTO agent_events (task_id, session_id, event_type, content, actor) VALUES (?, ?, ?, ?, ?)"
+ (taskId, sessionId, eventType, content, actor)
-- | Get all events for a task (most recent session)
getEventsForTask :: Text -> IO [StoredEvent]
@@ -1663,7 +1675,7 @@ getEventsForSession sessionId =
withDb <| \conn ->
SQL.query
conn
- "SELECT id, task_id, session_id, timestamp, event_type, content \
+ "SELECT id, task_id, session_id, timestamp, event_type, content, actor \
\FROM agent_events WHERE session_id = ? ORDER BY id ASC"
(SQL.Only sessionId)
@@ -1699,14 +1711,14 @@ getEventsSince sessionId lastId =
withDb <| \conn ->
SQL.query
conn
- "SELECT id, task_id, session_id, timestamp, event_type, content \
+ "SELECT id, task_id, session_id, timestamp, event_type, content, actor \
\FROM agent_events WHERE session_id = ? AND id > ? ORDER BY id ASC"
(sessionId, lastId)
-- | Insert a checkpoint event (for progress tracking)
insertCheckpoint :: Text -> Text -> Text -> IO ()
-insertCheckpoint taskId sessionId =
- insertAgentEvent taskId sessionId "Checkpoint"
+insertCheckpoint taskId sessionId content =
+ insertAgentEvent taskId sessionId "Checkpoint" content Junior
-- | Get all checkpoints for a task (across all sessions)
getCheckpointsForTask :: Text -> IO [StoredEvent]
@@ -1714,7 +1726,7 @@ getCheckpointsForTask taskId =
withDb <| \conn ->
SQL.query
conn
- "SELECT id, task_id, session_id, timestamp, event_type, content \
+ "SELECT id, task_id, session_id, timestamp, event_type, content, actor \
\FROM agent_events WHERE task_id = ? AND event_type = 'Checkpoint' ORDER BY id ASC"
(SQL.Only taskId)