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 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) (limited to 'Omni/Agent/Engine.hs') 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 -- cgit v1.2.3