robot_lab 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,1187 +0,0 @@
1
- # Building Robots
2
-
3
- This guide covers everything you need to know about creating robots in RobotLab.
4
-
5
- ## Basic Robot
6
-
7
- Create a robot using the `RobotLab.build` factory method with keyword arguments:
8
-
9
- ```ruby
10
- robot = RobotLab.build(
11
- name: "assistant",
12
- system_prompt: "You are a helpful assistant."
13
- )
14
-
15
- result = robot.run("Hello!")
16
- puts result.last_text_content
17
- ```
18
-
19
- ## Robot Properties
20
-
21
- ### Name
22
-
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
-
25
- ```ruby
26
- robot = RobotLab.build(name: "support_agent", system_prompt: "...")
27
-
28
- RobotLab.build.name # => "robot"
29
- ```
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
-
37
- ### Description
38
-
39
- Describes what the robot does (useful for routing decisions):
40
-
41
- ```ruby
42
- robot = RobotLab.build(
43
- name: "support_agent",
44
- description: "Handles customer support inquiries about orders and refunds",
45
- system_prompt: "..."
46
- )
47
- ```
48
-
49
- ### Model
50
-
51
- The LLM model to use. Defaults to the value in `RobotLab.config.ruby_llm.model`:
52
-
53
- ```ruby
54
- robot = RobotLab.build(
55
- name: "writer",
56
- model: "claude-sonnet-4",
57
- system_prompt: "You are a creative writer."
58
- )
59
- ```
60
-
61
- ### Provider
62
-
63
- For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter. This tells RubyLLM to skip model validation and connect directly:
64
-
65
- ```ruby
66
- robot = RobotLab.build(
67
- name: "local_bot",
68
- model: "llama3.2",
69
- provider: :ollama,
70
- system_prompt: "You are a helpful assistant."
71
- )
72
- ```
73
-
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.
77
-
78
- ### System Prompt
79
-
80
- An inline string that defines the robot's personality and behavior:
81
-
82
- ```ruby
83
- robot = RobotLab.build(
84
- name: "support",
85
- system_prompt: <<~PROMPT
86
- You are a customer support specialist for TechCo.
87
-
88
- Your responsibilities:
89
- - Answer questions about products and services
90
- - Help resolve order issues
91
- - Provide friendly, professional assistance
92
-
93
- Always be polite and acknowledge the customer's concerns.
94
- PROMPT
95
- )
96
- ```
97
-
98
- ## Template Files
99
-
100
- Templates are `.md` files managed by [prompt_manager](https://github.com/MadBomber/prompt_manager). Reference a template by symbol; RobotLab resolves it through the configured template path.
101
-
102
- ```ruby
103
- # Reference template by symbol (loads prompts/support.md)
104
- robot = RobotLab.build(
105
- name: "support",
106
- template: :support,
107
- context: { company: "TechCo", tone: "friendly" }
108
- )
109
- ```
110
-
111
- ### Template Format
112
-
113
- Templates use `.md` files with YAML front matter:
114
-
115
- ```markdown title="prompts/support.md"
116
- ---
117
- description: Customer support assistant
118
- parameters:
119
- company: "Acme"
120
- tone: "professional"
121
- model: claude-sonnet-4
122
- temperature: 0.7
123
- ---
124
- You are a support agent for <%= company %>.
125
- Your tone should be <%= tone %>.
126
- ```
127
-
128
- ### Front Matter Configuration
129
-
130
- The following YAML front matter keys are applied to the robot's chat automatically:
131
-
132
- **LLM Configuration:**
133
-
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
- > ```
159
-
160
- **Robot Identity and Capabilities:**
161
-
162
- | Key | Description |
163
- |-----|-------------|
164
- | `robot_name` | Override the robot's name (when constructor uses the default) |
165
- | `description` | Human-readable description of the robot |
166
- | `tools` | Array of tool class names (resolved via `Object.const_get`) |
167
- | `mcp` | Array of MCP server configurations |
168
- | `skills` | Array of skill template symbols to prepend (see [Composable Skills](#composable-skills)) |
169
-
170
- Constructor-provided values always take precedence over frontmatter values.
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
-
183
- ### Self-Contained Templates
184
-
185
- Templates can declare everything a robot needs — identity, tools, MCP servers, and LLM config — making the `.md` file a complete robot definition:
186
-
187
- ```markdown title="prompts/my_github_assistant.md"
188
- ---
189
- description: GitHub assistant with MCP tool access
190
- robot_name: github_bot
191
- mcp:
192
- - name: github
193
- transport:
194
- type: stdio
195
- command: npx
196
- args: ["-y", "@modelcontextprotocol/server-github"]
197
- model: claude-sonnet-4
198
- temperature: 0.3
199
- ---
200
- You are a helpful GitHub assistant with access to GitHub tools via MCP.
201
- Use the available tools to help answer questions about GitHub repositories.
202
- ```
203
-
204
- Build the robot with minimal constructor arguments:
205
-
206
- ```ruby
207
- # Template provides name, description, MCP config, model, and temperature
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)
212
- ```
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
-
224
- ### Tools in Front Matter
225
-
226
- Declare tool classes by name in the `tools:` key. RobotLab resolves each string to a Ruby constant and instantiates it:
227
-
228
- ```markdown title="prompts/order_support.md"
229
- ---
230
- description: Order support specialist
231
- tools:
232
- - OrderLookup
233
- - RefundProcessor
234
- ---
235
- You help customers with order inquiries and refunds.
236
- ```
237
-
238
- ```ruby
239
- # Tools are loaded from frontmatter — no local_tools: needed
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)
244
- ```
245
-
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.
247
-
248
- Constructor `local_tools:` overrides frontmatter `tools:` when provided:
249
-
250
- ```ruby
251
- # Constructor tools take precedence over frontmatter tools
252
- robot = RobotLab.build(
253
- template: :order_support,
254
- local_tools: [OrderLookup] # Only OrderLookup, not RefundProcessor
255
- )
256
- ```
257
-
258
- ### MCP in Front Matter
259
-
260
- Declare MCP server configurations directly in the template:
261
-
262
- ```markdown title="prompts/developer.md"
263
- ---
264
- description: Developer assistant with filesystem access
265
- mcp:
266
- - name: filesystem
267
- transport:
268
- type: stdio
269
- command: mcp-server-filesystem
270
- args: ["--root", "/home/user/projects"]
271
- ---
272
- You are a developer assistant with filesystem access.
273
- ```
274
-
275
- ```ruby
276
- robot = RobotLab.build(template: :developer)
277
- robot.run("List the files in lib/", mcp: :inherit, tools: :inherit)
278
- ```
279
-
280
- Constructor `mcp:` overrides frontmatter `mcp:` when provided.
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
-
290
- ### Template with System Prompt
291
-
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:
293
-
294
- ```ruby
295
- robot = RobotLab.build(
296
- name: "support",
297
- template: :support,
298
- context: { company: "TechCo" },
299
- system_prompt: "Always respond in Spanish."
300
- )
301
- ```
302
-
303
- ## Composable Skills
304
-
305
- Skills let you compose robot behaviors from reusable templates without creating a dedicated template for every combination. A skill is just a regular template whose prompt body gets prepended before the main template's body.
306
-
307
- ### Why Skills?
308
-
309
- Consider a support agent that needs to:
310
-
311
- - Ask clarifying questions before acting
312
- - Detect customer sentiment
313
- - Respond in structured JSON
314
-
315
- Without skills, you'd create a single monolithic template or copy-paste shared instructions across templates. With skills, each behavior is a standalone template that can be mixed into any robot.
316
-
317
- ### Defining a Skill
318
-
319
- A skill is a standard `.md` template file. There is no special syntax — any template can be used as a skill:
320
-
321
- ```markdown title="prompts/clarifier.md"
322
- ---
323
- description: Ask clarifying questions before acting
324
- ---
325
- Before answering, consider whether the user's request is ambiguous.
326
- If so, ask one focused clarifying question before proceeding.
327
- ```
328
-
329
- ```markdown title="prompts/json_responder.md"
330
- ---
331
- description: Respond in structured JSON
332
- temperature: 0.2
333
- ---
334
- Always respond with valid JSON. Use this structure:
335
- {"answer": "...", "confidence": 0.0-1.0, "sources": [...]}
336
- ```
337
-
338
- ### Using Skills via Constructor
339
-
340
- Pass `skills:` as a symbol or array of symbols:
341
-
342
- ```ruby
343
- # Single skill
344
- robot = RobotLab.build(
345
- name: "bot",
346
- template: :support,
347
- skills: :clarifier
348
- )
349
-
350
- # Multiple skills
351
- robot = RobotLab.build(
352
- name: "bot",
353
- template: :support,
354
- skills: [:clarifier, :json_responder],
355
- context: { company: "Acme Corp" }
356
- )
357
- ```
358
-
359
- The resulting system prompt is composed in order: clarifier body, then json_responder body, then the main support template body.
360
-
361
- ### Using Skills via Front Matter
362
-
363
- Templates can declare skills directly in their front matter:
364
-
365
- ```markdown title="prompts/smart_support.md"
366
- ---
367
- description: Support agent with built-in skills
368
- skills:
369
- - clarifier
370
- - json_responder
371
- parameters:
372
- company: null
373
- ---
374
- You are a support agent for <%= company %>.
375
- Help customers with their inquiries.
376
- ```
377
-
378
- ```ruby
379
- # Skills are loaded from front matter automatically
380
- robot = RobotLab.build(
381
- template: :smart_support,
382
- context: { company: "Acme Corp" }
383
- )
384
- ```
385
-
386
- Constructor `skills:` and front matter `skills:` are combined — constructor skills are processed first, then front matter skills.
387
-
388
- ### Nested Skills
389
-
390
- Skills can reference other skills, enabling layered composition:
391
-
392
- ```markdown title="prompts/safety.md"
393
- ---
394
- description: Safety guidelines
395
- skills:
396
- - content_filter
397
- - pii_redactor
398
- ---
399
- Follow all safety guidelines when responding.
400
- ```
401
-
402
- Nested skills are expanded depth-first. For the example above, the prompt order would be: content_filter, pii_redactor, safety, then the main template.
403
-
404
- ### Cycle Detection
405
-
406
- If skills form a cycle (A references B, B references A), RobotLab detects it automatically, logs a warning, and skips the duplicate. This prevents infinite loops.
407
-
408
- ### Config Cascade
409
-
410
- Skills can include LLM configuration in their front matter. Config cascades in processing order — later values override earlier ones:
411
-
412
- ```markdown title="prompts/creative_mode.md"
413
- ---
414
- description: Enable creative responses
415
- temperature: 0.9
416
- ---
417
- Be creative and imaginative in your responses.
418
- ```
419
-
420
- ```ruby
421
- robot = RobotLab.build(
422
- name: "writer",
423
- template: :article_writer,
424
- skills: [:creative_mode]
425
- )
426
- # temperature is 0.9 from the skill (unless the main template or constructor overrides it)
427
- ```
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
-
435
- The precedence order (highest wins):
436
-
437
- 1. Constructor kwargs (`temperature: 0.3`)
438
- 2. Main template front matter
439
- 3. Later skills override earlier skills
440
- 4. First skill in the list
441
-
442
- ### Skills Without a Main Template
443
-
444
- Skills work without a main template — useful for quick composition:
445
-
446
- ```ruby
447
- robot = RobotLab.build(
448
- name: "safe_bot",
449
- skills: [:safety, :json_responder],
450
- system_prompt: "You answer questions about our product."
451
- )
452
- ```
453
-
454
- ### Shared Context
455
-
456
- All skills and the main template render with the same `context:` hash. Define parameters in each skill's front matter and pass values through the shared context:
457
-
458
- ```markdown title="prompts/branded.md"
459
- ---
460
- description: Brand-aware responses
461
- parameters:
462
- company_name: null
463
- ---
464
- You represent <%= company_name %>. Always maintain brand voice.
465
- ```
466
-
467
- ```ruby
468
- robot = RobotLab.build(
469
- template: :support,
470
- skills: [:branded],
471
- context: { company_name: "Acme Corp" } # shared with all skills
472
- )
473
- ```
474
-
475
- ## Adding Tools
476
-
477
- Give robots capabilities via the `local_tools:` parameter. Tools can be `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances:
478
-
479
- ```ruby
480
- robot = RobotLab.build(
481
- name: "order_assistant",
482
- system_prompt: "You help customers with orders.",
483
- local_tools: [OrderLookup, InventoryCheck]
484
- )
485
-
486
- result = robot.run("Where is order 12345?", tools: :inherit)
487
- ```
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
-
520
- See the [Using Tools](using-tools.md) guide for details on defining tools.
521
-
522
- ## MCP Configuration
523
-
524
- Connect to MCP (Model Context Protocol) servers via the `mcp:` parameter:
525
-
526
- ```ruby
527
- robot = RobotLab.build(
528
- name: "coder",
529
- template: :developer,
530
- mcp: [
531
- {
532
- name: "filesystem",
533
- transport: { type: "stdio", command: "mcp-server-fs", args: ["--root", "/data"] }
534
- }
535
- ]
536
- )
537
- ```
538
-
539
- MCP configuration supports hierarchical resolution:
540
-
541
- | Value | Behavior |
542
- |-------|----------|
543
- | `:none` | No MCP servers (default) |
544
- | `:inherit` | Use parent network/config MCP servers |
545
- | `[...]` | Explicit array of server configurations |
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
-
555
- See the [MCP Integration](mcp-integration.md) guide for transport types and advanced patterns.
556
-
557
- ## Chaining Configuration
558
-
559
- Robots support `with_*` method chaining for runtime reconfiguration. Each method returns `self` for fluent usage:
560
-
561
- ```ruby
562
- robot = RobotLab.build(name: "bot")
563
-
564
- result = robot
565
- .with_instructions("Be concise and direct.")
566
- .with_temperature(0.9)
567
- .with_model("claude-sonnet-4")
568
- .run("Summarize quantum computing in one sentence.")
569
- ```
570
-
571
- ### Available Chain Methods
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
-
575
- | Method | Description |
576
- |--------|-------------|
577
- | `with_model(id)` | Change the LLM model |
578
- | `with_instructions(text)` | Set system instructions |
579
- | `with_temperature(val)` | Set temperature |
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 |
583
- | `with_tool(tool)` | Add a single tool |
584
- | `with_tools(*tools)` | Add multiple tools |
585
- | `with_schema(schema)` | Set structured output schema |
586
- | `with_thinking(config)` | Enable extended thinking |
587
- | `with_template(id, **ctx)` | Apply a prompt template |
588
- | `with_bus(bus)` | Connect to a message bus (creates one if nil) |
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
-
602
- ## Running Robots
603
-
604
- ### Standalone
605
-
606
- Run a robot directly with a string message:
607
-
608
- ```ruby
609
- result = robot.run("Hello!")
610
- puts result.last_text_content
611
- ```
612
-
613
- The `run` method returns a `RobotResult` with:
614
-
615
- ```ruby
616
- result.last_text_content # => "Hi there! How can I help?"
617
- result.reply # => alias for last_text_content
618
- result.output # => [TextMessage] built from the final response text
619
- result.tool_calls # => Array of tool call results (see note)
620
- result.robot_name # => "assistant"
621
- result.stop_reason # => always nil (see note)
622
- result.input_tokens # => Integer
623
- result.output_tokens # => Integer
624
- result.duration # => Float (elapsed seconds, set in pipeline execution)
625
- result.raw # => raw LLM response object
626
- ```
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
-
648
- ### With Runtime Memory
649
-
650
- Inject memory values for a single run:
651
-
652
- ```ruby
653
- result = robot.run("What's my account status?", memory: { user_id: 123 })
654
- ```
655
-
656
- ### In a Network
657
-
658
- Run through a network for orchestration:
659
-
660
- ```ruby
661
- network = RobotLab.create_network(name: "pipeline") do
662
- task :assistant, robot, depends_on: :none
663
- end
664
-
665
- result = network.run(message: "Hello!")
666
- puts result.value.last_text_content
667
- ```
668
-
669
- ### With Streaming
670
-
671
- Stream LLM content in real-time using a stored callback, a per-call block, or both. Each receives a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) object — use `chunk.content` for the text delta. Chunks also carry `model_id`, `tool_calls`, `thinking`, and token usage on the final chunk. See the [Streaming API reference](../api/core/robot.md#streaming) for the full chunk interface.
672
-
673
- **Stored callback** — wired at build time, fires on every `run()`:
674
-
675
- ```ruby
676
- robot = RobotLab.build(
677
- name: "assistant",
678
- system_prompt: "You are helpful.",
679
- on_content: ->(chunk) { print chunk.content }
680
- )
681
- robot.run("Tell me a story") # streams automatically
682
- ```
683
-
684
- **Per-call block** — passed to `run()`:
685
-
686
- ```ruby
687
- robot.run("Tell me a story") { |chunk| print chunk.content }
688
- ```
689
-
690
- **Both together** — stored fires first, then the block:
691
-
692
- ```ruby
693
- robot = RobotLab.build(
694
- name: "assistant",
695
- system_prompt: "You are helpful.",
696
- on_content: ->(chunk) { log_chunk(chunk.content) }
697
- )
698
- robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
699
- ```
700
-
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:
702
-
703
- ```ruby
704
- config = RobotLab::RunConfig.new(
705
- on_content: ->(chunk) { broadcast(chunk.content) }
706
- )
707
- robot = RobotLab.build(name: "bot", system_prompt: "...", config: config)
708
- ```
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
-
717
- You can also monitor tool activity via callbacks:
718
-
719
- ```ruby
720
- robot = RobotLab.build(
721
- name: "assistant",
722
- system_prompt: "...",
723
- on_tool_call: ->(tool_call) { puts "Calling: #{tool_call.name}" },
724
- on_tool_result: ->(result) { puts "Result: #{result}" }
725
- )
726
- ```
727
-
728
- ## Robot Patterns
729
-
730
- ### Classifier Robot
731
-
732
- Route requests to specialized handlers. Subclass `RobotLab::Robot` and override `call` for custom pipeline behavior:
733
-
734
- ```ruby
735
- class ClassifierRobot < RobotLab::Robot
736
- def call(result)
737
- context = extract_run_context(result)
738
- message = context.delete(:message)
739
- robot_result = run(message, **context)
740
-
741
- new_result = result
742
- .with_context(@name.to_sym, robot_result)
743
- .continue(robot_result)
744
-
745
- category = robot_result.last_text_content.to_s.strip.downcase
746
-
747
- case category
748
- when /billing/ then new_result.activate(:billing)
749
- when /technical/ then new_result.activate(:technical)
750
- else new_result.activate(:general)
751
- end
752
- end
753
- end
754
-
755
- classifier = ClassifierRobot.new(
756
- name: "classifier",
757
- system_prompt: <<~PROMPT
758
- Classify the user's message into exactly one category:
759
- - billing
760
- - technical
761
- - general
762
- Respond with only the category name, nothing else.
763
- PROMPT
764
- )
765
- ```
766
-
767
- ### Specialist Robot
768
-
769
- Handle specific domains with template and tools:
770
-
771
- ```ruby
772
- billing_specialist = RobotLab.build(
773
- name: "billing_specialist",
774
- description: "Handles billing and payment inquiries",
775
- template: :billing,
776
- context: { department: "billing" },
777
- local_tools: [InvoiceLookup, RefundProcessor]
778
- )
779
-
780
- billing_specialist.run("Refund order 12345", tools: :inherit)
781
- ```
782
-
783
- In a network, the equivalent opt-in is on the task: `task :billing, billing_specialist, tools: :inherit, depends_on: :optional`.
784
-
785
- ### Summarizer Robot
786
-
787
- Condense information:
788
-
789
- ```ruby
790
- summarizer = RobotLab.build(
791
- name: "summarizer",
792
- description: "Summarizes conversations and documents",
793
- system_prompt: <<~PROMPT
794
- Create concise summaries of the provided content.
795
- Focus on key points and actionable items.
796
- Use bullet points for clarity.
797
- PROMPT
798
- )
799
- ```
800
-
801
- ### Bus-Connected Robot
802
-
803
- Enable bidirectional communication between robots using a message bus. This pattern supports negotiation loops and convergence:
804
-
805
- ```ruby
806
- bus = TypedBus::MessageBus.new
807
-
808
- class Comedian < RobotLab::Robot
809
- def initialize(bus:)
810
- super(name: "bob", template: :comedian, bus: bus)
811
- on_message do |message|
812
- joke = run(message.content.to_s).last_text_content.strip
813
- send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
814
- end
815
- end
816
- end
817
-
818
- class ComedyCritic < RobotLab::Robot
819
- def initialize(bus:)
820
- super(name: "alice", template: :comedy_critic, bus: bus)
821
- @accepted = false
822
- on_message do |message|
823
- verdict = run("Evaluate: #{message.content}").last_text_content.strip
824
- @accepted = verdict.start_with?("FUNNY")
825
- send_message(to: :bob, content: "Try again.") unless @accepted
826
- end
827
- end
828
- attr_reader :accepted
829
- end
830
-
831
- bob = Comedian.new(bus: bus)
832
- alice = ComedyCritic.new(bus: bus)
833
- alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
834
- ```
835
-
836
- The `on_message` block arity controls delivery handling:
837
- - **1 argument** `|message|` — auto-acknowledges before calling
838
- - **2 arguments** `|delivery, message|` — manual `delivery.ack!` / `delivery.nack!`
839
-
840
- See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
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
-
875
- ### Spawning Robots Dynamically
876
-
877
- Create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
878
-
879
- ```ruby
880
- class Dispatcher < RobotLab::Robot
881
- attr_reader :spawned
882
-
883
- def initialize(bus: nil)
884
- super(name: "dispatcher", template: :dispatcher, bus: bus)
885
- @spawned = {}
886
-
887
- on_message do |message|
888
- puts "#{message.from} replied: #{message.content.to_s.lines.first&.strip}"
889
- end
890
- end
891
-
892
- def dispatch(question)
893
- # Ask LLM what specialist to create
894
- plan = run(question).last_text_content.strip
895
- role, instruction = plan.split("\n", 2)
896
- role = role.strip.downcase.gsub(/\s+/, "_")
897
-
898
- # Spawn (or reuse) a specialist
899
- specialist = @spawned[role] ||= spawn(
900
- name: role,
901
- system_prompt: instruction&.strip || "You are a helpful #{role}."
902
- )
903
-
904
- # Have the specialist answer and reply
905
- answer = specialist.run(question).last_text_content.strip
906
- specialist.send_message(to: :dispatcher, content: answer)
907
- end
908
- end
909
- ```
910
-
911
- Key features of `spawn`:
912
-
913
- - Creates a child robot on the same bus as the parent
914
- - Creates a bus lazily if the parent doesn't have one
915
- - Spawned robots can immediately send and receive messages
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
918
-
919
- Robots can also join a bus after creation:
920
-
921
- ```ruby
922
- bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
923
- bot.with_bus(existing_bus) # now connected and can send/receive messages
924
- ```
925
-
926
- ## Context Window Compression
927
-
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.
929
-
930
- ```ruby
931
- # Default settings: protect 3 most-recent turn pairs, drop anything below 0.2
932
- robot.compress_history
933
-
934
- # Tune all thresholds
935
- robot.compress_history(
936
- recent_turns: 5, # number of recent user+assistant pairs to always keep
937
- keep_threshold: 0.6, # turns with score >= this are kept verbatim (default 0.6)
938
- drop_threshold: 0.2 # turns with score < this are dropped (default 0.2)
939
- )
940
- ```
941
-
942
- Medium-relevance turns (between thresholds) are dropped by default. Pass a `summarizer:` callable to replace them with a one-sentence summary instead:
943
-
944
- ```ruby
945
- summarizer = RobotLab.build(name: "summarizer", system_prompt: "Summarize concisely in one sentence.")
946
-
947
- robot.compress_history(
948
- summarizer: ->(text) { summarizer.run("Summarize: #{text}").reply }
949
- )
950
- ```
951
-
952
- **What is always preserved regardless of score:**
953
- - System messages
954
- - Tool call/result message pairs (dropping half would corrupt the conversation)
955
- - The most recent `recent_turns` user+assistant pairs
956
-
957
- Requires the `classifier` gem (`~> 2.3`):
958
-
959
- ```ruby
960
- gem "classifier", "~> 2.3"
961
- ```
962
-
963
- ## Convergence Detection
964
-
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`.
966
-
967
- ```ruby
968
- # Check the similarity score directly (returns Float 0.0..1.0)
969
- score = RobotLab::Convergence.similarity(result_a.reply, result_b.reply)
970
-
971
- # Boolean convergence check (default threshold: 0.85)
972
- RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
973
-
974
- # Custom threshold
975
- RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
976
- ```
977
-
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:
979
-
980
- ```ruby
981
- verifier_a = RobotLab.build(name: "verifier_a", system_prompt: "Verify the answer.")
982
- verifier_b = RobotLab.build(name: "verifier_b", system_prompt: "Independently verify the answer.")
983
- reconciler = RobotLab.build(name: "reconciler", system_prompt: "Reconcile conflicting answers.")
984
-
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
991
-
992
- result.activate(:reconciler)
993
- end
994
- end
995
-
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
1001
- end
1002
-
1003
- network.run(message: "Is the deployment healthy?").activated_steps
1004
- # => [] when they agreed, [:reconciler] when they diverged
1005
- ```
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
-
1013
- Requires the `classifier` gem (`~> 2.3`).
1014
-
1015
- ## Structured Delegation
1016
-
1017
- A robot can delegate a task to another robot using `delegate(to:, task:)`. The result is a `RobotResult` annotated with `delegated_by`, `duration`, and token counts.
1018
-
1019
- ### Synchronous Delegation
1020
-
1021
- The default: blocks the calling robot until the delegatee finishes.
1022
-
1023
- ```ruby
1024
- analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
1025
- manager = RobotLab.build(name: "manager", system_prompt: "Coordinate work.")
1026
-
1027
- result = manager.delegate(to: analyst, task: "Summarize the Q3 report.")
1028
- puts result.reply
1029
- puts "Completed in %.2fs using %d tokens" % [result.duration, result.output_tokens]
1030
- puts result.delegated_by # => "manager"
1031
- ```
1032
-
1033
- ### Asynchronous Delegation (Fan-out)
1034
-
1035
- Pass `async: true` to get a `DelegationFuture` back immediately. Call `.value` to block for the result when you need it.
1036
-
1037
- ```ruby
1038
- writer = RobotLab.build(name: "writer", system_prompt: "Write clearly.")
1039
- analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
1040
- manager = RobotLab.build(name: "manager", system_prompt: "Coordinate.")
1041
-
1042
- # Fan out — both start immediately
1043
- f1 = manager.delegate(to: analyst, task: "Analyze Q3 numbers", async: true)
1044
- f2 = manager.delegate(to: writer, task: "Draft the intro paragraph", async: true)
1045
-
1046
- # ... do other work here ...
1047
-
1048
- # Collect results (blocks if not yet done)
1049
- analysis = f1.value
1050
- draft = f2.value
1051
-
1052
- # With a timeout (raises DelegationFuture::DelegationTimeout)
1053
- result = f1.value(timeout: 30)
1054
- ```
1055
-
1056
- `DelegationFuture` API:
1057
-
1058
- | Method | Description |
1059
- |--------|-------------|
1060
- | `resolved?` | Non-blocking poll — true if completed or errored |
1061
- | `value(timeout: nil)` | Block until done; re-raises any error from the delegatee |
1062
- | `wait` | Alias for `value` |
1063
- | `robot_name` | Name of the robot that was delegated to |
1064
- | `delegated_by` | Name of the robot that created this future |
1065
-
1066
- ## Configuration
1067
-
1068
- RobotLab uses `MywayConfig` for configuration. Read values off the config object directly:
1069
-
1070
- ```ruby
1071
- RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
1072
- RobotLab.config.ruby_llm.request_timeout # => 120
1073
- ```
1074
-
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
- ```
1082
-
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.
1126
-
1127
- ## Best Practices
1128
-
1129
- ### 1. Clear, Focused Prompts
1130
-
1131
- ```ruby
1132
- # Good: Specific and focused
1133
- robot = RobotLab.build(
1134
- name: "reviewer",
1135
- system_prompt: <<~PROMPT
1136
- You are a code reviewer. Review code for:
1137
- - Security vulnerabilities
1138
- - Performance issues
1139
- - Best practice violations
1140
-
1141
- Provide specific line numbers and suggestions.
1142
- PROMPT
1143
- )
1144
-
1145
- # Bad: Vague and unfocused
1146
- robot = RobotLab.build(
1147
- name: "reviewer",
1148
- system_prompt: "You help with code stuff."
1149
- )
1150
- ```
1151
-
1152
- ### 2. Compose Behaviors with Skills
1153
-
1154
- Instead of creating monolithic templates, break behaviors into composable skills:
1155
-
1156
- ```ruby
1157
- robot = RobotLab.build(
1158
- name: "support",
1159
- template: :support,
1160
- skills: [:clarifier, :safety, :json_responder]
1161
- )
1162
- ```
1163
-
1164
- ### 3. Use Templates for Reusable Prompts
1165
-
1166
- Templates keep prompts in version-controlled files and allow parameterization:
1167
-
1168
- ```ruby
1169
- robot = RobotLab.build(
1170
- name: "support",
1171
- template: :support,
1172
- context: { company: "TechCo", language: "English" }
1173
- )
1174
- ```
1175
-
1176
- ### 4. Handle Tool Errors Gracefully
1177
-
1178
- `RobotLab::Tool` automatically catches exceptions and returns plain-text errors to the LLM. For domain-specific error handling, catch known exceptions in `execute` and return structured data. See [Using Tools: Error Handling](using-tools.md#error-handling) for details.
1179
-
1180
- ## Next Steps
1181
-
1182
- - [Creating Networks](creating-networks.md) - Orchestrate multiple robots
1183
- - [Message Bus](../architecture/core-concepts.md#message-bus) - Bidirectional robot communication
1184
- - [Dynamic Spawning](../architecture/core-concepts.md#dynamic-spawning) - Robots creating robots at runtime
1185
- - [Using Tools](using-tools.md) - Advanced tool patterns
1186
- - [Memory Guide](memory.md) - Share data between runs and robots
1187
- - [API Reference: Robot](../api/core/robot.md) - Complete API documentation