robot_lab 0.2.6 → 0.2.7

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -64
  3. data/docs/api/core/index.md +41 -15
  4. data/docs/api/core/memory.md +247 -29
  5. data/docs/api/core/network.md +255 -33
  6. data/docs/api/core/result.md +120 -32
  7. data/docs/api/core/robot.md +551 -61
  8. data/docs/api/core/state.md +87 -197
  9. data/docs/api/core/tool.md +165 -20
  10. data/docs/api/errors.md +110 -17
  11. data/docs/api/hooks.md +469 -0
  12. data/docs/api/index.md +80 -7
  13. data/docs/api/mcp/client.md +129 -35
  14. data/docs/api/mcp/index.md +164 -23
  15. data/docs/api/mcp/server.md +27 -3
  16. data/docs/api/mcp/transports.md +94 -22
  17. data/docs/api/messages/index.md +26 -3
  18. data/docs/api/messages/text-message.md +33 -11
  19. data/docs/api/messages/tool-call-message.md +27 -4
  20. data/docs/api/messages/tool-result-message.md +23 -4
  21. data/docs/api/messages/user-message.md +45 -8
  22. data/docs/api/skills.md +519 -0
  23. data/docs/api/streaming/context.md +28 -5
  24. data/docs/api/streaming/index.md +57 -11
  25. data/docs/api/support.md +846 -0
  26. data/docs/architecture/core-concepts.md +79 -31
  27. data/docs/architecture/index.md +86 -11
  28. data/docs/architecture/message-flow.md +66 -29
  29. data/docs/architecture/network-orchestration.md +145 -38
  30. data/docs/architecture/robot-execution.md +172 -90
  31. data/docs/architecture/state-management.md +31 -12
  32. data/docs/concepts.md +176 -21
  33. data/docs/examples/basic-chat.md +72 -19
  34. data/docs/examples/index.md +117 -31
  35. data/docs/examples/mcp-server.md +154 -45
  36. data/docs/examples/multi-robot-network.md +91 -21
  37. data/docs/examples/tool-usage.md +104 -37
  38. data/docs/getting-started/configuration.md +284 -97
  39. data/docs/getting-started/installation.md +53 -41
  40. data/docs/getting-started/quick-start.md +51 -6
  41. data/docs/guides/building-robots.md +258 -50
  42. data/docs/guides/creating-networks.md +214 -30
  43. data/docs/guides/hooks.md +141 -54
  44. data/docs/guides/knowledge.md +35 -4
  45. data/docs/guides/mcp-integration.md +211 -44
  46. data/docs/guides/memory.md +103 -12
  47. data/docs/guides/observability.md +95 -47
  48. data/docs/guides/streaming.md +184 -125
  49. data/docs/guides/using-tools.md +237 -17
  50. data/docs/index.md +36 -4
  51. data/examples/01_simple_robot.rb +2 -2
  52. data/examples/02_tools.rb +14 -4
  53. data/examples/03_network.rb +12 -7
  54. data/examples/04_mcp.rb +11 -4
  55. data/examples/05_streaming.rb +8 -5
  56. data/examples/06_prompt_templates.rb +13 -9
  57. data/examples/07_network_memory.rb +5 -5
  58. data/examples/08_llm_config.rb +20 -15
  59. data/examples/09_chaining.rb +4 -4
  60. data/examples/11_network_introspection.rb +4 -4
  61. data/examples/12_message_bus.rb +2 -2
  62. data/examples/13_spawn.rb +2 -2
  63. data/examples/14_rusty_circuit/README.md +1 -0
  64. data/examples/14_rusty_circuit/comic.rb +7 -3
  65. data/examples/14_rusty_circuit/display.rb +14 -0
  66. data/examples/14_rusty_circuit/heckler.rb +8 -6
  67. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  68. data/examples/14_rusty_circuit/scout.rb +17 -10
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  71. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  72. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  73. data/examples/16_writers_room/writer.rb +22 -22
  74. data/examples/16_writers_room/writers_room.rb +2 -0
  75. data/examples/17_skills.rb +14 -13
  76. data/examples/18_rails/README.md +20 -1
  77. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  78. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  79. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  80. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  81. data/examples/19_token_tracking.rb +25 -9
  82. data/examples/20_circuit_breaker.rb +10 -7
  83. data/examples/21_learning_loop.rb +42 -16
  84. data/examples/22_context_compression.rb +23 -23
  85. data/examples/23_convergence.rb +24 -17
  86. data/examples/24_structured_delegation.rb +13 -8
  87. data/examples/25_history_search.rb +12 -8
  88. data/examples/27_incident_response/incident_response.rb +31 -13
  89. data/examples/28_mcp_discovery.rb +17 -13
  90. data/examples/29_ractor_tools.rb +4 -2
  91. data/examples/30_ractor_network.rb +22 -17
  92. data/examples/31_launch_assessment.rb +20 -9
  93. data/examples/32_newsletter_reader.rb +7 -2
  94. data/examples/33_stock_predictor.rb +34 -13
  95. data/examples/34_agentskills.rb +7 -3
  96. data/examples/35_hooks.rb +18 -8
  97. data/examples/README.md +199 -45
  98. data/examples/common.rb +79 -11
  99. data/examples/xyzzy.rb +8 -1
  100. data/lib/robot_lab/config.rb +10 -5
  101. data/lib/robot_lab/names.rb +402 -0
  102. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  103. data/lib/robot_lab/robot/bus_messaging.rb +16 -8
  104. data/lib/robot_lab/robot/template_rendering.rb +16 -3
  105. data/lib/robot_lab/robot.rb +23 -2
  106. data/lib/robot_lab/version.rb +1 -1
  107. data/lib/robot_lab.rb +21 -15
  108. data/mkdocs.yml +6 -1
  109. metadata +7 -2
@@ -8,13 +8,18 @@ Configuration values are loaded in priority order (lowest to highest):
8
8
 
9
9
  1. **Bundled defaults** -- `lib/robot_lab/config/defaults.yml` (shipped with the gem)
10
10
  2. **Environment-specific overrides** -- `development`, `test`, or `production` sections in defaults.yml
11
- 3. **User config file** -- `~/.config/robot_lab/config.yml`
11
+ 3. **User config file** -- `~/.config/robot_lab/robot_lab.yml`
12
12
  4. **Project config file** -- `./config/robot_lab.yml`
13
13
  5. **Environment variables** -- `ROBOT_LAB_*` prefix
14
14
  6. **Runtime attributes** -- e.g., `RobotLab.config.logger = ...`
15
15
 
16
16
  Higher-priority sources override lower-priority ones. You only need to set the values you want to change.
17
17
 
18
+ > [!IMPORTANT]
19
+ > The user config file is `~/.config/robot_lab/**robot_lab**.yml` — the filename
20
+ > repeats the application name. `~/.config/robot_lab/config.yml` is **never
21
+ > read**, and RobotLab gives no warning when it is present but ignored.
22
+
18
23
  ## Accessing Configuration
19
24
 
20
25
  Use `RobotLab.config` to access the configuration object:
@@ -24,8 +29,7 @@ Use `RobotLab.config` to access the configuration object:
24
29
  RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
25
30
  RobotLab.config.ruby_llm.anthropic_api_key #=> "sk-ant-..."
26
31
  RobotLab.config.ruby_llm.request_timeout #=> 120
27
- RobotLab.config.max_iterations #=> 10
28
- RobotLab.config.streaming_enabled #=> true
32
+ RobotLab.config.template_path #=> nil (auto-detected)
29
33
 
30
34
  # Check the environment
31
35
  RobotLab.config.development? #=> true/false
@@ -46,8 +50,7 @@ Environment variables use the `ROBOT_LAB_` prefix. Use double underscores (`__`)
46
50
 
47
51
  ```bash
48
52
  # Top-level settings
49
- export ROBOT_LAB_MAX_ITERATIONS=20
50
- export ROBOT_LAB_STREAMING_ENABLED=false
53
+ export ROBOT_LAB_TEMPLATE_PATH=prompts
51
54
 
52
55
  # Nested ruby_llm settings (note the double underscore)
53
56
  export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
@@ -63,71 +66,132 @@ The double underscore convention maps to nested YAML structure:
63
66
  ```
64
67
  ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY --> ruby_llm.anthropic_api_key
65
68
  ROBOT_LAB_RUBY_LLM__MODEL --> ruby_llm.model
66
- ROBOT_LAB_MAX_ITERATIONS --> max_iterations
69
+ ROBOT_LAB_TEMPLATE_PATH --> template_path
67
70
  ```
68
71
 
72
+ > [!WARNING]
73
+ > **Nested values arrive as Strings.** Only top-level keys are type-coerced.
74
+ > With `ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180` set,
75
+ > `RobotLab.config.ruby_llm.request_timeout` returns the String `"180"`, not the
76
+ > Integer `180`. If a numeric nested setting matters to your code, set it in a
77
+ > config file instead, or coerce it yourself with `.to_i` / `.to_f`.
78
+
69
79
  ## Config Files
70
80
 
81
+ > [!WARNING]
82
+ > **A `defaults:` wrapper is always ignored in your own files.** The `defaults:`
83
+ > key you see inside the gem's bundled `lib/robot_lab/config/defaults.yml` applies
84
+ > **only to that bundled file**. Wrap your own settings in it and every value
85
+ > silently falls back to the default — no error, no warning.
86
+ >
87
+ > ```yaml
88
+ > # WRONG — silently ignored in a user or project config file
89
+ > defaults:
90
+ > template_path: prompts
91
+ >
92
+ > # RIGHT
93
+ > template_path: prompts
94
+ > ```
95
+ >
96
+ > Sections named for the **current environment** (`development:`, `test:`,
97
+ > `production:`) are a different matter, and the two files disagree:
98
+ >
99
+ > | File | Flat keys | `development:` section |
100
+ > |------|-----------|------------------------|
101
+ > | `~/.config/robot_lab/robot_lab.yml` | honoured | honoured |
102
+ > | `./config/robot_lab.yml` (no Rails) | honoured | ignored |
103
+ > | `./config/robot_lab.yml` (under Rails) | **ignored** | **honoured** |
104
+ >
105
+ > The user file checks for a section matching the current environment and falls
106
+ > back to the file root, so both forms work. The project file is read by
107
+ > anyway_config, which only treats it as environmental once
108
+ > `Anyway::Settings.current_environment` is set — which is precisely what Rails
109
+ > does (it sets it to `Rails.env`). See [Rails Integration](#rails-integration).
110
+
71
111
  ### Project Config
72
112
 
73
- Create `./config/robot_lab.yml` in your project root:
113
+ Create `./config/robot_lab.yml` in your project root. Outside Rails, write the
114
+ keys at the top level (under Rails they must be nested under the environment name
115
+ instead — see [Rails Integration](#rails-integration)):
74
116
 
75
117
  ```yaml title="config/robot_lab.yml"
76
- defaults:
77
- ruby_llm:
78
- anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
79
- model: claude-sonnet-4
80
- request_timeout: 120
81
-
82
- max_iterations: 15
83
- template_path: prompts
84
-
85
- development:
86
- ruby_llm:
87
- log_level: :debug
88
-
89
- test:
90
- max_iterations: 3
91
- streaming_enabled: false
92
- ruby_llm:
93
- model: claude-haiku-3-5
94
- request_timeout: 30
95
- max_retries: 1
96
-
97
- production:
98
- max_iterations: 20
99
- ruby_llm:
100
- request_timeout: 180
101
- max_retries: 5
102
- log_level: :warn
118
+ ruby_llm:
119
+ anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
120
+ model: claude-sonnet-4
121
+ request_timeout: 120
122
+ max_retries: 3
123
+ log_level: info
124
+
125
+ template_path: prompts
103
126
  ```
104
127
 
105
- !!! tip "ERB support"
106
- Config files support ERB templating, so you can reference environment variables with `<%= ENV['...'] %>`. This is useful for keeping secrets out of config files while still using YAML structure.
128
+ > [!NOTE]
129
+ > **ERB works here, and only here.** The project config file is read through ERB,
130
+ > so `<%= ENV['ANTHROPIC_API_KEY'] %>` is expanded before the YAML is parsed. The
131
+ > user config file described below is **not** — see the warning there.
132
+
133
+ > [!WARNING]
134
+ > **No YAML symbols in the project config file.** It is parsed with an empty
135
+ > permitted-classes list, so `log_level: :info` raises
136
+ > `Psych::DisallowedClass: Tried to load unspecified class: Symbol` and your
137
+ > application fails to boot. Write the plain string `log_level: info`. (Symbols
138
+ > *are* permitted in the bundled `defaults.yml` and in the user config file,
139
+ > which is why you will see `:debug` there.)
107
140
 
108
141
  ### User Config
109
142
 
110
- Create `~/.config/robot_lab/config.yml` for personal defaults that apply across all your projects:
143
+ Create `~/.config/robot_lab/robot_lab.yml` for personal defaults that apply
144
+ across all your projects. Keys go at the top level here too:
111
145
 
112
- ```yaml title="~/.config/robot_lab/config.yml"
113
- defaults:
114
- ruby_llm:
115
- anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
116
- model: claude-sonnet-4
146
+ ```yaml title="~/.config/robot_lab/robot_lab.yml"
147
+ ruby_llm:
148
+ model: claude-sonnet-4
149
+ request_timeout: 120
117
150
  ```
118
151
 
152
+ > [!WARNING]
153
+ > **Do not put ERB in the user config file.** It is parsed with
154
+ > `YAML.safe_load` and never passed through ERB, so
155
+ > `anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>` is stored as the literal
156
+ > nine-character-plus string `"<%= ENV['ANTHROPIC_API_KEY'] %>"` and sent to the
157
+ > provider as your API key. Put secrets in environment variables
158
+ > (`ANTHROPIC_API_KEY` or `ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY`), or in the
159
+ > project config file where ERB is evaluated.
160
+
161
+ > [!NOTE]
162
+ > The user config file honours **both** forms: flat keys at the root, or a
163
+ > top-level section named for the current environment (`development:`, `test:`,
164
+ > `production:`). The loader looks for the environment section first and falls back
165
+ > to the root. Only `defaults:` is ignored here.
166
+
119
167
  ## Configuration Reference
120
168
 
121
169
  ### Core Settings
122
170
 
123
171
  | Key | Default | Description |
124
172
  |-----|---------|-------------|
125
- | `max_iterations` | `10` | Maximum robots per network run |
126
- | `max_tool_iterations` | `10` | Maximum tool calls per robot run |
127
- | `streaming_enabled` | `true` | Enable streaming by default |
128
173
  | `template_path` | `null` (auto-detected) | Directory for prompt templates |
129
174
  | `mcp` | `:none` | Global MCP server configuration |
130
- | `tools` | `:none` | Global tool whitelist |
175
+ | `tools` | `:none` | Global tool allowlist |
176
+ | `sandbox.*` | see below | Skill-script confinement ceiling |
177
+
178
+ > [!WARNING]
179
+ > **Reserved / not implemented.** `defaults.yml` also ships `max_iterations`,
180
+ > `max_tool_iterations`, `streaming_enabled`, and an entire `chat:` tree
181
+ > (`chat.with_model`, `chat.with_temperature`, `chat.with_tools`,
182
+ > `chat.with_params.*`). These keys resolve — `RobotLab.config.max_iterations`
183
+ > returns `10` — but **nothing in the library reads them**. Setting them has zero
184
+ > effect. They are placeholders; do not build on them.
185
+ >
186
+ > The real equivalents are:
187
+ >
188
+ > | Dead key | Use instead |
189
+ > |----------|-------------|
190
+ > | `max_tool_iterations` | `max_tool_rounds:` on a robot or `RunConfig` |
191
+ > | `streaming_enabled` | `on_content:` callback, or a block passed to `run` |
192
+ > | `chat.with_temperature` | `temperature:` on a robot or `RunConfig` |
193
+ > | `chat.with_params.*` | `top_p:` / `max_tokens:` / etc. on a robot or `RunConfig` |
194
+ > | `max_iterations` | no equivalent — networks are bounded by their task graph |
131
195
 
132
196
  ### RubyLLM Settings (`ruby_llm:` section)
133
197
 
@@ -152,12 +216,19 @@ All settings under the `ruby_llm:` key are applied to `RubyLLM.configure` automa
152
216
 
153
217
  | Key | Default | Description |
154
218
  |-----|---------|-------------|
155
- | `ruby_llm.provider` | `:anthropic` | Default LLM provider |
156
- | `ruby_llm.model` | `claude-sonnet-4` | Default model for robots |
219
+ | `ruby_llm.model` | `claude-sonnet-4` | Default model for robots that do not set `model:` |
157
220
  | `ruby_llm.default_model` | `null` | RubyLLM default model override |
158
221
  | `ruby_llm.default_embedding_model` | `null` | Default embedding model |
159
222
  | `ruby_llm.default_image_model` | `null` | Default image model |
160
223
 
224
+ > [!NOTE]
225
+ > `defaults.yml` also carries `ruby_llm.provider: :anthropic` and
226
+ > `ruby_llm.assume_model_exists: false`, but neither is read. A robot's provider
227
+ > comes from its own `provider:` keyword argument, and `assume_model_exists` is
228
+ > derived from whether that argument was given. Model ids must be full RubyLLM
229
+ > ids — an unknown id raises `RubyLLM::ModelNotFoundError` when the robot is
230
+ > constructed.
231
+
161
232
  #### Connection Settings
162
233
 
163
234
  | Key | Default | Description |
@@ -185,19 +256,25 @@ All settings under the `ruby_llm:` key are applied to `RubyLLM.configure` automa
185
256
  | `ruby_llm.log_level` | `:info` | Log level (`:debug`, `:info`, `:warn`, `:error`) |
186
257
  | `ruby_llm.log_stream_debug` | `false` | Log streaming debug output |
187
258
 
188
- ### Chat Configuration (`chat:` section)
259
+ ### Chat Configuration (`chat:` section) — not implemented
189
260
 
190
- Default chat parameters applied to all robots unless overridden:
261
+ The `chat:` tree in `defaults.yml` (`chat.with_temperature`,
262
+ `chat.with_params.top_p`, `chat.with_params.max_tokens`, `chat.with_tools`, …)
263
+ is **reserved and has no consumers**. Values set there are parsed and then
264
+ ignored.
191
265
 
192
- | Key | Default | Description |
193
- |-----|---------|-------------|
194
- | `chat.with_temperature` | `0.7` | Controls randomness (0.0-2.0) |
195
- | `chat.with_params.top_p` | `null` | Nucleus sampling threshold |
196
- | `chat.with_params.top_k` | `null` | Top-k sampling |
197
- | `chat.with_params.max_tokens` | `null` | Maximum tokens in response |
198
- | `chat.with_params.presence_penalty` | `null` | Presence penalty (-2.0 to 2.0) |
199
- | `chat.with_params.frequency_penalty` | `null` | Frequency penalty (-2.0 to 2.0) |
200
- | `chat.with_params.stop` | `null` | Stop sequences |
266
+ To set LLM parameters globally, pass a `RunConfig` to each robot, or set them per
267
+ robot with constructor keyword arguments:
268
+
269
+ ```ruby
270
+ robot = RobotLab.build(
271
+ name: "bot",
272
+ system_prompt: "You are helpful.",
273
+ temperature: 0.7,
274
+ max_tokens: 2000,
275
+ top_p: 0.9
276
+ )
277
+ ```
201
278
 
202
279
  ### Skill-Script Sandboxing (`sandbox:` section)
203
280
 
@@ -242,7 +319,8 @@ This clears the cached config and reloads from all sources on next access.
242
319
 
243
320
  ## Environment-Specific Configuration
244
321
 
245
- The `defaults.yml` shipped with RobotLab includes environment-specific overrides:
322
+ The `defaults.yml` shipped with RobotLab includes environment-specific overrides.
323
+ This is what the gem actually ships:
246
324
 
247
325
  === "Development"
248
326
 
@@ -256,10 +334,10 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
256
334
 
257
335
  ```yaml
258
336
  test:
259
- max_iterations: 3
260
- streaming_enabled: false
337
+ max_iterations: 3 # reserved, no effect
338
+ streaming_enabled: false # reserved, no effect
261
339
  ruby_llm:
262
- model: claude-haiku-3-5
340
+ model: claude-3-haiku-20240307
263
341
  request_timeout: 30
264
342
  max_retries: 1
265
343
  log_level: :warn
@@ -269,8 +347,8 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
269
347
 
270
348
  ```yaml
271
349
  production:
272
- streaming_enabled: false
273
- max_iterations: 20
350
+ streaming_enabled: false # reserved, no effect
351
+ max_iterations: 20 # reserved, no effect
274
352
  ruby_llm:
275
353
  request_timeout: 180
276
354
  max_retries: 5
@@ -279,26 +357,56 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
279
357
 
280
358
  The current environment is determined automatically (via `RAILS_ENV`, `RACK_ENV`, or defaults to `development`).
281
359
 
282
- ## Rails Integration
283
-
284
- In Rails, RobotLab is configured automatically via its Railtie. The logger defaults to `Rails.logger`, and templates default to `app/prompts/`.
360
+ > [!NOTE]
361
+ > Under `test`, the effective default model is `claude-3-haiku-20240307` — a full
362
+ > dated model id. Short aliases like `claude-haiku-3-5` are **not** valid RubyLLM
363
+ > model ids and raise `RubyLLM::ModelNotFoundError` at robot construction.
285
364
 
286
- Create a project config file for Rails-specific settings:
365
+ ## Rails Integration
287
366
 
288
- ```yaml title="config/robot_lab.yml"
289
- defaults:
367
+ > [!NOTE]
368
+ > Core RobotLab ships **no Railtie and no Engine**. It performs two bare
369
+ > `defined?(::Rails)` checks: the default logger becomes `Rails.logger`, and
370
+ > `template_path` resolves to `Rails.root/app/prompts` when left unset. Generators,
371
+ > `RobotLab::Job`, and Turbo broadcasting live in the separate
372
+ > [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) gem.
373
+
374
+ > [!WARNING]
375
+ > **Under Rails, `./config/robot_lab.yml` must be environment-sectioned — a flat
376
+ > file is ignored.** Rails' anyway_config integration sets
377
+ > `Anyway::Settings.current_environment` to `Rails.env`, which switches the project
378
+ > config loader into environmental mode. Keys then have to live under
379
+ > `development:` / `test:` / `production:`; anything written at the root of the file
380
+ > is dropped. Outside Rails the rule is exactly inverted — flat keys are read and an
381
+ > environment section is ignored.
382
+
383
+ ```yaml title="config/robot_lab.yml (under Rails)"
384
+ development:
290
385
  ruby_llm:
291
386
  anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
292
387
  model: claude-sonnet-4
293
-
388
+ request_timeout: 180
389
+ max_retries: 5
294
390
  template_path: null # auto-detects app/prompts in Rails
295
391
 
296
392
  production:
297
393
  ruby_llm:
394
+ anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
395
+ model: claude-sonnet-4
298
396
  request_timeout: 180
299
397
  max_retries: 5
300
398
  ```
301
399
 
400
+ The same file outside Rails would instead be written flat:
401
+
402
+ ```yaml title="config/robot_lab.yml (no Rails)"
403
+ ruby_llm:
404
+ model: claude-sonnet-4
405
+ request_timeout: 180
406
+
407
+ template_path: prompts
408
+ ```
409
+
302
410
  You can also use Rails credentials:
303
411
 
304
412
  ```bash
@@ -311,13 +419,10 @@ anthropic_api_key: sk-ant-...
311
419
  openai_api_key: sk-...
312
420
  ```
313
421
 
314
- Then reference them in your config file with ERB:
315
-
316
- ```yaml title="config/robot_lab.yml"
317
- defaults:
318
- ruby_llm:
319
- anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
320
- ```
422
+ `./config/robot_lab.yml` is evaluated through ERB, so credentials can be
423
+ referenced inline as shown above. Note that this only works in the **project**
424
+ config file — the `~/.config/robot_lab/robot_lab.yml` user file is not run
425
+ through ERB.
321
426
 
322
427
  ## RunConfig: Shared Operational Defaults
323
428
 
@@ -362,13 +467,32 @@ robot = RobotLab.build(
362
467
  temperature: 0.9 # overrides shared config's 0.5
363
468
  )
364
469
 
365
- # Network applies config to all member robots
470
+ # Network-level config
366
471
  network = RobotLab.create_network(name: "pipeline", config: shared) do
367
472
  task :analyzer, analyzer_robot, depends_on: :none
368
473
  task :writer, writer_robot, depends_on: [:analyzer]
369
474
  end
370
475
  ```
371
476
 
477
+ > [!WARNING]
478
+ > **A network-level `config:` propagates only `mcp` and `tools`** — and only when
479
+ > the member robot opts in by passing `mcp: :inherit` / `tools: :inherit` on its
480
+ > `run`. LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks
481
+ > (`on_content`, `on_tool_call`, `on_tool_result`) are read from each robot's
482
+ > **own** config at construction time and are never inherited from the network.
483
+ > The only field the network itself consumes is `max_concurrent_robots`.
484
+ >
485
+ > If you want a whole team on one model, pass the same `config:` to each robot:
486
+ >
487
+ > ```ruby
488
+ > shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
489
+ > analyst = RobotLab.build(name: "analyst", system_prompt: "...", config: shared)
490
+ > writer = RobotLab.build(name: "writer", system_prompt: "...", config: shared)
491
+ > ```
492
+ >
493
+ > A per-task `config:` is merged into the network config and is subject to the
494
+ > same `mcp`/`tools`-only limitation.
495
+
372
496
  ### Merging Configs
373
497
 
374
498
  RunConfig supports merge semantics where the more-specific config's values win:
@@ -387,10 +511,10 @@ effective.temperature #=> 0.9 (overridden)
387
511
  |----------|--------|
388
512
  | **LLM** | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
389
513
  | **Tools** | `mcp`, `tools` |
390
- | **Callbacks** | `on_tool_call`, `on_tool_result` |
514
+ | **Callbacks** | `on_tool_call`, `on_tool_result`, `on_content` |
391
515
  | **Infrastructure** | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `cost_budget`, `max_tools`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold` |
392
516
 
393
- `cost_budget` mirrors `token_budget` for cumulative dollar spend — see [Budgets](../guides/observability.md#budgets-token--cost). `max_tools` overrides the default 128-tool ceiling enforced on every turn before tools are handed to the provider — see [Tool Capping](../guides/using-tools.md#tool-capping-and-per-turn-filtering).
517
+ `cost_budget` mirrors `token_budget` for cumulative dollar spend — see [Budgets](../guides/observability.md#budgets-token-cost). `max_tools` overrides the default 128-tool ceiling enforced on every turn before tools are handed to the provider — see [Tool Capping](../guides/using-tools.md#tool-capping-and-per-turn-filtering).
394
518
 
395
519
  ### RunConfig vs RobotLab.config
396
520
 
@@ -410,43 +534,106 @@ Individual robots can override the global model and other settings:
410
534
  robot = RobotLab.build(
411
535
  name: "fast_bot",
412
536
  system_prompt: "You are a quick responder.",
413
- model: "claude-haiku-3-5",
537
+ model: "claude-3-haiku-20240307",
414
538
  temperature: 0.3,
415
539
  max_tokens: 500
416
540
  )
417
541
 
418
542
  # Or use chaining at runtime
419
- robot.with_temperature(0.9).with_max_tokens(2000).run("Tell me a story.")
543
+ robot.with_temperature(0.9).with_params(max_tokens: 2000).run("Tell me a story.")
420
544
  ```
421
545
 
546
+ > [!WARNING]
547
+ > There is **no `with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
548
+ > `with_presence_penalty`, or `with_frequency_penalty`** — calling them raises
549
+ > `NoMethodError`. The complete chainable set is `with_context`, `with_headers`,
550
+ > `with_instructions`, `with_model`, `with_params`, `with_schema`,
551
+ > `with_temperature`, `with_thinking`, `with_tool`, `with_tools`, plus RobotLab's
552
+ > own `with_template` and `with_bus`. For everything else use a constructor
553
+ > keyword argument or `with_params(...)`.
554
+
422
555
  ## Hierarchical MCP and Tools
423
556
 
424
557
  MCP servers and tools use a hierarchical configuration: `runtime > robot > network > global`. Each level can specify:
425
558
 
426
559
  - `:inherit` -- Use the parent level's configuration
427
560
  - `:none` -- No MCP servers or tools at this level
428
- - An explicit array -- Specific servers or tools
561
+ - An explicit array -- A name **allowlist** (not a local-vs-MCP switch). Entries are
562
+ compared as strings against each attached tool's `name`, so they must match the
563
+ form the tool was attached in: a tool attached as a class matches `[RefundTool]`
564
+ or `%w[RefundTool]`, while one attached as an instance matches `%w[refund]`
565
+
566
+ > [!WARNING]
567
+ > **`tools:` and `mcp:` both default to `:none` — including on `run()` itself.**
568
+ > `Robot#run` is declared `run(message = nil, ..., mcp: :none, tools: :none, ...)`,
569
+ > and an explicit `:none` means "send zero tools this turn". So a plain
570
+ > `robot.run("...")` sends the LLM **no tools and connects no MCP servers**, even
571
+ > when you passed `local_tools:` or `mcp:` at build time.
572
+ >
573
+ > The fix goes on the **run**, not the build:
574
+ >
575
+ > ```ruby
576
+ > robot.run("...", tools: :inherit) # send the attached local tools
577
+ > robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP and send its tools
578
+ > ```
579
+ >
580
+ > `mcp: :inherit` triggers the connection; `tools: :inherit` is additionally
581
+ > required for the MCP tools to actually be handed to the model.
582
+
583
+ > [!IMPORTANT]
584
+ > **For a standalone robot, do not pass `tools: :inherit` at build time.** Build-time
585
+ > `:inherit` resolves against the parent level, and for a standalone robot the parent
586
+ > is the global `:none` — producing an allowlist of `["none"]` that matches nothing
587
+ > and suppresses the tools even when the run asks for `:inherit`. Verified
588
+ > resolution for a standalone robot:
589
+ >
590
+ > | build `tools:` | run `tools:` | tools sent |
591
+ > |---|---|---|
592
+ > | unset | `:none` (default) | none |
593
+ > | unset | `:inherit` | the attached tools ✅ |
594
+ > | `:inherit` | `:inherit` | **none** ❌ |
595
+ > | `:inherit` | `:none` | none |
596
+ > | `:none` | `:inherit` | the attached tools ✅ |
597
+ >
598
+ > Leave `tools:` unset at build time — unless the robot is a member of a network.
599
+
600
+ > [!NOTE]
601
+ > **Inside a network, build-time `:inherit` is the opt-in, not a bug.** The parent
602
+ > is resolved at run time as the network's `config:`, so `:inherit` is how a robot
603
+ > asks for the network-level allowlist. With
604
+ > `RobotLab::RunConfig.new(tools: %w[RefundTool])` on the network and a robot
605
+ > holding `local_tools: [RefundTool, InvoiceTool]`:
606
+ >
607
+ > | build `tools:` | task `tools:` | tools sent |
608
+ > |---|---|---|
609
+ > | unset | omitted (`:none`) | none |
610
+ > | unset | `:inherit` | `refund`, `invoice` — the network allowlist is **not** applied |
611
+ > | `:inherit` | `:inherit` | `refund` — the network allowlist **is** applied ✅ |
612
+ > | `:inherit` | omitted (`:none`) | none |
613
+ >
614
+ > The same reasoning applies to `mcp:`.
429
615
 
430
616
  ```ruby
431
- # Robot inheriting network MCP config
432
- robot = RobotLab.build(
433
- name: "agent",
434
- system_prompt: "You are helpful.",
435
- mcp: :inherit,
436
- tools: :inherit
437
- )
438
-
439
- # Robot with no MCP, specific tools
617
+ # Correct: attach tools at build time, request them at run time
440
618
  robot = RobotLab.build(
441
619
  name: "calculator",
442
620
  system_prompt: "You solve math problems.",
443
- mcp: :none,
444
- tools: :inherit, # required: without this, local_tools below is never sent
445
621
  local_tools: [Calculator]
446
622
  )
623
+
624
+ robot.run("What is 17 * 23?", tools: :inherit)
625
+
626
+ # Robot with MCP servers attached at build time
627
+ robot = RobotLab.build(
628
+ name: "agent",
629
+ system_prompt: "You are helpful.",
630
+ mcp: [{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } }]
631
+ )
632
+
633
+ robot.run("List the files in ./lib", mcp: :inherit, tools: :inherit)
447
634
  ```
448
635
 
449
- > **`tools:` defaults to `:none` at every level**, including `run()` itself. Attaching tools via `local_tools:` does not by itself make them available — an explicit `:none` (or an unset runtime override, which is equivalent) now sends **zero** tools for that call. Pass `tools: :inherit` wherever you want a robot's attached tools to actually be used. See [Runtime Tool Filtering](../guides/using-tools.md#runtime-tool-filtering) for the full `:inherit`/`:none`/array semantics.
636
+ See [Runtime Tool Filtering](../guides/using-tools.md#runtime-tool-filtering) for the full `:inherit`/`:none`/array semantics.
450
637
 
451
638
  ## Next Steps
452
639