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/api/core/robot.md
CHANGED
|
@@ -23,13 +23,16 @@ Robot.new(
|
|
|
23
23
|
description: nil,
|
|
24
24
|
local_tools: [],
|
|
25
25
|
model: nil,
|
|
26
|
+
provider: nil,
|
|
26
27
|
mcp_servers: [],
|
|
27
28
|
mcp: :none,
|
|
28
29
|
tools: :none,
|
|
29
30
|
on_tool_call: nil,
|
|
30
31
|
on_tool_result: nil,
|
|
32
|
+
on_content: nil,
|
|
31
33
|
enable_cache: true,
|
|
32
34
|
bus: nil,
|
|
35
|
+
skills: nil,
|
|
33
36
|
temperature: nil,
|
|
34
37
|
top_p: nil,
|
|
35
38
|
top_k: nil,
|
|
@@ -52,13 +55,16 @@ Robot.new(
|
|
|
52
55
|
| `description` | `String`, `nil` | `nil` | Human-readable description of what the robot does |
|
|
53
56
|
| `local_tools` | `Array` | `[]` | Tools defined locally (`RubyLLM::Tool` subclasses or `RobotLab::Tool` instances) |
|
|
54
57
|
| `model` | `String`, `nil` | `nil` | LLM model ID (falls back to `RobotLab.config.ruby_llm.model`) |
|
|
58
|
+
| `provider` | `String`, `Symbol`, `nil` | `nil` | LLM provider for local providers (e.g., `:ollama`, `:gpustack`). Automatically sets `assume_model_exists: true` |
|
|
55
59
|
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
56
60
|
| `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
|
|
57
61
|
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name array) |
|
|
58
62
|
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
59
63
|
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
64
|
+
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
60
65
|
| `enable_cache` | `Boolean` | `true` | Whether to enable semantic caching |
|
|
61
66
|
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
|
|
67
|
+
| `skills` | `Symbol`, `Array<Symbol>`, `nil` | `nil` | Skill templates to prepend (see [Skills](#skills)) |
|
|
62
68
|
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
63
69
|
| `temperature` | `Float`, `nil` | `nil` | Controls randomness (0.0-1.0) |
|
|
64
70
|
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
|
|
@@ -80,6 +86,7 @@ robot = RobotLab.build(
|
|
|
80
86
|
context: {},
|
|
81
87
|
enable_cache: true,
|
|
82
88
|
bus: nil, # Optional TypedBus::MessageBus
|
|
89
|
+
skills: nil, # Optional skill templates
|
|
83
90
|
**options # All other Robot.new parameters
|
|
84
91
|
)
|
|
85
92
|
# => RobotLab::Robot
|
|
@@ -95,6 +102,8 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
95
102
|
| `description` | `String`, `nil` | Human-readable description |
|
|
96
103
|
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
97
104
|
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
105
|
+
| `skills` | `Array<Symbol>`, `nil` | Constructor-provided skill template IDs (nil if none) |
|
|
106
|
+
| `provider` | `String`, `nil` | LLM provider name (e.g., `"ollama"`) — set when using local providers |
|
|
98
107
|
| `local_tools` | `Array` | Locally defined tools |
|
|
99
108
|
| `mcp_clients` | `Hash<String, MCP::Client>` | Connected MCP clients, keyed by server name |
|
|
100
109
|
| `mcp_tools` | `Array<Tool>` | Tools discovered from MCP servers |
|
|
@@ -119,7 +128,7 @@ Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO.
|
|
|
119
128
|
### run
|
|
120
129
|
|
|
121
130
|
```ruby
|
|
122
|
-
result = robot.run(message, **kwargs)
|
|
131
|
+
result = robot.run(message, **kwargs, &block)
|
|
123
132
|
# => RobotResult
|
|
124
133
|
```
|
|
125
134
|
|
|
@@ -136,6 +145,9 @@ Primary execution method. Sends a message to the LLM with memory/MCP/tools resol
|
|
|
136
145
|
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override |
|
|
137
146
|
| `tools` | `Symbol`, `Array` | `:none` | Runtime tools override |
|
|
138
147
|
| `**kwargs` | `Hash` | `{}` | Additional keyword arguments passed to `Agent#ask` |
|
|
148
|
+
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
149
|
+
|
|
150
|
+
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
139
151
|
|
|
140
152
|
**Returns:** `RobotResult`
|
|
141
153
|
|
|
@@ -148,8 +160,8 @@ result = robot.run("What is 2+2?")
|
|
|
148
160
|
# With runtime memory
|
|
149
161
|
result = robot.run("Summarize the data", memory: { data: report })
|
|
150
162
|
|
|
151
|
-
# With streaming block
|
|
152
|
-
result = robot.run("Tell me a story") { |
|
|
163
|
+
# With per-call streaming block
|
|
164
|
+
result = robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
153
165
|
|
|
154
166
|
# With runtime overrides
|
|
155
167
|
result = robot.run("Help me", mcp: :none, tools: :none)
|
|
@@ -230,7 +242,9 @@ robot.call(result)
|
|
|
230
242
|
# => SimpleFlow::Result
|
|
231
243
|
```
|
|
232
244
|
|
|
233
|
-
SimpleFlow step interface. Extracts the message from `result.context[:run_params]`, calls `run`, and wraps the output in a continued `SimpleFlow::Result`.
|
|
245
|
+
SimpleFlow step interface. Extracts the message from `result.context[:run_params]`, calls `run`, and wraps the output in a continued `SimpleFlow::Result`. Automatically records `RobotResult#duration` (elapsed seconds).
|
|
246
|
+
|
|
247
|
+
If the robot raises any exception during 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 pipeline.
|
|
234
248
|
|
|
235
249
|
Override this method in subclasses for custom routing logic (e.g., classifiers).
|
|
236
250
|
|
|
@@ -392,6 +406,142 @@ bot.with_bus(bus1) # joins bus1
|
|
|
392
406
|
bot.with_bus(bus2) # leaves bus1, joins bus2
|
|
393
407
|
```
|
|
394
408
|
|
|
409
|
+
### connect_mcp!
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
robot.connect_mcp!
|
|
413
|
+
# => self
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Eagerly connect to configured MCP servers and discover tools. Normally MCP connections are lazy (established on first `run`). Call this to connect early, e.g., to display connection status at startup.
|
|
417
|
+
|
|
418
|
+
**Returns:** `self`
|
|
419
|
+
|
|
420
|
+
### failed_mcp_server_names
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
robot.failed_mcp_server_names
|
|
424
|
+
# => Array<String>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Returns server names that failed to connect. Useful for displaying connection status or deciding whether to retry.
|
|
428
|
+
|
|
429
|
+
### inject_mcp!
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
robot.inject_mcp!(clients: mcp_clients, tools: mcp_tools)
|
|
433
|
+
# => self
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Inject pre-connected MCP clients and their tools into this robot. Used by host applications that manage MCP connections externally and need to pass them to robots without re-connecting.
|
|
437
|
+
|
|
438
|
+
**Parameters:**
|
|
439
|
+
|
|
440
|
+
| Name | Type | Description |
|
|
441
|
+
|------|------|-------------|
|
|
442
|
+
| `clients` | `Hash<String, MCP::Client>` | Connected MCP clients keyed by server name |
|
|
443
|
+
| `tools` | `Array<Tool>` | Tools discovered from the MCP servers |
|
|
444
|
+
|
|
445
|
+
**Returns:** `self`
|
|
446
|
+
|
|
447
|
+
**Example:**
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
# Host app manages MCP connections
|
|
451
|
+
clients = { "github" => github_client }
|
|
452
|
+
tools = github_client.list_tools.map { |t| RobotLab::Tool.from_mcp(t) }
|
|
453
|
+
|
|
454
|
+
robot.inject_mcp!(clients: clients, tools: tools)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### chat
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
robot.chat
|
|
461
|
+
# => RubyLLM::Chat
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Access the underlying `RubyLLM::Chat` instance. Useful for checkpoint/restore operations that need direct access to conversation state.
|
|
465
|
+
|
|
466
|
+
### messages
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
robot.messages
|
|
470
|
+
# => Array<RubyLLM::Message>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Return the conversation messages from the underlying chat.
|
|
474
|
+
|
|
475
|
+
### clear_messages
|
|
476
|
+
|
|
477
|
+
```ruby
|
|
478
|
+
robot.clear_messages(keep_system: true)
|
|
479
|
+
# => self
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Clear conversation messages, optionally keeping the system prompt.
|
|
483
|
+
|
|
484
|
+
**Parameters:**
|
|
485
|
+
|
|
486
|
+
| Name | Type | Default | Description |
|
|
487
|
+
|------|------|---------|-------------|
|
|
488
|
+
| `keep_system` | `Boolean` | `true` | Whether to preserve the system message |
|
|
489
|
+
|
|
490
|
+
**Returns:** `self`
|
|
491
|
+
|
|
492
|
+
### replace_messages
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
robot.replace_messages(messages)
|
|
496
|
+
# => self
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Replace conversation messages with a saved set. Useful for checkpoint/restore workflows.
|
|
500
|
+
|
|
501
|
+
**Parameters:**
|
|
502
|
+
|
|
503
|
+
| Name | Type | Description |
|
|
504
|
+
|------|------|-------------|
|
|
505
|
+
| `messages` | `Array<RubyLLM::Message>` | The messages to restore |
|
|
506
|
+
|
|
507
|
+
**Returns:** `self`
|
|
508
|
+
|
|
509
|
+
**Example:**
|
|
510
|
+
|
|
511
|
+
```ruby
|
|
512
|
+
# Save a checkpoint
|
|
513
|
+
saved = robot.messages.dup
|
|
514
|
+
|
|
515
|
+
# ... later, restore it
|
|
516
|
+
robot.replace_messages(saved)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### chat_provider
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
robot.chat_provider
|
|
523
|
+
# => String or nil
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Return the provider for this robot's chat. Useful for displaying model/provider info without reaching into chat internals.
|
|
527
|
+
|
|
528
|
+
### mcp_client
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
robot.mcp_client("github")
|
|
532
|
+
# => MCP::Client or nil
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Find an MCP client by server name.
|
|
536
|
+
|
|
537
|
+
**Parameters:**
|
|
538
|
+
|
|
539
|
+
| Name | Type | Description |
|
|
540
|
+
|------|------|-------------|
|
|
541
|
+
| `server_name` | `String` | The MCP server name |
|
|
542
|
+
|
|
543
|
+
**Returns:** `MCP::Client` or `nil`
|
|
544
|
+
|
|
395
545
|
### disconnect
|
|
396
546
|
|
|
397
547
|
```ruby
|
|
@@ -408,7 +558,7 @@ robot.to_h
|
|
|
408
558
|
# => Hash
|
|
409
559
|
```
|
|
410
560
|
|
|
411
|
-
Returns a hash representation of the robot including name, description, template, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise).
|
|
561
|
+
Returns a hash representation of the robot including name, description, template, skills, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise). Nil values are compacted out.
|
|
412
562
|
|
|
413
563
|
## Memory Behavior
|
|
414
564
|
|
|
@@ -437,7 +587,7 @@ Front matter supports two categories of keys:
|
|
|
437
587
|
|
|
438
588
|
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the underlying chat.
|
|
439
589
|
|
|
440
|
-
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
590
|
+
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
441
591
|
|
|
442
592
|
| Key | Type | Description |
|
|
443
593
|
|-----|------|-------------|
|
|
@@ -445,6 +595,40 @@ Front matter supports two categories of keys:
|
|
|
445
595
|
| `description` | `String` | Human-readable description |
|
|
446
596
|
| `tools` | `Array<String>` | Tool class names resolved via `Object.const_get` |
|
|
447
597
|
| `mcp` | `Array<Hash>` | MCP server configurations |
|
|
598
|
+
| `skills` | `Array<Symbol>` | Skill templates to prepend (recursive, with cycle detection) |
|
|
599
|
+
|
|
600
|
+
## Skills
|
|
601
|
+
|
|
602
|
+
Skills compose robot behaviors from reusable templates. Each skill is a standard `.md` template whose prompt body is prepended before the main template. Skills are expanded depth-first with automatic cycle detection.
|
|
603
|
+
|
|
604
|
+
**Constructor:** `skills:` accepts `Symbol` or `Array<Symbol>`:
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
robot = RobotLab.build(
|
|
608
|
+
name: "support",
|
|
609
|
+
template: :support,
|
|
610
|
+
skills: [:clarifier, :json_responder]
|
|
611
|
+
)
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Front matter:** templates can declare skills via `skills:` key:
|
|
615
|
+
|
|
616
|
+
```markdown
|
|
617
|
+
---
|
|
618
|
+
skills:
|
|
619
|
+
- clarifier
|
|
620
|
+
- json_responder
|
|
621
|
+
---
|
|
622
|
+
Main template body here.
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Constructor `skills:` and front matter `skills:` are combined (constructor first, then front matter). Skills can nest (a skill can declare its own `skills:` in front matter).
|
|
626
|
+
|
|
627
|
+
**Config cascade:** skill config merges in processing order (deepest first). Later values override earlier. Constructor kwargs always win.
|
|
628
|
+
|
|
629
|
+
**Prompt order:** skill bodies are concatenated in expansion order, followed by the main template body. All are joined with `"\n\n"` and set as system instructions via a single `with_instructions` call.
|
|
630
|
+
|
|
631
|
+
**Cycle detection:** if skills form a cycle, the duplicate is skipped with a logger warning.
|
|
448
632
|
|
|
449
633
|
## RunConfig
|
|
450
634
|
|
|
@@ -463,10 +647,78 @@ robot = RobotLab.build(
|
|
|
463
647
|
robot.config #=> RunConfig with model: "claude-sonnet-4", temperature: 0.9, ...
|
|
464
648
|
```
|
|
465
649
|
|
|
466
|
-
RunConfig fields: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`, `mcp`, `tools`, `on_tool_call`, `on_tool_result`, `bus`, `enable_cache`.
|
|
650
|
+
RunConfig fields: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`, `mcp`, `tools`, `on_tool_call`, `on_tool_result`, `on_content`, `bus`, `enable_cache`.
|
|
467
651
|
|
|
468
652
|
See [Configuration: RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults) for full details.
|
|
469
653
|
|
|
654
|
+
## Streaming
|
|
655
|
+
|
|
656
|
+
Robots support two complementary approaches for streaming LLM content in real-time.
|
|
657
|
+
|
|
658
|
+
### The Chunk Object
|
|
659
|
+
|
|
660
|
+
Both callbacks and blocks receive a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) (subclass of `RubyLLM::Message`). Key accessors:
|
|
661
|
+
|
|
662
|
+
| Accessor | Type | Description |
|
|
663
|
+
|----------|------|-------------|
|
|
664
|
+
| `content` | `String`, `nil` | The text delta for this chunk (`nil` on tool-call or usage-only chunks) |
|
|
665
|
+
| `role` | `Symbol` | Always `:assistant` |
|
|
666
|
+
| `model_id` | `String` | The LLM model ID |
|
|
667
|
+
| `tool_calls` | `Array`, `nil` | Tool call deltas (partial JSON arguments) |
|
|
668
|
+
| `tool_call?` | `Boolean` | Whether this chunk contains tool call data |
|
|
669
|
+
| `thinking` | `Thinking`, `nil` | Extended thinking delta (Anthropic only) |
|
|
670
|
+
| `input_tokens` | `Integer`, `nil` | Input token count (populated on final chunk) |
|
|
671
|
+
| `output_tokens` | `Integer`, `nil` | Output token count (populated on final chunk) |
|
|
672
|
+
| `cached_tokens` | `Integer`, `nil` | Cached prompt tokens (final chunk) |
|
|
673
|
+
|
|
674
|
+
Most chunks carry only `content` (the text delta). The final chunk(s) carry token usage counts. Tool call chunks have `tool_calls` instead of `content`.
|
|
675
|
+
|
|
676
|
+
### Stored Callback (`on_content:`)
|
|
677
|
+
|
|
678
|
+
Wired at build time via constructor or RunConfig. Fires on every `run()` call automatically:
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
robot = RobotLab.build(
|
|
682
|
+
name: "assistant",
|
|
683
|
+
system_prompt: "You are helpful.",
|
|
684
|
+
on_content: ->(chunk) { broadcast(chunk.content) }
|
|
685
|
+
)
|
|
686
|
+
robot.run("Tell me a story") # streams via stored callback
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
The `on_content` callback participates in the RunConfig cascade:
|
|
690
|
+
|
|
691
|
+
```ruby
|
|
692
|
+
config = RobotLab::RunConfig.new(
|
|
693
|
+
on_content: ->(chunk) { log(chunk.content) }
|
|
694
|
+
)
|
|
695
|
+
robot = RobotLab.build(name: "bot", config: config)
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Constructor `on_content:` overrides RunConfig `on_content`.
|
|
699
|
+
|
|
700
|
+
### Per-Call Block
|
|
701
|
+
|
|
702
|
+
Pass a block to `run()` for one-off streaming:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Both Together
|
|
709
|
+
|
|
710
|
+
When both exist, both fire — stored callback first, then runtime block:
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
robot = RobotLab.build(
|
|
714
|
+
name: "bot",
|
|
715
|
+
system_prompt: "You are helpful.",
|
|
716
|
+
on_content: ->(chunk) { log(chunk.content) }
|
|
717
|
+
)
|
|
718
|
+
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
719
|
+
# log() fires first, then stream_to_client()
|
|
720
|
+
```
|
|
721
|
+
|
|
470
722
|
## Configuration Hierarchy
|
|
471
723
|
|
|
472
724
|
Tools and MCP servers use hierarchical resolution: **runtime > robot > network > global config**.
|
|
@@ -542,6 +794,18 @@ robot = RobotLab.build(
|
|
|
542
794
|
result = robot.run("What is 15 * 7?")
|
|
543
795
|
```
|
|
544
796
|
|
|
797
|
+
### Robot with Local Provider
|
|
798
|
+
|
|
799
|
+
```ruby
|
|
800
|
+
robot = RobotLab.build(
|
|
801
|
+
name: "local_bot",
|
|
802
|
+
model: "llama3.2",
|
|
803
|
+
provider: :ollama,
|
|
804
|
+
system_prompt: "You are helpful."
|
|
805
|
+
)
|
|
806
|
+
result = robot.run("Hello!")
|
|
807
|
+
```
|
|
808
|
+
|
|
545
809
|
### Robot with MCP
|
|
546
810
|
|
|
547
811
|
```ruby
|
|
@@ -559,6 +823,18 @@ result = robot.run("Search for popular Ruby repos")
|
|
|
559
823
|
robot.disconnect
|
|
560
824
|
```
|
|
561
825
|
|
|
826
|
+
### Robot with Skills
|
|
827
|
+
|
|
828
|
+
```ruby
|
|
829
|
+
robot = RobotLab.build(
|
|
830
|
+
name: "support",
|
|
831
|
+
template: :support,
|
|
832
|
+
skills: [:clarifier, :safety, :json_responder],
|
|
833
|
+
context: { company: "Acme Corp" }
|
|
834
|
+
)
|
|
835
|
+
result = robot.run("I need help with my order")
|
|
836
|
+
```
|
|
837
|
+
|
|
562
838
|
### Bare Robot with Chaining
|
|
563
839
|
|
|
564
840
|
```ruby
|
|
@@ -628,6 +904,6 @@ bot.send_message(to: :someone, content: "Hello!")
|
|
|
628
904
|
|
|
629
905
|
## See Also
|
|
630
906
|
|
|
631
|
-
- [Building Robots Guide](../../guides/building-robots.md)
|
|
907
|
+
- [Building Robots Guide](../../guides/building-robots.md) (includes [Composable Skills](../../guides/building-robots.md#composable-skills))
|
|
632
908
|
- [Tool](tool.md)
|
|
633
909
|
- [Network](network.md)
|
data/docs/api/core/tool.md
CHANGED
|
@@ -4,7 +4,7 @@ Callable function that robots can use to interact with external systems.
|
|
|
4
4
|
|
|
5
5
|
## Class: `RobotLab::Tool < RubyLLM::Tool`
|
|
6
6
|
|
|
7
|
-
RobotLab::Tool inherits from RubyLLM::Tool, adding a `robot:` constructor parameter
|
|
7
|
+
RobotLab::Tool inherits from RubyLLM::Tool, adding a `robot:` constructor parameter, a `Tool.create` factory for dynamic tools, and graceful error handling that returns plain-text errors to the LLM instead of crashing the run.
|
|
8
8
|
|
|
9
9
|
### Subclass Pattern
|
|
10
10
|
|
|
@@ -51,6 +51,15 @@ Tool.new(robot: nil)
|
|
|
51
51
|
|
|
52
52
|
## Class Methods
|
|
53
53
|
|
|
54
|
+
### raise_on_error / raise_on_error?
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
MyTool.raise_on_error = true
|
|
58
|
+
MyTool.raise_on_error? # => true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`. Does not affect other tool classes.
|
|
62
|
+
|
|
54
63
|
### Tool.create
|
|
55
64
|
|
|
56
65
|
Factory for dynamic tools (MCP wrappers, inline tools).
|
|
@@ -169,7 +178,24 @@ Whether this is an MCP-provided tool.
|
|
|
169
178
|
result = tool.call(args_hash)
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
Overrides `RubyLLM::Tool#call` with graceful error handling. Converts string keys to symbols and calls `execute(**args)`. If `execute` raises a `StandardError`, the error is caught and returned as a plain-text string the LLM can reason about:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Error (tool_name): exception message
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The error is also logged via `RobotLab.config.logger` at `:warn` level.
|
|
188
|
+
|
|
189
|
+
To propagate exceptions instead of catching them (for critical tools), set `raise_on_error` on the class:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class CriticalTool < RobotLab::Tool
|
|
193
|
+
self.raise_on_error = true
|
|
194
|
+
# ...
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`raise_on_error` is per-class (defaults to `false`) and does not affect other tool classes.
|
|
173
199
|
|
|
174
200
|
### params_schema
|
|
175
201
|
|
data/docs/api/mcp/client.md
CHANGED
|
@@ -36,6 +36,7 @@ Accepts either a `Server` instance or a Hash configuration. When a Hash is provi
|
|
|
36
36
|
|-----|------|----------|-------------|
|
|
37
37
|
| `name` | `String` | Yes | Server identifier |
|
|
38
38
|
| `transport` | `Hash` | Yes | Transport configuration (must include `type`) |
|
|
39
|
+
| `timeout` | `Numeric` | No | Request timeout in seconds (default: 15). Propagated to the transport layer |
|
|
39
40
|
|
|
40
41
|
**Raises:** `ArgumentError` if the config is neither a `Server` nor a `Hash`.
|
|
41
42
|
|
data/docs/api/mcp/server.md
CHANGED
|
@@ -13,31 +13,42 @@ server = RobotLab::MCP::Server.new(
|
|
|
13
13
|
name: "filesystem",
|
|
14
14
|
transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
|
|
15
15
|
)
|
|
16
|
+
|
|
17
|
+
# With custom timeout
|
|
18
|
+
server = RobotLab::MCP::Server.new(
|
|
19
|
+
name: "slow_server",
|
|
20
|
+
transport: { type: "stdio", command: "heavy-mcp-server" },
|
|
21
|
+
timeout: 30
|
|
22
|
+
)
|
|
16
23
|
```
|
|
17
24
|
|
|
18
25
|
## Constructor
|
|
19
26
|
|
|
20
27
|
```ruby
|
|
21
|
-
Server.new(name:, transport:)
|
|
28
|
+
Server.new(name:, transport:, timeout: nil, **_extra)
|
|
22
29
|
```
|
|
23
30
|
|
|
24
31
|
**Parameters:**
|
|
25
32
|
|
|
26
|
-
| Name | Type | Description |
|
|
27
|
-
|
|
28
|
-
| `name` | `String` | Unique server identifier |
|
|
29
|
-
| `transport` | `Hash` | Transport configuration (must include `type`) |
|
|
33
|
+
| Name | Type | Default | Description |
|
|
34
|
+
|------|------|---------|-------------|
|
|
35
|
+
| `name` | `String` | **required** | Unique server identifier |
|
|
36
|
+
| `transport` | `Hash` | **required** | Transport configuration (must include `type`) |
|
|
37
|
+
| `timeout` | `Numeric`, `nil` | `15` | Request timeout in seconds. Values >= 1000 are auto-converted from milliseconds. Minimum 1 second |
|
|
30
38
|
|
|
31
39
|
**Raises:** `ArgumentError` if:
|
|
32
40
|
- The transport type is not one of the valid types
|
|
33
41
|
- A stdio transport is missing the `:command` key
|
|
34
42
|
- A network transport (ws, websocket, sse, streamable-http, http) is missing the `:url` key
|
|
35
43
|
|
|
36
|
-
##
|
|
44
|
+
## Constants
|
|
37
45
|
|
|
38
46
|
```ruby
|
|
39
47
|
RobotLab::MCP::Server::VALID_TRANSPORT_TYPES
|
|
40
48
|
# => ["stdio", "sse", "ws", "websocket", "streamable-http", "http"]
|
|
49
|
+
|
|
50
|
+
RobotLab::MCP::Server::DEFAULT_TIMEOUT
|
|
51
|
+
# => 15 (seconds)
|
|
41
52
|
```
|
|
42
53
|
|
|
43
54
|
## Attributes
|
|
@@ -58,6 +69,14 @@ server.transport # => Hash
|
|
|
58
69
|
|
|
59
70
|
The normalized transport configuration hash (keys are symbols, type is downcased).
|
|
60
71
|
|
|
72
|
+
### timeout
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
server.timeout # => Numeric
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Request timeout in seconds. Defaults to `DEFAULT_TIMEOUT` (15). Values >= 1000 passed to the constructor are auto-converted from milliseconds to seconds. The minimum is 1 second.
|
|
79
|
+
|
|
61
80
|
## Methods
|
|
62
81
|
|
|
63
82
|
### transport_type
|
|
@@ -71,10 +90,10 @@ Returns the transport type string (e.g., `"stdio"`, `"ws"`, `"sse"`).
|
|
|
71
90
|
### to_h
|
|
72
91
|
|
|
73
92
|
```ruby
|
|
74
|
-
server.to_h # => { name: "...", transport: { ... } }
|
|
93
|
+
server.to_h # => { name: "...", transport: { ... }, timeout: 15 }
|
|
75
94
|
```
|
|
76
95
|
|
|
77
|
-
Converts the server configuration to a hash representation.
|
|
96
|
+
Converts the server configuration to a hash representation (includes `timeout`).
|
|
78
97
|
|
|
79
98
|
## Transport Configuration Options
|
|
80
99
|
|
data/docs/api/mcp/transports.md
CHANGED
|
@@ -21,7 +21,10 @@ All transports inherit from `RobotLab::MCP::Transports::Base` and implement:
|
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
23
|
class RobotLab::MCP::Transports::Base
|
|
24
|
-
|
|
24
|
+
DEFAULT_TIMEOUT = 15 # seconds
|
|
25
|
+
|
|
26
|
+
attr_reader :config # => Hash (symbolized keys, :timeout removed)
|
|
27
|
+
attr_reader :timeout # => Numeric (seconds, extracted from config)
|
|
25
28
|
|
|
26
29
|
def connect # Establish connection, returns self
|
|
27
30
|
def send_request(message) # Send JSON-RPC message, returns Hash response
|
|
@@ -30,11 +33,13 @@ class RobotLab::MCP::Transports::Base
|
|
|
30
33
|
end
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
The `timeout` is extracted from the config hash during initialization (and removed from `config`). If not provided, it defaults to `DEFAULT_TIMEOUT` (15 seconds). The timeout is propagated from `MCP::Server` through `MCP::Client` to the transport.
|
|
37
|
+
|
|
33
38
|
## Stdio Transport
|
|
34
39
|
|
|
35
40
|
**Class:** `RobotLab::MCP::Transports::Stdio`
|
|
36
41
|
|
|
37
|
-
Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP `initialize` and `notifications/initialized` on connect.
|
|
42
|
+
Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP `initialize` and `notifications/initialized` on connect. All blocking I/O is wrapped with `Timeout.timeout` so a missing or hung server cannot block the caller forever.
|
|
38
43
|
|
|
39
44
|
### Configuration
|
|
40
45
|
|
|
@@ -43,7 +48,8 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
43
48
|
type: "stdio",
|
|
44
49
|
command: "mcp-server-filesystem", # Required: executable command
|
|
45
50
|
args: ["--root", "/data"], # Optional: command arguments
|
|
46
|
-
env: { "DEBUG" => "true" }
|
|
51
|
+
env: { "DEBUG" => "true" }, # Optional: environment variables
|
|
52
|
+
timeout: 10 # Optional: request timeout in seconds (default: 15)
|
|
47
53
|
}
|
|
48
54
|
```
|
|
49
55
|
|
|
@@ -52,14 +58,18 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
52
58
|
| `command` | `String` | Yes | Executable command to spawn |
|
|
53
59
|
| `args` | `Array<String>` | No | Command arguments |
|
|
54
60
|
| `env` | `Hash` | No | Environment variables (merged with current env) |
|
|
61
|
+
| `timeout` | `Numeric` | No | Request timeout in seconds (default: 15) |
|
|
55
62
|
|
|
56
63
|
### Behavior
|
|
57
64
|
|
|
58
65
|
- Uses `Open3.popen3` to spawn the subprocess
|
|
66
|
+
- Verifies the process actually started (raises `MCPError` if it exits immediately)
|
|
59
67
|
- Writes JSON-RPC messages to stdin (one per line)
|
|
60
68
|
- Reads responses from stdout, skipping notifications (messages without `id`)
|
|
69
|
+
- All blocking reads are wrapped with `Timeout.timeout` — raises `MCPError` if the server does not respond within the timeout period
|
|
61
70
|
- `connected?` returns `true` when the subprocess is alive
|
|
62
|
-
- `close`
|
|
71
|
+
- `close` calls `cleanup_process` to reliably close stdin, stdout, stderr and kill the subprocess
|
|
72
|
+
- Handles `Errno::ENOENT` (command not found), `Errno::EPIPE` / `IOError` (broken pipe / connection lost), and `Timeout::Error` (hung server) with clear error messages
|
|
63
73
|
|
|
64
74
|
### Example
|
|
65
75
|
|
|
@@ -67,7 +77,8 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
67
77
|
transport = RobotLab::MCP::Transports::Stdio.new(
|
|
68
78
|
command: "mcp-server-filesystem",
|
|
69
79
|
args: ["--root", "/data"],
|
|
70
|
-
env: { "DEBUG" => "true" }
|
|
80
|
+
env: { "DEBUG" => "true" },
|
|
81
|
+
timeout: 10
|
|
71
82
|
)
|
|
72
83
|
|
|
73
84
|
transport.connect
|
|
@@ -243,7 +254,11 @@ end
|
|
|
243
254
|
Specific error cases:
|
|
244
255
|
- **Not connected** -- calling `send_request` before `connect` raises `MCPError`
|
|
245
256
|
- **Missing gem** -- WebSocket, SSE, and HTTP transports raise `MCPError` with a `LoadError` message if required gems are not installed
|
|
246
|
-
- **No response** -- Stdio transport raises `MCPError` if the subprocess produces no output
|
|
257
|
+
- **No response** -- Stdio transport raises `MCPError` if the subprocess produces no output (EOF on stdout)
|
|
258
|
+
- **Command not found** -- Stdio transport raises `MCPError` with the original `Errno::ENOENT` message
|
|
259
|
+
- **Timeout** -- Stdio transport raises `MCPError` if the server does not respond within the configured timeout
|
|
260
|
+
- **Broken pipe** -- Stdio transport raises `MCPError` and marks itself disconnected on `Errno::EPIPE` or `IOError`
|
|
261
|
+
- **Immediate exit** -- Stdio transport raises `MCPError` if the server process exits immediately after spawn
|
|
247
262
|
|
|
248
263
|
## See Also
|
|
249
264
|
|
|
@@ -9,7 +9,7 @@ A Robot is the primary unit of computation in RobotLab. It is a subclass of `Rub
|
|
|
9
9
|
- A unique identity (name, description)
|
|
10
10
|
- A personality (system prompt and/or template)
|
|
11
11
|
- Capabilities (tools, MCP connections)
|
|
12
|
-
- Model and inference configuration
|
|
12
|
+
- Model, provider, and inference configuration
|
|
13
13
|
- Inherent memory (key-value store)
|
|
14
14
|
|
|
15
15
|
### Robot Anatomy
|
|
@@ -164,7 +164,8 @@ def build_result(response, _memory)
|
|
|
164
164
|
robot_name: @name,
|
|
165
165
|
output: output,
|
|
166
166
|
tool_calls: normalize_tool_calls(tool_calls),
|
|
167
|
-
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil
|
|
167
|
+
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil,
|
|
168
|
+
raw: response
|
|
168
169
|
)
|
|
169
170
|
end
|
|
170
171
|
```
|
|
@@ -182,9 +183,12 @@ result.tool_calls # => [ToolResultMessage, ...]
|
|
|
182
183
|
result.stop_reason # => "stop" or nil
|
|
183
184
|
result.created_at # => Time
|
|
184
185
|
result.id # => UUID string
|
|
186
|
+
result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
|
|
187
|
+
result.raw # => raw LLM response object
|
|
185
188
|
|
|
186
189
|
# Convenience methods
|
|
187
190
|
result.last_text_content # => "Hi there!" (last text message content)
|
|
191
|
+
result.reply # => alias for last_text_content
|
|
188
192
|
result.has_tool_calls? # => false
|
|
189
193
|
result.stopped? # => true
|
|
190
194
|
```
|
|
@@ -275,17 +279,31 @@ sequenceDiagram
|
|
|
275
279
|
Robot-->>SF: result.continue(robot_result)
|
|
276
280
|
```
|
|
277
281
|
|
|
278
|
-
The `Task` wrapper deep-merges per-task configuration (context, mcp, tools) before delegating to the robot's `call`. The base `Robot#call` extracts the message and
|
|
282
|
+
The `Task` wrapper deep-merges per-task configuration (context, mcp, tools) before delegating to the robot's `call`. The base `Robot#call` extracts the message, calls `run`, and records the elapsed time in `RobotResult#duration`. If the robot raises any exception, the error is caught and wrapped in a `RobotResult` so one failing robot does not crash the entire pipeline:
|
|
279
283
|
|
|
280
284
|
```ruby
|
|
281
285
|
def call(result)
|
|
282
286
|
run_context = extract_run_context(result)
|
|
283
287
|
message = run_context.delete(:message)
|
|
288
|
+
|
|
289
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
284
290
|
robot_result = run(message, **run_context)
|
|
291
|
+
robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
285
292
|
|
|
286
293
|
result
|
|
287
294
|
.with_context(@name.to_sym, robot_result)
|
|
288
295
|
.continue(robot_result)
|
|
296
|
+
rescue Exception => e
|
|
297
|
+
# Error is wrapped in a RobotResult with the elapsed duration
|
|
298
|
+
error_result = RobotResult.new(
|
|
299
|
+
robot_name: @name,
|
|
300
|
+
output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
|
|
301
|
+
)
|
|
302
|
+
error_result.duration = elapsed
|
|
303
|
+
|
|
304
|
+
result
|
|
305
|
+
.with_context(@name.to_sym, error_result)
|
|
306
|
+
.continue(error_result)
|
|
289
307
|
end
|
|
290
308
|
```
|
|
291
309
|
|