Class: Ask::Agent::Compactor

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/agent/compactor.rb

Constant Summary collapse

CONTEXT_WINDOWS =
{
  "gpt-4o" => 128_000,
  "gpt-4o-mini" => 128_000,
  "gpt-4-turbo" => 128_000,
  "claude-sonnet-4" => 200_000,
  "claude-4" => 200_000,
  "gemini-2.0-flash" => 1_048_576,
  "gemini-2.5-pro" => 1_048_576,
  "deepseek-v4-flash" => 1_000_000,
  "deepseek-v4-pro" => 1_000_000,
}.tap { |h| h.default = 128_000 }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(threshold: 0.8, strategy: :proactive, llm: nil) ⇒ Compactor

Returns a new instance of Compactor.



20
21
22
23
24
25
26
# File 'lib/ask/agent/compactor.rb', line 20

def initialize(threshold: 0.8, strategy: :proactive, llm: nil)
  @threshold = threshold
  @strategy = strategy
  @llm = llm
  @already_compacted = false
  @overflow_recovered = false
end

Instance Attribute Details

#chatObject

Returns the value of attribute chat.



18
19
20
# File 'lib/ask/agent/compactor.rb', line 18

def chat
  @chat
end

#llmObject

Returns the value of attribute llm.



18
19
20
# File 'lib/ask/agent/compactor.rb', line 18

def llm
  @llm
end

Instance Method Details

#compact!Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ask/agent/compactor.rb', line 48

def compact!
  return unless @chat
  messages = @chat.messages.dup
  return if messages.size < 6

  keep_count = [messages.size, 8].min
  recent = messages.last(keep_count)
  older = messages.first(messages.size - keep_count)
  return if older.empty?

  summary = if @llm
    generate_llm_summary(older) || generate_summary(older)
  else
    generate_summary(older)
  end

  older.size.times { @chat.messages.delete_at(0) }
  @chat.add_message(role: :system, content: "[Previous conversation summary]: #{summary}")
end

#context_windowObject



91
92
93
94
# File 'lib/ask/agent/compactor.rb', line 91

def context_window
  model = @chat.model.to_s
  CONTEXT_WINDOWS[model]
end

#estimate_tokens(text) ⇒ Object



82
83
84
# File 'lib/ask/agent/compactor.rb', line 82

def estimate_tokens(text)
  (text.to_s.length / 4.0).ceil
end

#estimate_total_tokensObject



86
87
88
89
# File 'lib/ask/agent/compactor.rb', line 86

def estimate_total_tokens
  return 0 unless @chat
  @chat.messages.sum { |msg| estimate_message_tokens(msg) }
end

#microcompact!Object



68
69
70
71
72
73
74
# File 'lib/ask/agent/compactor.rb', line 68

def microcompact!
  return unless @chat
  @chat.messages.each do |msg|
    next unless msg.role == :tool
    msg.content = "[Tool result cleared by compaction]" if msg.content.to_s.length > 200
  end
end

#overflow_recovered?Boolean

Returns:

  • (Boolean)


28
# File 'lib/ask/agent/compactor.rb', line 28

def overflow_recovered? = @overflow_recovered

#recover_from_overflowObject



76
77
78
79
80
# File 'lib/ask/agent/compactor.rb', line 76

def recover_from_overflow
  if @already_compacted then microcompact! else compact! end
  @already_compacted = true
  @overflow_recovered = true
end

#run(event_emitter: nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
# File 'lib/ask/agent/compactor.rb', line 37

def run(event_emitter: nil)
  return unless @chat

  tokens_before = estimate_total_tokens
  event_emitter&.emit(Events::CompactionStart.new(tokens_before: tokens_before, reason: :threshold))
  compact!
  tokens_after = estimate_total_tokens
  @already_compacted = true
  event_emitter&.emit(Events::CompactionEnd.new(tokens_before: tokens_before, tokens_after: tokens_after, summary: extract_summary))
end

#should_compact?Boolean

Returns:

  • (Boolean)


30
31
32
33
34
35
# File 'lib/ask/agent/compactor.rb', line 30

def should_compact?
  return false unless @chat
  current = estimate_total_tokens
  window = context_window
  current >= window * @threshold
end