robot_lab 0.0.1 → 0.0.6

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -1,37 +1,87 @@
1
- # State Management
1
+ # Memory Management
2
2
 
3
- State in RobotLab tracks all data and history for a conversation or workflow.
3
+ Memory in RobotLab is a reactive key-value store that provides persistent storage for runtime data, conversation history, and arbitrary user-defined values. It replaces the old `State` class with a unified system that supports both standalone robot usage and shared network execution.
4
4
 
5
- ## State Structure
5
+ ## Memory Structure
6
6
 
7
- The `State` class holds:
7
+ The `Memory` class holds:
8
8
 
9
9
  ```ruby
10
- state = RobotLab.create_state(
11
- message: "Hello!", # Current user message
12
- data: { user_id: "123" } # Custom workflow data
10
+ memory = RobotLab.create_memory(data: { user_id: "123" })
11
+
12
+ memory.data # StateProxy - custom key-value data with method-style access
13
+ memory.results # Array<RobotResult> - execution history
14
+ memory.messages # Array<Message> - conversation history
15
+ memory.session_id # String - optional persistence identifier
16
+ memory.cache # RubyLLM::SemanticCache - semantic caching module
17
+ ```
18
+
19
+ ## Standalone Robot Memory
20
+
21
+ Every robot has its own inherent memory instance, accessible via `robot.memory`:
22
+
23
+ ```ruby
24
+ robot = RobotLab.build(
25
+ name: "assistant",
26
+ system_prompt: "You are helpful."
13
27
  )
14
28
 
15
- state.data # StateProxy - custom key-value data
16
- state.results # Array<RobotResult> - execution history
17
- state.messages # Array<Message> - formatted conversation
18
- state.thread_id # String - optional persistence ID
19
- state.memory # Memory - shared key-value store
29
+ # Access the robot's memory
30
+ robot.memory[:user_name] = "Alice"
31
+ robot.memory[:user_name] #=> "Alice"
32
+
33
+ # Run the robot
34
+ result = robot.run("Hello!")
35
+
36
+ # Memory persists between runs on the same robot instance
37
+ robot.memory[:preference] = "dark_mode"
38
+ result2 = robot.run("What are my preferences?")
39
+
40
+ # Reset memory to initial state
41
+ robot.reset_memory
42
+ ```
43
+
44
+ ## Network Shared Memory
45
+
46
+ When robots execute within a network, they share the network's memory instead of using their own inherent memory. This enables inter-robot communication.
47
+
48
+ ```ruby
49
+ classifier = RobotLab.build(name: "classifier", system_prompt: "Classify requests.")
50
+ handler = RobotLab.build(name: "handler", system_prompt: "Handle requests.")
51
+
52
+ network = RobotLab.create_network(name: "support") do
53
+ task :classifier, classifier, depends_on: :none
54
+ task :handler, handler, depends_on: [:classifier]
55
+ end
56
+
57
+ # The network has its own shared memory
58
+ network.memory[:customer_tier] = "premium"
59
+
60
+ # All robots in the network read/write from network.memory during execution
61
+ result = network.run(message: "I need help with billing")
62
+
63
+ # Reset network memory between runs if needed
64
+ network.reset_memory
20
65
  ```
21
66
 
22
- ## Creating State
67
+ The memory resolution logic is:
68
+
69
+ 1. If `network_memory` is provided at runtime, use that
70
+ 2. If the robot is in a network, use the network's shared memory
71
+ 3. Otherwise, use the robot's own inherent memory (`robot.memory`)
72
+
73
+ ## Creating Memory
23
74
 
24
75
  ### Basic Creation
25
76
 
26
77
  ```ruby
27
- state = RobotLab.create_state(message: "What's the weather?")
78
+ memory = RobotLab.create_memory
28
79
  ```
29
80
 
30
- ### With Custom Data
81
+ ### With Initial Data
31
82
 
32
83
  ```ruby
33
- state = RobotLab.create_state(
34
- message: "Process my order",
84
+ memory = RobotLab.create_memory(
35
85
  data: {
36
86
  user_id: "user_123",
37
87
  order_id: "ord_456",
@@ -40,284 +90,302 @@ state = RobotLab.create_state(
40
90
  )
41
91
  ```
42
92
 
43
- ### From Existing Results
93
+ ### With Caching Disabled
44
94
 
45
95
  ```ruby
46
- state = RobotLab.create_state(
47
- message: "Continue our conversation",
48
- results: previous_results,
49
- thread_id: "thread_abc"
50
- )
96
+ memory = RobotLab.create_memory(data: {}, enable_cache: false)
51
97
  ```
52
98
 
53
- ## StateProxy
54
-
55
- The `data` attribute is a `StateProxy` that provides convenient access:
56
-
57
- ```ruby
58
- state.data[:user_id] # Hash-style access
59
- state.data[:user_id] = "456" # Assignment
60
-
61
- state.data.user_id # Method-style access
62
- state.data.user_id = "456" # Method-style assignment
99
+ ## Reserved Keys
63
100
 
64
- state.data.key?(:user_id) # Check existence
65
- state.data.keys # Get all keys
66
- state.data.to_h # Convert to plain hash
67
- ```
101
+ Memory has five reserved keys with special behavior and dedicated accessors:
68
102
 
69
- ### Change Tracking
103
+ | Key | Type | Description |
104
+ |-----|------|-------------|
105
+ | `:data` | `StateProxy` | Runtime data with method-style access |
106
+ | `:results` | `Array<RobotResult>` | Accumulated robot execution results |
107
+ | `:messages` | `Array<Message>` | Conversation history |
108
+ | `:session_id` | `String` | Conversation session identifier |
109
+ | `:cache` | `RubyLLM::SemanticCache` | Semantic cache module (read-only after init) |
70
110
 
71
- StateProxy can track changes:
111
+ Reserved keys are accessed through dedicated methods and are excluded from `memory.keys`:
72
112
 
73
113
  ```ruby
74
- state = State.new(
75
- data: { count: 0 },
76
- on_change: ->(key, old_val, new_val) {
77
- puts "#{key}: #{old_val} -> #{new_val}"
78
- }
79
- )
114
+ memory.data[:category] = "billing"
115
+ memory.data.category #=> "billing" (method-style via StateProxy)
80
116
 
81
- state.data[:count] = 1 # Prints: "count: 0 -> 1"
117
+ memory.results #=> []
118
+ memory.session_id #=> nil
119
+ memory.cache #=> RubyLLM::SemanticCache
82
120
  ```
83
121
 
84
- ## Memory
122
+ ## StateProxy
85
123
 
86
- Memory provides a shared key-value store across robots:
124
+ The `data` attribute is a `StateProxy` that provides convenient hash-style and method-style access:
87
125
 
88
126
  ```ruby
89
- # Store values
90
- state.memory.remember("user_name", "Alice")
91
- state.memory.remember("preferences", { theme: "dark" })
127
+ memory.data[:user_id] # Hash-style access
128
+ memory.data[:user_id] = "456" # Assignment
92
129
 
93
- # Retrieve values
94
- name = state.memory.recall("user_name") # => "Alice"
130
+ memory.data.user_id # Method-style access
131
+ memory.data.user_id = "456" # Method-style assignment
95
132
 
96
- # Check existence
97
- state.memory.exists?("user_name") # => true
133
+ memory.data.key?(:user_id) # Check existence
134
+ memory.data.keys # Get all keys
135
+ memory.data.to_h # Convert to plain hash
136
+ ```
98
137
 
99
- # Remove values
100
- state.memory.forget("user_name")
138
+ ## Reactive Features
101
139
 
102
- # List all
103
- state.memory.all # => { "user_name" => "Alice", ... }
104
- ```
140
+ Memory supports pub/sub semantics where robots can subscribe to key changes and optionally block until values become available.
105
141
 
106
- ### Scoped Memory
142
+ ### Setting Values
107
143
 
108
- Organize memory with namespaces:
144
+ Use `memory.set(key, value)` to write a value and notify subscribers asynchronously:
109
145
 
110
146
  ```ruby
111
- # Create scoped view
112
- user_memory = state.memory.scoped("user:123")
113
- user_memory.remember("last_login", Time.now)
147
+ memory.set(:sentiment, { score: 0.8, confidence: 0.95 })
148
+ ```
114
149
 
115
- # Access scoped data
116
- user_memory.recall("last_login")
150
+ The `[]=` operator also triggers reactive notifications for non-reserved keys:
117
151
 
118
- # Full key is "user:123:last_login"
119
- state.memory.recall("user:123:last_login")
152
+ ```ruby
153
+ memory[:sentiment] = { score: 0.8 } # Equivalent to memory.set(:sentiment, ...)
120
154
  ```
121
155
 
122
- ### Shared Memory
156
+ ### Blocking Reads
123
157
 
124
- Use the `SHARED` namespace for cross-robot data:
158
+ Use `memory.get(key, wait:)` to block until a value becomes available. This is useful for concurrent pipeline execution where one robot needs to wait for another's output:
125
159
 
126
160
  ```ruby
127
- # In first robot
128
- state.memory.remember("SHARED:context", important_data)
161
+ # Immediate read (returns nil if missing)
162
+ memory.get(:sentiment)
163
+
164
+ # Block indefinitely until value exists
165
+ memory.get(:sentiment, wait: true)
166
+
167
+ # Block up to 30 seconds, raise AwaitTimeout if exceeded
168
+ memory.get(:sentiment, wait: 30)
129
169
 
130
- # In second robot (same or different network run)
131
- data = state.memory.recall("SHARED:context")
170
+ # Wait for multiple keys at once
171
+ results = memory.get(:sentiment, :entities, :keywords, wait: 60)
172
+ #=> { sentiment: {...}, entities: [...], keywords: [...] }
132
173
  ```
133
174
 
134
- ### Memory Operations
175
+ ### Subscriptions
176
+
177
+ Subscribe to key changes with async callbacks. The callback receives a `MemoryChange` object:
135
178
 
136
179
  ```ruby
137
- # Search by pattern
138
- matches = state.memory.search("user:*")
180
+ memory.subscribe(:raw_data) do |change|
181
+ puts "#{change.key} changed from #{change.previous} to #{change.value}"
182
+ puts "Written by: #{change.writer} at #{change.timestamp}"
183
+ end
184
+
185
+ # Subscribe to multiple keys
186
+ memory.subscribe(:sentiment, :entities) do |change|
187
+ update_dashboard(change.key, change.value)
188
+ end
189
+
190
+ # Pattern-based subscriptions (glob-style matching)
191
+ memory.subscribe_pattern("analysis:*") do |change|
192
+ puts "Analysis key #{change.key} updated"
193
+ end
194
+
195
+ # Unsubscribe
196
+ sub_id = memory.subscribe(:status) { |c| puts c.value }
197
+ memory.unsubscribe(sub_id)
198
+
199
+ # Check if key has subscribers
200
+ memory.subscribed?(:status) #=> true/false
201
+ ```
139
202
 
140
- # Get statistics
141
- state.memory.stats
142
- # => { total_keys: 15, namespaces: ["user", "session"] }
203
+ ### MemoryChange
143
204
 
144
- # Clear namespace
145
- state.memory.scoped("temp").clear
205
+ The `MemoryChange` object provides context about what changed:
146
206
 
147
- # Clear everything
148
- state.memory.clear_all
207
+ ```ruby
208
+ change.key #=> :sentiment
209
+ change.value #=> { score: 0.8 }
210
+ change.previous #=> nil (or previous value)
211
+ change.writer #=> "classifier" (robot name)
212
+ change.network_name #=> "support_pipeline"
213
+ change.timestamp #=> Time
214
+ change.created? #=> true (new key, no previous value)
215
+ change.updated? #=> false
216
+ change.deleted? #=> false
149
217
  ```
150
218
 
151
- ## Results
219
+ ## Memory Lifecycle
220
+
221
+ ### Results
152
222
 
153
223
  Results track the history of robot executions:
154
224
 
155
225
  ```ruby
156
226
  # Append a result
157
- state.append_result(robot_result)
227
+ memory.append_result(robot_result)
158
228
 
159
- # Get all results
160
- state.results
229
+ # Get all results (returns a copy)
230
+ memory.results
161
231
 
162
- # Get results from index
163
- state.results_from(5) # Results starting at index 5
164
-
165
- # Format for LLM conversation
166
- state.format_history
232
+ # Get results from a specific index (for incremental persistence)
233
+ memory.results_from(5)
167
234
  ```
168
235
 
169
- ### Result History
170
-
171
236
  Each `RobotResult` contains:
172
237
 
173
238
  ```ruby
174
- result.robot_name # Which robot produced this
175
- result.output # Array<Message> - response content
176
- result.tool_calls # Array<ToolMessage> - tools called
177
- result.stop_reason # "stop", "tool", etc.
178
- result.created_at # When it was created
239
+ result.robot_name # Which robot produced this
240
+ result.output # Array<Message> - response content
241
+ result.tool_calls # Array<ToolResultMessage> - tools called
242
+ result.stop_reason # Stop reason from LLM
243
+ result.last_text_content # Convenience: last text content string
244
+ result.has_tool_calls? # Whether any tools were called
245
+ result.created_at # When it was created
179
246
  ```
180
247
 
181
- ## Messages
248
+ ### Format History
182
249
 
183
- The `messages` method formats state for LLM consumption:
250
+ The `format_history` method prepares messages for LLM consumption:
184
251
 
185
252
  ```ruby
186
- messages = state.messages
187
-
188
- # Returns Array<Message> with:
189
- # - System message (if present)
190
- # - Alternating user/assistant messages
191
- # - Tool calls and results
253
+ formatted = memory.format_history
254
+ # Returns combined messages + formatted results
192
255
  ```
193
256
 
194
- ## Thread ID
257
+ ### Merge
195
258
 
196
- For persistent conversations:
259
+ Merge additional values into memory:
197
260
 
198
261
  ```ruby
199
- # Set thread ID
200
- state.thread_id = "thread_123"
262
+ memory.merge!(user_id: 123, category: "billing")
263
+ ```
201
264
 
202
- # Or via UserMessage
203
- message = UserMessage.new(
204
- "Continue",
205
- thread_id: "thread_123"
206
- )
207
- state = RobotLab.create_state(message: message)
265
+ ### Key Management
266
+
267
+ ```ruby
268
+ memory.key?(:user_id) # Check existence
269
+ memory.keys # Get all non-reserved keys
270
+ memory.all_keys # Get all keys including reserved
271
+ memory.delete(:temp_data) # Delete a specific key
272
+ memory.clear # Clear all non-reserved keys
273
+ memory.reset # Reset to initial state (preserves cache)
208
274
  ```
209
275
 
210
- ## State Cloning
276
+ ## Cloning
211
277
 
212
- Create independent copies:
278
+ Create independent copies of memory for isolated execution. Subscriptions are not cloned:
213
279
 
214
280
  ```ruby
215
- original = RobotLab.create_state(data: { count: 1 })
216
- clone = original.clone
281
+ original = RobotLab.create_memory(data: { count: 1 })
282
+ cloned = original.clone
217
283
 
218
- clone.data[:count] = 2
219
- original.data[:count] # Still 1
284
+ cloned[:count] = 2
285
+ original[:count] #=> still 1
220
286
  ```
221
287
 
222
288
  ## Serialization
223
289
 
224
- Convert state to/from hash:
290
+ Convert memory to and from hash for persistence:
225
291
 
226
292
  ```ruby
227
293
  # To hash
228
- hash = state.to_h
229
- json = state.to_json
294
+ hash = memory.to_h
295
+ #=> {
296
+ # data: { ... },
297
+ # results: [...],
298
+ # messages: [...],
299
+ # session_id: "abc123",
300
+ # custom: { my_key: "value" }
301
+ # }
302
+
303
+ # To JSON
304
+ json = memory.to_json
230
305
 
231
306
  # From hash
232
- state = State.from_hash(hash)
307
+ memory = Memory.from_hash(hash)
233
308
  ```
234
309
 
235
- ### Hash Structure
310
+ ## Semantic Cache
311
+
312
+ Memory includes a semantic cache via `RubyLLM::SemanticCache` that reduces costs and latency by returning cached responses for semantically equivalent queries:
236
313
 
237
314
  ```ruby
238
- {
239
- data: { ... },
240
- results: [
241
- {
242
- robot_name: "assistant",
243
- output: [...],
244
- tool_calls: [...],
245
- stop_reason: "stop"
246
- }
247
- ],
248
- thread_id: "thread_123"
249
- }
315
+ # Using the cache with fetch
316
+ response = memory.cache.fetch("What is Ruby?") do
317
+ RubyLLM.chat.ask("What is Ruby?")
318
+ end
319
+
320
+ # Wrapping a chat instance
321
+ chat = memory.cache.wrap(RubyLLM.chat(model: "gpt-4o"))
322
+ chat.ask("What is Ruby?") # Cached on semantic similarity
250
323
  ```
251
324
 
252
- ## UserMessage
325
+ Caching can be disabled per-memory or per-robot:
253
326
 
254
- Enhanced message with metadata:
327
+ ```ruby
328
+ memory = RobotLab.create_memory(enable_cache: false)
329
+ robot = RobotLab.build(name: "bot", system_prompt: "...", enable_cache: false)
330
+ ```
331
+
332
+ ## Backend Options
333
+
334
+ Memory defaults to a Hash-based backend but can use Redis for distributed scenarios:
255
335
 
256
336
  ```ruby
257
- message = UserMessage.new(
258
- "What's the status of my order?",
259
- thread_id: "thread_123",
260
- system_prompt: "Respond in Spanish", # Augment system prompt
261
- metadata: {
262
- user_id: "user_456",
263
- source: "web_chat"
264
- }
265
- )
337
+ # Auto-detect (uses Redis if available, falls back to Hash)
338
+ memory = Memory.new(backend: :auto)
266
339
 
267
- state = RobotLab.create_state(message: message)
268
- ```
340
+ # Force Hash backend
341
+ memory = Memory.new(backend: :hash)
269
342
 
270
- ### UserMessage Properties
343
+ # Force Redis backend
344
+ memory = Memory.new(backend: :redis)
271
345
 
272
- | Property | Description |
273
- |----------|-------------|
274
- | `content` | The message text |
275
- | `thread_id` | Conversation thread ID |
276
- | `system_prompt` | Additional system instructions |
277
- | `metadata` | Custom key-value data |
278
- | `id` | Unique message identifier |
279
- | `created_at` | Timestamp |
346
+ # Check backend
347
+ memory.redis? #=> true/false
348
+ ```
349
+
350
+ Redis is configured via `RobotLab.config.redis` or the `REDIS_URL` environment variable.
280
351
 
281
352
  ## Best Practices
282
353
 
283
354
  ### 1. Use Memory for Cross-Robot Data
284
355
 
285
356
  ```ruby
286
- # Don't pass data through routing
287
- router = ->(args) {
288
- # Bad: parsing previous output for data
289
- }
290
-
291
- # Do: use memory
292
- state.memory.remember("classification", "billing")
293
- # Later robot reads it directly
357
+ # In a network, robots share memory automatically.
358
+ # Robot A writes:
359
+ memory.set(:classification, "billing")
360
+
361
+ # Robot B reads:
362
+ category = memory.get(:classification)
294
363
  ```
295
364
 
296
- ### 2. Scope Memory Appropriately
365
+ ### 2. Use Blocking Reads for Concurrent Pipelines
297
366
 
298
367
  ```ruby
299
- # Session data
300
- session = state.memory.scoped("session:#{session_id}")
301
-
302
- # User preferences
303
- user = state.memory.scoped("user:#{user_id}")
304
-
305
- # Temporary working data
306
- temp = state.memory.scoped("temp")
368
+ # When robots run in parallel, use blocking reads
369
+ # to synchronize on shared data:
370
+ results = memory.get(:sentiment, :entities, wait: 60)
307
371
  ```
308
372
 
309
373
  ### 3. Keep Data Minimal
310
374
 
311
375
  ```ruby
312
- # Don't store large objects
313
- state.data[:huge_response] = api_response # Bad
376
+ # Store references instead of large objects
377
+ memory[:response_id] = response.id # Preferred
378
+ # memory[:huge_response] = api_response # Avoid
379
+ ```
380
+
381
+ ### 4. Reset Between Independent Runs
314
382
 
315
- # Store references instead
316
- state.data[:response_id] = response.id # Good
383
+ ```ruby
384
+ network.reset_memory
385
+ result = network.run(message: "New conversation")
317
386
  ```
318
387
 
319
388
  ## Next Steps
320
389
 
321
- - [Memory System](../guides/memory.md) - Advanced memory patterns
322
- - [History Guide](../guides/history.md) - Persisting state
390
+ - [Network Orchestration](network-orchestration.md) - How networks share memory
323
391
  - [Message Flow](message-flow.md) - How messages are processed