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/Ava.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/Ava.hs')
| -rwxr-xr-x | Omni/Ava.hs | 57 |
1 files changed, 52 insertions, 5 deletions
diff --git a/Omni/Ava.hs b/Omni/Ava.hs index 0788658..3640fc2 100755 --- a/Omni/Ava.hs +++ b/Omni/Ava.hs @@ -9,19 +9,27 @@ -- ava # Uses TELEGRAM_BOT_TOKEN env var -- ava --token=XXX # Explicit token -- ava --model=MODEL # Override LLM model +-- ava logs [--last=N] [SUBAGENT_ID] # View audit logs -- -- : out ava -- : dep aeson -- : dep http-conduit -- : dep stm +-- : dep time +-- : dep uuid module Omni.Ava where import Alpha +import qualified Data.Aeson as Aeson import qualified Data.Text as Text +import qualified Data.Text.IO as TextIO +import qualified Data.Time as Time +import qualified Omni.Agent.AuditLog as AuditLog import qualified Omni.Agent.Telegram as Telegram import qualified Omni.Cli as Cli import qualified Omni.Test as Test import qualified System.Console.Docopt as Docopt +import qualified System.Directory as Dir import qualified System.IO as IO main :: IO () @@ -43,21 +51,60 @@ ava - AI assistant via Telegram Usage: ava [--token=TOKEN] [--model=MODEL] + ava logs [--last=N] [<subagent_id>] ava test ava (-h | --help) Options: - -h --help Show this help - --token=TOKEN Telegram bot token (or use TELEGRAM_BOT_TOKEN env) - --model=MODEL LLM model to use [default: anthropic/claude-sonnet-4] + -h --help Show this help + --token=TOKEN Telegram bot token (or use TELEGRAM_BOT_TOKEN env) + --model=MODEL LLM model to use [default: anthropic/claude-sonnet-4] + --last=N Number of recent log entries to show [default: 50] + <subagent_id> Show logs for a specific subagent (e.g. S-abc123) |] move :: Cli.Arguments -> IO () move args = do IO.hSetBuffering IO.stdout IO.LineBuffering IO.hSetBuffering IO.stderr IO.LineBuffering - let maybeToken = fmap Text.pack (Cli.getArg args (Cli.longOption "token")) - Telegram.startBot maybeToken + if args `Cli.has` Cli.command "logs" + then showLogs args + else do + let maybeToken = fmap Text.pack (Cli.getArg args (Cli.longOption "token")) + Telegram.startBot maybeToken + +showLogs :: Cli.Arguments -> IO () +showLogs args = do + let maybeSubagentId = Cli.getArg args (Cli.argument "subagent_id") + let lastN = fromMaybe 50 (readMaybe =<< Cli.getArg args (Cli.longOption "last")) + + case maybeSubagentId of + Just sidStr -> do + let sid = AuditLog.SubagentId (Text.pack sidStr) + let path = AuditLog.subagentLogPath sid + exists <- Dir.doesFileExist path + if exists + then do + entries <- AuditLog.readSubagentLogs sid + putText <| "=== Subagent " <> Text.pack sidStr <> " (" <> tshow (length entries) <> " entries) ===" + traverse_ printLogEntry entries + else putText <| "No logs found for subagent: " <> Text.pack sidStr + Nothing -> do + entries <- AuditLog.getRecentAvaLogs lastN + today <- Time.utctDay </ Time.getCurrentTime + putText <| "=== Ava logs for " <> Text.pack (Time.formatTime Time.defaultTimeLocale "%Y-%m-%d" today) <> " (last " <> tshow lastN <> ") ===" + traverse_ printLogEntry entries + +printLogEntry :: AuditLog.AuditLogEntry -> IO () +printLogEntry entry = do + let ts = Text.pack <| Time.formatTime Time.defaultTimeLocale "%H:%M:%S" (AuditLog.logTimestamp entry) + let evType = tshow (AuditLog.logEventType entry) + let agent = AuditLog.unAgentId (AuditLog.logAgentId entry) + let content = case AuditLog.logContent entry of + Aeson.String t -> Text.take 100 t + Aeson.Object _ -> "<object>" + _ -> "<value>" + TextIO.putStrLn <| "[" <> ts <> "] " <> agent <> " | " <> evType <> ": " <> content test :: Test.Tree test = |
