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/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# RobotLab
|
|
2
2
|
|
|
3
|
-
> [!
|
|
4
|
-
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
|
|
5
5
|
|
|
6
6
|
<br>
|
|
7
7
|
<table>
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
- <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
|
|
22
22
|
- <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
|
|
23
23
|
- <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
|
|
24
|
-
- <strong>Local LLM Providers</strong> - Ollama
|
|
24
|
+
- <strong>Local LLM Providers</strong> - Ollama and GPUStack via provider passthrough<br>
|
|
25
25
|
- <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
|
|
26
26
|
- <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
|
|
27
27
|
- <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
|
|
28
|
+
- <strong>Hook System</strong> - Lifecycle hooks across every execution boundary for instrumentation, caching, and extensions<br>
|
|
28
29
|
- <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
|
|
29
30
|
- <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting (via <a href="https://github.com/MadBomber/robot_lab-rails">robot_lab-rails</a>)<br>
|
|
30
31
|
- <strong>Token & Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
|
|
31
32
|
- <strong>Tool Loop Circuit Breaker</strong> - <code>max_tool_rounds:</code> guards against runaway tool call loops<br>
|
|
33
|
+
- <strong>Doom Loop Detection</strong> - always-on detection of repeated or cyclic tool call patterns, tunable via <code>doom_loop_threshold:</code><br>
|
|
32
34
|
- <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
|
|
33
35
|
- <strong>Context Window Compression</strong> - <code>robot.compress_history()</code> prunes irrelevant old turns via TF cosine scoring<br>
|
|
34
36
|
- <strong>Convergence Detection</strong> - <code>RobotLab::Convergence</code> detects when independent agents agree, enabling reconciler fast-path<br>
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
</tr>
|
|
38
40
|
</table>
|
|
39
41
|
|
|
40
|
-
<p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab)
|
|
42
|
+
<p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) and explore the [many examples](examples/README.md) available as working demo applications.</p>
|
|
41
43
|
|
|
42
44
|
## Installation
|
|
43
45
|
|
|
@@ -80,7 +82,11 @@ puts result.last_text_content
|
|
|
80
82
|
|
|
81
83
|
### Local LLM Providers
|
|
82
84
|
|
|
83
|
-
For local LLM providers (
|
|
85
|
+
For local LLM providers, use the `provider:` parameter. RubyLLM's provider list is the authority on valid values — `:ollama` and `:gpustack` are the local ones (alongside `:anthropic`, `:azure`, `:bedrock`, `:deepseek`, `:gemini`, `:mistral`, `:openai`, `:openrouter`, `:perplexity`, `:vertexai`, and `:xai`). The provider's API base must be configured first, or building the robot raises `RubyLLM::ConfigurationError`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export ROBOT_LAB_RUBY_LLM__OLLAMA_API_BASE=http://localhost:11434/v1
|
|
89
|
+
```
|
|
84
90
|
|
|
85
91
|
```ruby
|
|
86
92
|
robot = RobotLab.build(
|
|
@@ -91,13 +97,15 @@ robot = RobotLab.build(
|
|
|
91
97
|
)
|
|
92
98
|
```
|
|
93
99
|
|
|
100
|
+
Specifying `provider:` also sets `assume_model_exists`, so the model name is not validated against a known-model list.
|
|
101
|
+
|
|
94
102
|
### Configuration
|
|
95
103
|
|
|
96
104
|
RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
|
|
97
105
|
|
|
98
106
|
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
99
107
|
2. Environment-specific overrides (development, test, production)
|
|
100
|
-
3. XDG user config (`~/.config/robot_lab/
|
|
108
|
+
3. XDG user config (`~/.config/robot_lab/robot_lab.yml` — note the filename repeats the app name)
|
|
101
109
|
4. Project config (`./config/robot_lab.yml`)
|
|
102
110
|
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
103
111
|
|
|
@@ -112,7 +120,6 @@ export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
|
|
|
112
120
|
# Access configuration values
|
|
113
121
|
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
114
122
|
RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
115
|
-
RobotLab.config.streaming_enabled #=> true
|
|
116
123
|
```
|
|
117
124
|
|
|
118
125
|
Or create a project config file at `./config/robot_lab.yml`:
|
|
@@ -191,9 +198,10 @@ tools:
|
|
|
191
198
|
- CodeSearchTool
|
|
192
199
|
mcp:
|
|
193
200
|
- name: github
|
|
194
|
-
transport:
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
transport:
|
|
202
|
+
type: stdio
|
|
203
|
+
command: npx
|
|
204
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
197
205
|
model: claude-sonnet-4
|
|
198
206
|
---
|
|
199
207
|
You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
@@ -204,7 +212,10 @@ You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
|
204
212
|
robot = RobotLab.build(template: :github_assistant)
|
|
205
213
|
```
|
|
206
214
|
|
|
207
|
-
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys
|
|
215
|
+
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys. Constructor-provided values always take precedence over front matter.
|
|
216
|
+
|
|
217
|
+
> [!IMPORTANT]
|
|
218
|
+
> Of the LLM config keys, only **`model`** and **`temperature`** currently take effect from front matter. `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed but not applied — set those as constructor kwargs (or via `config:`), where they do work.
|
|
208
219
|
|
|
209
220
|
### Composable Skills
|
|
210
221
|
|
|
@@ -236,7 +247,7 @@ Skills are expanded depth-first and can reference other skills (with automatic c
|
|
|
236
247
|
|
|
237
248
|
### Combining Templates with System Prompts
|
|
238
249
|
|
|
239
|
-
The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
|
|
250
|
+
The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended, producing a single combined system message. The appended text also survives template re-rendering, so it persists across `run()` calls that supply runtime context. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
|
|
240
251
|
|
|
241
252
|
```ruby
|
|
242
253
|
robot = RobotLab.build(
|
|
@@ -249,7 +260,7 @@ robot = RobotLab.build(
|
|
|
249
260
|
|
|
250
261
|
### Shared Configuration with RunConfig
|
|
251
262
|
|
|
252
|
-
`RunConfig` lets you define operational defaults
|
|
263
|
+
`RunConfig` lets you define operational defaults. For a single robot they cascade from least- to most-specific: template front matter -> `config:` -> constructor kwargs. Front matter is the *base*, not an override — constructor kwargs always win.
|
|
253
264
|
|
|
254
265
|
```ruby
|
|
255
266
|
# Create a shared config
|
|
@@ -266,7 +277,7 @@ robot = RobotLab.build(
|
|
|
266
277
|
config: shared
|
|
267
278
|
)
|
|
268
279
|
|
|
269
|
-
# Apply
|
|
280
|
+
# Apply at the network level (see the note below on what actually propagates)
|
|
270
281
|
network = RobotLab.create_network(name: "pipeline", config: shared) do
|
|
271
282
|
task :analyzer, analyzer_robot, depends_on: :none
|
|
272
283
|
task :writer, writer_robot, depends_on: [:analyzer]
|
|
@@ -281,6 +292,11 @@ robot = RobotLab.build(
|
|
|
281
292
|
)
|
|
282
293
|
```
|
|
283
294
|
|
|
295
|
+
> [!IMPORTANT]
|
|
296
|
+
> A network-level `config:` propagates only `mcp` and `tools` down to its robots, and only when the robot opts in with `mcp: :inherit` / `tools: :inherit` — `:none` at any level resolves to an empty list. LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks (`on_content`) are read from each robot's own config at construction time, so they are **not** inherited from the network. To share LLM settings, pass the same `RunConfig` to each robot via `config:`.
|
|
297
|
+
>
|
|
298
|
+
> `max_concurrent_robots` is the one field the network itself consumes.
|
|
299
|
+
|
|
284
300
|
RunConfig supports keyword construction, block DSL, and merge semantics:
|
|
285
301
|
|
|
286
302
|
```ruby
|
|
@@ -300,7 +316,7 @@ effective.model #=> "claude-sonnet-4"
|
|
|
300
316
|
|
|
301
317
|
### Chaining Configuration
|
|
302
318
|
|
|
303
|
-
Robots support method chaining to adjust configuration after creation
|
|
319
|
+
Robots support method chaining to adjust configuration after creation. The `with_*` methods are delegated dynamically from the underlying `RubyLLM::Chat`, so the available set is whatever that class defines — currently `with_context`, `with_headers`, `with_instructions`, `with_model`, `with_params`, `with_schema`, `with_temperature`, `with_thinking`, `with_tool`, and `with_tools`. Each returns the robot, so calls chain.
|
|
304
320
|
|
|
305
321
|
```ruby
|
|
306
322
|
robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer.")
|
|
@@ -308,10 +324,18 @@ robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer
|
|
|
308
324
|
result = robot
|
|
309
325
|
.with_temperature(0.9)
|
|
310
326
|
.with_model("claude-sonnet-4")
|
|
311
|
-
.with_max_tokens(2000)
|
|
312
327
|
.run("Write a haiku about Ruby programming")
|
|
313
328
|
```
|
|
314
329
|
|
|
330
|
+
There is no `with_max_tokens`. Set token limits in the constructor, or pass them through `with_params`:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
robot = RobotLab.build(name: "writer", system_prompt: "...", max_tokens: 2000)
|
|
334
|
+
|
|
335
|
+
# or, after creation
|
|
336
|
+
robot.with_params(max_tokens: 2000)
|
|
337
|
+
```
|
|
338
|
+
|
|
315
339
|
## Graceful Tool Error Handling
|
|
316
340
|
|
|
317
341
|
`RobotLab::Tool` automatically catches exceptions in `execute` and returns a plain-text error to the LLM instead of crashing the run. The LLM can then reason about the error and try an alternative approach.
|
|
@@ -367,9 +391,22 @@ robot = RobotLab.build(
|
|
|
367
391
|
local_tools: [Magic8Ball]
|
|
368
392
|
)
|
|
369
393
|
|
|
370
|
-
|
|
394
|
+
# NOTE the `tools: :inherit` — see the warning below
|
|
395
|
+
result = robot.run("Should I start learning Rust?", tools: :inherit)
|
|
371
396
|
```
|
|
372
397
|
|
|
398
|
+
> [!WARNING]
|
|
399
|
+
> `run()` defaults to `tools: :none` and `mcp: :none`, and `:none` means **send zero tools this turn**. Attaching tools with `local_tools:` (or servers with `mcp:`) is therefore not enough on its own — a plain `robot.run("...")` sends the LLM no tools at all and connects no MCP servers.
|
|
400
|
+
>
|
|
401
|
+
> Pass `tools: :inherit` / `mcp: :inherit` on each run to use what the robot has attached:
|
|
402
|
+
>
|
|
403
|
+
> ```ruby
|
|
404
|
+
> robot.run("...", tools: :inherit) # send attached tools
|
|
405
|
+
> robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP + send its tools
|
|
406
|
+
> ```
|
|
407
|
+
>
|
|
408
|
+
> `:inherit` means "no filter, use everything attached"; an explicit array acts as a name allowlist. Note that `tools: :inherit` at *build* time behaves differently — it resolves against the parent level, so keep `:inherit` on the `run()` call.
|
|
409
|
+
|
|
373
410
|
## Orchestrating Multiple Robots
|
|
374
411
|
|
|
375
412
|
Networks use [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with optional task activation for intelligent routing:
|
|
@@ -422,24 +459,29 @@ puts result.value.last_text_content
|
|
|
422
459
|
|
|
423
460
|
## Memory
|
|
424
461
|
|
|
425
|
-
|
|
462
|
+
Two independent mechanisms carry state across runs, and it is worth keeping them straight:
|
|
463
|
+
|
|
464
|
+
- **Chat history** — each robot owns a persistent `RubyLLM::Chat`, so conversational context carries forward automatically with no work on your part.
|
|
465
|
+
- **`RobotLab::Memory`** — an explicit key-value store you read and write yourself, shared across robots when they run inside a network.
|
|
426
466
|
|
|
427
467
|
```ruby
|
|
428
|
-
# Standalone robot with inherent memory
|
|
429
468
|
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
430
469
|
|
|
431
470
|
robot.run("My name is Alice")
|
|
432
|
-
robot.run("What's my name?") #
|
|
471
|
+
robot.run("What's my name?") # answered from the persistent chat history
|
|
433
472
|
|
|
434
|
-
# Access robot's memory
|
|
473
|
+
# Access robot's memory (the explicit key-value store)
|
|
435
474
|
robot.memory[:user_id] = 123
|
|
436
475
|
robot.memory.data[:category] = "billing"
|
|
437
476
|
|
|
438
477
|
# Runtime memory injection
|
|
439
478
|
robot.run("Help me", memory: { session_id: "abc123", tier: "premium" })
|
|
440
479
|
|
|
441
|
-
# Reset
|
|
480
|
+
# Reset the key-value store (does NOT clear chat history)
|
|
442
481
|
robot.reset_memory
|
|
482
|
+
|
|
483
|
+
# Clear chat history instead, keeping the system prompt
|
|
484
|
+
robot.clear_messages
|
|
443
485
|
```
|
|
444
486
|
|
|
445
487
|
Networks pass context through SimpleFlow::Result:
|
|
@@ -466,6 +508,8 @@ billing_result = result.context[:billing]
|
|
|
466
508
|
puts result.value.last_text_content
|
|
467
509
|
```
|
|
468
510
|
|
|
511
|
+
Results are keyed in `result.context` by the **robot's** `name`, not by the task name given to `task`. Keep the two identical unless you have a reason not to.
|
|
512
|
+
|
|
469
513
|
## MCP Integration
|
|
470
514
|
|
|
471
515
|
Connect to external tool servers via Model Context Protocol:
|
|
@@ -489,17 +533,18 @@ robot = RobotLab.build(
|
|
|
489
533
|
mcp: [filesystem_server]
|
|
490
534
|
)
|
|
491
535
|
|
|
492
|
-
#
|
|
536
|
+
# Connect. Do this explicitly — run() defaults to mcp: :none, so a plain
|
|
537
|
+
# run() never connects the servers configured above.
|
|
493
538
|
robot.connect_mcp!
|
|
494
539
|
|
|
495
540
|
# Check connection status
|
|
496
541
|
puts "Failed: #{robot.failed_mcp_server_names}" if robot.failed_mcp_server_names.any?
|
|
497
542
|
|
|
498
|
-
# Robot can now use filesystem tools
|
|
499
|
-
result = robot.run("List the files in the current directory")
|
|
543
|
+
# Robot can now use filesystem tools — :inherit is required on the run
|
|
544
|
+
result = robot.run("List the files in the current directory", mcp: :inherit, tools: :inherit)
|
|
500
545
|
```
|
|
501
546
|
|
|
502
|
-
MCP connections are resilient: failed servers are automatically retried on subsequent `run()` calls, and one failing server does not prevent others from connecting.
|
|
547
|
+
MCP connections are resilient: failed servers are automatically retried on subsequent `run(mcp: :inherit)` calls, and one failing server does not prevent others from connecting. Connection failures are logged and recorded in `failed_mcp_server_names` rather than raised, so a robot whose servers all failed still builds and runs — just without those tools.
|
|
503
548
|
|
|
504
549
|
## Message Bus
|
|
505
550
|
|
|
@@ -633,7 +678,7 @@ robot = RobotLab.build(
|
|
|
633
678
|
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
634
679
|
```
|
|
635
680
|
|
|
636
|
-
The `on_content:` callback
|
|
681
|
+
The `on_content:` callback is a `RunConfig` field, so it can also be supplied through a robot's `config:` instead of as a constructor kwarg. It is read from the robot's own config at construction time — a network-level `config:` does not supply it.
|
|
637
682
|
|
|
638
683
|
## Token & Cost Tracking
|
|
639
684
|
|
|
@@ -674,7 +719,8 @@ robot = RobotLab.build(
|
|
|
674
719
|
begin
|
|
675
720
|
robot.run("Run all steps.")
|
|
676
721
|
rescue RobotLab::ToolLoopError => e
|
|
677
|
-
puts e.message
|
|
722
|
+
puts e.message
|
|
723
|
+
# "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
678
724
|
end
|
|
679
725
|
```
|
|
680
726
|
|
|
@@ -687,14 +733,16 @@ result = robot.run("Something new.") # robot is healthy again
|
|
|
687
733
|
|
|
688
734
|
## Doom Loop Detection
|
|
689
735
|
|
|
690
|
-
Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over.
|
|
736
|
+
Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over.
|
|
737
|
+
|
|
738
|
+
Unlike the circuit breaker, doom loop detection is **always active** — it is installed on every run. `doom_loop_threshold:` tunes the sensitivity; it does not switch the feature on. The default threshold is `3`.
|
|
691
739
|
|
|
692
740
|
```ruby
|
|
693
741
|
robot = RobotLab.build(
|
|
694
742
|
name: "runner",
|
|
695
743
|
system_prompt: "Execute steps.",
|
|
696
744
|
local_tools: [StepTool],
|
|
697
|
-
doom_loop_threshold:
|
|
745
|
+
doom_loop_threshold: 5 # tolerate more repetition before warning (default: 3)
|
|
698
746
|
)
|
|
699
747
|
```
|
|
700
748
|
|
|
@@ -709,27 +757,33 @@ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
|
709
757
|
|
|
710
758
|
`auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
|
|
711
759
|
|
|
760
|
+
`auto_compact` and `compact_threshold` are `RunConfig` fields, not constructor keywords — pass them via `config:`:
|
|
761
|
+
|
|
712
762
|
```ruby
|
|
713
763
|
# Built-in trigger: compact when estimated token usage exceeds 80% of context window
|
|
714
764
|
robot = RobotLab.build(
|
|
715
765
|
name: "analyst",
|
|
716
766
|
system_prompt: "You are a research analyst.",
|
|
717
|
-
auto_compact: :context_window
|
|
767
|
+
config: RobotLab::RunConfig.new(auto_compact: :context_window)
|
|
718
768
|
)
|
|
719
769
|
|
|
720
770
|
# Tune the threshold (here: compact at 70%)
|
|
721
771
|
robot = RobotLab.build(
|
|
722
772
|
name: "analyst",
|
|
723
773
|
system_prompt: "You are a research analyst.",
|
|
724
|
-
|
|
725
|
-
|
|
774
|
+
config: RobotLab::RunConfig.new(
|
|
775
|
+
auto_compact: :context_window,
|
|
776
|
+
compact_threshold: 0.70
|
|
777
|
+
)
|
|
726
778
|
)
|
|
727
779
|
|
|
728
780
|
# Application-owned compaction: full control over when and how
|
|
729
781
|
robot = RobotLab.build(
|
|
730
782
|
name: "analyst",
|
|
731
783
|
system_prompt: "You are a research analyst.",
|
|
732
|
-
|
|
784
|
+
config: RobotLab::RunConfig.new(
|
|
785
|
+
auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
|
|
786
|
+
)
|
|
733
787
|
)
|
|
734
788
|
```
|
|
735
789
|
|
|
@@ -739,12 +793,7 @@ robot = RobotLab.build(
|
|
|
739
793
|
| `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
|
|
740
794
|
| `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
|
|
741
795
|
|
|
742
|
-
`compact_threshold` defaults to `0.80` (80%).
|
|
743
|
-
|
|
744
|
-
```ruby
|
|
745
|
-
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
|
|
746
|
-
robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
|
|
747
|
-
```
|
|
796
|
+
`compact_threshold` defaults to `0.80` (80%). The built-in `:context_window` strategy uses the optional `classifier` gem; if it is missing, compaction is skipped with a logged warning rather than raising.
|
|
748
797
|
|
|
749
798
|
## Learning Accumulation
|
|
750
799
|
|
|
@@ -762,18 +811,9 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
|
|
|
762
811
|
reviewer.run("Review snippet B") # learning is injected automatically
|
|
763
812
|
```
|
|
764
813
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
```ruby
|
|
768
|
-
reviewer = RobotLab.build(
|
|
769
|
-
name: "reviewer",
|
|
770
|
-
system_prompt: "You are a Ruby code reviewer.",
|
|
771
|
-
learn: true,
|
|
772
|
-
learn_domain: "ruby_review"
|
|
773
|
-
)
|
|
774
|
-
```
|
|
814
|
+
Cross-session learning promotion is provided by the [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) gem, which hooks the `:learn` lifecycle family. See that gem's documentation for setup.
|
|
775
815
|
|
|
776
|
-
Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory`
|
|
816
|
+
Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory` for the lifetime of that robot.
|
|
777
817
|
|
|
778
818
|
```ruby
|
|
779
819
|
reviewer.learnings # => ["This codebase prefers map/collect..."]
|
|
@@ -782,7 +822,9 @@ reviewer.learn("new fact") # deduplicates before storing
|
|
|
782
822
|
|
|
783
823
|
## Context Window Compression
|
|
784
824
|
|
|
785
|
-
`robot.compress_history` prunes old conversation turns using
|
|
825
|
+
`robot.compress_history` prunes old conversation turns using term-frequency cosine similarity, keeping only turns that are relevant to the most recent context. System messages and tool call/result pairs are always preserved.
|
|
826
|
+
|
|
827
|
+
Scoring uses raw term frequencies without IDF weighting — on a topic-focused conversation, IDF would suppress exactly the recurring terms that signal relevance.
|
|
786
828
|
|
|
787
829
|
```ruby
|
|
788
830
|
# Basic compression: protect the 3 most recent turns, drop unrelated old turns
|
|
@@ -810,7 +852,7 @@ gem "classifier", "~> 2.3"
|
|
|
810
852
|
|
|
811
853
|
## Convergence Detection
|
|
812
854
|
|
|
813
|
-
`RobotLab::Convergence` detects when two independent agents have reached the same conclusion using
|
|
855
|
+
`RobotLab::Convergence` detects when two independent agents have reached the same conclusion using term-frequency cosine similarity (no IDF — on a two-document comparison, IDF collapses the shared terms to zero). Use it to skip an expensive reconciler LLM call when verifiers already agree.
|
|
814
856
|
|
|
815
857
|
```ruby
|
|
816
858
|
# Check similarity directly
|
|
@@ -825,18 +867,30 @@ RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
|
825
867
|
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
826
868
|
```
|
|
827
869
|
|
|
828
|
-
|
|
870
|
+
`similarity` returns `0.0` if either text is blank or shorter than 30 characters (`Convergence::MIN_TEXT_LENGTH`), since term-frequency scoring is not meaningful on very short strings. Short replies like `"yes"` will therefore never register as converged.
|
|
871
|
+
|
|
872
|
+
A common pattern is a gate robot that decides whether reconciliation is needed. Routing is done by overriding `call` and activating an `depends_on: :optional` task — networks have no separate router parameter:
|
|
829
873
|
|
|
830
874
|
```ruby
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
875
|
+
class ConvergenceGate < RobotLab::Robot
|
|
876
|
+
def call(result)
|
|
877
|
+
a = result.context[:verifier_a]&.reply.to_s
|
|
878
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
879
|
+
|
|
880
|
+
# Converged? Pass the result straight through and leave :reconciler dormant.
|
|
881
|
+
RobotLab::Convergence.detected?(a, b) ? result : result.activate(:reconciler)
|
|
882
|
+
end
|
|
835
883
|
end
|
|
836
884
|
|
|
837
|
-
network = RobotLab.create_network(name: "verify"
|
|
838
|
-
|
|
885
|
+
network = RobotLab.create_network(name: "verify") do
|
|
886
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
887
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
888
|
+
task :gate, ConvergenceGate.new(name: "gate"), depends_on: [:verifier_a, :verifier_b]
|
|
889
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
839
890
|
end
|
|
891
|
+
|
|
892
|
+
result = network.run(message: "Audit this change")
|
|
893
|
+
result.context.key?(:reconciler) # => false when the verifiers agreed
|
|
840
894
|
```
|
|
841
895
|
|
|
842
896
|
Requires the `classifier` gem (`~> 2.3`).
|
|
@@ -876,17 +930,84 @@ future.robot_name # => "analyst"
|
|
|
876
930
|
future.delegated_by # => "manager"
|
|
877
931
|
```
|
|
878
932
|
|
|
933
|
+
## Hook System
|
|
934
|
+
|
|
935
|
+
RobotLab's hook system lets you intercept any point in a robot's execution pipeline without modifying framework code. Hooks use handler classes — subclasses of `RobotLab::Hook` that define lifecycle callbacks as class methods.
|
|
936
|
+
|
|
937
|
+
```ruby
|
|
938
|
+
class TimerHook < RobotLab::Hook
|
|
939
|
+
# namespace is auto-derived: TimerHook → :timer_hook
|
|
940
|
+
# override with: self.namespace = :timer
|
|
941
|
+
|
|
942
|
+
def self.before_run(ctx)
|
|
943
|
+
ctx.local.start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def self.after_run(ctx)
|
|
947
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.local.start) * 1000).round(1)
|
|
948
|
+
puts "[timer] #{ctx.robot.name} #{elapsed}ms"
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def self.on_error(ctx)
|
|
952
|
+
puts "[timer] #{ctx.robot.name} failed: #{ctx.error.message}"
|
|
953
|
+
end
|
|
954
|
+
end
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Register at the global, network, or robot level — or for a single call:
|
|
958
|
+
|
|
959
|
+
```ruby
|
|
960
|
+
RobotLab.on(TimerHook) # every robot in this process
|
|
961
|
+
network.on(TimerHook) # robots inside this network only
|
|
962
|
+
robot.on(TimerHook) # this robot only
|
|
963
|
+
robot.run("msg", hooks: TimerHook) # this call only
|
|
964
|
+
robot.run("msg", hooks: [TimerHook, OtherHook])
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
Pass `context:` to set default `DotState` values for the handler's namespace before each callback fires:
|
|
968
|
+
|
|
969
|
+
```ruby
|
|
970
|
+
RobotLab.on(TimerHook, context: { threshold_ms: 500 })
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
Around hooks receive a block and must call it — and return its value — so the actual work executes:
|
|
974
|
+
|
|
975
|
+
```ruby
|
|
976
|
+
class PerfHook < RobotLab::Hook
|
|
977
|
+
self.namespace = :perf
|
|
978
|
+
|
|
979
|
+
def self.around_run(ctx, &block)
|
|
980
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
981
|
+
result = block.call # executes the run
|
|
982
|
+
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(1)
|
|
983
|
+
$stderr.puts "[perf] #{ctx.robot.name} #{ms}ms"
|
|
984
|
+
result # must return the result
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Because handler classes are Ruby constants (not Procs), all hook registrations are natively Ractor-serializable.
|
|
990
|
+
|
|
991
|
+
See the [full Hook System guide](docs/guides/hooks.md) for all hook families, context objects, and extension patterns.
|
|
992
|
+
|
|
879
993
|
## Extension Gems
|
|
880
994
|
|
|
881
995
|
RobotLab's optional capabilities are packaged as separate gems:
|
|
882
996
|
|
|
883
997
|
| Gem | Description |
|
|
884
998
|
|-----|-------------|
|
|
885
|
-
| [robot_lab-
|
|
999
|
+
| [robot_lab-a2a](https://github.com/MadBomber/robot_lab-a2a) | Expose robots and networks as Agent2Agent (A2A) protocol services over HTTP+SSE |
|
|
1000
|
+
| [robot_lab-audit](https://github.com/MadBomber/robot_lab-audit) | SQLite-backed execution audit log wired through the Hook system |
|
|
1001
|
+
| [robot_lab-discovery](https://github.com/MadBomber/robot_lab-discovery) | Zero-configuration mDNS/DNS-SD robot discovery on local networks |
|
|
1002
|
+
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document store for search / RAG |
|
|
1003
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | HTM-backed long-term memory and cross-session knowledge persistence |
|
|
1004
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | Ractor-based parallel tool execution and DAG-scheduled parallel networks |
|
|
886
1005
|
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
|
|
887
|
-
| [robot_lab-
|
|
888
|
-
| [robot_lab-
|
|
889
|
-
|
|
1006
|
+
| [robot_lab-to](https://github.com/MadBomber/robot_lab-to) | Autonomous overnight agent loop — iterate a robot toward an objective |
|
|
1007
|
+
| [robot_lab-web](https://github.com/MadBomber/robot_lab-web) | Browser console — stream a robot's run over Server-Sent Events |
|
|
1008
|
+
|
|
1009
|
+
> [!NOTE]
|
|
1010
|
+
> `robot_lab-acp` is retired; its functionality is superseded by `robot_lab-a2a`.
|
|
890
1011
|
|
|
891
1012
|
## Documentation
|
|
892
1013
|
|
data/Rakefile
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
|
5
5
|
|
|
6
|
+
RUBOCOP_ENV = { "RUBOCOP_CACHE_ROOT" => "tmp/rubocop_cache" }.freeze
|
|
7
|
+
|
|
6
8
|
Rake::TestTask.new(:test) do |t|
|
|
7
9
|
t.libs << "test"
|
|
8
10
|
t.libs << "lib"
|
|
@@ -34,16 +36,6 @@ Rake::TestTask.new(:integration) do |t|
|
|
|
34
36
|
t.ruby_opts << "-rtest_helper"
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
desc "Check code style with RuboCop"
|
|
38
|
-
task :rubocop do
|
|
39
|
-
sh "bundle exec rubocop"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
desc "Auto-correct RuboCop offenses"
|
|
43
|
-
task :rubocop_fix do
|
|
44
|
-
sh "bundle exec rubocop -a"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
39
|
desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
|
|
48
40
|
task :flog_check do
|
|
49
41
|
require 'flog'
|
|
@@ -82,7 +74,26 @@ task :flog_check do
|
|
|
82
74
|
end
|
|
83
75
|
end
|
|
84
76
|
|
|
85
|
-
desc "
|
|
77
|
+
desc "Check for structural code duplication with Flay (mass >= 50)"
|
|
78
|
+
task :flay_check do
|
|
79
|
+
require 'flay'
|
|
80
|
+
|
|
81
|
+
mass_threshold = 50
|
|
82
|
+
|
|
83
|
+
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
84
|
+
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
85
|
+
flay.analyze
|
|
86
|
+
|
|
87
|
+
if flay.hashes.empty?
|
|
88
|
+
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
89
|
+
else
|
|
90
|
+
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
91
|
+
flay.report
|
|
92
|
+
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay"
|
|
86
97
|
task :quality do
|
|
87
98
|
results = {}
|
|
88
99
|
|
|
@@ -94,13 +105,18 @@ task :quality do
|
|
|
94
105
|
puts "\n#{'=' * 60}"
|
|
95
106
|
puts "Quality Gate: RuboCop"
|
|
96
107
|
puts '=' * 60
|
|
97
|
-
results[:rubocop] = system("bundle exec rubocop") ? :pass : :fail
|
|
108
|
+
results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
|
|
98
109
|
|
|
99
110
|
puts "\n#{'=' * 60}"
|
|
100
111
|
puts "Quality Gate: Flog Complexity"
|
|
101
112
|
puts '=' * 60
|
|
102
113
|
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
103
114
|
|
|
115
|
+
puts "\n#{'=' * 60}"
|
|
116
|
+
puts "Quality Gate: Flay Duplication"
|
|
117
|
+
puts '=' * 60
|
|
118
|
+
results[:flay] = system("bundle exec rake flay_check") ? :pass : :fail
|
|
119
|
+
|
|
104
120
|
puts "\n#{'=' * 60}"
|
|
105
121
|
puts "Quality Summary"
|
|
106
122
|
puts '=' * 60
|