robot_lab 0.3.0 → 0.3.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,145 +0,0 @@
1
- # StateProxy
2
-
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.
8
-
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)**.
12
-
13
- ## Class: `RobotLab::StateProxy`
14
-
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.
18
-
19
- ```ruby
20
- proxy = RobotLab::StateProxy.new({ count: 0, name: "test" })
21
-
22
- proxy[:count] = 1
23
- proxy.count # => 1
24
- proxy[:name] # => "test"
25
- proxy.to_h # => { count: 1, name: "test" }
26
- ```
27
-
28
- ## Where you get one
29
-
30
- `memory.data` — the reserved `:data` key of a `Memory` — is the only place the
31
- framework hands you a `StateProxy`:
32
-
33
- ```ruby
34
- memory = RobotLab.create_memory(data: { user_id: "123" })
35
-
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"
40
- ```
41
-
42
- The proxy is memoized (`@data ||= StateProxy.new(...)`) and is reset whenever
43
- `memory[:data] = ...` or `memory.reset` is called.
44
-
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.
50
-
51
- ## Constructor
52
-
53
- ```ruby
54
- RobotLab::StateProxy.new(data = {}, on_change: nil)
55
- ```
56
-
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`) |
61
-
62
- `Memory#data` constructs the proxy **without** an `on_change` callback.
63
-
64
- ## Methods
65
-
66
- ### Element access
67
-
68
- ```ruby
69
- proxy[:key] # => value (keys are symbolized on read)
70
- proxy[:key] = value # fires on_change when the value differs
71
- ```
72
-
73
- Both `[]` and `[]=` call `key.to_sym`, so `proxy["a"]` and `proxy[:a]` are the same entry.
74
-
75
- ### Method-style access
76
-
77
- ```ruby
78
- proxy.name # same as proxy[:name] — only when :name already exists
79
- proxy.name = "x" # same as proxy[:name] = "x" — always works
80
- ```
81
-
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.
85
-
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.
89
-
90
- ### Full method list
91
-
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 {...}>` |
108
-
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.
112
-
113
- ## Examples
114
-
115
- ### Nested data
116
-
117
- ```ruby
118
- memory = RobotLab.create_memory(
119
- data: { user: { name: "Alice", plan: "pro" } }
120
- )
121
-
122
- memory.data[:user][:name] # => "Alice" (plain Hash below the top level)
123
- memory.data.user # => { name: "Alice", plan: "pro" }
124
- memory.data.to_h # => { user: { name: "Alice", plan: "pro" } }
125
- ```
126
-
127
- Only the top level is proxied. Nested values are returned as-is — a nested Hash
128
- is a plain Hash, not another `StateProxy`.
129
-
130
- ### Change tracking
131
-
132
- ```ruby
133
- proxy = RobotLab::StateProxy.new({ status: "pending" }, on_change: lambda { |key, old, new|
134
- puts "#{key}: #{old.inspect} -> #{new.inspect}"
135
- })
136
-
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
140
- ```
141
-
142
- ## See Also
143
-
144
- - [Memory](memory.md) — the store that owns the proxy: keys, subscriptions, blocking reads, results, serialization
145
- - [State Management Architecture](../../architecture/state-management.md)
@@ -1,520 +0,0 @@
1
- # Tool
2
-
3
- Callable function that robots can use to interact with external systems.
4
-
5
- ## Class: `RobotLab::Tool < RubyLLM::Tool`
6
-
7
- RobotLab::Tool inherits from RubyLLM::Tool, adding a `robot:` constructor parameter, a `Tool.create` factory for dynamic tools, and graceful error handling that returns plain-text errors to the LLM instead of crashing the run.
8
-
9
- ### Subclass Pattern
10
-
11
- ```ruby
12
- class GetWeather < RobotLab::Tool
13
- description "Get weather for a location"
14
-
15
- param :location, type: "string", desc: "City name or zip code"
16
- param :unit, type: "string", desc: "Temperature unit", required: false
17
-
18
- def execute(location:, unit: "celsius")
19
- WeatherService.current(location, unit: unit)
20
- end
21
- end
22
- ```
23
-
24
- ### Factory Pattern
25
-
26
- ```ruby
27
- tool = RobotLab::Tool.create(
28
- name: "get_weather",
29
- description: "Get current weather for a location",
30
- parameters: {
31
- type: "object",
32
- properties: {
33
- location: { type: "string", description: "City name" }
34
- },
35
- required: ["location"]
36
- }
37
- ) { |args| WeatherService.current(args[:location]) }
38
- ```
39
-
40
- ## Constructor
41
-
42
- ```ruby
43
- Tool.new(robot: nil)
44
- ```
45
-
46
- **Parameters:**
47
-
48
- | Name | Type | Description |
49
- |------|------|-------------|
50
- | `robot` | `Robot, nil` | The owning robot instance |
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
-
58
- ## Class Methods
59
-
60
- ### raise_on_error / raise_on_error?
61
-
62
- ```ruby
63
- MyTool.raise_on_error = true
64
- MyTool.raise_on_error? # => true
65
- ```
66
-
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.
121
-
122
- ### Tool.create
123
-
124
- Factory for dynamic tools (MCP wrappers, inline tools).
125
-
126
- ```ruby
127
- Tool.create(
128
- name:,
129
- description: nil,
130
- parameters: nil,
131
- mcp: nil,
132
- robot: nil,
133
- &handler
134
- )
135
- ```
136
-
137
- **Parameters:**
138
-
139
- | Name | Type | Description |
140
- |------|------|-------------|
141
- | `name` | `String, Symbol` | Tool identifier |
142
- | `description` | `String` | What the tool does |
143
- | `parameters` | `Hash` | JSON Schema parameter definition |
144
- | `mcp` | `String` | MCP server name; makes `mcp?` true |
145
- | `robot` | `Robot` | Owning robot instance |
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`.
156
-
157
- ## Inherited DSL (from RubyLLM::Tool)
158
-
159
- ### description
160
-
161
- ```ruby
162
- class MyTool < RobotLab::Tool
163
- description "What this tool does"
164
- end
165
- ```
166
-
167
- ### param
168
-
169
- ```ruby
170
- class MyTool < RobotLab::Tool
171
- param :name, type: "string", desc: "User's name"
172
- param :age, type: "integer", desc: "User's age", required: false
173
- end
174
- ```
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
-
192
- ### execute
193
-
194
- ```ruby
195
- class MyTool < RobotLab::Tool
196
- def execute(name:, age: nil)
197
- # Implementation
198
- end
199
- end
200
- ```
201
-
202
- ### halt
203
-
204
- Stop the tool use loop from within execute:
205
-
206
- ```ruby
207
- def execute(**)
208
- halt("Done processing")
209
- end
210
- ```
211
-
212
- ### with_params
213
-
214
- Set provider-specific parameters:
215
-
216
- ```ruby
217
- class MyTool < RobotLab::Tool
218
- with_params(strict: true)
219
- end
220
- ```
221
-
222
- ## Attributes
223
-
224
- ### robot
225
-
226
- ```ruby
227
- tool.robot # => Robot or nil
228
- tool.robot = some_robot
229
- ```
230
-
231
- Read/write accessor for the owning robot. Set via constructor or assigned later.
232
-
233
- ### mcp
234
-
235
- ```ruby
236
- tool.mcp # => String or nil
237
- ```
238
-
239
- The MCP server name, set via `Tool.create(mcp: "server_name")`.
240
-
241
- ## Methods
242
-
243
- ### name
244
-
245
- ```ruby
246
- tool.name # => String
247
- ```
248
-
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.
262
-
263
- ### mcp?
264
-
265
- ```ruby
266
- tool.mcp? # => Boolean
267
- ```
268
-
269
- Whether this is an MCP-provided tool.
270
-
271
- ### call
272
-
273
- ```ruby
274
- result = tool.call(args_hash)
275
- ```
276
-
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)`.
281
-
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
302
- ```
303
-
304
- `" (retryable)"` is appended only for `ToolError`/`MCPError` instances
305
- constructed with `retryable: true` — see [Retryable Errors](../errors.md#retryable-errors).
306
-
307
- To propagate exceptions instead of catching them (for critical tools), set `raise_on_error` on the class:
308
-
309
- ```ruby
310
- class CriticalTool < RobotLab::Tool
311
- self.raise_on_error = true
312
- # ...
313
- end
314
- ```
315
-
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).
318
-
319
- ### params_schema
320
-
321
- ```ruby
322
- tool.params_schema # => Hash or nil
323
- ```
324
-
325
- Inherited. Returns the JSON Schema for tool parameters.
326
-
327
- ### provider_params
328
-
329
- ```ruby
330
- tool.provider_params # => Hash
331
- ```
332
-
333
- Inherited. Returns provider-specific parameters (e.g., `{ strict: true }`).
334
-
335
- ### to_h
336
-
337
- ```ruby
338
- tool.to_h # => Hash
339
- ```
340
-
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
- ```
351
-
352
- ### to_json
353
-
354
- ```ruby
355
- tool.to_json # => String
356
- ```
357
-
358
- JSON representation.
359
-
360
- ### to_json_schema
361
-
362
- ```ruby
363
- tool.to_json_schema # => Hash
364
- ```
365
-
366
- JSON Schema representation for LLM function calling. Returns `{ name:, description:, parameters: }`.
367
-
368
- ## Robot-Aware Tools
369
-
370
- Tools that modify their owning robot use the `robot` accessor:
371
-
372
- ```ruby
373
- class AdjustTemperature < RobotLab::Tool
374
- description "Adjust the robot's creativity level"
375
-
376
- param :level, type: "number", desc: "Temperature from 0.0 to 1.0"
377
-
378
- def execute(level:)
379
- robot.with_temperature(level)
380
- "Temperature adjusted to #{level}"
381
- end
382
- end
383
-
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)
389
- ```
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
-
396
- ## Parameter Types
397
-
398
- ### String
399
-
400
- ```ruby
401
- param :name, type: "string", desc: "User's full name"
402
- ```
403
-
404
- ### Integer
405
-
406
- ```ruby
407
- param :count, type: "integer", desc: "Number of results"
408
- ```
409
-
410
- ### Number (Float)
411
-
412
- ```ruby
413
- param :price, type: "number", desc: "Price in dollars"
414
- ```
415
-
416
- ### Boolean
417
-
418
- ```ruby
419
- param :active, type: "boolean", desc: "Whether the user is active"
420
- ```
421
-
422
- ### Required vs Optional
423
-
424
- Parameters are required by default. Mark optional with `required: false`:
425
-
426
- ```ruby
427
- param :query, type: "string", desc: "Search query" # required
428
- param :limit, type: "integer", desc: "Max results", required: false # optional
429
- ```
430
-
431
- ## Built-in: AskUser
432
-
433
- `RobotLab::AskUser` is a built-in tool that lets a robot ask the user a question via the terminal. The LLM decides when human input is needed and calls this tool.
434
-
435
- ### Class: `RobotLab::AskUser < RobotLab::Tool`
436
-
437
- ```ruby
438
- class RobotLab::AskUser < RobotLab::Tool
439
- description "Ask the user a question and wait for their typed response"
440
- param :question, type: "string", desc: "The question to ask the user"
441
- param :choices, type: "array", desc: "Optional list of choices to present", required: false
442
- param :default, type: "string", desc: "Default value if user presses Enter", required: false
443
- end
444
- ```
445
-
446
- ### Parameters
447
-
448
- | Name | Type | Required | Description |
449
- |------|------|----------|-------------|
450
- | `question` | `String` | Yes | The question to display |
451
- | `choices` | `Array` | No | Numbered choices to present |
452
- | `default` | `String` | No | Value returned when user presses Enter without typing |
453
-
454
- Its LLM-facing name is **`robot_lab--ask_user`** (namespace included), not `ask_user`.
455
-
456
- ### IO Resolution
457
-
458
- The tool reads input and writes output using the owning robot's `input`/`output` accessors:
459
-
460
- 1. `robot.input` / `robot.output` if set
461
- 2. Falls back to `$stdin` / `$stdout`
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
-
471
- ### Terminal Output
472
-
473
- ```
474
- [interviewer] What programming language do you want to learn?
475
- 1. Ruby
476
- 2. Python
477
- 3. Go
478
- > [Ruby]
479
- ```
480
-
481
- ### Usage
482
-
483
- ```ruby
484
- robot = RobotLab.build(
485
- name: "interviewer",
486
- system_prompt: "Interview the user about their project needs. " \
487
- "Use robot_lab--ask_user to gather information."
488
- )
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)
493
- ```
494
-
495
- ### Testing with StringIO
496
-
497
- ```ruby
498
- robot = RobotLab::Robot.new(name: "bot", template: :assistant)
499
- robot.input = StringIO.new("Ruby\n")
500
- robot.output = StringIO.new
501
-
502
- tool = RobotLab::AskUser.new(robot: robot)
503
- result = tool.call("question" => "Pick a language:", "choices" => ["Ruby", "Python"])
504
- # => "Ruby"
505
- ```
506
-
507
- ### Choice Mapping
508
-
509
- When `choices` are provided, the user can type either:
510
-
511
- - A **number** (e.g., `2`) — mapped to the corresponding choice text
512
- - **Text** (e.g., `Python`) — returned as-is
513
-
514
- Out-of-range numbers are returned as-is (the LLM can re-ask if needed).
515
-
516
- ## See Also
517
-
518
- - [Using Tools Guide](../../guides/using-tools.md)
519
- - [Robot](robot.md)
520
- - [MCP Integration](../../guides/mcp-integration.md)