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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -3
- data/README.md +125 -26
- data/Rakefile +39 -1
- data/docs/api/core/robot.md +284 -8
- data/docs/api/core/tool.md +28 -2
- data/docs/api/mcp/client.md +1 -0
- data/docs/api/mcp/server.md +27 -8
- data/docs/api/mcp/transports.md +21 -6
- data/docs/architecture/core-concepts.md +1 -1
- data/docs/architecture/robot-execution.md +20 -2
- data/docs/concepts.md +11 -3
- data/docs/examples/index.md +1 -1
- data/docs/examples/rails-application.md +1 -1
- data/docs/guides/building-robots.md +245 -11
- data/docs/guides/creating-networks.md +18 -0
- data/docs/guides/mcp-integration.md +74 -2
- data/docs/guides/rails-integration.md +145 -17
- data/docs/guides/using-tools.md +45 -4
- data/docs/index.md +62 -6
- data/examples/05_streaming.rb +113 -94
- data/examples/14_rusty_circuit/.gitignore +1 -0
- data/examples/14_rusty_circuit/open_mic.rb +1 -1
- data/examples/17_skills.rb +242 -0
- data/examples/18_rails/.envrc +3 -0
- data/examples/18_rails/.gitignore +5 -0
- data/examples/18_rails/Gemfile +10 -0
- data/examples/18_rails/README.md +48 -0
- data/examples/18_rails/Rakefile +4 -0
- data/examples/18_rails/app/controllers/application_controller.rb +4 -0
- data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
- data/examples/18_rails/app/jobs/application_job.rb +4 -0
- data/examples/18_rails/app/jobs/robot_run_job.rb +79 -0
- data/examples/18_rails/app/models/application_record.rb +5 -0
- data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
- data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
- data/examples/18_rails/app/robots/chat_robot.rb +14 -0
- data/examples/18_rails/app/tools/time_tool.rb +9 -0
- data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
- data/examples/18_rails/app/views/chat/index.html.erb +67 -0
- data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
- data/examples/18_rails/bin/dev +7 -0
- data/examples/18_rails/bin/rails +6 -0
- data/examples/18_rails/bin/setup +15 -0
- data/examples/18_rails/config/application.rb +33 -0
- data/examples/18_rails/config/cable.yml +2 -0
- data/examples/18_rails/config/database.yml +5 -0
- data/examples/18_rails/config/environment.rb +4 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
- data/examples/18_rails/config/routes.rb +6 -0
- data/examples/18_rails/config.ru +4 -0
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
- data/examples/README.md +30 -0
- data/examples/prompts/audit_trail.md +8 -0
- data/examples/prompts/incident_responder.md +18 -0
- data/examples/prompts/parameterized_main_test.md +6 -0
- data/examples/prompts/pii_redactor.md +7 -0
- data/examples/prompts/runbook_protocol.md +12 -0
- data/examples/prompts/skill_a_test.md +4 -0
- data/examples/prompts/skill_b_test.md +4 -0
- data/examples/prompts/skill_config_test.md +5 -0
- data/examples/prompts/skill_cycle_a_test.md +6 -0
- data/examples/prompts/skill_cycle_b_test.md +6 -0
- data/examples/prompts/skill_description_test.md +4 -0
- data/examples/prompts/skill_leaf_test.md +4 -0
- data/examples/prompts/skill_nested_test.md +6 -0
- data/examples/prompts/skill_refs_main_test.md +6 -0
- data/examples/prompts/skill_self_ref_test.md +6 -0
- data/examples/prompts/skill_with_params_test.md +6 -0
- data/examples/prompts/sre_compliance.md +10 -0
- data/examples/prompts/structured_output.md +7 -0
- data/examples/prompts/template_with_skills_test.md +6 -0
- data/lib/generators/robot_lab/install_generator.rb +12 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +36 -22
- data/lib/generators/robot_lab/templates/job.rb.tt +92 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +6 -21
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +5 -3
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +41 -35
- data/lib/robot_lab/mcp/client.rb +6 -4
- data/lib/robot_lab/mcp/server.rb +21 -3
- data/lib/robot_lab/mcp/transports/base.rb +10 -2
- data/lib/robot_lab/mcp/transports/stdio.rb +52 -26
- data/lib/robot_lab/{rails → rails_integration}/engine.rb +1 -1
- data/lib/robot_lab/{rails → rails_integration}/railtie.rb +1 -1
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +72 -0
- data/lib/robot_lab/robot/mcp_management.rb +61 -4
- data/lib/robot_lab/robot/template_rendering.rb +181 -4
- data/lib/robot_lab/robot.rb +196 -33
- data/lib/robot_lab/robot_result.rb +3 -1
- data/lib/robot_lab/run_config.rb +1 -1
- data/lib/robot_lab/tool.rb +26 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +6 -4
- metadata +55 -19
- 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
|
-
- **
|
|
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.
|
|
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
|
---
|
data/docs/examples/index.md
CHANGED
|
@@ -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-
|
|
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::
|
|
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
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
###
|
|
805
|
+
### 4. Handle Tool Errors Gracefully
|
|
572
806
|
|
|
573
|
-
See [Using Tools: Error Handling](using-tools.md#error-handling) for
|
|
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
|
-
|
|
306
|
-
|
|
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
|
|