robot_lab 0.2.1 → 0.2.7

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 (236) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.loki +60 -0
  4. data/.quality/reek_baseline.txt +43 -0
  5. data/.rubocop.yml +5 -167
  6. data/CHANGELOG.md +54 -0
  7. data/README.md +185 -64
  8. data/Rakefile +28 -12
  9. data/docs/api/core/index.md +41 -14
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +285 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +636 -60
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +152 -14
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +83 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +519 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +118 -30
  33. data/docs/architecture/index.md +86 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +153 -38
  36. data/docs/architecture/robot-execution.md +173 -91
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +299 -93
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +296 -50
  48. data/docs/guides/creating-networks.md +220 -31
  49. data/docs/guides/hooks.md +1084 -0
  50. data/docs/guides/index.md +5 -0
  51. data/docs/guides/knowledge.md +37 -6
  52. data/docs/guides/mcp-integration.md +211 -44
  53. data/docs/guides/memory.md +103 -12
  54. data/docs/guides/observability.md +166 -51
  55. data/docs/guides/streaming.md +184 -125
  56. data/docs/guides/using-tools.md +300 -11
  57. data/docs/index.md +37 -5
  58. data/examples/01_simple_robot.rb +2 -2
  59. data/examples/02_tools.rb +14 -4
  60. data/examples/03_network.rb +23 -9
  61. data/examples/04_mcp.rb +11 -4
  62. data/examples/05_streaming.rb +8 -5
  63. data/examples/06_prompt_templates.rb +13 -9
  64. data/examples/07_network_memory.rb +5 -5
  65. data/examples/08_llm_config.rb +20 -15
  66. data/examples/09_chaining.rb +4 -4
  67. data/examples/11_network_introspection.rb +4 -4
  68. data/examples/12_message_bus.rb +2 -2
  69. data/examples/13_spawn.rb +2 -2
  70. data/examples/14_rusty_circuit/README.md +1 -0
  71. data/examples/14_rusty_circuit/comic.rb +7 -3
  72. data/examples/14_rusty_circuit/display.rb +14 -0
  73. data/examples/14_rusty_circuit/heckler.rb +8 -6
  74. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  75. data/examples/14_rusty_circuit/scout.rb +17 -10
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  78. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  79. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  80. data/examples/16_writers_room/writer.rb +22 -22
  81. data/examples/16_writers_room/writers_room.rb +2 -0
  82. data/examples/17_skills.rb +14 -13
  83. data/examples/18_rails/README.md +20 -1
  84. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  85. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  86. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  87. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  88. data/examples/19_token_tracking.rb +25 -9
  89. data/examples/20_circuit_breaker.rb +10 -7
  90. data/examples/21_learning_loop.rb +42 -16
  91. data/examples/22_context_compression.rb +23 -23
  92. data/examples/23_convergence.rb +24 -17
  93. data/examples/24_structured_delegation.rb +13 -8
  94. data/examples/25_history_search.rb +12 -8
  95. data/examples/27_incident_response/incident_response.rb +31 -13
  96. data/examples/28_mcp_discovery.rb +17 -13
  97. data/examples/29_ractor_tools.rb +4 -2
  98. data/examples/30_ractor_network.rb +22 -17
  99. data/examples/31_launch_assessment.rb +20 -9
  100. data/examples/32_newsletter_reader.rb +7 -2
  101. data/examples/33_stock_predictor.rb +34 -13
  102. data/examples/34_agentskills.rb +7 -3
  103. data/examples/35_hooks.rb +266 -0
  104. data/examples/README.md +203 -42
  105. data/examples/common.rb +79 -7
  106. data/examples/xyzzy.rb +97 -0
  107. data/lib/robot_lab/agent_skill.rb +5 -4
  108. data/lib/robot_lab/budget/ledger.rb +98 -0
  109. data/lib/robot_lab/capabilities.rb +84 -0
  110. data/lib/robot_lab/config/defaults.yml +10 -0
  111. data/lib/robot_lab/config.rb +14 -9
  112. data/lib/robot_lab/error.rb +34 -4
  113. data/lib/robot_lab/errors.rb +45 -0
  114. data/lib/robot_lab/hook.rb +79 -0
  115. data/lib/robot_lab/hook_context.rb +194 -0
  116. data/lib/robot_lab/hook_registry.rb +55 -0
  117. data/lib/robot_lab/hooks.rb +87 -0
  118. data/lib/robot_lab/mcp/connection_poller.rb +2 -2
  119. data/lib/robot_lab/names.rb +402 -0
  120. data/lib/robot_lab/narrator.rb +87 -0
  121. data/lib/robot_lab/network.rb +77 -20
  122. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  123. data/lib/robot_lab/robot/budget.rb +89 -0
  124. data/lib/robot_lab/robot/bus_messaging.rb +90 -18
  125. data/lib/robot_lab/robot/hooking.rb +56 -0
  126. data/lib/robot_lab/robot/mcp_management.rb +8 -2
  127. data/lib/robot_lab/robot/template_rendering.rb +18 -5
  128. data/lib/robot_lab/robot.rb +233 -89
  129. data/lib/robot_lab/run_config.rb +24 -5
  130. data/lib/robot_lab/runnable.rb +51 -0
  131. data/lib/robot_lab/sandbox/null.rb +13 -0
  132. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  133. data/lib/robot_lab/sandbox.rb +52 -0
  134. data/lib/robot_lab/script_tool.rb +60 -2
  135. data/lib/robot_lab/task.rb +26 -20
  136. data/lib/robot_lab/tool.rb +52 -11
  137. data/lib/robot_lab/version.rb +1 -1
  138. data/lib/robot_lab.rb +70 -15
  139. data/mkdocs.yml +7 -1
  140. metadata +41 -99
  141. data/examples/temp.md +0 -51
  142. data/site/404.html +0 -2300
  143. data/site/api/core/index.html +0 -2706
  144. data/site/api/core/memory/index.html +0 -3793
  145. data/site/api/core/network/index.html +0 -3500
  146. data/site/api/core/robot/index.html +0 -4566
  147. data/site/api/core/state/index.html +0 -3390
  148. data/site/api/core/tool/index.html +0 -3843
  149. data/site/api/index.html +0 -2635
  150. data/site/api/mcp/client/index.html +0 -3435
  151. data/site/api/mcp/index.html +0 -2783
  152. data/site/api/mcp/server/index.html +0 -3252
  153. data/site/api/mcp/transports/index.html +0 -3352
  154. data/site/api/messages/index.html +0 -2641
  155. data/site/api/messages/text-message/index.html +0 -3087
  156. data/site/api/messages/tool-call-message/index.html +0 -3159
  157. data/site/api/messages/tool-result-message/index.html +0 -3252
  158. data/site/api/messages/user-message/index.html +0 -3212
  159. data/site/api/streaming/context/index.html +0 -3282
  160. data/site/api/streaming/events/index.html +0 -3347
  161. data/site/api/streaming/index.html +0 -2738
  162. data/site/architecture/core-concepts/index.html +0 -3757
  163. data/site/architecture/index.html +0 -2797
  164. data/site/architecture/message-flow/index.html +0 -3238
  165. data/site/architecture/network-orchestration/index.html +0 -3433
  166. data/site/architecture/robot-execution/index.html +0 -3140
  167. data/site/architecture/state-management/index.html +0 -3498
  168. data/site/assets/css/custom.css +0 -56
  169. data/site/assets/images/favicon.png +0 -0
  170. data/site/assets/images/robot_lab.jpg +0 -0
  171. data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
  172. data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
  173. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  174. data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  175. data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  176. data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  177. data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  178. data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  179. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  180. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  181. data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  182. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  183. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  184. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  185. data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  186. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  187. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  188. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  189. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  190. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  191. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  192. data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  193. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  194. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  195. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  196. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  197. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  198. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  199. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  200. data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  201. data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  202. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  203. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  204. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  205. data/site/assets/javascripts/lunr/tinyseg.js +0 -206
  206. data/site/assets/javascripts/lunr/wordcut.js +0 -6708
  207. data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
  208. data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
  209. data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
  210. data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
  211. data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  212. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
  213. data/site/concepts/index.html +0 -3455
  214. data/site/examples/basic-chat/index.html +0 -2880
  215. data/site/examples/index.html +0 -2907
  216. data/site/examples/mcp-server/index.html +0 -3018
  217. data/site/examples/multi-robot-network/index.html +0 -3131
  218. data/site/examples/rails-application/index.html +0 -3329
  219. data/site/examples/tool-usage/index.html +0 -3085
  220. data/site/getting-started/configuration/index.html +0 -3745
  221. data/site/getting-started/index.html +0 -2572
  222. data/site/getting-started/installation/index.html +0 -2981
  223. data/site/getting-started/quick-start/index.html +0 -2942
  224. data/site/guides/building-robots/index.html +0 -4290
  225. data/site/guides/creating-networks/index.html +0 -3858
  226. data/site/guides/index.html +0 -2586
  227. data/site/guides/mcp-integration/index.html +0 -3581
  228. data/site/guides/memory/index.html +0 -3586
  229. data/site/guides/rails-integration/index.html +0 -4019
  230. data/site/guides/streaming/index.html +0 -3157
  231. data/site/guides/using-tools/index.html +0 -3802
  232. data/site/index.html +0 -2671
  233. data/site/search/search_index.json +0 -1
  234. data/site/sitemap.xml +0 -183
  235. data/site/sitemap.xml.gz +0 -0
  236. data/site/tags.json +0 -1
@@ -1,255 +1,145 @@
1
- # Memory (State Management)
1
+ # StateProxy
2
2
 
3
- Memory manages conversation data, results, and runtime state. There is no separate `State` class; `Memory` serves this role.
3
+ !!! danger "There is no `RobotLab::State` class"
4
+ RobotLab has never had a `State` class. Runtime state lives in
5
+ [`RobotLab::Memory`](memory.md). The only state object is
6
+ **`RobotLab::StateProxy`** (`lib/robot_lab/state_proxy.rb`), which is what
7
+ `memory.data` returns — and that is what this page documents.
4
8
 
5
- ## Class: `RobotLab::Memory`
9
+ This page is listed as "State" in the site navigation for historical reasons.
10
+ For the key-value store, subscriptions, blocking reads, results, and
11
+ serialization, go to **[Memory](memory.md)**.
6
12
 
7
- ```ruby
8
- memory = RobotLab.create_memory(
9
- data: { user_id: "123" }
10
- )
11
- ```
12
-
13
- ## Attributes
14
-
15
- ### session_id
16
-
17
- ```ruby
18
- memory.session_id # => String | nil
19
- ```
20
-
21
- Conversation session identifier for persistence.
22
-
23
- ### data
24
-
25
- ```ruby
26
- memory.data # => StateProxy
27
- ```
28
-
29
- Access workflow data as a proxy object.
30
-
31
- ```ruby
32
- memory.data[:user_id] # Hash access
33
- memory.data.user_id # Method access
34
- memory.data[:status] = "active"
35
- ```
36
-
37
- ### results
38
-
39
- ```ruby
40
- memory.results # => Array<RobotResult>
41
- ```
42
-
43
- All robot execution results.
13
+ ## Class: `RobotLab::StateProxy`
44
14
 
45
- ### messages
15
+ A thin wrapper around a symbol-keyed Hash that adds method-style access and an
16
+ optional change callback. It is not a `Hash` subclass and does not implement the
17
+ full `Hash` interface — the supported methods are listed below.
46
18
 
47
19
  ```ruby
48
- memory.messages # => Array<Message>
49
- ```
50
-
51
- Formatted conversation messages for LLM.
20
+ proxy = RobotLab::StateProxy.new({ count: 0, name: "test" })
52
21
 
53
- ## Methods
54
-
55
- ### append_result
56
-
57
- ```ruby
58
- memory.append_result(robot_result)
22
+ proxy[:count] = 1
23
+ proxy.count # => 1
24
+ proxy[:name] # => "test"
25
+ proxy.to_h # => { count: 1, name: "test" }
59
26
  ```
60
27
 
61
- Add a robot result to history.
28
+ ## Where you get one
62
29
 
63
- ### set_results
30
+ `memory.data` — the reserved `:data` key of a `Memory` — is the only place the
31
+ framework hands you a `StateProxy`:
64
32
 
65
33
  ```ruby
66
- memory.set_results(array_of_results)
67
- ```
68
-
69
- Replace all results.
70
-
71
- ### results_from
72
-
73
- ```ruby
74
- memory.results_from(5) # => Array<RobotResult>
75
- ```
76
-
77
- Get results starting at index.
78
-
79
- ### session_id=
80
-
81
- ```ruby
82
- memory.session_id = "session_123"
83
- ```
84
-
85
- Set the session identifier.
34
+ memory = RobotLab.create_memory(data: { user_id: "123" })
86
35
 
87
- ### format_history
88
-
89
- ```ruby
90
- memory.format_history # => Array<Message>
91
- ```
92
-
93
- Format results as conversation history.
94
-
95
- ### clone
96
-
97
- ```ruby
98
- new_memory = memory.clone
99
- ```
100
-
101
- Create a deep copy.
102
-
103
- ### to_h
104
-
105
- ```ruby
106
- memory.to_h # => Hash
36
+ memory.data # => #<RobotLab::StateProxy {user_id: "123"}>
37
+ memory.data[:user_id] # => "123" Hash access
38
+ memory.data.user_id # => "123" method access
39
+ memory.data[:status] = "active"
107
40
  ```
108
41
 
109
- Hash representation.
110
-
111
- ### to_json
112
-
113
- ```ruby
114
- memory.to_json # => String
115
- ```
42
+ The proxy is memoized (`@data ||= StateProxy.new(...)`) and is reset whenever
43
+ `memory[:data] = ...` or `memory.reset` is called.
116
44
 
117
- JSON representation.
45
+ !!! warning "`memory.data` mutations are not reactive"
46
+ `memory.data[:x] = 1` writes through the proxy into the `:data` hash. It does
47
+ **not** go through `Memory#set`, so it wakes no blocking readers and notifies
48
+ no subscribers. Use `memory.set(:x, 1)` on a non-reserved key when you need
49
+ reactive semantics.
118
50
 
119
- ### from_hash (class method)
51
+ ## Constructor
120
52
 
121
53
  ```ruby
122
- memory = Memory.from_hash(hash)
54
+ RobotLab::StateProxy.new(data = {}, on_change: nil)
123
55
  ```
124
56
 
125
- Restore from hash.
126
-
127
- ## StateProxy
128
-
129
- The `data` attribute is a `StateProxy`:
57
+ | Name | Type | Default | Description |
58
+ |------|------|---------|-------------|
59
+ | `data` | `Hash` | `{}` | Initial data. Keys are converted to Symbols via `transform_keys(&:to_sym)` |
60
+ | `on_change` | `Proc`, `nil` | `nil` | Called as `on_change.call(key, old_value, new_value)` on `[]=`, and only when the value actually changed (`old_value != value`) |
130
61
 
131
- ```ruby
132
- proxy = memory.data
133
-
134
- # Hash-style access
135
- proxy[:key]
136
- proxy[:key] = value
137
-
138
- # Method-style access
139
- proxy.key
140
- proxy.key = value
141
-
142
- # Hash operations
143
- proxy.key?(:key)
144
- proxy.keys
145
- proxy.values
146
- proxy.each { |k, v| ... }
147
- proxy.merge!(other_hash)
148
- proxy.delete(:key)
149
- proxy.to_h
150
- proxy.empty?
151
- proxy.size
152
- ```
62
+ `Memory#data` constructs the proxy **without** an `on_change` callback.
153
63
 
154
- ## Creating Memory
64
+ ## Methods
155
65
 
156
- ### Basic
66
+ ### Element access
157
67
 
158
68
  ```ruby
159
- memory = RobotLab.create_memory
69
+ proxy[:key] # => value (keys are symbolized on read)
70
+ proxy[:key] = value # fires on_change when the value differs
160
71
  ```
161
72
 
162
- ### With Data
163
-
164
- ```ruby
165
- memory = RobotLab.create_memory(
166
- data: {
167
- user_id: "user_123",
168
- order_id: "ord_456"
169
- }
170
- )
171
- ```
73
+ Both `[]` and `[]=` call `key.to_sym`, so `proxy["a"]` and `proxy[:a]` are the same entry.
172
74
 
173
- ### With Session ID
75
+ ### Method-style access
174
76
 
175
77
  ```ruby
176
- memory = RobotLab.create_memory
177
- memory.session_id = "session_123"
78
+ proxy.name # same as proxy[:name] — only when :name already exists
79
+ proxy.name = "x" # same as proxy[:name] = "x" — always works
178
80
  ```
179
81
 
180
- ### With Existing Results
82
+ Implemented with `method_missing`. A **getter** for a key that does not exist
83
+ falls through to `super` and raises `NoMethodError`; a **setter** always works and
84
+ creates the key. `respond_to?(:name)` is true only once the key exists.
181
85
 
182
- ```ruby
183
- memory = RobotLab.create_memory
184
- memory.set_results(previous_results)
185
- ```
86
+ Method-style access cannot reach a key whose name collides with a real
87
+ `StateProxy` method (`keys`, `size`, `map`, `each`, `delete`, `dup`, `inspect`, …).
88
+ Use `proxy[:keys]` for those.
186
89
 
187
- ## UserMessage
90
+ ### Full method list
188
91
 
189
- Enhanced message with metadata:
190
-
191
- ```ruby
192
- message = UserMessage.new(
193
- "What's my order status?",
194
- session_id: "session_123",
195
- system_prompt: "Respond in Spanish",
196
- metadata: { source: "web" }
197
- )
92
+ | Method | Returns | Notes |
93
+ |--------|---------|-------|
94
+ | `[](key)` | value | Symbolizes the key |
95
+ | `[]=(key, value)` | value | Symbolizes the key; fires `on_change` |
96
+ | `key?(key)` | `Boolean` | Aliases: `has_key?`, `include?` |
97
+ | `keys` | `Array<Symbol>` | |
98
+ | `values` | `Array` | |
99
+ | `each { \|k, v\| }` | delegates to `Hash#each` | |
100
+ | `map { \|k, v\| }` | `Array` | |
101
+ | `delete(key)` | deleted value | Does **not** fire `on_change` |
102
+ | `merge!(other)` | `self` | Assigns each pair through `[]=`, so `on_change` fires per key |
103
+ | `to_h` | `Hash` | A **shallow** `dup`; aliased as `to_hash` |
104
+ | `dup` | `StateProxy` | Deep duplicate, preserving `on_change` |
105
+ | `empty?` | `Boolean` | |
106
+ | `size` | `Integer` | Aliased as `length` |
107
+ | `inspect` | `String` | `#<RobotLab::StateProxy {...}>` |
198
108
 
199
- message.content # => "What's my order status?"
200
- message.session_id # => "session_123"
201
- message.system_prompt # => "Respond in Spanish"
202
- message.metadata # => { source: "web" }
203
- message.id # => UUID
204
- message.created_at # => Time
205
- ```
109
+ There is no `clear`, no `fetch`, no `dig`, and no `clone`. `to_h` is a shallow
110
+ copy, so nested Hashes are shared with the proxy — mutate them and the proxy sees
111
+ it. Use `dup` when you need isolation.
206
112
 
207
113
  ## Examples
208
114
 
209
- ### Accessing Data
115
+ ### Nested data
210
116
 
211
117
  ```ruby
212
118
  memory = RobotLab.create_memory(
213
119
  data: { user: { name: "Alice", plan: "pro" } }
214
120
  )
215
121
 
216
- memory.data[:user][:name] # => "Alice"
122
+ memory.data[:user][:name] # => "Alice" (plain Hash below the top level)
123
+ memory.data.user # => { name: "Alice", plan: "pro" }
217
124
  memory.data.to_h # => { user: { name: "Alice", plan: "pro" } }
218
125
  ```
219
126
 
220
- ### Working with Results
221
-
222
- ```ruby
223
- # After running network
224
- memory.results.size # Number of results
225
- memory.results.last # Most recent
226
- memory.results.map(&:robot_name) # ["classifier", "support"]
227
- ```
228
-
229
- ### Using Reactive Memory
230
-
231
- ```ruby
232
- memory.set(:intent, "billing")
233
- intent = memory.get(:intent)
234
-
235
- memory.subscribe(:status) do |change|
236
- puts "Status changed to #{change.value} by #{change.writer}"
237
- end
238
- ```
127
+ Only the top level is proxied. Nested values are returned as-is — a nested Hash
128
+ is a plain Hash, not another `StateProxy`.
239
129
 
240
- ### Serialization
130
+ ### Change tracking
241
131
 
242
132
  ```ruby
243
- # Save memory
244
- json = memory.to_json
245
- File.write("memory.json", json)
133
+ proxy = RobotLab::StateProxy.new({ status: "pending" }, on_change: lambda { |key, old, new|
134
+ puts "#{key}: #{old.inspect} -> #{new.inspect}"
135
+ })
246
136
 
247
- # Restore memory
248
- data = JSON.parse(File.read("memory.json"))
249
- memory = Memory.from_hash(data)
137
+ proxy[:status] = "active" # "status: \"pending\" -> \"active\""
138
+ proxy[:status] = "active" # silent — value unchanged
139
+ proxy.delete(:status) # silent — delete does not fire on_change
250
140
  ```
251
141
 
252
142
  ## See Also
253
143
 
144
+ - [Memory](memory.md) — the store that owns the proxy: keys, subscriptions, blocking reads, results, serialization
254
145
  - [State Management Architecture](../../architecture/state-management.md)
255
- - [Memory](memory.md)
@@ -49,6 +49,12 @@ Tool.new(robot: nil)
49
49
  |------|------|-------------|
50
50
  | `robot` | `Robot, nil` | The owning robot instance |
51
51
 
52
+ ## Constants
53
+
54
+ | Constant | Value | Description |
55
+ |----------|-------|-------------|
56
+ | `RobotLab::Robot::DEFAULT_MAX_TOOLS` | `128` | Ceiling on the number of tools a robot hands the provider per turn. Override per robot with `RunConfig#max_tools`; a nil, zero, or negative value falls back to 128, so the cap cannot be disabled |
57
+
52
58
  ## Class Methods
53
59
 
54
60
  ### raise_on_error / raise_on_error?
@@ -58,7 +64,60 @@ MyTool.raise_on_error = true
58
64
  MyTool.raise_on_error? # => true
59
65
  ```
60
66
 
61
- Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`. Does not affect other tool classes.
67
+ Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`.
68
+
69
+ !!! warning "`raise_on_error` does not walk the inheritance chain"
70
+ The reader is `defined?(@raise_on_error) ? @raise_on_error : false` — it
71
+ inspects only the receiving class's own instance variable. A subclass of a
72
+ class that set `self.raise_on_error = true` silently reverts to `false`:
73
+
74
+ ```ruby
75
+ class Critical < RobotLab::Tool; self.raise_on_error = true; end
76
+ class Derived < Critical; end
77
+
78
+ Critical.raise_on_error? # => true
79
+ Derived.raise_on_error? # => false <-- errors are swallowed again
80
+ ```
81
+
82
+ Set the flag explicitly on every subclass that needs it. Contrast
83
+ [`ractor_safe`](#ractor_safe-ractor_safe), which *does* walk the chain.
84
+
85
+ ### ractor_safe / ractor_safe?
86
+
87
+ ```ruby
88
+ class Pure < RobotLab::Tool
89
+ ractor_safe true
90
+ end
91
+
92
+ Pure.ractor_safe? # => true
93
+ ```
94
+
95
+ Declares that the tool class is safe to run inside a Ractor. With no argument it
96
+ acts as a getter; with a Boolean it sets the value. `ractor_safe?` is an alias
97
+ for `ractor_safe`.
98
+
99
+ Unlike `raise_on_error`, the getter **does** walk the inheritance chain: if the
100
+ class has no `@ractor_safe` of its own it asks its superclass, terminating at
101
+ `false`.
102
+
103
+ ```ruby
104
+ class Pure < RobotLab::Tool; ractor_safe true; end
105
+ class Derived < Pure; end
106
+
107
+ Derived.ractor_safe? # => true — inherited
108
+ ```
109
+
110
+ **Ractor pool dispatch.** `Tool#call` routes through the Ractor worker pool
111
+ (`RobotLab.ractor_pool.submit(self.class.name, args)`) only when **all three**
112
+ hold:
113
+
114
+ 1. `self.class.ractor_safe?` is true
115
+ 2. `self.class.name` is not `nil` (an anonymous class — including everything from
116
+ `Tool.create` — always falls through to the inline path)
117
+ 3. `RobotLab.extension_loaded?(:ractor)` — i.e. the `robot_lab-ractor` gem is loaded
118
+
119
+ Otherwise `execute` runs inline on the calling thread. Ractor-safe tools must be
120
+ stateless: they are instantiated fresh inside the worker for each call.
62
121
 
63
122
  ### Tool.create
64
123
 
@@ -82,9 +141,18 @@ Tool.create(
82
141
  | `name` | `String, Symbol` | Tool identifier |
83
142
  | `description` | `String` | What the tool does |
84
143
  | `parameters` | `Hash` | JSON Schema parameter definition |
85
- | `mcp` | `String` | MCP server name |
144
+ | `mcp` | `String` | MCP server name; makes `mcp?` true |
86
145
  | `robot` | `Robot` | Owning robot instance |
87
- | `&handler` | `Block` | Receives `args` hash, returns result |
146
+ | `&handler` | `Block` | Receives the `args` hash, returns the result |
147
+
148
+ `create` builds an **anonymous** subclass (`Class.new(self)`) and stashes the
149
+ explicit name on the instance. Two consequences:
150
+
151
+ - `parameters` is read with **symbol** keys (`params_hash[:properties]`,
152
+ `pdef[:type]`, `pdef[:description]`, `params_hash[:required]`). A string-keyed
153
+ schema is silently ignored and the tool ends up with no parameters.
154
+ - Because `self.class.name` is `nil`, a `create`d tool never dispatches to the
155
+ Ractor pool even if you set `ractor_safe`.
88
156
 
89
157
  ## Inherited DSL (from RubyLLM::Tool)
90
158
 
@@ -105,6 +173,22 @@ class MyTool < RobotLab::Tool
105
173
  end
106
174
  ```
107
175
 
176
+ `param` accepts exactly four options — anything else raises
177
+ `ArgumentError: unknown keyword`:
178
+
179
+ | Option | Type | Default | Description |
180
+ |--------|------|---------|-------------|
181
+ | `type` | `String` | `"string"` | JSON Schema type (`"string"`, `"integer"`, `"number"`, `"boolean"`, `"array"`, `"object"`) |
182
+ | `desc` | `String` | `nil` | Description shown to the LLM |
183
+ | `description` | `String` | `nil` | Synonym for `desc` |
184
+ | `required` | `Boolean` | `true` | Whether the LLM must supply the parameter |
185
+
186
+ !!! warning "There is no `enum:` option"
187
+ `param :unit, type: "string", enum: %w[c f]` raises
188
+ `ArgumentError: unknown keyword: :enum`. Constrain the value in the
189
+ description and validate it inside `execute`, or supply a full JSON Schema
190
+ through [`Tool.create(parameters:)`](#toolcreate).
191
+
108
192
  ### execute
109
193
 
110
194
  ```ruby
@@ -162,7 +246,19 @@ The MCP server name, set via `Tool.create(mcp: "server_name")`.
162
246
  tool.name # => String
163
247
  ```
164
248
 
165
- Returns the tool name. For subclasses, derived from the class name (CamelCase to snake_case). For `create`d tools, returns the explicit name.
249
+ Returns the tool name. For `create`d tools, the explicit `name:` you passed. For
250
+ subclasses, ruby_llm derives it from the class name — **including the namespace**,
251
+ with `::` rendered as `--`:
252
+
253
+ ```ruby
254
+ RobotLab::AskUser.new.name # => "robot_lab--ask_user"
255
+
256
+ class Sub1 < RobotLab::Tool; end
257
+ Sub1.new.name # => "sub1" (top-level class, no namespace)
258
+ ```
259
+
260
+ This matters when writing a `tools:` name allowlist: the allowlist must contain
261
+ the fully-derived name (`"robot_lab--ask_user"`), not the short one.
166
262
 
167
263
  ### mcp?
168
264
 
@@ -178,13 +274,35 @@ Whether this is an MCP-provided tool.
178
274
  result = tool.call(args_hash)
179
275
  ```
180
276
 
181
- Overrides `RubyLLM::Tool#call` with graceful error handling. Converts string keys to symbols and calls `execute(**args)`. If `execute` raises a `StandardError`, the error is caught and returned as a plain-text string the LLM can reason about:
277
+ Overrides `RubyLLM::Tool#call`. The whole invocation is wrapped in the
278
+ `:tool_call` hook family, then dispatched either to the Ractor pool or inline
279
+ (see [`ractor_safe`](#ractor_safe-ractor_safe)); ruby_llm's `call` converts
280
+ string keys to symbols and invokes `execute(**args)`.
182
281
 
183
- ```
184
- Error (tool_name): exception message
282
+ Errors are caught and returned to the LLM as a plain-text string it can reason
283
+ about. There are two distinct paths:
284
+
285
+ | Raised | Returned text | Logged? |
286
+ |--------|---------------|---------|
287
+ | `RobotLab::ToolError` | `Error (<name>): <message>` — plus `" (retryable)"` when `RobotLab::Errors.retryable?` is true | **No** |
288
+ | any other `StandardError` | `Error (<name>): <message>` | Yes — `RobotLab.config.logger.warn("Tool '<name>' error: <Class>: <message>")` |
289
+
290
+ ```ruby
291
+ class T2 < RobotLab::Tool
292
+ description "t"
293
+ def execute(**) = raise RobotLab::ToolError.new("nope", retryable: true)
294
+ end
295
+ T2.new.call({}) # => "Error (t2): nope (retryable)" -- nothing logged
296
+
297
+ class T3 < RobotLab::Tool
298
+ description "t"
299
+ def execute(**) = raise ArgumentError, "bad"
300
+ end
301
+ T3.new.call({}) # => "Error (t3): bad" -- WARN logged
185
302
  ```
186
303
 
187
- The error is also logged via `RobotLab.config.logger` at `:warn` level.
304
+ `" (retryable)"` is appended only for `ToolError`/`MCPError` instances
305
+ constructed with `retryable: true` — see [Retryable Errors](../errors.md#retryable-errors).
188
306
 
189
307
  To propagate exceptions instead of catching them (for critical tools), set `raise_on_error` on the class:
190
308
 
@@ -195,7 +313,8 @@ class CriticalTool < RobotLab::Tool
195
313
  end
196
314
  ```
197
315
 
198
- `raise_on_error` is per-class (defaults to `false`) and does not affect other tool classes.
316
+ `raise_on_error` applies to **both** paths, is per-class, defaults to `false`, and
317
+ [is not inherited by subclasses](#raise_on_error-raise_on_error).
199
318
 
200
319
  ### params_schema
201
320
 
@@ -219,7 +338,16 @@ Inherited. Returns provider-specific parameters (e.g., `{ strict: true }`).
219
338
  tool.to_h # => Hash
220
339
  ```
221
340
 
222
- Hash representation with `:name`, `:description`, `:mcp`.
341
+ Hash representation with `:name`, `:description`, `:mcp` — `.compact`ed, so
342
+ `:description` and `:mcp` are absent when nil:
343
+
344
+ ```ruby
345
+ class T4 < RobotLab::Tool
346
+ description "t"
347
+ def execute(**) = nil
348
+ end
349
+ T4.new.to_h # => { name: "t4", description: "t" } -- no :mcp key
350
+ ```
223
351
 
224
352
  ### to_json
225
353
 
@@ -253,14 +381,18 @@ class AdjustTemperature < RobotLab::Tool
253
381
  end
254
382
  end
255
383
 
256
- # Pass robot: self when constructing
257
- robot = RobotLab.build(
258
- name: "creative_bot",
259
- system_prompt: "You are creative.",
260
- local_tools: [AdjustTemperature.new(robot: self)]
261
- )
384
+ # The robot must exist before the tool can reference it, so attach afterwards
385
+ robot = RobotLab.build(name: "creative_bot", system_prompt: "You are creative.")
386
+ robot.local_tools << AdjustTemperature.new(robot: robot)
387
+
388
+ robot.run("Be more creative.", tools: :inherit)
262
389
  ```
263
390
 
391
+ Passing the bare **class** (`local_tools: [AdjustTemperature]`) leaves
392
+ `tool.robot` as `nil` — ruby_llm instantiates it with no arguments — and
393
+ `robot.with_temperature` then raises `NoMethodError` on nil. Instantiate with
394
+ `robot:` whenever the tool needs its owner.
395
+
264
396
  ## Parameter Types
265
397
 
266
398
  ### String
@@ -319,6 +451,8 @@ end
319
451
  | `choices` | `Array` | No | Numbered choices to present |
320
452
  | `default` | `String` | No | Value returned when user presses Enter without typing |
321
453
 
454
+ Its LLM-facing name is **`robot_lab--ask_user`** (namespace included), not `ask_user`.
455
+
322
456
  ### IO Resolution
323
457
 
324
458
  The tool reads input and writes output using the owning robot's `input`/`output` accessors:
@@ -326,10 +460,18 @@ The tool reads input and writes output using the owning robot's `input`/`output`
326
460
  1. `robot.input` / `robot.output` if set
327
461
  2. Falls back to `$stdin` / `$stdout`
328
462
 
463
+ The prompt label is `robot&.name || "Robot"`.
464
+
465
+ !!! warning "Attach an instance, not the class"
466
+ `local_tools: [RobotLab::AskUser]` gives ruby_llm a bare class, which it
467
+ instantiates with no arguments — so `tool.robot` is `nil`, IO falls back to
468
+ `$stdin`/`$stdout`, and every prompt is labelled `[Robot]`. Pass
469
+ `RobotLab::AskUser.new(robot: robot)` to get the robot's own name and streams.
470
+
329
471
  ### Terminal Output
330
472
 
331
473
  ```
332
- [robot_name] What programming language do you want to learn?
474
+ [interviewer] What programming language do you want to learn?
333
475
  1. Ruby
334
476
  2. Python
335
477
  3. Go
@@ -341,10 +483,13 @@ The tool reads input and writes output using the owning robot's `input`/`output`
341
483
  ```ruby
342
484
  robot = RobotLab.build(
343
485
  name: "interviewer",
344
- system_prompt: "Interview the user about their project needs. Use ask_user to gather information.",
345
- local_tools: [RobotLab::AskUser]
486
+ system_prompt: "Interview the user about their project needs. " \
487
+ "Use robot_lab--ask_user to gather information."
346
488
  )
347
- robot.run("Find out what the user wants to build")
489
+ robot.local_tools << RobotLab::AskUser.new(robot: robot)
490
+
491
+ # run() defaults to tools: :none — :inherit is required for the tool to be sent
492
+ robot.run("Find out what the user wants to build", tools: :inherit)
348
493
  ```
349
494
 
350
495
  ### Testing with StringIO