summaryrefslogtreecommitdiff
path: root/Omni/Ava.hs
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-17 22:05:40 -0500
committerBen Sima <ben@bensima.com>2025-12-17 22:05:40 -0500
commite2ea8308d74582d5651ed933dea9428ce8982d25 (patch)
treee19662b502f1dedb5396673032be39ebc31a99f3 /Omni/Ava.hs
parentb384667997140a5e561572e41fe924d10ea7a660 (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-xOmni/Ava.hs57
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 =