summaryrefslogtreecommitdiff
path: root/Omni/Agent/Log.hs
blob: 28dbab24b40db6c3a4ee2d207a4c5014e9e711b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Omni.Agent.Log
  ( LogEntry (..),
    Status (..),
    initialStatus,
    updateStatus,
    renderStatus,
    parseLine,
    format,
  )
where

import Alpha
import Data.Aeson (FromJSON (..), (.:), (.:?))
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Set as Set

data LogEntry = LogEntry
  { leMessage :: Text,
    leLevel :: Maybe Text,
    leToolName :: Maybe Text,
    leBatches :: Maybe [[Text]],
    leMethod :: Maybe Text,
    lePath :: Maybe Text,
    leTimestamp :: Maybe Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON LogEntry where
  parseJSON =
    Aeson.withObject "LogEntry" <| \v ->
      ( LogEntry
          </ (v .: "message")
      )
        <*> v
        .:? "level"
        <*> v
        .:? "toolName"
        <*> v
        .:? "batches"
        <*> v
        .:? "method"
        <*> v
        .:? "path"
        <*> v
        .:? "timestamp"

data Status = Status
  { sWorkerName :: Text,
    sTaskId :: Maybe Text,
    sFiles :: Set.Set Text,
    sStartTime :: Maybe Text,
    sLastActivity :: Text
  }
  deriving (Show, Eq, Generic)

initialStatus :: Text -> Status
initialStatus name =
  Status
    { sWorkerName = name,
      sTaskId = Nothing,
      sFiles = Set.empty,
      sStartTime = Nothing,
      sLastActivity = "Idle"
    }

updateStatus :: LogEntry -> Status -> Status
updateStatus e s =
  let s' = case format e of
        Just msg -> s {sLastActivity = msg}
        Nothing -> s
      s'' = case leTimestamp e of
        Just t -> if isNothing (sStartTime s) then s' {sStartTime = Just t} else s'
        Nothing -> s'
   in case (leMessage e, lePath e) of
        ("ide-fs", Just p) -> s'' {sFiles = Set.insert p (sFiles s'')}
        _ -> s''

renderStatus :: Status -> Text
renderStatus s =
  let line1 =
        "[Worker: "
          <> sWorkerName s
          <> "] "
          <> "Task: "
          <> fromMaybe "None" (sTaskId s)
          <> " | Files: "
          <> show (Set.size (sFiles s))
      line2 = sLastActivity s
   in line1 <> "\n" <> line2

parseLine :: Text -> Maybe LogEntry
parseLine line = Aeson.decode <| BSL.fromStrict <| encodeUtf8 line

format :: LogEntry -> Maybe Text
format e =
  case leMessage e of
    "executing 1 tools in 1 batch(es)" ->
      let tool = case leBatches e of
            Just ((t : _) : _) -> t
            _ -> "unknown"
       in Just <| "🤖  THOUGHT: Planning tool execution (" <> tool <> ")"
    "Tool Bash permitted - action: allow" ->
      Just "🔧  TOOL: Bash command executed"
    msg
      | "Processing tool completion for ledger" == msg && isJust (leToolName e) ->
          Just <| "✅  TOOL: " <> fromMaybe "" (leToolName e) <> " completed"
    "ide-fs" ->
      case leMethod e of
        Just "readFile" -> Just <| "📂  READ: " <> fromMaybe "" (lePath e)
        _ -> Nothing
    "System prompt build complete (no changes)" ->
      Just "🧠  THINKING..."
    "System prompt build complete (first build)" ->
      Just "🚀  STARTING new task context"
    msg ->
      case leLevel e of
        Just "error" -> Just <| "❌  ERROR: " <> msg
        _ -> Nothing