robot_lab 0.0.8 → 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 +39 -0
- data/README.md +26 -3
- data/docs/api/core/robot.md +154 -1
- 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 +4 -0
- data/docs/guides/building-robots.md +18 -0
- data/docs/guides/creating-networks.md +18 -0
- data/docs/guides/mcp-integration.md +74 -2
- data/examples/14_rusty_circuit/.gitignore +1 -0
- data/examples/14_rusty_circuit/open_mic.rb +1 -1
- 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/robot/mcp_management.rb +61 -4
- data/lib/robot_lab/robot.rb +125 -2
- data/lib/robot_lab/robot_result.rb +3 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c852fcf7f4aed4ce95fabdc5b0296723ca8aa10e780dabaa7759e618a22bc640
|
|
4
|
+
data.tar.gz: 1bcb205c958ede9967886dae78a1d1a6d47da42e4cd9bd29d7bdd3e094b0a088
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5620e7798ac04441cb23c6a7cc5f0cdad7447103825db35ef6f3a3987785b8ff5fb355ec03a309ef9c8a5ce5b0b7a29d9f5adef0e6a5d9de5cd66d3c94fb0469
|
|
7
|
+
data.tar.gz: 9300b1f5ed98e70226c7c670bcf2e3dee033310db6b2182b2705085f02474a1ea6157a011c93906da1d45ba38b4c9f8b9e62545cdb5fd304ca1550734f7dc043
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.0.9] - 2026-03-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Provider passthrough** — `provider:` parameter on Robot constructor for local LLM providers (Ollama, GPUStack, etc.)
|
|
16
|
+
- Automatically sets `assume_model_exists: true` when provider is specified
|
|
17
|
+
- Exposed via `robot.provider` accessor
|
|
18
|
+
- **MCP request timeouts** — configurable timeout for all MCP transports
|
|
19
|
+
- `MCP::Server` accepts `timeout:` parameter (default 15s); auto-converts millisecond values
|
|
20
|
+
- `MCP::Transports::Base` extracts and exposes `timeout` from config
|
|
21
|
+
- `MCP::Transports::Stdio` wraps all blocking I/O with `Timeout.timeout` — hung servers no longer block the caller forever
|
|
22
|
+
- Timeout propagated from `MCP::Server` through `MCP::Client` to transport layer
|
|
23
|
+
- **MCP connection resilience** — improved error handling and retry logic
|
|
24
|
+
- `ensure_mcp_clients` retries previously failed servers on subsequent calls
|
|
25
|
+
- `@failed_mcp_configs` tracks servers that failed to connect
|
|
26
|
+
- `robot.failed_mcp_server_names` — query which MCP servers are down
|
|
27
|
+
- `robot.connect_mcp!` — eagerly connect to MCP servers (normally lazy)
|
|
28
|
+
- `init_mcp_client` rescues `StandardError` so one bad server doesn't prevent others from connecting
|
|
29
|
+
- `cleanup_process` in Stdio transport for reliable resource cleanup
|
|
30
|
+
- Better error messages for command-not-found (`Errno::ENOENT`), broken pipe (`Errno::EPIPE`), and EOF conditions
|
|
31
|
+
- **`robot.inject_mcp!`** — inject pre-connected MCP clients and tools from an external host application
|
|
32
|
+
- **Conversation management APIs** on Robot
|
|
33
|
+
- `robot.chat` — access the underlying `RubyLLM::Chat` instance
|
|
34
|
+
- `robot.messages` — return conversation messages
|
|
35
|
+
- `robot.clear_messages(keep_system:)` — clear history, optionally preserving the system prompt
|
|
36
|
+
- `robot.replace_messages(messages)` — restore a saved conversation (checkpoint/restore)
|
|
37
|
+
- `robot.chat_provider` — query the provider name without reaching into chat internals
|
|
38
|
+
- `robot.mcp_client(server_name)` — find an MCP client by server name
|
|
39
|
+
- **`RobotResult#duration`** — elapsed seconds for a robot run, set automatically during pipeline execution
|
|
40
|
+
- **`RobotResult#raw`** — raw LLM response stored on every result (previously only settable via accessor)
|
|
41
|
+
- **Pipeline error resilience** — `Robot#call` (pipeline step) rescues all exceptions so one failing robot doesn't crash the entire network; error is captured in a `RobotResult` with the elapsed duration
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Bumped version to 0.0.9
|
|
46
|
+
- Display `scout_path` in Rusty Circuit example updated to use `output/` subdirectory
|
|
47
|
+
- Updated `onnxruntime` dependency to 0.11.0
|
|
48
|
+
- Updated Gemfile.lock dependencies (erb, minitest, rails-html-sanitizer, json_schemer)
|
|
49
|
+
|
|
11
50
|
## [0.0.8] - 2026-02-22
|
|
12
51
|
|
|
13
52
|
### Added
|
data/README.md
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
- <strong>Extensible Tools</strong> - Custom capabilities with graceful error handling<br>
|
|
21
21
|
- <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
|
|
22
22
|
- <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
|
|
23
|
-
- <strong>MCP Integration</strong> - Connect to external tool servers<br>
|
|
23
|
+
- <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
|
|
24
|
+
- <strong>Local LLM Providers</strong> - Ollama, GPUStack, LM Studio via provider passthrough<br>
|
|
24
25
|
- <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
|
|
25
26
|
- <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
|
|
26
27
|
- <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
|
|
@@ -71,6 +72,19 @@ puts result.last_text_content
|
|
|
71
72
|
# => "The capital of France is Paris."
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
### Local LLM Providers
|
|
76
|
+
|
|
77
|
+
For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
robot = RobotLab.build(
|
|
81
|
+
name: "local_bot",
|
|
82
|
+
model: "llama3.2",
|
|
83
|
+
provider: :ollama,
|
|
84
|
+
system_prompt: "You are a helpful assistant."
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
74
88
|
### Configuration
|
|
75
89
|
|
|
76
90
|
RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. There is no `configure` block. Configuration is loaded automatically from multiple sources in priority order:
|
|
@@ -443,14 +457,15 @@ puts result.value.last_text_content
|
|
|
443
457
|
Connect to external tool servers via Model Context Protocol:
|
|
444
458
|
|
|
445
459
|
```ruby
|
|
446
|
-
# Configure MCP server
|
|
460
|
+
# Configure MCP server (with optional timeout)
|
|
447
461
|
filesystem_server = {
|
|
448
462
|
name: "filesystem",
|
|
449
463
|
transport: {
|
|
450
464
|
type: "stdio",
|
|
451
465
|
command: "mcp-server-filesystem",
|
|
452
466
|
args: ["/path/to/allowed/directory"]
|
|
453
|
-
}
|
|
467
|
+
},
|
|
468
|
+
timeout: 30 # seconds (default: 15)
|
|
454
469
|
}
|
|
455
470
|
|
|
456
471
|
# Create robot with MCP server - tools are auto-discovered
|
|
@@ -460,10 +475,18 @@ robot = RobotLab.build(
|
|
|
460
475
|
mcp: [filesystem_server]
|
|
461
476
|
)
|
|
462
477
|
|
|
478
|
+
# Optionally connect eagerly (default is lazy on first run)
|
|
479
|
+
robot.connect_mcp!
|
|
480
|
+
|
|
481
|
+
# Check connection status
|
|
482
|
+
puts "Failed: #{robot.failed_mcp_server_names}" if robot.failed_mcp_server_names.any?
|
|
483
|
+
|
|
463
484
|
# Robot can now use filesystem tools
|
|
464
485
|
result = robot.run("List the files in the current directory")
|
|
465
486
|
```
|
|
466
487
|
|
|
488
|
+
MCP connections are resilient: failed servers are automatically retried on subsequent `run()` calls, and one failing server does not prevent others from connecting.
|
|
489
|
+
|
|
467
490
|
## Message Bus
|
|
468
491
|
|
|
469
492
|
Robots can communicate bidirectionally via an optional message bus, independent of the Network pipeline. This enables negotiation loops, convergence patterns, and cyclic workflows.
|
data/docs/api/core/robot.md
CHANGED
|
@@ -23,6 +23,7 @@ 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,
|
|
@@ -54,6 +55,7 @@ Robot.new(
|
|
|
54
55
|
| `description` | `String`, `nil` | `nil` | Human-readable description of what the robot does |
|
|
55
56
|
| `local_tools` | `Array` | `[]` | Tools defined locally (`RubyLLM::Tool` subclasses or `RobotLab::Tool` instances) |
|
|
56
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` |
|
|
57
59
|
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
58
60
|
| `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
|
|
59
61
|
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name array) |
|
|
@@ -101,6 +103,7 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
101
103
|
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
102
104
|
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
103
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 |
|
|
104
107
|
| `local_tools` | `Array` | Locally defined tools |
|
|
105
108
|
| `mcp_clients` | `Hash<String, MCP::Client>` | Connected MCP clients, keyed by server name |
|
|
106
109
|
| `mcp_tools` | `Array<Tool>` | Tools discovered from MCP servers |
|
|
@@ -239,7 +242,9 @@ robot.call(result)
|
|
|
239
242
|
# => SimpleFlow::Result
|
|
240
243
|
```
|
|
241
244
|
|
|
242
|
-
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.
|
|
243
248
|
|
|
244
249
|
Override this method in subclasses for custom routing logic (e.g., classifiers).
|
|
245
250
|
|
|
@@ -401,6 +406,142 @@ bot.with_bus(bus1) # joins bus1
|
|
|
401
406
|
bot.with_bus(bus2) # leaves bus1, joins bus2
|
|
402
407
|
```
|
|
403
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
|
+
|
|
404
545
|
### disconnect
|
|
405
546
|
|
|
406
547
|
```ruby
|
|
@@ -653,6 +794,18 @@ robot = RobotLab.build(
|
|
|
653
794
|
result = robot.run("What is 15 * 7?")
|
|
654
795
|
```
|
|
655
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
|
+
|
|
656
809
|
### Robot with MCP
|
|
657
810
|
|
|
658
811
|
```ruby
|
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
|
|
data/docs/concepts.md
CHANGED
|
@@ -12,6 +12,7 @@ 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
|
+
- **Provider**: Optional LLM provider for local models (Ollama, GPUStack, etc.)
|
|
15
16
|
- **Skills**: Composable template behaviors prepended before the main template
|
|
16
17
|
- **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances (with automatic error handling)
|
|
17
18
|
- **Streaming**: Real-time content via stored `on_content` callback or per-call block
|
|
@@ -195,12 +196,15 @@ tool = RobotLab::Tool.create(
|
|
|
195
196
|
result = robot.run("Hello!")
|
|
196
197
|
|
|
197
198
|
result.last_text_content # => "Hi there!" (String or nil)
|
|
199
|
+
result.reply # => alias for last_text_content
|
|
198
200
|
result.output # => [TextMessage, ...] array of output messages
|
|
199
201
|
result.tool_calls # => [] array of tool call results
|
|
200
202
|
result.robot_name # => "assistant"
|
|
201
203
|
result.stop_reason # => "end_turn" or nil
|
|
202
204
|
result.has_tool_calls? # => false
|
|
203
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
|
|
204
208
|
```
|
|
205
209
|
|
|
206
210
|
## Memory
|
|
@@ -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:
|
|
@@ -479,10 +494,13 @@ The `run` method returns a `RobotResult` with:
|
|
|
479
494
|
|
|
480
495
|
```ruby
|
|
481
496
|
result.last_text_content # => "Hi there! How can I help?"
|
|
497
|
+
result.reply # => alias for last_text_content
|
|
482
498
|
result.output # => Array of output messages
|
|
483
499
|
result.tool_calls # => Array of tool call results
|
|
484
500
|
result.robot_name # => "assistant"
|
|
485
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
|
|
486
504
|
```
|
|
487
505
|
|
|
488
506
|
### With Runtime Memory
|
|
@@ -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
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/output/
|
data/lib/robot_lab/mcp/client.rb
CHANGED
|
@@ -162,15 +162,17 @@ module RobotLab
|
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
def create_transport
|
|
165
|
+
config = @server.transport.merge(timeout: @server.timeout)
|
|
166
|
+
|
|
165
167
|
case @server.transport_type
|
|
166
168
|
when "stdio"
|
|
167
|
-
Transports::Stdio.new(
|
|
169
|
+
Transports::Stdio.new(config)
|
|
168
170
|
when "ws", "websocket"
|
|
169
|
-
Transports::WebSocket.new(
|
|
171
|
+
Transports::WebSocket.new(config)
|
|
170
172
|
when "sse"
|
|
171
|
-
Transports::SSE.new(
|
|
173
|
+
Transports::SSE.new(config)
|
|
172
174
|
when "streamable-http", "http"
|
|
173
|
-
Transports::StreamableHTTP.new(
|
|
175
|
+
Transports::StreamableHTTP.new(config)
|
|
174
176
|
else
|
|
175
177
|
raise MCPError, "Unsupported transport type: #{@server.transport_type}"
|
|
176
178
|
end
|
data/lib/robot_lab/mcp/server.rb
CHANGED
|
@@ -24,20 +24,28 @@ module RobotLab
|
|
|
24
24
|
# Valid transport types for MCP connections
|
|
25
25
|
VALID_TRANSPORT_TYPES = %w[stdio sse ws websocket streamable-http http].freeze
|
|
26
26
|
|
|
27
|
+
# Default timeout for MCP requests (in seconds)
|
|
28
|
+
DEFAULT_TIMEOUT = 15
|
|
29
|
+
|
|
27
30
|
# @!attribute [r] name
|
|
28
31
|
# @return [String] the server name
|
|
29
32
|
# @!attribute [r] transport
|
|
30
33
|
# @return [Hash] the transport configuration
|
|
31
|
-
|
|
34
|
+
# @!attribute [r] timeout
|
|
35
|
+
# @return [Numeric] request timeout in seconds
|
|
36
|
+
attr_reader :name, :transport, :timeout
|
|
32
37
|
|
|
33
38
|
# Creates a new Server configuration.
|
|
34
39
|
#
|
|
35
40
|
# @param name [String] the server name
|
|
36
41
|
# @param transport [Hash] the transport configuration
|
|
42
|
+
# @param timeout [Numeric, nil] request timeout in seconds (default: 15)
|
|
43
|
+
# @param _extra [Hash] additional keys are silently ignored for forward compatibility
|
|
37
44
|
# @raise [ArgumentError] if transport type is invalid or required fields are missing
|
|
38
|
-
def initialize(name:, transport:)
|
|
45
|
+
def initialize(name:, transport:, timeout: nil, **_extra)
|
|
39
46
|
@name = name.to_s
|
|
40
47
|
@transport = normalize_transport(transport)
|
|
48
|
+
@timeout = normalize_timeout(timeout)
|
|
41
49
|
validate!
|
|
42
50
|
end
|
|
43
51
|
|
|
@@ -54,7 +62,8 @@ module RobotLab
|
|
|
54
62
|
def to_h
|
|
55
63
|
{
|
|
56
64
|
name: name,
|
|
57
|
-
transport: transport
|
|
65
|
+
transport: transport,
|
|
66
|
+
timeout: timeout
|
|
58
67
|
}
|
|
59
68
|
end
|
|
60
69
|
|
|
@@ -79,6 +88,15 @@ module RobotLab
|
|
|
79
88
|
raise ArgumentError, "Transport requires :url" unless transport[:url]
|
|
80
89
|
end
|
|
81
90
|
end
|
|
91
|
+
|
|
92
|
+
def normalize_timeout(value)
|
|
93
|
+
return DEFAULT_TIMEOUT if value.nil?
|
|
94
|
+
|
|
95
|
+
seconds = value.to_f
|
|
96
|
+
# If the caller passed milliseconds (>= 1000), convert to seconds
|
|
97
|
+
seconds = seconds / 1000.0 if seconds >= 1000
|
|
98
|
+
[seconds, 1].max
|
|
99
|
+
end
|
|
82
100
|
end
|
|
83
101
|
end
|
|
84
102
|
end
|
|
@@ -8,15 +8,23 @@ module RobotLab
|
|
|
8
8
|
# @abstract Subclass and implement {#connect}, {#send_request}, {#close}
|
|
9
9
|
#
|
|
10
10
|
class Base
|
|
11
|
+
# Default timeout for request operations (in seconds)
|
|
12
|
+
DEFAULT_TIMEOUT = 15
|
|
13
|
+
|
|
11
14
|
# @!attribute [r] config
|
|
12
15
|
# @return [Hash] the transport configuration
|
|
13
|
-
|
|
16
|
+
# @!attribute [r] timeout
|
|
17
|
+
# @return [Numeric] request timeout in seconds
|
|
18
|
+
attr_reader :config, :timeout
|
|
14
19
|
|
|
15
20
|
# Creates a new transport instance.
|
|
16
21
|
#
|
|
17
|
-
# @param config [Hash] transport configuration options
|
|
22
|
+
# @param config [Hash] transport configuration options.
|
|
23
|
+
# May include a :timeout key (in seconds) that controls how long
|
|
24
|
+
# blocking operations wait before raising an error.
|
|
18
25
|
def initialize(config)
|
|
19
26
|
@config = config.transform_keys(&:to_sym)
|
|
27
|
+
@timeout = @config.delete(:timeout) || DEFAULT_TIMEOUT
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
# Connect to the server
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "json"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module RobotLab
|
|
7
8
|
module MCP
|
|
@@ -9,12 +10,13 @@ module RobotLab
|
|
|
9
10
|
# StdIO transport for local MCP servers
|
|
10
11
|
#
|
|
11
12
|
# Spawns a subprocess and communicates via stdin/stdout.
|
|
13
|
+
# All blocking I/O is wrapped with a configurable timeout
|
|
14
|
+
# so a missing or hung server cannot block the caller forever.
|
|
12
15
|
#
|
|
13
16
|
# @example
|
|
14
17
|
# transport = Stdio.new(
|
|
15
|
-
# command: "mcp-server-filesystem",
|
|
16
|
-
#
|
|
17
|
-
# env: { "DEBUG" => "true" }
|
|
18
|
+
# { command: "mcp-server-filesystem", args: ["--root", "/data"] },
|
|
19
|
+
# timeout: 10
|
|
18
20
|
# )
|
|
19
21
|
#
|
|
20
22
|
class Stdio < Base
|
|
@@ -24,6 +26,7 @@ module RobotLab
|
|
|
24
26
|
# @option config [String] :command the command to execute
|
|
25
27
|
# @option config [Array<String>] :args command arguments
|
|
26
28
|
# @option config [Hash] :env environment variables
|
|
29
|
+
# @option config [Numeric] :timeout request timeout in seconds (default: 15)
|
|
27
30
|
def initialize(config)
|
|
28
31
|
super
|
|
29
32
|
@stdin = nil
|
|
@@ -36,7 +39,8 @@ module RobotLab
|
|
|
36
39
|
# Connect to the MCP server via stdio.
|
|
37
40
|
#
|
|
38
41
|
# @return [self]
|
|
39
|
-
# @raise [MCPError] if
|
|
42
|
+
# @raise [MCPError] if the server process cannot be started or does not
|
|
43
|
+
# respond to the MCP initialize handshake within the timeout period
|
|
40
44
|
def connect
|
|
41
45
|
return self if @connected
|
|
42
46
|
|
|
@@ -44,44 +48,59 @@ module RobotLab
|
|
|
44
48
|
args = @config[:args] || []
|
|
45
49
|
env = @config[:env] || {}
|
|
46
50
|
|
|
47
|
-
# Merge with current environment
|
|
48
51
|
full_env = ENV.to_h.merge(env.transform_keys(&:to_s))
|
|
49
52
|
|
|
50
53
|
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(full_env, command, *args)
|
|
54
|
+
|
|
55
|
+
# Verify the process actually started
|
|
56
|
+
unless @wait_thread.alive?
|
|
57
|
+
raise MCPError, "MCP server process exited immediately (command: #{command})"
|
|
58
|
+
end
|
|
59
|
+
|
|
51
60
|
@connected = true
|
|
52
61
|
|
|
53
|
-
# Initialize MCP protocol
|
|
62
|
+
# Initialize MCP protocol — this must complete within the timeout
|
|
54
63
|
send_initialize
|
|
55
64
|
|
|
56
65
|
self
|
|
66
|
+
rescue Errno::ENOENT => e
|
|
67
|
+
cleanup_process
|
|
68
|
+
raise MCPError, "MCP server command not found: #{command} (#{e.message})"
|
|
69
|
+
rescue Timeout::Error
|
|
70
|
+
cleanup_process
|
|
71
|
+
raise MCPError, "MCP server '#{command}' did not respond within #{@timeout}s"
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
# Send a JSON-RPC request to the MCP server.
|
|
60
75
|
#
|
|
61
76
|
# @param message [Hash] JSON-RPC message
|
|
62
77
|
# @return [Hash] the response
|
|
63
|
-
# @raise [MCPError] if not connected
|
|
78
|
+
# @raise [MCPError] if not connected, no response, or timeout
|
|
64
79
|
def send_request(message)
|
|
65
80
|
raise MCPError, "Not connected" unless @connected
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
Timeout.timeout(@timeout, Timeout::Error) do
|
|
83
|
+
json = message.to_json
|
|
84
|
+
@stdin.puts(json)
|
|
85
|
+
@stdin.flush
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
raise MCPError, "No response from MCP server" unless response_line
|
|
87
|
+
loop do
|
|
88
|
+
response_line = @stdout.gets
|
|
89
|
+
raise MCPError, "No response from MCP server (EOF on stdout)" unless response_line
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
parsed = JSON.parse(response_line, symbolize_names: true)
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
# Skip notifications (messages without an id)
|
|
94
|
+
next if parsed[:method] && !parsed.key?(:id)
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
return parsed
|
|
97
|
+
end
|
|
84
98
|
end
|
|
99
|
+
rescue Timeout::Error
|
|
100
|
+
raise MCPError, "MCP server did not respond within #{@timeout}s"
|
|
101
|
+
rescue Errno::EPIPE, IOError => e
|
|
102
|
+
@connected = false
|
|
103
|
+
raise MCPError, "MCP server connection lost: #{e.message}"
|
|
85
104
|
end
|
|
86
105
|
|
|
87
106
|
# Close the connection to the MCP server.
|
|
@@ -90,12 +109,7 @@ module RobotLab
|
|
|
90
109
|
def close
|
|
91
110
|
return self unless @connected
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
@stdout&.close
|
|
95
|
-
@stderr&.close
|
|
96
|
-
@wait_thread&.kill if @wait_thread&.alive?
|
|
97
|
-
|
|
98
|
-
@connected = false
|
|
112
|
+
cleanup_process
|
|
99
113
|
self
|
|
100
114
|
end
|
|
101
115
|
|
|
@@ -127,6 +141,18 @@ module RobotLab
|
|
|
127
141
|
@stdin.puts({ jsonrpc: "2.0", method: "notifications/initialized" }.to_json)
|
|
128
142
|
@stdin.flush
|
|
129
143
|
end
|
|
144
|
+
|
|
145
|
+
def cleanup_process
|
|
146
|
+
@connected = false
|
|
147
|
+
@stdin&.close rescue nil
|
|
148
|
+
@stdout&.close rescue nil
|
|
149
|
+
@stderr&.close rescue nil
|
|
150
|
+
@wait_thread&.kill if @wait_thread&.alive?
|
|
151
|
+
@stdin = nil
|
|
152
|
+
@stdout = nil
|
|
153
|
+
@stderr = nil
|
|
154
|
+
@wait_thread = nil
|
|
155
|
+
end
|
|
130
156
|
end
|
|
131
157
|
end
|
|
132
158
|
end
|
|
@@ -26,17 +26,22 @@ module RobotLab
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
# Ensure MCP clients are initialized for the given server configs
|
|
29
|
+
# Ensure MCP clients are initialized for the given server configs.
|
|
30
|
+
# On subsequent calls, retries any servers that previously failed to connect.
|
|
30
31
|
def ensure_mcp_clients(mcp_servers)
|
|
31
32
|
return if mcp_servers.empty?
|
|
32
33
|
|
|
33
34
|
needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
|
|
34
|
-
return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
if @mcp_initialized
|
|
37
|
+
# Already initialized — retry any servers that are needed but not connected
|
|
38
|
+
retry_failed_servers(mcp_servers, needed_servers)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
37
41
|
|
|
38
42
|
@mcp_clients = {}
|
|
39
43
|
@mcp_tools = []
|
|
44
|
+
@failed_mcp_configs = {}
|
|
40
45
|
|
|
41
46
|
mcp_servers.each do |server_config|
|
|
42
47
|
init_mcp_client(server_config)
|
|
@@ -55,8 +60,48 @@ module RobotLab
|
|
|
55
60
|
@mcp_clients[server_name] = client
|
|
56
61
|
discover_mcp_tools(client, server_name)
|
|
57
62
|
else
|
|
63
|
+
server_name = extract_server_name(server_config)
|
|
64
|
+
@failed_mcp_configs[server_name] = server_config
|
|
58
65
|
RobotLab.config.logger.warn(
|
|
59
|
-
"Robot '#{@name}' failed to connect to MCP server: #{
|
|
66
|
+
"Robot '#{@name}' failed to connect to MCP server: #{server_name}"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
server_name = extract_server_name(server_config)
|
|
71
|
+
@failed_mcp_configs[server_name] = server_config
|
|
72
|
+
RobotLab.config.logger.warn(
|
|
73
|
+
"Robot '#{@name}' error connecting to MCP server '#{server_name}': #{e.message}"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Retry connecting to servers that previously failed
|
|
79
|
+
def retry_failed_servers(mcp_servers, needed_servers)
|
|
80
|
+
return if @failed_mcp_configs.nil? || @failed_mcp_configs.empty?
|
|
81
|
+
|
|
82
|
+
# Only retry servers that are still needed and still failed
|
|
83
|
+
to_retry = @failed_mcp_configs.select { |name, _| needed_servers.include?(name) }
|
|
84
|
+
return if to_retry.empty?
|
|
85
|
+
|
|
86
|
+
to_retry.each do |name, server_config|
|
|
87
|
+
RobotLab.config.logger.info(
|
|
88
|
+
"Robot '#{@name}' retrying MCP server: #{name}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
client = MCP::Client.new(server_config)
|
|
92
|
+
client.connect
|
|
93
|
+
|
|
94
|
+
if client.connected?
|
|
95
|
+
@mcp_clients[name] = client
|
|
96
|
+
@failed_mcp_configs.delete(name)
|
|
97
|
+
discover_mcp_tools(client, name)
|
|
98
|
+
RobotLab.config.logger.info(
|
|
99
|
+
"Robot '#{@name}' successfully connected to MCP server '#{name}' on retry"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
RobotLab.config.logger.warn(
|
|
104
|
+
"Robot '#{@name}' retry failed for MCP server '#{name}': #{e.message}"
|
|
60
105
|
)
|
|
61
106
|
end
|
|
62
107
|
end
|
|
@@ -83,6 +128,18 @@ module RobotLab
|
|
|
83
128
|
"Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
|
|
84
129
|
)
|
|
85
130
|
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def extract_server_name(server_config)
|
|
134
|
+
case server_config
|
|
135
|
+
when Hash
|
|
136
|
+
server_config[:name] || server_config['name'] || server_config.to_s
|
|
137
|
+
when MCP::Server
|
|
138
|
+
server_config.name
|
|
139
|
+
else
|
|
140
|
+
server_config.to_s
|
|
141
|
+
end
|
|
142
|
+
end
|
|
86
143
|
end
|
|
87
144
|
end
|
|
88
145
|
end
|
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -66,7 +66,7 @@ module RobotLab
|
|
|
66
66
|
|
|
67
67
|
attr_reader :name, :description, :template, :system_prompt,
|
|
68
68
|
:local_tools, :mcp_clients, :mcp_tools, :memory,
|
|
69
|
-
:bus, :outbox, :config, :skills
|
|
69
|
+
:bus, :outbox, :config, :skills, :provider
|
|
70
70
|
|
|
71
71
|
# @!attribute [r] mcp_config
|
|
72
72
|
# @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
|
|
@@ -108,6 +108,7 @@ module RobotLab
|
|
|
108
108
|
description: nil,
|
|
109
109
|
local_tools: [],
|
|
110
110
|
model: nil,
|
|
111
|
+
provider: nil,
|
|
111
112
|
mcp_servers: [],
|
|
112
113
|
mcp: :none,
|
|
113
114
|
tools: :none,
|
|
@@ -187,6 +188,15 @@ module RobotLab
|
|
|
187
188
|
resolved_model = @config.model || config.ruby_llm.model
|
|
188
189
|
chat_kwargs = { model: resolved_model }
|
|
189
190
|
|
|
191
|
+
# Pass provider through for local providers (Ollama, GPUStack, etc.)
|
|
192
|
+
# RubyLLM auto-sets assume_model_exists for local providers when
|
|
193
|
+
# provider is specified.
|
|
194
|
+
@provider = provider
|
|
195
|
+
if @provider
|
|
196
|
+
chat_kwargs[:provider] = @provider
|
|
197
|
+
chat_kwargs[:assume_model_exists] = true
|
|
198
|
+
end
|
|
199
|
+
|
|
190
200
|
# Create the persistent chat via Agent's initialize
|
|
191
201
|
super(chat: nil, **chat_kwargs)
|
|
192
202
|
|
|
@@ -354,11 +364,26 @@ module RobotLab
|
|
|
354
364
|
# Extract the message from run context
|
|
355
365
|
message = run_context.delete(:message)
|
|
356
366
|
|
|
367
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
357
368
|
robot_result = run(message, **run_context)
|
|
369
|
+
robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
358
370
|
|
|
359
371
|
result
|
|
360
372
|
.with_context(@name.to_sym, robot_result)
|
|
361
373
|
.continue(robot_result)
|
|
374
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
375
|
+
# Catch all errors (including SecurityError, Timeout::Error, etc.)
|
|
376
|
+
# so one failing robot doesn't crash the entire network pipeline.
|
|
377
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
378
|
+
error_result = RobotResult.new(
|
|
379
|
+
robot_name: @name,
|
|
380
|
+
output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
|
|
381
|
+
)
|
|
382
|
+
error_result.duration = elapsed
|
|
383
|
+
|
|
384
|
+
result
|
|
385
|
+
.with_context(@name.to_sym, error_result)
|
|
386
|
+
.continue(error_result)
|
|
362
387
|
end
|
|
363
388
|
|
|
364
389
|
|
|
@@ -371,6 +396,26 @@ module RobotLab
|
|
|
371
396
|
end
|
|
372
397
|
|
|
373
398
|
|
|
399
|
+
# Eagerly connect to configured MCP servers and discover tools.
|
|
400
|
+
# Normally MCP connections are lazy (established on first run).
|
|
401
|
+
# Call this to connect early, e.g. to display connection status at startup.
|
|
402
|
+
#
|
|
403
|
+
# @return [self]
|
|
404
|
+
def connect_mcp!
|
|
405
|
+
resolved_mcp = resolve_mcp_hierarchy(@mcp_config)
|
|
406
|
+
ensure_mcp_clients(resolved_mcp) if resolved_mcp.is_a?(Array) && resolved_mcp.any?
|
|
407
|
+
self
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Returns server names that failed to connect.
|
|
411
|
+
#
|
|
412
|
+
# @return [Array<String>]
|
|
413
|
+
def failed_mcp_server_names
|
|
414
|
+
return [] unless @failed_mcp_configs
|
|
415
|
+
|
|
416
|
+
@failed_mcp_configs.keys
|
|
417
|
+
end
|
|
418
|
+
|
|
374
419
|
# Disconnect all MCP clients and bus channel.
|
|
375
420
|
#
|
|
376
421
|
# @return [self]
|
|
@@ -381,6 +426,83 @@ module RobotLab
|
|
|
381
426
|
end
|
|
382
427
|
|
|
383
428
|
|
|
429
|
+
# --- Public APIs for external MCP and history management (A4) ---
|
|
430
|
+
|
|
431
|
+
# Inject pre-connected MCP clients and their tools into this robot.
|
|
432
|
+
# Used by host applications (e.g. AIA) that manage MCP connections
|
|
433
|
+
# externally and need to pass them to robots without re-connecting.
|
|
434
|
+
#
|
|
435
|
+
# @param clients [Hash<String, MCP::Client>] connected MCP clients by server name
|
|
436
|
+
# @param tools [Array<Tool>] tools discovered from the MCP servers
|
|
437
|
+
# @return [self]
|
|
438
|
+
def inject_mcp!(clients:, tools:)
|
|
439
|
+
@mcp_clients = clients
|
|
440
|
+
@mcp_tools = tools
|
|
441
|
+
@mcp_initialized = true
|
|
442
|
+
self
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Access the underlying RubyLLM::Chat instance.
|
|
446
|
+
# Useful for checkpoint/restore operations that need direct
|
|
447
|
+
# access to conversation state.
|
|
448
|
+
#
|
|
449
|
+
# @return [RubyLLM::Chat]
|
|
450
|
+
def chat
|
|
451
|
+
@chat
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Return the conversation messages from the underlying chat.
|
|
455
|
+
#
|
|
456
|
+
# @return [Array<RubyLLM::Message>]
|
|
457
|
+
def messages
|
|
458
|
+
@chat.messages
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Clear conversation messages, optionally keeping the system prompt.
|
|
462
|
+
#
|
|
463
|
+
# @param keep_system [Boolean] whether to preserve the system message
|
|
464
|
+
# @return [self]
|
|
465
|
+
def clear_messages(keep_system: true)
|
|
466
|
+
if keep_system
|
|
467
|
+
system_msg = @chat.messages.find { |m| m.role == :system }
|
|
468
|
+
@chat.instance_variable_set(:@messages, [])
|
|
469
|
+
@chat.add_message(system_msg) if system_msg
|
|
470
|
+
else
|
|
471
|
+
@chat.instance_variable_set(:@messages, [])
|
|
472
|
+
end
|
|
473
|
+
self
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Replace conversation messages with a saved set (for checkpoint restore).
|
|
477
|
+
#
|
|
478
|
+
# @param messages [Array<RubyLLM::Message>] the messages to restore
|
|
479
|
+
# @return [self]
|
|
480
|
+
def replace_messages(messages)
|
|
481
|
+
@chat.instance_variable_set(:@messages, messages)
|
|
482
|
+
self
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Return the provider for this robot's chat.
|
|
486
|
+
# Useful for displaying model/provider info without reaching
|
|
487
|
+
# into chat internals.
|
|
488
|
+
#
|
|
489
|
+
# @return [String, nil]
|
|
490
|
+
def chat_provider
|
|
491
|
+
m = @chat.model
|
|
492
|
+
m.respond_to?(:provider) ? m.provider : nil
|
|
493
|
+
rescue StandardError
|
|
494
|
+
nil
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Find an MCP client by server name.
|
|
498
|
+
#
|
|
499
|
+
# @param server_name [String] the MCP server name
|
|
500
|
+
# @return [MCP::Client, nil]
|
|
501
|
+
def mcp_client(server_name)
|
|
502
|
+
@mcp_clients[server_name]
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
|
|
384
506
|
# Converts the robot to a hash representation
|
|
385
507
|
#
|
|
386
508
|
# @return [Hash]
|
|
@@ -460,7 +582,8 @@ module RobotLab
|
|
|
460
582
|
robot_name: @name,
|
|
461
583
|
output: output,
|
|
462
584
|
tool_calls: normalize_tool_calls(tool_calls),
|
|
463
|
-
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil
|
|
585
|
+
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil,
|
|
586
|
+
raw: response
|
|
464
587
|
)
|
|
465
588
|
end
|
|
466
589
|
|
|
@@ -30,13 +30,15 @@ module RobotLab
|
|
|
30
30
|
# @return [String, nil] reason execution stopped
|
|
31
31
|
attr_reader :robot_name, :output, :tool_calls, :created_at, :id, :stop_reason
|
|
32
32
|
|
|
33
|
+
# @!attribute [rw] duration
|
|
34
|
+
# @return [Float, nil] elapsed seconds for this run
|
|
33
35
|
# @!attribute [rw] prompt
|
|
34
36
|
# @return [Array<Message>, nil] the prompt messages used (debug)
|
|
35
37
|
# @!attribute [rw] history
|
|
36
38
|
# @return [Array<Message>, nil] the history used (debug)
|
|
37
39
|
# @!attribute [rw] raw
|
|
38
40
|
# @return [Object, nil] the raw LLM response (debug)
|
|
39
|
-
attr_accessor :prompt, :history, :raw
|
|
41
|
+
attr_accessor :duration, :prompt, :history, :raw
|
|
40
42
|
|
|
41
43
|
# Creates a new RobotResult instance.
|
|
42
44
|
#
|
data/lib/robot_lab/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -273,6 +273,7 @@ files:
|
|
|
273
273
|
- examples/11_network_introspection.rb
|
|
274
274
|
- examples/12_message_bus.rb
|
|
275
275
|
- examples/13_spawn.rb
|
|
276
|
+
- examples/14_rusty_circuit/.gitignore
|
|
276
277
|
- examples/14_rusty_circuit/comic.rb
|
|
277
278
|
- examples/14_rusty_circuit/display.rb
|
|
278
279
|
- examples/14_rusty_circuit/heckler.rb
|
|
@@ -463,7 +464,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
463
464
|
- !ruby/object:Gem::Version
|
|
464
465
|
version: '0'
|
|
465
466
|
requirements: []
|
|
466
|
-
rubygems_version: 4.0.
|
|
467
|
+
rubygems_version: 4.0.7
|
|
467
468
|
specification_version: 4
|
|
468
469
|
summary: Ruby framework for building and orchestrating multi-robot LLM workflows
|
|
469
470
|
test_files: []
|