robot_lab 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.github/workflows/deploy-yard-docs.yml +52 -0
  5. data/CHANGELOG.md +55 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +332 -0
  9. data/Rakefile +67 -0
  10. data/docs/api/adapters/anthropic.md +121 -0
  11. data/docs/api/adapters/gemini.md +133 -0
  12. data/docs/api/adapters/index.md +104 -0
  13. data/docs/api/adapters/openai.md +134 -0
  14. data/docs/api/core/index.md +113 -0
  15. data/docs/api/core/memory.md +314 -0
  16. data/docs/api/core/network.md +291 -0
  17. data/docs/api/core/robot.md +273 -0
  18. data/docs/api/core/state.md +273 -0
  19. data/docs/api/core/tool.md +353 -0
  20. data/docs/api/history/active-record-adapter.md +195 -0
  21. data/docs/api/history/config.md +191 -0
  22. data/docs/api/history/index.md +132 -0
  23. data/docs/api/history/thread-manager.md +144 -0
  24. data/docs/api/index.md +82 -0
  25. data/docs/api/mcp/client.md +221 -0
  26. data/docs/api/mcp/index.md +111 -0
  27. data/docs/api/mcp/server.md +225 -0
  28. data/docs/api/mcp/transports.md +264 -0
  29. data/docs/api/messages/index.md +67 -0
  30. data/docs/api/messages/text-message.md +102 -0
  31. data/docs/api/messages/tool-call-message.md +144 -0
  32. data/docs/api/messages/tool-result-message.md +154 -0
  33. data/docs/api/messages/user-message.md +171 -0
  34. data/docs/api/streaming/context.md +174 -0
  35. data/docs/api/streaming/events.md +237 -0
  36. data/docs/api/streaming/index.md +108 -0
  37. data/docs/architecture/core-concepts.md +243 -0
  38. data/docs/architecture/index.md +138 -0
  39. data/docs/architecture/message-flow.md +320 -0
  40. data/docs/architecture/network-orchestration.md +216 -0
  41. data/docs/architecture/robot-execution.md +243 -0
  42. data/docs/architecture/state-management.md +323 -0
  43. data/docs/assets/css/custom.css +56 -0
  44. data/docs/assets/images/robot_lab.jpg +0 -0
  45. data/docs/concepts.md +216 -0
  46. data/docs/examples/basic-chat.md +193 -0
  47. data/docs/examples/index.md +129 -0
  48. data/docs/examples/mcp-server.md +290 -0
  49. data/docs/examples/multi-robot-network.md +312 -0
  50. data/docs/examples/rails-application.md +420 -0
  51. data/docs/examples/tool-usage.md +310 -0
  52. data/docs/getting-started/configuration.md +230 -0
  53. data/docs/getting-started/index.md +56 -0
  54. data/docs/getting-started/installation.md +179 -0
  55. data/docs/getting-started/quick-start.md +203 -0
  56. data/docs/guides/building-robots.md +376 -0
  57. data/docs/guides/creating-networks.md +366 -0
  58. data/docs/guides/history.md +359 -0
  59. data/docs/guides/index.md +68 -0
  60. data/docs/guides/mcp-integration.md +356 -0
  61. data/docs/guides/memory.md +309 -0
  62. data/docs/guides/rails-integration.md +432 -0
  63. data/docs/guides/streaming.md +314 -0
  64. data/docs/guides/using-tools.md +394 -0
  65. data/docs/index.md +160 -0
  66. data/examples/01_simple_robot.rb +38 -0
  67. data/examples/02_tools.rb +106 -0
  68. data/examples/03_network.rb +103 -0
  69. data/examples/04_mcp.rb +219 -0
  70. data/examples/05_streaming.rb +124 -0
  71. data/examples/06_prompt_templates.rb +324 -0
  72. data/examples/07_network_memory.rb +329 -0
  73. data/examples/prompts/assistant/system.txt.erb +2 -0
  74. data/examples/prompts/assistant/user.txt.erb +1 -0
  75. data/examples/prompts/billing/system.txt.erb +7 -0
  76. data/examples/prompts/billing/user.txt.erb +1 -0
  77. data/examples/prompts/classifier/system.txt.erb +4 -0
  78. data/examples/prompts/classifier/user.txt.erb +1 -0
  79. data/examples/prompts/entity_extractor/system.txt.erb +11 -0
  80. data/examples/prompts/entity_extractor/user.txt.erb +3 -0
  81. data/examples/prompts/escalation/system.txt.erb +35 -0
  82. data/examples/prompts/escalation/user.txt.erb +34 -0
  83. data/examples/prompts/general/system.txt.erb +4 -0
  84. data/examples/prompts/general/user.txt.erb +1 -0
  85. data/examples/prompts/github_assistant/system.txt.erb +6 -0
  86. data/examples/prompts/github_assistant/user.txt.erb +1 -0
  87. data/examples/prompts/helper/system.txt.erb +1 -0
  88. data/examples/prompts/helper/user.txt.erb +1 -0
  89. data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
  90. data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
  91. data/examples/prompts/order_support/system.txt.erb +27 -0
  92. data/examples/prompts/order_support/user.txt.erb +22 -0
  93. data/examples/prompts/product_support/system.txt.erb +30 -0
  94. data/examples/prompts/product_support/user.txt.erb +32 -0
  95. data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
  96. data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
  97. data/examples/prompts/synthesizer/system.txt.erb +14 -0
  98. data/examples/prompts/synthesizer/user.txt.erb +15 -0
  99. data/examples/prompts/technical/system.txt.erb +7 -0
  100. data/examples/prompts/technical/user.txt.erb +1 -0
  101. data/examples/prompts/triage/system.txt.erb +16 -0
  102. data/examples/prompts/triage/user.txt.erb +17 -0
  103. data/lib/generators/robot_lab/install_generator.rb +78 -0
  104. data/lib/generators/robot_lab/robot_generator.rb +55 -0
  105. data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
  106. data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
  107. data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
  108. data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
  109. data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
  110. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
  111. data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
  112. data/lib/robot_lab/adapters/anthropic.rb +163 -0
  113. data/lib/robot_lab/adapters/base.rb +85 -0
  114. data/lib/robot_lab/adapters/gemini.rb +193 -0
  115. data/lib/robot_lab/adapters/openai.rb +159 -0
  116. data/lib/robot_lab/adapters/registry.rb +81 -0
  117. data/lib/robot_lab/configuration.rb +143 -0
  118. data/lib/robot_lab/error.rb +32 -0
  119. data/lib/robot_lab/errors.rb +70 -0
  120. data/lib/robot_lab/history/active_record_adapter.rb +146 -0
  121. data/lib/robot_lab/history/config.rb +115 -0
  122. data/lib/robot_lab/history/thread_manager.rb +93 -0
  123. data/lib/robot_lab/mcp/client.rb +210 -0
  124. data/lib/robot_lab/mcp/server.rb +84 -0
  125. data/lib/robot_lab/mcp/transports/base.rb +56 -0
  126. data/lib/robot_lab/mcp/transports/sse.rb +117 -0
  127. data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
  128. data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
  129. data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
  130. data/lib/robot_lab/memory.rb +882 -0
  131. data/lib/robot_lab/memory_change.rb +123 -0
  132. data/lib/robot_lab/message.rb +357 -0
  133. data/lib/robot_lab/network.rb +350 -0
  134. data/lib/robot_lab/rails/engine.rb +29 -0
  135. data/lib/robot_lab/rails/railtie.rb +42 -0
  136. data/lib/robot_lab/robot.rb +560 -0
  137. data/lib/robot_lab/robot_result.rb +205 -0
  138. data/lib/robot_lab/robotic_model.rb +324 -0
  139. data/lib/robot_lab/state_proxy.rb +188 -0
  140. data/lib/robot_lab/streaming/context.rb +144 -0
  141. data/lib/robot_lab/streaming/events.rb +95 -0
  142. data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
  143. data/lib/robot_lab/task.rb +117 -0
  144. data/lib/robot_lab/tool.rb +223 -0
  145. data/lib/robot_lab/tool_config.rb +112 -0
  146. data/lib/robot_lab/tool_manifest.rb +234 -0
  147. data/lib/robot_lab/user_message.rb +118 -0
  148. data/lib/robot_lab/version.rb +5 -0
  149. data/lib/robot_lab/waiter.rb +73 -0
  150. data/lib/robot_lab.rb +195 -0
  151. data/mkdocs.yml +214 -0
  152. data/sig/robot_lab.rbs +4 -0
  153. metadata +442 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Handles hierarchical MCP and tools configuration resolution
5
+ #
6
+ # Configuration hierarchy (each level overrides the previous):
7
+ # 1. RobotLab.configuration (global)
8
+ # 2. Network.new (network scope)
9
+ # 3. Robot.new (robot definition scope)
10
+ # 4. robot.run (runtime scope)
11
+ #
12
+ # Value semantics:
13
+ # - :inherit -> Use parent level's configuration
14
+ # - nil -> No items allowed
15
+ # - [] -> No items allowed
16
+ # - :none -> No items allowed
17
+ # - [item1, ...] -> Only these specific items allowed
18
+ #
19
+ # @example
20
+ # ToolConfig.resolve(:inherit, parent_value: %w[tool1 tool2])
21
+ # # => ["tool1", "tool2"]
22
+ #
23
+ # ToolConfig.resolve(nil, parent_value: %w[tool1 tool2])
24
+ # # => []
25
+ #
26
+ # ToolConfig.resolve(%w[tool3], parent_value: %w[tool1 tool2])
27
+ # # => ["tool3"]
28
+ #
29
+ module ToolConfig
30
+ NONE_VALUES = [nil, [], :none].freeze
31
+
32
+ class << self
33
+ # Resolve a configuration value against its parent
34
+ #
35
+ # @param value [Symbol, Array, nil] The current level's value
36
+ # @param parent_value [Array] The parent level's resolved value
37
+ # @return [Array] The resolved configuration
38
+ #
39
+ def resolve(value, parent_value:)
40
+ return Array(parent_value) if value == :inherit
41
+ return [] if none_value?(value)
42
+
43
+ Array(value)
44
+ end
45
+
46
+ # Resolve MCP servers configuration
47
+ #
48
+ # @param value [Symbol, Array, nil] MCP configuration
49
+ # @param parent_value [Array] Parent's MCP servers
50
+ # @return [Array] Resolved MCP server configurations
51
+ #
52
+ def resolve_mcp(value, parent_value:)
53
+ resolve(value, parent_value: parent_value)
54
+ end
55
+
56
+ # Resolve tools configuration
57
+ #
58
+ # @param value [Symbol, Array, nil] Tools configuration (tool names as strings)
59
+ # @param parent_value [Array] Parent's tools
60
+ # @return [Array<String>] Resolved tool names
61
+ #
62
+ def resolve_tools(value, parent_value:)
63
+ resolved = resolve(value, parent_value: parent_value)
64
+ resolved.map(&:to_s)
65
+ end
66
+
67
+ # Check if value represents "no items"
68
+ #
69
+ # @param value [Object] Value to check
70
+ # @return [Boolean]
71
+ #
72
+ def none_value?(value)
73
+ NONE_VALUES.include?(value)
74
+ end
75
+
76
+ # Check if value represents "inherit from parent"
77
+ #
78
+ # @param value [Object] Value to check
79
+ # @return [Boolean]
80
+ #
81
+ def inherit_value?(value)
82
+ value == :inherit
83
+ end
84
+
85
+ # Filter tools based on allowed tool names
86
+ #
87
+ # Given a list of tool objects and a whitelist of tool names,
88
+ # returns only the tools whose names are in the whitelist.
89
+ #
90
+ # @param tools [Array] Tool objects (must respond to :name)
91
+ # @param allowed_names [Array<String>] Whitelist of tool names
92
+ # @return [Array] Filtered tools
93
+ #
94
+ def filter_tools(tools, allowed_names:)
95
+ return [] if allowed_names.empty?
96
+
97
+ allowed_set = allowed_names.map(&:to_s).to_set
98
+ tools.select { |tool| allowed_set.include?(tool_name(tool)) }
99
+ end
100
+
101
+ private
102
+
103
+ def tool_name(tool)
104
+ case tool
105
+ when String then tool
106
+ when Symbol then tool.to_s
107
+ else tool.respond_to?(:name) ? tool.name.to_s : tool.to_s
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Registry of tools with lookup by name
5
+ #
6
+ # ToolManifest provides a collection interface for managing multiple tools,
7
+ # with methods for lookup, iteration, and conversion to various formats.
8
+ #
9
+ # @example Creating a manifest
10
+ # manifest = ToolManifest.new([weather_tool, calculator_tool])
11
+ # manifest[:get_weather] # => Tool
12
+ # manifest.names # => ["get_weather", "calculate"]
13
+ #
14
+ class ToolManifest
15
+ include Enumerable
16
+
17
+ # Creates a new ToolManifest instance.
18
+ #
19
+ # @param tools [Array<Tool>] initial tools to add to the manifest
20
+ #
21
+ # @example
22
+ # manifest = ToolManifest.new([weather_tool, calculator_tool])
23
+ def initialize(tools = [])
24
+ @tools = {}
25
+ Array(tools).each { |tool| add(tool) }
26
+ end
27
+
28
+ # Add a tool to the manifest
29
+ #
30
+ # @param tool [Tool] The tool to add
31
+ # @return [self]
32
+ #
33
+ def add(tool)
34
+ @tools[tool.name] = tool
35
+ self
36
+ end
37
+
38
+ # @!method <<(tool)
39
+ # Alias for {#add}.
40
+ # @param tool [Tool] the tool to add
41
+ # @return [self]
42
+ alias << add
43
+
44
+ # Remove a tool from the manifest
45
+ #
46
+ # @param name [String, Symbol] The tool name
47
+ # @return [Tool, nil] The removed tool
48
+ #
49
+ def remove(name)
50
+ @tools.delete(name.to_s)
51
+ end
52
+
53
+ # Get a tool by name
54
+ #
55
+ # @param name [String, Symbol] The tool name
56
+ # @return [Tool, nil]
57
+ #
58
+ def [](name)
59
+ @tools[name.to_s]
60
+ end
61
+
62
+ # Get a tool by name, raising if not found
63
+ #
64
+ # @param name [String, Symbol] The tool name
65
+ # @return [Tool]
66
+ # @raise [ToolNotFoundError] If tool doesn't exist
67
+ #
68
+ def fetch(name)
69
+ @tools.fetch(name.to_s) do
70
+ raise ToolNotFoundError, "Tool not found: #{name}. Available tools: #{names.join(', ')}"
71
+ end
72
+ end
73
+
74
+ # Check if a tool exists
75
+ #
76
+ # @param name [String, Symbol] The tool name
77
+ # @return [Boolean]
78
+ #
79
+ def include?(name)
80
+ @tools.key?(name.to_s)
81
+ end
82
+
83
+ # @!method has?(name)
84
+ # Alias for {#include?}.
85
+ # @param name [String, Symbol] the tool name
86
+ # @return [Boolean]
87
+ alias has? include?
88
+
89
+ # Get all tool names
90
+ #
91
+ # @return [Array<String>]
92
+ #
93
+ def names
94
+ @tools.keys
95
+ end
96
+
97
+ # Get all tools
98
+ #
99
+ # @return [Array<Tool>]
100
+ #
101
+ def values
102
+ @tools.values
103
+ end
104
+
105
+ # @!method all
106
+ # Alias for {#values}.
107
+ # @return [Array<Tool>]
108
+ alias all values
109
+
110
+ # @!method to_a
111
+ # Alias for {#values}.
112
+ # @return [Array<Tool>]
113
+ alias to_a values
114
+
115
+ # Number of tools
116
+ #
117
+ # @return [Integer]
118
+ #
119
+ def size
120
+ @tools.size
121
+ end
122
+
123
+ # @!method count
124
+ # Alias for {#size}.
125
+ # @return [Integer]
126
+ alias count size
127
+
128
+ # @!method length
129
+ # Alias for {#size}.
130
+ # @return [Integer]
131
+ alias length size
132
+
133
+ # Check if manifest is empty
134
+ #
135
+ # @return [Boolean]
136
+ #
137
+ def empty?
138
+ @tools.empty?
139
+ end
140
+
141
+ # Iterate over tools
142
+ #
143
+ # @yield [Tool] Each tool in the manifest
144
+ #
145
+ def each(&block)
146
+ @tools.values.each(&block)
147
+ end
148
+
149
+ # Clear all tools
150
+ #
151
+ # @return [self]
152
+ #
153
+ def clear
154
+ @tools.clear
155
+ self
156
+ end
157
+
158
+ # Replace all tools
159
+ #
160
+ # @param tools [Array<Tool>] New tools
161
+ # @return [self]
162
+ #
163
+ def replace(tools)
164
+ clear
165
+ Array(tools).each { |tool| add(tool) }
166
+ self
167
+ end
168
+
169
+ # Merge another manifest or array of tools
170
+ #
171
+ # @param other [ToolManifest, Array<Tool>] Tools to merge
172
+ # @return [self]
173
+ #
174
+ def merge(other)
175
+ case other
176
+ when ToolManifest
177
+ other.each { |tool| add(tool) }
178
+ when Array
179
+ other.each { |tool| add(tool) }
180
+ when Tool
181
+ add(other)
182
+ end
183
+ self
184
+ end
185
+
186
+ # Convert to hash for JSON Schema
187
+ #
188
+ # @return [Hash] Map of tool names to their JSON schemas
189
+ #
190
+ def to_json_schema
191
+ @tools.transform_values(&:to_json_schema)
192
+ end
193
+
194
+ # Convert to array of ruby_llm Tool classes
195
+ #
196
+ # @return [Array<Class>]
197
+ #
198
+ def to_ruby_llm_tools
199
+ @tools.values.map(&:to_ruby_llm_tool)
200
+ end
201
+
202
+ # Converts the manifest to a hash representation.
203
+ #
204
+ # @return [Hash<String, Hash>] map of tool names to their hash representations
205
+ def to_h
206
+ @tools.transform_values(&:to_h)
207
+ end
208
+
209
+ # Converts the manifest to JSON.
210
+ #
211
+ # @param args [Array] arguments passed to to_json
212
+ # @return [String] JSON representation
213
+ def to_json(*args)
214
+ to_h.to_json(*args)
215
+ end
216
+
217
+ # Create manifest from hash of tool definitions
218
+ #
219
+ # @param hash [Hash] Map of names to tool configs
220
+ # @return [ToolManifest]
221
+ #
222
+ def self.from_hash(hash)
223
+ tools = hash.map do |name, config|
224
+ Tool.new(
225
+ name: name,
226
+ description: config[:description],
227
+ parameters: config[:parameters],
228
+ handler: config[:handler]
229
+ )
230
+ end
231
+ new(tools)
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Enhanced user message with metadata and system prompt augmentation
5
+ #
6
+ # UserMessage wraps the user's input with additional context like
7
+ # thread ID, system prompt additions, and other metadata that can
8
+ # influence robot behavior.
9
+ #
10
+ # @example Basic usage
11
+ # message = UserMessage.new("What is the weather?")
12
+ # message.content # => "What is the weather?"
13
+ #
14
+ # @example With metadata
15
+ # message = UserMessage.new(
16
+ # "What is the weather?",
17
+ # session_id: "thread_123",
18
+ # system_prompt: "Respond in Spanish",
19
+ # metadata: { user_id: "user_456" }
20
+ # )
21
+ #
22
+ class UserMessage
23
+ # @!attribute [r] content
24
+ # @return [String] the message content
25
+ # @!attribute [r] session_id
26
+ # @return [String, nil] the conversation thread identifier
27
+ # @!attribute [r] system_prompt
28
+ # @return [String, nil] additional system prompt to inject
29
+ # @!attribute [r] metadata
30
+ # @return [Hash] additional metadata
31
+ # @!attribute [r] id
32
+ # @return [String] unique message identifier
33
+ # @!attribute [r] created_at
34
+ # @return [Time] when the message was created
35
+ attr_reader :content, :session_id, :system_prompt, :metadata, :id, :created_at
36
+
37
+ # Creates a new UserMessage instance.
38
+ #
39
+ # @param content [String] the message content
40
+ # @param session_id [String, nil] conversation thread identifier
41
+ # @param system_prompt [String, nil] additional system prompt
42
+ # @param metadata [Hash, nil] additional metadata
43
+ # @param id [String, nil] unique identifier (defaults to UUID)
44
+ def initialize(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)
45
+ @content = content.to_s
46
+ @session_id = session_id
47
+ @system_prompt = system_prompt
48
+ @metadata = metadata || {}
49
+ @id = id || SecureRandom.uuid
50
+ @created_at = Time.now
51
+ end
52
+
53
+ # Convert to a simple text message for the conversation
54
+ #
55
+ # @return [TextMessage]
56
+ #
57
+ def to_message
58
+ RobotLab::TextMessage.new(role: "user", content: content)
59
+ end
60
+
61
+ # Get the string content (for compatibility with String inputs)
62
+ #
63
+ # @return [String]
64
+ #
65
+ def to_s
66
+ content
67
+ end
68
+
69
+ # Converts the message to a hash representation.
70
+ #
71
+ # @return [Hash]
72
+ def to_h
73
+ {
74
+ content: content,
75
+ session_id: session_id,
76
+ system_prompt: system_prompt,
77
+ metadata: metadata,
78
+ id: id,
79
+ created_at: created_at.iso8601
80
+ }.compact
81
+ end
82
+
83
+ # Converts the message to JSON.
84
+ #
85
+ # @param args [Array] arguments passed to to_json
86
+ # @return [String] JSON representation
87
+ def to_json(*args)
88
+ to_h.to_json(*args)
89
+ end
90
+
91
+ # Create from string or hash
92
+ #
93
+ # @param input [String, Hash, UserMessage] Input to normalize
94
+ # @return [UserMessage]
95
+ #
96
+ def self.from(input)
97
+ case input
98
+ when UserMessage
99
+ input
100
+ when String
101
+ new(input)
102
+ when Hash
103
+ input = input.transform_keys(&:to_sym)
104
+ new(
105
+ input[:content],
106
+ session_id: input[:session_id],
107
+ system_prompt: input[:system_prompt],
108
+ metadata: input[:metadata],
109
+ id: input[:id]
110
+ )
111
+ when TextMessage
112
+ new(input.content)
113
+ else
114
+ new(input.to_s)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Thread-safe waiter for blocking get operations on Memory
5
+ #
6
+ # Waiter provides a condition variable wrapper that allows one thread
7
+ # to wait for a value that will be provided by another thread.
8
+ #
9
+ # @example Basic usage
10
+ # waiter = Waiter.new
11
+ #
12
+ # # In thread A (waiting)
13
+ # value = waiter.wait(timeout: 30)
14
+ #
15
+ # # In thread B (signaling)
16
+ # waiter.signal("the value")
17
+ #
18
+ # @api private
19
+ class Waiter
20
+ # Creates a new Waiter instance.
21
+ def initialize
22
+ @mutex = Mutex.new
23
+ @condition = ConditionVariable.new
24
+ @value = nil
25
+ @signaled = false
26
+ end
27
+
28
+ # Wait for a value to be signaled.
29
+ #
30
+ # @param timeout [Numeric, nil] maximum seconds to wait (nil = indefinite)
31
+ # @return [Object, :timeout] the signaled value, or :timeout if timed out
32
+ #
33
+ def wait(timeout: nil)
34
+ @mutex.synchronize do
35
+ return @value if @signaled
36
+
37
+ if timeout
38
+ deadline = Time.now + timeout
39
+ until @signaled
40
+ remaining = deadline - Time.now
41
+ return :timeout if remaining <= 0
42
+ @condition.wait(@mutex, remaining)
43
+ end
44
+ @value
45
+ else
46
+ @condition.wait(@mutex) until @signaled
47
+ @value
48
+ end
49
+ end
50
+ end
51
+
52
+ # Signal a value to waiting threads.
53
+ #
54
+ # @param value [Object] the value to signal
55
+ # @return [void]
56
+ #
57
+ def signal(value)
58
+ @mutex.synchronize do
59
+ @value = value
60
+ @signaled = true
61
+ @condition.broadcast
62
+ end
63
+ end
64
+
65
+ # Check if this waiter has been signaled.
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ def signaled?
70
+ @mutex.synchronize { @signaled }
71
+ end
72
+ end
73
+ end