robot_lab 0.2.1 → 0.2.7
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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
data/docs/api/core/state.md
CHANGED
|
@@ -1,255 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# StateProxy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
!!! danger "There is no `RobotLab::State` class"
|
|
4
|
+
RobotLab has never had a `State` class. Runtime state lives in
|
|
5
|
+
[`RobotLab::Memory`](memory.md). The only state object is
|
|
6
|
+
**`RobotLab::StateProxy`** (`lib/robot_lab/state_proxy.rb`), which is what
|
|
7
|
+
`memory.data` returns — and that is what this page documents.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
This page is listed as "State" in the site navigation for historical reasons.
|
|
10
|
+
For the key-value store, subscriptions, blocking reads, results, and
|
|
11
|
+
serialization, go to **[Memory](memory.md)**.
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
memory = RobotLab.create_memory(
|
|
9
|
-
data: { user_id: "123" }
|
|
10
|
-
)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Attributes
|
|
14
|
-
|
|
15
|
-
### session_id
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
memory.session_id # => String | nil
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Conversation session identifier for persistence.
|
|
22
|
-
|
|
23
|
-
### data
|
|
24
|
-
|
|
25
|
-
```ruby
|
|
26
|
-
memory.data # => StateProxy
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Access workflow data as a proxy object.
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
memory.data[:user_id] # Hash access
|
|
33
|
-
memory.data.user_id # Method access
|
|
34
|
-
memory.data[:status] = "active"
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### results
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
memory.results # => Array<RobotResult>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
All robot execution results.
|
|
13
|
+
## Class: `RobotLab::StateProxy`
|
|
44
14
|
|
|
45
|
-
|
|
15
|
+
A thin wrapper around a symbol-keyed Hash that adds method-style access and an
|
|
16
|
+
optional change callback. It is not a `Hash` subclass and does not implement the
|
|
17
|
+
full `Hash` interface — the supported methods are listed below.
|
|
46
18
|
|
|
47
19
|
```ruby
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Formatted conversation messages for LLM.
|
|
20
|
+
proxy = RobotLab::StateProxy.new({ count: 0, name: "test" })
|
|
52
21
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
memory.append_result(robot_result)
|
|
22
|
+
proxy[:count] = 1
|
|
23
|
+
proxy.count # => 1
|
|
24
|
+
proxy[:name] # => "test"
|
|
25
|
+
proxy.to_h # => { count: 1, name: "test" }
|
|
59
26
|
```
|
|
60
27
|
|
|
61
|
-
|
|
28
|
+
## Where you get one
|
|
62
29
|
|
|
63
|
-
|
|
30
|
+
`memory.data` — the reserved `:data` key of a `Memory` — is the only place the
|
|
31
|
+
framework hands you a `StateProxy`:
|
|
64
32
|
|
|
65
33
|
```ruby
|
|
66
|
-
memory.
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Replace all results.
|
|
70
|
-
|
|
71
|
-
### results_from
|
|
72
|
-
|
|
73
|
-
```ruby
|
|
74
|
-
memory.results_from(5) # => Array<RobotResult>
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Get results starting at index.
|
|
78
|
-
|
|
79
|
-
### session_id=
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
memory.session_id = "session_123"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Set the session identifier.
|
|
34
|
+
memory = RobotLab.create_memory(data: { user_id: "123" })
|
|
86
35
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
memory.
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Format results as conversation history.
|
|
94
|
-
|
|
95
|
-
### clone
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
new_memory = memory.clone
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Create a deep copy.
|
|
102
|
-
|
|
103
|
-
### to_h
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
106
|
-
memory.to_h # => Hash
|
|
36
|
+
memory.data # => #<RobotLab::StateProxy {user_id: "123"}>
|
|
37
|
+
memory.data[:user_id] # => "123" Hash access
|
|
38
|
+
memory.data.user_id # => "123" method access
|
|
39
|
+
memory.data[:status] = "active"
|
|
107
40
|
```
|
|
108
41
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
### to_json
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
memory.to_json # => String
|
|
115
|
-
```
|
|
42
|
+
The proxy is memoized (`@data ||= StateProxy.new(...)`) and is reset whenever
|
|
43
|
+
`memory[:data] = ...` or `memory.reset` is called.
|
|
116
44
|
|
|
117
|
-
|
|
45
|
+
!!! warning "`memory.data` mutations are not reactive"
|
|
46
|
+
`memory.data[:x] = 1` writes through the proxy into the `:data` hash. It does
|
|
47
|
+
**not** go through `Memory#set`, so it wakes no blocking readers and notifies
|
|
48
|
+
no subscribers. Use `memory.set(:x, 1)` on a non-reserved key when you need
|
|
49
|
+
reactive semantics.
|
|
118
50
|
|
|
119
|
-
|
|
51
|
+
## Constructor
|
|
120
52
|
|
|
121
53
|
```ruby
|
|
122
|
-
|
|
54
|
+
RobotLab::StateProxy.new(data = {}, on_change: nil)
|
|
123
55
|
```
|
|
124
56
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
The `data` attribute is a `StateProxy`:
|
|
57
|
+
| Name | Type | Default | Description |
|
|
58
|
+
|------|------|---------|-------------|
|
|
59
|
+
| `data` | `Hash` | `{}` | Initial data. Keys are converted to Symbols via `transform_keys(&:to_sym)` |
|
|
60
|
+
| `on_change` | `Proc`, `nil` | `nil` | Called as `on_change.call(key, old_value, new_value)` on `[]=`, and only when the value actually changed (`old_value != value`) |
|
|
130
61
|
|
|
131
|
-
|
|
132
|
-
proxy = memory.data
|
|
133
|
-
|
|
134
|
-
# Hash-style access
|
|
135
|
-
proxy[:key]
|
|
136
|
-
proxy[:key] = value
|
|
137
|
-
|
|
138
|
-
# Method-style access
|
|
139
|
-
proxy.key
|
|
140
|
-
proxy.key = value
|
|
141
|
-
|
|
142
|
-
# Hash operations
|
|
143
|
-
proxy.key?(:key)
|
|
144
|
-
proxy.keys
|
|
145
|
-
proxy.values
|
|
146
|
-
proxy.each { |k, v| ... }
|
|
147
|
-
proxy.merge!(other_hash)
|
|
148
|
-
proxy.delete(:key)
|
|
149
|
-
proxy.to_h
|
|
150
|
-
proxy.empty?
|
|
151
|
-
proxy.size
|
|
152
|
-
```
|
|
62
|
+
`Memory#data` constructs the proxy **without** an `on_change` callback.
|
|
153
63
|
|
|
154
|
-
##
|
|
64
|
+
## Methods
|
|
155
65
|
|
|
156
|
-
###
|
|
66
|
+
### Element access
|
|
157
67
|
|
|
158
68
|
```ruby
|
|
159
|
-
|
|
69
|
+
proxy[:key] # => value (keys are symbolized on read)
|
|
70
|
+
proxy[:key] = value # fires on_change when the value differs
|
|
160
71
|
```
|
|
161
72
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
memory = RobotLab.create_memory(
|
|
166
|
-
data: {
|
|
167
|
-
user_id: "user_123",
|
|
168
|
-
order_id: "ord_456"
|
|
169
|
-
}
|
|
170
|
-
)
|
|
171
|
-
```
|
|
73
|
+
Both `[]` and `[]=` call `key.to_sym`, so `proxy["a"]` and `proxy[:a]` are the same entry.
|
|
172
74
|
|
|
173
|
-
###
|
|
75
|
+
### Method-style access
|
|
174
76
|
|
|
175
77
|
```ruby
|
|
176
|
-
|
|
177
|
-
|
|
78
|
+
proxy.name # same as proxy[:name] — only when :name already exists
|
|
79
|
+
proxy.name = "x" # same as proxy[:name] = "x" — always works
|
|
178
80
|
```
|
|
179
81
|
|
|
180
|
-
|
|
82
|
+
Implemented with `method_missing`. A **getter** for a key that does not exist
|
|
83
|
+
falls through to `super` and raises `NoMethodError`; a **setter** always works and
|
|
84
|
+
creates the key. `respond_to?(:name)` is true only once the key exists.
|
|
181
85
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
```
|
|
86
|
+
Method-style access cannot reach a key whose name collides with a real
|
|
87
|
+
`StateProxy` method (`keys`, `size`, `map`, `each`, `delete`, `dup`, `inspect`, …).
|
|
88
|
+
Use `proxy[:keys]` for those.
|
|
186
89
|
|
|
187
|
-
|
|
90
|
+
### Full method list
|
|
188
91
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
92
|
+
| Method | Returns | Notes |
|
|
93
|
+
|--------|---------|-------|
|
|
94
|
+
| `[](key)` | value | Symbolizes the key |
|
|
95
|
+
| `[]=(key, value)` | value | Symbolizes the key; fires `on_change` |
|
|
96
|
+
| `key?(key)` | `Boolean` | Aliases: `has_key?`, `include?` |
|
|
97
|
+
| `keys` | `Array<Symbol>` | |
|
|
98
|
+
| `values` | `Array` | |
|
|
99
|
+
| `each { \|k, v\| }` | delegates to `Hash#each` | |
|
|
100
|
+
| `map { \|k, v\| }` | `Array` | |
|
|
101
|
+
| `delete(key)` | deleted value | Does **not** fire `on_change` |
|
|
102
|
+
| `merge!(other)` | `self` | Assigns each pair through `[]=`, so `on_change` fires per key |
|
|
103
|
+
| `to_h` | `Hash` | A **shallow** `dup`; aliased as `to_hash` |
|
|
104
|
+
| `dup` | `StateProxy` | Deep duplicate, preserving `on_change` |
|
|
105
|
+
| `empty?` | `Boolean` | |
|
|
106
|
+
| `size` | `Integer` | Aliased as `length` |
|
|
107
|
+
| `inspect` | `String` | `#<RobotLab::StateProxy {...}>` |
|
|
198
108
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
message.metadata # => { source: "web" }
|
|
203
|
-
message.id # => UUID
|
|
204
|
-
message.created_at # => Time
|
|
205
|
-
```
|
|
109
|
+
There is no `clear`, no `fetch`, no `dig`, and no `clone`. `to_h` is a shallow
|
|
110
|
+
copy, so nested Hashes are shared with the proxy — mutate them and the proxy sees
|
|
111
|
+
it. Use `dup` when you need isolation.
|
|
206
112
|
|
|
207
113
|
## Examples
|
|
208
114
|
|
|
209
|
-
###
|
|
115
|
+
### Nested data
|
|
210
116
|
|
|
211
117
|
```ruby
|
|
212
118
|
memory = RobotLab.create_memory(
|
|
213
119
|
data: { user: { name: "Alice", plan: "pro" } }
|
|
214
120
|
)
|
|
215
121
|
|
|
216
|
-
memory.data[:user][:name] # => "Alice"
|
|
122
|
+
memory.data[:user][:name] # => "Alice" (plain Hash below the top level)
|
|
123
|
+
memory.data.user # => { name: "Alice", plan: "pro" }
|
|
217
124
|
memory.data.to_h # => { user: { name: "Alice", plan: "pro" } }
|
|
218
125
|
```
|
|
219
126
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```ruby
|
|
223
|
-
# After running network
|
|
224
|
-
memory.results.size # Number of results
|
|
225
|
-
memory.results.last # Most recent
|
|
226
|
-
memory.results.map(&:robot_name) # ["classifier", "support"]
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Using Reactive Memory
|
|
230
|
-
|
|
231
|
-
```ruby
|
|
232
|
-
memory.set(:intent, "billing")
|
|
233
|
-
intent = memory.get(:intent)
|
|
234
|
-
|
|
235
|
-
memory.subscribe(:status) do |change|
|
|
236
|
-
puts "Status changed to #{change.value} by #{change.writer}"
|
|
237
|
-
end
|
|
238
|
-
```
|
|
127
|
+
Only the top level is proxied. Nested values are returned as-is — a nested Hash
|
|
128
|
+
is a plain Hash, not another `StateProxy`.
|
|
239
129
|
|
|
240
|
-
###
|
|
130
|
+
### Change tracking
|
|
241
131
|
|
|
242
132
|
```ruby
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
133
|
+
proxy = RobotLab::StateProxy.new({ status: "pending" }, on_change: lambda { |key, old, new|
|
|
134
|
+
puts "#{key}: #{old.inspect} -> #{new.inspect}"
|
|
135
|
+
})
|
|
246
136
|
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
137
|
+
proxy[:status] = "active" # "status: \"pending\" -> \"active\""
|
|
138
|
+
proxy[:status] = "active" # silent — value unchanged
|
|
139
|
+
proxy.delete(:status) # silent — delete does not fire on_change
|
|
250
140
|
```
|
|
251
141
|
|
|
252
142
|
## See Also
|
|
253
143
|
|
|
144
|
+
- [Memory](memory.md) — the store that owns the proxy: keys, subscriptions, blocking reads, results, serialization
|
|
254
145
|
- [State Management Architecture](../../architecture/state-management.md)
|
|
255
|
-
- [Memory](memory.md)
|
data/docs/api/core/tool.md
CHANGED
|
@@ -49,6 +49,12 @@ Tool.new(robot: nil)
|
|
|
49
49
|
|------|------|-------------|
|
|
50
50
|
| `robot` | `Robot, nil` | The owning robot instance |
|
|
51
51
|
|
|
52
|
+
## Constants
|
|
53
|
+
|
|
54
|
+
| Constant | Value | Description |
|
|
55
|
+
|----------|-------|-------------|
|
|
56
|
+
| `RobotLab::Robot::DEFAULT_MAX_TOOLS` | `128` | Ceiling on the number of tools a robot hands the provider per turn. Override per robot with `RunConfig#max_tools`; a nil, zero, or negative value falls back to 128, so the cap cannot be disabled |
|
|
57
|
+
|
|
52
58
|
## Class Methods
|
|
53
59
|
|
|
54
60
|
### raise_on_error / raise_on_error?
|
|
@@ -58,7 +64,60 @@ MyTool.raise_on_error = true
|
|
|
58
64
|
MyTool.raise_on_error? # => true
|
|
59
65
|
```
|
|
60
66
|
|
|
61
|
-
Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`.
|
|
67
|
+
Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`.
|
|
68
|
+
|
|
69
|
+
!!! warning "`raise_on_error` does not walk the inheritance chain"
|
|
70
|
+
The reader is `defined?(@raise_on_error) ? @raise_on_error : false` — it
|
|
71
|
+
inspects only the receiving class's own instance variable. A subclass of a
|
|
72
|
+
class that set `self.raise_on_error = true` silently reverts to `false`:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Critical < RobotLab::Tool; self.raise_on_error = true; end
|
|
76
|
+
class Derived < Critical; end
|
|
77
|
+
|
|
78
|
+
Critical.raise_on_error? # => true
|
|
79
|
+
Derived.raise_on_error? # => false <-- errors are swallowed again
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Set the flag explicitly on every subclass that needs it. Contrast
|
|
83
|
+
[`ractor_safe`](#ractor_safe-ractor_safe), which *does* walk the chain.
|
|
84
|
+
|
|
85
|
+
### ractor_safe / ractor_safe?
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class Pure < RobotLab::Tool
|
|
89
|
+
ractor_safe true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Pure.ractor_safe? # => true
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Declares that the tool class is safe to run inside a Ractor. With no argument it
|
|
96
|
+
acts as a getter; with a Boolean it sets the value. `ractor_safe?` is an alias
|
|
97
|
+
for `ractor_safe`.
|
|
98
|
+
|
|
99
|
+
Unlike `raise_on_error`, the getter **does** walk the inheritance chain: if the
|
|
100
|
+
class has no `@ractor_safe` of its own it asks its superclass, terminating at
|
|
101
|
+
`false`.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class Pure < RobotLab::Tool; ractor_safe true; end
|
|
105
|
+
class Derived < Pure; end
|
|
106
|
+
|
|
107
|
+
Derived.ractor_safe? # => true — inherited
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Ractor pool dispatch.** `Tool#call` routes through the Ractor worker pool
|
|
111
|
+
(`RobotLab.ractor_pool.submit(self.class.name, args)`) only when **all three**
|
|
112
|
+
hold:
|
|
113
|
+
|
|
114
|
+
1. `self.class.ractor_safe?` is true
|
|
115
|
+
2. `self.class.name` is not `nil` (an anonymous class — including everything from
|
|
116
|
+
`Tool.create` — always falls through to the inline path)
|
|
117
|
+
3. `RobotLab.extension_loaded?(:ractor)` — i.e. the `robot_lab-ractor` gem is loaded
|
|
118
|
+
|
|
119
|
+
Otherwise `execute` runs inline on the calling thread. Ractor-safe tools must be
|
|
120
|
+
stateless: they are instantiated fresh inside the worker for each call.
|
|
62
121
|
|
|
63
122
|
### Tool.create
|
|
64
123
|
|
|
@@ -82,9 +141,18 @@ Tool.create(
|
|
|
82
141
|
| `name` | `String, Symbol` | Tool identifier |
|
|
83
142
|
| `description` | `String` | What the tool does |
|
|
84
143
|
| `parameters` | `Hash` | JSON Schema parameter definition |
|
|
85
|
-
| `mcp` | `String` | MCP server name |
|
|
144
|
+
| `mcp` | `String` | MCP server name; makes `mcp?` true |
|
|
86
145
|
| `robot` | `Robot` | Owning robot instance |
|
|
87
|
-
| `&handler` | `Block` | Receives `args` hash, returns result |
|
|
146
|
+
| `&handler` | `Block` | Receives the `args` hash, returns the result |
|
|
147
|
+
|
|
148
|
+
`create` builds an **anonymous** subclass (`Class.new(self)`) and stashes the
|
|
149
|
+
explicit name on the instance. Two consequences:
|
|
150
|
+
|
|
151
|
+
- `parameters` is read with **symbol** keys (`params_hash[:properties]`,
|
|
152
|
+
`pdef[:type]`, `pdef[:description]`, `params_hash[:required]`). A string-keyed
|
|
153
|
+
schema is silently ignored and the tool ends up with no parameters.
|
|
154
|
+
- Because `self.class.name` is `nil`, a `create`d tool never dispatches to the
|
|
155
|
+
Ractor pool even if you set `ractor_safe`.
|
|
88
156
|
|
|
89
157
|
## Inherited DSL (from RubyLLM::Tool)
|
|
90
158
|
|
|
@@ -105,6 +173,22 @@ class MyTool < RobotLab::Tool
|
|
|
105
173
|
end
|
|
106
174
|
```
|
|
107
175
|
|
|
176
|
+
`param` accepts exactly four options — anything else raises
|
|
177
|
+
`ArgumentError: unknown keyword`:
|
|
178
|
+
|
|
179
|
+
| Option | Type | Default | Description |
|
|
180
|
+
|--------|------|---------|-------------|
|
|
181
|
+
| `type` | `String` | `"string"` | JSON Schema type (`"string"`, `"integer"`, `"number"`, `"boolean"`, `"array"`, `"object"`) |
|
|
182
|
+
| `desc` | `String` | `nil` | Description shown to the LLM |
|
|
183
|
+
| `description` | `String` | `nil` | Synonym for `desc` |
|
|
184
|
+
| `required` | `Boolean` | `true` | Whether the LLM must supply the parameter |
|
|
185
|
+
|
|
186
|
+
!!! warning "There is no `enum:` option"
|
|
187
|
+
`param :unit, type: "string", enum: %w[c f]` raises
|
|
188
|
+
`ArgumentError: unknown keyword: :enum`. Constrain the value in the
|
|
189
|
+
description and validate it inside `execute`, or supply a full JSON Schema
|
|
190
|
+
through [`Tool.create(parameters:)`](#toolcreate).
|
|
191
|
+
|
|
108
192
|
### execute
|
|
109
193
|
|
|
110
194
|
```ruby
|
|
@@ -162,7 +246,19 @@ The MCP server name, set via `Tool.create(mcp: "server_name")`.
|
|
|
162
246
|
tool.name # => String
|
|
163
247
|
```
|
|
164
248
|
|
|
165
|
-
Returns the tool name. For
|
|
249
|
+
Returns the tool name. For `create`d tools, the explicit `name:` you passed. For
|
|
250
|
+
subclasses, ruby_llm derives it from the class name — **including the namespace**,
|
|
251
|
+
with `::` rendered as `--`:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
RobotLab::AskUser.new.name # => "robot_lab--ask_user"
|
|
255
|
+
|
|
256
|
+
class Sub1 < RobotLab::Tool; end
|
|
257
|
+
Sub1.new.name # => "sub1" (top-level class, no namespace)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This matters when writing a `tools:` name allowlist: the allowlist must contain
|
|
261
|
+
the fully-derived name (`"robot_lab--ask_user"`), not the short one.
|
|
166
262
|
|
|
167
263
|
### mcp?
|
|
168
264
|
|
|
@@ -178,13 +274,35 @@ Whether this is an MCP-provided tool.
|
|
|
178
274
|
result = tool.call(args_hash)
|
|
179
275
|
```
|
|
180
276
|
|
|
181
|
-
Overrides `RubyLLM::Tool#call
|
|
277
|
+
Overrides `RubyLLM::Tool#call`. The whole invocation is wrapped in the
|
|
278
|
+
`:tool_call` hook family, then dispatched either to the Ractor pool or inline
|
|
279
|
+
(see [`ractor_safe`](#ractor_safe-ractor_safe)); ruby_llm's `call` converts
|
|
280
|
+
string keys to symbols and invokes `execute(**args)`.
|
|
182
281
|
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
Errors are caught and returned to the LLM as a plain-text string it can reason
|
|
283
|
+
about. There are two distinct paths:
|
|
284
|
+
|
|
285
|
+
| Raised | Returned text | Logged? |
|
|
286
|
+
|--------|---------------|---------|
|
|
287
|
+
| `RobotLab::ToolError` | `Error (<name>): <message>` — plus `" (retryable)"` when `RobotLab::Errors.retryable?` is true | **No** |
|
|
288
|
+
| any other `StandardError` | `Error (<name>): <message>` | Yes — `RobotLab.config.logger.warn("Tool '<name>' error: <Class>: <message>")` |
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
class T2 < RobotLab::Tool
|
|
292
|
+
description "t"
|
|
293
|
+
def execute(**) = raise RobotLab::ToolError.new("nope", retryable: true)
|
|
294
|
+
end
|
|
295
|
+
T2.new.call({}) # => "Error (t2): nope (retryable)" -- nothing logged
|
|
296
|
+
|
|
297
|
+
class T3 < RobotLab::Tool
|
|
298
|
+
description "t"
|
|
299
|
+
def execute(**) = raise ArgumentError, "bad"
|
|
300
|
+
end
|
|
301
|
+
T3.new.call({}) # => "Error (t3): bad" -- WARN logged
|
|
185
302
|
```
|
|
186
303
|
|
|
187
|
-
|
|
304
|
+
`" (retryable)"` is appended only for `ToolError`/`MCPError` instances
|
|
305
|
+
constructed with `retryable: true` — see [Retryable Errors](../errors.md#retryable-errors).
|
|
188
306
|
|
|
189
307
|
To propagate exceptions instead of catching them (for critical tools), set `raise_on_error` on the class:
|
|
190
308
|
|
|
@@ -195,7 +313,8 @@ class CriticalTool < RobotLab::Tool
|
|
|
195
313
|
end
|
|
196
314
|
```
|
|
197
315
|
|
|
198
|
-
`raise_on_error` is per-class
|
|
316
|
+
`raise_on_error` applies to **both** paths, is per-class, defaults to `false`, and
|
|
317
|
+
[is not inherited by subclasses](#raise_on_error-raise_on_error).
|
|
199
318
|
|
|
200
319
|
### params_schema
|
|
201
320
|
|
|
@@ -219,7 +338,16 @@ Inherited. Returns provider-specific parameters (e.g., `{ strict: true }`).
|
|
|
219
338
|
tool.to_h # => Hash
|
|
220
339
|
```
|
|
221
340
|
|
|
222
|
-
Hash representation with `:name`, `:description`, `:mcp`.
|
|
341
|
+
Hash representation with `:name`, `:description`, `:mcp` — `.compact`ed, so
|
|
342
|
+
`:description` and `:mcp` are absent when nil:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
class T4 < RobotLab::Tool
|
|
346
|
+
description "t"
|
|
347
|
+
def execute(**) = nil
|
|
348
|
+
end
|
|
349
|
+
T4.new.to_h # => { name: "t4", description: "t" } -- no :mcp key
|
|
350
|
+
```
|
|
223
351
|
|
|
224
352
|
### to_json
|
|
225
353
|
|
|
@@ -253,14 +381,18 @@ class AdjustTemperature < RobotLab::Tool
|
|
|
253
381
|
end
|
|
254
382
|
end
|
|
255
383
|
|
|
256
|
-
#
|
|
257
|
-
robot = RobotLab.build(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
384
|
+
# The robot must exist before the tool can reference it, so attach afterwards
|
|
385
|
+
robot = RobotLab.build(name: "creative_bot", system_prompt: "You are creative.")
|
|
386
|
+
robot.local_tools << AdjustTemperature.new(robot: robot)
|
|
387
|
+
|
|
388
|
+
robot.run("Be more creative.", tools: :inherit)
|
|
262
389
|
```
|
|
263
390
|
|
|
391
|
+
Passing the bare **class** (`local_tools: [AdjustTemperature]`) leaves
|
|
392
|
+
`tool.robot` as `nil` — ruby_llm instantiates it with no arguments — and
|
|
393
|
+
`robot.with_temperature` then raises `NoMethodError` on nil. Instantiate with
|
|
394
|
+
`robot:` whenever the tool needs its owner.
|
|
395
|
+
|
|
264
396
|
## Parameter Types
|
|
265
397
|
|
|
266
398
|
### String
|
|
@@ -319,6 +451,8 @@ end
|
|
|
319
451
|
| `choices` | `Array` | No | Numbered choices to present |
|
|
320
452
|
| `default` | `String` | No | Value returned when user presses Enter without typing |
|
|
321
453
|
|
|
454
|
+
Its LLM-facing name is **`robot_lab--ask_user`** (namespace included), not `ask_user`.
|
|
455
|
+
|
|
322
456
|
### IO Resolution
|
|
323
457
|
|
|
324
458
|
The tool reads input and writes output using the owning robot's `input`/`output` accessors:
|
|
@@ -326,10 +460,18 @@ The tool reads input and writes output using the owning robot's `input`/`output`
|
|
|
326
460
|
1. `robot.input` / `robot.output` if set
|
|
327
461
|
2. Falls back to `$stdin` / `$stdout`
|
|
328
462
|
|
|
463
|
+
The prompt label is `robot&.name || "Robot"`.
|
|
464
|
+
|
|
465
|
+
!!! warning "Attach an instance, not the class"
|
|
466
|
+
`local_tools: [RobotLab::AskUser]` gives ruby_llm a bare class, which it
|
|
467
|
+
instantiates with no arguments — so `tool.robot` is `nil`, IO falls back to
|
|
468
|
+
`$stdin`/`$stdout`, and every prompt is labelled `[Robot]`. Pass
|
|
469
|
+
`RobotLab::AskUser.new(robot: robot)` to get the robot's own name and streams.
|
|
470
|
+
|
|
329
471
|
### Terminal Output
|
|
330
472
|
|
|
331
473
|
```
|
|
332
|
-
[
|
|
474
|
+
[interviewer] What programming language do you want to learn?
|
|
333
475
|
1. Ruby
|
|
334
476
|
2. Python
|
|
335
477
|
3. Go
|
|
@@ -341,10 +483,13 @@ The tool reads input and writes output using the owning robot's `input`/`output`
|
|
|
341
483
|
```ruby
|
|
342
484
|
robot = RobotLab.build(
|
|
343
485
|
name: "interviewer",
|
|
344
|
-
system_prompt: "Interview the user about their project needs.
|
|
345
|
-
|
|
486
|
+
system_prompt: "Interview the user about their project needs. " \
|
|
487
|
+
"Use robot_lab--ask_user to gather information."
|
|
346
488
|
)
|
|
347
|
-
robot.
|
|
489
|
+
robot.local_tools << RobotLab::AskUser.new(robot: robot)
|
|
490
|
+
|
|
491
|
+
# run() defaults to tools: :none — :inherit is required for the tool to be sent
|
|
492
|
+
robot.run("Find out what the user wants to build", tools: :inherit)
|
|
348
493
|
```
|
|
349
494
|
|
|
350
495
|
### Testing with StringIO
|