Class: Legion::LLM::ContextCurator

Inherits:
Object
  • Object
show all
Includes:
Legion::Logging::Helper
Defined in:
lib/legion/llm/context_curator.rb

Constant Summary collapse

CURATED_KEY =
:__curated__

Instance Method Summary collapse

Constructor Details

#initialize(conversation_id:) ⇒ ContextCurator

Returns a new instance of ContextCurator.



11
12
13
14
# File 'lib/legion/llm/context_curator.rb', line 11

def initialize(conversation_id:)
  @conversation_id = conversation_id
  @curated_cache   = nil
end

Instance Method Details

#curate_turn(turn_messages:, assistant_response:) ⇒ Object

Called async after each turn completes — zero latency impact.



17
18
19
20
21
22
23
24
25
26
27
# File 'lib/legion/llm/context_curator.rb', line 17

def curate_turn(turn_messages:, assistant_response:)
  return unless enabled?

  Thread.new do
    curated = turn_messages.map { |msg| curate_message(msg, assistant_response) }
    store_curated(@conversation_id, curated)
    @curated_cache = nil
  rescue StandardError => e
    handle_exception(e, level: :warn)
  end
end

#curated_messagesObject

Called sync when building next API request. Returns curated messages when available; nil means use raw history.



31
32
33
34
35
# File 'lib/legion/llm/context_curator.rb', line 31

def curated_messages
  return nil unless enabled?

  @curated_messages ||= load_curated(@conversation_id)
end

#dedup_similar(messages, threshold: nil) ⇒ Object

Heuristic: deduplicate near-identical messages using Jaccard similarity.



105
106
107
108
109
110
111
# File 'lib/legion/llm/context_curator.rb', line 105

def dedup_similar(messages, threshold: nil)
  return messages unless setting(:dedup_enabled, true)

  threshold ||= setting(:dedup_threshold, 0.85)
  result = Compressor.deduplicate_messages(messages, threshold: threshold)
  result[:messages]
end

#distill_tool_result(msg, _assistant_context = nil) ⇒ Object

Heuristic: distill a single tool-result message to a compact summary.



38
39
40
41
42
43
44
45
# File 'lib/legion/llm/context_curator.rb', line 38

def distill_tool_result(msg, _assistant_context = nil)
  content = msg[:content].to_s
  max_chars = setting(:tool_result_max_chars, 2000)
  return msg if content.length <= max_chars

  summary = heuristic_tool_summary(content, tool_name_from(msg))
  msg.merge(content: summary, curated: true, original_content: content)
end

#evict_superseded(messages) ⇒ Object

Heuristic: if same file was read multiple times, keep only the latest read.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/legion/llm/context_curator.rb', line 89

def evict_superseded(messages)
  return messages unless setting(:superseded_eviction, true)

  file_last_seen = {}
  messages.each_with_index do |msg, idx|
    path = extract_file_path(msg[:content].to_s)
    file_last_seen[path] = idx if path
  end

  messages.each_with_index.reject do |msg, idx|
    path = extract_file_path(msg[:content].to_s)
    path && file_last_seen[path] != idx
  end.map(&:first)
end

#fold_resolved_exchanges(messages) ⇒ Object

Heuristic: detect multi-turn clarification that reached agreement; fold to single system note.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/legion/llm/context_curator.rb', line 63

def fold_resolved_exchanges(messages)
  return messages unless setting(:exchange_folding, true)

  result = []
  i = 0
  while i < messages.length
    window = messages[i, 4]
    if resolved_exchange?(window)
      conclusion = window.last[:content].to_s[0, 300]
      note = {
        role:             :system,
        content:          "[Exchange resolved: #{conclusion}]",
        curated:          true,
        original_content: window.map { |m| m[:content] }.join("\n")
      }
      result << note
      i += window.length
    else
      result << messages[i]
      i += 1
    end
  end
  result
end

#llm_distill_tool_result(msg, assistant_response = nil) ⇒ Object

LLM-assisted distillation: uses small/fast model to summarize tool results. Falls back to heuristic on any error.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/legion/llm/context_curator.rb', line 115

def llm_distill_tool_result(msg, assistant_response = nil)
  return distill_tool_result(msg, assistant_response) unless llm_assisted?

  content = msg[:content].to_s
  max_chars = setting(:tool_result_max_chars, 2000)
  return msg if content.length <= max_chars

  summary = llm_summarize_tool_result(content, tool_name_from(msg))
  if summary
    msg.merge(content: summary, curated: true, original_content: content)
  else
    distill_tool_result(msg, assistant_response)
  end
rescue StandardError => e
  handle_exception(e, level: :warn)
  distill_tool_result(msg, assistant_response)
end

#strip_thinking(msg) ⇒ Object

Heuristic: remove extended thinking blocks, keep conclusions.



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/legion/llm/context_curator.rb', line 48

def strip_thinking(msg)
  return msg unless setting(:thinking_eviction, true)

  content = msg[:content].to_s
  stripped = content
             .gsub(%r{<thinking>.*?</thinking>}m, '')
             .gsub(/^#+\s*[Tt]hinking.*?\n(?:(?!^#+\s).)*\n/m, '')
             .strip

  return msg if stripped == content || stripped.empty?

  msg.merge(content: stripped, curated: true, original_content: content)
end