robot_lab 0.2.1 → 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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
|
@@ -20,12 +20,20 @@ puts result.last_text_content
|
|
|
20
20
|
|
|
21
21
|
### Name
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
An identifier used for routing, logging, and as the key under which a robot's result is stored in a network's `result.context`. If omitted it defaults to the literal string `"robot"` — nothing is auto-generated, so two unnamed robots share the same name.
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
26
|
robot = RobotLab.build(name: "support_agent", system_prompt: "...")
|
|
27
|
+
|
|
28
|
+
RobotLab.build.name # => "robot"
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> The default is load-bearing. A robot records whether `name:` was supplied by
|
|
33
|
+
> comparing it against `"robot"`; front-matter `robot_name:` is applied **only**
|
|
34
|
+
> when the constructor left the name at its default. Passing `name: "robot"`
|
|
35
|
+
> explicitly therefore still counts as "not named", and front matter wins.
|
|
36
|
+
|
|
29
37
|
### Description
|
|
30
38
|
|
|
31
39
|
Describes what the robot does (useful for routing decisions):
|
|
@@ -63,7 +71,9 @@ robot = RobotLab.build(
|
|
|
63
71
|
)
|
|
64
72
|
```
|
|
65
73
|
|
|
66
|
-
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`.
|
|
74
|
+
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`. This context is preserved across every re-application of the robot's config — including when a template's front matter re-renders mid-run — so a local-provider robot doesn't fall back to RubyLLM's static model registry (and raise a "model not found" error) on later turns.
|
|
75
|
+
|
|
76
|
+
Some local models route their entire response through reasoning/thinking content instead of the normal response text (e.g. `qwen3` on Ollama). When that happens, `result.reply` falls back to the thinking text automatically — see [Robot with Local Provider](../api/core/robot.md#robot-with-local-provider) for details.
|
|
67
77
|
|
|
68
78
|
### System Prompt
|
|
69
79
|
|
|
@@ -121,16 +131,31 @@ The following YAML front matter keys are applied to the robot's chat automatical
|
|
|
121
131
|
|
|
122
132
|
**LLM Configuration:**
|
|
123
133
|
|
|
124
|
-
| Key | Description |
|
|
125
|
-
|
|
126
|
-
| `model` | Override the LLM model |
|
|
127
|
-
| `temperature` | Controls randomness (0.0 - 1.0) |
|
|
128
|
-
| `top_p` | Nucleus sampling threshold |
|
|
129
|
-
| `top_k` | Top-k sampling |
|
|
130
|
-
| `max_tokens` | Maximum tokens in response |
|
|
131
|
-
| `presence_penalty` | Penalize based on presence |
|
|
132
|
-
| `frequency_penalty` | Penalize based on frequency |
|
|
133
|
-
| `stop` | Stop sequences |
|
|
134
|
+
| Key | Description | Applied from front matter? |
|
|
135
|
+
|-----|-------------|----------------------------|
|
|
136
|
+
| `model` | Override the LLM model | Yes |
|
|
137
|
+
| `temperature` | Controls randomness (0.0 - 1.0) | Yes |
|
|
138
|
+
| `top_p` | Nucleus sampling threshold | **No — silently dropped** |
|
|
139
|
+
| `top_k` | Top-k sampling | **No — silently dropped** |
|
|
140
|
+
| `max_tokens` | Maximum tokens in response | **No — silently dropped** |
|
|
141
|
+
| `presence_penalty` | Penalize based on presence | **No — silently dropped** |
|
|
142
|
+
| `frequency_penalty` | Penalize based on frequency | **No — silently dropped** |
|
|
143
|
+
| `stop` | Stop sequences | **No — silently dropped** |
|
|
144
|
+
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> Only `model` and `temperature` take effect from front matter. The other six are
|
|
147
|
+
> parsed into the robot's `RunConfig` and then dropped: `RunConfig#apply_to`
|
|
148
|
+
> dispatches `chat.with_<field>` guarded by `respond_to?`, and `RubyLLM::Chat`
|
|
149
|
+
> only implements `with_model` and `with_temperature`. A template declaring all
|
|
150
|
+
> eight leaves the chat's params hash empty — no warning, no error.
|
|
151
|
+
>
|
|
152
|
+
> The same six **do** work as constructor kwargs or via a `config:` `RunConfig`,
|
|
153
|
+
> which route through `with_params`:
|
|
154
|
+
>
|
|
155
|
+
> ```ruby
|
|
156
|
+
> RobotLab.build(name: "w", system_prompt: "...", top_p: 0.5, max_tokens: 1200)
|
|
157
|
+
> # chat params => {top_p: 0.5, max_tokens: 1200}
|
|
158
|
+
> ```
|
|
134
159
|
|
|
135
160
|
**Robot Identity and Capabilities:**
|
|
136
161
|
|
|
@@ -144,19 +169,31 @@ The following YAML front matter keys are applied to the robot's chat automatical
|
|
|
144
169
|
|
|
145
170
|
Constructor-provided values always take precedence over frontmatter values.
|
|
146
171
|
|
|
172
|
+
### Rendering a Template to a String
|
|
173
|
+
|
|
174
|
+
`RobotLab.render_template` renders a named template directly to a `String` — using the same configured template library (`prompts_dir` / `ROBOT_LAB_TEMPLATE_PATH`) as `template:` on `RobotLab.build` — without constructing a robot. Front-matter parameters are supplied as keyword arguments:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
RobotLab.render_template(:objective, topic: "commit messages")
|
|
178
|
+
# => "<rendered body of prompts/objective.md, with topic substituted>"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Unlike `template:` on `build` (which renders a template as a robot's *system prompt*), this returns the plain text — useful as a one-off task message, an evaluation rubric, or any other place you want a parameterized `.md` template's body without the overhead of a robot.
|
|
182
|
+
|
|
147
183
|
### Self-Contained Templates
|
|
148
184
|
|
|
149
185
|
Templates can declare everything a robot needs — identity, tools, MCP servers, and LLM config — making the `.md` file a complete robot definition:
|
|
150
186
|
|
|
151
|
-
```markdown title="prompts/
|
|
187
|
+
```markdown title="prompts/my_github_assistant.md"
|
|
152
188
|
---
|
|
153
189
|
description: GitHub assistant with MCP tool access
|
|
154
190
|
robot_name: github_bot
|
|
155
191
|
mcp:
|
|
156
192
|
- name: github
|
|
157
|
-
transport:
|
|
158
|
-
|
|
159
|
-
|
|
193
|
+
transport:
|
|
194
|
+
type: stdio
|
|
195
|
+
command: npx
|
|
196
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
160
197
|
model: claude-sonnet-4
|
|
161
198
|
temperature: 0.3
|
|
162
199
|
---
|
|
@@ -168,9 +205,22 @@ Build the robot with minimal constructor arguments:
|
|
|
168
205
|
|
|
169
206
|
```ruby
|
|
170
207
|
# Template provides name, description, MCP config, model, and temperature
|
|
171
|
-
robot = RobotLab.build(template: :
|
|
208
|
+
robot = RobotLab.build(template: :my_github_assistant)
|
|
209
|
+
|
|
210
|
+
# MCP still has to be requested at run time — see the warning below
|
|
211
|
+
robot.run("What are the open issues?", mcp: :inherit, tools: :inherit)
|
|
172
212
|
```
|
|
173
213
|
|
|
214
|
+
> [!WARNING]
|
|
215
|
+
> `transport:` **must be a nested hash**. The shipped
|
|
216
|
+
> `examples/prompts/github_assistant.md` uses the flat form
|
|
217
|
+
> (`transport: stdio` with sibling `command:`/`args:` keys), which raises
|
|
218
|
+
> `NoMethodError: undefined method 'transform_keys' for an instance of String`
|
|
219
|
+
> internally. The error is swallowed and logged as a warning, the server lands in
|
|
220
|
+
> `robot.failed_mcp_server_names`, and the robot builds with **zero tools**. That
|
|
221
|
+
> shipped template also declares no `model:` or `temperature:`, so it does not
|
|
222
|
+
> demonstrate the full self-contained pattern shown here.
|
|
223
|
+
|
|
174
224
|
### Tools in Front Matter
|
|
175
225
|
|
|
176
226
|
Declare tool classes by name in the `tools:` key. RobotLab resolves each string to a Ruby constant and instantiates it:
|
|
@@ -188,6 +238,9 @@ You help customers with order inquiries and refunds.
|
|
|
188
238
|
```ruby
|
|
189
239
|
# Tools are loaded from frontmatter — no local_tools: needed
|
|
190
240
|
robot = RobotLab.build(template: :order_support)
|
|
241
|
+
|
|
242
|
+
# ...but they are only sent to the model when the run asks for them
|
|
243
|
+
robot.run("Where is order 12345?", tools: :inherit)
|
|
191
244
|
```
|
|
192
245
|
|
|
193
246
|
Tool classes must be defined and loaded before the robot is built. If a tool name cannot be resolved, it is skipped with a warning.
|
|
@@ -211,19 +264,29 @@ Declare MCP server configurations directly in the template:
|
|
|
211
264
|
description: Developer assistant with filesystem access
|
|
212
265
|
mcp:
|
|
213
266
|
- name: filesystem
|
|
214
|
-
transport:
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
transport:
|
|
268
|
+
type: stdio
|
|
269
|
+
command: mcp-server-filesystem
|
|
270
|
+
args: ["--root", "/home/user/projects"]
|
|
217
271
|
---
|
|
218
272
|
You are a developer assistant with filesystem access.
|
|
219
273
|
```
|
|
220
274
|
|
|
221
275
|
```ruby
|
|
222
276
|
robot = RobotLab.build(template: :developer)
|
|
277
|
+
robot.run("List the files in lib/", mcp: :inherit, tools: :inherit)
|
|
223
278
|
```
|
|
224
279
|
|
|
225
280
|
Constructor `mcp:` overrides frontmatter `mcp:` when provided.
|
|
226
281
|
|
|
282
|
+
> [!WARNING]
|
|
283
|
+
> `transport:` takes a nested hash — `type:` plus the transport's own keys. A flat
|
|
284
|
+
> `transport: stdio` with sibling `command:`/`args:` keys fails silently (the
|
|
285
|
+
> `transform_keys` NoMethodError is swallowed) and the robot ends up with no MCP
|
|
286
|
+
> tools. Valid `type:` values are `stdio`, `sse`, `ws`, `websocket`,
|
|
287
|
+
> `streamable-http`, and `http`; the underscored `streamable_http` raises
|
|
288
|
+
> `ArgumentError`.
|
|
289
|
+
|
|
227
290
|
### Template with System Prompt
|
|
228
291
|
|
|
229
292
|
You can combine a template and an inline system prompt. Both are applied to the chat -- the template first, then the system prompt is appended as additional instructions:
|
|
@@ -350,7 +413,6 @@ Skills can include LLM configuration in their front matter. Config cascades in p
|
|
|
350
413
|
---
|
|
351
414
|
description: Enable creative responses
|
|
352
415
|
temperature: 0.9
|
|
353
|
-
top_p: 0.95
|
|
354
416
|
---
|
|
355
417
|
Be creative and imaginative in your responses.
|
|
356
418
|
```
|
|
@@ -364,6 +426,12 @@ robot = RobotLab.build(
|
|
|
364
426
|
# temperature is 0.9 from the skill (unless the main template or constructor overrides it)
|
|
365
427
|
```
|
|
366
428
|
|
|
429
|
+
> [!NOTE]
|
|
430
|
+
> Skill front matter is subject to the same limitation as template front matter:
|
|
431
|
+
> only `model` and `temperature` reach the chat. Adding `top_p: 0.95` to
|
|
432
|
+
> `creative_mode.md` would be parsed and then silently discarded. Set it as a
|
|
433
|
+
> constructor kwarg (`top_p: 0.95`) instead.
|
|
434
|
+
|
|
367
435
|
The precedence order (highest wins):
|
|
368
436
|
|
|
369
437
|
1. Constructor kwargs (`temperature: 0.3`)
|
|
@@ -414,8 +482,41 @@ robot = RobotLab.build(
|
|
|
414
482
|
system_prompt: "You help customers with orders.",
|
|
415
483
|
local_tools: [OrderLookup, InventoryCheck]
|
|
416
484
|
)
|
|
485
|
+
|
|
486
|
+
result = robot.run("Where is order 12345?", tools: :inherit)
|
|
417
487
|
```
|
|
418
488
|
|
|
489
|
+
> [!WARNING]
|
|
490
|
+
> **`run` defaults to `tools: :none` and `mcp: :none`.** Attaching tools at build
|
|
491
|
+
> time is not enough — a plain `robot.run("...")` sends the model **zero** tools,
|
|
492
|
+
> because an explicit `:none` means "send no tools this turn" rather than "fall
|
|
493
|
+
> back to the attached set". Pass `tools: :inherit` on the call to send the
|
|
494
|
+
> attached tools, and `mcp: :inherit, tools: :inherit` to connect MCP servers and
|
|
495
|
+
> send their tools.
|
|
496
|
+
>
|
|
497
|
+
> For a **standalone** robot, do not pass `tools: :inherit` at *build* time: the
|
|
498
|
+
> parent level is the global config's `:none`, so it resolves to an allowlist of
|
|
499
|
+
> `["none"]`, which matches nothing. Leave `tools:` unset in the constructor.
|
|
500
|
+
>
|
|
501
|
+
> | build `tools:` | run `tools:` | tools sent |
|
|
502
|
+
> |---|---|---|
|
|
503
|
+
> | unset | `:none` (default) | none |
|
|
504
|
+
> | unset | `:inherit` | all attached — **the correct pattern** |
|
|
505
|
+
> | `:inherit` | `:inherit` | none — the standalone trap |
|
|
506
|
+
> | `:none` | `:inherit` | all attached |
|
|
507
|
+
>
|
|
508
|
+
> This table is for a robot run on its own. Inside a **network** whose `config:`
|
|
509
|
+
> sets `tools:`/`mcp:`, build-time `:inherit` is not a trap — it is exactly how a
|
|
510
|
+
> robot opts in to the network-level list, and the parent is that list rather
|
|
511
|
+
> than `:none`. See
|
|
512
|
+
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
513
|
+
>
|
|
514
|
+
> An explicit array (`tools: [OrderLookup]`) is an **allowlist** over the attached
|
|
515
|
+
> tools; it selects from them and cannot add new ones. Entries must match how the
|
|
516
|
+
> tool was attached — a tool attached as a class matches its class name
|
|
517
|
+
> (`[OrderLookup]`), one attached as an instance matches RubyLLM's derived name
|
|
518
|
+
> (`%w[order_lookup]`). The two forms do not cross-match.
|
|
519
|
+
|
|
419
520
|
See the [Using Tools](using-tools.md) guide for details on defining tools.
|
|
420
521
|
|
|
421
522
|
## MCP Configuration
|
|
@@ -443,6 +544,14 @@ MCP configuration supports hierarchical resolution:
|
|
|
443
544
|
| `:inherit` | Use parent network/config MCP servers |
|
|
444
545
|
| `[...]` | Explicit array of server configurations |
|
|
445
546
|
|
|
547
|
+
`run` also defaults to `mcp: :none`, so the servers configured above are not connected by a plain `run`:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
robot.run("Read config/database.yml", mcp: :inherit, tools: :inherit)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
`robot.connect_mcp!` connects eagerly if you want the handshake to happen up front, but a later plain `run()` still sends no tools — `tools: :inherit` is what puts the MCP tools in the request. Connection failures are logged and recorded in `robot.failed_mcp_server_names`; they are never raised.
|
|
554
|
+
|
|
446
555
|
See the [MCP Integration](mcp-integration.md) guide for transport types and advanced patterns.
|
|
447
556
|
|
|
448
557
|
## Chaining Configuration
|
|
@@ -461,24 +570,35 @@ result = robot
|
|
|
461
570
|
|
|
462
571
|
### Available Chain Methods
|
|
463
572
|
|
|
573
|
+
This is the complete set — the LLM-facing methods are delegated dynamically from `RubyLLM::Chat`, and `with_template` / `with_bus` are RobotLab's own:
|
|
574
|
+
|
|
464
575
|
| Method | Description |
|
|
465
576
|
|--------|-------------|
|
|
466
577
|
| `with_model(id)` | Change the LLM model |
|
|
467
578
|
| `with_instructions(text)` | Set system instructions |
|
|
468
579
|
| `with_temperature(val)` | Set temperature |
|
|
469
|
-
| `
|
|
470
|
-
| `
|
|
471
|
-
| `
|
|
472
|
-
| `with_presence_penalty(val)` | Set presence penalty |
|
|
473
|
-
| `with_frequency_penalty(val)` | Set frequency penalty |
|
|
474
|
-
| `with_stop(sequences)` | Set stop sequences |
|
|
580
|
+
| `with_params(**params)` | Set arbitrary provider params (`top_p`, `max_tokens`, …) |
|
|
581
|
+
| `with_context(ctx)` | Set the RubyLLM context |
|
|
582
|
+
| `with_headers(**headers)` | Set extra request headers |
|
|
475
583
|
| `with_tool(tool)` | Add a single tool |
|
|
476
584
|
| `with_tools(*tools)` | Add multiple tools |
|
|
477
|
-
| `with_template(id, **ctx)` | Apply a prompt template |
|
|
478
585
|
| `with_schema(schema)` | Set structured output schema |
|
|
479
586
|
| `with_thinking(config)` | Enable extended thinking |
|
|
587
|
+
| `with_template(id, **ctx)` | Apply a prompt template |
|
|
480
588
|
| `with_bus(bus)` | Connect to a message bus (creates one if nil) |
|
|
481
589
|
|
|
590
|
+
> [!WARNING]
|
|
591
|
+
> `with_top_p`, `with_top_k`, `with_max_tokens`, `with_presence_penalty`,
|
|
592
|
+
> `with_frequency_penalty`, and `with_stop` **do not exist** — calling any of them
|
|
593
|
+
> raises `NoMethodError`. `RubyLLM::Chat` exposes those knobs through
|
|
594
|
+
> `with_params`, so use either the constructor kwarg or `with_params`:
|
|
595
|
+
>
|
|
596
|
+
> ```ruby
|
|
597
|
+
> robot.with_params(max_tokens: 2000, top_p: 0.3).run("...")
|
|
598
|
+
> # or
|
|
599
|
+
> RobotLab.build(name: "bot", system_prompt: "...", max_tokens: 2000, top_p: 0.3)
|
|
600
|
+
> ```
|
|
601
|
+
|
|
482
602
|
## Running Robots
|
|
483
603
|
|
|
484
604
|
### Standalone
|
|
@@ -495,14 +615,36 @@ The `run` method returns a `RobotResult` with:
|
|
|
495
615
|
```ruby
|
|
496
616
|
result.last_text_content # => "Hi there! How can I help?"
|
|
497
617
|
result.reply # => alias for last_text_content
|
|
498
|
-
result.output # =>
|
|
499
|
-
result.tool_calls # => Array of tool call results
|
|
618
|
+
result.output # => [TextMessage] built from the final response text
|
|
619
|
+
result.tool_calls # => Array of tool call results (see note)
|
|
500
620
|
result.robot_name # => "assistant"
|
|
501
|
-
result.stop_reason # =>
|
|
621
|
+
result.stop_reason # => always nil (see note)
|
|
622
|
+
result.input_tokens # => Integer
|
|
623
|
+
result.output_tokens # => Integer
|
|
502
624
|
result.duration # => Float (elapsed seconds, set in pipeline execution)
|
|
503
625
|
result.raw # => raw LLM response object
|
|
504
626
|
```
|
|
505
627
|
|
|
628
|
+
> [!NOTE]
|
|
629
|
+
> `result.tool_calls` is effectively always empty. It is read from the *final*
|
|
630
|
+
> assistant message, and by the time ruby_llm's tool loop has finished that
|
|
631
|
+
> message carries no tool calls. Use `:tool_call` [hooks](hooks.md) or the
|
|
632
|
+
> `on_tool_call:` / `on_tool_result:` callbacks to observe tool activity.
|
|
633
|
+
> Similarly, `result.output` holds only the final response text, not the full
|
|
634
|
+
> turn. There is no `result.content` and no `result.text?`.
|
|
635
|
+
|
|
636
|
+
> [!WARNING]
|
|
637
|
+
> `result.stop_reason` is **always `nil`**. `RubyLLM::Message` does not define
|
|
638
|
+
> `stop_reason`, and `build_result` fills the field with
|
|
639
|
+
> `response.respond_to?(:stop_reason) ? response.stop_reason : nil` — so no
|
|
640
|
+
> provider value ever lands there, and `.compact` drops the key from
|
|
641
|
+
> `result.export` entirely. Do not branch on `"end_turn"`, `"tool_use"`, or
|
|
642
|
+
> `"stop"`. Consequently `result.stopped?` is simply "this result has no tool
|
|
643
|
+
> calls".
|
|
644
|
+
>
|
|
645
|
+
> (`RobotLab::Message::VALID_STOP_REASONS` is `["tool", "stop"]`, but that
|
|
646
|
+
> constant governs the `Message` classes you build yourself, not `RobotResult`.)
|
|
647
|
+
|
|
506
648
|
### With Runtime Memory
|
|
507
649
|
|
|
508
650
|
Inject memory values for a single run:
|
|
@@ -556,7 +698,7 @@ robot = RobotLab.build(
|
|
|
556
698
|
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
557
699
|
```
|
|
558
700
|
|
|
559
|
-
|
|
701
|
+
`on_content` is also a `RunConfig` field, so it can be supplied through a `config:` on the robot itself rather than as a constructor kwarg:
|
|
560
702
|
|
|
561
703
|
```ruby
|
|
562
704
|
config = RobotLab::RunConfig.new(
|
|
@@ -565,6 +707,13 @@ config = RobotLab::RunConfig.new(
|
|
|
565
707
|
robot = RobotLab.build(name: "bot", system_prompt: "...", config: config)
|
|
566
708
|
```
|
|
567
709
|
|
|
710
|
+
> [!WARNING]
|
|
711
|
+
> This only works for the robot's **own** config. A network-level `config:` does
|
|
712
|
+
> **not** supply `on_content` (or any other callback or LLM field) to its member
|
|
713
|
+
> robots — a network propagates only `mcp` and `tools`. Each robot reads
|
|
714
|
+
> `on_content` from its own config at construction time, so streaming callbacks
|
|
715
|
+
> must be set per robot.
|
|
716
|
+
|
|
568
717
|
You can also monitor tool activity via callbacks:
|
|
569
718
|
|
|
570
719
|
```ruby
|
|
@@ -627,8 +776,12 @@ billing_specialist = RobotLab.build(
|
|
|
627
776
|
context: { department: "billing" },
|
|
628
777
|
local_tools: [InvoiceLookup, RefundProcessor]
|
|
629
778
|
)
|
|
779
|
+
|
|
780
|
+
billing_specialist.run("Refund order 12345", tools: :inherit)
|
|
630
781
|
```
|
|
631
782
|
|
|
783
|
+
In a network, the equivalent opt-in is on the task: `task :billing, billing_specialist, tools: :inherit, depends_on: :optional`.
|
|
784
|
+
|
|
632
785
|
### Summarizer Robot
|
|
633
786
|
|
|
634
787
|
Condense information:
|
|
@@ -686,6 +839,39 @@ The `on_message` block arity controls delivery handling:
|
|
|
686
839
|
|
|
687
840
|
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
688
841
|
|
|
842
|
+
### Auto-Responding to Bus Tasks
|
|
843
|
+
|
|
844
|
+
The `Comedian`/`ComedyCritic` example above hand-wires `on_message` to run and reply. For the common case — run every inbound task through the robot and reply with the result — `respond_to_tasks`/`serve` do it in one call:
|
|
845
|
+
|
|
846
|
+
```ruby
|
|
847
|
+
bob = RobotLab.build(name: "bob", template: :comedian, bus: bus)
|
|
848
|
+
bob.serve # every inbound task runs through bob.run and replies automatically
|
|
849
|
+
|
|
850
|
+
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
`serve` is shorthand for `respond_to_tasks(auto_reply: true) { |message| run(bus_task_content(message)).reply }`.
|
|
854
|
+
|
|
855
|
+
`bus_task_content` flattens the message payload for `run`: a `String` content is passed through via `to_s`, while a `Hash` content becomes one `"key: value"` line per entry. So a task sent as `{ topic: "robots", style: "dry" }` reaches the LLM as:
|
|
856
|
+
|
|
857
|
+
```
|
|
858
|
+
topic: robots
|
|
859
|
+
style: dry
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
Use `respond_to_tasks` directly when the reply shouldn't just be `run(...).reply` — e.g. to build the prompt yourself, or to post-process the result:
|
|
863
|
+
|
|
864
|
+
```ruby
|
|
865
|
+
bob.respond_to_tasks do |message|
|
|
866
|
+
joke = run(message.content.to_s).reply.strip
|
|
867
|
+
joke.end_with?("!") ? joke : "#{joke}!"
|
|
868
|
+
end
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Both ignore messages that are themselves replies (`message.reply?`), so two robots calling `serve`/`respond_to_tasks` on each other don't loop.
|
|
872
|
+
|
|
873
|
+
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
874
|
+
|
|
689
875
|
### Spawning Robots Dynamically
|
|
690
876
|
|
|
691
877
|
Create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
@@ -728,6 +914,7 @@ Key features of `spawn`:
|
|
|
728
914
|
- Creates a bus lazily if the parent doesn't have one
|
|
729
915
|
- Spawned robots can immediately send and receive messages
|
|
730
916
|
- Multiple robots with the same name enable fan-out messaging
|
|
917
|
+
- The child inherits the parent's `model`/`provider` (caller-supplied `model:`/`provider:` still override) — a dispatcher running on a local Ollama model spawns specialists that also target that model, instead of falling back to the global default model/provider
|
|
731
918
|
|
|
732
919
|
Robots can also join a bus after creation:
|
|
733
920
|
|
|
@@ -738,7 +925,7 @@ bot.with_bus(existing_bus) # now connected and can send/receive messages
|
|
|
738
925
|
|
|
739
926
|
## Context Window Compression
|
|
740
927
|
|
|
741
|
-
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using
|
|
928
|
+
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using stemmed term-frequency cosine similarity (term frequencies only — no IDF weighting) against the most recent context, keeping turns that are still relevant and discarding or summarizing those that aren't.
|
|
742
929
|
|
|
743
930
|
```ruby
|
|
744
931
|
# Default settings: protect 3 most-recent turn pairs, drop anything below 0.2
|
|
@@ -775,7 +962,7 @@ gem "classifier", "~> 2.3"
|
|
|
775
962
|
|
|
776
963
|
## Convergence Detection
|
|
777
964
|
|
|
778
|
-
`RobotLab::Convergence` uses
|
|
965
|
+
`RobotLab::Convergence` uses stemmed term-frequency cosine similarity (not TF-IDF — on a two-document corpus, IDF suppresses exactly the shared terms that signal agreement) to detect when two independent agents have reached the same conclusion. The primary use case is skipping an expensive reconciler robot when two verifiers already agree. Texts shorter than 30 characters always score `0.0`.
|
|
779
966
|
|
|
780
967
|
```ruby
|
|
781
968
|
# Check the similarity score directly (returns Float 0.0..1.0)
|
|
@@ -788,26 +975,41 @@ RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
|
788
975
|
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
789
976
|
```
|
|
790
977
|
|
|
791
|
-
Wire it into a network
|
|
978
|
+
Wire it into a network for the reconciler fast-path. There is no router object in RobotLab — the reconciler is declared `depends_on: :optional` and a gate robot activates it only when the verifiers disagree:
|
|
792
979
|
|
|
793
980
|
```ruby
|
|
794
981
|
verifier_a = RobotLab.build(name: "verifier_a", system_prompt: "Verify the answer.")
|
|
795
982
|
verifier_b = RobotLab.build(name: "verifier_b", system_prompt: "Independently verify the answer.")
|
|
796
983
|
reconciler = RobotLab.build(name: "reconciler", system_prompt: "Reconcile conflicting answers.")
|
|
797
984
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
985
|
+
class ConvergenceGate < RobotLab::Robot
|
|
986
|
+
def call(result)
|
|
987
|
+
a = result.context[:verifier_a]&.reply.to_s # keyed by ROBOT name
|
|
988
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
989
|
+
|
|
990
|
+
return result if RobotLab::Convergence.detected?(a, b) # agree — skip reconciler
|
|
801
991
|
|
|
802
|
-
|
|
803
|
-
|
|
992
|
+
result.activate(:reconciler)
|
|
993
|
+
end
|
|
804
994
|
end
|
|
805
995
|
|
|
806
|
-
network = RobotLab.create_network(name: "verify"
|
|
807
|
-
|
|
996
|
+
network = RobotLab.create_network(name: "verify") do
|
|
997
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
998
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
999
|
+
task :gate, ConvergenceGate.new(name: "gate"), depends_on: %i[verifier_a verifier_b]
|
|
1000
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
808
1001
|
end
|
|
1002
|
+
|
|
1003
|
+
network.run(message: "Is the deployment healthy?").activated_steps
|
|
1004
|
+
# => [] when they agreed, [:reconciler] when they diverged
|
|
809
1005
|
```
|
|
810
1006
|
|
|
1007
|
+
> [!WARNING]
|
|
1008
|
+
> `RobotLab.create_network` accepts only `name:`, `concurrency:`, `config:`, and a
|
|
1009
|
+
> block. There are no `router:` or `robots:` keyword arguments — passing them
|
|
1010
|
+
> raises `ArgumentError: unknown keywords: :robots, :router` — and no `Router` or
|
|
1011
|
+
> `Router::Args` class exists anywhere in the library.
|
|
1012
|
+
|
|
811
1013
|
Requires the `classifier` gem (`~> 2.3`).
|
|
812
1014
|
|
|
813
1015
|
## Structured Delegation
|
|
@@ -863,20 +1065,64 @@ result = f1.value(timeout: 30)
|
|
|
863
1065
|
|
|
864
1066
|
## Configuration
|
|
865
1067
|
|
|
866
|
-
RobotLab uses `MywayConfig` for configuration.
|
|
1068
|
+
RobotLab uses `MywayConfig` for configuration. Read values off the config object directly:
|
|
867
1069
|
|
|
868
1070
|
```ruby
|
|
869
|
-
RobotLab.config.ruby_llm.model
|
|
1071
|
+
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
870
1072
|
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
871
1073
|
```
|
|
872
1074
|
|
|
873
|
-
|
|
1075
|
+
`RobotLab.configure` also exists, and yields the config object for imperative setup:
|
|
1076
|
+
|
|
1077
|
+
```ruby
|
|
1078
|
+
RobotLab.configure do |config|
|
|
1079
|
+
config.logger = Logger.new($stdout)
|
|
1080
|
+
end
|
|
1081
|
+
```
|
|
874
1082
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
-
|
|
879
|
-
|
|
1083
|
+
Configuration is layered, lowest precedence first:
|
|
1084
|
+
|
|
1085
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
1086
|
+
2. Environment-specific overrides (development, test, production)
|
|
1087
|
+
3. XDG user config (`~/.config/robot_lab/robot_lab.yml`)
|
|
1088
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
1089
|
+
5. Environment variables (`ROBOT_LAB_*` prefix; `__` for nesting)
|
|
1090
|
+
6. Constructor parameters
|
|
1091
|
+
|
|
1092
|
+
> [!WARNING]
|
|
1093
|
+
> The XDG file is `~/.config/robot_lab/**robot_lab.yml**` — the filename repeats
|
|
1094
|
+
> the app name. `~/.config/robot_lab/config.yml` is never read.
|
|
1095
|
+
>
|
|
1096
|
+
> A top-level `defaults:` wrapper is **always** ignored — that key means
|
|
1097
|
+
> something only inside the gem's own bundled `defaults.yml`. Write
|
|
1098
|
+
> `max_tool_rounds: 12`, not `defaults:\n max_tool_rounds: 12`.
|
|
1099
|
+
>
|
|
1100
|
+
> An **environment-named** wrapper is a different story, and the two config
|
|
1101
|
+
> files behave differently:
|
|
1102
|
+
>
|
|
1103
|
+
> | File | flat keys | `development:` / `test:` / `production:` wrapper |
|
|
1104
|
+
> |---|---|---|
|
|
1105
|
+
> | `~/.config/robot_lab/robot_lab.yml` | honored | **honored** for the current environment |
|
|
1106
|
+
> | `./config/robot_lab.yml` (no Rails) | honored | ignored |
|
|
1107
|
+
> | `./config/robot_lab.yml` (in Rails) | ignored | **required** — keys must be nested under `Rails.env` |
|
|
1108
|
+
>
|
|
1109
|
+
> The XDG loader checks for a section named for the current environment and only
|
|
1110
|
+
> falls back to the file root when there is none. Outside Rails the environment
|
|
1111
|
+
> defaults to `development` (or `RAILS_ENV` / `RACK_ENV` when set), so a
|
|
1112
|
+
> `development:` section in the XDG file takes effect while `test:` and
|
|
1113
|
+
> `production:` sections sit dormant. Verified with a `max_iterations: 777`
|
|
1114
|
+
> XDG file: flat → 777, `development:` → 777, `production:` → 10 (until
|
|
1115
|
+
> `RACK_ENV=production`, then 777), `defaults:` → 10.
|
|
1116
|
+
>
|
|
1117
|
+
> Inside Rails, `anyway_config` sets the current environment to `Rails.env`,
|
|
1118
|
+
> which makes the **project** file environmental too — a flat
|
|
1119
|
+
> `./config/robot_lab.yml` is then ignored.
|
|
1120
|
+
>
|
|
1121
|
+
> ERB is evaluated only in `./config/robot_lab.yml`. The XDG loader uses
|
|
1122
|
+
> `YAML.safe_load` with no ERB pass, so `<%= ENV['KEY'] %>` there stays a literal
|
|
1123
|
+
> string. Nested env vars also arrive as strings
|
|
1124
|
+
> (`ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180` yields `"180"`); top-level keys are
|
|
1125
|
+
> type-coerced.
|
|
880
1126
|
|
|
881
1127
|
## Best Practices
|
|
882
1128
|
|