diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-01 10:43:59 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-01 10:43:59 -0500 |
| commit | 046e6d1ca55651379f938b4481570bcb1b122e1e (patch) | |
| tree | 511a0bfaa9f06c58964da24509bb59cc89c954e1 /Omni | |
| parent | 661beb3802e3d827febcb163bfb90e4f18ad8127 (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
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Agent/Worker.hs | 22 | ||||
| -rw-r--r-- | Omni/Jr/Web.hs | 2 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 60 |
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) |
