never executed always true always false
1 module PureClaw.Agent.Context
2 ( -- * Context type (constructor intentionally NOT exported)
3 Context
4 -- * Construction
5 , emptyContext
6 -- * Operations
7 , addMessage
8 , contextMessages
9 , contextSystemPrompt
10 -- * Token tracking
11 , estimateTokens
12 , estimateBlockTokens
13 , estimateMessageTokens
14 , contextTokenEstimate
15 -- * Usage tracking
16 , recordUsage
17 , contextTotalInputTokens
18 , contextTotalOutputTokens
19 -- * Context management
20 , contextMessageCount
21 , replaceMessages
22 , clearMessages
23 ) where
24
25 import Data.Aeson (encode)
26 import Data.ByteString qualified as BS
27 import Data.ByteString.Lazy qualified as BL
28 import Data.Text (Text)
29 import Data.Text qualified as T
30
31 import PureClaw.Providers.Class
32
33 -- | Conversation context. Maintains message history, an optional
34 -- system prompt, and cumulative token usage. Constructor is not
35 -- exported — use 'emptyContext'.
36 data Context = Context
37 { _ctx_systemPrompt :: Maybe Text
38 , _ctx_messages :: [Message] -- oldest first
39 , _ctx_totalInputTokens :: !Int
40 , _ctx_totalOutputTokens :: !Int
41 }
42 deriving stock (Show, Eq)
43
44 -- | Create an empty context with an optional system prompt.
45 emptyContext :: Maybe Text -> Context
46 emptyContext sys = Context sys [] 0 0
47
48 -- | Append a message to the conversation history.
49 addMessage :: Message -> Context -> Context
50 addMessage msg ctx = ctx { _ctx_messages = _ctx_messages ctx ++ [msg] }
51
52 -- | Get all messages in chronological order.
53 contextMessages :: Context -> [Message]
54 contextMessages = _ctx_messages
55
56 -- | Get the system prompt, if any.
57 contextSystemPrompt :: Context -> Maybe Text
58 contextSystemPrompt = _ctx_systemPrompt
59
60 -- Token estimation (approximate: ~4 characters per token for mixed
61 -- English text and code). Used for context window management — the
62 -- provider returns actual usage after each completion.
63
64 -- | Estimate token count for a text string.
65 -- Uses the ~4 characters per token heuristic.
66 estimateTokens :: Text -> Int
67 estimateTokens t
68 | T.null t = 0
69 | otherwise = max 1 (T.length t `div` 4)
70
71 -- | Estimate token count for a content block.
72 estimateBlockTokens :: ContentBlock -> Int
73 estimateBlockTokens (TextBlock t) = estimateTokens t
74 estimateBlockTokens (ToolUseBlock _ name input) =
75 estimateTokens name + fromIntegral (BL.length (encode input) `div` 4)
76 estimateBlockTokens (ImageBlock _ bs) = max 1 (BS.length bs `div` 4)
77 estimateBlockTokens (ToolResultBlock _ parts _) = sum (map estimatePartTokens parts)
78
79 -- | Estimate tokens for a tool result part.
80 estimatePartTokens :: ToolResultPart -> Int
81 estimatePartTokens (TRPText t) = estimateTokens t
82 estimatePartTokens (TRPImage _ bs) = max 1 (BS.length bs `div` 4)
83
84 -- | Estimate token count for a message.
85 estimateMessageTokens :: Message -> Int
86 estimateMessageTokens msg =
87 4 + sum (map estimateBlockTokens (_msg_content msg))
88 -- 4 tokens overhead per message for role, delimiters
89
90 -- | Estimate total token count of the current context window.
91 -- Includes system prompt and all messages.
92 contextTokenEstimate :: Context -> Int
93 contextTokenEstimate ctx =
94 let sysTokens = maybe 0 estimateTokens (_ctx_systemPrompt ctx)
95 msgTokens = sum (map estimateMessageTokens (_ctx_messages ctx))
96 in sysTokens + msgTokens
97
98 -- Usage tracking — records actual provider-reported token usage.
99
100 -- | Record usage from a provider response.
101 recordUsage :: Maybe Usage -> Context -> Context
102 recordUsage Nothing ctx = ctx
103 recordUsage (Just usage) ctx = ctx
104 { _ctx_totalInputTokens = _ctx_totalInputTokens ctx + _usage_inputTokens usage
105 , _ctx_totalOutputTokens = _ctx_totalOutputTokens ctx + _usage_outputTokens usage
106 }
107
108 -- | Get total input tokens consumed (from provider reports).
109 contextTotalInputTokens :: Context -> Int
110 contextTotalInputTokens = _ctx_totalInputTokens
111
112 -- | Get total output tokens consumed (from provider reports).
113 contextTotalOutputTokens :: Context -> Int
114 contextTotalOutputTokens = _ctx_totalOutputTokens
115
116 -- Context management — for compaction and session reset.
117
118 -- | Get the number of messages in the context.
119 contextMessageCount :: Context -> Int
120 contextMessageCount = length . _ctx_messages
121
122 -- | Replace all messages with a new list. Used by compaction to
123 -- swap old messages for a summary.
124 replaceMessages :: [Message] -> Context -> Context
125 replaceMessages msgs ctx = ctx { _ctx_messages = msgs }
126
127 -- | Clear all messages (keep system prompt and usage counters).
128 -- Used by @/new@ to start a fresh session.
129 clearMessages :: Context -> Context
130 clearMessages ctx = ctx { _ctx_messages = [] }