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



98
99
100
101
# File 'lib/legion/mcp/observer.rb', line 98

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

.buffer_mutexObject



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

def buffer_mutex
  @buffer_mutex ||= Mutex.new
end

.countersObject

Internal state accessors



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

def counters
  @counters ||= {}
end

.counters_mutexObject



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

def counters_mutex
  @counters_mutex ||= Mutex.new
end

.intent_bufferObject



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

def intent_buffer
  @intent_buffer ||= []
end

.intent_mutexObject



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

def intent_mutex
  @intent_mutex ||= Mutex.new
end

.recent(limit = 10) ⇒ Object



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

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

.recent_intents(limit = 10) ⇒ Object



127
128
129
# File 'lib/legion/mcp/observer.rb', line 127

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

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



16
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
# File 'lib/legion/mcp/observer.rb', line 16

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



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

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



51
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
77
78
79
# File 'lib/legion/mcp/observer.rb', line 51

def record_intent_with_result(intent:, tool_name:, success:, request_id: nil)
  log.debug("[mcp][observer] action=record_intent_with_result tool=#{tool_name} success=#{success}")
  record_intent(intent, tool_name)
  return unless success
  return unless defined?(Legion::MCP::Patterns::Store)

  normalized  = intent.to_s.strip.downcase.gsub(/\s+/, ' ')
  intent_hash = Digest::SHA256.hexdigest(normalized)
  candidate_key = Digest::SHA256.hexdigest("#{normalized}:#{tool_name}")

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

  return unless promotion&.dig(:promote)

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

.reset!Object



131
132
133
134
135
136
# File 'lib/legion/mcp/observer.rb', line 131

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



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

def ring_buffer
  @ring_buffer ||= []
end

.started_atObject



163
164
165
# File 'lib/legion/mcp/observer.rb', line 163

def started_at
  @started_at ||= Time.now
end

.statsObject



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

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



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

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



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

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')
  nil
end