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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4b2a3fafbdf3a54de3044b57597b42d86c68bd2afdad6ce866ac82483e61091
4
- data.tar.gz: 5137cff56485a26fabe5ab6606b144c4c3c21c1673ecec1d2254a392e015c25c
3
+ metadata.gz: 9a1898a9909e7fdd795350b7f10cf80c57d69c1cec1b2533e56b9652cc2b3930
4
+ data.tar.gz: 641d4bd4022788a4ce53965a1ff8413ccf1420f71a1981f975618f825c29b80c
5
5
  SHA512:
6
- metadata.gz: 33045f27ec803094a020caee4133c1d6c65887446330c294d9a9babd56a0fe7e71979fe0d032c2421488d1dcae886ad784a78dae579ba01b2786b5f9f91c0172
7
- data.tar.gz: 5554296590bfb3dea031c95090ef8a47e946ac1c7a92b3efc6924439f55df7267e0b04d385e2332ea827099c37688c4988aa908caf5bb9d2f21eabc2c50c3167
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
@@ -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
@@ -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