robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. 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)