diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-17 22:05:40 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-17 22:05:40 -0500 |
| commit | e2ea8308d74582d5651ed933dea9428ce8982d25 (patch) | |
| tree | e19662b502f1dedb5396673032be39ebc31a99f3 /Omni/Agent/Tools/AvaLogs.hs | |
| parent | b384667997140a5e561572e41fe924d10ea7a660 (diff) | |
feat(ava): subagent hardening with audit logging
Based on Anthropic's effective harnesses research.
New modules:
- Omni/Agent/AuditLog.hs: JSONL audit logging with SubagentId linking
- Omni/Agent/Tools/AvaLogs.hs: Tool for Ava to query her own logs
- Omni/Agent/Subagent/HARDENING.md: Design documentation
Key features:
- SubagentHandle with TVar status for async execution and polling
- spawnSubagentAsync, querySubagentStatus, waitSubagent, cancelSubagent
- User confirmation: spawn_subagent requires confirmed=true after approval
- Audit logs stored in $AVA_DATA_ROOT/logs/{ava,subagents}/
- CLI: ava logs [--last=N] [<subagent_id>]
- read_ava_logs tool for Ava self-diagnosis
Tasks: t-267, t-268, t-269, t-270, t-271
Diffstat (limited to 'Omni/Agent/Tools/AvaLogs.hs')
| -rw-r--r-- | Omni/Agent/Tools/AvaLogs.hs | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/Omni/Agent/Tools/AvaLogs.hs b/Omni/Agent/Tools/AvaLogs.hs new file mode 100644 index 0000000..582b3a6 --- /dev/null +++ b/Omni/Agent/Tools/AvaLogs.hs @@ -0,0 +1,109 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Tool for Ava to read her own audit logs and subagent traces. +-- +-- Enables self-diagnosis: "Let me check my logs for that subagent run..." +-- +-- : out omni-agent-tools-avalogs +-- : dep aeson +-- : dep time +module Omni.Agent.Tools.AvaLogs + ( readAvaLogsTool, + main, + ) +where + +import Alpha +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.Text as Text +import qualified Data.Time as Time +import qualified Omni.Agent.AuditLog as AuditLog +import qualified Omni.Agent.Engine as Engine + +main :: IO () +main = putText "Omni.Agent.Tools.AvaLogs - no standalone execution" + +readAvaLogsTool :: Engine.Tool +readAvaLogsTool = + Engine.Tool + { Engine.toolName = "read_ava_logs", + Engine.toolDescription = + "Read Ava's audit logs or subagent traces for self-diagnosis. " + <> "Use to review past conversations, inspect subagent runs, or debug issues. " + <> "Pass subagent_id to view a specific subagent's trace, or last_n for recent Ava logs.", + Engine.toolJsonSchema = + Aeson.object + [ "type" .= ("object" :: Text), + "properties" + .= Aeson.object + [ "subagent_id" + .= Aeson.object + [ "type" .= ("string" :: Text), + "description" .= ("Subagent ID to view (e.g. 'abc123')" :: Text) + ], + "last_n" + .= Aeson.object + [ "type" .= ("integer" :: Text), + "description" .= ("Number of recent log entries to return (default: 20)" :: Text) + ] + ] + ], + Engine.toolExecute = executeReadLogs + } + +executeReadLogs :: Aeson.Value -> IO Aeson.Value +executeReadLogs v = do + let maybeSubagentId = case v of + Aeson.Object obj -> case KeyMap.lookup "subagent_id" obj of + Just (Aeson.String sid) -> Just sid + _ -> Nothing + _ -> Nothing + + let lastN = case v of + Aeson.Object obj -> case KeyMap.lookup "last_n" obj of + Just (Aeson.Number n) -> round n + _ -> 20 + _ -> 20 + + case maybeSubagentId of + Just sid -> do + let subagentId = AuditLog.SubagentId sid + entries <- AuditLog.readSubagentLogs subagentId + pure + <| Aeson.object + [ "subagent_id" .= sid, + "entry_count" .= length entries, + "entries" .= map formatEntry entries + ] + Nothing -> do + entries <- AuditLog.getRecentAvaLogs lastN + today <- Time.utctDay </ Time.getCurrentTime + pure + <| Aeson.object + [ "date" .= Time.formatTime Time.defaultTimeLocale "%Y-%m-%d" today, + "entry_count" .= length entries, + "entries" .= map formatEntry entries + ] + +formatEntry :: AuditLog.AuditLogEntry -> Aeson.Value +formatEntry entry = + Aeson.object + [ "timestamp" .= Time.formatTime Time.defaultTimeLocale "%H:%M:%S" (AuditLog.logTimestamp entry), + "event_type" .= tshow (AuditLog.logEventType entry), + "agent_id" .= AuditLog.unAgentId (AuditLog.logAgentId entry), + "content" .= summarizeContent (AuditLog.logContent entry) + ] + +summarizeContent :: Aeson.Value -> Text +summarizeContent (Aeson.String t) = Text.take 200 t +summarizeContent (Aeson.Object obj) = + let keys = KeyMap.keys obj + in "object with keys: " <> Text.intercalate ", " (map Key.toText keys) +summarizeContent (Aeson.Array arr) = "array with " <> tshow (length arr) <> " items" +summarizeContent Aeson.Null = "null" +summarizeContent (Aeson.Bool b) = if b then "true" else "false" +summarizeContent (Aeson.Number n) = tshow n |
