robot_lab 0.0.7 → 0.0.9

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -3
  3. data/README.md +125 -26
  4. data/Rakefile +39 -1
  5. data/docs/api/core/robot.md +284 -8
  6. data/docs/api/core/tool.md +28 -2
  7. data/docs/api/mcp/client.md +1 -0
  8. data/docs/api/mcp/server.md +27 -8
  9. data/docs/api/mcp/transports.md +21 -6
  10. data/docs/architecture/core-concepts.md +1 -1
  11. data/docs/architecture/robot-execution.md +20 -2
  12. data/docs/concepts.md +11 -3
  13. data/docs/examples/index.md +1 -1
  14. data/docs/examples/rails-application.md +1 -1
  15. data/docs/guides/building-robots.md +245 -11
  16. data/docs/guides/creating-networks.md +18 -0
  17. data/docs/guides/mcp-integration.md +74 -2
  18. data/docs/guides/rails-integration.md +145 -17
  19. data/docs/guides/using-tools.md +45 -4
  20. data/docs/index.md +62 -6
  21. data/examples/05_streaming.rb +113 -94
  22. data/examples/14_rusty_circuit/.gitignore +1 -0
  23. data/examples/14_rusty_circuit/open_mic.rb +1 -1
  24. data/examples/17_skills.rb +242 -0
  25. data/examples/18_rails/.envrc +3 -0
  26. data/examples/18_rails/.gitignore +5 -0
  27. data/examples/18_rails/Gemfile +10 -0
  28. data/examples/18_rails/README.md +48 -0
  29. data/examples/18_rails/Rakefile +4 -0
  30. data/examples/18_rails/app/controllers/application_controller.rb +4 -0
  31. data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
  32. data/examples/18_rails/app/jobs/application_job.rb +4 -0
  33. data/examples/18_rails/app/jobs/robot_run_job.rb +79 -0
  34. data/examples/18_rails/app/models/application_record.rb +5 -0
  35. data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
  36. data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
  37. data/examples/18_rails/app/robots/chat_robot.rb +14 -0
  38. data/examples/18_rails/app/tools/time_tool.rb +9 -0
  39. data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
  40. data/examples/18_rails/app/views/chat/index.html.erb +67 -0
  41. data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
  42. data/examples/18_rails/bin/dev +7 -0
  43. data/examples/18_rails/bin/rails +6 -0
  44. data/examples/18_rails/bin/setup +15 -0
  45. data/examples/18_rails/config/application.rb +33 -0
  46. data/examples/18_rails/config/cable.yml +2 -0
  47. data/examples/18_rails/config/database.yml +5 -0
  48. data/examples/18_rails/config/environment.rb +4 -0
  49. data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
  50. data/examples/18_rails/config/routes.rb +6 -0
  51. data/examples/18_rails/config.ru +4 -0
  52. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
  53. data/examples/README.md +30 -0
  54. data/examples/prompts/audit_trail.md +8 -0
  55. data/examples/prompts/incident_responder.md +18 -0
  56. data/examples/prompts/parameterized_main_test.md +6 -0
  57. data/examples/prompts/pii_redactor.md +7 -0
  58. data/examples/prompts/runbook_protocol.md +12 -0
  59. data/examples/prompts/skill_a_test.md +4 -0
  60. data/examples/prompts/skill_b_test.md +4 -0
  61. data/examples/prompts/skill_config_test.md +5 -0
  62. data/examples/prompts/skill_cycle_a_test.md +6 -0
  63. data/examples/prompts/skill_cycle_b_test.md +6 -0
  64. data/examples/prompts/skill_description_test.md +4 -0
  65. data/examples/prompts/skill_leaf_test.md +4 -0
  66. data/examples/prompts/skill_nested_test.md +6 -0
  67. data/examples/prompts/skill_refs_main_test.md +6 -0
  68. data/examples/prompts/skill_self_ref_test.md +6 -0
  69. data/examples/prompts/skill_with_params_test.md +6 -0
  70. data/examples/prompts/sre_compliance.md +10 -0
  71. data/examples/prompts/structured_output.md +7 -0
  72. data/examples/prompts/template_with_skills_test.md +6 -0
  73. data/lib/generators/robot_lab/install_generator.rb +12 -0
  74. data/lib/generators/robot_lab/templates/initializer.rb.tt +36 -22
  75. data/lib/generators/robot_lab/templates/job.rb.tt +92 -0
  76. data/lib/generators/robot_lab/templates/robot.rb.tt +6 -21
  77. data/lib/generators/robot_lab/templates/robot_test.rb.tt +5 -3
  78. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +41 -35
  79. data/lib/robot_lab/mcp/client.rb +6 -4
  80. data/lib/robot_lab/mcp/server.rb +21 -3
  81. data/lib/robot_lab/mcp/transports/base.rb +10 -2
  82. data/lib/robot_lab/mcp/transports/stdio.rb +52 -26
  83. data/lib/robot_lab/{rails → rails_integration}/engine.rb +1 -1
  84. data/lib/robot_lab/{rails → rails_integration}/railtie.rb +1 -1
  85. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +72 -0
  86. data/lib/robot_lab/robot/mcp_management.rb +61 -4
  87. data/lib/robot_lab/robot/template_rendering.rb +181 -4
  88. data/lib/robot_lab/robot.rb +196 -33
  89. data/lib/robot_lab/robot_result.rb +3 -1
  90. data/lib/robot_lab/run_config.rb +1 -1
  91. data/lib/robot_lab/tool.rb +26 -0
  92. data/lib/robot_lab/version.rb +1 -1
  93. data/lib/robot_lab.rb +6 -4
  94. metadata +55 -19
  95. data/examples/14_rusty_circuit/scout_notes.md +0 -89
data/docs/concepts.md CHANGED
@@ -12,7 +12,10 @@ Each robot has:
12
12
  - **Template**: A `.md` file with YAML front matter managed by prompt_manager, referenced by symbol
13
13
  - **System Prompt**: Inline instructions (can be used alone or combined with a template)
14
14
  - **Model**: The LLM model to use (defaults to `RobotLab.config.ruby_llm.model`)
15
- - **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances
15
+ - **Provider**: Optional LLM provider for local models (Ollama, GPUStack, etc.)
16
+ - **Skills**: Composable template behaviors prepended before the main template
17
+ - **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances (with automatic error handling)
18
+ - **Streaming**: Real-time content via stored `on_content` callback or per-call block
16
19
  - **Memory**: Persistent key-value store across runs
17
20
 
18
21
  ```ruby
@@ -137,7 +140,9 @@ result.continued? # Whether execution continues
137
140
 
138
141
  ## Tool
139
142
 
140
- **Tools** give robots the ability to interact with external systems. There are two patterns for defining tools:
143
+ **Tools** give robots the ability to interact with external systems. `RobotLab::Tool` extends `RubyLLM::Tool` with graceful error handling — if `execute` raises a `StandardError`, the error is caught and returned as a plain-text string (`"Error (tool_name): message"`) so the LLM can reason about it. Critical tools can opt out with `self.raise_on_error = true`.
144
+
145
+ There are two patterns for defining tools:
141
146
 
142
147
  ### RubyLLM::Tool Subclass (Preferred)
143
148
 
@@ -191,12 +196,15 @@ tool = RobotLab::Tool.create(
191
196
  result = robot.run("Hello!")
192
197
 
193
198
  result.last_text_content # => "Hi there!" (String or nil)
199
+ result.reply # => alias for last_text_content
194
200
  result.output # => [TextMessage, ...] array of output messages
195
201
  result.tool_calls # => [] array of tool call results
196
202
  result.robot_name # => "assistant"
197
203
  result.stop_reason # => "end_turn" or nil
198
204
  result.has_tool_calls? # => false
199
205
  result.checksum # => "a1b2c3d4..." (for dedup)
206
+ result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
207
+ result.raw # => raw LLM response object
200
208
  ```
201
209
 
202
210
  ## Memory
@@ -446,7 +454,7 @@ Templates support two categories of front matter keys:
446
454
 
447
455
  **LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
448
456
 
449
- **Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
457
+ **Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
450
458
 
451
459
  ```markdown
452
460
  ---
@@ -29,7 +29,7 @@ These examples show how to use RobotLab for common scenarios, from simple chatbo
29
29
  ### Advanced Examples
30
30
 
31
31
  - [Streaming Responses](basic-chat.md#with-streaming)
32
- - [Persistent Conversations](basic-chat.md#with-conversation-history)
32
+ - [Persistent Conversations](basic-chat.md#with-memory)
33
33
  - [MCP Integration](mcp-server.md)
34
34
  - [Message Bus Communication](#message-bus)
35
35
  - [Spawning Robots](#spawning-robots)
@@ -88,7 +88,7 @@ RobotLab.config.logger #=> Rails.logger
88
88
 
89
89
  ### Rails Engine and Railtie
90
90
 
91
- RobotLab provides both a Rails Engine (`RobotLab::Rails::Engine`) and a Railtie (`RobotLab::Rails::Railtie`). These are loaded automatically when Rails is detected. The Engine isolates the RobotLab namespace and adds `app/robots` and `app/tools` to the autoload paths. The Railtie loads rake tasks and generators.
91
+ RobotLab provides both a Rails Engine (`RobotLab::RailsIntegration::Engine`) and a Railtie (`RobotLab::RailsIntegration::Railtie`). These are loaded automatically when Rails is detected. The Engine isolates the RobotLab namespace and adds `app/robots` and `app/tools` to the autoload paths. The Railtie loads rake tasks and generators.
92
92
 
93
93
  ## Models
94
94
 
@@ -50,6 +50,21 @@ robot = RobotLab.build(
50
50
  )
51
51
  ```
52
52
 
53
+ ### Provider
54
+
55
+ For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter. This tells RubyLLM to skip model validation and connect directly:
56
+
57
+ ```ruby
58
+ robot = RobotLab.build(
59
+ name: "local_bot",
60
+ model: "llama3.2",
61
+ provider: :ollama,
62
+ system_prompt: "You are a helpful assistant."
63
+ )
64
+ ```
65
+
66
+ When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`.
67
+
53
68
  ### System Prompt
54
69
 
55
70
  An inline string that defines the robot's personality and behavior:
@@ -125,6 +140,7 @@ The following YAML front matter keys are applied to the robot's chat automatical
125
140
  | `description` | Human-readable description of the robot |
126
141
  | `tools` | Array of tool class names (resolved via `Object.const_get`) |
127
142
  | `mcp` | Array of MCP server configurations |
143
+ | `skills` | Array of skill template symbols to prepend (see [Composable Skills](#composable-skills)) |
128
144
 
129
145
  Constructor-provided values always take precedence over frontmatter values.
130
146
 
@@ -221,6 +237,173 @@ robot = RobotLab.build(
221
237
  )
222
238
  ```
223
239
 
240
+ ## Composable Skills
241
+
242
+ 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.
243
+
244
+ ### Why Skills?
245
+
246
+ Consider a support agent that needs to:
247
+
248
+ - Ask clarifying questions before acting
249
+ - Detect customer sentiment
250
+ - Respond in structured JSON
251
+
252
+ 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.
253
+
254
+ ### Defining a Skill
255
+
256
+ A skill is a standard `.md` template file. There is no special syntax — any template can be used as a skill:
257
+
258
+ ```markdown title="prompts/clarifier.md"
259
+ ---
260
+ description: Ask clarifying questions before acting
261
+ ---
262
+ Before answering, consider whether the user's request is ambiguous.
263
+ If so, ask one focused clarifying question before proceeding.
264
+ ```
265
+
266
+ ```markdown title="prompts/json_responder.md"
267
+ ---
268
+ description: Respond in structured JSON
269
+ temperature: 0.2
270
+ ---
271
+ Always respond with valid JSON. Use this structure:
272
+ {"answer": "...", "confidence": 0.0-1.0, "sources": [...]}
273
+ ```
274
+
275
+ ### Using Skills via Constructor
276
+
277
+ Pass `skills:` as a symbol or array of symbols:
278
+
279
+ ```ruby
280
+ # Single skill
281
+ robot = RobotLab.build(
282
+ name: "bot",
283
+ template: :support,
284
+ skills: :clarifier
285
+ )
286
+
287
+ # Multiple skills
288
+ robot = RobotLab.build(
289
+ name: "bot",
290
+ template: :support,
291
+ skills: [:clarifier, :json_responder],
292
+ context: { company: "Acme Corp" }
293
+ )
294
+ ```
295
+
296
+ The resulting system prompt is composed in order: clarifier body, then json_responder body, then the main support template body.
297
+
298
+ ### Using Skills via Front Matter
299
+
300
+ Templates can declare skills directly in their front matter:
301
+
302
+ ```markdown title="prompts/smart_support.md"
303
+ ---
304
+ description: Support agent with built-in skills
305
+ skills:
306
+ - clarifier
307
+ - json_responder
308
+ parameters:
309
+ company: null
310
+ ---
311
+ You are a support agent for <%= company %>.
312
+ Help customers with their inquiries.
313
+ ```
314
+
315
+ ```ruby
316
+ # Skills are loaded from front matter automatically
317
+ robot = RobotLab.build(
318
+ template: :smart_support,
319
+ context: { company: "Acme Corp" }
320
+ )
321
+ ```
322
+
323
+ Constructor `skills:` and front matter `skills:` are combined — constructor skills are processed first, then front matter skills.
324
+
325
+ ### Nested Skills
326
+
327
+ Skills can reference other skills, enabling layered composition:
328
+
329
+ ```markdown title="prompts/safety.md"
330
+ ---
331
+ description: Safety guidelines
332
+ skills:
333
+ - content_filter
334
+ - pii_redactor
335
+ ---
336
+ Follow all safety guidelines when responding.
337
+ ```
338
+
339
+ Nested skills are expanded depth-first. For the example above, the prompt order would be: content_filter, pii_redactor, safety, then the main template.
340
+
341
+ ### Cycle Detection
342
+
343
+ 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.
344
+
345
+ ### Config Cascade
346
+
347
+ Skills can include LLM configuration in their front matter. Config cascades in processing order — later values override earlier ones:
348
+
349
+ ```markdown title="prompts/creative_mode.md"
350
+ ---
351
+ description: Enable creative responses
352
+ temperature: 0.9
353
+ top_p: 0.95
354
+ ---
355
+ Be creative and imaginative in your responses.
356
+ ```
357
+
358
+ ```ruby
359
+ robot = RobotLab.build(
360
+ name: "writer",
361
+ template: :article_writer,
362
+ skills: [:creative_mode]
363
+ )
364
+ # temperature is 0.9 from the skill (unless the main template or constructor overrides it)
365
+ ```
366
+
367
+ The precedence order (highest wins):
368
+
369
+ 1. Constructor kwargs (`temperature: 0.3`)
370
+ 2. Main template front matter
371
+ 3. Later skills override earlier skills
372
+ 4. First skill in the list
373
+
374
+ ### Skills Without a Main Template
375
+
376
+ Skills work without a main template — useful for quick composition:
377
+
378
+ ```ruby
379
+ robot = RobotLab.build(
380
+ name: "safe_bot",
381
+ skills: [:safety, :json_responder],
382
+ system_prompt: "You answer questions about our product."
383
+ )
384
+ ```
385
+
386
+ ### Shared Context
387
+
388
+ 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:
389
+
390
+ ```markdown title="prompts/branded.md"
391
+ ---
392
+ description: Brand-aware responses
393
+ parameters:
394
+ company_name: null
395
+ ---
396
+ You represent <%= company_name %>. Always maintain brand voice.
397
+ ```
398
+
399
+ ```ruby
400
+ robot = RobotLab.build(
401
+ template: :support,
402
+ skills: [:branded],
403
+ context: { company_name: "Acme Corp" } # shared with all skills
404
+ )
405
+ ```
406
+
224
407
  ## Adding Tools
225
408
 
226
409
  Give robots capabilities via the `local_tools:` parameter. Tools can be `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances:
@@ -311,10 +494,13 @@ The `run` method returns a `RobotResult` with:
311
494
 
312
495
  ```ruby
313
496
  result.last_text_content # => "Hi there! How can I help?"
497
+ result.reply # => alias for last_text_content
314
498
  result.output # => Array of output messages
315
499
  result.tool_calls # => Array of tool call results
316
500
  result.robot_name # => "assistant"
317
501
  result.stop_reason # => stop reason from the LLM
502
+ result.duration # => Float (elapsed seconds, set in pipeline execution)
503
+ result.raw # => raw LLM response object
318
504
  ```
319
505
 
320
506
  ### With Runtime Memory
@@ -340,18 +526,54 @@ puts result.value.last_text_content
340
526
 
341
527
  ### With Streaming
342
528
 
343
- Stream responses in real-time by registering callbacks before calling `run`:
529
+ 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.
530
+
531
+ **Stored callback** — wired at build time, fires on every `run()`:
344
532
 
345
533
  ```ruby
346
- robot.on_new_message do |message|
347
- print message.content if message.content
348
- end
534
+ robot = RobotLab.build(
535
+ name: "assistant",
536
+ system_prompt: "You are helpful.",
537
+ on_content: ->(chunk) { print chunk.content }
538
+ )
539
+ robot.run("Tell me a story") # streams automatically
540
+ ```
349
541
 
350
- robot.on_tool_call do |tool_call|
351
- puts "\nCalling tool: #{tool_call.name}"
352
- end
542
+ **Per-call block** — passed to `run()`:
543
+
544
+ ```ruby
545
+ robot.run("Tell me a story") { |chunk| print chunk.content }
546
+ ```
547
+
548
+ **Both together** — stored fires first, then the block:
549
+
550
+ ```ruby
551
+ robot = RobotLab.build(
552
+ name: "assistant",
553
+ system_prompt: "You are helpful.",
554
+ on_content: ->(chunk) { log_chunk(chunk.content) }
555
+ )
556
+ robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
557
+ ```
558
+
559
+ The `on_content` callback participates in the RunConfig cascade, so it can be set at the config level and inherited by robots:
560
+
561
+ ```ruby
562
+ config = RobotLab::RunConfig.new(
563
+ on_content: ->(chunk) { broadcast(chunk.content) }
564
+ )
565
+ robot = RobotLab.build(name: "bot", system_prompt: "...", config: config)
566
+ ```
353
567
 
354
- result = robot.run("Tell me a story")
568
+ You can also monitor tool activity via callbacks:
569
+
570
+ ```ruby
571
+ robot = RobotLab.build(
572
+ name: "assistant",
573
+ system_prompt: "...",
574
+ on_tool_call: ->(tool_call) { puts "Calling: #{tool_call.name}" },
575
+ on_tool_result: ->(result) { puts "Result: #{result}" }
576
+ )
355
577
  ```
356
578
 
357
579
  ## Robot Patterns
@@ -556,7 +778,19 @@ robot = RobotLab.build(
556
778
  )
557
779
  ```
558
780
 
559
- ### 2. Use Templates for Reusable Prompts
781
+ ### 2. Compose Behaviors with Skills
782
+
783
+ Instead of creating monolithic templates, break behaviors into composable skills:
784
+
785
+ ```ruby
786
+ robot = RobotLab.build(
787
+ name: "support",
788
+ template: :support,
789
+ skills: [:clarifier, :safety, :json_responder]
790
+ )
791
+ ```
792
+
793
+ ### 3. Use Templates for Reusable Prompts
560
794
 
561
795
  Templates keep prompts in version-controlled files and allow parameterization:
562
796
 
@@ -568,9 +802,9 @@ robot = RobotLab.build(
568
802
  )
569
803
  ```
570
804
 
571
- ### 3. Handle Tool Errors Gracefully
805
+ ### 4. Handle Tool Errors Gracefully
572
806
 
573
- See [Using Tools: Error Handling](using-tools.md#error-handling) for patterns.
807
+ `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.
574
808
 
575
809
  ## Next Steps
576
810
 
@@ -291,6 +291,24 @@ network = RobotLab.create_network(name: "multi_analysis") do
291
291
  end
292
292
  ```
293
293
 
294
+ ### Pipeline Error Resilience
295
+
296
+ When a robot raises an exception during pipeline execution, the error is caught and wrapped in a `RobotResult` with the error message as content. This ensures one failing robot does not crash the entire network:
297
+
298
+ ```ruby
299
+ # If billing_robot raises an error, the network continues
300
+ # The error is available in the result context:
301
+ result = network.run(message: "Process this")
302
+ billing_result = result.context[:billing]
303
+
304
+ if billing_result&.last_text_content&.start_with?("Error:")
305
+ puts "Billing failed: #{billing_result.last_text_content}"
306
+ puts "Took: #{billing_result.duration}s"
307
+ end
308
+ ```
309
+
310
+ Each robot's `RobotResult` includes a `duration` field (elapsed seconds) that is set automatically during pipeline execution, even for errored results.
311
+
294
312
  ### Conditional Continuation
295
313
 
296
314
  A robot can halt execution early:
@@ -101,6 +101,26 @@ Global (RobotLab.config.mcp)
101
101
  -> Runtime (robot.run("msg", mcp: [...]))
102
102
  ```
103
103
 
104
+ ## Timeout Configuration
105
+
106
+ All transports support a configurable request timeout. The default is 15 seconds. Set a custom timeout at the server level:
107
+
108
+ ```ruby
109
+ robot = RobotLab.build(
110
+ name: "patient_bot",
111
+ system_prompt: "You help with slow operations.",
112
+ mcp: [
113
+ {
114
+ name: "heavy_server",
115
+ transport: { type: "stdio", command: "heavy-mcp-server" },
116
+ timeout: 60 # seconds
117
+ }
118
+ ]
119
+ )
120
+ ```
121
+
122
+ Values >= 1000 are auto-converted from milliseconds to seconds. The minimum timeout is 1 second.
123
+
104
124
  ## Transport Types
105
125
 
106
126
  ### Stdio Transport
@@ -290,6 +310,50 @@ client.list_resources # => Array of resource definitions
290
310
  client.disconnect
291
311
  ```
292
312
 
313
+ ## Connection Resilience
314
+
315
+ ### Eager Connection
316
+
317
+ By default, MCP connections are lazy — established on the first `run()` call. Use `connect_mcp!` to connect early:
318
+
319
+ ```ruby
320
+ robot = RobotLab.build(
321
+ name: "assistant",
322
+ system_prompt: "You help with tasks.",
323
+ mcp: [
324
+ { name: "github", transport: { type: "stdio", command: "mcp-server-github" } },
325
+ { name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
326
+ ]
327
+ )
328
+
329
+ robot.connect_mcp!
330
+
331
+ # Check which servers failed
332
+ if robot.failed_mcp_server_names.any?
333
+ puts "Failed to connect: #{robot.failed_mcp_server_names.join(', ')}"
334
+ end
335
+ ```
336
+
337
+ ### Automatic Retry
338
+
339
+ Failed MCP servers are automatically retried on subsequent `run()` calls. If a server was down when the robot first connected, it will be retried transparently:
340
+
341
+ ```ruby
342
+ robot.run("First message") # github connects, filesystem fails
343
+ # ... filesystem comes back up ...
344
+ robot.run("Second message") # filesystem retried and connects
345
+ ```
346
+
347
+ ### Injecting External MCP Clients
348
+
349
+ Host applications that manage MCP connections externally can inject pre-connected clients into a robot:
350
+
351
+ ```ruby
352
+ robot.inject_mcp!(clients: my_clients, tools: my_tools)
353
+ ```
354
+
355
+ This skips the normal connection process and marks the robot as MCP-initialized.
356
+
293
357
  ## Error Handling
294
358
 
295
359
  ### Connection Errors
@@ -302,8 +366,16 @@ rescue RobotLab::MCPError => e
302
366
  end
303
367
  ```
304
368
 
305
- !!! tip
306
- MCP connection failures are logged as warnings but do not raise errors by default. The robot will continue without MCP tools if a server is unreachable.
369
+ MCP connection failures are logged as warnings but do not raise errors by default. The robot will continue without MCP tools if a server is unreachable. One failing server does not prevent other servers from connecting.
370
+
371
+ ### Timeout Errors
372
+
373
+ Stdio transports wrap all blocking I/O with a configurable timeout. If a server does not respond within the timeout period, an `MCPError` is raised with a descriptive message:
374
+
375
+ ```ruby
376
+ # Server that takes too long will raise:
377
+ # RobotLab::MCPError: MCP server 'heavy-server' did not respond within 15s
378
+ ```
307
379
 
308
380
  ## Disconnecting
309
381