Class: RubyLLM::MCP::Handlers::HumanInTheLoopRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb

Overview

Registry for tracking pending human-in-the-loop approvals. Registries are scoped per native client, with global ID routing so approval IDs can still be completed externally.

Constant Summary collapse

GLOBAL_OWNER =
"__global__"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(owner_id:) ⇒ HumanInTheLoopRegistry

Returns a new instance of HumanInTheLoopRegistry.



121
122
123
124
125
126
127
128
129
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 121

def initialize(owner_id:)
  @owner_id = owner_id
  @registry = {}
  @deadlines = {}
  @registry_mutex = Mutex.new
  @condition = ConditionVariable.new
  @stopped = false
  start_timeout_scheduler
end

Instance Attribute Details

#owner_idObject (readonly)

Returns the value of attribute owner_id.



119
120
121
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 119

def owner_id
  @owner_id
end

Class Method Details

.approve(id) ⇒ Object



44
45
46
47
48
49
50
51
52
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 44

def approve(id)
  registry = route_registry(id)
  if registry
    registry.approve(id)
  else
    RubyLLM::MCP.logger.warn("Attempted to approve unknown approval #{id}")
    false
  end
end

.clear(owner_id: nil) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 64

def clear(owner_id: nil)
  if owner_id
    release(owner_id)
  else
    registries = registries_mutex.synchronize do
      current = (@registries ||= {}).values
      @registries = {}
      current
    end
    registries.each(&:shutdown)
  end
end

.deny(id, reason: "Denied") ⇒ Object



54
55
56
57
58
59
60
61
62
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 54

def deny(id, reason: "Denied")
  registry = route_registry(id)
  if registry
    registry.deny(id, reason: reason)
  else
    RubyLLM::MCP.logger.warn("Attempted to deny unknown approval #{id}")
    false
  end
end

.for_owner(owner_id) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 17

def for_owner(owner_id)
  key = owner_id.to_s
  registries_mutex.synchronize do
    @registries ||= {}
    @registries[key] ||= new(owner_id: key)
  end
end

.instanceObject



13
14
15
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 13

def instance
  for_owner(GLOBAL_OWNER)
end

.register_approval(id, owner_id) ⇒ Object



86
87
88
89
90
91
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 86

def register_approval(id, owner_id)
  approval_index_mutex.synchronize do
    @approval_index ||= {}
    @approval_index[id.to_s] = owner_id.to_s
  end
end

.release(owner_id) ⇒ Object



25
26
27
28
29
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 25

def release(owner_id)
  key = owner_id.to_s
  registry = registries_mutex.synchronize { (@registries ||= {}).delete(key) }
  registry&.shutdown
end

.remove(id) ⇒ Object



40
41
42
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 40

def remove(id)
  route_registry(id)&.remove(id)
end

.retrieve(id) ⇒ Object



36
37
38
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 36

def retrieve(id)
  route_registry(id)&.retrieve(id)
end

.route_registry(id) ⇒ Object



99
100
101
102
103
104
105
106
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 99

def route_registry(id)
  owner_id = approval_index_mutex.synchronize { (@approval_index ||= {})[id.to_s] }
  if owner_id
    registries_mutex.synchronize { (@registries ||= {})[owner_id] }
  else
    registries_mutex.synchronize { (@registries ||= {})[GLOBAL_OWNER] }
  end
end

.size(owner_id: nil) ⇒ Object



77
78
79
80
81
82
83
84
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 77

def size(owner_id: nil)
  if owner_id
    registry = registries_mutex.synchronize { (@registries ||= {})[owner_id.to_s] }
    registry ? registry.size : 0
  else
    registries_mutex.synchronize { (@registries ||= {}).values.sum(&:size) }
  end
end

.store(id, approval) ⇒ Object

Backward-compatible global store path.



32
33
34
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 32

def store(id, approval)
  instance.store(id, approval)
end

.unregister_approval(id) ⇒ Object



93
94
95
96
97
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 93

def unregister_approval(id)
  approval_index_mutex.synchronize do
    (@approval_index ||= {}).delete(id.to_s)
  end
end

Instance Method Details

#approve(id) ⇒ Object

rubocop:disable Naming/PredicateMethod



169
170
171
172
173
174
175
176
177
178
179
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 169

def approve(id) # rubocop:disable Naming/PredicateMethod
  approval = remove(id)
  unless approval && approval[:promise]
    RubyLLM::MCP.logger.warn("Attempted to approve unknown approval #{id}")
    return false
  end

  RubyLLM::MCP.logger.info("Approving #{id}")
  approval[:promise].resolve(true)
  true
end

#clearObject



193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 193

def clear
  approvals = @registry_mutex.synchronize do
    current = @registry.dup
    @registry.clear
    @deadlines.clear
    @condition.broadcast
    current
  end

  approvals.each_key { |id| self.class.unregister_approval(id) }
  RubyLLM::MCP.logger.debug("Cleared human-in-the-loop registry for #{owner_id}")
end

#deny(id, reason: "Denied") ⇒ Object

rubocop:disable Naming/PredicateMethod



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 181

def deny(id, reason: "Denied") # rubocop:disable Naming/PredicateMethod
  approval = remove(id)
  unless approval && approval[:promise]
    RubyLLM::MCP.logger.warn("Attempted to deny unknown approval #{id}")
    return false
  end

  RubyLLM::MCP.logger.info("Denying #{id}: #{reason}")
  approval[:promise].resolve(false)
  true
end

#remove(id) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 154

def remove(id)
  key = id.to_s
  approval = nil

  @registry_mutex.synchronize do
    approval = @registry.delete(key)
    @deadlines.delete(key)
    @condition.signal
  end

  self.class.unregister_approval(key) if approval
  RubyLLM::MCP.logger.debug("Removed approval #{key} from registry for #{owner_id}") if approval
  approval
end

#retrieve(id) ⇒ Object



150
151
152
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 150

def retrieve(id)
  @registry_mutex.synchronize { @registry[id.to_s] }
end

#shutdownObject



210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 210

def shutdown
  clear
  @registry_mutex.synchronize do
    @stopped = true
    @condition.broadcast
  end
  @scheduler_thread&.join(0.5)
rescue StandardError => e
  RubyLLM::MCP.logger.debug("Error shutting down approval registry #{owner_id}: #{e.message}")
ensure
  @scheduler_thread = nil
end

#sizeObject



206
207
208
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 206

def size
  @registry_mutex.synchronize { @registry.size }
end

#store(id, approval_context) ⇒ Object

Store approval context: { promise:, timeout:, tool_name:, parameters: }



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb', line 132

def store(id, approval_context)
  key = id.to_s
  timeout = approval_context[:timeout]

  @registry_mutex.synchronize do
    @registry[key] = approval_context
    if timeout
      @deadlines[key] = monotonic_now + timeout.to_f
    else
      @deadlines.delete(key)
    end
    @condition.signal
  end

  self.class.register_approval(key, owner_id)
  RubyLLM::MCP.logger.debug("Stored approval #{key} in registry for #{owner_id}")
end