From fcb8629182fa1552e4a840ccd4ec0aa2b8042cc0 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 19 Dec 2025 21:54:54 -0500 Subject: feat(ava): add tool trace viewer mini-app - Add SQLite storage for tool traces (Omni/Ava/Trace.hs) - Add web server to serve trace viewer (Omni/Ava/Web.hs) - Add HTML/CSS/JS trace viewer UI (Omni/Ava/Web/trace.html) - Integrate trace storage into Engine.hs tool execution callback - Add trace links to Telegram responses when AVA_WEB_URL is set - Configure Tailscale Funnel for public access - Fix pre-push hook variable scope bug - Add direnv, bash, nix to Ava service PATH - Add mustache dep to Ava.hs for template rendering Epic: t-272 --- Omni/Agent/Engine.hs | 26 ++++++-- Omni/Agent/Memory.hs | 15 +++++ Omni/Agent/Subagent.hs | 3 +- Omni/Agent/Subagent/Coder.hs | 3 +- Omni/Agent/Telegram.hs | 54 +++++++++++++++- Omni/Ava.hs | 11 ++++ Omni/Ava/Trace.hs | 148 +++++++++++++++++++++++++++++++++++++++++++ Omni/Ava/Web.hs | 84 ++++++++++++++++++++++++ Omni/Ava/Web/trace.html | 71 +++++++++++++++++++++ Omni/Bild.nix | 2 + Omni/Bild/Deps/Haskell.nix | 1 + Omni/Dev/Beryllium/Ava.nix | 9 +++ Omni/Ide/hooks/pre-push | 4 +- 13 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 Omni/Ava/Trace.hs create mode 100644 Omni/Ava/Web.hs create mode 100644 Omni/Ava/Web/trace.html (limited to 'Omni') diff --git a/Omni/Agent/Engine.hs b/Omni/Agent/Engine.hs index f137ddb..0dc7c50 100644 --- a/Omni/Agent/Engine.hs +++ b/Omni/Agent/Engine.hs @@ -14,6 +14,7 @@ -- : dep http-conduit -- : dep aeson -- : dep case-insensitive +-- : dep time module Omni.Agent.Engine ( Tool (..), LLM (..), @@ -56,6 +57,7 @@ import Data.IORef (newIORef, writeIORef) import qualified Data.Map.Strict as Map import qualified Data.Text as Text import qualified Data.Text.Encoding as TE +import qualified Data.Time as Time import qualified Network.HTTP.Simple as HTTP import qualified Omni.Agent.Provider as Provider import qualified Omni.Test as Test @@ -378,7 +380,8 @@ data EngineConfig = EngineConfig engineOnToolResult :: Text -> Bool -> Text -> IO (), engineOnComplete :: IO (), engineOnError :: Text -> IO (), - engineOnGuardrail :: GuardrailResult -> IO () + engineOnGuardrail :: GuardrailResult -> IO (), + engineOnToolTrace :: Text -> Text -> Text -> Int -> IO (Maybe Text) } defaultEngineConfig :: EngineConfig @@ -392,7 +395,8 @@ defaultEngineConfig = engineOnToolResult = \_ _ _ -> pure (), engineOnComplete = pure (), engineOnError = \_ -> pure (), - engineOnGuardrail = \_ -> pure () + engineOnGuardrail = \_ -> pure (), + engineOnToolTrace = \_ _ _ _ -> pure Nothing } data AgentResult = AgentResult @@ -791,14 +795,18 @@ executeToolCallsWithTracking engineCfg toolMap tcs initialTestFailures initialEd engineOnToolResult engineCfg name False errMsg pure (Message ToolRole errMsg Nothing (Just callId), 0, 0) Just args -> do + startTime <- Time.getCurrentTime resultValue <- toolExecute tool args - let resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) + endTime <- Time.getCurrentTime + let durationMs = round (Time.diffUTCTime endTime startTime * 1000) + resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) isTestCall = name == "bash" && ("bild --test" `Text.isInfixOf` argsText || "bild -t" `Text.isInfixOf` argsText) isTestFailure = isTestCall && isFailureResult resultValue testDelta = if isTestFailure then 1 else 0 isEditFailure = name == "edit_file" && isOldStrNotFoundError resultValue editDelta = if isEditFailure then 1 else 0 engineOnToolResult engineCfg name True resultText + _ <- engineOnToolTrace engineCfg name argsText resultText durationMs pure (Message ToolRole resultText Nothing (Just callId), testDelta, editDelta) isFailureResult :: Aeson.Value -> Bool @@ -976,14 +984,18 @@ runAgentWithProvider engineCfg provider agentCfg userPrompt = do engineOnToolResult eCfg name False errMsg pure (Provider.Message Provider.ToolRole errMsg Nothing (Just callId), 0, 0) Just args -> do + startTime <- Time.getCurrentTime resultValue <- toolExecute tool args - let resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) + endTime <- Time.getCurrentTime + let durationMs = round (Time.diffUTCTime endTime startTime * 1000) + resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) isTestCall = name == "bash" && ("bild --test" `Text.isInfixOf` argsText || "bild -t" `Text.isInfixOf` argsText) isTestFailure = isTestCall && isFailureResultProvider resultValue testDelta = if isTestFailure then 1 else 0 isEditFailure = name == "edit_file" && isOldStrNotFoundProvider resultValue editDelta = if isEditFailure then 1 else 0 engineOnToolResult eCfg name True resultText + _ <- engineOnToolTrace eCfg name argsText resultText durationMs pure (Provider.Message Provider.ToolRole resultText Nothing (Just callId), testDelta, editDelta) isFailureResultProvider :: Aeson.Value -> Bool @@ -1157,14 +1169,18 @@ runAgentWithProviderStreaming engineCfg provider agentCfg userPrompt onStreamChu engineOnToolResult eCfg name False errMsg pure (Provider.Message Provider.ToolRole errMsg Nothing (Just callId), 0, 0) Just args -> do + startTime <- Time.getCurrentTime resultValue <- toolExecute tool args - let resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) + endTime <- Time.getCurrentTime + let durationMs = round (Time.diffUTCTime endTime startTime * 1000) + resultText = TE.decodeUtf8 (BL.toStrict (Aeson.encode resultValue)) isTestCall = name == "bash" && ("bild --test" `Text.isInfixOf` argsText || "bild -t" `Text.isInfixOf` argsText) isTestFailure = isTestCall && isFailureResultStreaming resultValue testDelta = if isTestFailure then 1 else 0 isEditFailure = name == "edit_file" && isOldStrNotFoundStreaming resultValue editDelta = if isEditFailure then 1 else 0 engineOnToolResult eCfg name True resultText + _ <- engineOnToolTrace eCfg name argsText resultText durationMs pure (Provider.Message Provider.ToolRole resultText Nothing (Just callId), testDelta, editDelta) isFailureResultStreaming :: Aeson.Value -> Bool diff --git a/Omni/Agent/Memory.hs b/Omni/Agent/Memory.hs index d59104c..c869b26 100644 --- a/Omni/Agent/Memory.hs +++ b/Omni/Agent/Memory.hs @@ -795,6 +795,21 @@ initMemoryDb conn = do SQL.execute_ conn "CREATE INDEX IF NOT EXISTS idx_chat_history_time ON chat_history(created_at)" + SQL.execute_ + conn + "CREATE TABLE IF NOT EXISTS tool_traces (\ + \ id TEXT PRIMARY KEY,\ + \ created_at TEXT NOT NULL,\ + \ tool_name TEXT NOT NULL,\ + \ input TEXT NOT NULL,\ + \ output TEXT NOT NULL,\ + \ duration_ms INTEGER NOT NULL,\ + \ user_id TEXT,\ + \ chat_id TEXT\ + \)" + SQL.execute_ + conn + "CREATE INDEX IF NOT EXISTS idx_traces_created ON tool_traces(created_at)" -- | Migrate conversation_messages to add sender_name and thread_id columns. migrateConversationMessages :: SQL.Connection -> IO () diff --git a/Omni/Agent/Subagent.hs b/Omni/Agent/Subagent.hs index 9f3052d..cb8c090 100644 --- a/Omni/Agent/Subagent.hs +++ b/Omni/Agent/Subagent.hs @@ -830,7 +830,8 @@ runGenericSubagent keys config callbacks = do Engine.engineOnToolResult = \_ _ _ -> pure (), Engine.engineOnComplete = pure (), Engine.engineOnError = \_ -> pure (), - Engine.engineOnGuardrail = \_ -> pure () + Engine.engineOnGuardrail = \_ -> pure (), + Engine.engineOnToolTrace = \_ _ _ _ -> pure Nothing } let timeoutMicros = subagentTimeout config * 1000000 diff --git a/Omni/Agent/Subagent/Coder.hs b/Omni/Agent/Subagent/Coder.hs index 865a97e..ad97ee7 100644 --- a/Omni/Agent/Subagent/Coder.hs +++ b/Omni/Agent/Subagent/Coder.hs @@ -139,9 +139,10 @@ defaultCoderConfig namespace task = } -- | Run a bash command and capture output +-- Uses direnv exec to ensure the nix shell environment is loaded runBashCapture :: Text -> IO (Exit.ExitCode, Text, Text) runBashCapture cmd = do - (code, out, err) <- Process.readProcessWithExitCode "bash" ["-c", Text.unpack cmd] "" + (code, out, err) <- Process.readProcessWithExitCode "direnv" ["exec", ".", "bash", "-c", Text.unpack cmd] "" pure (code, Text.pack out, Text.pack err) -- | Phase 1: Initialize - check environment, detect broken state diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index 7b2beaa..7183592 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -76,6 +76,7 @@ import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.KeyMap as KeyMap import qualified Data.ByteString.Lazy as BL +import Data.IORef (newIORef, readIORef, writeIORef) import qualified Data.Text as Text import qualified Data.Text.Encoding as TE import Data.Time (getCurrentTime, utcToLocalTime) @@ -110,6 +111,7 @@ import qualified Omni.Agent.Tools.Python as Python import qualified Omni.Agent.Tools.Todos as Todos import qualified Omni.Agent.Tools.WebReader as WebReader import qualified Omni.Agent.Tools.WebSearch as WebSearch +import qualified Omni.Ava.Trace as Trace import qualified Omni.Test as Test import System.Environment (lookupEnv) import Text.Printf (printf) @@ -524,6 +526,14 @@ leaveChat cfg chatId = do runTelegramBot :: Types.TelegramConfig -> Provider.Provider -> IO () runTelegramBot tgConfig provider = do putText "Starting Telegram bot..." + + cleanedCount <- Memory.withMemoryDb Trace.cleanupOldTraces + when (cleanedCount > 0) + <| putText + <| "Cleaned up " + <> tshow cleanedCount + <> " old tool traces" + offsetVar <- newTVarIO 0 botUsername <- getBotUsername tgConfig @@ -551,7 +561,23 @@ runTelegramBot tgConfig provider = do Engine.engineOnToolResult = \toolName success result -> putText <| "Tool result: " <> toolName <> " " <> (if success then "ok" else "err") <> " " <> Text.take 200 result, Engine.engineOnActivity = \activity -> - putText <| "Agent: " <> activity + putText <| "Agent: " <> activity, + Engine.engineOnToolTrace = \toolName input output durationMs -> do + now <- getCurrentTime + let truncatedOutput = Text.take 1000000 output + traceRecord = + Trace.TraceRecord + { Trace.trcId = "", + Trace.trcCreatedAt = tshow now, + Trace.trcToolName = toolName, + Trace.trcInput = input, + Trace.trcOutput = truncatedOutput, + Trace.trcDurationMs = durationMs, + Trace.trcUserId = Nothing, + Trace.trcChatId = Nothing + } + tid <- Memory.withMemoryDb <| \conn -> Trace.insertTrace conn traceRecord + pure (Just tid) } let processBatch = handleMessageBatch tgConfig provider engineCfg botName @@ -1061,6 +1087,17 @@ processEngagedMessage :: processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMessage conversationContext = do let isGroup = Types.isGroupChat msg + lastTraceIdRef <- newIORef (Nothing :: Maybe Text) + let engineCfgWithTrace = + engineCfg + { Engine.engineOnToolTrace = \toolName input output durationMs -> do + maybeTid <- Engine.engineOnToolTrace engineCfg toolName input output durationMs + case maybeTid of + Just tid -> writeIORef lastTraceIdRef (Just tid) + Nothing -> pure () + pure maybeTid + } + personalMemories <- Memory.recallMemories uid userMessage 5 groupMemories <- if isGroup @@ -1245,7 +1282,11 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe result <- withTypingIndicator tgConfig chatId - <| Engine.runAgentWithProvider engineCfg provider agentCfg userMessage + <| Engine.runAgentWithProvider engineCfgWithTrace provider agentCfg userMessage + + lastTraceId <- readIORef lastTraceIdRef + maybeWebUrl <- lookupEnv "AVA_WEB_URL" + let traceLink = formatTraceLink lastTraceId (Text.pack do @@ -1253,7 +1294,8 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe _ <- Messages.enqueueImmediate (Just uid) chatId (Types.tmThreadId msg) "sorry, i hit an error. please try again." (Just "agent_error") Nothing pure () Right agentResult -> do - let response = Engine.resultFinalMessage agentResult + let baseResponse = Engine.resultFinalMessage agentResult + response = baseResponse <> traceLink threadId = Types.tmThreadId msg putText <| "Response text: " <> Text.take 200 response @@ -1367,6 +1409,12 @@ mergeTooShort (x : y : rest) | Text.length x < 100 = mergeTooShort ((x <> "\n\n" <> y) : rest) | otherwise = x : mergeTooShort (y : rest) +formatTraceLink :: Maybe Text -> Maybe Text -> Text +formatTraceLink Nothing _ = "" +formatTraceLink _ Nothing = "" +formatTraceLink (Just tid) (Just baseUrl) = + "\n\n[view trace](" <> baseUrl <> "/trace/" <> tid <> ")" + enqueueMultipart :: Maybe Text -> Int -> Maybe Int -> [Text] -> Maybe Text -> IO () enqueueMultipart _ _ _ [] _ = pure () enqueueMultipart mUid chatId mThreadId parts msgType = do diff --git a/Omni/Ava.hs b/Omni/Ava.hs index 3640fc2..6058425 100755 --- a/Omni/Ava.hs +++ b/Omni/Ava.hs @@ -14,22 +14,29 @@ -- : out ava -- : dep aeson -- : dep http-conduit +-- : dep http-types +-- : dep mustache -- : dep stm -- : dep time -- : dep uuid +-- : dep wai +-- : dep warp module Omni.Ava where import Alpha +import qualified Control.Concurrent as Concurrent 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.Ava.Web as Web 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.Environment as Environment import qualified System.IO as IO main :: IO () @@ -70,6 +77,10 @@ move args = do if args `Cli.has` Cli.command "logs" then showLogs args else do + webPort <- Environment.lookupEnv "AVA_WEB_PORT" /> maybe Web.defaultPort (fromMaybe Web.defaultPort <. readMaybe) + dataRoot <- Environment.getEnv "AVA_DATA_ROOT" + let dbPath = dataRoot <> "/ava.db" + _ <- Concurrent.forkIO <| Web.startWebServer webPort dbPath let maybeToken = fmap Text.pack (Cli.getArg args (Cli.longOption "token")) Telegram.startBot maybeToken diff --git a/Omni/Ava/Trace.hs b/Omni/Ava/Trace.hs new file mode 100644 index 0000000..6dbdf51 --- /dev/null +++ b/Omni/Ava/Trace.hs @@ -0,0 +1,148 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Tool trace storage for Ava. +-- +-- Records tool execution traces for debugging and analytics. +-- +-- : out omni-ava-trace +-- : dep aeson +-- : dep sqlite-simple +-- : dep uuid +module Omni.Ava.Trace + ( TraceRecord (..), + insertTrace, + getTrace, + cleanupOldTraces, + main, + test, + ) +where + +import Alpha +import Data.Aeson ((.=)) +import qualified Data.Aeson as Aeson +import qualified Data.Text as Text +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUID +import qualified Database.SQLite.Simple as SQL +import qualified Database.SQLite.Simple.ToField as SQL +import qualified Omni.Test as Test + +main :: IO () +main = Test.run test + +test :: Test.Tree +test = + Test.group + "Omni.Ava.Trace" + [ Test.unit "TraceRecord JSON roundtrip" <| do + let tr = + TraceRecord + { trcId = "trace-123", + trcCreatedAt = "2024-01-15T10:30:00Z", + trcToolName = "web_search", + trcInput = "{\"query\":\"test\"}", + trcOutput = "{\"results\":[]}", + trcDurationMs = 150, + trcUserId = Just "user-456", + trcChatId = Just "chat-789" + } + case Aeson.decode (Aeson.encode tr) of + Nothing -> Test.assertFailure "Failed to decode TraceRecord" + Just decoded -> do + trcToolName decoded Test.@=? "web_search" + trcDurationMs decoded Test.@=? 150 + ] + +data TraceRecord = TraceRecord + { trcId :: Text, + trcCreatedAt :: Text, + trcToolName :: Text, + trcInput :: Text, + trcOutput :: Text, + trcDurationMs :: Int, + trcUserId :: Maybe Text, + trcChatId :: Maybe Text + } + deriving (Show, Eq, Generic) + +instance Aeson.ToJSON TraceRecord where + toJSON tr = + Aeson.object + [ "id" .= trcId tr, + "created_at" .= trcCreatedAt tr, + "tool_name" .= trcToolName tr, + "input" .= trcInput tr, + "output" .= trcOutput tr, + "duration_ms" .= trcDurationMs tr, + "user_id" .= trcUserId tr, + "chat_id" .= trcChatId tr + ] + +instance Aeson.FromJSON TraceRecord where + parseJSON = + Aeson.withObject "TraceRecord" <| \v -> + (TraceRecord (v Aeson..: "created_at") + <*> (v Aeson..: "tool_name") + <*> (v Aeson..: "input") + <*> (v Aeson..: "output") + <*> (v Aeson..: "duration_ms") + <*> (v Aeson..:? "user_id") + <*> (v Aeson..:? "chat_id") + +instance SQL.FromRow TraceRecord where + fromRow = + (TraceRecord SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + +instance SQL.ToRow TraceRecord where + toRow tr = + [ SQL.toField (trcId tr), + SQL.toField (trcCreatedAt tr), + SQL.toField (trcToolName tr), + SQL.toField (trcInput tr), + SQL.toField (trcOutput tr), + SQL.toField (trcDurationMs tr), + SQL.toField (trcUserId tr), + SQL.toField (trcChatId tr) + ] + +insertTrace :: SQL.Connection -> TraceRecord -> IO Text +insertTrace conn tr = do + tid <- + if Text.null (trcId tr) + then (Text.pack <. UUID.toString) Text -> IO (Maybe TraceRecord) +getTrace conn tid = do + results <- + SQL.query + conn + "SELECT id, created_at, tool_name, input, output, duration_ms, user_id, chat_id \ + \FROM tool_traces WHERE id = ?" + (SQL.Only tid) + pure (listToMaybe results) + +cleanupOldTraces :: SQL.Connection -> IO Int +cleanupOldTraces conn = do + SQL.execute_ + conn + "DELETE FROM tool_traces WHERE created_at < datetime('now', '-7 days')" + SQL.changes conn diff --git a/Omni/Ava/Web.hs b/Omni/Ava/Web.hs new file mode 100644 index 0000000..86a8280 --- /dev/null +++ b/Omni/Ava/Web.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Web server for Ava trace viewer. +-- +-- Serves the trace viewer UI for debugging tool executions. +-- +-- : out omni-ava-web +-- : dep warp +-- : dep wai +-- : dep http-types +-- : dep aeson +-- : dep text +-- : dep bytestring +-- : dep sqlite-simple +module Omni.Ava.Web + ( startWebServer, + app, + defaultPort, + ) +where + +import Alpha +import qualified Data.Aeson as Aeson +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Encoding +import qualified Data.Text.IO as TextIO +import qualified Database.SQLite.Simple as SQL +import qualified Network.HTTP.Types as HTTP +import qualified Network.Wai as Wai +import qualified Network.Wai.Handler.Warp as Warp +import qualified Omni.Ava.Trace as Trace +import qualified System.Environment as Environment + +defaultPort :: Int +defaultPort = 8079 + +startWebServer :: Int -> FilePath -> IO () +startWebServer port dbPath = do + putText <| "Starting Ava web server on port " <> tshow port + Warp.run port (app dbPath) + +app :: FilePath -> Wai.Application +app dbPath request respond = do + case Wai.pathInfo request of + ["trace", tid] -> serveTracePage dbPath tid request respond + ["api", "trace", tid] -> serveTraceJson dbPath tid request respond + ["health"] -> respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "text/plain")] "ok" + _ -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "text/plain")] "Not found" + +serveTracePage :: FilePath -> Text -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived +serveTracePage dbPath tid _req respond = do + SQL.withConnection dbPath <| \conn -> do + maybeRec <- Trace.getTrace conn tid + case maybeRec of + Nothing -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "text/plain")] "Trace not found" + Just rec -> do + html <- renderTraceHtml rec + respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "text/html; charset=utf-8")] (LBS.fromStrict <| Encoding.encodeUtf8 html) + +serveTraceJson :: FilePath -> Text -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived +serveTraceJson dbPath tid _req respond = do + SQL.withConnection dbPath <| \conn -> do + maybeRec <- Trace.getTrace conn tid + case maybeRec of + Nothing -> respond <| Wai.responseLBS HTTP.status404 [(HTTP.hContentType, "application/json")] "{\"error\":\"not found\"}" + Just rec -> do + let json = Aeson.encode rec + respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "application/json")] json + +renderTraceHtml :: Trace.TraceRecord -> IO Text +renderTraceHtml rec = do + coderoot <- Environment.getEnv "CODEROOT" + let templatePath = coderoot <> "/Omni/Ava/Web/trace.html" + template <- TextIO.readFile templatePath + pure + <| Text.replace "{{trace_id}}" (Trace.trcId rec) + <| Text.replace "{{tool_name}}" (Trace.trcToolName rec) + <| Text.replace "{{created_at}}" (Trace.trcCreatedAt rec) + <| Text.replace "{{duration_ms}}" (tshow <| Trace.trcDurationMs rec) + <| Text.replace "{{input_json}}" (Trace.trcInput rec) + <| Text.replace "{{output_json}}" (Trace.trcOutput rec) + <| template diff --git a/Omni/Ava/Web/trace.html b/Omni/Ava/Web/trace.html new file mode 100644 index 0000000..ce990a4 --- /dev/null +++ b/Omni/Ava/Web/trace.html @@ -0,0 +1,71 @@ + + + + + + Trace: {{tool_name}} + + + +

{{tool_name}}

+

{{created_at}} · {{duration_ms}}ms

+ +
+
+ Input + +
+
{{input_json}}
+
+ +
+
+ Output + +
+
{{output_json}}
+
+ + + + + + diff --git a/Omni/Bild.nix b/Omni/Bild.nix index cec1ea0..940de2d 100644 --- a/Omni/Bild.nix +++ b/Omni/Bild.nix @@ -111,6 +111,7 @@ inherit alejandra awscli2 + bash bat bc cmark @@ -118,6 +119,7 @@ universal-ctags datasette deadnix + direnv doctl fd figlet diff --git a/Omni/Bild/Deps/Haskell.nix b/Omni/Bild/Deps/Haskell.nix index 138a80e..e4830bb 100644 --- a/Omni/Bild/Deps/Haskell.nix +++ b/Omni/Bild/Deps/Haskell.nix @@ -32,6 +32,7 @@ "lucid" "monad-logger" "mtl" + "mustache" "neat-interpolation" "network-uri" "niv" diff --git a/Omni/Dev/Beryllium/Ava.nix b/Omni/Dev/Beryllium/Ava.nix index becbf9e..7563b26 100644 --- a/Omni/Dev/Beryllium/Ava.nix +++ b/Omni/Dev/Beryllium/Ava.nix @@ -35,12 +35,14 @@ bild.pkgs.git bild.pkgs.sqlite bild.pkgs.ffmpeg + bild.pkgs.bash pkgs.curl pkgs.pandoc pkgs.imagemagick pkgs.csvkit pkgs.openai-whisper-cpp pkgs.direnv + pkgs.nix ]} ''; }; @@ -61,6 +63,8 @@ in { "AVA_DATA_ROOT=/home/ava" "HOME=/home/ava" "OLLAMA_URL=http://localhost:11434" + "AVA_WEB_PORT=8079" + "AVA_WEB_URL=https://beryllium.oryx-ide.ts.net" ]; EnvironmentFile = "/run/secrets/ava.env"; @@ -111,4 +115,9 @@ in { chown ava:users /home/ava/.bashrc fi ''; + + # Note: Tailscale Funnel for Ava web server is configured via: + # tailscale funnel --bg 8079 + # This persists in tailscaled config and doesn't need a systemd service. + # URL: https://beryllium.oryx-ide.ts.net/ } diff --git a/Omni/Ide/hooks/pre-push b/Omni/Ide/hooks/pre-push index adbf858..86e6e3c 100755 --- a/Omni/Ide/hooks/pre-push +++ b/Omni/Ide/hooks/pre-push @@ -22,7 +22,7 @@ do else range="$remote_sha..$local_sha" fi + gitlint --commits "$range" lint + git test run --command ci "$range" done -gitlint --commits "$range" lint -git test run --command ci "$range" git push "$remote" refs/notes/ci --no-verify -- cgit v1.2.3