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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4b2a3fafbdf3a54de3044b57597b42d86c68bd2afdad6ce866ac82483e61091
4
- data.tar.gz: 5137cff56485a26fabe5ab6606b144c4c3c21c1673ecec1d2254a392e015c25c
3
+ metadata.gz: 3a8ae2e2cf690116950548d732987e16756870f8444c91504ea14fe039f25996
4
+ data.tar.gz: 115694d1449233b3a17a28e87deda8bd3d0ac204f51301aee7781156a3b2003e
5
5
  SHA512:
6
- metadata.gz: 33045f27ec803094a020caee4133c1d6c65887446330c294d9a9babd56a0fe7e71979fe0d032c2421488d1dcae886ad784a78dae579ba01b2786b5f9f91c0172
7
- data.tar.gz: 5554296590bfb3dea031c95090ef8a47e946ac1c7a92b3efc6924439f55df7267e0b04d385e2332ea827099c37688c4988aa908caf5bb9d2f21eabc2c50c3167
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
@@ -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:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.0.11"
4
+ VERSION = "0.0.12"
5
5
  end
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.11
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