Class: Legion::LLM::Context::Curator

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

Constant Summary collapse

CURATED_KEY =
:__curated__
THINKING_OPEN =
'<thinking>'
THINKING_CLOSE =
'</thinking>'

Instance Method Summary collapse

Constructor Details

#initialize(conversation_id:) ⇒ Curator

Returns a new instance of Curator.



14
15
16
17
# File 'lib/legion/llm/context/curator.rb', line 14

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.



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/legion/llm/context/curator.rb', line 20

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.



34
35
36
37
38
# File 'lib/legion/llm/context/curator.rb', line 34

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.



106
107
108
109
110
111
112
# File 'lib/legion/llm/context/curator.rb', line 106

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

  threshold ||= setting(:dedup_threshold, 0.85)
  result = Context::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.



41
42
43
44
45
46
47
48
# File 'lib/legion/llm/context/curator.rb', line 41

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.



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

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.



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

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.



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

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.



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/legion/llm/context/curator.rb', line 51

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

  content = msg[:content].to_s
  stripped = strip_thinking_tags(content)
  stripped = stripped.gsub(/^#+\s*[Tt]hinking[^\n]*\n(?:[^#\n][^\n]*\n)*/m, '').strip

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

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