robot_lab 0.0.11 → 0.0.12
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 +21 -0
- data/README.md +130 -0
- data/docs/api/core/result.md +123 -0
- data/docs/api/errors.md +185 -0
- data/docs/guides/building-robots.md +125 -0
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a8ae2e2cf690116950548d732987e16756870f8444c91504ea14fe039f25996
|
|
4
|
+
data.tar.gz: 115694d1449233b3a17a28e87deda8bd3d0ac204f51301aee7781156a3b2003e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d512eea2ce533c92b4f791c0d3527fe61805fdca2638e926c0869e0e8f5b0c9a9dc5bac0791db2e5bae8a326b46eaea31c5ffae9ba761b17d1b93b3113735087
|
|
7
|
+
data.tar.gz: 7e6025d5bbe7252e61e4d7922eea639cda523d7c197535e46400caeeec7f30e7554a015cada107eef796a47c10564d286cdb1d2d4b539a7ac51cc71975b65352
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.0.12] - 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **README: Context Window Compression section** — documents `robot.compress_history` with threshold tuning (`recent_turns`, `keep_threshold`, `drop_threshold`) and summarizer lambda pattern
|
|
16
|
+
- **README: Convergence Detection section** — documents `RobotLab::Convergence.detected?` / `.similarity` with network router fast-path example
|
|
17
|
+
- **README: Structured Delegation section** — documents `robot.delegate(to:, task:)` sync and async modes, `DelegationFuture` fan-out pattern, and timeout handling
|
|
18
|
+
- **README: Ractor Parallelism section** — documents `ractor_safe true` tool macro and `parallel_mode: :ractor` network mode with link to full guide
|
|
19
|
+
- **`docs/guides/building-robots.md`** — added matching sections for all four features above with expanded API detail, `DelegationFuture` method table, and convergence router example
|
|
20
|
+
- **`docs/api/core/result.md`** — new API reference for `RobotResult`: attributes, token tracking, delegation metadata, persistence (`export`, `from_hash`, `checksum`), and debug fields
|
|
21
|
+
- **`docs/api/errors.md`** — new error hierarchy reference covering all `RobotLab::Error` subclasses (`ConfigurationError`, `DependencyError`, `InferenceError`, `ToolLoopError`, `ToolNotFoundError`, `MCPError`, `BusError`, `RactorBoundaryError`, `ToolError`, `DelegationFuture::DelegationTimeout`) with rescue examples
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Bumped version to 0.0.12
|
|
26
|
+
- Updated `bigdecimal` to 4.1.2
|
|
27
|
+
- Updated `protocol-http` to 0.62.2
|
|
28
|
+
- Updated `protocol-websocket` to 0.21.0
|
|
29
|
+
- Updated `rake` to 13.4.2
|
|
30
|
+
- Updated `sqlite3` to 2.9.3
|
|
31
|
+
|
|
11
32
|
## [0.0.11] - 2026-04-14
|
|
12
33
|
|
|
13
34
|
### Added
|
data/README.md
CHANGED
|
@@ -700,6 +700,136 @@ reviewer.learnings # => ["This codebase prefers map/collect..."]
|
|
|
700
700
|
reviewer.learn("new fact") # deduplicates before storing
|
|
701
701
|
```
|
|
702
702
|
|
|
703
|
+
## Context Window Compression
|
|
704
|
+
|
|
705
|
+
`robot.compress_history` prunes old conversation turns using TF-IDF cosine similarity, keeping only turns that are relevant to the most recent context. System messages and tool call/result pairs are always preserved.
|
|
706
|
+
|
|
707
|
+
```ruby
|
|
708
|
+
# Basic compression: protect the 3 most recent turns, drop unrelated old turns
|
|
709
|
+
robot.compress_history
|
|
710
|
+
|
|
711
|
+
# Tune the thresholds
|
|
712
|
+
robot.compress_history(
|
|
713
|
+
recent_turns: 5, # protect this many recent user+assistant pairs
|
|
714
|
+
keep_threshold: 0.6, # turns scoring >= this are kept verbatim
|
|
715
|
+
drop_threshold: 0.2 # turns scoring < this are dropped
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Summarize medium-relevance turns instead of dropping them
|
|
719
|
+
summarizer_bot = RobotLab.build(name: "summarizer", system_prompt: "Summarize concisely.")
|
|
720
|
+
robot.compress_history(
|
|
721
|
+
summarizer: ->(text) { summarizer_bot.run("One sentence: #{text}").reply }
|
|
722
|
+
)
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Requires the optional `classifier` gem (`~> 2.3`). Add it to your Gemfile:
|
|
726
|
+
|
|
727
|
+
```ruby
|
|
728
|
+
gem "classifier", "~> 2.3"
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## Convergence Detection
|
|
732
|
+
|
|
733
|
+
`RobotLab::Convergence` detects when two independent agents have reached the same conclusion using TF-IDF cosine similarity. Use it as a router fast-path to skip an expensive reconciler LLM call when verifiers already agree.
|
|
734
|
+
|
|
735
|
+
```ruby
|
|
736
|
+
# Check similarity directly
|
|
737
|
+
score = RobotLab::Convergence.similarity(result_a.reply, result_b.reply)
|
|
738
|
+
# => 0.92
|
|
739
|
+
|
|
740
|
+
# Boolean check against a threshold (default: 0.85)
|
|
741
|
+
RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
742
|
+
# => true
|
|
743
|
+
|
|
744
|
+
# Use a custom threshold
|
|
745
|
+
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
A common pattern is wiring convergence into a network router to skip reconciliation:
|
|
749
|
+
|
|
750
|
+
```ruby
|
|
751
|
+
router = ->(args) do
|
|
752
|
+
a = args.context[:verifier_a]&.reply.to_s
|
|
753
|
+
b = args.context[:verifier_b]&.reply.to_s
|
|
754
|
+
RobotLab::Convergence.detected?(a, b) ? nil : ["reconciler"]
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
network = RobotLab.create_network(name: "verify", router: router) do
|
|
758
|
+
# ...
|
|
759
|
+
end
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Requires the `classifier` gem (`~> 2.3`).
|
|
763
|
+
|
|
764
|
+
## Structured Delegation
|
|
765
|
+
|
|
766
|
+
`robot.delegate(to:, task:)` dispatches work to another robot and returns the result, with duration and token metadata attached. Pass `async: true` for non-blocking fan-out.
|
|
767
|
+
|
|
768
|
+
```ruby
|
|
769
|
+
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
|
|
770
|
+
writer = RobotLab.build(name: "writer", system_prompt: "Write reports.")
|
|
771
|
+
manager = RobotLab.build(name: "manager", system_prompt: "Coordinate work.")
|
|
772
|
+
|
|
773
|
+
# Synchronous delegation — blocks until done
|
|
774
|
+
result = manager.delegate(to: analyst, task: "Analyze Q3 sales data")
|
|
775
|
+
puts result.reply
|
|
776
|
+
puts "%.2fs, %d tokens" % [result.duration, result.output_tokens]
|
|
777
|
+
|
|
778
|
+
# Asynchronous fan-out — returns immediately
|
|
779
|
+
f1 = manager.delegate(to: analyst, task: "Analyze Q3 sales", async: true)
|
|
780
|
+
f2 = manager.delegate(to: writer, task: "Draft Q3 summary", async: true)
|
|
781
|
+
|
|
782
|
+
# Do other work here while both run in parallel...
|
|
783
|
+
|
|
784
|
+
analysis = f1.value # blocks until resolved
|
|
785
|
+
summary = f2.value # blocks until resolved
|
|
786
|
+
|
|
787
|
+
# With a timeout
|
|
788
|
+
result = f1.value(timeout: 30) # raises DelegationFuture::DelegationTimeout if too slow
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
`DelegationFuture` attributes:
|
|
792
|
+
|
|
793
|
+
```ruby
|
|
794
|
+
future.resolved? # => true/false (non-blocking poll)
|
|
795
|
+
future.robot_name # => "analyst"
|
|
796
|
+
future.delegated_by # => "manager"
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## Ractor Parallelism
|
|
800
|
+
|
|
801
|
+
RobotLab supports true CPU parallelism via Ruby Ractors — isolated execution contexts that bypass the GVL. Two modes are available:
|
|
802
|
+
|
|
803
|
+
**CPU-bound tools** — mark a tool `ractor_safe true` and RobotLab automatically routes its calls through a global `RactorWorkerPool` instead of running inline:
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
class TranscribeAudio < RubyLLM::Tool
|
|
807
|
+
ractor_safe true
|
|
808
|
+
description "Transcribe an audio file"
|
|
809
|
+
param :path, type: :string, desc: "Path to audio file"
|
|
810
|
+
|
|
811
|
+
def execute(path:)
|
|
812
|
+
AudioTranscriber.run(path) # pure computation, no shared mutable state
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
**Parallel robot networks** — pass `parallel_mode: :ractor` when creating a network to dispatch independent robots across hardware threads simultaneously:
|
|
818
|
+
|
|
819
|
+
```ruby
|
|
820
|
+
network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
|
|
821
|
+
task :fetch, fetcher_robot, depends_on: :none
|
|
822
|
+
task :sentiment, sentiment_robot, depends_on: [:fetch]
|
|
823
|
+
task :entities, entity_robot, depends_on: [:fetch] # runs in parallel with sentiment
|
|
824
|
+
task :summarize, summary_robot, depends_on: [:sentiment, :entities]
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
results = network.run(message: "Analyze customer feedback")
|
|
828
|
+
# => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
See the [Ractor Parallelism guide](https://madbomber.github.io/robot_lab/guides/ractor-parallelism) for constraints, the frozen-data contract, and `RactorMemoryProxy` for shared state.
|
|
832
|
+
|
|
703
833
|
## Rails Integration
|
|
704
834
|
|
|
705
835
|
```bash
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# RobotResult
|
|
2
|
+
|
|
3
|
+
`RobotResult` is returned by every `robot.run()` call. It carries the LLM output, tool call results, token usage, timing, and delegation metadata for that execution.
|
|
4
|
+
|
|
5
|
+
## Accessing the Response
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
result = robot.run("What is the capital of France?")
|
|
9
|
+
|
|
10
|
+
result.reply # => "The capital of France is Paris."
|
|
11
|
+
result.last_text_content # => alias for reply
|
|
12
|
+
result.output # => Array of Message objects (full turn)
|
|
13
|
+
result.tool_calls # => Array of ToolResultMessage objects
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`reply` / `last_text_content` returns the content of the last text message in `output`. This is the string you want for the vast majority of use cases.
|
|
17
|
+
|
|
18
|
+
## Token & Cost Tracking
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
result.input_tokens # => Integer — tokens sent to the LLM this run
|
|
22
|
+
result.output_tokens # => Integer — tokens generated this run
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Token counts are zero for providers that do not return usage data.
|
|
26
|
+
|
|
27
|
+
## Timing
|
|
28
|
+
|
|
29
|
+
`duration` is set when the result travels through a network pipeline or a `delegate` call. It is `nil` when calling `robot.run()` directly.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
result.duration # => Float (elapsed seconds) or nil
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Delegation Metadata
|
|
36
|
+
|
|
37
|
+
When a result comes back through `robot.delegate(to:, task:)`, two additional fields are populated:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
result.delegated_by # => "manager" (the robot that issued the delegation)
|
|
41
|
+
result.duration # => 2.34 (always set by delegate)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Identity & Status
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
result.robot_name # => "analyst"
|
|
48
|
+
result.id # => "550e8400-e29b-..." (UUID, unique per run)
|
|
49
|
+
result.created_at # => Time instance
|
|
50
|
+
result.stop_reason # => "end_turn", "tool_use", or nil
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Inspecting the Full Output
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
result.output.each do |message|
|
|
57
|
+
puts message.role # :assistant, :tool, etc.
|
|
58
|
+
puts message.content # String or Array
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
result.has_tool_calls? # => true if the LLM called any tools
|
|
62
|
+
result.stopped? # => true if execution ended naturally (not mid-tool-call)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Persistence
|
|
66
|
+
|
|
67
|
+
Export for serialization (excludes debug fields):
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
hash = result.export
|
|
71
|
+
# {
|
|
72
|
+
# robot_name: "analyst",
|
|
73
|
+
# output: [...],
|
|
74
|
+
# tool_calls: [...],
|
|
75
|
+
# created_at: "2026-04-18T12:00:00Z",
|
|
76
|
+
# id: "550e8400-...",
|
|
77
|
+
# checksum: "a1b2c3...",
|
|
78
|
+
# stop_reason: "end_turn",
|
|
79
|
+
# duration: 2.34,
|
|
80
|
+
# input_tokens: 512,
|
|
81
|
+
# output_tokens: 128
|
|
82
|
+
# }
|
|
83
|
+
|
|
84
|
+
json = result.to_json
|
|
85
|
+
|
|
86
|
+
# Reconstruct from hash
|
|
87
|
+
restored = RobotLab::RobotResult.from_hash(hash)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`checksum` is a SHA-256 digest of `output + tool_calls + created_at`. Use it for deduplication when persisting results.
|
|
91
|
+
|
|
92
|
+
## Debug Fields
|
|
93
|
+
|
|
94
|
+
These are `nil` by default and only populated when explicitly set for debugging:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
result.prompt # Array<Message> — prompt sent to the LLM
|
|
98
|
+
result.history # Array<Message> — history used
|
|
99
|
+
result.raw # raw LLM response object from ruby_llm
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Attribute Reference
|
|
103
|
+
|
|
104
|
+
| Attribute | Type | Description |
|
|
105
|
+
|-----------|------|-------------|
|
|
106
|
+
| `robot_name` | String | Name of the robot that produced this result |
|
|
107
|
+
| `reply` | String, nil | Last text content (alias: `last_text_content`) |
|
|
108
|
+
| `output` | Array\<Message\> | All output messages from this run |
|
|
109
|
+
| `tool_calls` | Array\<ToolResultMessage\> | Tool call results |
|
|
110
|
+
| `input_tokens` | Integer | Tokens sent to LLM |
|
|
111
|
+
| `output_tokens` | Integer | Tokens generated |
|
|
112
|
+
| `duration` | Float, nil | Elapsed seconds (set by delegate/pipeline) |
|
|
113
|
+
| `delegated_by` | String, nil | Delegating robot's name |
|
|
114
|
+
| `id` | String | UUID |
|
|
115
|
+
| `created_at` | Time | Creation timestamp |
|
|
116
|
+
| `stop_reason` | String, nil | LLM stop reason |
|
|
117
|
+
| `checksum` | String | SHA-256 of output content |
|
|
118
|
+
|
|
119
|
+
## Related
|
|
120
|
+
|
|
121
|
+
- [Robot API](robot.md) — `run`, `delegate`, `compress_history`
|
|
122
|
+
- [Building Robots](../../guides/building-robots.md) — Robot construction patterns
|
|
123
|
+
- [Structured Delegation](../../guides/building-robots.md#structured-delegation) — `DelegationFuture` and async fan-out
|
data/docs/api/errors.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Error Reference
|
|
2
|
+
|
|
3
|
+
All RobotLab exceptions inherit from `RobotLab::Error`, which inherits from `StandardError`. Rescue `RobotLab::Error` to catch any framework exception in one clause, or rescue specific subclasses for targeted handling.
|
|
4
|
+
|
|
5
|
+
## Hierarchy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
StandardError
|
|
9
|
+
└── RobotLab::Error
|
|
10
|
+
├── RobotLab::ConfigurationError
|
|
11
|
+
│ └── RobotLab::DependencyError
|
|
12
|
+
├── RobotLab::InferenceError
|
|
13
|
+
│ └── RobotLab::ToolLoopError
|
|
14
|
+
├── RobotLab::ToolNotFoundError
|
|
15
|
+
├── RobotLab::MCPError
|
|
16
|
+
├── RobotLab::BusError
|
|
17
|
+
├── RobotLab::RactorBoundaryError
|
|
18
|
+
└── RobotLab::ToolError
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Additionally, `DelegationFuture` defines its own scoped error:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
StandardError
|
|
25
|
+
└── RobotLab::Error
|
|
26
|
+
└── RobotLab::DelegationFuture::DelegationTimeout
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## RobotLab::Error
|
|
32
|
+
|
|
33
|
+
Base class for all RobotLab errors. Rescue this to catch any framework exception.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
begin
|
|
37
|
+
robot.run("hello")
|
|
38
|
+
rescue RobotLab::Error => e
|
|
39
|
+
puts "RobotLab error: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## RobotLab::ConfigurationError
|
|
46
|
+
|
|
47
|
+
Raised when configuration is invalid or missing required values.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Example: missing API key, invalid template path
|
|
51
|
+
rescue RobotLab::ConfigurationError => e
|
|
52
|
+
puts "Bad config: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## RobotLab::DependencyError < ConfigurationError
|
|
59
|
+
|
|
60
|
+
Raised when a required optional gem dependency is not installed.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Triggered by: Convergence, HistoryCompressor when 'classifier' gem is absent
|
|
64
|
+
rescue RobotLab::DependencyError => e
|
|
65
|
+
puts e.message # "Add gem 'classifier', '~> 2.3' to your Gemfile"
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## RobotLab::InferenceError
|
|
72
|
+
|
|
73
|
+
Raised when LLM inference fails (API errors, timeouts, rate limits).
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
rescue RobotLab::InferenceError => e
|
|
77
|
+
puts "LLM call failed: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## RobotLab::ToolLoopError < InferenceError
|
|
84
|
+
|
|
85
|
+
Raised when a robot's tool call count exceeds `max_tool_rounds:`. The chat history will contain a dangling `tool_use` block with no matching `tool_result`; call `robot.clear_messages` before reusing the robot.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
robot = RobotLab.build(
|
|
89
|
+
name: "runner",
|
|
90
|
+
system_prompt: "Execute every step.",
|
|
91
|
+
local_tools: [StepTool],
|
|
92
|
+
max_tool_rounds: 10
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
robot.run("Run all steps.")
|
|
97
|
+
rescue RobotLab::ToolLoopError => e
|
|
98
|
+
puts e.message # "Tool call limit of 10 exceeded"
|
|
99
|
+
robot.clear_messages # required before reuse
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## RobotLab::ToolNotFoundError
|
|
106
|
+
|
|
107
|
+
Raised when a tool name is referenced but cannot be found in the `ToolManifest`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## RobotLab::MCPError
|
|
112
|
+
|
|
113
|
+
Raised when MCP server communication fails (connection refused, timeout, protocol error).
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
rescue RobotLab::MCPError => e
|
|
117
|
+
puts "MCP failed: #{e.message}"
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## RobotLab::BusError
|
|
124
|
+
|
|
125
|
+
Raised when message bus communication fails (no bus configured, channel not found).
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
rescue RobotLab::BusError => e
|
|
129
|
+
puts "Bus error: #{e.message}"
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## RobotLab::RactorBoundaryError
|
|
136
|
+
|
|
137
|
+
Raised when a value cannot be made Ractor-shareable before crossing a Ractor boundary (e.g., a live `IO` object, a `Proc`, or an object with mutable state).
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
begin
|
|
141
|
+
RobotLab::RactorBoundary.freeze_deep({ io: StringIO.new })
|
|
142
|
+
rescue RobotLab::RactorBoundaryError => e
|
|
143
|
+
puts e.message # "Cannot make value Ractor-shareable: ..."
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Raised proactively by `RactorWorkerPool#submit` and `RactorMemoryProxy#set` before any Ractor is involved.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## RobotLab::ToolError
|
|
152
|
+
|
|
153
|
+
Raised when a tool fails during execution inside a Ractor worker (the pool unwraps `RactorJobError` and re-raises as `ToolError`).
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
begin
|
|
157
|
+
pool.submit("MyTool", { input: "bad" })
|
|
158
|
+
rescue RobotLab::ToolError => e
|
|
159
|
+
puts e.message # "Tool 'MyTool' failed in Ractor: ..."
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## RobotLab::DelegationFuture::DelegationTimeout
|
|
166
|
+
|
|
167
|
+
Raised by `DelegationFuture#value(timeout: N)` when the delegated task does not complete within `N` seconds.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
future = manager.delegate(to: analyst, task: "...", async: true)
|
|
171
|
+
|
|
172
|
+
begin
|
|
173
|
+
result = future.value(timeout: 10)
|
|
174
|
+
rescue RobotLab::DelegationFuture::DelegationTimeout => e
|
|
175
|
+
puts e.message # "Delegation to 'analyst' timed out after 10s"
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Related
|
|
182
|
+
|
|
183
|
+
- [Building Robots](../guides/building-robots.md) — Tool loop circuit breaker, delegation
|
|
184
|
+
- [Ractor Parallelism](../guides/ractor-parallelism.md) — Ractor boundary and tool errors
|
|
185
|
+
- [MCP Integration](../guides/mcp-integration.md) — MCP connection errors
|
|
@@ -736,6 +736,131 @@ bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
|
|
|
736
736
|
bot.with_bus(existing_bus) # now connected and can send/receive messages
|
|
737
737
|
```
|
|
738
738
|
|
|
739
|
+
## Context Window Compression
|
|
740
|
+
|
|
741
|
+
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using TF-IDF cosine similarity against the most recent context, keeping turns that are still relevant and discarding or summarizing those that aren't.
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
# Default settings: protect 3 most-recent turn pairs, drop anything below 0.2
|
|
745
|
+
robot.compress_history
|
|
746
|
+
|
|
747
|
+
# Tune all thresholds
|
|
748
|
+
robot.compress_history(
|
|
749
|
+
recent_turns: 5, # number of recent user+assistant pairs to always keep
|
|
750
|
+
keep_threshold: 0.6, # turns with score >= this are kept verbatim (default 0.6)
|
|
751
|
+
drop_threshold: 0.2 # turns with score < this are dropped (default 0.2)
|
|
752
|
+
)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Medium-relevance turns (between thresholds) are dropped by default. Pass a `summarizer:` callable to replace them with a one-sentence summary instead:
|
|
756
|
+
|
|
757
|
+
```ruby
|
|
758
|
+
summarizer = RobotLab.build(name: "summarizer", system_prompt: "Summarize concisely in one sentence.")
|
|
759
|
+
|
|
760
|
+
robot.compress_history(
|
|
761
|
+
summarizer: ->(text) { summarizer.run("Summarize: #{text}").reply }
|
|
762
|
+
)
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**What is always preserved regardless of score:**
|
|
766
|
+
- System messages
|
|
767
|
+
- Tool call/result message pairs (dropping half would corrupt the conversation)
|
|
768
|
+
- The most recent `recent_turns` user+assistant pairs
|
|
769
|
+
|
|
770
|
+
Requires the `classifier` gem (`~> 2.3`):
|
|
771
|
+
|
|
772
|
+
```ruby
|
|
773
|
+
gem "classifier", "~> 2.3"
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
## Convergence Detection
|
|
777
|
+
|
|
778
|
+
`RobotLab::Convergence` uses TF-IDF cosine similarity to detect when two independent agents have reached the same conclusion. The primary use case is a network router that skips an expensive reconciler robot when two verifiers already agree.
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
# Check the similarity score directly (returns Float 0.0..1.0)
|
|
782
|
+
score = RobotLab::Convergence.similarity(result_a.reply, result_b.reply)
|
|
783
|
+
|
|
784
|
+
# Boolean convergence check (default threshold: 0.85)
|
|
785
|
+
RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
786
|
+
|
|
787
|
+
# Custom threshold
|
|
788
|
+
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Wire it into a network router for the reconciler fast-path:
|
|
792
|
+
|
|
793
|
+
```ruby
|
|
794
|
+
verifier_a = RobotLab.build(name: "verifier_a", system_prompt: "Verify the answer.")
|
|
795
|
+
verifier_b = RobotLab.build(name: "verifier_b", system_prompt: "Independently verify the answer.")
|
|
796
|
+
reconciler = RobotLab.build(name: "reconciler", system_prompt: "Reconcile conflicting answers.")
|
|
797
|
+
|
|
798
|
+
router = lambda do |args|
|
|
799
|
+
a = args.context[:verifier_a]&.reply.to_s
|
|
800
|
+
b = args.context[:verifier_b]&.reply.to_s
|
|
801
|
+
|
|
802
|
+
# Skip reconciler when verifiers agree
|
|
803
|
+
RobotLab::Convergence.detected?(a, b) ? nil : ["reconciler"]
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
network = RobotLab.create_network(name: "verify", router: router) do
|
|
807
|
+
# ...
|
|
808
|
+
end
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Requires the `classifier` gem (`~> 2.3`).
|
|
812
|
+
|
|
813
|
+
## Structured Delegation
|
|
814
|
+
|
|
815
|
+
A robot can delegate a task to another robot using `delegate(to:, task:)`. The result is a `RobotResult` annotated with `delegated_by`, `duration`, and token counts.
|
|
816
|
+
|
|
817
|
+
### Synchronous Delegation
|
|
818
|
+
|
|
819
|
+
The default: blocks the calling robot until the delegatee finishes.
|
|
820
|
+
|
|
821
|
+
```ruby
|
|
822
|
+
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
|
|
823
|
+
manager = RobotLab.build(name: "manager", system_prompt: "Coordinate work.")
|
|
824
|
+
|
|
825
|
+
result = manager.delegate(to: analyst, task: "Summarize the Q3 report.")
|
|
826
|
+
puts result.reply
|
|
827
|
+
puts "Completed in %.2fs using %d tokens" % [result.duration, result.output_tokens]
|
|
828
|
+
puts result.delegated_by # => "manager"
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Asynchronous Delegation (Fan-out)
|
|
832
|
+
|
|
833
|
+
Pass `async: true` to get a `DelegationFuture` back immediately. Call `.value` to block for the result when you need it.
|
|
834
|
+
|
|
835
|
+
```ruby
|
|
836
|
+
writer = RobotLab.build(name: "writer", system_prompt: "Write clearly.")
|
|
837
|
+
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
|
|
838
|
+
manager = RobotLab.build(name: "manager", system_prompt: "Coordinate.")
|
|
839
|
+
|
|
840
|
+
# Fan out — both start immediately
|
|
841
|
+
f1 = manager.delegate(to: analyst, task: "Analyze Q3 numbers", async: true)
|
|
842
|
+
f2 = manager.delegate(to: writer, task: "Draft the intro paragraph", async: true)
|
|
843
|
+
|
|
844
|
+
# ... do other work here ...
|
|
845
|
+
|
|
846
|
+
# Collect results (blocks if not yet done)
|
|
847
|
+
analysis = f1.value
|
|
848
|
+
draft = f2.value
|
|
849
|
+
|
|
850
|
+
# With a timeout (raises DelegationFuture::DelegationTimeout)
|
|
851
|
+
result = f1.value(timeout: 30)
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
`DelegationFuture` API:
|
|
855
|
+
|
|
856
|
+
| Method | Description |
|
|
857
|
+
|--------|-------------|
|
|
858
|
+
| `resolved?` | Non-blocking poll — true if completed or errored |
|
|
859
|
+
| `value(timeout: nil)` | Block until done; re-raises any error from the delegatee |
|
|
860
|
+
| `wait` | Alias for `value` |
|
|
861
|
+
| `robot_name` | Name of the robot that was delegated to |
|
|
862
|
+
| `delegated_by` | Name of the robot that created this future |
|
|
863
|
+
|
|
739
864
|
## Configuration
|
|
740
865
|
|
|
741
866
|
RobotLab uses `MywayConfig` for configuration. Access the config object directly -- there is no `RobotLab.configure` block:
|
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.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -244,9 +244,11 @@ files:
|
|
|
244
244
|
- docs/api/core/index.md
|
|
245
245
|
- docs/api/core/memory.md
|
|
246
246
|
- docs/api/core/network.md
|
|
247
|
+
- docs/api/core/result.md
|
|
247
248
|
- docs/api/core/robot.md
|
|
248
249
|
- docs/api/core/state.md
|
|
249
250
|
- docs/api/core/tool.md
|
|
251
|
+
- docs/api/errors.md
|
|
250
252
|
- docs/api/index.md
|
|
251
253
|
- docs/api/mcp/client.md
|
|
252
254
|
- docs/api/mcp/index.md
|