Skip to content

Conversation Pruner — Ruby source

Plan how to fit a long chat history into a context budget — which turns to keep, fold into a summary, or drop, protecting system messages and the current request. 100% client-side.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# Conversation Pruner — compute a deterministic pruning plan for a
# token-budgeted chat history.
#
# Language: Ruby (3.x, zero dependencies)
# Port of src/lib/conversationPruner.ts (the canonical TypeScript
#           implementation) for the CosmoDev polyglot showcase
#           (slug: conversation-pruner).
#
# Given per-message token counts and a context budget, decide which messages
# to keep verbatim, which to fold into one running summary, and which to drop
# outright — protecting system messages, pinned turns, the first turn, and
# the current (last user) request.

module ConversationPruner
  # Summary compression model: fixed framing tokens.
  SUMMARY_FIXED_TOKENS = 60

  # Summary compression model: share of the folded content.
  SUMMARY_RATIO = 0.1

  Message = Struct.new(:role, :content, :tokens, :pinned, keyword_init: true) do
    def initialize(role:, content:, tokens:, pinned: false)
      super
    end
  end

  Decision = Struct.new(:index, :role, :action, :tokens, keyword_init: true)

  Plan = Struct.new(
    :decisions, :kept_tokens, :summarized_tokens, :dropped_tokens,
    :summary_cost_tokens, :projected_tokens, :fits_budget, :warnings,
    keyword_init: true
  )

  module_function

  # Group an integer with thousands separators (toLocaleString stand-in).
  def fmt(v)
    v.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
  end

  # Compute the pruning plan for +messages+ under +budget_tokens+.
  # Raises ArgumentError on a negative budget or any negative per-message
  # token count.
  def plan_prune(messages, budget_tokens:)
    raise ArgumentError, 'budgetTokens must be >= 0' if budget_tokens.negative?
    raise ArgumentError, 'message tokens must be >= 0' if messages.any? { |m| m.tokens.negative? }

    warnings = []
    n = messages.length
    last_user = messages.rindex { |m| m.role == 'user' } || -1

    # Untouchable: every system message, pinned messages, the first turn
    # (the opening user request), and the current request (the last user
    # message and everything after it).
    protected_idx = {}
    messages.each_with_index do |m, i|
      protected_idx[i] = true if m.role == 'system' || m.pinned
    end
    protected_idx[0] = true if n.positive?
    first_turn = messages.index { |m| m.role != 'system' }
    protected_idx[first_turn] = true unless first_turn.nil?
    from = last_user == -1 ? n - 1 : last_user
    ([from, 0].max...n).each { |i| protected_idx[i] = true }

    protected_tokens = protected_idx.keys.sum { |i| messages[i].tokens }
    if protected_tokens > budget_tokens
      warnings << "Protected messages alone are #{fmt(protected_tokens)} tokens against a " \
                  "#{fmt(budget_tokens)} budget — raise the budget (or reserve less for the " \
                  'reply) before pruning anything else.'
    end

    # Fill the remaining budget newest-to-oldest through the middle.
    actions = Array.new(n, 'drop')
    protected_idx.each_key { |i| actions[i] = 'keep' }
    used = protected_tokens
    (n - 1).downto(0) do |i|
      next unless actions[i] == 'drop'

      if used + messages[i].tokens <= budget_tokens
        actions[i] = 'keep'
        used += messages[i].tokens
      else
        break # oldest-unfilled remain drop/summarize candidates
      end
    end

    # Everything still 'drop' in the middle folds into ONE running summary
    # when the compressed form fits where the raw turns did not.
    summarize_idx = (0...n).select { |i| actions[i] == 'drop' && !protected_idx.key?(i) }
    summarize_tokens = summarize_idx.sum { |i| messages[i].tokens }
    attempted = summarize_idx.empty? ? 0 : SUMMARY_FIXED_TOKENS + (summarize_tokens * SUMMARY_RATIO).ceil

    # The summary only costs anything when it is actually applied.
    summary_cost = 0
    if attempted.positive? && used + attempted <= budget_tokens
      summarize_idx.each { |i| actions[i] = 'summarize' }
      summary_cost = attempted
      used += summary_cost
    elsif attempted.positive?
      warnings << "Even the compressed summary (#{fmt(attempted)} tokens) does not fit the " \
                  'remaining budget — the oldest turns are dropped instead.'
    end

    decisions = messages.each_with_index.map do |m, i|
      Decision.new(index: i, role: m.role, action: actions[i], tokens: m.tokens)
    end

    kept = dropped = folded = 0
    decisions.each do |d|
      case d.action
      when 'keep' then kept += d.tokens
      when 'drop' then dropped += d.tokens
      else folded += d.tokens
      end
    end

    Plan.new(
      decisions: decisions, kept_tokens: kept, summarized_tokens: folded,
      dropped_tokens: dropped, summary_cost_tokens: summary_cost,
      projected_tokens: kept + summary_cost,
      fits_budget: kept + summary_cost <= budget_tokens,
      warnings: warnings
    )
  end

  # Human-readable one-line summary of a plan.
  def describe_prune(plan)
    unless plan.fits_budget
      return "Does not fit: #{fmt(plan.projected_tokens)} tokens projected against the budget."
    end

    parts = ["#{fmt(plan.kept_tokens)} kept"]
    if plan.summarized_tokens.positive?
      parts << "#{fmt(plan.summarized_tokens)} folded into a #{fmt(plan.summary_cost_tokens)}-token summary"
    end
    parts << "#{fmt(plan.dropped_tokens)} dropped" if plan.dropped_tokens.positive?
    "#{parts.join(' · ')} — fits the budget."
  end
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →