never executed always true always false
    1 module PureClaw.Agent.Compaction
    2   ( -- * Compaction
    3     compactContext
    4   , CompactionResult (..)
    5     -- * Configuration
    6   , defaultTokenLimit
    7   , defaultKeepRecent
    8   ) where
    9 
   10 import Control.Exception
   11 import Data.Text (Text)
   12 import Data.Text qualified as T
   13 
   14 import PureClaw.Agent.Context
   15 import PureClaw.Core.Types
   16 import PureClaw.Providers.Class
   17 
   18 -- | Result of a compaction attempt.
   19 data CompactionResult
   20   = Compacted Int Int     -- ^ old message count, new message count
   21   | NotNeeded             -- ^ context is below threshold
   22   | CompactionError Text -- ^ provider error during summarization
   23   deriving stock (Show, Eq)
   24 
   25 -- | Default context window token limit (200k for Claude).
   26 defaultTokenLimit :: Int
   27 defaultTokenLimit = 200000
   28 
   29 -- | Default number of recent messages to keep uncompacted.
   30 defaultKeepRecent :: Int
   31 defaultKeepRecent = 10
   32 
   33 -- | Compact a context by summarizing old messages via the provider.
   34 --
   35 -- Keeps the most recent @keepRecent@ messages intact and asks the
   36 -- provider to summarize the older ones into a single message. The
   37 -- summary replaces the old messages in the context.
   38 --
   39 -- Returns 'NotNeeded' if the context is below the token threshold.
   40 compactContext
   41   :: Provider p
   42   => p
   43   -> ModelId
   44   -> Int        -- ^ token threshold (compact when estimated tokens exceed this)
   45   -> Int        -- ^ number of recent messages to keep
   46   -> Context
   47   -> IO (Context, CompactionResult)
   48 compactContext provider model threshold keepRecent ctx
   49   | contextTokenEstimate ctx < threshold = pure (ctx, NotNeeded)
   50   | contextMessageCount ctx <= keepRecent = pure (ctx, NotNeeded)
   51   | otherwise = do
   52       let msgs = contextMessages ctx
   53           oldCount = length msgs - keepRecent
   54           (oldMsgs, recentMsgs) = splitAt oldCount msgs
   55           summaryPrompt = buildSummaryPrompt oldMsgs
   56       summaryResult <- summarize provider model summaryPrompt
   57       case summaryResult of
   58         Left err -> pure (ctx, CompactionError err)
   59         Right summaryText ->
   60           let summaryMsg = textMessage User ("[Context summary] " <> summaryText)
   61               newMsgs = summaryMsg : recentMsgs
   62               ctx' = replaceMessages newMsgs ctx
   63           in pure (ctx', Compacted (length msgs) (length newMsgs))
   64 
   65 -- | Build a prompt asking the provider to summarize conversation history.
   66 buildSummaryPrompt :: [Message] -> Text
   67 buildSummaryPrompt msgs =
   68   "Summarize the following conversation history concisely. "
   69   <> "Preserve key facts, decisions, file paths, and context needed "
   70   <> "to continue the conversation. Be brief but complete.\n\n"
   71   <> T.intercalate "\n\n" (map formatMessage msgs)
   72 
   73 -- | Format a message for the summary prompt.
   74 formatMessage :: Message -> Text
   75 formatMessage msg =
   76   let role = case _msg_role msg of
   77         User      -> "User"
   78         Assistant -> "Assistant"
   79       content = T.intercalate " " [extractText b | b <- _msg_content msg]
   80   in role <> ": " <> content
   81 
   82 -- | Extract text content from a block.
   83 extractText :: ContentBlock -> Text
   84 extractText (TextBlock t) = t
   85 extractText (ToolUseBlock _ name _) = "[tool:" <> name <> "]"
   86 extractText (ImageBlock mediaType _) = "[image:" <> mediaType <> "]"
   87 extractText (ToolResultBlock _ parts _) = "[result:" <> T.take 100 (partsText parts) <> "]"
   88 
   89 -- | Extract text from tool result parts.
   90 partsText :: [ToolResultPart] -> Text
   91 partsText ps = T.intercalate " " [t | TRPText t <- ps]
   92 
   93 -- | Call the provider to generate a summary.
   94 summarize :: Provider p => p -> ModelId -> Text -> IO (Either Text Text)
   95 summarize provider model prompt = do
   96   let req = CompletionRequest
   97         { _cr_model        = model
   98         , _cr_messages     = [textMessage User prompt]
   99         , _cr_systemPrompt = Just "You are a conversation summarizer. Produce a concise summary."
  100         , _cr_maxTokens    = Just 1024
  101         , _cr_tools        = []
  102         , _cr_toolChoice   = Nothing
  103         }
  104   result <- try @SomeException (complete provider req)
  105   case result of
  106     Left e -> pure (Left (T.pack (show e)))
  107     Right resp ->
  108       let text = responseText resp
  109       in if T.null (T.strip text)
  110            then pure (Left "Provider returned empty summary")
  111            else pure (Right text)