Class: RubyLLM::MCP::Handlers::HumanInTheLoopRegistry
- Inherits:
-
Object
- Object
- RubyLLM::MCP::Handlers::HumanInTheLoopRegistry
- 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
-
#owner_id ⇒ Object
readonly
Returns the value of attribute owner_id.
Class Method Summary collapse
- .approve(id) ⇒ Object
- .clear(owner_id: nil) ⇒ Object
- .deny(id, reason: "Denied") ⇒ Object
- .for_owner(owner_id) ⇒ Object
- .instance ⇒ Object
- .register_approval(id, owner_id) ⇒ Object
- .release(owner_id) ⇒ Object
- .remove(id) ⇒ Object
- .retrieve(id) ⇒ Object
- .route_registry(id) ⇒ Object
- .size(owner_id: nil) ⇒ Object
-
.store(id, approval) ⇒ Object
Backward-compatible global store path.
- .unregister_approval(id) ⇒ Object
Instance Method Summary collapse
-
#approve(id) ⇒ Object
rubocop:disable Naming/PredicateMethod.
- #clear ⇒ Object
-
#deny(id, reason: "Denied") ⇒ Object
rubocop:disable Naming/PredicateMethod.
-
#initialize(owner_id:) ⇒ HumanInTheLoopRegistry
constructor
A new instance of HumanInTheLoopRegistry.
- #remove(id) ⇒ Object
- #retrieve(id) ⇒ Object
- #shutdown ⇒ Object
- #size ⇒ Object
-
#store(id, approval_context) ⇒ Object
Store approval context: { promise:, timeout:, tool_name:, parameters: }.
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_id ⇒ Object (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 |
.instance ⇒ Object
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 |
#clear ⇒ Object
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 |
#shutdown ⇒ Object
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.}") ensure @scheduler_thread = nil end |
#size ⇒ Object
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 |