robot_lab 0.1.0 → 0.2.1

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.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.irbrc +2 -2
  10. data/.rubocop.yml +172 -0
  11. data/CHANGELOG.md +72 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -95
  14. data/Rakefile +109 -3
  15. data/agent2agent_review.md +192 -0
  16. data/agentf_improvements.md +253 -0
  17. data/agents.md +14 -0
  18. data/docs/examples/index.md +37 -2
  19. data/docs/getting-started/configuration.md +20 -7
  20. data/docs/guides/index.md +16 -16
  21. data/docs/guides/knowledge.md +7 -1
  22. data/docs/guides/observability.md +132 -0
  23. data/docs/index.md +30 -3
  24. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  25. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  26. data/examples/.envrc +1 -0
  27. data/examples/01_simple_robot.rb +5 -9
  28. data/examples/02_tools.rb +5 -9
  29. data/examples/03_network.rb +8 -9
  30. data/examples/04_mcp.rb +21 -29
  31. data/examples/05_streaming.rb +12 -18
  32. data/examples/06_prompt_templates.rb +11 -19
  33. data/examples/07_network_memory.rb +16 -31
  34. data/examples/08_llm_config.rb +10 -22
  35. data/examples/09_chaining.rb +16 -27
  36. data/examples/10_memory.rb +12 -28
  37. data/examples/11_network_introspection.rb +15 -29
  38. data/examples/12_message_bus.rb +5 -12
  39. data/examples/13_spawn.rb +5 -10
  40. data/examples/14_rusty_circuit/.envrc +1 -0
  41. data/examples/14_rusty_circuit/comic.rb +2 -0
  42. data/examples/14_rusty_circuit/heckler.rb +1 -1
  43. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  44. data/examples/14_rusty_circuit/scout.rb +2 -0
  45. data/examples/15_memory_network_and_bus/.envrc +1 -0
  46. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  47. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  48. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  49. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  50. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  51. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  52. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  53. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  54. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  55. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  56. data/examples/16_writers_room/.envrc +1 -0
  57. data/examples/16_writers_room/writers_room.rb +2 -4
  58. data/examples/17_skills.rb +8 -17
  59. data/examples/18_rails/Gemfile +1 -0
  60. data/examples/19_token_tracking.rb +9 -15
  61. data/examples/20_circuit_breaker.rb +10 -19
  62. data/examples/21_learning_loop.rb +11 -20
  63. data/examples/22_context_compression.rb +6 -13
  64. data/examples/23_convergence.rb +6 -17
  65. data/examples/24_structured_delegation.rb +11 -15
  66. data/examples/25_history_search.rb +5 -12
  67. data/examples/26_document_store.rb +6 -13
  68. data/examples/27_incident_response/incident_response.rb +4 -5
  69. data/examples/28_mcp_discovery.rb +8 -11
  70. data/examples/29_ractor_tools.rb +4 -9
  71. data/examples/30_ractor_network.rb +10 -19
  72. data/examples/31_launch_assessment.rb +10 -23
  73. data/examples/32_newsletter_reader.rb +188 -0
  74. data/examples/33_stock_generator.rb +80 -0
  75. data/examples/33_stock_predictor.rb +306 -0
  76. data/examples/34_agentskills.rb +72 -0
  77. data/examples/README.md +1 -1
  78. data/examples/common.rb +76 -0
  79. data/examples/ruboruby.md +423 -0
  80. data/examples/temp.md +51 -0
  81. data/lib/robot_lab/agent_skill.rb +63 -0
  82. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  83. data/lib/robot_lab/ask_user.rb +2 -2
  84. data/lib/robot_lab/bus_poller.rb +12 -5
  85. data/lib/robot_lab/config.rb +1 -12
  86. data/lib/robot_lab/delegation_future.rb +1 -1
  87. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  88. data/lib/robot_lab/history_compressor.rb +4 -10
  89. data/lib/robot_lab/mcp/client.rb +1 -2
  90. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  91. data/lib/robot_lab/mcp/server.rb +1 -1
  92. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  93. data/lib/robot_lab/memory.rb +32 -27
  94. data/lib/robot_lab/memory_change.rb +2 -2
  95. data/lib/robot_lab/message.rb +4 -4
  96. data/lib/robot_lab/network.rb +11 -6
  97. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  98. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  99. data/lib/robot_lab/robot/history_search.rb +4 -1
  100. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  101. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  102. data/lib/robot_lab/robot.rb +323 -206
  103. data/lib/robot_lab/robot_result.rb +6 -5
  104. data/lib/robot_lab/run_config.rb +5 -11
  105. data/lib/robot_lab/script_tool.rb +76 -0
  106. data/lib/robot_lab/state_proxy.rb +7 -5
  107. data/lib/robot_lab/tool.rb +3 -3
  108. data/lib/robot_lab/tool_config.rb +1 -1
  109. data/lib/robot_lab/tool_manifest.rb +5 -7
  110. data/lib/robot_lab/user_message.rb +2 -2
  111. data/lib/robot_lab/version.rb +1 -1
  112. data/lib/robot_lab/waiter.rb +1 -1
  113. data/lib/robot_lab.rb +41 -52
  114. data/logfile +8 -0
  115. data/mkdocs.yml +2 -3
  116. data/robot_concurrency.md +38 -0
  117. data/simple_acp_review.md +298 -0
  118. data/site/404.html +2300 -0
  119. data/site/api/core/index.html +2706 -0
  120. data/site/api/core/memory/index.html +3793 -0
  121. data/site/api/core/network/index.html +3500 -0
  122. data/site/api/core/robot/index.html +4566 -0
  123. data/site/api/core/state/index.html +3390 -0
  124. data/site/api/core/tool/index.html +3843 -0
  125. data/site/api/index.html +2635 -0
  126. data/site/api/mcp/client/index.html +3435 -0
  127. data/site/api/mcp/index.html +2783 -0
  128. data/site/api/mcp/server/index.html +3252 -0
  129. data/site/api/mcp/transports/index.html +3352 -0
  130. data/site/api/messages/index.html +2641 -0
  131. data/site/api/messages/text-message/index.html +3087 -0
  132. data/site/api/messages/tool-call-message/index.html +3159 -0
  133. data/site/api/messages/tool-result-message/index.html +3252 -0
  134. data/site/api/messages/user-message/index.html +3212 -0
  135. data/site/api/streaming/context/index.html +3282 -0
  136. data/site/api/streaming/events/index.html +3347 -0
  137. data/site/api/streaming/index.html +2738 -0
  138. data/site/architecture/core-concepts/index.html +3757 -0
  139. data/site/architecture/index.html +2797 -0
  140. data/site/architecture/message-flow/index.html +3238 -0
  141. data/site/architecture/network-orchestration/index.html +3433 -0
  142. data/site/architecture/robot-execution/index.html +3140 -0
  143. data/site/architecture/state-management/index.html +3498 -0
  144. data/site/assets/css/custom.css +56 -0
  145. data/site/assets/images/favicon.png +0 -0
  146. data/site/assets/images/robot_lab.jpg +0 -0
  147. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  148. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  149. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  150. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  151. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  152. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  153. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  154. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  157. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  158. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  159. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  162. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  163. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  168. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  169. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  170. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  173. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  174. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  178. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  179. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  181. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  182. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  183. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  184. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  185. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  186. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  187. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  188. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  189. data/site/concepts/index.html +3455 -0
  190. data/site/examples/basic-chat/index.html +2880 -0
  191. data/site/examples/index.html +2907 -0
  192. data/site/examples/mcp-server/index.html +3018 -0
  193. data/site/examples/multi-robot-network/index.html +3131 -0
  194. data/site/examples/rails-application/index.html +3329 -0
  195. data/site/examples/tool-usage/index.html +3085 -0
  196. data/site/getting-started/configuration/index.html +3745 -0
  197. data/site/getting-started/index.html +2572 -0
  198. data/site/getting-started/installation/index.html +2981 -0
  199. data/site/getting-started/quick-start/index.html +2942 -0
  200. data/site/guides/building-robots/index.html +4290 -0
  201. data/site/guides/creating-networks/index.html +3858 -0
  202. data/site/guides/index.html +2586 -0
  203. data/site/guides/mcp-integration/index.html +3581 -0
  204. data/site/guides/memory/index.html +3586 -0
  205. data/site/guides/rails-integration/index.html +4019 -0
  206. data/site/guides/streaming/index.html +3157 -0
  207. data/site/guides/using-tools/index.html +3802 -0
  208. data/site/index.html +2671 -0
  209. data/site/search/search_index.json +1 -0
  210. data/site/sitemap.xml +183 -0
  211. data/site/sitemap.xml.gz +0 -0
  212. data/site/tags.json +1 -0
  213. data/temp.md +6 -0
  214. data/tool_manifest_plan.md +155 -0
  215. metadata +154 -92
  216. data/docs/examples/rails-application.md +0 -419
  217. data/docs/guides/ractor-parallelism.md +0 -364
  218. data/docs/guides/rails-integration.md +0 -681
  219. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  220. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  221. data/lib/generators/robot_lab/install_generator.rb +0 -90
  222. data/lib/generators/robot_lab/job_generator.rb +0 -40
  223. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  224. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  225. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  226. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  227. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  228. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  229. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  230. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  231. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  232. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  233. data/lib/robot_lab/document_store.rb +0 -155
  234. data/lib/robot_lab/ractor_boundary.rb +0 -42
  235. data/lib/robot_lab/ractor_job.rb +0 -37
  236. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  237. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  238. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  239. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  240. data/lib/robot_lab/rails_integration/job.rb +0 -158
  241. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  242. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
data/CHANGELOG.md CHANGED
@@ -8,6 +8,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.1] - 2026-05-19
12
+
13
+ ### Added
14
+
15
+ - **`examples/common.rb`** — shared setup file required by all numbered examples. Defines:
16
+ - `LlmConfig = Data.define(:provider, :model)` and a frozen `LLM` hash with `:default` (OpenAI/gpt-5.4), `:local` (Ollama/llama3.2), and `:anthropic` (claude-opus-4-7) entries — access as `LLM[:default].model`
17
+ - `RubyLLM.configure` with null logger and `LLM[:default].model` as `default_model`
18
+ - `RobotLab.configure` with null logger
19
+ - Output helpers: `banner(title)`, `section(title)`, `hr`, `show_code(ruby_string, label:)` using `rouge` for syntax highlighting
20
+ - **`rouge` gem** added to development group for syntax-highlighted example output
21
+ - **`.envrc` files** in `examples/`, `examples/14_rusty_circuit/`, `examples/15_memory_network_and_bus/`, and `examples/16_writers_room/` — each exports `ROBOT_LAB_TEMPLATE_PATH` pointing at the local `prompts/` directory for use with [direnv](https://direnv.net/)
22
+
23
+ ### Changed
24
+
25
+ - **All 27+ numbered examples** refactored to `require_relative "common"` instead of requiring the gem directly, and to use `LLM[:default].model` instead of the hardcoded string `"gpt-5.4"`
26
+ - **Example 04 (MCP)** now calls `robot.connect_mcp!` before inspecting MCP attributes, fixing a lazy-initialization issue where `mcp_clients` and `mcp_tools` were empty until the first `run()` call
27
+
28
+ ## [0.2.1] - 2026-05-11 (unreleased)
29
+
30
+ ### Added
31
+
32
+ - **`flog` complexity gate** — `flog_check` Rake task enforces method-level complexity limits (warn ≥20, fail ≥50); `quality` task runs tests, RuboCop, and Flog in sequence with a unified pass/fail summary
33
+ - `flog` gem added to development/test group
34
+ - Branch coverage enabled unconditionally (previously CI-only) with minimum thresholds: line 95%, branch 75%
35
+
36
+ ### Changed
37
+
38
+ - Bumped version to 0.2.1
39
+ - **`Robot#initialize` decomposed** into focused private methods: `assign_identity_ivars`, `build_effective_config`, `extract_config_ivars`, `initialize_runtime_state`, `initialize_memory`, `configure_learning`, `apply_template`, `apply_system_prompt`, `apply_chat_params`, `register_chat_callbacks`
40
+ - **`Robot#run` decomposed** into: `resolve_run_memory`, `prepare_tools`, `invoke_ask`, `enforce_token_budget!`
41
+ - **`BusPoller#process_and_drain`** split into `drain_queued_deliveries` and `release_robot` for independent testability
42
+ - **`TemplateRendering#apply_skills_and_template_to_chat`** split into `collect_prompt_content` (pure computation) and `apply_prompt_to_chat` (pure mutation)
43
+ - Removed `.serena/` project configuration files from version control
44
+ - Removed `.claude/memory.sqlite3` database files from version control
45
+
46
+ ## [0.2.0] - 2026-05-07
47
+
48
+ ### Added
49
+
50
+ - **`Durable::Learning`** — cross-session and within-session learning capability for robots. Robots accept `learn: true` and `learn_domain:` constructor params to persist knowledge across sessions via `~/.robot_lab/durable/` YAML files.
51
+ - **`Durable::Store`** — YAML-backed knowledge store with file locking, keyword recall, and confidence tracking.
52
+ - **`Durable::Entry`** — immutable value object for knowledge records with confidence progression.
53
+ - **`Durable::Reflector`** — promotes session learnings to durable storage at end of each run.
54
+ - **`RecallKnowledge` tool** — robots query past knowledge before uncertain decisions.
55
+ - **`RecordKnowledge` tool** — robots persist new knowledge learned during a session.
56
+ - **`AgentSkill`** — value object for discoverable skills defined by `SKILL.md` files with YAML front matter (name, description, version, dependencies, parameters)
57
+ - **`AgentSkillCatalog`** — service for locating and indexing `AgentSkills/` directories at runtime
58
+ - **`AgentSkillMatching`** — `Robot` mixin enabling runtime embedding-based skill injection: the robot selects the most relevant skills from a catalog based on semantic similarity before each run
59
+ - **`ScriptTool`** — factory that wraps shell scripts as robot tools; auto-generates JSON schema from script `--help` output
60
+ - **`DoomLoopDetector`** — detects consecutive and cyclic tool-call repetition; wired into `Robot#run` via singleton `execute_tool` override
61
+ - **`doom_loop_threshold`** field on `RunConfig` — configures the repetition threshold before a `ToolLoopError` is raised
62
+ - **`auto_compact` and `compact_threshold`** fields on `RunConfig` — `:context_window` mode estimates token usage before each run and calls `compress_history` when usage exceeds the threshold (default 80%); a `Proc` value delegates the decision and strategy entirely to the caller
63
+
64
+ ### Changed
65
+
66
+ - Bumped version to 0.2.0
67
+ - **Extension gems extracted** — `robot_lab-document_store`, `robot_lab-durable`, `robot_lab-ractor`, and `robot_lab-rails` are now separate gems distributed via rubygems.org; the core gemspec drops `fastembed`, `ractor_queue`, and `ractor-wrapper` as hard dependencies
68
+ - **`Durable::Learning` inclusion** is now conditional on the `robot_lab-durable` gem being loaded
69
+ - **Tool Ractor routing** guarded on `RobotLab.respond_to?(:ractor_pool)` so the core gem runs without the ractor extension
70
+ - **Memory drainer scheduling** — `@drainer_scheduled` remains `true` when rescheduling to prevent concurrent writers from spawning competing drain fibers
71
+ - `@chat` state reset now uses `reset_messages!` / `add_message` public API instead of internal instance variable manipulation
72
+ - Rails generators moved to `robot_lab-rails` extension gem
73
+
74
+ ### Fixed
75
+
76
+ - Memory drainer double-schedule race when concurrent writers triggered overlapping drain cycles
77
+ - `DocumentStore` instantiation guarded with a `LoadError` message when the extension gem is absent
78
+ - `AgentSkill` YAML parsing hardened against empty strings and non-Hash front matter
79
+ - Missing requires added to examples after gem extraction
80
+ - `doom_loop_threshold` and `auto_compact` documented in README and guides
81
+ - `learn:` parameter and `robot_lab-acp` documented in README
82
+
11
83
  ## [0.1.0] - 2026-04-29
12
84
 
13
85
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,139 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ RobotLab is a Ruby framework for building and orchestrating multi-robot LLM workflows. It provides:
8
+ - **Robots**: LLM agents with tools, templates, and memory
9
+ - **Networks**: Orchestration of multiple robots with routing logic
10
+ - **MCP Integration**: Model Context Protocol for external tool servers
11
+ - **Rails Integration**: Generators and ActiveRecord support for conversation history
12
+
13
+ Built on top of [ruby_llm](https://rubyllm.com) and uses Zeitwerk for autoloading.
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ # Run all tests
19
+ bundle exec rake test
20
+
21
+ # Run a single test file
22
+ bundle exec rake test_file[robot_lab/robot_test.rb]
23
+
24
+ # Run tests with verbose output
25
+ bundle exec rake test_verbose
26
+
27
+ # Run integration tests only
28
+ bundle exec rake integration
29
+
30
+ # Lint with RuboCop
31
+ bundle exec rubocop
32
+
33
+ # Auto-fix RuboCop offenses
34
+ bundle exec rubocop -a
35
+
36
+ # Run all examples
37
+ bundle exec rake examples:all
38
+
39
+ # Run specific example by number (e.g., 01, 02)
40
+ bundle exec rake examples:run[1]
41
+ ```
42
+
43
+ ## Architecture
44
+
45
+ ### Core Classes
46
+
47
+ - **`RobotLab`** (`lib/robot_lab.rb`): Module entry point with factory methods `build()` for robots and `create_network()` for networks
48
+ - **`Robot`** (`lib/robot_lab/robot.rb`): Subclass of `RubyLLM::Agent` with template-based prompts, tools, MCP clients, and memory. Creates a persistent chat on initialization. Use `robot.run("...")` to interact. When standalone uses its own memory; when in a network uses shared network memory
49
+ - **`Network`** (`lib/robot_lab/network.rb`): Orchestrates multiple robots with routing logic. Robots execute sequentially sharing memory. Router is a lambda that returns robot names
50
+ - **`NetworkRun`** (`lib/robot_lab/network_run.rb`): Stateful execution of a network run with isolated memory clone
51
+
52
+ ### RunConfig
53
+
54
+ - **`RunConfig`** (`lib/robot_lab/run_config.rb`): Shared configuration object for LLM, tools, callbacks, and infrastructure settings. Flows through the hierarchy: `RobotLab.config -> Network -> Robot -> Template front matter -> Task -> Runtime`. Supports keyword construction, block DSL, merge semantics (more-specific wins), and `apply_to(chat)` for LLM field application. Both Robot and Network accept `config:` parameter. Infrastructure fields include: `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold`.
55
+
56
+ ### Memory System
57
+
58
+ - **`Memory`** (`lib/robot_lab/memory.rb`): Key-value store with reserved keys (`:data`, `:results`, `:messages`, `:session_id`, `:cache`). Supports Redis backend. Includes semantic caching via RubyLLM::SemanticCache
59
+
60
+ ### MCP (Model Context Protocol)
61
+
62
+ - **`MCP::Client`** (`lib/robot_lab/mcp/client.rb`): Connects to MCP servers
63
+ - **Transports** (`lib/robot_lab/mcp/transports/`): stdio, websocket, SSE, streamable HTTP
64
+
65
+ ### Built-in Tools
66
+
67
+ - **`AskUser`** (`lib/robot_lab/ask_user.rb`): Tool that lets a robot ask the user a question via the terminal. Supports open-ended text, multiple choice, and default values. IO sourced from `robot.input`/`robot.output` (defaults to `$stdin`/`$stdout`)
68
+
69
+ ### Adapters
70
+
71
+ Provider adapters in `lib/robot_lab/adapters/`: Anthropic, OpenAI, Gemini (for provider-specific formatting)
72
+
73
+ ### Configuration Hierarchy
74
+
75
+ Tools and MCP servers use hierarchical configuration: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
76
+
77
+ ## Key Patterns
78
+
79
+ ### Creating Robots
80
+
81
+ ```ruby
82
+ # Bare robot (no template or prompt)
83
+ robot = RobotLab.build
84
+ robot.with_instructions("You are helpful.").run("Hello!")
85
+
86
+ # With template (.md file in prompts directory with YAML front matter)
87
+ robot = RobotLab.build(name: "helper", template: :helper, context: { key: "value" })
88
+ result = robot.run("Hello!")
89
+
90
+ # With inline system prompt
91
+ robot = RobotLab.build(name: "bot", system_prompt: "You are helpful.")
92
+ result = robot.run("What can you do?")
93
+
94
+ # With tools
95
+ robot = RobotLab.build(name: "bot", system_prompt: "...", local_tools: [my_tool])
96
+
97
+ # Chaining with_* methods
98
+ robot.with_temperature(0.9).with_model("claude-sonnet-4").run("Be creative!")
99
+ ```
100
+
101
+ ### Creating Networks
102
+
103
+ ```ruby
104
+ router = ->(args) { args.call_count.zero? ? ["classifier"] : nil }
105
+ network = RobotLab.create_network(name: "support", robots: [robot1, robot2], router: router)
106
+ result = network.run(message: "Hello")
107
+ ```
108
+
109
+ ### Router Args
110
+
111
+ Router receives `Router::Args` with: `context`, `network`, `stack`, `call_count`, `last_result`
112
+
113
+ ## Testing
114
+
115
+ - Uses Minitest with SimpleCov for coverage
116
+ - Test helper at `test/test_helper.rb` provides `build_robot`, `build_network`, `build_tool` helpers
117
+ - Templates for tests are in `examples/prompts/`
118
+ - VCR and WebMock for HTTP stubbing
119
+
120
+ ## Dependencies
121
+
122
+ Core: zeitwerk, ruby_llm (~> 1.12), ruby_llm-mcp, prompt_manager, ruby_llm-schema, ruby_llm-semantic_cache, async, simple_flow, state_machines
123
+
124
+ ### Templates
125
+
126
+ Templates use prompt_manager format: single `.md` files with YAML front matter in the configured prompts directory.
127
+
128
+ ```markdown
129
+ ---
130
+ description: A helpful assistant
131
+ parameters:
132
+ company_name: null
133
+ tone: friendly
134
+ ---
135
+ You are a helpful assistant for <%= company_name %>.
136
+ Respond in a <%= tone %> manner.
137
+ ```
138
+
139
+ Front matter supports: `description`, `parameters` (null = required), LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, etc.), and robot extras (`robot_name`, `tools`, `mcp`). Constructor-provided values always override front matter.
data/README.md CHANGED
@@ -26,7 +26,7 @@
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
28
  - <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
29
- - <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting<br>
29
+ - <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
30
  - <strong>Token &amp; Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
31
31
  - <strong>Tool Loop Circuit Breaker</strong> - <code>max_tool_rounds:</code> guards against runaway tool call loops<br>
32
32
  - <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
@@ -93,7 +93,7 @@ robot = RobotLab.build(
93
93
 
94
94
  ### Configuration
95
95
 
96
- RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. There is no `configure` block. Configuration is loaded automatically from multiple sources in priority order:
96
+ RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
97
97
 
98
98
  1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
99
99
  2. Environment-specific overrides (development, test, production)
@@ -124,6 +124,14 @@ ruby_llm:
124
124
  request_timeout: 180
125
125
  ```
126
126
 
127
+ Runtime-only attributes (such as the logger) can be set with a `configure` block:
128
+
129
+ ```ruby
130
+ RobotLab.configure do |c|
131
+ c.logger = Logger.new(File::NULL) # silence logging
132
+ end
133
+ ```
134
+
127
135
  ### Using Templates
128
136
 
129
137
  For production applications, RobotLab supports a template system built on [PromptManager](https://github.com/MadBomber/prompt_manager). Templates allow you to:
@@ -677,6 +685,67 @@ robot.clear_messages # flushes broken history; system prompt is kept
677
685
  result = robot.run("Something new.") # robot is healthy again
678
686
  ```
679
687
 
688
+ ## Doom Loop Detection
689
+
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. Set `doom_loop_threshold:` to enable it:
691
+
692
+ ```ruby
693
+ robot = RobotLab.build(
694
+ name: "runner",
695
+ system_prompt: "Execute steps.",
696
+ local_tools: [StepTool],
697
+ doom_loop_threshold: 3 # alert after 3 identical consecutive or cyclic sequences
698
+ )
699
+ ```
700
+
701
+ When a doom loop is detected, a warning is embedded directly into the tool result, prompting the LLM to try a different approach. Detection covers both consecutive repetition (`A,A,A`) and cyclic patterns (`A,B,C,A,B,C`). Via `RunConfig`:
702
+
703
+ ```ruby
704
+ config = RobotLab::RunConfig.new(doom_loop_threshold: 3)
705
+ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
706
+ ```
707
+
708
+ ## Automatic Context Compaction
709
+
710
+ `auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
711
+
712
+ ```ruby
713
+ # Built-in trigger: compact when estimated token usage exceeds 80% of context window
714
+ robot = RobotLab.build(
715
+ name: "analyst",
716
+ system_prompt: "You are a research analyst.",
717
+ auto_compact: :context_window
718
+ )
719
+
720
+ # Tune the threshold (here: compact at 70%)
721
+ robot = RobotLab.build(
722
+ name: "analyst",
723
+ system_prompt: "You are a research analyst.",
724
+ auto_compact: :context_window,
725
+ compact_threshold: 0.70
726
+ )
727
+
728
+ # Application-owned compaction: full control over when and how
729
+ robot = RobotLab.build(
730
+ name: "analyst",
731
+ system_prompt: "You are a research analyst.",
732
+ auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
733
+ )
734
+ ```
735
+
736
+ | Value | Behaviour |
737
+ |-------|-----------|
738
+ | `nil` / `:none` | No automatic compaction (default) |
739
+ | `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
740
+ | `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
741
+
742
+ `compact_threshold` defaults to `0.80` (80%). Requires the `classifier` gem when using the built-in `:context_window` strategy. Via `RunConfig`:
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
+ ```
748
+
680
749
  ## Learning Accumulation
681
750
 
682
751
  `robot.learn(text)` records a cross-run observation. On each subsequent `run()`, active learnings are automatically prepended to the user message as a `LEARNINGS FROM PREVIOUS RUNS:` block so the LLM can incorporate prior context without needing a persistent chat:
@@ -693,6 +762,17 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
693
762
  reviewer.run("Review snippet B") # learning is injected automatically
694
763
  ```
695
764
 
765
+ Pass `learn: true` in the constructor to enable automatic end-of-session learning promotion via the `robot_lab-durable` gem:
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
+ ```
775
+
696
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` and survive a robot rebuild when the same `Memory` object is reused.
697
777
 
698
778
  ```ruby
@@ -796,101 +876,17 @@ future.robot_name # => "analyst"
796
876
  future.delegated_by # => "manager"
797
877
  ```
798
878
 
799
- ## Ractor Parallelism
800
-
801
- RobotLab supports true CPU parallelism via Ruby Ractors — isolated execution contexts that bypass the GVL. Two modes are available:
802
-
803
- **CPU-bound tools** — mark a tool `ractor_safe true` and RobotLab automatically routes its calls through a global `RactorWorkerPool` instead of running inline:
804
-
805
- ```ruby
806
- class TranscribeAudio < RubyLLM::Tool
807
- ractor_safe true
808
- description "Transcribe an audio file"
809
- param :path, type: :string, desc: "Path to audio file"
810
-
811
- def execute(path:)
812
- AudioTranscriber.run(path) # pure computation, no shared mutable state
813
- end
814
- end
815
- ```
816
-
817
- **Parallel robot networks** — pass `parallel_mode: :ractor` when creating a network to dispatch independent robots across hardware threads simultaneously:
818
-
819
- ```ruby
820
- network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
821
- task :fetch, fetcher_robot, depends_on: :none
822
- task :sentiment, sentiment_robot, depends_on: [:fetch]
823
- task :entities, entity_robot, depends_on: [:fetch] # runs in parallel with sentiment
824
- task :summarize, summary_robot, depends_on: [:sentiment, :entities]
825
- end
826
-
827
- results = network.run(message: "Analyze customer feedback")
828
- # => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
829
- ```
830
-
831
- See the [Ractor Parallelism guide](https://madbomber.github.io/robot_lab/guides/ractor-parallelism) for constraints, the frozen-data contract, and `RactorMemoryProxy` for shared state.
832
-
833
- ## Rails Integration
834
-
835
- ```bash
836
- rails generate robot_lab:install
837
- rails db:migrate
838
- ```
839
-
840
- This creates:
841
- - `config/initializers/robot_lab.rb` - Configuration
842
- - `app/robots/` - Directory for your robots
843
- - Database tables for conversation history
844
-
845
- ### Background Jobs
846
-
847
- RobotLab ships with `RobotLab::Job`, an `ActiveJob::Base` subclass that handles the full robot-run lifecycle: robot class resolution, Turbo Stream wiring, thread-record persistence, and completion/error broadcasting.
848
-
849
- **Generic job** (robot class supplied at enqueue time):
850
-
851
- ```bash
852
- rails generate robot_lab:install # creates app/jobs/robot_run_job.rb
853
- ```
854
-
855
- ```ruby
856
- # app/jobs/robot_run_job.rb (generated)
857
- class RobotRunJob < RobotLab::Job
858
- queue_as :default
859
- end
860
-
861
- # Enqueue from a controller:
862
- RobotRunJob.perform_later(
863
- robot_class: "SupportRobot",
864
- message: params[:message],
865
- thread_id: session_id
866
- )
867
- ```
868
-
869
- **Dedicated job** (robot class bound at the class level via DSL):
870
-
871
- ```bash
872
- rails generate robot_lab:job Support # binds to SupportRobot, queue: default
873
- rails generate robot_lab:job Support --queue ai # custom queue
874
- ```
875
-
876
- ```ruby
877
- # app/jobs/support_job.rb (generated)
878
- class SupportJob < RobotLab::Job
879
- queue_as :default
880
- robot_class SupportRobot
881
- end
882
-
883
- # Enqueue (no robot_class: needed):
884
- SupportJob.perform_later(message: params[:message], thread_id: session_id)
885
- ```
886
-
887
- When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
879
+ ## Extension Gems
888
880
 
889
- - Wires `on_content` / `on_tool_call` Turbo Stream callbacks so the UI updates in real time
890
- - Broadcasts a **completion** event to `"robot_lab_thread_#{thread_id}"` when the run finishes
891
- - Broadcasts an **error** event (HTML-escaped) if the job raises
881
+ RobotLab's optional capabilities are packaged as separate gems:
892
882
 
893
- Omitting `thread_id` runs the robot in fire-and-forget mode — no persistence, no broadcasting.
883
+ | Gem | Description |
884
+ |-----|-------------|
885
+ | [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism via Ruby Ractors — `ractor_safe` tools and DAG-scheduled parallel networks |
886
+ | [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
887
+ | [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence via YAML-backed durable store |
888
+ | [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | In-memory vector store with fastembed embeddings for semantic search / RAG |
889
+ | [robot_lab-acp](https://github.com/MadBomber/robot_lab-acp) | Expose robots and networks as ACP (Agent Communication Protocol) HTTP+SSE services |
894
890
 
895
891
  ## Documentation
896
892
 
data/Rakefile CHANGED
@@ -44,6 +44,76 @@ task :rubocop_fix do
44
44
  sh "bundle exec rubocop -a"
45
45
  end
46
46
 
47
+ desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
48
+ task :flog_check do
49
+ require 'flog'
50
+
51
+ # Target to work toward; methods above this are warned but don't fail the gate.
52
+ METHOD_WARN = 20.0
53
+ # Current baseline floor — established from first run. Reduce incrementally.
54
+ METHOD_FAIL = 50.0
55
+
56
+ flogger = Flog.new(all: true)
57
+ flogger.flog(*Dir.glob('lib/**/*.rb'))
58
+
59
+ warnings = []
60
+ failures = []
61
+
62
+ flogger.each_by_score do |method, score|
63
+ next if method.end_with?('#none') # skip file-level non-method code
64
+ if score > METHOD_FAIL
65
+ failures << "#{'%.1f' % score}: #{method}"
66
+ elsif score > METHOD_WARN
67
+ warnings << "#{'%.1f' % score}: #{method}"
68
+ end
69
+ end
70
+
71
+ unless warnings.empty?
72
+ puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
73
+ warnings.each { |v| puts " #{v}" }
74
+ end
75
+
76
+ if failures.empty?
77
+ puts "\nFlog: no methods exceed the failure threshold (≥#{METHOD_FAIL})"
78
+ else
79
+ puts "\nFlog failures (≥#{METHOD_FAIL}) — must be refactored:"
80
+ failures.each { |v| puts " #{v}" }
81
+ abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
82
+ end
83
+ end
84
+
85
+ desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
86
+ task :quality do
87
+ results = {}
88
+
89
+ puts "\n#{'=' * 60}"
90
+ puts "Quality Gate: Tests + Coverage"
91
+ puts '=' * 60
92
+ results[:tests] = system("bundle exec rake test") ? :pass : :fail
93
+
94
+ puts "\n#{'=' * 60}"
95
+ puts "Quality Gate: RuboCop"
96
+ puts '=' * 60
97
+ results[:rubocop] = system("bundle exec rubocop") ? :pass : :fail
98
+
99
+ puts "\n#{'=' * 60}"
100
+ puts "Quality Gate: Flog Complexity"
101
+ puts '=' * 60
102
+ results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
103
+
104
+ puts "\n#{'=' * 60}"
105
+ puts "Quality Summary"
106
+ puts '=' * 60
107
+ results.each do |gate, status|
108
+ icon = status == :pass ? 'PASS' : 'FAIL'
109
+ puts " [#{icon}] #{gate}"
110
+ end
111
+ puts '=' * 60
112
+
113
+ abort "\nQuality gate failed" if results.values.any?(:fail)
114
+ puts "\nAll quality gates passed."
115
+ end
116
+
47
117
  namespace :examples do
48
118
  # Map of subdirectory-based demos to their entry point scripts
49
119
  SUBDIR_ENTRY_POINTS = {
@@ -58,14 +128,37 @@ namespace :examples do
58
128
  "18_rails" => { setup: "bin/setup", run: "bin/dev" }
59
129
  }.freeze
60
130
 
131
+ # Examples that require external services or user setup not guaranteed to be present
132
+ EXTERNAL_SERVICE_EXAMPLES = {
133
+ "33_stock_generator.rb" => "Redis server on localhost:6379",
134
+ "33_stock_predictor.rb" => "Redis server on localhost:6379 + running 33_stock_generator",
135
+ "34_agentskills.rb" => "AgentSkills skill file at ~/.prompts/skills/code_reviewer/SKILL.md"
136
+ }.freeze
137
+
61
138
  desc "Run all examples (excludes standalone apps like 18_rails)"
62
139
  task :all do
140
+ failed = []
141
+
63
142
  # Single-file examples
64
- Dir.glob("examples/*.rb").sort.each do |example|
143
+ Dir.glob("examples/*.rb").each do |example|
144
+ base = File.basename(example)
145
+
146
+ if EXTERNAL_SERVICE_EXAMPLES.key?(base)
147
+ puts "\n#{'=' * 60}"
148
+ puts "Skipped: #{example} (requires #{EXTERNAL_SERVICE_EXAMPLES[base]})"
149
+ puts '=' * 60
150
+ next
151
+ end
152
+
65
153
  puts "\n#{'=' * 60}"
66
154
  puts "Running: #{example}"
67
155
  puts '=' * 60
68
- ruby example
156
+ begin
157
+ ruby example
158
+ rescue RuntimeError => e
159
+ puts "FAILED: #{example} — #{e.message}"
160
+ failed << example
161
+ end
69
162
  end
70
163
 
71
164
  # Subdirectory-based demos
@@ -76,7 +169,12 @@ namespace :examples do
76
169
  puts "\n#{'=' * 60}"
77
170
  puts "Running: #{path}"
78
171
  puts '=' * 60
79
- ruby path
172
+ begin
173
+ ruby path
174
+ rescue RuntimeError => e
175
+ puts "FAILED: #{path} — #{e.message}"
176
+ failed << path
177
+ end
80
178
  end
81
179
 
82
180
  # Remind about standalone apps
@@ -87,6 +185,14 @@ namespace :examples do
87
185
  puts " Run: cd examples/#{dir} && #{commands[:run]}"
88
186
  puts '=' * 60
89
187
  end
188
+
189
+ if failed.any?
190
+ puts "\n#{'=' * 60}"
191
+ puts "#{failed.size} example(s) failed:"
192
+ failed.each { |f| puts " #{f}" }
193
+ puts '=' * 60
194
+ exit 1
195
+ end
90
196
  end
91
197
 
92
198
  desc "Run a specific example by number (e.g., rake examples:run[1])"