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,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Represents a change to a Memory key
5
+ #
6
+ # MemoryChange is passed to subscription callbacks when a key's value changes.
7
+ # It provides context about what changed, who changed it, and when.
8
+ #
9
+ # @example Subscription callback
10
+ # memory.subscribe(:sentiment) do |change|
11
+ # puts "Key #{change.key} changed from #{change.previous} to #{change.value}"
12
+ # puts "Written by: #{change.writer} at #{change.timestamp}"
13
+ # end
14
+ #
15
+ # @note This class is designed to be compatible with SmartMessage::Base.
16
+ # When smart_message is added as a dependency, this class can inherit
17
+ # from SmartMessage::Base for distributed pub/sub support.
18
+ #
19
+ class MemoryChange
20
+ # @!attribute [r] key
21
+ # @return [Symbol] the memory key that changed
22
+ # @!attribute [r] value
23
+ # @return [Object] the new value
24
+ # @!attribute [r] previous
25
+ # @return [Object, nil] the previous value (nil if key was created)
26
+ # @!attribute [r] writer
27
+ # @return [String, nil] name of the robot that wrote the value
28
+ # @!attribute [r] network_name
29
+ # @return [String, nil] name of the network
30
+ # @!attribute [r] timestamp
31
+ # @return [Time] when the change occurred
32
+ # @!attribute [r] correlation_id
33
+ # @return [String, nil] optional correlation ID for tracing
34
+ attr_reader :key, :value, :previous, :writer, :network_name, :timestamp, :correlation_id
35
+
36
+ # Creates a new MemoryChange instance.
37
+ #
38
+ # @param key [Symbol, String] the memory key that changed
39
+ # @param value [Object] the new value
40
+ # @param previous [Object, nil] the previous value
41
+ # @param writer [String, nil] name of the robot that wrote the value
42
+ # @param network_name [String, nil] name of the network
43
+ # @param timestamp [Time] when the change occurred (defaults to now)
44
+ # @param correlation_id [String, nil] optional correlation ID
45
+ #
46
+ def initialize(key:, value:, previous: nil, writer: nil, network_name: nil, timestamp: nil, correlation_id: nil)
47
+ @key = key.to_sym
48
+ @value = value
49
+ @previous = previous
50
+ @writer = writer
51
+ @network_name = network_name
52
+ @timestamp = timestamp || Time.now
53
+ @correlation_id = correlation_id
54
+ end
55
+
56
+ # Check if this is a new key (no previous value).
57
+ #
58
+ # @return [Boolean]
59
+ #
60
+ def created?
61
+ @previous.nil? && !@value.nil?
62
+ end
63
+
64
+ # Check if this is an update to an existing key.
65
+ #
66
+ # @return [Boolean]
67
+ #
68
+ def updated?
69
+ !@previous.nil? && !@value.nil?
70
+ end
71
+
72
+ # Check if the key was deleted.
73
+ #
74
+ # @return [Boolean]
75
+ #
76
+ def deleted?
77
+ @value.nil? && !@previous.nil?
78
+ end
79
+
80
+ # Convert to hash representation.
81
+ #
82
+ # @return [Hash]
83
+ #
84
+ def to_h
85
+ {
86
+ key: @key,
87
+ value: @value,
88
+ previous: @previous,
89
+ writer: @writer,
90
+ network_name: @network_name,
91
+ timestamp: @timestamp.iso8601,
92
+ correlation_id: @correlation_id
93
+ }.compact
94
+ end
95
+
96
+ # Convert to JSON.
97
+ #
98
+ # @param args [Array] arguments passed to to_json
99
+ # @return [String]
100
+ #
101
+ def to_json(*args)
102
+ to_h.to_json(*args)
103
+ end
104
+
105
+ # Reconstruct from hash.
106
+ #
107
+ # @param hash [Hash] the hash representation
108
+ # @return [MemoryChange]
109
+ #
110
+ def self.from_hash(hash)
111
+ hash = hash.transform_keys(&:to_sym)
112
+ new(
113
+ key: hash[:key],
114
+ value: hash[:value],
115
+ previous: hash[:previous],
116
+ writer: hash[:writer],
117
+ network_name: hash[:network_name],
118
+ timestamp: hash[:timestamp] ? Time.parse(hash[:timestamp]) : nil,
119
+ correlation_id: hash[:correlation_id]
120
+ )
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ # Base class for all message types in RobotLab
5
+ #
6
+ # Messages represent the communication between users, assistants, and tools
7
+ # in a conversation. This mirrors the TypeScript Message union type.
8
+ #
9
+ # @abstract Subclass and implement specific message types
10
+ #
11
+ class Message
12
+ # Valid message types
13
+ VALID_TYPES = %w[text tool_call tool_result].freeze
14
+ # Valid message roles
15
+ VALID_ROLES = %w[system user assistant tool_result].freeze
16
+ # Valid stop reasons
17
+ VALID_STOP_REASONS = %w[tool stop].freeze
18
+
19
+ # @!attribute [r] type
20
+ # @return [String] the message type (text, tool_call, or tool_result)
21
+ # @!attribute [r] role
22
+ # @return [String] the message role (system, user, assistant, or tool_result)
23
+ # @!attribute [r] content
24
+ # @return [String, Hash, nil] the message content
25
+ # @!attribute [r] stop_reason
26
+ # @return [String, nil] the stop reason (tool or stop)
27
+ attr_reader :type, :role, :content, :stop_reason
28
+
29
+ # Creates a new Message instance.
30
+ #
31
+ # @param type [String, Symbol] the message type
32
+ # @param role [String, Symbol] the message role
33
+ # @param content [String, Hash, nil] the message content
34
+ # @param stop_reason [String, Symbol, nil] the stop reason
35
+ # @raise [ArgumentError] if type, role, or stop_reason is invalid
36
+ def initialize(type:, role:, content:, stop_reason: nil)
37
+ validate_type!(type)
38
+ validate_role!(role)
39
+ validate_stop_reason!(stop_reason) if stop_reason
40
+
41
+ @type = type.to_s
42
+ @role = role.to_s
43
+ @content = content
44
+ @stop_reason = stop_reason&.to_s
45
+ end
46
+
47
+ # @return [Boolean] true if this is a text message
48
+ def text? = type == "text"
49
+ # @return [Boolean] true if this is a tool call message
50
+ def tool_call? = type == "tool_call"
51
+ # @return [Boolean] true if this is a tool result message
52
+ def tool_result? = type == "tool_result"
53
+
54
+ # @return [Boolean] true if this is a system message
55
+ def system? = role == "system"
56
+ # @return [Boolean] true if this is a user message
57
+ def user? = role == "user"
58
+ # @return [Boolean] true if this is an assistant message
59
+ def assistant? = role == "assistant"
60
+
61
+ # @return [Boolean] true if the conversation stopped naturally
62
+ def stopped? = stop_reason == "stop"
63
+ # @return [Boolean] true if the conversation stopped for a tool call
64
+ def tool_stop? = stop_reason == "tool"
65
+
66
+ # Converts the message to a hash representation.
67
+ #
68
+ # @return [Hash] a hash containing the message data
69
+ def to_h
70
+ {
71
+ type: type,
72
+ role: role,
73
+ content: content,
74
+ stop_reason: stop_reason
75
+ }.compact
76
+ end
77
+
78
+ # Converts the message to JSON.
79
+ #
80
+ # @param args [Array] arguments passed to to_json
81
+ # @return [String] JSON representation of the message
82
+ def to_json(*args)
83
+ to_h.to_json(*args)
84
+ end
85
+
86
+ # Creates a Message instance from a hash.
87
+ #
88
+ # Automatically determines the appropriate subclass based on the type.
89
+ #
90
+ # @param hash [Hash] the hash representation of a message
91
+ # @return [Message] the appropriate Message subclass instance
92
+ def self.from_hash(hash)
93
+ hash = hash.transform_keys(&:to_sym)
94
+
95
+ case hash[:type]&.to_s
96
+ when "text"
97
+ TextMessage.new(**hash.slice(:role, :content, :stop_reason))
98
+ when "tool_call"
99
+ ToolCallMessage.new(
100
+ role: hash[:role],
101
+ tools: hash[:tools],
102
+ stop_reason: hash[:stop_reason]
103
+ )
104
+ when "tool_result"
105
+ ToolResultMessage.new(
106
+ tool: hash[:tool],
107
+ content: hash[:content],
108
+ stop_reason: hash[:stop_reason]
109
+ )
110
+ else
111
+ new(**hash)
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def validate_type!(type)
118
+ return if VALID_TYPES.include?(type.to_s)
119
+
120
+ raise ArgumentError, "Invalid message type: #{type}. Must be one of: #{VALID_TYPES.join(', ')}"
121
+ end
122
+
123
+ def validate_role!(role)
124
+ return if VALID_ROLES.include?(role.to_s)
125
+
126
+ raise ArgumentError, "Invalid role: #{role}. Must be one of: #{VALID_ROLES.join(', ')}"
127
+ end
128
+
129
+ def validate_stop_reason!(stop_reason)
130
+ return if VALID_STOP_REASONS.include?(stop_reason.to_s)
131
+
132
+ raise ArgumentError, "Invalid stop_reason: #{stop_reason}. Must be one of: #{VALID_STOP_REASONS.join(', ')}"
133
+ end
134
+ end
135
+
136
+ # Text message from system, user, or assistant
137
+ #
138
+ # @example System message
139
+ # TextMessage.new(role: :system, content: "You are a helpful assistant")
140
+ #
141
+ # @example User message
142
+ # TextMessage.new(role: :user, content: "Hello!")
143
+ #
144
+ # @example Assistant response
145
+ # TextMessage.new(role: :assistant, content: "Hi there!", stop_reason: :stop)
146
+ #
147
+ class TextMessage < Message
148
+ # Creates a new TextMessage instance.
149
+ #
150
+ # @param role [String, Symbol] the message role (system, user, or assistant)
151
+ # @param content [String] the text content
152
+ # @param stop_reason [String, Symbol, nil] the stop reason
153
+ def initialize(role:, content:, stop_reason: nil)
154
+ super(type: "text", role: role, content: content, stop_reason: stop_reason)
155
+ end
156
+ end
157
+
158
+ # Represents a tool/function definition for tool calls
159
+ #
160
+ # @example
161
+ # ToolMessage.new(
162
+ # id: "call_123",
163
+ # name: "get_weather",
164
+ # input: { location: "Berlin" }
165
+ # )
166
+ #
167
+ class ToolMessage
168
+ # @!attribute [r] id
169
+ # @return [String] the unique identifier for this tool call
170
+ # @!attribute [r] name
171
+ # @return [String] the name of the tool being called
172
+ # @!attribute [r] input
173
+ # @return [Hash] the input arguments for the tool
174
+ attr_reader :id, :name, :input
175
+
176
+ # Creates a new ToolMessage instance.
177
+ #
178
+ # @param id [String] the unique identifier for this tool call
179
+ # @param name [String] the name of the tool
180
+ # @param input [Hash, nil] the input arguments
181
+ def initialize(id:, name:, input:)
182
+ @id = id
183
+ @name = name
184
+ @input = input || {}
185
+ end
186
+
187
+ # Converts the tool message to a hash representation.
188
+ #
189
+ # @return [Hash] a hash containing the tool call data
190
+ def to_h
191
+ {
192
+ type: "tool",
193
+ id: id,
194
+ name: name,
195
+ input: input
196
+ }
197
+ end
198
+
199
+ # Converts the tool message to JSON.
200
+ #
201
+ # @param args [Array] arguments passed to to_json
202
+ # @return [String] JSON representation
203
+ def to_json(*args)
204
+ to_h.to_json(*args)
205
+ end
206
+
207
+ # Creates a ToolMessage from a hash.
208
+ #
209
+ # @param hash [Hash] the hash representation
210
+ # @return [ToolMessage]
211
+ def self.from_hash(hash)
212
+ hash = hash.transform_keys(&:to_sym)
213
+ new(
214
+ id: hash[:id],
215
+ name: hash[:name],
216
+ input: hash[:input] || hash[:arguments] || {}
217
+ )
218
+ end
219
+ end
220
+
221
+ # Message containing one or more tool calls from the assistant
222
+ #
223
+ # @example
224
+ # ToolCallMessage.new(
225
+ # role: :assistant,
226
+ # tools: [
227
+ # ToolMessage.new(id: "call_1", name: "get_weather", input: { location: "Berlin" })
228
+ # ]
229
+ # )
230
+ #
231
+ class ToolCallMessage < Message
232
+ # @!attribute [r] tools
233
+ # @return [Array<ToolMessage>] the tool calls in this message
234
+ attr_reader :tools
235
+
236
+ # Creates a new ToolCallMessage instance.
237
+ #
238
+ # @param role [String, Symbol] the message role (usually assistant)
239
+ # @param tools [Array<ToolMessage, Hash>] the tool calls
240
+ # @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
241
+ def initialize(role:, tools:, stop_reason: nil)
242
+ @tools = normalize_tools(tools)
243
+ super(type: "tool_call", role: role, content: nil, stop_reason: stop_reason || "tool")
244
+ end
245
+
246
+ # Converts the tool call message to a hash representation.
247
+ #
248
+ # @return [Hash] a hash containing the tool call data
249
+ def to_h
250
+ {
251
+ type: type,
252
+ role: role,
253
+ tools: tools.map(&:to_h),
254
+ stop_reason: stop_reason
255
+ }
256
+ end
257
+
258
+ private
259
+
260
+ def normalize_tools(tools)
261
+ tools.map do |tool|
262
+ case tool
263
+ when ToolMessage
264
+ tool
265
+ when Hash
266
+ ToolMessage.from_hash(tool)
267
+ else
268
+ raise ArgumentError, "Invalid tool: must be ToolMessage or Hash"
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ # Result from executing a tool
275
+ #
276
+ # @example Successful result
277
+ # ToolResultMessage.new(
278
+ # tool: ToolMessage.new(id: "call_1", name: "get_weather", input: { location: "Berlin" }),
279
+ # content: { data: { temperature: 15, unit: "celsius" } }
280
+ # )
281
+ #
282
+ # @example Error result
283
+ # ToolResultMessage.new(
284
+ # tool: ToolMessage.new(id: "call_1", name: "get_weather", input: {}),
285
+ # content: { error: "Location is required" }
286
+ # )
287
+ #
288
+ class ToolResultMessage < Message
289
+ # @!attribute [r] tool
290
+ # @return [ToolMessage] the tool call that was executed
291
+ attr_reader :tool
292
+
293
+ # Creates a new ToolResultMessage instance.
294
+ #
295
+ # @param tool [ToolMessage, Hash] the tool call that was executed
296
+ # @param content [Hash] the result content (with :data or :error key)
297
+ # @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
298
+ def initialize(tool:, content:, stop_reason: nil)
299
+ @tool = normalize_tool(tool)
300
+ super(type: "tool_result", role: "tool_result", content: content, stop_reason: stop_reason || "tool")
301
+ end
302
+
303
+ # Checks if the tool execution was successful.
304
+ #
305
+ # @return [Boolean] true if content contains a :data key
306
+ def success?
307
+ content.is_a?(Hash) && content.key?(:data)
308
+ end
309
+
310
+ # Checks if the tool execution resulted in an error.
311
+ #
312
+ # @return [Boolean] true if content contains an :error key
313
+ def error?
314
+ content.is_a?(Hash) && content.key?(:error)
315
+ end
316
+
317
+ # Returns the result data if successful.
318
+ #
319
+ # @return [Object, nil] the result data, or nil if not successful
320
+ def data
321
+ content[:data] if success?
322
+ end
323
+
324
+ # Returns the error message if there was an error.
325
+ #
326
+ # @return [String, nil] the error message, or nil if no error
327
+ def error
328
+ content[:error] if error?
329
+ end
330
+
331
+ # Converts the tool result message to a hash representation.
332
+ #
333
+ # @return [Hash] a hash containing the tool result data
334
+ def to_h
335
+ {
336
+ type: type,
337
+ role: role,
338
+ tool: tool.to_h,
339
+ content: content,
340
+ stop_reason: stop_reason
341
+ }
342
+ end
343
+
344
+ private
345
+
346
+ def normalize_tool(tool)
347
+ case tool
348
+ when ToolMessage
349
+ tool
350
+ when Hash
351
+ ToolMessage.from_hash(tool)
352
+ else
353
+ raise ArgumentError, "Invalid tool: must be ToolMessage or Hash"
354
+ end
355
+ end
356
+ end
357
+ end