Module: Legion::MCP::Observer

Extended by:
Logging::Helper
Defined in:
lib/legion/mcp/observer.rb

Constant Summary collapse

RING_BUFFER_MAX =
500
INTENT_BUFFER_MAX =
200

Class Method Summary collapse

Class Method Details

.all_tool_statsObject



95
96
97
98
# File 'lib/legion/mcp/observer.rb', line 95

def all_tool_stats
  names = counters_mutex.synchronize { counters.keys.dup }
  names.to_h { |name| [name, tool_stats(name)] }
end

.buffer_mutexObject



148
149
150
# File 'lib/legion/mcp/observer.rb', line 148

def buffer_mutex
  @buffer_mutex ||= Mutex.new
end

.countersObject

Internal state accessors



136
137
138
# File 'lib/legion/mcp/observer.rb', line 136

def counters
  @counters ||= {}
end

.counters_mutexObject



140
141
142
# File 'lib/legion/mcp/observer.rb', line 140

def counters_mutex
  @counters_mutex ||= Mutex.new
end

.intent_bufferObject



152
153
154
# File 'lib/legion/mcp/observer.rb', line 152

def intent_buffer
  @intent_buffer ||= []
end

.intent_mutexObject



156
157
158
# File 'lib/legion/mcp/observer.rb', line 156

def intent_mutex
  @intent_mutex ||= Mutex.new
end

.recent(limit = 10) ⇒ Object



120
121
122
# File 'lib/legion/mcp/observer.rb', line 120

def recent(limit = 10)
  buffer_mutex.synchronize { ring_buffer.last(limit) }
end

.recent_intents(limit = 10) ⇒ Object



124
125
126
# File 'lib/legion/mcp/observer.rb', line 124

def recent_intents(limit = 10)
  intent_mutex.synchronize { intent_buffer.last(limit) }
end

.record(tool_name:, duration_ms:, success:, params_keys: [], error: nil) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/legion/mcp/observer.rb', line 17

def record(tool_name:, duration_ms:, success:, params_keys: [], error: nil)
  now = Time.now

  counters_mutex.synchronize do
    entry = counters[tool_name] || { call_count: 0, total_latency_ms: 0.0, failure_count: 0,
                                     last_used: nil, last_error: nil }
    counters[tool_name] = {
      call_count:       entry[:call_count] + 1,
      total_latency_ms: entry[:total_latency_ms] + duration_ms.to_f,
      failure_count:    entry[:failure_count] + (success ? 0 : 1),
      last_used:        now,
      last_error:       success ? entry[:last_error] : error
    }
  end

  buffer_mutex.synchronize do
    ring_buffer << {
      tool_name:   tool_name,
      duration_ms: duration_ms,
      success:     success,
      params_keys: params_keys,
      error:       error,
      recorded_at: now
    }
    ring_buffer.shift if ring_buffer.size > RING_BUFFER_MAX
  end
end

.record_intent(intent, matched_tool_name) ⇒ Object



45
46
47
48
49
50
# File 'lib/legion/mcp/observer.rb', line 45

def record_intent(intent, matched_tool_name)
  intent_mutex.synchronize do
    intent_buffer << { intent: intent, matched_tool: matched_tool_name, recorded_at: Time.now }
    intent_buffer.shift if intent_buffer.size > INTENT_BUFFER_MAX
  end
end

.record_intent_with_result(intent:, tool_name:, success:, request_id: nil) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/legion/mcp/observer.rb', line 52

def record_intent_with_result(intent:, tool_name:, success:, request_id: nil)
  record_intent(intent, tool_name)
  return unless success
  return unless defined?(Legion::MCP::PatternStore)

  normalized  = intent.to_s.strip.downcase.gsub(/\s+/, ' ')
  intent_hash = Digest::SHA256.hexdigest(normalized)

  promotion = Legion::MCP::PatternStore.record_candidate(
    intent_hash: intent_hash,
    tool_chain:  [tool_name],
    intent_text: intent,
    request_id:  request_id
  )

  return unless promotion&.dig(:promote)

  Legion::MCP::PatternStore.promote_candidate(
    intent_hash:   promotion[:intent_hash],
    tool_chain:    promotion[:tool_chain],
    intent_text:   promotion[:intent_text],
    intent_vector: try_embed(normalized),
    request_id:    request_id
  )
end

.reset!Object



128
129
130
131
132
133
# File 'lib/legion/mcp/observer.rb', line 128

def reset!
  counters_mutex.synchronize { counters.clear }
  buffer_mutex.synchronize { ring_buffer.clear }
  intent_mutex.synchronize { intent_buffer.clear }
  @started_at = Time.now
end

.ring_bufferObject



144
145
146
# File 'lib/legion/mcp/observer.rb', line 144

def ring_buffer
  @ring_buffer ||= []
end

.started_atObject



160
161
162
# File 'lib/legion/mcp/observer.rb', line 160

def started_at
  @started_at ||= Time.now
end

.statsObject



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/legion/mcp/observer.rb', line 100

def stats
  all_names = counters_mutex.synchronize { counters.keys.dup }
  total     = all_names.sum { |n| counters_mutex.synchronize { counters[n][:call_count] } }
  failures  = all_names.sum { |n| counters_mutex.synchronize { counters[n][:failure_count] } }
  rate      = total.positive? ? (failures.to_f / total).round(4) : 0.0

  top = all_names
        .map { |n| tool_stats(n) }
        .sort_by { |s| -s[:call_count] }
        .first(10)

  {
    total_calls:  total,
    tool_count:   all_names.size,
    failure_rate: rate,
    top_tools:    top,
    since:        started_at
  }
end

.tool_stats(tool_name) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/legion/mcp/observer.rb', line 78

def tool_stats(tool_name)
  entry = counters_mutex.synchronize { counters[tool_name] }
  return nil unless entry

  count = entry[:call_count]
  avg   = count.positive? ? (entry[:total_latency_ms] / count).round(2) : 0.0

  {
    name:           tool_name,
    call_count:     count,
    avg_latency_ms: avg,
    failure_count:  entry[:failure_count],
    last_used:      entry[:last_used],
    last_error:     entry[:last_error]
  }
end

.try_embed(text) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/legion/mcp/observer.rb', line 164

def try_embed(text)
  return nil unless defined?(Legion::MCP::EmbeddingIndex)

  embedder = Legion::MCP::EmbeddingIndex.instance_variable_get(:@embedder)
  return nil unless embedder

  embedder.call(text)
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'legion.mcp.observer.try_embed')
  LoggingSupport.debug('observer.embed.failed', error: e.message)
  nil
end