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.
Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.loki +60 -0
  4. data/.quality/reek_baseline.txt +43 -0
  5. data/.rubocop.yml +5 -167
  6. data/CHANGELOG.md +54 -0
  7. data/README.md +185 -64
  8. data/Rakefile +28 -12
  9. data/docs/api/core/index.md +41 -14
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +285 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +636 -60
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +152 -14
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +83 -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 +519 -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 +118 -30
  33. data/docs/architecture/index.md +86 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +153 -38
  36. data/docs/architecture/robot-execution.md +173 -91
  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 +299 -93
  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 +296 -50
  48. data/docs/guides/creating-networks.md +220 -31
  49. data/docs/guides/hooks.md +1084 -0
  50. data/docs/guides/index.md +5 -0
  51. data/docs/guides/knowledge.md +37 -6
  52. data/docs/guides/mcp-integration.md +211 -44
  53. data/docs/guides/memory.md +103 -12
  54. data/docs/guides/observability.md +166 -51
  55. data/docs/guides/streaming.md +184 -125
  56. data/docs/guides/using-tools.md +300 -11
  57. data/docs/index.md +37 -5
  58. data/examples/01_simple_robot.rb +2 -2
  59. data/examples/02_tools.rb +14 -4
  60. data/examples/03_network.rb +23 -9
  61. data/examples/04_mcp.rb +11 -4
  62. data/examples/05_streaming.rb +8 -5
  63. data/examples/06_prompt_templates.rb +13 -9
  64. data/examples/07_network_memory.rb +5 -5
  65. data/examples/08_llm_config.rb +20 -15
  66. data/examples/09_chaining.rb +4 -4
  67. data/examples/11_network_introspection.rb +4 -4
  68. data/examples/12_message_bus.rb +2 -2
  69. data/examples/13_spawn.rb +2 -2
  70. data/examples/14_rusty_circuit/README.md +1 -0
  71. data/examples/14_rusty_circuit/comic.rb +7 -3
  72. data/examples/14_rusty_circuit/display.rb +14 -0
  73. data/examples/14_rusty_circuit/heckler.rb +8 -6
  74. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  75. data/examples/14_rusty_circuit/scout.rb +17 -10
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  78. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  79. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  80. data/examples/16_writers_room/writer.rb +22 -22
  81. data/examples/16_writers_room/writers_room.rb +2 -0
  82. data/examples/17_skills.rb +14 -13
  83. data/examples/18_rails/README.md +20 -1
  84. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  85. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  86. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  87. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  88. data/examples/19_token_tracking.rb +25 -9
  89. data/examples/20_circuit_breaker.rb +10 -7
  90. data/examples/21_learning_loop.rb +42 -16
  91. data/examples/22_context_compression.rb +23 -23
  92. data/examples/23_convergence.rb +24 -17
  93. data/examples/24_structured_delegation.rb +13 -8
  94. data/examples/25_history_search.rb +12 -8
  95. data/examples/27_incident_response/incident_response.rb +31 -13
  96. data/examples/28_mcp_discovery.rb +17 -13
  97. data/examples/29_ractor_tools.rb +4 -2
  98. data/examples/30_ractor_network.rb +22 -17
  99. data/examples/31_launch_assessment.rb +20 -9
  100. data/examples/32_newsletter_reader.rb +7 -2
  101. data/examples/33_stock_predictor.rb +34 -13
  102. data/examples/34_agentskills.rb +7 -3
  103. data/examples/35_hooks.rb +266 -0
  104. data/examples/README.md +203 -42
  105. data/examples/common.rb +79 -7
  106. data/examples/xyzzy.rb +97 -0
  107. data/lib/robot_lab/agent_skill.rb +5 -4
  108. data/lib/robot_lab/budget/ledger.rb +98 -0
  109. data/lib/robot_lab/capabilities.rb +84 -0
  110. data/lib/robot_lab/config/defaults.yml +10 -0
  111. data/lib/robot_lab/config.rb +14 -9
  112. data/lib/robot_lab/error.rb +34 -4
  113. data/lib/robot_lab/errors.rb +45 -0
  114. data/lib/robot_lab/hook.rb +79 -0
  115. data/lib/robot_lab/hook_context.rb +194 -0
  116. data/lib/robot_lab/hook_registry.rb +55 -0
  117. data/lib/robot_lab/hooks.rb +87 -0
  118. data/lib/robot_lab/mcp/connection_poller.rb +2 -2
  119. data/lib/robot_lab/names.rb +402 -0
  120. data/lib/robot_lab/narrator.rb +87 -0
  121. data/lib/robot_lab/network.rb +77 -20
  122. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  123. data/lib/robot_lab/robot/budget.rb +89 -0
  124. data/lib/robot_lab/robot/bus_messaging.rb +90 -18
  125. data/lib/robot_lab/robot/hooking.rb +56 -0
  126. data/lib/robot_lab/robot/mcp_management.rb +8 -2
  127. data/lib/robot_lab/robot/template_rendering.rb +18 -5
  128. data/lib/robot_lab/robot.rb +233 -89
  129. data/lib/robot_lab/run_config.rb +24 -5
  130. data/lib/robot_lab/runnable.rb +51 -0
  131. data/lib/robot_lab/sandbox/null.rb +13 -0
  132. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  133. data/lib/robot_lab/sandbox.rb +52 -0
  134. data/lib/robot_lab/script_tool.rb +60 -2
  135. data/lib/robot_lab/task.rb +26 -20
  136. data/lib/robot_lab/tool.rb +52 -11
  137. data/lib/robot_lab/version.rb +1 -1
  138. data/lib/robot_lab.rb +70 -15
  139. data/mkdocs.yml +7 -1
  140. metadata +41 -99
  141. data/examples/temp.md +0 -51
  142. data/site/404.html +0 -2300
  143. data/site/api/core/index.html +0 -2706
  144. data/site/api/core/memory/index.html +0 -3793
  145. data/site/api/core/network/index.html +0 -3500
  146. data/site/api/core/robot/index.html +0 -4566
  147. data/site/api/core/state/index.html +0 -3390
  148. data/site/api/core/tool/index.html +0 -3843
  149. data/site/api/index.html +0 -2635
  150. data/site/api/mcp/client/index.html +0 -3435
  151. data/site/api/mcp/index.html +0 -2783
  152. data/site/api/mcp/server/index.html +0 -3252
  153. data/site/api/mcp/transports/index.html +0 -3352
  154. data/site/api/messages/index.html +0 -2641
  155. data/site/api/messages/text-message/index.html +0 -3087
  156. data/site/api/messages/tool-call-message/index.html +0 -3159
  157. data/site/api/messages/tool-result-message/index.html +0 -3252
  158. data/site/api/messages/user-message/index.html +0 -3212
  159. data/site/api/streaming/context/index.html +0 -3282
  160. data/site/api/streaming/events/index.html +0 -3347
  161. data/site/api/streaming/index.html +0 -2738
  162. data/site/architecture/core-concepts/index.html +0 -3757
  163. data/site/architecture/index.html +0 -2797
  164. data/site/architecture/message-flow/index.html +0 -3238
  165. data/site/architecture/network-orchestration/index.html +0 -3433
  166. data/site/architecture/robot-execution/index.html +0 -3140
  167. data/site/architecture/state-management/index.html +0 -3498
  168. data/site/assets/css/custom.css +0 -56
  169. data/site/assets/images/favicon.png +0 -0
  170. data/site/assets/images/robot_lab.jpg +0 -0
  171. data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
  172. data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
  173. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  174. data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  175. data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  176. data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  177. data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  178. data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  179. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  180. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  181. data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  182. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  183. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  184. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  185. data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  186. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  187. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  188. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  189. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  190. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  191. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  192. data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  193. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  194. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  195. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  196. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  197. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  198. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  199. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  200. data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  201. data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  202. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  203. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  204. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  205. data/site/assets/javascripts/lunr/tinyseg.js +0 -206
  206. data/site/assets/javascripts/lunr/wordcut.js +0 -6708
  207. data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
  208. data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
  209. data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
  210. data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
  211. data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  212. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
  213. data/site/concepts/index.html +0 -3455
  214. data/site/examples/basic-chat/index.html +0 -2880
  215. data/site/examples/index.html +0 -2907
  216. data/site/examples/mcp-server/index.html +0 -3018
  217. data/site/examples/multi-robot-network/index.html +0 -3131
  218. data/site/examples/rails-application/index.html +0 -3329
  219. data/site/examples/tool-usage/index.html +0 -3085
  220. data/site/getting-started/configuration/index.html +0 -3745
  221. data/site/getting-started/index.html +0 -2572
  222. data/site/getting-started/installation/index.html +0 -2981
  223. data/site/getting-started/quick-start/index.html +0 -2942
  224. data/site/guides/building-robots/index.html +0 -4290
  225. data/site/guides/creating-networks/index.html +0 -3858
  226. data/site/guides/index.html +0 -2586
  227. data/site/guides/mcp-integration/index.html +0 -3581
  228. data/site/guides/memory/index.html +0 -3586
  229. data/site/guides/rails-integration/index.html +0 -4019
  230. data/site/guides/streaming/index.html +0 -3157
  231. data/site/guides/using-tools/index.html +0 -3802
  232. data/site/index.html +0 -2671
  233. data/site/search/search_index.json +0 -1
  234. data/site/sitemap.xml +0 -183
  235. data/site/sitemap.xml.gz +0 -0
  236. data/site/tags.json +0 -1
@@ -20,12 +20,20 @@ puts result.last_text_content
20
20
 
21
21
  ### Name
22
22
 
23
- A unique identifier used for routing and logging. If omitted, an auto-generated name is used:
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/github_assistant.md"
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: stdio
158
- command: npx
159
- args: ["-y", "@modelcontextprotocol/server-github"]
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: :github_assistant)
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: stdio
215
- command: mcp-server-filesystem
216
- args: ["--root", "/home/user/projects"]
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
- | `with_top_p(val)` | Set nucleus sampling |
470
- | `with_top_k(val)` | Set top-k sampling |
471
- | `with_max_tokens(val)` | Set max output tokens |
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 # => Array of output messages
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 # => stop reason from the LLM
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
- The `on_content` callback participates in the RunConfig cascade, so it can be set at the config level and inherited by robots:
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 TF-IDF cosine similarity against the most recent context, keeping turns that are still relevant and discarding or summarizing those that aren't.
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 TF-IDF cosine similarity to detect when two independent agents have reached the same conclusion. The primary use case is a network router that skips an expensive reconciler robot when two verifiers already agree.
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 router for the reconciler fast-path:
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
- router = lambda do |args|
799
- a = args.context[:verifier_a]&.reply.to_s
800
- b = args.context[:verifier_b]&.reply.to_s
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
- # Skip reconciler when verifiers agree
803
- RobotLab::Convergence.detected?(a, b) ? nil : ["reconciler"]
992
+ result.activate(:reconciler)
993
+ end
804
994
  end
805
995
 
806
- network = RobotLab.create_network(name: "verify", router: router) do
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. Access the config object directly -- there is no `RobotLab.configure` block:
1068
+ RobotLab uses `MywayConfig` for configuration. Read values off the config object directly:
867
1069
 
868
1070
  ```ruby
869
- RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
1071
+ RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
870
1072
  RobotLab.config.ruby_llm.request_timeout # => 120
871
1073
  ```
872
1074
 
873
- Configuration is loaded from:
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
- - Bundled defaults (`lib/robot_lab/config/defaults.yml`)
876
- - Environment-specific overrides (development, test, production)
877
- - XDG config files (`~/.config/robot_lab/config.yml`)
878
- - Project config (`./config/robot_lab.yml`)
879
- - Environment variables (`ROBOT_LAB_*` prefix)
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