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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
data/docs/guides/using-tools.md
DELETED
|
@@ -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
|