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,780 +0,0 @@
1
- # Using Tools
2
-
3
- Tools give robots the ability to interact with external systems. RobotLab supports three approaches: `RubyLLM::Tool` subclasses, `RobotLab::Tool` subclasses (with robot access), and `RobotLab::Tool.create` for dynamic tools.
4
-
5
- ## Defining Tools
6
-
7
- ### RubyLLM::Tool Subclass
8
-
9
- For reusable tools that don't need robot access:
10
-
11
- ```ruby
12
- class GetWeather < RubyLLM::Tool
13
- description "Get current 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
- ### RobotLab::Tool Subclass
25
-
26
- For tools that need access to their owning robot (self-modification, spawning, etc.):
27
-
28
- ```ruby
29
- class AdjustEnergy < RobotLab::Tool
30
- description "Adjust the robot's creativity level"
31
-
32
- param :level, type: "number", desc: "Temperature from 0.0 to 1.0"
33
-
34
- def execute(level:)
35
- robot.with_temperature(level)
36
- "Temperature adjusted to #{level}"
37
- end
38
- end
39
- ```
40
-
41
- Pass `robot: self` when constructing:
42
-
43
- ```ruby
44
- class MyRobot < RobotLab::Robot
45
- def initialize
46
- super(
47
- name: "creative_bot",
48
- system_prompt: "You are creative.",
49
- local_tools: [AdjustEnergy.new(robot: self)]
50
- )
51
- end
52
- end
53
- ```
54
-
55
- ### RobotLab::Tool.create (Dynamic Tools)
56
-
57
- For quick, inline tools use the `Tool.create` factory:
58
-
59
- ```ruby
60
- get_time = RobotLab::Tool.create(
61
- name: "get_time",
62
- description: "Get the current time"
63
- ) { |_args| Time.now.to_s }
64
- ```
65
-
66
- With parameters:
67
-
68
- ```ruby
69
- weather_tool = RobotLab::Tool.create(
70
- name: "get_weather",
71
- description: "Get current weather for a location",
72
- parameters: {
73
- type: "object",
74
- properties: {
75
- location: { type: "string", description: "City name" }
76
- },
77
- required: ["location"]
78
- }
79
- ) { |args| WeatherService.current(args[:location]) }
80
- ```
81
-
82
- ## Built-in Tools
83
-
84
- ### AskUser
85
-
86
- `RobotLab::AskUser` lets a robot ask the user a question via the terminal. The LLM decides when it needs human input and calls the tool with a question, optional choices, and an optional default.
87
-
88
- `AskUser` reads its IO from `robot.input` / `robot.output`, and it finds the
89
- robot through its own `robot` accessor — which is only populated when you attach
90
- an **instance constructed with `robot:`**. Build the robot first, then attach:
91
-
92
- ```ruby
93
- robot = RobotLab.build(
94
- name: "onboarding",
95
- system_prompt: "Walk the user through project setup. Ask questions to understand their needs."
96
- )
97
- robot.local_tools << RobotLab::AskUser.new(robot: robot)
98
-
99
- robot.run("Help the user set up a new project", tools: :inherit)
100
- ```
101
-
102
- > [!WARNING]
103
- > Passing the bare class — `local_tools: [RobotLab::AskUser]` — leaves
104
- > `tool.robot` as `nil`. The tool then ignores `robot.input`/`robot.output`,
105
- > reads from the real `$stdin` (blocking your process, and hanging any test
106
- > suite), and labels its prompt `[Robot]` instead of the robot's name.
107
- > Note also the `tools: :inherit` above: a plain `run()` sends **zero** tools,
108
- > so the LLM would never see `AskUser` at all.
109
-
110
- The tool displays the robot's name and question, then waits for input:
111
-
112
- ```
113
- [onboarding] What programming language will you use?
114
- 1. Ruby
115
- 2. Python
116
- 3. Go
117
- >
118
- ```
119
-
120
- Features:
121
-
122
- - **Open-ended**: just a question, free-text response
123
- - **Multiple choice**: numbered options, user types the number or text
124
- - **Default value**: shown in the prompt, used when user presses Enter
125
-
126
- Because the IO is sourced from the robot, a `StringIO` pair makes the tool
127
- testable without a terminal:
128
-
129
- ```ruby
130
- robot.input = StringIO.new("2\n")
131
- robot.output = StringIO.new
132
-
133
- # ... the LLM calls ask_user(question: "Which language?", choices: %w[Ruby Python Go])
134
- robot.output.string
135
- # => "\n[onboarding] Which language?\n 1. Ruby\n 2. Python\n 3. Go\n> "
136
- ```
137
-
138
- > [!NOTE]
139
- > The name the LLM sees is **`robot_lab--ask_user`**, not `ask_user`. RubyLLM
140
- > derives tool names from the full class name including its namespace, so
141
- > `RobotLab::AskUser` becomes `robot_lab--ask_user`. (A trailing `Tool` is
142
- > stripped: a top-level `WeatherTool` is exposed as `weather`.) These derived
143
- > names are what an allowlist must match **for tools attached as instances** —
144
- > a tool attached as a class matches its class name instead. See
145
- > [Runtime Tool Filtering](#runtime-tool-filtering).
146
-
147
- See the [AskUser API reference](../api/core/tool.md#built-in-askuser) for full details.
148
-
149
- ## Attaching Tools to Robots
150
-
151
- ### Via Constructor
152
-
153
- Pass tools via the `local_tools:` parameter when building a robot:
154
-
155
- ```ruby
156
- robot = RobotLab.build(
157
- name: "assistant",
158
- system_prompt: "You are a helpful assistant with tool access.",
159
- local_tools: [GetWeather, CalculatorTool]
160
- )
161
- ```
162
-
163
- `local_tools:` and `tools:` are different mechanisms — don't confuse them. `local_tools:` attaches tool **instances or classes**; `tools:` (see [Runtime Tool Filtering](#runtime-tool-filtering) below) is a **name allowlist** that filters which of the already-attached tools are sent for a given turn. Passing an instance or class to `tools:` raises `ArgumentError` immediately, naming the offending class and pointing you at `local_tools:` instead:
164
-
165
- ```ruby
166
- RobotLab.build(name: "bot", tools: [GetWeather.new])
167
- # => ArgumentError: `tools:` expects tool names (String/Symbol) to allow, but
168
- # received GetWeather. To attach tool instances or classes, pass them as
169
- # `local_tools:` (e.g. RobotLab.build(local_tools: [MyTool.new])).
170
- ```
171
-
172
- ### Via Template Front Matter
173
-
174
- Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via `Object.const_get` and instantiates it:
175
-
176
- ```markdown title="prompts/weather_bot.md"
177
- ---
178
- description: Weather assistant with forecast tools
179
- tools:
180
- - GetWeather
181
- - GetForecast
182
- ---
183
- You are a weather assistant. Use your tools to look up weather information.
184
- ```
185
-
186
- ```ruby
187
- # Tools are resolved from frontmatter — no local_tools: needed
188
- robot = RobotLab.build(template: :weather_bot)
189
- ```
190
-
191
- Tool classes must be defined and loaded before building the robot. Unresolvable names are skipped with a warning. Constructor `local_tools:` overrides frontmatter `tools:` when provided.
192
-
193
- ### Via Chaining
194
-
195
- `with_tools` is delegated straight to the underlying `RubyLLM::Chat`, so it
196
- writes tools onto the chat rather than onto the robot's `local_tools`:
197
-
198
- ```ruby
199
- robot = RobotLab.build(name: "assistant", system_prompt: "...")
200
- robot.with_tools(GetWeather, CalculatorTool)
201
- ```
202
-
203
- > [!WARNING]
204
- > Chained tools do not survive a `run`. Because `run` defaults to
205
- > `tools: :none`, it replaces the chat's tool list with an empty one before
206
- > asking the LLM — wiping whatever `with_tools` put there:
207
- >
208
- > ```ruby
209
- > robot.with_tools(GetWeather, CalculatorTool)
210
- > robot.chat.tools.keys # => [:get_weather, :calculator]
211
- > robot.run("Hello")
212
- > robot.chat.tools.keys # => [] <- cleared by the run
213
- > ```
214
- >
215
- > For tools that should persist across runs, attach them with `local_tools:`
216
- > and pass `tools: :inherit` on each `run`.
217
-
218
- ## Runtime Tool Filtering
219
-
220
- `tools:` (and `mcp:`) also work as a **per-run** override, passed to `run()` itself, on top of the build-time/network/global hierarchy described in [Hierarchical MCP and Tools](../getting-started/configuration.md#hierarchical-mcp-and-tools):
221
-
222
- ```ruby
223
- robot.run("What's the weather?", tools: :inherit) # every attached tool
224
- robot.run("Just chat, no tools needed.", tools: :none) # zero tools this turn
225
- # Allowlist. This robot attached its tools as CLASSES, so match the class names:
226
- robot.run("Only use the calculator.", tools: %w[CalculatorTool])
227
- ```
228
-
229
- | Value | Meaning |
230
- |-------|---------|
231
- | `:inherit` | Propagate the parent level's allowlist (build-time / network / global) |
232
- | `:none`, `[]` | Send **zero** tools this turn |
233
- | `nil` | No filter at all — every attached tool, **discarding** any parent allowlist |
234
- | `["name", ...]` | Only these tools, matched against how each was attached |
235
-
236
- > [!CAUTION]
237
- > `tools: nil` does **not** send zero tools, and it is **not** the same as
238
- > `:inherit`. `:inherit` carries the parent's allowlist down; `nil` throws the
239
- > filter away entirely. With a robot built `tools: %w[GetWeather]` holding
240
- > `GetWeather` and `CalculatorTool`:
241
- >
242
- > ```
243
- > run(tools: :inherit) -> [:get_weather] # parent allowlist honored
244
- > run(tools: nil) -> [:get_weather, :calculator] # allowlist discarded
245
- > ```
246
- >
247
- > If you mean zero tools, write `:none`.
248
-
249
- > [!IMPORTANT]
250
- > An allowlist entry must match **how the tool was attached**, because the
251
- > comparison uses `tool.name` and `Class#name` differs from `RubyLLM::Tool#name`:
252
- >
253
- > | Attached as | Allowlist that matches |
254
- > |---|---|
255
- > | `local_tools: [CalculatorTool]` (class) | `[CalculatorTool]` or `%w[CalculatorTool]` |
256
- > | `local_tools: [CalculatorTool.new]` (instance) | `%w[calculator]` |
257
- >
258
- > Note the constructor's `tools:` is validated and **rejects** classes and
259
- > instances outright (`ArgumentError`); only task-level and `run()` values accept
260
- > them.
261
-
262
- An explicit `:none`/`[]` is useful for a relevance filter that decided no tool is useful for the current message — it now genuinely sends zero tools for that turn (previously an empty allowlist was silently treated as "all tools," which could overflow small-context local models with the full tool set). Each turn's resolved tool set fully **replaces** the chat's tools rather than accumulating, so a later `:none` turn correctly clears whatever a prior turn attached.
263
-
264
- > [!CAUTION]
265
- > **Watch the default.** Both `Robot.new`'s and `run()`'s `tools:`/`mcp:`
266
- > parameters default to `:none`, not `:inherit`. If you build a robot with
267
- > `local_tools:` and then call `robot.run(message)` with no `tools:` override at
268
- > all, the runtime default takes the explicit-`:none` path above — sending no
269
- > tools for that turn. **Pass `tools: :inherit` on the `run()` call** (or on the
270
- > network `task`, which is forwarded to `run()`) anywhere you need the robot's
271
- > attached tools available.
272
-
273
- > [!WARNING]
274
- > For a **standalone** robot, do not pass `tools: :inherit` at *build* time as a
275
- > way to turn tools on. Build-time `:inherit` resolves against the level above
276
- > it, which for a standalone robot is the global `:none` — producing an
277
- > allowlist of `["none"]` that matches nothing, and the empty result then
278
- > carries into the runtime pass. Leave build-time `tools:` unset:
279
- >
280
- > ```ruby
281
- > # build tools: unset + run(tools: :inherit) -> [:t1] correct
282
- > # build tools: :inherit + run(tools: :inherit) -> [] the standalone trap
283
- > # build tools: :none + run(tools: :inherit) -> [:t1] also fine
284
- > ```
285
- >
286
- > This applies to a robot run on its own. Inside a **network** whose `config:`
287
- > supplies `tools:`/`mcp:`, the parent level is that network list rather than
288
- > `:none`, and build-time `:inherit` is *required* — it is the robot's opt-in to
289
- > the network value. See
290
- > [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
291
-
292
- ### Tool Capping and Per-Turn Filtering
293
-
294
- Most LLM providers reject a tool array longer than 128 entries and fail the whole turn. RobotLab clamps the fully-resolved tool list to a ceiling right before handing it to the chat provider — the definitive choke point regardless of how the tools were configured, filtered, or MCP-connected:
295
-
296
- ```ruby
297
- robot = RobotLab.build(
298
- name: "power_user",
299
- system_prompt: "...",
300
- local_tools: many_tools, # say, 150 tools
301
- config: RobotLab::RunConfig.new(max_tools: 50) # override the default ceiling
302
- )
303
- ```
304
-
305
- - Default ceiling: **128** tools per turn (`RobotLab::Robot::DEFAULT_MAX_TOOLS`)
306
- - Override with a **positive** `max_tools:` on `RunConfig` (or the `max_tools:` cascade field — see [Available Fields](../getting-started/configuration.md#available-fields))
307
- - When a turn's resolved tools exceed the cap, RobotLab logs a warning naming how many were dropped and sends the first `max_tools` entries
308
-
309
- > [!WARNING]
310
- > The cap **cannot be disabled.** `max_tools: nil`, `0`, or a negative number
311
- > all fall back to the 128 default — only a positive integer changes the
312
- > ceiling. If you attach more than 128 tools and set `max_tools: 0` expecting
313
- > "unlimited", you get the first 128.
314
- >
315
- > The truncation is not silent, though — `cap_tools` logs it at `WARN` through
316
- > `RobotLab.config.logger` every time it fires:
317
- >
318
- > ```
319
- > [power_user] tool list (150) exceeds max_tools (128); sending 128, dropping 22
320
- > ```
321
-
322
- ## Skill Scripts and Sandboxing
323
-
324
- A skill bundle (a directory with a `SKILL.md` plus `scripts/`, discovered via `AgentSkill`) can expose its scripts as tools (`ScriptTool`). Because those scripts run as real OS processes, each `SKILL.md` can declare the capabilities its scripts need directly in front matter, alongside `name`/`description`:
325
-
326
- ```markdown title="skills/deploy-checker/SKILL.md"
327
- ---
328
- name: deploy-checker
329
- description: Verifies a deployment's health before promoting it.
330
- fs_read: ["./data", "/etc/hosts"]
331
- fs_write: ["./out"]
332
- network: true
333
- timeout: 30
334
- trust: external # or "core" for trusted, always-unconfined skills
335
- ---
336
- ```
337
-
338
- `Capabilities` (the declaration above) lives in core, but core itself has **no sandboxing
339
- behavior and no built-in limitations** — a script always runs unconfined via a plain
340
- `Open3.capture2e`, with no timeout, unless the optional
341
- [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem is required.
342
- Requiring it installs `RobotLab::Sandbox::Executor` as `RobotLab::ScriptTool.executor`;
343
- confinement still stays **opt-in and off by default** from there — see the
344
- [`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section).
345
- When enabled:
346
-
347
- - The global `sandbox:` config is a **ceiling** (`fs_read`, `fs_write`, `network`, `timeout`); each skill's front matter is its **declared** request. The script actually runs under the **intersection** of the two — a path outside the ceiling's roots is dropped even if the skill declares it, `network` requires both sides to allow it, and `timeout` is the smaller of the two.
348
- - On macOS, confinement is enforced with a generated `sandbox-exec` (Seatbelt) profile: deny-by-default, with narrow allowances for the interpreter to boot, the granted read/write paths, and (optionally) the network. Notably, `$HOME` is never implicitly readable — SSH keys and cloud credentials stay out of reach unless a path under `$HOME` is explicitly granted.
349
- - Off macOS, or for any skill declaring `trust: core`, sandboxing is a passthrough — confinement is currently macOS-only and is always skipped for trusted "core" skills regardless of platform.
350
- - A script that runs past its `timeout` is killed (its whole process group) and reported back to the LLM as a timed-out error rather than hanging the turn.
351
-
352
- Full API reference: [robot_lab-sandbox](https://github.com/MadBomber/robot_lab-sandbox).
353
-
354
- ## Parameter Types
355
-
356
- Define parameters on `RubyLLM::Tool` subclasses using `param`:
357
-
358
- ### String
359
-
360
- ```ruby
361
- param :name, type: :string, desc: "User's full name"
362
- ```
363
-
364
- ### Integer
365
-
366
- ```ruby
367
- param :count, type: :integer, desc: "Number of results"
368
- ```
369
-
370
- ### Number (Float)
371
-
372
- ```ruby
373
- param :price, type: :number, desc: "Price in dollars"
374
- ```
375
-
376
- ### Boolean
377
-
378
- ```ruby
379
- param :active, type: :boolean, desc: "Whether the user is active"
380
- ```
381
-
382
- ### Array
383
-
384
- ```ruby
385
- param :tags, type: :array, desc: "List of tags"
386
- ```
387
-
388
- ### Enumerated Values
389
-
390
- `param` has **no `enum:` option**. Its full signature is
391
- `param(name, type: "string", desc: nil, description: nil, required: true)` —
392
- anything else raises `ArgumentError: unknown keyword: :enum`. Express the
393
- allowed values in the description instead, and validate in `execute`:
394
-
395
- ```ruby
396
- STATUSES = %w[pending active completed].freeze
397
-
398
- param :status, type: :string, desc: "Order status — one of: pending, active, completed"
399
-
400
- def execute(status:)
401
- return { error: "status must be one of #{STATUSES.join(', ')}" } unless STATUSES.include?(status)
402
- # ...
403
- end
404
- ```
405
-
406
- ### Required vs Optional
407
-
408
- Parameters are required by default. Mark optional with `required: false`:
409
-
410
- ```ruby
411
- param :query, type: :string, desc: "Search query" # required
412
- param :limit, type: :integer, desc: "Max results", required: false # optional
413
- ```
414
-
415
- ## Tool Patterns
416
-
417
- ### Database Lookup
418
-
419
- ```ruby
420
- class FindUser < RubyLLM::Tool
421
- description "Find user by email or ID"
422
-
423
- param :identifier, type: :string, desc: "Email address or user ID"
424
-
425
- def execute(identifier:)
426
- user = User.find_by(id: identifier) || User.find_by(email: identifier)
427
- user ? user.to_h : { error: "User not found" }
428
- end
429
- end
430
- ```
431
-
432
- ### API Integration
433
-
434
- ```ruby
435
- class GetStockPrice < RubyLLM::Tool
436
- description "Get current stock price for a ticker symbol"
437
-
438
- param :symbol, type: :string, desc: "Stock ticker symbol (e.g. AAPL)"
439
-
440
- def execute(symbol:)
441
- response = HTTP.get("https://api.stocks.example/quote/#{symbol}")
442
- JSON.parse(response.body)
443
- rescue HTTP::Error => e
444
- { error: "Failed to fetch stock price: #{e.message}" }
445
- end
446
- end
447
- ```
448
-
449
- ### File Operations
450
-
451
- ```ruby
452
- class ReadFile < RubyLLM::Tool
453
- description "Read contents of a file"
454
-
455
- param :path, type: :string, desc: "Absolute path to the file"
456
-
457
- def execute(path:)
458
- if File.exist?(path) && File.readable?(path)
459
- { content: File.read(path), size: File.size(path) }
460
- else
461
- { error: "File not found or not readable" }
462
- end
463
- end
464
- end
465
- ```
466
-
467
- ### Multi-Step Operations
468
-
469
- ```ruby
470
- class ProcessOrder < RubyLLM::Tool
471
- description "Validate and process a customer order"
472
-
473
- param :order_id, type: :string, desc: "The order ID to process"
474
-
475
- def execute(order_id:)
476
- order = Order.find(order_id)
477
-
478
- # Validate
479
- return { error: "Invalid order" } unless order.valid?
480
-
481
- # Process payment
482
- result = PaymentProcessor.charge(order)
483
- return { error: result[:error] } unless result[:success]
484
-
485
- # Update status
486
- order.update!(status: "paid")
487
-
488
- { success: true, order_id: order.id, amount: order.total }
489
- end
490
- end
491
- ```
492
-
493
- ## Tool Return Values
494
-
495
- ### Structured Data
496
-
497
- Return hashes with consistent structure:
498
-
499
- ```ruby
500
- def execute(user_id:)
501
- user = User.find(user_id)
502
- {
503
- id: user.id,
504
- name: user.name,
505
- email: user.email,
506
- created_at: user.created_at.iso8601
507
- }
508
- end
509
- ```
510
-
511
- ### Simple Values
512
-
513
- ```ruby
514
- def execute(**_)
515
- Time.now.to_s
516
- end
517
- ```
518
-
519
- ### Lists
520
-
521
- ```ruby
522
- def execute(query:)
523
- results = Search.query(query)
524
- results.map { |r| { id: r.id, title: r.title, score: r.score } }
525
- end
526
- ```
527
-
528
- ## Error Handling
529
-
530
- ### Automatic Error Handling
531
-
532
- `RobotLab::Tool` automatically catches `StandardError` exceptions from `execute` and returns a plain-text error string to the LLM. The LLM can then reason about the failure and try an alternative approach — without crashing the run.
533
-
534
- ```ruby
535
- class FetchResource < RobotLab::Tool
536
- description "Fetch a resource from an external API"
537
- param :id, type: :string, desc: "Resource ID"
538
-
539
- def execute(id:)
540
- ExternalAPI.fetch(id)
541
- end
542
- end
543
-
544
- tool = FetchResource.new
545
- result = tool.call({ "id" => "missing" })
546
- # If ExternalAPI.fetch raises, result is:
547
- # => "Error (fetch_resource): connection refused"
548
- ```
549
-
550
- This applies to all `RobotLab::Tool` variants — subclasses, `Tool.create` factory tools, and MCP tools.
551
-
552
- ### RobotLab::ToolError and Retryability
553
-
554
- `RobotLab::ToolError` is handled on a separate path from ordinary
555
- `StandardError`s. Raising one with `retryable: true` appends `" (retryable)"`
556
- to the text the LLM sees, hinting that another attempt may succeed:
557
-
558
- ```ruby
559
- class FetchResource < RobotLab::Tool
560
- description "Fetch a resource from an external API"
561
- param :id, type: :string, desc: "Resource ID"
562
-
563
- def execute(id:)
564
- raise RobotLab::ToolError.new("upstream 503", retryable: true)
565
- end
566
- end
567
-
568
- FetchResource.new.call({ "id" => "1" })
569
- # => "Error (fetch_resource): upstream 503 (retryable)"
570
-
571
- # Without retryable:, no suffix is appended:
572
- # raise RobotLab::ToolError, "bad input"
573
- # => "Error (fetch_resource): bad input"
574
- ```
575
-
576
- > [!WARNING]
577
- > Only the ordinary `StandardError` path writes to the log
578
- > (`RobotLab.config.logger.warn("Tool 'name' error: RuntimeError: …")`).
579
- > A `RobotLab::ToolError` is turned into text for the LLM and logged
580
- > **nowhere** — if you rely on `ToolError` for expected failures, add your own
581
- > logging inside `execute`, or those failures leave no trace outside the
582
- > transcript.
583
-
584
- ### Critical Tools (Opt-Out)
585
-
586
- For tools where you want exceptions to propagate (e.g., a tool whose failure should abort the run), set `raise_on_error` on the class:
587
-
588
- ```ruby
589
- class CriticalPayment < RobotLab::Tool
590
- self.raise_on_error = true
591
-
592
- description "Process a payment"
593
- param :amount, type: :number, desc: "Payment amount"
594
-
595
- def execute(amount:)
596
- PaymentGateway.charge(amount)
597
- end
598
- end
599
- ```
600
-
601
- `raise_on_error` is per-class and defaults to `false`. Setting it on one class does not affect others.
602
-
603
- > [!WARNING]
604
- > `raise_on_error` does **not** walk the inheritance chain. A subclass of a
605
- > class that set it silently reverts to `false`:
606
- >
607
- > ```ruby
608
- > class CriticalBase < RobotLab::Tool
609
- > self.raise_on_error = true
610
- > end
611
- >
612
- > class ChargeCard < CriticalBase; end
613
- >
614
- > CriticalBase.raise_on_error? # => true
615
- > ChargeCard.raise_on_error? # => false <- NOT inherited
616
- > ```
617
- >
618
- > Set `self.raise_on_error = true` on every class that needs it. (This differs
619
- > from `ractor_safe`, below, which *does* consult the superclass.)
620
-
621
- ### Manual Error Handling
622
-
623
- You can still handle specific errors inside `execute` for domain-specific responses:
624
-
625
- ```ruby
626
- class FetchResource < RobotLab::Tool
627
- description "Fetch a resource from an external API"
628
- param :id, type: :string, desc: "Resource ID"
629
-
630
- def execute(id:)
631
- result = ExternalAPI.fetch(id)
632
- { success: true, data: result }
633
- rescue ExternalAPI::NotFound
634
- { success: false, error: "Resource not found", id: id }
635
- rescue ExternalAPI::RateLimited => e
636
- { success: false, error: "Rate limited", retry_after: e.retry_after }
637
- end
638
- # Any other StandardError is still caught by the automatic handler
639
- end
640
- ```
641
-
642
- ## Ractor-Safe Tools
643
-
644
- A tool class can declare itself safe to run inside a Ractor. When it is, and the
645
- [`robot_lab-ractor`](https://github.com/MadBomber/robot_lab-ractor) extension
646
- gem is loaded, `call` dispatches the work to `RobotLab.ractor_pool` instead of
647
- running it inline on the calling thread — giving real CPU parallelism for
648
- compute-bound tools.
649
-
650
- ```ruby
651
- class Fibonacci < RobotLab::Tool
652
- ractor_safe true
653
-
654
- description "Compute the nth Fibonacci number"
655
- param :n, type: :integer, desc: "Which Fibonacci number to compute"
656
-
657
- def execute(n:)
658
- a, b = 0, 1
659
- n.times { a, b = b, a + b }
660
- a
661
- end
662
- end
663
-
664
- Fibonacci.ractor_safe? # => true
665
- ```
666
-
667
- Dispatch to the pool happens only when **all three** hold:
668
-
669
- 1. `ractor_safe?` is true
670
- 2. the class has a resolvable name — anonymous classes (including every tool
671
- built by `Tool.create`) fall back to the inline path
672
- 3. the `robot_lab-ractor` extension is loaded
673
-
674
- Otherwise `execute` runs inline, exactly as an ordinary tool would. Pool size
675
- comes from the `ractor_pool_size` `RunConfig` field (`:auto` when unset).
676
-
677
- > [!WARNING]
678
- > A Ractor-safe tool must be genuinely stateless: it is instantiated **fresh
679
- > inside the Ractor worker** for every call, so `robot`, captured closures, and
680
- > mutable class-level state are not available to it, and the result comes back
681
- > frozen. Do not set `ractor_safe true` on a tool that touches its robot.
682
-
683
- Unlike `raise_on_error`, `ractor_safe` **does** walk the inheritance chain — a
684
- subclass of a Ractor-safe tool is Ractor-safe unless it says otherwise.
685
-
686
- ## Tool Callbacks
687
-
688
- Robots support `on_tool_call` and `on_tool_result` callbacks for monitoring tool usage. Each receives exactly **one** argument — `on_tool_result` gets the result only, not the originating call:
689
-
690
- ```ruby
691
- robot = RobotLab.build(
692
- name: "assistant",
693
- system_prompt: "...",
694
- local_tools: [GetWeather],
695
- on_tool_call: ->(tool_call) { puts "Calling: #{tool_call.name}" },
696
- on_tool_result: ->(result) { puts "Result: #{result}" }
697
- )
698
-
699
- robot.run("What's the weather in Tokyo?", tools: :inherit)
700
- ```
701
-
702
- See [Streaming](streaming.md#tool-callbacks) for details, including the RubyLLM deprecation warning these emit.
703
-
704
- ## RobotLab::Tool.create with Schema
705
-
706
- For dynamic tools via `Tool.create`, pass parameters as a JSON Schema hash:
707
-
708
- ```ruby
709
- tool = RobotLab::Tool.create(
710
- name: "search",
711
- description: "Search for items",
712
- parameters: {
713
- type: "object",
714
- properties: {
715
- query: { type: "string", description: "Search query" },
716
- limit: { type: "integer", description: "Max results" }
717
- },
718
- required: ["query"]
719
- }
720
- ) { |args| Search.query(args[:query], limit: args[:limit] || 10) }
721
- ```
722
-
723
- ## Best Practices
724
-
725
- ### 1. Clear Descriptions
726
-
727
- Write descriptions that help the LLM understand when and how to use the tool:
728
-
729
- ```ruby
730
- # Good: Specific and actionable
731
- class SearchOrders < RubyLLM::Tool
732
- description "Search customer orders by date range, status, or customer email. Returns up to 50 matching orders sorted by date."
733
- # ...
734
- end
735
-
736
- # Bad: Vague
737
- class Search < RubyLLM::Tool
738
- description "Searches stuff"
739
- # ...
740
- end
741
- ```
742
-
743
- ### 2. Validate Inputs
744
-
745
- ```ruby
746
- def execute(email:)
747
- unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
748
- return { error: "Invalid email format" }
749
- end
750
- # ... rest of logic
751
- end
752
- ```
753
-
754
- ### 3. Return Structured Data
755
-
756
- ```ruby
757
- # Good: Structured and consistent
758
- def execute(**_)
759
- {
760
- success: true,
761
- data: { id: 1, name: "Item" },
762
- metadata: { fetched_at: Time.now.iso8601 }
763
- }
764
- end
765
-
766
- # Bad: Unstructured
767
- def execute(**_)
768
- "Found item with id 1 named Item"
769
- end
770
- ```
771
-
772
- ### 4. Keep Tools Focused
773
-
774
- Each tool should do one thing well. Prefer multiple focused tools over one tool that does everything.
775
-
776
- ## Next Steps
777
-
778
- - [MCP Integration](mcp-integration.md) - External tool servers
779
- - [Building Robots](building-robots.md) - Robot creation patterns
780
- - [API Reference: Tool](../api/core/tool.md) - Complete API