From e2ea8308d74582d5651ed933dea9428ce8982d25 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Wed, 17 Dec 2025 22:05:40 -0500 Subject: 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] [] - read_ava_logs tool for Ava self-diagnosis Tasks: t-267, t-268, t-269, t-270, t-271 --- Omni/Ava.hs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) (limited to 'Omni/Ava.hs') 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] [] 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] + 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 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 _ -> "" + _ -> "" + TextIO.putStrLn <| "[" <> ts <> "] " <> agent <> " | " <> evType <> ": " <> content test :: Test.Tree test = -- cgit v1.2.3