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
|
@@ -19,6 +19,10 @@ network = RobotLab.create_network(name: "customer_service") do
|
|
|
19
19
|
end
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Runnable Protocol
|
|
23
|
+
|
|
24
|
+
`Network` implements `RobotLab::Runnable` — the same interface `Robot` implements — so code that needs to run "a robot or a network" doesn't have to branch on `is_a?(RobotLab::Network)`. For a `Network`: `crew` returns `robots.values` (pipeline order), `chief` is `crew.first`, `robot_count` is `crew.size`, and `network?` is `true`. `run(message = nil, **opts)` accepts a positional message the same way `Robot#run` does — it's folded into `message:` — while the existing `run(message: ...)` keyword form still works unchanged. See [Runnable Protocol](../architecture/core-concepts.md#runnable-protocol) in Core Concepts for the full comparison against `Robot`.
|
|
25
|
+
|
|
22
26
|
## Creating Networks
|
|
23
27
|
|
|
24
28
|
Networks are created via `RobotLab.create_network` with a block DSL:
|
|
@@ -42,6 +46,10 @@ result = network.run(message: "Analyze this quarterly data")
|
|
|
42
46
|
Tasks can have per-task configuration that is deep-merged with network run params:
|
|
43
47
|
|
|
44
48
|
```ruby
|
|
49
|
+
# The allowlist below matches because the tool was attached as a CLASS.
|
|
50
|
+
billing_robot = RobotLab.build(name: "billing", system_prompt: "...",
|
|
51
|
+
local_tools: [RefundTool])
|
|
52
|
+
|
|
45
53
|
network = RobotLab.create_network(name: "support") do
|
|
46
54
|
task :classifier, classifier_robot, depends_on: :none
|
|
47
55
|
task :billing, billing_robot,
|
|
@@ -55,6 +63,28 @@ network = RobotLab.create_network(name: "support") do
|
|
|
55
63
|
end
|
|
56
64
|
```
|
|
57
65
|
|
|
66
|
+
!!! warning "An explicit `tools:` array must match the attachment form"
|
|
67
|
+
`ToolConfig.filter_tools` selects with `allowed_set.include?(tool_name(tool))`,
|
|
68
|
+
and `tool_name` is just `tool.name.to_s`. But `Class#name` and
|
|
69
|
+
`RubyLLM::Tool#name` return different strings, so the entry you list has to
|
|
70
|
+
match how the tool was attached:
|
|
71
|
+
|
|
72
|
+
| Attached as | Allowlist entry | Result |
|
|
73
|
+
|-------------|-----------------|--------|
|
|
74
|
+
| `local_tools: [RefundTool]` (class) | `tools: [RefundTool]` → `"RefundTool"` | matches |
|
|
75
|
+
| `local_tools: [RefundTool]` (class) | `tools: %w[refund]` | no match |
|
|
76
|
+
| `local_tools: [RefundTool.new]` (instance) | `tools: [RefundTool]` | no match |
|
|
77
|
+
| `local_tools: [RefundTool.new]` (instance) | `tools: %w[refund]` | matches |
|
|
78
|
+
|
|
79
|
+
Both forms work; mixing them silently yields an empty tool list. Pick one
|
|
80
|
+
convention per robot and keep the allowlist in the same form.
|
|
81
|
+
|
|
82
|
+
A task's `tools:` is *not* validated — it lands in `run_params` and reaches
|
|
83
|
+
the robot as a runtime value. The robot **constructor**'s `tools:` kwarg is
|
|
84
|
+
validated by `validate_tools_filter!` and raises `ArgumentError` for
|
|
85
|
+
anything that is not a String or Symbol, so the class form shown above is
|
|
86
|
+
usable only at the task/`run` level.
|
|
87
|
+
|
|
58
88
|
### Task Parameters
|
|
59
89
|
|
|
60
90
|
| Parameter | Type | Description |
|
|
@@ -65,7 +95,11 @@ end
|
|
|
65
95
|
| `mcp` | Symbol, Array | MCP server config (`:none`, `:inherit`, or array) |
|
|
66
96
|
| `tools` | Symbol, Array | Tools config (`:none`, `:inherit`, or array) |
|
|
67
97
|
| `memory` | Memory, Hash, nil | Task-specific memory |
|
|
98
|
+
| `config` | RunConfig, nil | Per-task RunConfig, merged into the network config the robot sees (`mcp`/`tools` only in practice) |
|
|
68
99
|
| `depends_on` | Symbol, Array | Dependencies (`:none`, `:optional`, or task names) |
|
|
100
|
+
| `poller_group` | Symbol | Bus poller group label for this robot (default `:default`; purely organizational) |
|
|
101
|
+
|
|
102
|
+
`mcp:` and `tools:` default to `:none` here. Anything other than `:none` is written into `run_params` and reaches the robot as its **runtime** value, so it is resolved against the robot's build-time config exactly as if it had been passed to `run`.
|
|
69
103
|
|
|
70
104
|
## Execution Model
|
|
71
105
|
|
|
@@ -94,28 +128,33 @@ stateDiagram-v2
|
|
|
94
128
|
Each robot implements the SimpleFlow step interface via `call(result)`:
|
|
95
129
|
|
|
96
130
|
```ruby
|
|
97
|
-
# Inside Robot (simplified)
|
|
131
|
+
# Inside Robot (simplified -- timing and the rescue are elided)
|
|
98
132
|
def call(result)
|
|
99
133
|
run_context = extract_run_context(result)
|
|
100
134
|
message = run_context.delete(:message)
|
|
101
135
|
|
|
102
136
|
robot_result = run(message, **run_context)
|
|
137
|
+
robot_result.duration = ... # monotonic elapsed seconds
|
|
103
138
|
|
|
104
139
|
result
|
|
105
|
-
.with_context(@name.to_sym, robot_result)
|
|
140
|
+
.with_context(@name.to_sym, robot_result) # keyed by the ROBOT's name
|
|
106
141
|
.continue(robot_result)
|
|
107
142
|
end
|
|
108
143
|
```
|
|
109
144
|
|
|
145
|
+
The real method also wraps the body so that any exception — including non-`StandardError` ones — is turned into a `RobotResult` whose text is `"Error: <class>: <message>"`, so one failing robot does not crash the pipeline.
|
|
146
|
+
|
|
110
147
|
### extract_run_context
|
|
111
148
|
|
|
112
149
|
The `extract_run_context` method pulls parameters from the SimpleFlow result:
|
|
113
150
|
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
151
|
+
- Deletes `:mcp`, `:tools`, `:memory`, `:network_memory`, `:network_config`, `:network`, and `:task` out of `run_params`, then re-attaches them as explicit keyword arguments to `run`
|
|
152
|
+
- `:mcp` and `:tools` default to `:none` when the task did not set them — matching `run`'s own defaults
|
|
153
|
+
- Merges the current result value into the remaining context
|
|
154
|
+
- If the previous result value is a `RobotResult`, uses its `last_text_content` as the message
|
|
117
155
|
- If it is a String, uses it directly as the message
|
|
118
|
-
- If it is a Hash, merges it
|
|
156
|
+
- If it is a Hash, merges it into the context
|
|
157
|
+
- Anything else is coerced with `to_s` and used as the message
|
|
119
158
|
|
|
120
159
|
## Task#call Interface
|
|
121
160
|
|
|
@@ -124,22 +163,36 @@ Each `Task` wraps a robot and enhances the SimpleFlow result before delegation:
|
|
|
124
163
|
```ruby
|
|
125
164
|
# Inside Task (simplified)
|
|
126
165
|
def call(result)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
context = TaskHookContext.new(network: @network, task: self, robot: @robot,
|
|
167
|
+
memory: @memory || @network&.memory, config: @config)
|
|
168
|
+
|
|
169
|
+
RobotLab::Hooks.run(:task, context, registries: [RobotLab.hooks, @network&.hooks]) do
|
|
170
|
+
@robot.call(enhanced_result(result))
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def enhanced_result(result)
|
|
175
|
+
run_params = deep_merge(result.context[:run_params] || {}, @context)
|
|
176
|
+
|
|
177
|
+
run_params[:mcp] = @mcp unless @mcp == :none
|
|
178
|
+
run_params[:tools] = @tools unless @tools == :none
|
|
136
179
|
run_params[:memory] = @memory if @memory
|
|
137
180
|
|
|
138
|
-
|
|
139
|
-
|
|
181
|
+
# Back-references the robot needs for hooks and config resolution
|
|
182
|
+
run_params[:task] = self
|
|
183
|
+
run_params[:network] = @network if @network
|
|
184
|
+
|
|
185
|
+
if @config
|
|
186
|
+
network_rc = run_params[:network_config]
|
|
187
|
+
run_params[:network_config] = network_rc ? network_rc.merge(@config) : @config
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
result.with_context(:run_params, run_params)
|
|
140
191
|
end
|
|
141
192
|
```
|
|
142
193
|
|
|
194
|
+
Two things to note. The `:task` hook family is dispatched against `[RobotLab.hooks, network&.hooks]` only — a handler registered with `robot.on` never fires for task hooks. And the task's own `config:` is merged into `run_params[:network_config]`, which is the value `Robot#resolve_mcp_hierarchy` / `#resolve_tools_hierarchy` consult as the parent level.
|
|
195
|
+
|
|
143
196
|
## SimpleFlow::Result
|
|
144
197
|
|
|
145
198
|
The result object flows through the pipeline:
|
|
@@ -147,30 +200,53 @@ The result object flows through the pipeline:
|
|
|
147
200
|
```ruby
|
|
148
201
|
result.value # Current task's output (RobotResult)
|
|
149
202
|
result.context # Accumulated context from all tasks
|
|
150
|
-
result.
|
|
151
|
-
result.continued? # Whether execution continues
|
|
203
|
+
result.continue? # Whether execution continues (the only status predicate)
|
|
152
204
|
```
|
|
153
205
|
|
|
154
206
|
### Result Methods
|
|
155
207
|
|
|
208
|
+
This is the complete public API of `SimpleFlow::Result` (simple_flow 0.4):
|
|
209
|
+
|
|
156
210
|
| Method | Description |
|
|
157
211
|
|--------|-------------|
|
|
212
|
+
| `value` | Current value flowing through the pipeline |
|
|
213
|
+
| `context` | Accumulated context hash |
|
|
158
214
|
| `continue(value)` | Continue to next tasks |
|
|
215
|
+
| `continue?` | Whether the pipeline is still continuing |
|
|
159
216
|
| `halt(value)` | Stop pipeline execution |
|
|
160
217
|
| `with_context(key, val)` | Add data to context |
|
|
218
|
+
| `with_error(key, message)` | Record an error (both arguments required) |
|
|
219
|
+
| `errors` | Recorded errors |
|
|
161
220
|
| `activate(task_name)` | Enable an optional task |
|
|
221
|
+
| `activated_steps` | Optional tasks that have been activated |
|
|
222
|
+
|
|
223
|
+
There is no `halted?`, no `continued?`, and no `with_value` — use `continue?` for status and `continue(value)` to set a new value.
|
|
162
224
|
|
|
163
225
|
### Context Structure
|
|
164
226
|
|
|
165
227
|
```ruby
|
|
166
228
|
{
|
|
167
|
-
run_params: { message: "...", customer_id: 123,
|
|
168
|
-
|
|
229
|
+
run_params: { message: "...", customer_id: 123,
|
|
230
|
+
network_memory: memory, network: network, task: task },
|
|
231
|
+
classifier: RobotResult, # Stored by Robot#call under the ROBOT's name
|
|
169
232
|
billing: RobotResult,
|
|
170
|
-
# ... other
|
|
233
|
+
# ... other robot results
|
|
171
234
|
}
|
|
172
235
|
```
|
|
173
236
|
|
|
237
|
+
`Robot#call` stores its output with `result.with_context(@name.to_sym, robot_result)` — the key is the **robot's** `name`, not the task name. The two coincide only when you name them identically:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
worker = RobotLab.build(name: "worker_bot", system_prompt: "...")
|
|
241
|
+
|
|
242
|
+
net = RobotLab.create_network(name: "n") do
|
|
243
|
+
task :analysis, worker, depends_on: :none
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
res = net.run(message: "hi")
|
|
247
|
+
res.context.keys #=> [:run_params, :worker_bot] -- not :analysis
|
|
248
|
+
```
|
|
249
|
+
|
|
174
250
|
## Optional Task Activation
|
|
175
251
|
|
|
176
252
|
Optional tasks (those with `depends_on: :optional`) do not run automatically. They must be activated by a preceding task using `result.activate(:task_name)`.
|
|
@@ -232,16 +308,37 @@ All robots in a network share the network's memory during execution. The network
|
|
|
232
308
|
|
|
233
309
|
```ruby
|
|
234
310
|
# Inside Network#run
|
|
235
|
-
def run(**run_context)
|
|
311
|
+
def run(message = nil, **run_context)
|
|
312
|
+
run_context[:message] = message unless message.nil? # Runnable protocol
|
|
313
|
+
|
|
236
314
|
run_context[:network_memory] = @memory
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
315
|
+
run_context[:network] = self
|
|
316
|
+
run_context[:network_config] = @config unless @config.empty?
|
|
317
|
+
|
|
318
|
+
context = NetworkRunHookContext.new(network: self, context: run_context,
|
|
319
|
+
memory: @memory, config: @config)
|
|
320
|
+
|
|
321
|
+
RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
|
|
322
|
+
if @parallel_mode == :ractor
|
|
323
|
+
run_with_ractor_scheduler(context.context)
|
|
324
|
+
else
|
|
325
|
+
initial_result = SimpleFlow::Result.new(
|
|
326
|
+
context.context,
|
|
327
|
+
context: { run_params: context.context }
|
|
328
|
+
)
|
|
329
|
+
@pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
242
332
|
end
|
|
243
333
|
```
|
|
244
334
|
|
|
335
|
+
Beyond injecting the shared memory, this does four things worth knowing:
|
|
336
|
+
|
|
337
|
+
- It puts the network itself and (when non-empty) the network's `RunConfig` into `run_params`, which is how robots find the parent level for `mcp`/`tools` resolution.
|
|
338
|
+
- The whole run is wrapped in the `:network_run` hook, dispatched against `[RobotLab.hooks, network.hooks]`.
|
|
339
|
+
- `parallel_mode: :ractor` routes to `run_with_ractor_scheduler` instead of the SimpleFlow pipeline; it raises `RobotLab::DependencyError` unless the `robot_lab-ractor` gem is loaded.
|
|
340
|
+
- `max_concurrent:` comes from `@config.max_concurrent_robots` — the one `RunConfig` field the network itself consumes.
|
|
341
|
+
|
|
245
342
|
Robots use the shared memory for inter-robot communication:
|
|
246
343
|
|
|
247
344
|
```ruby
|
|
@@ -274,7 +371,7 @@ network.broadcast(event: :pause, reason: "rate limit hit")
|
|
|
274
371
|
network.broadcast(event: :phase_complete, phase: "analysis")
|
|
275
372
|
```
|
|
276
373
|
|
|
277
|
-
|
|
374
|
+
Each handler is invoked inside an `Async { }` block, and the message is also written to memory at the `_network_broadcast` key (`Network::BROADCAST_KEY`), so robots can subscribe via `memory.subscribe(:_network_broadcast)`. Outside a running reactor, `Async { }` runs the block synchronously on the caller's thread. The message handed to a handler is `{ payload:, network:, timestamp: }`, which is why the examples above reach for `message[:payload][:event]`.
|
|
278
375
|
|
|
279
376
|
## Parallel Execution
|
|
280
377
|
|
|
@@ -296,17 +393,23 @@ end
|
|
|
296
393
|
|
|
297
394
|
### Concurrency Modes
|
|
298
395
|
|
|
396
|
+
`create_network`'s `concurrency:` is passed straight to `SimpleFlow::Pipeline`:
|
|
397
|
+
|
|
299
398
|
| Mode | Description |
|
|
300
399
|
|------|-------------|
|
|
301
|
-
| `:auto` | SimpleFlow chooses best mode |
|
|
400
|
+
| `:auto` | SimpleFlow chooses best mode (default) |
|
|
302
401
|
| `:threads` | Use Ruby threads |
|
|
303
402
|
| `:async` | Use async/fiber |
|
|
304
403
|
|
|
404
|
+
The number of tasks running at once is capped by the network config's `max_concurrent_robots`, which `Network#run` passes to `call_parallel` as `max_concurrent:`.
|
|
405
|
+
|
|
406
|
+
Separately, `Network.new(parallel_mode:)` selects the execution backend. It defaults to `:async` (the SimpleFlow pipeline above). Setting `parallel_mode: :ractor` bypasses SimpleFlow entirely for a Ractor-based scheduler and requires the `robot_lab-ractor` gem — without it, `run` raises `RobotLab::DependencyError`.
|
|
407
|
+
|
|
305
408
|
## Data Flow
|
|
306
409
|
|
|
307
|
-
1. **Initial Value**: `network.run(**params)` creates an initial `SimpleFlow::Result` with the run context
|
|
308
|
-
2. **Run Params**: Stored in `result.context[:run_params]`
|
|
309
|
-
3. **Task Results**: Each
|
|
410
|
+
1. **Initial Value**: `network.run(message, **params)` creates an initial `SimpleFlow::Result` with the run context
|
|
411
|
+
2. **Run Params**: Stored in `result.context[:run_params]`, including `network_memory`, `network`, and (per task) `task`
|
|
412
|
+
3. **Task Results**: Each robot adds its `RobotResult` to context under **its own `name`**
|
|
310
413
|
4. **Final Value**: Last task's output becomes `result.value`
|
|
311
414
|
|
|
312
415
|
```ruby
|
|
@@ -315,12 +418,15 @@ result = network.run(
|
|
|
315
418
|
customer_id: 123
|
|
316
419
|
)
|
|
317
420
|
|
|
318
|
-
result.context[:run_params] #=> { message: "...", customer_id: 123,
|
|
319
|
-
|
|
320
|
-
result.context[:
|
|
421
|
+
result.context[:run_params] #=> { message: "...", customer_id: 123,
|
|
422
|
+
# network_memory: ..., network: ..., task: ... }
|
|
423
|
+
result.context[:classifier] #=> RobotResult from the robot NAMED "classifier"
|
|
424
|
+
result.context[:billing] #=> RobotResult from the robot NAMED "billing"
|
|
321
425
|
result.value #=> Final RobotResult
|
|
322
426
|
```
|
|
323
427
|
|
|
428
|
+
If a robot's `name:` differs from the task name it was registered under, look it up by the robot's name.
|
|
429
|
+
|
|
324
430
|
## Visualization
|
|
325
431
|
|
|
326
432
|
Networks provide visualization methods via the underlying SimpleFlow pipeline:
|
|
@@ -342,21 +448,30 @@ puts network.execution_plan
|
|
|
342
448
|
## Network Inspection
|
|
343
449
|
|
|
344
450
|
```ruby
|
|
345
|
-
# Get a robot by
|
|
451
|
+
# Get a robot by its registration key
|
|
346
452
|
network.robot(:classifier) #=> Robot
|
|
347
453
|
network[:classifier] #=> Robot (alias)
|
|
348
454
|
|
|
349
455
|
# List all robots
|
|
350
456
|
network.available_robots #=> [Robot, Robot, ...]
|
|
457
|
+
network.crew #=> same array, via the Runnable protocol
|
|
351
458
|
|
|
352
459
|
# Add a robot without a task
|
|
353
460
|
network.add_robot(extra_robot)
|
|
354
461
|
|
|
462
|
+
# Remove a dynamically-added robot (returns it, or nil if absent).
|
|
463
|
+
# Only affects the crew (@robots) -- does not rewrite the pipeline.
|
|
464
|
+
network.remove_robot(:extra_robot)
|
|
465
|
+
|
|
355
466
|
# Convert to hash
|
|
356
467
|
network.to_h
|
|
357
|
-
#=> { name: "support", robots: ["classifier", "billing"],
|
|
468
|
+
#=> { name: "support", robots: ["classifier", "billing"],
|
|
469
|
+
# tasks: ["classifier", "billing"], optional_tasks: [],
|
|
470
|
+
# config: { ... } } # :config present only when the network config is non-empty
|
|
358
471
|
```
|
|
359
472
|
|
|
473
|
+
The `@robots` hash is keyed **by task name** for robots registered with `task`, but **by `robot.name`** for robots added with `add_robot`. So for a robot built as `name: "worker_bot"` and registered as `task :analysis, worker`, `network.robot(:analysis)` returns it and `network.robot(:worker_bot)` returns `nil` — the opposite of how `result.context` is keyed. `to_h[:robots]` therefore lists task names for task-registered robots.
|
|
474
|
+
|
|
360
475
|
## Next Steps
|
|
361
476
|
|
|
362
477
|
- [Memory Management](state-management.md) - Shared memory and reactive features
|