robot_lab 0.0.11 → 0.1.0
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 +57 -0
- data/README.md +180 -0
- data/docs/api/core/result.md +123 -0
- data/docs/api/errors.md +185 -0
- data/docs/api/messages/index.md +21 -0
- data/docs/getting-started/configuration.md +1 -1
- data/docs/guides/building-robots.md +125 -0
- data/docs/guides/creating-networks.md +23 -0
- data/docs/guides/rails-integration.md +52 -13
- data/examples/18_rails/app/jobs/robot_run_job.rb +15 -75
- data/examples/31_launch_assessment.rb +248 -0
- data/examples/README.md +9 -0
- data/lib/generators/robot_lab/job_generator.rb +40 -0
- data/lib/generators/robot_lab/templates/job.rb.tt +10 -81
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +18 -0
- data/lib/robot_lab/message.rb +1 -1
- data/lib/robot_lab/network.rb +1 -1
- data/lib/robot_lab/rails_integration/job.rb +158 -0
- data/lib/robot_lab/rails_integration/railtie.rb +9 -0
- data/lib/robot_lab/run_config.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +4 -0
- metadata +9 -4
- data/.github/workflows/deploy-yard-docs.yml +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a1898a9909e7fdd795350b7f10cf80c57d69c1cec1b2533e56b9652cc2b3930
|
|
4
|
+
data.tar.gz: 641d4bd4022788a4ce53965a1ff8413ccf1420f71a1981f975618f825c29b80c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d51060ef5a929f5d9b895ba7e11220d47765fb92a342e9c3c76d5c223043747d53e7b924c9ecc53fccc90b90a0b08c180e75d5955527cc8b3cd99411a9c64026
|
|
7
|
+
data.tar.gz: c59f404518a69d97c65d960982367c582b8410d3754d3cf238ab8117f400eb229a02891bb449ab311bceaa16f0382c6cfd355c8f976bcc8f49b8191f8035e221
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0] - 2026-04-29
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`RobotLab::Job` base class** (`lib/robot_lab/rails_integration/job.rb`) — `ActiveJob::Base` subclass encapsulating the full robot-run lifecycle for Rails background jobs
|
|
16
|
+
- `robot_class` DSL — bind a job subclass to a specific robot class at the class level; per-subclass, not inherited
|
|
17
|
+
- `perform(message:, robot_class: nil, thread_id: nil, **context)` — resolves robot class, wires Turbo Stream callbacks, runs robot, persists `RobotResult`, broadcasts completion/error
|
|
18
|
+
- `thread_id` omitted → fire-and-forget mode (no persistence, no broadcasting)
|
|
19
|
+
- Turbo Stream wiring is a graceful no-op when `turbo-rails` is absent
|
|
20
|
+
- `retry_on StandardError, wait: 5.seconds, attempts: 3` and `discard_on ActiveJob::DeserializationError` configured by default
|
|
21
|
+
- `RobotLab::Job` top-level alias registered in `robot_lab.rb` when Rails is present so job subclasses can write `< RobotLab::Job`
|
|
22
|
+
- **`rails generate robot_lab:job NAME`** (`lib/generators/robot_lab/job_generator.rb`) — generates a dedicated job subclass pre-wired to `<NAME>Robot` via `robot_class` DSL
|
|
23
|
+
- `--queue` option (default `"default"`)
|
|
24
|
+
- Template: `lib/generators/robot_lab/templates/robot_job.rb.tt`
|
|
25
|
+
- **`max_concurrent_robots` field on `RunConfig`** — caps the number of fiber-concurrent robots in a parallel network execution; passed to `SimpleFlow::Pipeline#call_parallel` as `max_concurrent:`
|
|
26
|
+
- **Example 31: Launch Assessment** (`examples/31_launch_assessment.rb`) — six `AnalystRobot` instances run in parallel (market, competitive, tech, risk, financial, legal) with a cap of 4 concurrent robots; a `LaunchDirector` synthesizes findings into a GO/NO-GO decision
|
|
27
|
+
- **20 unit tests for `RobotLab::RailsIntegration::Job`** (`test/robot_lab/rails_integration/job_test.rb`) covering `robot_class` DSL, `resolve_robot_class`, `setup_thread`, `build_robot`, `broadcast_completion`, `broadcast_error`, and `turbo_available?`
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Bumped version to 0.1.0
|
|
32
|
+
- **`RobotRunJob` (generated job)** is now a thin two-line subclass of `RobotLab::Job` — all lifecycle logic lives in the base class
|
|
33
|
+
- **`job.rb.tt` install generator template** updated to the thin-subclass pattern
|
|
34
|
+
- **`examples/18_rails` `RobotRunJob`** updated to thin subclass
|
|
35
|
+
- **`Network#call_parallel`** now forwards `max_concurrent: @config.max_concurrent_robots` to `SimpleFlow::Pipeline`, enabling the concurrency cap introduced in `RunConfig`
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`Message.from_hash`** — records persisted without a `type` key (e.g. legacy user-message rows) previously raised `ArgumentError: missing keyword: :type`; `from_hash` now defaults a nil or absent `type` to `"text"` so old rows deserialize as `TextMessage` without error
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- **`docs/guides/rails-integration.md`** — rewrote Background Jobs section to document `RobotLab::Job` base class, lifecycle steps, `robot_class` DSL, dedicated job generator, fire-and-forget mode, and when to use a custom `ApplicationJob` instead
|
|
44
|
+
- **`docs/api/messages/index.md`** — added "Deserializing from Hash" section documenting `Message.from_hash` dispatch logic and the missing-type fallback
|
|
45
|
+
- **README.md** — expanded Rails Integration section with full Background Jobs documentation including both generic and dedicated job patterns
|
|
46
|
+
|
|
47
|
+
## [0.0.12] - 2026-04-18
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **README: Context Window Compression section** — documents `robot.compress_history` with threshold tuning (`recent_turns`, `keep_threshold`, `drop_threshold`) and summarizer lambda pattern
|
|
52
|
+
- **README: Convergence Detection section** — documents `RobotLab::Convergence.detected?` / `.similarity` with network router fast-path example
|
|
53
|
+
- **README: Structured Delegation section** — documents `robot.delegate(to:, task:)` sync and async modes, `DelegationFuture` fan-out pattern, and timeout handling
|
|
54
|
+
- **README: Ractor Parallelism section** — documents `ractor_safe true` tool macro and `parallel_mode: :ractor` network mode with link to full guide
|
|
55
|
+
- **`docs/guides/building-robots.md`** — added matching sections for all four features above with expanded API detail, `DelegationFuture` method table, and convergence router example
|
|
56
|
+
- **`docs/api/core/result.md`** — new API reference for `RobotResult`: attributes, token tracking, delegation metadata, persistence (`export`, `from_hash`, `checksum`), and debug fields
|
|
57
|
+
- **`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
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- Bumped version to 0.0.12
|
|
62
|
+
- Updated `bigdecimal` to 4.1.2
|
|
63
|
+
- Updated `protocol-http` to 0.62.2
|
|
64
|
+
- Updated `protocol-websocket` to 0.21.0
|
|
65
|
+
- Updated `rake` to 13.4.2
|
|
66
|
+
- Updated `sqlite3` to 2.9.3
|
|
67
|
+
|
|
11
68
|
## [0.0.11] - 2026-04-14
|
|
12
69
|
|
|
13
70
|
### 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
|
|
@@ -712,6 +842,56 @@ This creates:
|
|
|
712
842
|
- `app/robots/` - Directory for your robots
|
|
713
843
|
- Database tables for conversation history
|
|
714
844
|
|
|
845
|
+
### Background Jobs
|
|
846
|
+
|
|
847
|
+
RobotLab ships with `RobotLab::Job`, an `ActiveJob::Base` subclass that handles the full robot-run lifecycle: robot class resolution, Turbo Stream wiring, thread-record persistence, and completion/error broadcasting.
|
|
848
|
+
|
|
849
|
+
**Generic job** (robot class supplied at enqueue time):
|
|
850
|
+
|
|
851
|
+
```bash
|
|
852
|
+
rails generate robot_lab:install # creates app/jobs/robot_run_job.rb
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
```ruby
|
|
856
|
+
# app/jobs/robot_run_job.rb (generated)
|
|
857
|
+
class RobotRunJob < RobotLab::Job
|
|
858
|
+
queue_as :default
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Enqueue from a controller:
|
|
862
|
+
RobotRunJob.perform_later(
|
|
863
|
+
robot_class: "SupportRobot",
|
|
864
|
+
message: params[:message],
|
|
865
|
+
thread_id: session_id
|
|
866
|
+
)
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**Dedicated job** (robot class bound at the class level via DSL):
|
|
870
|
+
|
|
871
|
+
```bash
|
|
872
|
+
rails generate robot_lab:job Support # binds to SupportRobot, queue: default
|
|
873
|
+
rails generate robot_lab:job Support --queue ai # custom queue
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
```ruby
|
|
877
|
+
# app/jobs/support_job.rb (generated)
|
|
878
|
+
class SupportJob < RobotLab::Job
|
|
879
|
+
queue_as :default
|
|
880
|
+
robot_class SupportRobot
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Enqueue (no robot_class: needed):
|
|
884
|
+
SupportJob.perform_later(message: params[:message], thread_id: session_id)
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
|
|
888
|
+
|
|
889
|
+
- Wires `on_content` / `on_tool_call` Turbo Stream callbacks so the UI updates in real time
|
|
890
|
+
- Broadcasts a **completion** event to `"robot_lab_thread_#{thread_id}"` when the run finishes
|
|
891
|
+
- Broadcasts an **error** event (HTML-escaped) if the job raises
|
|
892
|
+
|
|
893
|
+
Omitting `thread_id` runs the robot in fire-and-forget mode — no persistence, no broadcasting.
|
|
894
|
+
|
|
715
895
|
## Documentation
|
|
716
896
|
|
|
717
897
|
Full documentation is available at **[https://madbomber.github.io/robot_lab](https://madbomber.github.io/robot_lab)**
|
|
@@ -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
|
data/docs/api/messages/index.md
CHANGED
|
@@ -76,6 +76,27 @@ memory.messages # => Array<Message>
|
|
|
76
76
|
memory.format_history # => Array<Message>
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Deserializing from Hash
|
|
80
|
+
|
|
81
|
+
`Message.from_hash` reconstructs the correct subclass from a stored hash:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
RobotLab::Message.from_hash({ type: "text", role: "user", content: "Hello" })
|
|
85
|
+
# => #<RobotLab::TextMessage ...>
|
|
86
|
+
|
|
87
|
+
RobotLab::Message.from_hash({ type: "tool_call", role: "assistant", tools: [...] })
|
|
88
|
+
# => #<RobotLab::ToolCallMessage ...>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
When the `type` key is absent or `nil` (e.g. records persisted before the field was introduced), `from_hash` defaults to `TextMessage`:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
RobotLab::Message.from_hash({ role: "user", content: "legacy row" })
|
|
95
|
+
# => #<RobotLab::TextMessage ...>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
String keys are normalised automatically via `transform_keys(&:to_sym)`.
|
|
99
|
+
|
|
79
100
|
## See Also
|
|
80
101
|
|
|
81
102
|
- [Memory](../core/memory.md)
|
|
@@ -361,7 +361,7 @@ effective.temperature #=> 0.9 (overridden)
|
|
|
361
361
|
| **LLM** | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
|
|
362
362
|
| **Tools** | `mcp`, `tools` |
|
|
363
363
|
| **Callbacks** | `on_tool_call`, `on_tool_result` |
|
|
364
|
-
| **Infrastructure** | `bus`, `enable_cache` |
|
|
364
|
+
| **Infrastructure** | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `ractor_pool_size`, `max_concurrent_robots` |
|
|
365
365
|
|
|
366
366
|
### RunConfig vs RobotLab.config
|
|
367
367
|
|