robot_lab 0.1.0 → 0.2.6

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 (180) 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/.envrc +1 -0
  10. data/.irbrc +2 -2
  11. data/.loki +60 -0
  12. data/.quality/reek_baseline.txt +43 -0
  13. data/.rubocop.yml +10 -0
  14. data/CHANGELOG.md +126 -0
  15. data/CLAUDE.md +139 -0
  16. data/README.md +130 -73
  17. data/Rakefile +131 -9
  18. data/agent2agent_review.md +192 -0
  19. data/agentf_improvements.md +253 -0
  20. data/agents.md +14 -0
  21. data/docs/api/core/index.md +1 -0
  22. data/docs/api/core/network.md +31 -1
  23. data/docs/api/core/robot.md +91 -5
  24. data/docs/api/errors.md +47 -2
  25. data/docs/api/index.md +3 -0
  26. data/docs/architecture/core-concepts.md +40 -0
  27. data/docs/architecture/network-orchestration.md +8 -0
  28. data/docs/architecture/robot-execution.md +1 -1
  29. data/docs/examples/index.md +37 -2
  30. data/docs/getting-started/configuration.md +39 -7
  31. data/docs/guides/building-robots.md +39 -1
  32. data/docs/guides/creating-networks.md +6 -1
  33. data/docs/guides/hooks.md +997 -0
  34. data/docs/guides/index.md +19 -14
  35. data/docs/guides/knowledge.md +9 -3
  36. data/docs/guides/observability.md +206 -7
  37. data/docs/guides/using-tools.md +69 -0
  38. data/docs/index.md +31 -4
  39. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  40. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  41. data/examples/.envrc +1 -0
  42. data/examples/01_simple_robot.rb +5 -9
  43. data/examples/02_tools.rb +5 -9
  44. data/examples/03_network.rb +18 -10
  45. data/examples/04_mcp.rb +21 -29
  46. data/examples/05_streaming.rb +12 -18
  47. data/examples/06_prompt_templates.rb +11 -19
  48. data/examples/07_network_memory.rb +16 -31
  49. data/examples/08_llm_config.rb +10 -22
  50. data/examples/09_chaining.rb +16 -27
  51. data/examples/10_memory.rb +12 -28
  52. data/examples/11_network_introspection.rb +15 -29
  53. data/examples/12_message_bus.rb +5 -12
  54. data/examples/13_spawn.rb +5 -10
  55. data/examples/14_rusty_circuit/.envrc +1 -0
  56. data/examples/14_rusty_circuit/comic.rb +2 -0
  57. data/examples/14_rusty_circuit/heckler.rb +1 -1
  58. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  59. data/examples/14_rusty_circuit/scout.rb +2 -0
  60. data/examples/15_memory_network_and_bus/.envrc +1 -0
  61. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  62. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  63. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  64. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  65. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  66. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  67. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  68. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  69. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  70. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  71. data/examples/16_writers_room/.envrc +1 -0
  72. data/examples/16_writers_room/writers_room.rb +2 -4
  73. data/examples/17_skills.rb +8 -17
  74. data/examples/18_rails/Gemfile +1 -0
  75. data/examples/19_token_tracking.rb +9 -15
  76. data/examples/20_circuit_breaker.rb +10 -19
  77. data/examples/21_learning_loop.rb +11 -20
  78. data/examples/22_context_compression.rb +6 -13
  79. data/examples/23_convergence.rb +6 -17
  80. data/examples/24_structured_delegation.rb +11 -15
  81. data/examples/25_history_search.rb +5 -12
  82. data/examples/26_document_store.rb +6 -13
  83. data/examples/27_incident_response/incident_response.rb +4 -5
  84. data/examples/28_mcp_discovery.rb +8 -11
  85. data/examples/29_ractor_tools.rb +4 -9
  86. data/examples/30_ractor_network.rb +10 -19
  87. data/examples/31_launch_assessment.rb +10 -23
  88. data/examples/32_newsletter_reader.rb +188 -0
  89. data/examples/33_stock_generator.rb +80 -0
  90. data/examples/33_stock_predictor.rb +306 -0
  91. data/examples/34_agentskills.rb +72 -0
  92. data/examples/35_hooks.rb +256 -0
  93. data/examples/README.md +8 -1
  94. data/examples/common.rb +80 -0
  95. data/examples/ruboruby.md +423 -0
  96. data/examples/xyzzy.rb +90 -0
  97. data/lib/robot_lab/agent_skill.rb +64 -0
  98. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  99. data/lib/robot_lab/ask_user.rb +2 -2
  100. data/lib/robot_lab/budget/ledger.rb +98 -0
  101. data/lib/robot_lab/bus_poller.rb +12 -5
  102. data/lib/robot_lab/capabilities.rb +84 -0
  103. data/lib/robot_lab/config/defaults.yml +10 -0
  104. data/lib/robot_lab/config.rb +5 -16
  105. data/lib/robot_lab/delegation_future.rb +1 -1
  106. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  107. data/lib/robot_lab/error.rb +34 -4
  108. data/lib/robot_lab/errors.rb +45 -0
  109. data/lib/robot_lab/history_compressor.rb +4 -10
  110. data/lib/robot_lab/hook.rb +79 -0
  111. data/lib/robot_lab/hook_context.rb +194 -0
  112. data/lib/robot_lab/hook_registry.rb +55 -0
  113. data/lib/robot_lab/hooks.rb +87 -0
  114. data/lib/robot_lab/mcp/client.rb +1 -2
  115. data/lib/robot_lab/mcp/connection_poller.rb +5 -5
  116. data/lib/robot_lab/mcp/server.rb +1 -1
  117. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  118. data/lib/robot_lab/memory.rb +32 -27
  119. data/lib/robot_lab/memory_change.rb +2 -2
  120. data/lib/robot_lab/message.rb +4 -4
  121. data/lib/robot_lab/narrator.rb +87 -0
  122. data/lib/robot_lab/network.rb +87 -25
  123. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  124. data/lib/robot_lab/robot/budget.rb +89 -0
  125. data/lib/robot_lab/robot/bus_messaging.rb +79 -33
  126. data/lib/robot_lab/robot/history_search.rb +4 -1
  127. data/lib/robot_lab/robot/hooking.rb +56 -0
  128. data/lib/robot_lab/robot/mcp_management.rb +12 -12
  129. data/lib/robot_lab/robot/template_rendering.rb +62 -42
  130. data/lib/robot_lab/robot.rb +482 -242
  131. data/lib/robot_lab/robot_result.rb +6 -5
  132. data/lib/robot_lab/run_config.rb +27 -14
  133. data/lib/robot_lab/runnable.rb +51 -0
  134. data/lib/robot_lab/sandbox/null.rb +13 -0
  135. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  136. data/lib/robot_lab/sandbox.rb +52 -0
  137. data/lib/robot_lab/script_tool.rb +134 -0
  138. data/lib/robot_lab/state_proxy.rb +7 -5
  139. data/lib/robot_lab/task.rb +26 -20
  140. data/lib/robot_lab/tool.rb +54 -13
  141. data/lib/robot_lab/tool_config.rb +1 -1
  142. data/lib/robot_lab/tool_manifest.rb +5 -7
  143. data/lib/robot_lab/user_message.rb +2 -2
  144. data/lib/robot_lab/version.rb +1 -1
  145. data/lib/robot_lab/waiter.rb +1 -1
  146. data/lib/robot_lab.rb +89 -51
  147. data/logfile +8 -0
  148. data/mkdocs.yml +3 -3
  149. data/robot_concurrency.md +38 -0
  150. data/simple_acp_review.md +298 -0
  151. data/temp.md +6 -0
  152. data/tool_manifest_plan.md +155 -0
  153. metadata +82 -83
  154. data/docs/examples/rails-application.md +0 -419
  155. data/docs/guides/ractor-parallelism.md +0 -364
  156. data/docs/guides/rails-integration.md +0 -681
  157. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  158. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  159. data/lib/generators/robot_lab/install_generator.rb +0 -90
  160. data/lib/generators/robot_lab/job_generator.rb +0 -40
  161. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  162. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  163. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  164. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  165. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  166. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  167. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  168. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  169. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  170. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  171. data/lib/robot_lab/document_store.rb +0 -155
  172. data/lib/robot_lab/ractor_boundary.rb +0 -42
  173. data/lib/robot_lab/ractor_job.rb +0 -37
  174. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  175. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  176. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  177. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  178. data/lib/robot_lab/rails_integration/job.rb +0 -158
  179. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  180. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
data/CHANGELOG.md CHANGED
@@ -8,6 +8,132 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Added
12
+ - `BusMessaging#respond_to_tasks` and `#serve` — a member auto-answers inbound
13
+ (non-reply) bus tasks and replies to the sender. `serve` runs each task through
14
+ `#run`; `respond_to_tasks { |m| ... }` is the generic form. Makes a Robot a
15
+ first-class bus responder (symmetric with how a Cyborg answers its human)
16
+ without hand-wiring `on_message`.
17
+ - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, `integration`, `docs`, and `examples` tasks via the Asgard task runner
18
+
19
+ ### Fixed
20
+ - `BusMessaging` now synchronizes its message counter and outbox with a mutex, so
21
+ concurrent sends and poller-thread reply correlation can't clobber each other.
22
+ - `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
23
+ - `flay` gem added to development dependencies
24
+ - `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
25
+
26
+ ### Changed
27
+ - `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
28
+ - `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
29
+
30
+ ## [0.2.6] - 2026-05-23
31
+
32
+ ### Added
33
+
34
+ - **`:compaction` hook family** — fires when `maybe_compact` determines that conversation history should be compressed. Provides `before_compaction`, `around_compaction`, `after_compaction`, and `on_compaction` callbacks.
35
+ - `on_compaction` allows an extension to supply a complete replacement message set via `ctx.compacted_messages`; when set, the core skips its own compaction strategy entirely (`ctx.handled?` returns true).
36
+ - `CompactionHookContext` — carries `robot`, `messages_before` (frozen snapshot), `config`, `strategy` (`:context_window`, `:custom`, or other symbol), `compacted_messages`, and `handled?`.
37
+ - **`:learn` hook family** — fires on every `robot.learn(text)` call with non-empty text. Provides `before_learn`, `around_learn`, `after_learn`, and `on_learn` callbacks.
38
+ - `on_learn` fires after the text has been stored and `ctx.stored = true`, giving extensions a reliable hook point for cross-session persistence.
39
+ - `LearnHookContext` — carries `robot`, `text`, `learnings_before` (frozen snapshot), and `stored`.
40
+ - **`over_compact_threshold?`** private predicate extracted from `compact_if_over_context_window` for independent testability.
41
+ - Hook tests for both new families added to `test/robot_lab/hooks_test.rb` (18 new tests across `RobotLabCompactionHooksTest` and `RobotLabLearnHooksTest`).
42
+ - Documentation for `:compaction` and `:learn` hook families added to `docs/guides/hooks.md`.
43
+
44
+ ### Changed
45
+
46
+ - **`Robot#on`** now accepts and forwards `context:` to `HookRegistry#on`, allowing extensions to pass per-registration domain config at registration time (e.g. `robot.on(MyHook, context: { domain: "finance" })`). Previously `context:` was silently dropped.
47
+ - **`maybe_compact`** refactored to dispatch through the `:compaction` hook family and delegate to `on_compaction` before falling back to the built-in strategy.
48
+ - **`Robot#learn`** refactored to dispatch through the `:learn` hook family; `on_learn` fires after deduplication with `ctx.stored` reflecting whether the text was actually added.
49
+
50
+ ### Added
51
+
52
+ - **`RobotLab::Hook` base class** (`lib/robot_lab/hook.rb`) — all hook handlers inherit from this class. Subclasses define lifecycle callbacks as `class << self` methods (`before_run`, `after_run`, etc.). Namespace is auto-derived from the class name via snake_case conversion of the final class name segment (e.g. `AuditHook` → `:audit_hook`); override with `self.namespace = :custom`. Ractor-safe by design: handler classes are Ruby constants, not Procs, so registrations are natively serializable across Ractor boundaries.
53
+ - **Hook system** — lifecycle hooks across all execution boundaries. Register handler classes with `RobotLab.on`, `network.on`, or `robot.on` for five families: `:run`, `:llm_generation`, `:tool_call`, `:network_run`, and `:task`, each with `before_*`, `around_*`, `after_*`, and (where applicable) `on_error` variants.
54
+ - `HookRegistry::Registration` — `Data.define(:handler_class, :context)`; stores a handler class reference and optional per-registration default `DotState` values. No Proc, no namespace field — namespace is read from the handler class.
55
+ - `HookContext` and five typed subclasses: `RunHookContext`, `LlmGenerationHookContext`, `ToolCallHookContext`, `NetworkRunHookContext`, `TaskHookContext` — unchanged
56
+ - `DotState` / `ExtensionState` — namespace-isolated dot-access state carried on every context object; `ctx.local` reads/writes state for the active handler's namespace
57
+ - `context:` parameter on `on(handler_class, context: {...})` — sets per-registration default `DotState` values merged in before each callback fires
58
+ - Around hooks chain correctly across handler class registrations; return value is propagated up the chain
59
+ - Per-run hooks via `robot.run("msg", hooks: MyHook)` or `robot.run("msg", hooks: [MyHook, OtherHook])`
60
+ - `on_error` fires for `:run`, `:network_run`, and `:task` families; exception is re-raised after all error handlers complete
61
+ - **`examples/xyzzy.rb`** — updated reference extension: `RobotLab::Xyzzy < RobotLab::Hook` with `class << self` methods for every hook family; logs each callback with a timestamped stdout tagline and a structured logger context snapshot; registered globally with `RobotLab.on(RobotLab::Xyzzy)`
62
+ - **Example 35** (`examples/35_hooks.rb`) — updated full hook pipeline demo: xyzzy extension tracing all hooks, `around_run` perf timer handler class, `around_llm_generation` response cache handler class (cache hits skip the LLM entirely), `before/after_llm_generation` tracer, tool call hooks, network/task hooks, and `on_error`
63
+ - **`examples/common.rb`** — added explicit `openai_api_key`, `openai_organization_id`, and `openai_project_id` to `RubyLLM.configure` to match provider configurator expectations; updated default model to `gpt-4.1-mini`
64
+
65
+ ## [0.2.1] - 2026-05-19
66
+
67
+ ### Added
68
+
69
+ - **`examples/common.rb`** — shared setup file required by all numbered examples. Defines:
70
+ - `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`
71
+ - `RubyLLM.configure` with null logger and `LLM[:default].model` as `default_model`
72
+ - `RobotLab.configure` with null logger
73
+ - Output helpers: `banner(title)`, `section(title)`, `hr`, `show_code(ruby_string, label:)` using `rouge` for syntax highlighting
74
+ - **`rouge` gem** added to development group for syntax-highlighted example output
75
+ - **`.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/)
76
+
77
+ ### Changed
78
+
79
+ - **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"`
80
+ - **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
81
+
82
+ ## [0.2.1] - 2026-05-11 (unreleased)
83
+
84
+ ### Added
85
+
86
+ - **`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
87
+ - `flog` gem added to development/test group
88
+ - Branch coverage enabled unconditionally (previously CI-only) with minimum thresholds: line 95%, branch 75%
89
+
90
+ ### Changed
91
+
92
+ - Bumped version to 0.2.1
93
+ - **`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`
94
+ - **`Robot#run` decomposed** into: `resolve_run_memory`, `prepare_tools`, `invoke_ask`, `enforce_token_budget!`
95
+ - **`BusPoller#process_and_drain`** split into `drain_queued_deliveries` and `release_robot` for independent testability
96
+ - **`TemplateRendering#apply_skills_and_template_to_chat`** split into `collect_prompt_content` (pure computation) and `apply_prompt_to_chat` (pure mutation)
97
+ - Removed `.serena/` project configuration files from version control
98
+ - Removed `.claude/memory.sqlite3` database files from version control
99
+
100
+ ## [0.2.0] - 2026-05-07
101
+
102
+ ### Added
103
+
104
+ - **`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.
105
+ - **`Durable::Store`** — YAML-backed knowledge store with file locking, keyword recall, and confidence tracking.
106
+ - **`Durable::Entry`** — immutable value object for knowledge records with confidence progression.
107
+ - **`Durable::Reflector`** — promotes session learnings to durable storage at end of each run.
108
+ - **`RecallKnowledge` tool** — robots query past knowledge before uncertain decisions.
109
+ - **`RecordKnowledge` tool** — robots persist new knowledge learned during a session.
110
+ - **`AgentSkill`** — value object for discoverable skills defined by `SKILL.md` files with YAML front matter (name, description, version, dependencies, parameters)
111
+ - **`AgentSkillCatalog`** — service for locating and indexing `AgentSkills/` directories at runtime
112
+ - **`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
113
+ - **`ScriptTool`** — factory that wraps shell scripts as robot tools; auto-generates JSON schema from script `--help` output
114
+ - **`DoomLoopDetector`** — detects consecutive and cyclic tool-call repetition; wired into `Robot#run` via singleton `execute_tool` override
115
+ - **`doom_loop_threshold`** field on `RunConfig` — configures the repetition threshold before a `ToolLoopError` is raised
116
+ - **`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
117
+
118
+ ### Changed
119
+
120
+ - Bumped version to 0.2.0
121
+ - **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
122
+ - **`Durable::Learning` inclusion** is now conditional on the `robot_lab-durable` gem being loaded
123
+ - **Tool Ractor routing** guarded on `RobotLab.respond_to?(:ractor_pool)` so the core gem runs without the ractor extension
124
+ - **Memory drainer scheduling** — `@drainer_scheduled` remains `true` when rescheduling to prevent concurrent writers from spawning competing drain fibers
125
+ - `@chat` state reset now uses `reset_messages!` / `add_message` public API instead of internal instance variable manipulation
126
+ - Rails generators moved to `robot_lab-rails` extension gem
127
+
128
+ ### Fixed
129
+
130
+ - Memory drainer double-schedule race when concurrent writers triggered overlapping drain cycles
131
+ - `DocumentStore` instantiation guarded with a `LoadError` message when the extension gem is absent
132
+ - `AgentSkill` YAML parsing hardened against empty strings and non-Hash front matter
133
+ - Missing requires added to examples after gem extraction
134
+ - `doom_loop_threshold` and `auto_compact` documented in README and guides
135
+ - `learn:` parameter and `robot_lab-acp` documented in README
136
+
11
137
  ## [0.1.0] - 2026-04-29
12
138
 
13
139
  ### 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
@@ -25,8 +25,9 @@
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
- - <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting<br>
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 &amp; 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>
32
33
  - <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
@@ -93,7 +94,7 @@ robot = RobotLab.build(
93
94
 
94
95
  ### Configuration
95
96
 
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:
97
+ RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
97
98
 
98
99
  1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
99
100
  2. Environment-specific overrides (development, test, production)
@@ -124,6 +125,14 @@ ruby_llm:
124
125
  request_timeout: 180
125
126
  ```
126
127
 
128
+ Runtime-only attributes (such as the logger) can be set with a `configure` block:
129
+
130
+ ```ruby
131
+ RobotLab.configure do |c|
132
+ c.logger = Logger.new(File::NULL) # silence logging
133
+ end
134
+ ```
135
+
127
136
  ### Using Templates
128
137
 
129
138
  For production applications, RobotLab supports a template system built on [PromptManager](https://github.com/MadBomber/prompt_manager). Templates allow you to:
@@ -677,6 +686,67 @@ robot.clear_messages # flushes broken history; system prompt is kept
677
686
  result = robot.run("Something new.") # robot is healthy again
678
687
  ```
679
688
 
689
+ ## Doom Loop Detection
690
+
691
+ 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:
692
+
693
+ ```ruby
694
+ robot = RobotLab.build(
695
+ name: "runner",
696
+ system_prompt: "Execute steps.",
697
+ local_tools: [StepTool],
698
+ doom_loop_threshold: 3 # alert after 3 identical consecutive or cyclic sequences
699
+ )
700
+ ```
701
+
702
+ 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`:
703
+
704
+ ```ruby
705
+ config = RobotLab::RunConfig.new(doom_loop_threshold: 3)
706
+ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
707
+ ```
708
+
709
+ ## Automatic Context Compaction
710
+
711
+ `auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
712
+
713
+ ```ruby
714
+ # Built-in trigger: compact when estimated token usage exceeds 80% of context window
715
+ robot = RobotLab.build(
716
+ name: "analyst",
717
+ system_prompt: "You are a research analyst.",
718
+ auto_compact: :context_window
719
+ )
720
+
721
+ # Tune the threshold (here: compact at 70%)
722
+ robot = RobotLab.build(
723
+ name: "analyst",
724
+ system_prompt: "You are a research analyst.",
725
+ auto_compact: :context_window,
726
+ compact_threshold: 0.70
727
+ )
728
+
729
+ # Application-owned compaction: full control over when and how
730
+ robot = RobotLab.build(
731
+ name: "analyst",
732
+ system_prompt: "You are a research analyst.",
733
+ auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
734
+ )
735
+ ```
736
+
737
+ | Value | Behaviour |
738
+ |-------|-----------|
739
+ | `nil` / `:none` | No automatic compaction (default) |
740
+ | `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
741
+ | `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
742
+
743
+ `compact_threshold` defaults to `0.80` (80%). Requires the `classifier` gem when using the built-in `:context_window` strategy. Via `RunConfig`:
744
+
745
+ ```ruby
746
+ config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
747
+ robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
748
+ ```
749
+
680
750
  ## Learning Accumulation
681
751
 
682
752
  `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 +763,17 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
693
763
  reviewer.run("Review snippet B") # learning is injected automatically
694
764
  ```
695
765
 
766
+ Pass `learn: true` in the constructor to enable automatic end-of-session learning promotion via the `robot_lab-durable` gem:
767
+
768
+ ```ruby
769
+ reviewer = RobotLab.build(
770
+ name: "reviewer",
771
+ system_prompt: "You are a Ruby code reviewer.",
772
+ learn: true,
773
+ learn_domain: "ruby_review"
774
+ )
775
+ ```
776
+
696
777
  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
778
 
698
779
  ```ruby
@@ -796,101 +877,77 @@ future.robot_name # => "analyst"
796
877
  future.delegated_by # => "manager"
797
878
  ```
798
879
 
799
- ## Ractor Parallelism
800
-
801
- RobotLab supports true CPU parallelism via Ruby Ractors — isolated execution contexts that bypass the GVL. Two modes are available:
880
+ ## Hook System
802
881
 
803
- **CPU-bound tools** mark a tool `ractor_safe true` and RobotLab automatically routes its calls through a global `RactorWorkerPool` instead of running inline:
882
+ 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.
804
883
 
805
884
  ```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"
885
+ class TimerHook < RobotLab::Hook
886
+ # namespace is auto-derived: TimerHook → :timer_hook
887
+ # override with: self.namespace = :timer
810
888
 
811
- def execute(path:)
812
- AudioTranscriber.run(path) # pure computation, no shared mutable state
889
+ def self.before_run(ctx)
890
+ ctx.local.start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
813
891
  end
814
- end
815
- ```
816
892
 
817
- **Parallel robot networks** — pass `parallel_mode: :ractor` when creating a network to dispatch independent robots across hardware threads simultaneously:
893
+ def self.after_run(ctx)
894
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.local.start) * 1000).round(1)
895
+ puts "[timer] #{ctx.robot.name} #{elapsed}ms"
896
+ end
818
897
 
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]
898
+ def self.on_error(ctx)
899
+ puts "[timer] #{ctx.robot.name} failed: #{ctx.error.message}"
900
+ end
825
901
  end
826
-
827
- results = network.run(message: "Analyze customer feedback")
828
- # => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
829
902
  ```
830
903
 
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
904
+ Register at the global, network, or robot level or for a single call:
834
905
 
835
- ```bash
836
- rails generate robot_lab:install
837
- rails db:migrate
906
+ ```ruby
907
+ RobotLab.on(TimerHook) # every robot in this process
908
+ network.on(TimerHook) # robots inside this network only
909
+ robot.on(TimerHook) # this robot only
910
+ robot.run("msg", hooks: TimerHook) # this call only
911
+ robot.run("msg", hooks: [TimerHook, OtherHook])
838
912
  ```
839
913
 
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
- ```
914
+ Pass `context:` to set default `DotState` values for the handler's namespace before each callback fires:
854
915
 
855
916
  ```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
- )
917
+ RobotLab.on(TimerHook, context: { threshold_ms: 500 })
867
918
  ```
868
919
 
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
- ```
920
+ Around hooks receive a block and must call it — and return its value — so the actual work executes:
875
921
 
876
922
  ```ruby
877
- # app/jobs/support_job.rb (generated)
878
- class SupportJob < RobotLab::Job
879
- queue_as :default
880
- robot_class SupportRobot
881
- end
923
+ class PerfHook < RobotLab::Hook
924
+ self.namespace = :perf
882
925
 
883
- # Enqueue (no robot_class: needed):
884
- SupportJob.perform_later(message: params[:message], thread_id: session_id)
926
+ def self.around_run(ctx, &block)
927
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
928
+ result = block.call # executes the run
929
+ ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(1)
930
+ $stderr.puts "[perf] #{ctx.robot.name} #{ms}ms"
931
+ result # must return the result
932
+ end
933
+ end
885
934
  ```
886
935
 
887
- When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
936
+ Because handler classes are Ruby constants (not Procs), all hook registrations are natively Ractor-serializable.
937
+
938
+ See the [full Hook System guide](docs/guides/hooks.md) for all hook families, context objects, and extension patterns.
939
+
940
+ ## Extension Gems
888
941
 
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
942
+ RobotLab's optional capabilities are packaged as separate gems:
892
943
 
893
- Omitting `thread_id` runs the robot in fire-and-forget mode — no persistence, no broadcasting.
944
+ | Gem | Description |
945
+ |-----|-------------|
946
+ | [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism via Ruby Ractors — `ractor_safe` tools and DAG-scheduled parallel networks |
947
+ | [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
948
+ | [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence via YAML-backed durable store |
949
+ | [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | In-memory vector store with fastembed embeddings for semantic search / RAG |
950
+ | [robot_lab-acp](https://github.com/MadBomber/robot_lab-acp) | Expose robots and networks as ACP (Agent Communication Protocol) HTTP+SSE services |
894
951
 
895
952
  ## Documentation
896
953