robot_lab 0.3.0 → 0.5.0

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