robot_lab 0.2.6 → 0.2.8

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +17 -1
  6. data/README.md +124 -64
  7. data/Rakefile +6 -111
  8. data/_typos.toml +21 -0
  9. data/docs/api/core/index.md +41 -15
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +255 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +551 -61
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +110 -17
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +80 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +423 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +79 -31
  33. data/docs/architecture/index.md +87 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +145 -38
  36. data/docs/architecture/robot-execution.md +172 -90
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +288 -98
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +258 -50
  48. data/docs/guides/creating-networks.md +214 -30
  49. data/docs/guides/hooks.md +263 -54
  50. data/docs/guides/knowledge.md +35 -4
  51. data/docs/guides/mcp-integration.md +211 -44
  52. data/docs/guides/memory.md +103 -12
  53. data/docs/guides/observability.md +95 -47
  54. data/docs/guides/streaming.md +184 -125
  55. data/docs/guides/using-tools.md +247 -18
  56. data/docs/index.md +36 -4
  57. data/examples/01_simple_robot.rb +2 -2
  58. data/examples/02_tools.rb +14 -4
  59. data/examples/03_network.rb +12 -7
  60. data/examples/04_mcp.rb +11 -4
  61. data/examples/05_streaming.rb +8 -5
  62. data/examples/06_prompt_templates.rb +13 -9
  63. data/examples/07_network_memory.rb +5 -5
  64. data/examples/08_llm_config.rb +20 -15
  65. data/examples/09_chaining.rb +4 -4
  66. data/examples/11_network_introspection.rb +4 -4
  67. data/examples/12_message_bus.rb +2 -2
  68. data/examples/13_spawn.rb +2 -2
  69. data/examples/14_rusty_circuit/README.md +1 -0
  70. data/examples/14_rusty_circuit/comic.rb +7 -3
  71. data/examples/14_rusty_circuit/display.rb +14 -0
  72. data/examples/14_rusty_circuit/heckler.rb +8 -6
  73. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  74. data/examples/14_rusty_circuit/scout.rb +17 -10
  75. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  76. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  77. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  78. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  79. data/examples/16_writers_room/writer.rb +22 -22
  80. data/examples/16_writers_room/writers_room.rb +2 -0
  81. data/examples/17_skills.rb +14 -13
  82. data/examples/18_rails/README.md +20 -1
  83. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  84. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  85. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  86. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  87. data/examples/19_token_tracking.rb +25 -9
  88. data/examples/20_circuit_breaker.rb +10 -7
  89. data/examples/21_learning_loop.rb +42 -16
  90. data/examples/22_context_compression.rb +23 -23
  91. data/examples/23_convergence.rb +24 -17
  92. data/examples/24_structured_delegation.rb +13 -8
  93. data/examples/25_history_search.rb +12 -8
  94. data/examples/27_incident_response/incident_response.rb +31 -13
  95. data/examples/28_mcp_discovery.rb +17 -13
  96. data/examples/29_ractor_tools.rb +4 -2
  97. data/examples/30_ractor_network.rb +22 -17
  98. data/examples/31_launch_assessment.rb +20 -9
  99. data/examples/32_newsletter_reader.rb +7 -2
  100. data/examples/33_stock_predictor.rb +34 -13
  101. data/examples/34_agentskills.rb +7 -3
  102. data/examples/35_hooks.rb +18 -8
  103. data/examples/README.md +199 -45
  104. data/examples/common.rb +79 -11
  105. data/examples/xyzzy.rb +8 -1
  106. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  107. data/lib/robot_lab/ask_user.rb +2 -0
  108. data/lib/robot_lab/bus_poller.rb +2 -0
  109. data/lib/robot_lab/capabilities.rb +4 -0
  110. data/lib/robot_lab/config.rb +18 -5
  111. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  112. data/lib/robot_lab/history_compressor.rb +5 -0
  113. data/lib/robot_lab/hook.rb +1 -0
  114. data/lib/robot_lab/hook_context.rb +4 -0
  115. data/lib/robot_lab/hook_registry.rb +1 -0
  116. data/lib/robot_lab/hooks.rb +6 -3
  117. data/lib/robot_lab/mcp/client.rb +2 -2
  118. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  119. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  120. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  121. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  122. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  123. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  124. data/lib/robot_lab/memory.rb +23 -6
  125. data/lib/robot_lab/memory_change.rb +1 -0
  126. data/lib/robot_lab/message.rb +3 -0
  127. data/lib/robot_lab/names.rb +400 -0
  128. data/lib/robot_lab/network.rb +13 -6
  129. data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
  130. data/lib/robot_lab/robot/bus_messaging.rb +19 -8
  131. data/lib/robot_lab/robot/history_search.rb +2 -0
  132. data/lib/robot_lab/robot/hooking.rb +3 -0
  133. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  134. data/lib/robot_lab/robot/template_rendering.rb +29 -7
  135. data/lib/robot_lab/robot.rb +81 -19
  136. data/lib/robot_lab/robot_result.rb +3 -0
  137. data/lib/robot_lab/run_config.rb +5 -0
  138. data/lib/robot_lab/script_tool.rb +20 -39
  139. data/lib/robot_lab/state_proxy.rb +1 -0
  140. data/lib/robot_lab/streaming/context.rb +1 -0
  141. data/lib/robot_lab/streaming/events.rb +1 -0
  142. data/lib/robot_lab/task.rb +2 -0
  143. data/lib/robot_lab/tool.rb +4 -0
  144. data/lib/robot_lab/user_message.rb +1 -0
  145. data/lib/robot_lab/utils.rb +2 -0
  146. data/lib/robot_lab/version.rb +1 -1
  147. data/lib/robot_lab/waiter.rb +3 -0
  148. data/lib/robot_lab.rb +27 -18
  149. data/mkdocs.yml +6 -1
  150. metadata +12 -7
  151. data/lib/robot_lab/sandbox/null.rb +0 -13
  152. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  153. data/lib/robot_lab/sandbox.rb +0 -52
@@ -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,23 +256,32 @@ 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
 
204
- Opt-in confinement for the scripts a [skill bundle](../guides/using-tools.md#skill-scripts-and-sandboxing) exposes as tools. Disabled by default scripts run exactly as before until you turn it on:
281
+ Core declares this config schema but has no sandboxing behavior of its ownenforcement
282
+ lives in the optional [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox)
283
+ gem. Without it, skill scripts always run unconfined regardless of these values; requiring
284
+ it installs a `RobotLab::ScriptTool.executor` that honors them:
205
285
 
206
286
  | Key | Default | Description |
207
287
  |-----|---------|-------------|
@@ -242,7 +322,8 @@ This clears the cached config and reloads from all sources on next access.
242
322
 
243
323
  ## Environment-Specific Configuration
244
324
 
245
- The `defaults.yml` shipped with RobotLab includes environment-specific overrides:
325
+ The `defaults.yml` shipped with RobotLab includes environment-specific overrides.
326
+ This is what the gem actually ships:
246
327
 
247
328
  === "Development"
248
329
 
@@ -256,10 +337,10 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
256
337
 
257
338
  ```yaml
258
339
  test:
259
- max_iterations: 3
260
- streaming_enabled: false
340
+ max_iterations: 3 # reserved, no effect
341
+ streaming_enabled: false # reserved, no effect
261
342
  ruby_llm:
262
- model: claude-haiku-3-5
343
+ model: claude-3-haiku-20240307
263
344
  request_timeout: 30
264
345
  max_retries: 1
265
346
  log_level: :warn
@@ -269,8 +350,8 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
269
350
 
270
351
  ```yaml
271
352
  production:
272
- streaming_enabled: false
273
- max_iterations: 20
353
+ streaming_enabled: false # reserved, no effect
354
+ max_iterations: 20 # reserved, no effect
274
355
  ruby_llm:
275
356
  request_timeout: 180
276
357
  max_retries: 5
@@ -279,26 +360,56 @@ The `defaults.yml` shipped with RobotLab includes environment-specific overrides
279
360
 
280
361
  The current environment is determined automatically (via `RAILS_ENV`, `RACK_ENV`, or defaults to `development`).
281
362
 
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/`.
363
+ > [!NOTE]
364
+ > Under `test`, the effective default model is `claude-3-haiku-20240307` — a full
365
+ > dated model id. Short aliases like `claude-haiku-3-5` are **not** valid RubyLLM
366
+ > model ids and raise `RubyLLM::ModelNotFoundError` at robot construction.
285
367
 
286
- Create a project config file for Rails-specific settings:
368
+ ## Rails Integration
287
369
 
288
- ```yaml title="config/robot_lab.yml"
289
- defaults:
370
+ > [!NOTE]
371
+ > Core RobotLab ships **no Railtie and no Engine**. It performs two bare
372
+ > `defined?(::Rails)` checks: the default logger becomes `Rails.logger`, and
373
+ > `template_path` resolves to `Rails.root/app/prompts` when left unset. Generators,
374
+ > `RobotLab::Job`, and Turbo broadcasting live in the separate
375
+ > [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) gem.
376
+
377
+ > [!WARNING]
378
+ > **Under Rails, `./config/robot_lab.yml` must be environment-sectioned — a flat
379
+ > file is ignored.** Rails' anyway_config integration sets
380
+ > `Anyway::Settings.current_environment` to `Rails.env`, which switches the project
381
+ > config loader into environmental mode. Keys then have to live under
382
+ > `development:` / `test:` / `production:`; anything written at the root of the file
383
+ > is dropped. Outside Rails the rule is exactly inverted — flat keys are read and an
384
+ > environment section is ignored.
385
+
386
+ ```yaml title="config/robot_lab.yml (under Rails)"
387
+ development:
290
388
  ruby_llm:
291
389
  anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
292
390
  model: claude-sonnet-4
293
-
391
+ request_timeout: 180
392
+ max_retries: 5
294
393
  template_path: null # auto-detects app/prompts in Rails
295
394
 
296
395
  production:
297
396
  ruby_llm:
397
+ anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
398
+ model: claude-sonnet-4
298
399
  request_timeout: 180
299
400
  max_retries: 5
300
401
  ```
301
402
 
403
+ The same file outside Rails would instead be written flat:
404
+
405
+ ```yaml title="config/robot_lab.yml (no Rails)"
406
+ ruby_llm:
407
+ model: claude-sonnet-4
408
+ request_timeout: 180
409
+
410
+ template_path: prompts
411
+ ```
412
+
302
413
  You can also use Rails credentials:
303
414
 
304
415
  ```bash
@@ -311,13 +422,10 @@ anthropic_api_key: sk-ant-...
311
422
  openai_api_key: sk-...
312
423
  ```
313
424
 
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
- ```
425
+ `./config/robot_lab.yml` is evaluated through ERB, so credentials can be
426
+ referenced inline as shown above. Note that this only works in the **project**
427
+ config file — the `~/.config/robot_lab/robot_lab.yml` user file is not run
428
+ through ERB.
321
429
 
322
430
  ## RunConfig: Shared Operational Defaults
323
431
 
@@ -362,13 +470,32 @@ robot = RobotLab.build(
362
470
  temperature: 0.9 # overrides shared config's 0.5
363
471
  )
364
472
 
365
- # Network applies config to all member robots
473
+ # Network-level config
366
474
  network = RobotLab.create_network(name: "pipeline", config: shared) do
367
475
  task :analyzer, analyzer_robot, depends_on: :none
368
476
  task :writer, writer_robot, depends_on: [:analyzer]
369
477
  end
370
478
  ```
371
479
 
480
+ > [!WARNING]
481
+ > **A network-level `config:` propagates only `mcp` and `tools`** — and only when
482
+ > the member robot opts in by passing `mcp: :inherit` / `tools: :inherit` on its
483
+ > `run`. LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks
484
+ > (`on_content`, `on_tool_call`, `on_tool_result`) are read from each robot's
485
+ > **own** config at construction time and are never inherited from the network.
486
+ > The only field the network itself consumes is `max_concurrent_robots`.
487
+ >
488
+ > If you want a whole team on one model, pass the same `config:` to each robot:
489
+ >
490
+ > ```ruby
491
+ > shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
492
+ > analyst = RobotLab.build(name: "analyst", system_prompt: "...", config: shared)
493
+ > writer = RobotLab.build(name: "writer", system_prompt: "...", config: shared)
494
+ > ```
495
+ >
496
+ > A per-task `config:` is merged into the network config and is subject to the
497
+ > same `mcp`/`tools`-only limitation.
498
+
372
499
  ### Merging Configs
373
500
 
374
501
  RunConfig supports merge semantics where the more-specific config's values win:
@@ -387,10 +514,10 @@ effective.temperature #=> 0.9 (overridden)
387
514
  |----------|--------|
388
515
  | **LLM** | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
389
516
  | **Tools** | `mcp`, `tools` |
390
- | **Callbacks** | `on_tool_call`, `on_tool_result` |
517
+ | **Callbacks** | `on_tool_call`, `on_tool_result`, `on_content` |
391
518
  | **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
519
 
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).
520
+ `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
521
 
395
522
  ### RunConfig vs RobotLab.config
396
523
 
@@ -410,43 +537,106 @@ Individual robots can override the global model and other settings:
410
537
  robot = RobotLab.build(
411
538
  name: "fast_bot",
412
539
  system_prompt: "You are a quick responder.",
413
- model: "claude-haiku-3-5",
540
+ model: "claude-3-haiku-20240307",
414
541
  temperature: 0.3,
415
542
  max_tokens: 500
416
543
  )
417
544
 
418
545
  # Or use chaining at runtime
419
- robot.with_temperature(0.9).with_max_tokens(2000).run("Tell me a story.")
546
+ robot.with_temperature(0.9).with_params(max_tokens: 2000).run("Tell me a story.")
420
547
  ```
421
548
 
549
+ > [!WARNING]
550
+ > There is **no `with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
551
+ > `with_presence_penalty`, or `with_frequency_penalty`** — calling them raises
552
+ > `NoMethodError`. The complete chainable set is `with_context`, `with_headers`,
553
+ > `with_instructions`, `with_model`, `with_params`, `with_schema`,
554
+ > `with_temperature`, `with_thinking`, `with_tool`, `with_tools`, plus RobotLab's
555
+ > own `with_template` and `with_bus`. For everything else use a constructor
556
+ > keyword argument or `with_params(...)`.
557
+
422
558
  ## Hierarchical MCP and Tools
423
559
 
424
560
  MCP servers and tools use a hierarchical configuration: `runtime > robot > network > global`. Each level can specify:
425
561
 
426
562
  - `:inherit` -- Use the parent level's configuration
427
563
  - `:none` -- No MCP servers or tools at this level
428
- - An explicit array -- Specific servers or tools
564
+ - An explicit array -- A name **allowlist** (not a local-vs-MCP switch). Entries are
565
+ compared as strings against each attached tool's `name`, so they must match the
566
+ form the tool was attached in: a tool attached as a class matches `[RefundTool]`
567
+ or `%w[RefundTool]`, while one attached as an instance matches `%w[refund]`
568
+
569
+ > [!WARNING]
570
+ > **`tools:` and `mcp:` both default to `:none` — including on `run()` itself.**
571
+ > `Robot#run` is declared `run(message = nil, ..., mcp: :none, tools: :none, ...)`,
572
+ > and an explicit `:none` means "send zero tools this turn". So a plain
573
+ > `robot.run("...")` sends the LLM **no tools and connects no MCP servers**, even
574
+ > when you passed `local_tools:` or `mcp:` at build time.
575
+ >
576
+ > The fix goes on the **run**, not the build:
577
+ >
578
+ > ```ruby
579
+ > robot.run("...", tools: :inherit) # send the attached local tools
580
+ > robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP and send its tools
581
+ > ```
582
+ >
583
+ > `mcp: :inherit` triggers the connection; `tools: :inherit` is additionally
584
+ > required for the MCP tools to actually be handed to the model.
585
+
586
+ > [!IMPORTANT]
587
+ > **For a standalone robot, do not pass `tools: :inherit` at build time.** Build-time
588
+ > `:inherit` resolves against the parent level, and for a standalone robot the parent
589
+ > is the global `:none` — producing an allowlist of `["none"]` that matches nothing
590
+ > and suppresses the tools even when the run asks for `:inherit`. Verified
591
+ > resolution for a standalone robot:
592
+ >
593
+ > | build `tools:` | run `tools:` | tools sent |
594
+ > |---|---|---|
595
+ > | unset | `:none` (default) | none |
596
+ > | unset | `:inherit` | the attached tools ✅ |
597
+ > | `:inherit` | `:inherit` | **none** ❌ |
598
+ > | `:inherit` | `:none` | none |
599
+ > | `:none` | `:inherit` | the attached tools ✅ |
600
+ >
601
+ > Leave `tools:` unset at build time — unless the robot is a member of a network.
602
+
603
+ > [!NOTE]
604
+ > **Inside a network, build-time `:inherit` is the opt-in, not a bug.** The parent
605
+ > is resolved at run time as the network's `config:`, so `:inherit` is how a robot
606
+ > asks for the network-level allowlist. With
607
+ > `RobotLab::RunConfig.new(tools: %w[RefundTool])` on the network and a robot
608
+ > holding `local_tools: [RefundTool, InvoiceTool]`:
609
+ >
610
+ > | build `tools:` | task `tools:` | tools sent |
611
+ > |---|---|---|
612
+ > | unset | omitted (`:none`) | none |
613
+ > | unset | `:inherit` | `refund`, `invoice` — the network allowlist is **not** applied |
614
+ > | `:inherit` | `:inherit` | `refund` — the network allowlist **is** applied ✅ |
615
+ > | `:inherit` | omitted (`:none`) | none |
616
+ >
617
+ > The same reasoning applies to `mcp:`.
429
618
 
430
619
  ```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
620
+ # Correct: attach tools at build time, request them at run time
440
621
  robot = RobotLab.build(
441
622
  name: "calculator",
442
623
  system_prompt: "You solve math problems.",
443
- mcp: :none,
444
- tools: :inherit, # required: without this, local_tools below is never sent
445
624
  local_tools: [Calculator]
446
625
  )
626
+
627
+ robot.run("What is 17 * 23?", tools: :inherit)
628
+
629
+ # Robot with MCP servers attached at build time
630
+ robot = RobotLab.build(
631
+ name: "agent",
632
+ system_prompt: "You are helpful.",
633
+ mcp: [{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } }]
634
+ )
635
+
636
+ robot.run("List the files in ./lib", mcp: :inherit, tools: :inherit)
447
637
  ```
448
638
 
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.
639
+ See [Runtime Tool Filtering](../guides/using-tools.md#runtime-tool-filtering) for the full `:inherit`/`:none`/array semantics.
450
640
 
451
641
  ## Next Steps
452
642