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
@@ -1,258 +0,0 @@
1
- # Ractor Integration Design
2
-
3
- **Date:** 2026-04-14
4
- **Status:** Approved
5
- **Gems:** `ractor_queue`, `ractor-wrapper`
6
-
7
- ## Goals
8
-
9
- 1. True CPU parallelism (GIL-bypassing) for CPU-bound tool execution
10
- 2. True CPU parallelism for parallel robot execution in Networks
11
- 3. Use `ractor_queue` as the queue backbone for both tracks
12
- 4. Use `ractor-wrapper` to expose shared `Memory` to Ractor workers
13
- 5. Deliver both tracks as independent, composable layers
14
-
15
- ## Non-Goals
16
-
17
- - Making `ruby_llm` or the `async` gem Ractor-safe
18
- - Replacing the existing `:async` concurrency model (it remains the default)
19
- - Ractor-isolating `Robot` instances that are long-lived across multiple tasks
20
-
21
- ---
22
-
23
- ## Architecture Overview
24
-
25
- Two parallel tracks share a frozen-message convention and `ractor_queue` as the communication backbone.
26
-
27
- ```
28
- ┌─────────────────────────────────────────────────────────────────┐
29
- │ Thread/Fiber World │
30
- │ Robot (ruby_llm, async) ──▶ Tool.call() ──▶ RobotResult │
31
- │ │ │ │
32
- │ BusPoller ractor_safe? │
33
- │ (ractor_queue) │ │ │
34
- └────────────────────────────────│────────│────────────────────────┘
35
- │ yes │ no
36
- ┌───────────────────┘ └──► Thread executor
37
-
38
- ┌─────────────────────────────────────────────────────────────────┐
39
- │ Ractor World │
40
- │ RactorWorkerPool ◀──ractor_queue── frozen RactorJob │
41
- │ (N Ractor workers) │
42
- │ │ │
43
- │ RactorMemoryProxy (ractor-wrapper around Memory) │
44
- │ ◀── get/set via Ractor messages ──▶ │
45
- └─────────────────────────────────────────────────────────────────┘
46
- ```
47
-
48
- **Key constraint:** only frozen, `Ractor.shareable?` objects cross Ractor boundaries. A `RactorJob` is a `Data.define` struct (shareable by design) carrying a frozen payload and a per-job reply `ractor_queue`.
49
-
50
- ---
51
-
52
- ## Shared Infrastructure
53
-
54
- ### `RactorJob`
55
-
56
- ```ruby
57
- RactorJob = Data.define(:id, :type, :payload, :reply_queue)
58
- ```
59
-
60
- Single cross-boundary carrier for both tracks. `payload` must be frozen by the caller before submission. `reply_queue` is a `ractor_queue` instance (Ractor-safe).
61
-
62
- ### `RactorJobError`
63
-
64
- ```ruby
65
- RactorJobError = Data.define(:message, :backtrace)
66
- ```
67
-
68
- Frozen error representation for exceptions that occur inside a Ractor worker. Serialized at the Ractor boundary, re-raised on the thread side.
69
-
70
- ### `RobotSpec`
71
-
72
- ```ruby
73
- RobotSpec = Data.define(:name, :template, :system_prompt, :config_hash)
74
- ```
75
-
76
- Carries everything needed to reconstruct a `Robot` inside a Ractor. All fields must be frozen strings/hashes.
77
-
78
- ### `RactorBoundary`
79
-
80
- A utility module with a `freeze_deep(obj)` method that recursively freezes nested `Hash`/`Array` structures before they cross a Ractor boundary. Similar in spirit to the existing `deep_dup` in `Utils`. Raises `RobotLab::RactorBoundaryError` (a subclass of `RobotLab::Error`) if a value cannot be made shareable (e.g., a live IO or Proc).
81
-
82
- ```ruby
83
- module RactorBoundary
84
- def self.freeze_deep(obj)
85
- case obj
86
- when Hash then obj.transform_values { freeze_deep(_1) }.freeze
87
- when Array then obj.map { freeze_deep(_1) }.freeze
88
- else obj.frozen? ? obj : obj.dup.freeze
89
- end
90
- rescue TypeError => e
91
- raise RobotLab::RactorBoundaryError, "Cannot make value Ractor-shareable: #{e.message}"
92
- end
93
- end
94
- ```
95
-
96
- ---
97
-
98
- ## Track 1: RactorWorkerPool (Tool CPU Parallelism)
99
-
100
- ### Tool opt-in
101
-
102
- `RobotLab::Tool` gets a `ractor_safe` class macro (default `false`). Ractor-safe tools must be stateless — no captured mutable closures, no non-shareable constants.
103
-
104
- ```ruby
105
- class EmbeddingTool < RobotLab::Tool
106
- ractor_safe true
107
-
108
- def execute(text:)
109
- # CPU-bound embedding work — runs inside a Ractor worker
110
- end
111
- end
112
- ```
113
-
114
- The framework raises `RobotLab::ConfigurationError` at class-definition time if a declared-safe tool captures unshareable state (detected via `Ractor.shareable?` check on the class object).
115
-
116
- ### `RactorWorkerPool`
117
-
118
- A pool of N Ractor workers (configurable via `RunConfig#ractor_pool_size`, default `Etc.nprocessors`). Each worker runs:
119
-
120
- ```ruby
121
- loop do
122
- job = work_queue.pop # blocks on ractor_queue
123
- result = dispatch(job) # instantiates tool class, calls execute
124
- job.reply_queue.push(result) # frozen result back to caller
125
- rescue => e
126
- job.reply_queue.push(RactorJobError.new(message: e.message, backtrace: e.backtrace))
127
- end
128
- ```
129
-
130
- The pool is lazily initialized on first use and shared across robots in a Network via the existing `RunConfig` hierarchy. It lives for the lifetime of the process (or the `RunConfig` that owns it). `RactorWorkerPool#shutdown` drains in-flight jobs, then closes the work `ractor_queue` so all workers exit their loops cleanly. `RunConfig` calls `shutdown` on `ObjectSpace` finalizer or explicit `RobotLab.shutdown` call.
131
-
132
- If a worker Ractor crashes (unhandled exception kills the Ractor), the pool detects the dead Ractor via `Ractor#take` and spawns a replacement. The failed job's reply queue receives a `RactorJobError`.
133
-
134
- ### Submission path (inside `Robot#call_tool`)
135
-
136
- 1. Look up `tool_class` from `ToolManifest`
137
- 2. Check `tool_class.ractor_safe?`
138
- 3. **If yes:** `RactorBoundary.freeze_deep(args)`, build `RactorJob`, push to pool's work `ractor_queue`, block on reply queue
139
- 4. **If no:** run in current thread/fiber as today
140
- 5. On reply: if result is `RactorJobError`, re-raise as `RobotLab::ToolError` in the calling thread
141
-
142
- ### `RunConfig` additions
143
-
144
- ```ruby
145
- ractor_pool_size: :auto # :auto = Etc.nprocessors, or an Integer
146
- ```
147
-
148
- ---
149
-
150
- ## Track 2: RactorMemoryProxy + RactorNetworkScheduler (Robot Parallelism)
151
-
152
- ### `RactorMemoryProxy`
153
-
154
- Wraps the existing `Memory` instance via `ractor-wrapper`. The wrapper Ractor acts as a method-dispatch server: it receives frozen messages and replies with frozen results.
155
-
156
- Supported operations proxied across the Ractor boundary:
157
-
158
- | Message | Reply |
159
- |---------|-------|
160
- | `[:get, key]` | frozen value or `nil` |
161
- | `[:set, key, frozen_value]` | `:ok` |
162
- | `[:keys]` | frozen array of keys |
163
-
164
- Subscriptions (callbacks) are **not** proxied — closures are not Ractor-safe. Robots that need reactive subscriptions use the thread-side `Memory` directly. `RactorMemoryProxy` is for Ractor workers that need read/write access to shared state.
165
-
166
- No changes to `Memory` itself.
167
-
168
- ### `RactorNetworkScheduler`
169
-
170
- Replaces `SimpleFlow::Pipeline#call_parallel` for Networks with `parallel_mode: :ractor`. Distributes frozen task descriptions to worker Ractors, collects frozen results.
171
-
172
- `depends_on` ordering is preserved: the scheduler reads the pipeline's existing dependency graph (from `SimpleFlow::Pipeline`) and uses it to determine which tasks are ready to dispatch. A task is submitted to the `ractor_queue` only once all its dependencies have resolved. This mirrors how `call_parallel` works today — the scheduler wraps the same topological resolution logic.
173
-
174
- ```
175
- Scheduler ──► ractor_queue (frozen RobotSpec + task payload)
176
-
177
-
178
- Worker Ractor
179
- (constructs fresh Robot from RobotSpec,
180
- runs task, freezes RobotResult,
181
- pushes to reply ractor_queue)
182
-
183
- Scheduler ◀── ractor_queue (frozen results)
184
- ```
185
-
186
- Each worker Ractor constructs its own `Robot` instance from a `RobotSpec`. The LLM call happens inside the Ractor. This is safe because `ruby_llm` HTTP calls use no shared mutable state between instances — the Ractor constraint is about *shared* non-shareable objects, not fresh instances created inside a Ractor.
187
-
188
- Results are collected via a reply `ractor_queue` and assembled into the pipeline's `SimpleFlow::Result` context on the thread side.
189
-
190
- ### `BusPoller` queue upgrade
191
-
192
- `BusPoller#@robot_queues` changes from `Hash<String, Array>` to `Hash<String, ractor_queue>`. Delivery mechanics (mutex-guarded drain, `process_and_drain`) are unchanged — only the backing store is swapped. This makes `BusPoller` capable of receiving deliveries from Ractor workers.
193
-
194
- ### Network opt-in
195
-
196
- ```ruby
197
- network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
198
- task :sentiment, sentiment_robot, depends_on: :none
199
- task :entities, entity_robot, depends_on: :none
200
- task :summarize, summary_robot, depends_on: [:sentiment, :entities]
201
- end
202
- ```
203
-
204
- `parallel_mode: :async` remains the default and is unchanged.
205
-
206
- ---
207
-
208
- ## Error Handling
209
-
210
- | Scenario | Mechanism |
211
- |----------|-----------|
212
- | Tool raises inside Ractor worker | Serialized as `RactorJobError`, re-raised as `RobotLab::ToolError` in calling thread |
213
- | Robot raises inside `RactorNetworkScheduler` | Serialized as `RactorJobError`, surfaced as failed step in `SimpleFlow::Result` |
214
- | Worker Ractor crashes (unhandled exception) | Pool detects dead Ractor, spawns replacement, failed job gets `RactorJobError` on reply queue |
215
- | Non-shareable value submitted to pool | `RobotLab::RactorBoundaryError` raised before the Ractor boundary |
216
-
217
- ---
218
-
219
- ## Testing
220
-
221
- - `RactorWorkerPool` is testable standalone — no Robot or Network required
222
- - `RactorMemoryProxy` is testable standalone — wrap a `Memory`, call proxy methods from a test Ractor
223
- - Tools that declare `ractor_safe true` should pass `assert_ractor_safe(tool_class)` — a test helper that spins up a single-worker pool and round-trips a frozen payload
224
- - `RactorNetworkScheduler` tests use a minimal two-robot network with `parallel_mode: :ractor`
225
- - All existing tests are unaffected — `:async` remains the default; no existing class is modified in a breaking way
226
-
227
- ---
228
-
229
- ## New Files
230
-
231
- | File | Purpose |
232
- |------|---------|
233
- | `lib/robot_lab/ractor_job.rb` | `RactorJob`, `RactorJobError`, `RobotSpec` data classes |
234
- | `lib/robot_lab/ractor_boundary.rb` | `RactorBoundary.freeze_deep` utility |
235
- | `lib/robot_lab/ractor_worker_pool.rb` | `RactorWorkerPool` — N Ractor workers fed by `ractor_queue` |
236
- | `lib/robot_lab/ractor_memory_proxy.rb` | `RactorMemoryProxy` — `ractor-wrapper` around `Memory` |
237
- | `lib/robot_lab/ractor_network_scheduler.rb` | `RactorNetworkScheduler` — distributes robot tasks to Ractor workers |
238
-
239
- ## Modified Files
240
-
241
- | File | Change |
242
- |------|--------|
243
- | `lib/robot_lab/tool.rb` | Add `ractor_safe` class macro |
244
- | `lib/robot_lab/robot.rb` | Check `ractor_safe?` in `call_tool`, submit to pool if true |
245
- | `lib/robot_lab/run_config.rb` | Add `ractor_pool_size:` field |
246
- | `lib/robot_lab/bus_poller.rb` | Swap `Array` queues for `ractor_queue` instances |
247
- | `lib/robot_lab/network.rb` | Add `parallel_mode:` option, delegate to `RactorNetworkScheduler` |
248
- | `lib/robot_lab/error.rb` | Add `RobotLab::RactorBoundaryError` subclass |
249
- | `lib/robot_lab.rb` | Require new files |
250
-
251
- ---
252
-
253
- ## Dependencies to Add
254
-
255
- ```ruby
256
- gem "ractor_queue"
257
- gem "ractor-wrapper"
258
- ```
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module RobotLab
7
- module Generators
8
- # Installs RobotLab into a Rails application
9
- #
10
- # Usage:
11
- # rails generate robot_lab:install
12
- #
13
- class InstallGenerator < ::Rails::Generators::Base
14
- include ::Rails::Generators::Migration
15
-
16
- source_root File.expand_path("templates", __dir__)
17
-
18
- class_option :skip_migration, type: :boolean, default: false,
19
- desc: "Skip database migration generation"
20
- class_option :skip_job, type: :boolean, default: false,
21
- desc: "Skip background job generation"
22
-
23
- # Returns the next migration number for ActiveRecord migrations.
24
- #
25
- # @param dirname [String] the migrations directory
26
- # @return [String] the next migration number
27
- def self.next_migration_number(dirname)
28
- ::ActiveRecord::Generators::Base.next_migration_number(dirname)
29
- end
30
-
31
- # Creates the RobotLab initializer file.
32
- #
33
- # @return [void]
34
- def create_initializer
35
- template "initializer.rb.tt", "config/initializers/robot_lab.rb"
36
- end
37
-
38
- # Creates the database migration for RobotLab tables.
39
- #
40
- # @return [void]
41
- def create_migration
42
- return if options[:skip_migration]
43
-
44
- migration_template "migration.rb.tt", "db/migrate/create_robot_lab_tables.rb"
45
- end
46
-
47
- # Creates the ActiveRecord model files.
48
- #
49
- # @return [void]
50
- def create_models
51
- return if options[:skip_migration]
52
-
53
- template "thread_model.rb.tt", "app/models/robot_lab_thread.rb"
54
- template "result_model.rb.tt", "app/models/robot_lab_result.rb"
55
- end
56
-
57
- # Creates the background job for robot execution.
58
- #
59
- # @return [void]
60
- def create_job
61
- return if options[:skip_job]
62
-
63
- template "job.rb.tt", "app/jobs/robot_run_job.rb"
64
- end
65
-
66
- # Creates the robots and tools directories.
67
- #
68
- # @return [void]
69
- def create_directories
70
- empty_directory "app/robots"
71
- empty_directory "app/tools"
72
- end
73
-
74
- # Displays post-installation instructions.
75
- #
76
- # @return [void]
77
- def display_post_install
78
- say ""
79
- say "RobotLab installed successfully!", :green
80
- say ""
81
- say "Next steps:"
82
- say " 1. Run migrations: rails db:migrate"
83
- say " 2. Configure your LLM API keys in config/initializers/robot_lab.rb"
84
- say " 3. Generate your first robot: rails g robot_lab:robot MyRobot"
85
- say " 4. Enqueue robot runs via RobotRunJob (app/jobs/robot_run_job.rb)" unless options[:skip_job]
86
- say ""
87
- end
88
- end
89
- end
90
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
-
5
- module RobotLab
6
- module Generators
7
- # Generates a RobotLab job subclass pre-wired to a specific robot class.
8
- #
9
- # Usage:
10
- # rails generate robot_lab:job NAME [options]
11
- #
12
- # Examples:
13
- # rails generate robot_lab:job Support
14
- # # => app/jobs/support_job.rb (robot_class SupportRobot)
15
- #
16
- class JobGenerator < ::Rails::Generators::NamedBase
17
- source_root File.expand_path("templates", __dir__)
18
-
19
- class_option :queue, type: :string, default: "default",
20
- desc: "ActiveJob queue name"
21
-
22
- # Creates the job file.
23
- #
24
- # @return [void]
25
- def create_job_file
26
- template "robot_job.rb.tt", "app/jobs/#{file_name}_job.rb"
27
- end
28
-
29
- private
30
-
31
- def queue_name
32
- options[:queue]
33
- end
34
-
35
- def robot_class_name
36
- "#{class_name}Robot"
37
- end
38
- end
39
- end
40
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
-
5
- module RobotLab
6
- module Generators
7
- # Generates a new RobotLab robot
8
- #
9
- # Usage:
10
- # rails generate robot_lab:robot NAME [options]
11
- #
12
- # Examples:
13
- # rails generate robot_lab:robot Support
14
- # rails generate robot_lab:robot Billing --description="Handles billing queries"
15
- #
16
- class RobotGenerator < ::Rails::Generators::NamedBase
17
- source_root File.expand_path("templates", __dir__)
18
-
19
- class_option :description, type: :string, default: nil,
20
- desc: "Robot description"
21
- class_option :routing, type: :boolean, default: false,
22
- desc: "Generate a routing robot"
23
- class_option :tools, type: :array, default: [],
24
- desc: "List of tools to include"
25
-
26
- # Creates the robot class file.
27
- #
28
- # @return [void]
29
- def create_robot_file
30
- if options[:routing]
31
- template "routing_robot.rb.tt", "app/robots/#{file_name}_robot.rb"
32
- else
33
- template "robot.rb.tt", "app/robots/#{file_name}_robot.rb"
34
- end
35
- end
36
-
37
- # Creates the robot test file.
38
- #
39
- # @return [void]
40
- def create_test_file
41
- template "robot_test.rb.tt", "test/robots/#{file_name}_robot_test.rb"
42
- end
43
-
44
- private
45
-
46
- def robot_description
47
- options[:description] || "A helpful #{class_name.titleize} robot"
48
- end
49
-
50
- def robot_tools
51
- options[:tools]
52
- end
53
- end
54
- end
55
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # RobotLab Configuration
4
- #
5
- # RobotLab uses MywayConfig for configuration. Settings are loaded from:
6
- #
7
- # 1. Bundled defaults (lib/robot_lab/config/defaults.yml)
8
- # 2. Environment-specific overrides (development, test, production)
9
- # 3. XDG user config (~/.config/robot_lab/config.yml)
10
- # 4. Project config (./config/robot_lab.yml)
11
- # 5. Environment variables (ROBOT_LAB_* prefix)
12
- #
13
- # Create config/robot_lab.yml for project-specific settings:
14
- #
15
- # defaults:
16
- # ruby_llm:
17
- # anthropic_api_key: <%%= ENV['ANTHROPIC_API_KEY'] %>
18
- # openai_api_key: <%%= ENV['OPENAI_API_KEY'] %>
19
- # model: claude-sonnet-4
20
- # request_timeout: 180
21
- #
22
- # development:
23
- # ruby_llm:
24
- # log_level: :debug
25
- #
26
- # test:
27
- # streaming_enabled: false
28
- # ruby_llm:
29
- # model: claude-3-haiku-20240307
30
- # request_timeout: 30
31
- #
32
- # Or use environment variables with double underscores for nested keys:
33
- #
34
- # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
35
- # ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
36
- #
37
- # RobotLab also falls back to standard provider env vars
38
- # (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY) when the prefixed
39
- # versions are not set.
40
-
41
- # Set the RobotLab logger to use Rails.logger
42
- RobotLab.config.logger = Rails.logger
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Generic background job for executing any robot asynchronously.
4
- #
5
- # Inherits from RobotLab::Job — Turbo Stream wiring, thread persistence,
6
- # and completion/error broadcasting are all handled by the base class.
7
- #
8
- # Pass robot_class: at enqueue time to select which robot to run, or
9
- # generate a dedicated job with `rails generate robot_lab:job NAME` to
10
- # bind a job class to a specific robot via the robot_class DSL.
11
- #
12
- # @example Enqueue from a controller
13
- # RobotRunJob.perform_later(
14
- # robot_class: "SupportRobot",
15
- # message: params[:message],
16
- # thread_id: session_id
17
- # )
18
- #
19
- class RobotRunJob < RobotLab::Job
20
- queue_as :default
21
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRobotLabTables < ActiveRecord::Migration[7.0]
4
- def change
5
- create_table :robot_lab_threads do |t|
6
- t.string :session_id, null: false, index: { unique: true }
7
- t.text :initial_input
8
- t.json :input_metadata, default: {}
9
- t.json :state_data, default: {}
10
- t.text :last_user_message
11
- t.datetime :last_user_message_at
12
-
13
- t.timestamps
14
- end
15
-
16
- create_table :robot_lab_results do |t|
17
- t.string :session_id, null: false, index: true
18
- t.string :robot_name, null: false
19
- t.integer :sequence_number, null: false, default: 0
20
- t.json :output_data, default: []
21
- t.json :tool_calls_data, default: []
22
- t.string :stop_reason
23
- t.string :checksum
24
-
25
- t.timestamps
26
- end
27
-
28
- add_index :robot_lab_results, [:session_id, :sequence_number]
29
- add_foreign_key :robot_lab_results, :robot_lab_threads,
30
- column: :session_id, primary_key: :session_id
31
- end
32
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # RobotLab Result Model
4
- #
5
- # Stores robot execution results for history persistence.
6
- #
7
- class RobotLabResult < ApplicationRecord
8
- belongs_to :thread,
9
- class_name: "RobotLabThread",
10
- foreign_key: :session_id,
11
- primary_key: :session_id
12
-
13
- validates :session_id, presence: true
14
- validates :robot_name, presence: true
15
- validates :sequence_number, presence: true,
16
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
17
-
18
- default_scope { order(sequence_number: :asc) }
19
-
20
- # Get output messages as RobotLab::Message objects
21
- #
22
- # @return [Array<RobotLab::Message>]
23
- #
24
- def output_messages
25
- (output_data || []).map do |data|
26
- RobotLab::Message.from_hash(data.symbolize_keys)
27
- end
28
- end
29
-
30
- # Get tool calls as RobotLab::Message objects
31
- #
32
- # @return [Array<RobotLab::Message>]
33
- #
34
- def tool_call_messages
35
- (tool_calls_data || []).map do |data|
36
- RobotLab::Message.from_hash(data.symbolize_keys)
37
- end
38
- end
39
-
40
- # Convert to RobotLab::RobotResult
41
- #
42
- # @return [RobotLab::RobotResult]
43
- #
44
- def to_robot_result
45
- RobotLab::RobotResult.new(
46
- robot_name: robot_name,
47
- output: output_messages,
48
- tool_calls: tool_call_messages,
49
- stop_reason: stop_reason
50
- )
51
- end
52
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # <%= class_name %> Robot
4
- #
5
- # <%= robot_description %>
6
- #
7
- class <%= class_name %>Robot
8
- SYSTEM_PROMPT = <<~PROMPT
9
- You are a helpful <%= class_name.titleize %> assistant.
10
-
11
- Your role is to assist users with their requests in a friendly and efficient manner.
12
- PROMPT
13
-
14
- def self.build(**options)
15
- RobotLab.build(
16
- name: "<%= file_name %>",
17
- description: "<%= robot_description %>",
18
- system_prompt: SYSTEM_PROMPT,
19
- local_tools: tools,
20
- **options
21
- )
22
- end
23
-
24
- def self.tools
25
- [
26
- <%- robot_tools.each do |tool| -%>
27
- # <%= tool.camelize %>,
28
- <%- end -%>
29
- ]
30
- end
31
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Background job that runs <%= robot_class_name %> asynchronously.
4
- #
5
- # Inherits from RobotLab::Job — Turbo Stream wiring, thread persistence,
6
- # and completion/error broadcasting are all handled by the base class.
7
- #
8
- # @example Enqueue from a controller
9
- # <%= class_name %>Job.perform_later(
10
- # message: params[:message],
11
- # thread_id: session_id
12
- # )
13
- #
14
- class <%= class_name %>Job < RobotLab::Job
15
- queue_as :<%= queue_name %>
16
-
17
- robot_class <%= robot_class_name %>
18
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_helper"
4
-
5
- class <%= class_name %>RobotTest < ActiveSupport::TestCase
6
- def setup
7
- @robot = <%= class_name %>Robot.build
8
- end
9
-
10
- test "robot has correct name" do
11
- assert_equal "<%= file_name %>", @robot.name
12
- end
13
-
14
- test "robot has description" do
15
- assert_not_nil @robot.description
16
- end
17
-
18
- test "robot has system prompt" do
19
- assert_not_nil @robot.system_prompt
20
- end
21
-
22
- # Add more tests as needed:
23
- #
24
- # test "robot runs successfully" do
25
- # result = @robot.run("Hello, I need help")
26
- # assert result.is_a?(RobotLab::RobotResult)
27
- # assert_not_nil result.last_text_content
28
- # end
29
- #
30
- # test "robot has expected tools" do
31
- # tool_names = @robot.local_tools.map(&:name)
32
- # assert_includes tool_names, "expected_tool_name"
33
- # end
34
- end