robot_lab 0.2.1 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.loki +60 -0
  4. data/.quality/reek_baseline.txt +43 -0
  5. data/.rubocop.yml +5 -167
  6. data/CHANGELOG.md +54 -0
  7. data/README.md +185 -64
  8. data/Rakefile +28 -12
  9. data/docs/api/core/index.md +41 -14
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +285 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +636 -60
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +152 -14
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +83 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +519 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +118 -30
  33. data/docs/architecture/index.md +86 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +153 -38
  36. data/docs/architecture/robot-execution.md +173 -91
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +299 -93
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +296 -50
  48. data/docs/guides/creating-networks.md +220 -31
  49. data/docs/guides/hooks.md +1084 -0
  50. data/docs/guides/index.md +5 -0
  51. data/docs/guides/knowledge.md +37 -6
  52. data/docs/guides/mcp-integration.md +211 -44
  53. data/docs/guides/memory.md +103 -12
  54. data/docs/guides/observability.md +166 -51
  55. data/docs/guides/streaming.md +184 -125
  56. data/docs/guides/using-tools.md +300 -11
  57. data/docs/index.md +37 -5
  58. data/examples/01_simple_robot.rb +2 -2
  59. data/examples/02_tools.rb +14 -4
  60. data/examples/03_network.rb +23 -9
  61. data/examples/04_mcp.rb +11 -4
  62. data/examples/05_streaming.rb +8 -5
  63. data/examples/06_prompt_templates.rb +13 -9
  64. data/examples/07_network_memory.rb +5 -5
  65. data/examples/08_llm_config.rb +20 -15
  66. data/examples/09_chaining.rb +4 -4
  67. data/examples/11_network_introspection.rb +4 -4
  68. data/examples/12_message_bus.rb +2 -2
  69. data/examples/13_spawn.rb +2 -2
  70. data/examples/14_rusty_circuit/README.md +1 -0
  71. data/examples/14_rusty_circuit/comic.rb +7 -3
  72. data/examples/14_rusty_circuit/display.rb +14 -0
  73. data/examples/14_rusty_circuit/heckler.rb +8 -6
  74. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  75. data/examples/14_rusty_circuit/scout.rb +17 -10
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  78. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  79. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  80. data/examples/16_writers_room/writer.rb +22 -22
  81. data/examples/16_writers_room/writers_room.rb +2 -0
  82. data/examples/17_skills.rb +14 -13
  83. data/examples/18_rails/README.md +20 -1
  84. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  85. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  86. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  87. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  88. data/examples/19_token_tracking.rb +25 -9
  89. data/examples/20_circuit_breaker.rb +10 -7
  90. data/examples/21_learning_loop.rb +42 -16
  91. data/examples/22_context_compression.rb +23 -23
  92. data/examples/23_convergence.rb +24 -17
  93. data/examples/24_structured_delegation.rb +13 -8
  94. data/examples/25_history_search.rb +12 -8
  95. data/examples/27_incident_response/incident_response.rb +31 -13
  96. data/examples/28_mcp_discovery.rb +17 -13
  97. data/examples/29_ractor_tools.rb +4 -2
  98. data/examples/30_ractor_network.rb +22 -17
  99. data/examples/31_launch_assessment.rb +20 -9
  100. data/examples/32_newsletter_reader.rb +7 -2
  101. data/examples/33_stock_predictor.rb +34 -13
  102. data/examples/34_agentskills.rb +7 -3
  103. data/examples/35_hooks.rb +266 -0
  104. data/examples/README.md +203 -42
  105. data/examples/common.rb +79 -7
  106. data/examples/xyzzy.rb +97 -0
  107. data/lib/robot_lab/agent_skill.rb +5 -4
  108. data/lib/robot_lab/budget/ledger.rb +98 -0
  109. data/lib/robot_lab/capabilities.rb +84 -0
  110. data/lib/robot_lab/config/defaults.yml +10 -0
  111. data/lib/robot_lab/config.rb +14 -9
  112. data/lib/robot_lab/error.rb +34 -4
  113. data/lib/robot_lab/errors.rb +45 -0
  114. data/lib/robot_lab/hook.rb +79 -0
  115. data/lib/robot_lab/hook_context.rb +194 -0
  116. data/lib/robot_lab/hook_registry.rb +55 -0
  117. data/lib/robot_lab/hooks.rb +87 -0
  118. data/lib/robot_lab/mcp/connection_poller.rb +2 -2
  119. data/lib/robot_lab/names.rb +402 -0
  120. data/lib/robot_lab/narrator.rb +87 -0
  121. data/lib/robot_lab/network.rb +77 -20
  122. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  123. data/lib/robot_lab/robot/budget.rb +89 -0
  124. data/lib/robot_lab/robot/bus_messaging.rb +90 -18
  125. data/lib/robot_lab/robot/hooking.rb +56 -0
  126. data/lib/robot_lab/robot/mcp_management.rb +8 -2
  127. data/lib/robot_lab/robot/template_rendering.rb +18 -5
  128. data/lib/robot_lab/robot.rb +233 -89
  129. data/lib/robot_lab/run_config.rb +24 -5
  130. data/lib/robot_lab/runnable.rb +51 -0
  131. data/lib/robot_lab/sandbox/null.rb +13 -0
  132. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  133. data/lib/robot_lab/sandbox.rb +52 -0
  134. data/lib/robot_lab/script_tool.rb +60 -2
  135. data/lib/robot_lab/task.rb +26 -20
  136. data/lib/robot_lab/tool.rb +52 -11
  137. data/lib/robot_lab/version.rb +1 -1
  138. data/lib/robot_lab.rb +70 -15
  139. data/mkdocs.yml +7 -1
  140. metadata +41 -99
  141. data/examples/temp.md +0 -51
  142. data/site/404.html +0 -2300
  143. data/site/api/core/index.html +0 -2706
  144. data/site/api/core/memory/index.html +0 -3793
  145. data/site/api/core/network/index.html +0 -3500
  146. data/site/api/core/robot/index.html +0 -4566
  147. data/site/api/core/state/index.html +0 -3390
  148. data/site/api/core/tool/index.html +0 -3843
  149. data/site/api/index.html +0 -2635
  150. data/site/api/mcp/client/index.html +0 -3435
  151. data/site/api/mcp/index.html +0 -2783
  152. data/site/api/mcp/server/index.html +0 -3252
  153. data/site/api/mcp/transports/index.html +0 -3352
  154. data/site/api/messages/index.html +0 -2641
  155. data/site/api/messages/text-message/index.html +0 -3087
  156. data/site/api/messages/tool-call-message/index.html +0 -3159
  157. data/site/api/messages/tool-result-message/index.html +0 -3252
  158. data/site/api/messages/user-message/index.html +0 -3212
  159. data/site/api/streaming/context/index.html +0 -3282
  160. data/site/api/streaming/events/index.html +0 -3347
  161. data/site/api/streaming/index.html +0 -2738
  162. data/site/architecture/core-concepts/index.html +0 -3757
  163. data/site/architecture/index.html +0 -2797
  164. data/site/architecture/message-flow/index.html +0 -3238
  165. data/site/architecture/network-orchestration/index.html +0 -3433
  166. data/site/architecture/robot-execution/index.html +0 -3140
  167. data/site/architecture/state-management/index.html +0 -3498
  168. data/site/assets/css/custom.css +0 -56
  169. data/site/assets/images/favicon.png +0 -0
  170. data/site/assets/images/robot_lab.jpg +0 -0
  171. data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
  172. data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
  173. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  174. data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  175. data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  176. data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  177. data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  178. data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  179. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  180. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  181. data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  182. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  183. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  184. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  185. data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  186. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  187. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  188. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  189. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  190. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  191. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  192. data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  193. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  194. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  195. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  196. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  197. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  198. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  199. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  200. data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  201. data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  202. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  203. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  204. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  205. data/site/assets/javascripts/lunr/tinyseg.js +0 -206
  206. data/site/assets/javascripts/lunr/wordcut.js +0 -6708
  207. data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
  208. data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
  209. data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
  210. data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
  211. data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  212. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
  213. data/site/concepts/index.html +0 -3455
  214. data/site/examples/basic-chat/index.html +0 -2880
  215. data/site/examples/index.html +0 -2907
  216. data/site/examples/mcp-server/index.html +0 -3018
  217. data/site/examples/multi-robot-network/index.html +0 -3131
  218. data/site/examples/rails-application/index.html +0 -3329
  219. data/site/examples/tool-usage/index.html +0 -3085
  220. data/site/getting-started/configuration/index.html +0 -3745
  221. data/site/getting-started/index.html +0 -2572
  222. data/site/getting-started/installation/index.html +0 -2981
  223. data/site/getting-started/quick-start/index.html +0 -2942
  224. data/site/guides/building-robots/index.html +0 -4290
  225. data/site/guides/creating-networks/index.html +0 -3858
  226. data/site/guides/index.html +0 -2586
  227. data/site/guides/mcp-integration/index.html +0 -3581
  228. data/site/guides/memory/index.html +0 -3586
  229. data/site/guides/rails-integration/index.html +0 -4019
  230. data/site/guides/streaming/index.html +0 -3157
  231. data/site/guides/using-tools/index.html +0 -3802
  232. data/site/index.html +0 -2671
  233. data/site/search/search_index.json +0 -1
  234. data/site/sitemap.xml +0 -183
  235. data/site/sitemap.xml.gz +0 -0
  236. data/site/tags.json +0 -1
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 35: Hooks Architecture with robot_lab-xyzzy
5
+ #
6
+ # Demonstrates the hook handler class pattern. Hooks are named classes that
7
+ # inherit from RobotLab::Hook and implement class methods for each lifecycle
8
+ # phase they care about. All hooks are Ractor-safe by default.
9
+ #
10
+ # Usage:
11
+ # bundle exec ruby examples/35_hooks.rb
12
+
13
+ require_relative "common"
14
+
15
+ # RobotLab::Hook has no logger accessor — handlers own their own output.
16
+ # xyzzy.rb freezes its log destination into a constant at load time, so the
17
+ # redirect has to happen before the require.
18
+ XYZZY_LOG = File.join(__dir__, "xyzzy_hooks.log")
19
+ ENV["XYZZY_LOG_PATH"] = XYZZY_LOG
20
+
21
+ require_relative "xyzzy"
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Handler classes used in this demo
25
+ # ---------------------------------------------------------------------------
26
+
27
+ class RuntimeDemoHook < RobotLab::Hook
28
+ self.namespace = :runtime_demo
29
+
30
+ def self.after_run(ctx)
31
+ ctx.local.reply_seen = ctx.response.reply
32
+ end
33
+ end
34
+
35
+ class LocalDemoHook < RobotLab::Hook
36
+ self.namespace = :local_demo
37
+
38
+ def self.before_run(ctx)
39
+ ctx.local.note = "local robot hook state"
40
+ ctx.local.demo = "robot_run"
41
+ end
42
+ end
43
+
44
+ class PerfHook < RobotLab::Hook
45
+ self.namespace = :perf
46
+ @timings = []
47
+ @mutex = Mutex.new
48
+
49
+ class << self
50
+ def reset!
51
+ @mutex.synchronize { @timings = [] }
52
+ end
53
+
54
+ def timings
55
+ @mutex.synchronize { @timings.dup }
56
+ end
57
+
58
+ def around_run(ctx, &block)
59
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
60
+ result = block.call
61
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000).round(3)
62
+ @mutex.synchronize { @timings << elapsed }
63
+ puts " [perf] #{ctx.request.inspect} — #{elapsed}ms"
64
+ result
65
+ end
66
+ end
67
+ end
68
+
69
+ class GenerationTracerHook < RobotLab::Hook
70
+ self.namespace = :tracer
71
+
72
+ def self.before_llm_generation(ctx)
73
+ puts " [tracer] llm_generation start request=#{ctx.request.inspect}"
74
+ end
75
+
76
+ def self.after_llm_generation(ctx)
77
+ msg = ctx.generation_response
78
+ puts " [tracer] llm_generation done model=#{msg&.model_id} tokens=#{msg&.output_tokens}"
79
+ end
80
+ end
81
+
82
+ class GenCacheHook < RobotLab::Hook
83
+ self.namespace = :gen_cache
84
+ @cache = {}
85
+ @mutex = Mutex.new
86
+ @call_count = 0
87
+
88
+ class << self
89
+ def reset!
90
+ @mutex.synchronize do
91
+ @cache = {}
92
+ @call_count = 0
93
+ end
94
+ end
95
+
96
+ def call_count
97
+ @mutex.synchronize { @call_count }
98
+ end
99
+
100
+ def around_llm_generation(ctx, &block)
101
+ cached = @mutex.synchronize { @cache[ctx.request] }
102
+ if cached
103
+ puts " [gen_cache] HIT — skipping LLM"
104
+ cached
105
+ else
106
+ puts " [gen_cache] MISS — calling LLM"
107
+ @mutex.synchronize { @call_count += 1 }
108
+ block.call.tap { |r| @mutex.synchronize { @cache[ctx.request] = r } }
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ class NetworkDemoHook < RobotLab::Hook
115
+ self.namespace = :network_demo
116
+
117
+ def self.before_task(ctx)
118
+ ctx.local.task_note = "network-scoped task hook"
119
+ end
120
+ end
121
+
122
+ # ---------------------------------------------------------------------------
123
+
124
+ HookDemoResponse = Data.define(:content, :tool_calls, :stop_reason) do
125
+ def initialize(content:, tool_calls: nil, stop_reason: "end_turn")
126
+ super
127
+ end
128
+ end
129
+
130
+ class HookDemoTool < RobotLab::Tool
131
+ description "Returns a deterministic hook demo value"
132
+ param :label, type: "string", desc: "The label to echo"
133
+
134
+ def execute(label:)
135
+ { label: label, status: "handled by HookDemoTool" }
136
+ end
137
+ end
138
+
139
+ class HookDemo
140
+ def run
141
+ banner "Hooks Architecture with robot_lab-xyzzy"
142
+
143
+ explain_setup
144
+ run_robot
145
+ run_llm_loop
146
+ run_tool
147
+ run_network
148
+ run_error
149
+
150
+ hr
151
+ puts "Hook demo complete."
152
+ end
153
+
154
+ private
155
+
156
+ def explain_setup
157
+ section "Extension Registration"
158
+ puts <<~TEXT
159
+ xyzzy.rb is a single-file hook extension loaded from examples/.
160
+
161
+ It is a RobotLab::Hook subclass registered under namespace :#{RobotLab::Xyzzy.namespace}.
162
+ stdout : [xyzzy] timestamp hook_name (one line per call)
163
+ logfile : full context snapshot → #{XYZZY_LOG}
164
+ TEXT
165
+ end
166
+
167
+ def run_robot
168
+ section "Robot Run Hooks"
169
+ robot = hooked_robot("hook_demo_robot")
170
+ robot.on(LocalDemoHook)
171
+
172
+ result = robot.run("show the hook pipeline", hooks: RuntimeDemoHook)
173
+
174
+ puts "Robot reply: #{result.reply}"
175
+ end
176
+
177
+ def run_llm_loop
178
+ section "LLM Request/Response Loop"
179
+ puts <<~TEXT
180
+ Demonstrates before/around/after_llm_generation hooks.
181
+ GenCacheHook implements around_llm_generation: on a cache hit the block
182
+ (the real LLM call) is never invoked. PerfHook times each robot.run call.
183
+ TEXT
184
+
185
+ GenCacheHook.reset!
186
+ PerfHook.reset!
187
+
188
+ puts " provider=#{LLM[:default].provider} model=#{LLM[:default].model}\n\n"
189
+
190
+ # with_model alone would leave the provider unset, and an Ollama model is
191
+ # not in RubyLLM's registry — pass provider and model together.
192
+ robot = RobotLab.build(
193
+ name: "loop_demo_robot",
194
+ system_prompt: "You are a helpful assistant. Answer every question in one concise sentence.",
195
+ **llm_opts
196
+ )
197
+
198
+ robot.on(PerfHook)
199
+ robot.on(GenerationTracerHook)
200
+ robot.on(GenCacheHook)
201
+
202
+ queries = [
203
+ "what is the boiling point of water?",
204
+ "what is 6 * 7?",
205
+ "what is the boiling point of water?",
206
+ "what is 6 * 7?"
207
+ ]
208
+
209
+ queries.each_with_index do |query, i|
210
+ puts "\n--- query #{i + 1}: #{query.inspect} ---"
211
+ result = robot.run(query)
212
+ puts " reply: #{result.reply}"
213
+ end
214
+
215
+ timings = PerfHook.timings
216
+ cache_hits = queries.size - GenCacheHook.call_count
217
+ avg_ms = (timings.sum / timings.size).round(3)
218
+ puts "\n--- summary ---"
219
+ puts " robot.run calls : #{queries.size}"
220
+ puts " LLM calls made : #{GenCacheHook.call_count} (#{cache_hits} served from gen_cache)"
221
+ puts " avg run time : #{avg_ms}ms"
222
+ end
223
+
224
+ def run_tool
225
+ section "Tool Call Hooks"
226
+ tool = HookDemoTool.new
227
+ result = tool.call({ "label" => "tool hook payload" })
228
+ puts "Tool result: #{result.inspect}"
229
+ end
230
+
231
+ def run_network
232
+ section "Network and Task Hooks"
233
+ network = RobotLab::Network.new(name: "hook_demo_network")
234
+ network.on(NetworkDemoHook)
235
+ network.task(:summarize, hooked_robot("network_worker"), depends_on: :none)
236
+
237
+ result = network.run(message: "summarize hook activity")
238
+ reply = result.respond_to?(:reply) ? result.reply : result&.value&.reply
239
+ puts "Network result: #{reply}"
240
+ end
241
+
242
+ def run_error
243
+ section "Error Hook"
244
+ failing_robot = RobotLab.build(name: "failing_hook_robot", system_prompt: "raise", **llm_opts)
245
+ # Robot#chat is a public reader — no need to reach for @chat.
246
+ failing_robot.chat.define_singleton_method(:ask) do |_message = nil, **_kwargs|
247
+ raise "planned hook demo failure"
248
+ end
249
+
250
+ failing_robot.run("trigger the on_error hook")
251
+ rescue RuntimeError => e
252
+ puts "Caught expected error: #{e.message}"
253
+ end
254
+
255
+ # Stubs the chat so the hook pipeline is exercised deterministically with no
256
+ # LLM traffic. Robot#chat is public, so this needs no ivar surgery.
257
+ def hooked_robot(name)
258
+ RobotLab.build(name: name, system_prompt: "deterministic hook demo", **llm_opts).tap do |robot|
259
+ robot.chat.define_singleton_method(:ask) do |message = nil, **_kwargs, &_block|
260
+ HookDemoResponse.new(content: "deterministic response to #{message.inspect}")
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ HookDemo.new.run
data/examples/README.md CHANGED
@@ -6,7 +6,71 @@ Working demonstrations of RobotLab features, from single-robot basics to multi-r
6
6
 
7
7
  - Ruby >= 3.2
8
8
  - `bundle install` (from the project root)
9
- - An LLM API key (e.g., `ANTHROPIC_API_KEY`)
9
+ - [Ollama](https://ollama.com) running locally with the demo model pulled:
10
+
11
+ ```bash
12
+ ollama serve
13
+ ollama pull qwen3.6
14
+ ```
15
+
16
+ Every example runs against a **local** model — no API keys, no egress, no
17
+ per-token cost. The model, provider and endpoint are set in one place,
18
+ `examples/common.rb`:
19
+
20
+ ```ruby
21
+ LLM = {
22
+ default: LlmConfig.new(provider: "ollama", model: "qwen3.6:latest"),
23
+ small: LlmConfig.new(provider: "ollama", model: "qwen2.5:7b"),
24
+ large: LlmConfig.new(provider: "ollama", model: "llama3.3:latest")
25
+ }
26
+ ```
27
+
28
+ Robots pick it up with the `llm_opts` helper:
29
+
30
+ ```ruby
31
+ robot = RobotLab.build(name: "helper", **llm_opts) # default model
32
+ cheap = RobotLab.build(name: "helper", **llm_opts(:small)) # smaller model
33
+ ```
34
+
35
+ `provider:` is not optional here. Ollama models are absent from RubyLLM's
36
+ model registry, so passing `model:` alone raises `RubyLLM::ModelNotFoundError`
37
+ — supplying `provider:` is what makes RubyLLM skip the registry lookup. Note
38
+ also that `RunConfig` has no `provider` field, so provider and model travel
39
+ together on the robot even when other settings come from a shared `RunConfig`.
40
+
41
+ `common.rb` also raises `request_timeout` to 900s. A 20B+ model on consumer
42
+ hardware routinely takes longer than robot_lab's 120s default on a long
43
+ answer, which otherwise surfaces mid-example as `Net::ReadTimeout`. Override
44
+ with `LLM_REQUEST_TIMEOUT`.
45
+
46
+ ## Runtime: pick your model with `LLM_PROFILE`
47
+
48
+ Every example is a *sequence* of LLM calls, so wall-clock is dominated by
49
+ `calls × seconds-per-call`. On `qwen3.6:latest` expect roughly **60-70s per
50
+ call**. That is fine for the single-robot examples and painful for the
51
+ multi-robot ones:
52
+
53
+ | Example | LLM calls | `qwen3.6:latest` | `LLM_PROFILE=small` |
54
+ |---------|-----------|------------------|---------------------|
55
+ | 01, 02, 19, 20 | 1-6 | seconds to ~5 min | fast |
56
+ | 03, 06, 07, 24, 27, 31 | 4-8 | ~5-10 min | ~2 min |
57
+ | 13, 14, 15 | ~20-30 | **30-40 min** | ~5-10 min |
58
+ | 16 | unbounded (600s cap) | usually hits the cap | often completes |
59
+
60
+ `LLM_PROFILE` switches the model for any example without editing it:
61
+
62
+ ```bash
63
+ LLM_PROFILE=small bundle exec ruby examples/14_rusty_circuit/open_mic.rb
64
+ ```
65
+
66
+ Valid values are the keys of `LLM` in `common.rb`: `default`, `small`, `large`.
67
+
68
+ **Long silences are not hangs.** In example 14 the scout writes its notes to
69
+ `output/scout_notes.md` rather than STDOUT, and a scout turn is 1-4 sequential
70
+ calls — minutes of blank terminal. The `· Scout …` lines exist so you can tell
71
+ progress from a stall. Example 16 is the same idea at larger scale: writers
72
+ coordinate through shared memory, and much of the work shows up in
73
+ `output/room.log` rather than on screen.
10
74
 
11
75
  ## Running Examples
12
76
 
@@ -21,17 +85,36 @@ bundle exec rake examples:all
21
85
  bundle exec ruby examples/01_simple_robot.rb
22
86
  ```
23
87
 
88
+ ## Tools require `tools: :inherit` at run time
89
+
90
+ This trips up everyone once. `Robot#run` defaults to `tools: :none`, which
91
+ means *"send zero tools this turn"* — the robot still holds them in
92
+ `local_tools`, but the provider never sees them and the model answers from
93
+ memory instead of calling anything:
94
+
95
+ ```ruby
96
+ robot = RobotLab.build(name: "bot", **llm_opts, local_tools: [Calculator])
97
+
98
+ robot.run("What is 15 * 7?") # Calculator is NEVER offered
99
+ robot.run("What is 15 * 7?", tools: :inherit) # Calculator is offered
100
+ ```
101
+
102
+ The same applies to MCP (`mcp: :inherit`), to network tasks
103
+ (`task :name, robot, tools: :inherit`), and to anything that forwards keywords
104
+ into `run` — including `RobotLab::RailsIntegration::Job`. Examples 02, 04, 06,
105
+ 14, 15, 16, 20, 33 and the Rails app all pass it explicitly.
106
+
24
107
  ## Directory Structure
25
108
 
26
109
  ```
27
110
  examples/
28
- 27_incident_response/ # Phase 5 infra — BusPoller, reactive memory, poller groups
29
- incident_response.rb # Main entrypoint wires up the war room
111
+ common.rb # Shared LLM config (Ollama), llm_opts, output helpers
112
+ xyzzy.rb # Single-file hook extension used by 35
30
113
  01_simple_robot.rb # Basic robot with template
31
114
  02_tools.rb # Robot with custom tools
32
115
  03_network.rb # Multi-robot network with routing
33
116
  04_mcp.rb # MCP server integration (GitHub)
34
- 05_streaming.rb # Real-time streaming events
117
+ 05_streaming.rb # Real-time streaming callbacks
35
118
  06_prompt_templates.rb # Template-based e-commerce support
36
119
  07_network_memory.rb # Shared memory with concurrent robots
37
120
  08_llm_config.rb # Configuration hierarchy demo
@@ -47,6 +130,20 @@ examples/
47
130
  scout.rb # Talent scout with analyst spawning
48
131
  display.rb # Terminal formatting (color, wrapping, file output)
49
132
  prompts/ # Templates for comic, heckler, and scout
133
+ 15_memory_network_and_bus/ # Network + memory + bus + spawn in one pipeline
134
+ editorial_pipeline.rb # Main entrypoint — three writers, editor, chief
135
+ 16_writers_room/ # Self-organizing group: no orchestration at all
136
+ writers_room.rb # Main entrypoint — book and screenplay modes
137
+ 17_skills.rb # Composable skills, flat and recursive
138
+ 18_rails/ # Minimal Rails 8 demo app (full integration)
139
+ app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
140
+ app/tools/time_tool.rb # Custom RobotLab::Tool subclass
141
+ app/jobs/robot_run_job.rb # Background job with Turbo Stream callbacks
142
+ app/controllers/ # Chat controller (index + create)
143
+ app/views/ # Layout with CDN importmap, chat view with streaming
144
+ app/models/ # RobotLabThread, RobotLabResult
145
+ config/ # Minimal Rails 8 config (async adapters, no asset pipeline)
146
+ db/migrate/ # Migration from generator template
50
147
  19_token_tracking.rb # Per-robot token & cost tracking
51
148
  20_circuit_breaker.rb # Tool loop circuit breaker with max_tool_rounds
52
149
  21_learning_loop.rb # Learning accumulation across runs with robot.learn
@@ -55,18 +152,17 @@ examples/
55
152
  24_structured_delegation.rb # Structured delegation with duration and token tracking
56
153
  25_history_search.rb # Semantic search over a robot's conversation history
57
154
  26_document_store.rb # Embedding-based document store (RAG) via fastembed
155
+ 27_incident_response/ # BusPoller, reactive memory, poller groups
156
+ incident_response.rb # Main entrypoint — wires up the war room
157
+ 28_mcp_discovery.rb # Semantic MCP server selection before connecting
58
158
  29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
59
159
  30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
60
160
  31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
61
- 18_rails/ # Minimal Rails 8 demo app (full integration)
62
- app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
63
- app/tools/time_tool.rb # Custom RobotLab::Tool subclass
64
- app/jobs/robot_run_job.rb # Background job with Turbo Stream callbacks
65
- app/controllers/ # Chat controller (index + create)
66
- app/views/ # Layout with CDN importmap, chat view with streaming
67
- app/models/ # RobotLabThread, RobotLabResult
68
- config/ # Minimal Rails 8 config (async adapters, no asset pipeline)
69
- db/migrate/ # Migration from generator template
161
+ 32_newsletter_reader.rb # Utility script (no RobotLab) RSS to Markdown
162
+ 33_stock_generator.rb # Companion publisher for 33_stock_predictor (Redis)
163
+ 33_stock_predictor.rb # Durable cross-session learning via robot_lab-durable
164
+ 34_agentskills.rb # AgentSkills.io folder-format skills, matched per run
165
+ 35_hooks.rb # Hook architecture demo using xyzzy.rb
70
166
  prompts/ # Prompt templates (.md with YAML front matter)
71
167
  ```
72
168
 
@@ -76,43 +172,43 @@ examples/
76
172
 
77
173
  Create and run a basic robot using a prompt template. Sends a single message and displays the response.
78
174
 
79
- **Requires:** LLM API key
175
+ **Requires:** Ollama
80
176
 
81
177
  ### 02 — Tools
82
178
 
83
179
  Give a robot custom tools (`Calculator`, `FortuneCookie`) defined as `RubyLLM::Tool` subclasses. The LLM decides when to call each tool based on the user's request.
84
180
 
85
- **Requires:** LLM API key
181
+ **Requires:** Ollama
86
182
 
87
183
  ### 03 — Multi-Robot Network
88
184
 
89
185
  Build a customer support network with a classifier robot that routes requests to billing, technical, or general specialists. Uses SimpleFlow's optional task activation for conditional routing.
90
186
 
91
- **Requires:** LLM API key
187
+ **Requires:** Ollama
92
188
 
93
189
  ### 04 — MCP Integration
94
190
 
95
191
  Connect to the GitHub MCP server via stdio transport. Part 1 demonstrates direct `MCP::Client` usage (listing tools, calling `search_repositories`). Part 2 wraps the MCP server inside a robot for natural-language queries.
96
192
 
97
- **Requires:** LLM API key, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
193
+ **Requires:** Ollama, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
98
194
 
99
195
  ### 05 — Streaming
100
196
 
101
- Real-time streaming of robot responses using `RobotLab::Streaming::Context`. Simulates text deltas with timing to demonstrate the streaming event model, then shows the code pattern for streaming with a robot or network.
197
+ Real-time streaming of robot responses through four routes: the stored `on_content:` callback wired at build time, a per-call block passed to `run()`, both together (stored fires first, then the block), and `on_content` arriving through a `RunConfig`.
102
198
 
103
- **Requires:** None (simulated events, no LLM calls)
199
+ **Requires:** Ollama (makes four live calls)
104
200
 
105
201
  ### 06 — Prompt Templates
106
202
 
107
203
  Full e-commerce support system using prompt_manager templates with YAML front matter. A triage robot classifies customer requests and routes to order, product, or escalation specialists. Demonstrates build-time context (company info, policies) and run-time context (customer data, order history).
108
204
 
109
- **Requires:** LLM API key
205
+ **Requires:** Ollama
110
206
 
111
207
  ### 07 — Network Memory
112
208
 
113
209
  Reactive shared memory with concurrent robots. Multiple analysis robots (sentiment, entity extraction, keywords) run in parallel and write to shared memory. A synthesizer robot waits for all results using blocking reads, then produces a combined analysis. Demonstrates subscriptions, notifications, and network broadcast.
114
210
 
115
- **Requires:** LLM API key
211
+ **Requires:** Ollama
116
212
 
117
213
  ### 08 — LLM Configuration
118
214
 
@@ -154,7 +250,7 @@ Bidirectional robot communication via TypedBus. A comedy critic (Alice) tasks a
154
250
 
155
251
  Demonstrates: Robot subclasses, prompt templates, auto-ack `on_message`, `reply()` convenience, temperature ramping, convergence patterns.
156
252
 
157
- **Requires:** LLM API key
253
+ **Requires:** Ollama
158
254
 
159
255
  ### 13 — Spawning Robots
160
256
 
@@ -162,7 +258,7 @@ Dynamic specialist creation at runtime. A dispatcher robot receives questions, a
162
258
 
163
259
  Demonstrates: `spawn` for dynamic robot creation, lazy bus creation, `on_message` for reply handling, LLM-driven delegation.
164
260
 
165
- **Requires:** LLM API key
261
+ **Requires:** Ollama
166
262
 
167
263
  ### 14 — The Rusty Circuit (Open Mic Night)
168
264
 
@@ -170,27 +266,51 @@ A comedy club where three robots interact through a shared message bus. A comedi
170
266
 
171
267
  Terminal output is color-formatted: comic bits in cyan (left-aligned), heckler reactions in yellow (right-indented), tool annotations dimmed. Scout notes go to `scout_notes.md` instead of STDOUT. The final verdict appears in green on both STDOUT and the scout file.
172
268
 
173
- Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, processing guards for async serialization, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
269
+ Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, `enqueue_delivery` for serializing async deliveries, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
270
+
271
+ **Requires:** Ollama. The most expensive example in the suite — ~30 LLM calls, 30+ minutes on `qwen3.6`. Use `LLM_PROFILE=small` to watch it in under ten.
272
+
273
+ ### 15 — Memory, Network, Bus & Spawn Together
274
+
275
+ An editorial pipeline where three writers advocate for macOS, Windows, and Linux/BSD as a home AI research lab platform. The network runs the writers in parallel and hands their drafts to an editor through shared memory; the Linux writer spawns three distro specialists mid-pipeline; an editor-in-chief outside the pipeline reviews the combined article over the bus and can demand revisions.
276
+
277
+ Demonstrates: all four coordination mechanisms in one program, plus the direct `shared_memory` reference pattern that parallel pipeline steps need (`extract_run_context` deletes from a shared hash, so only the first parallel step would otherwise see `network_memory`).
278
+
279
+ **Requires:** Ollama
174
280
 
175
- **Requires:** LLM API key
281
+ ### 16 The Writers' Room (Self-Organizing Group)
282
+
283
+ A team of identical writer robots produces a 10-chapter novella with no orchestration, no pipeline, and no assigned roles. Each writer subscribes to a `:room` broadcast channel and a personal channel, and has tools to read/write shared memory, broadcast, DM, spawn more writers, and mark the work complete. The script seeds the room with an assignment and waits.
284
+
285
+ `--screenplay-from output/memory.json` re-runs the room in screenplay mode, adapting a finished book into a 4-act TV movie pilot at scene granularity.
286
+
287
+ Demonstrates: emergent coordination, `clear_messages(keep_system: true)` as a per-message conversation reset, dynamic team growth, memory as the single source of truth.
288
+
289
+ **Requires:** Ollama (long-running — `--timeout` defaults to 600s)
290
+
291
+ ### 17 — Composable Skills
292
+
293
+ Skills are ordinary templates whose bodies are prepended to the main template. An SRE incident responder is composed from `runbook_protocol` and `structured_output` (flat skills) plus `sre_compliance`, which recursively expands to `pii_redactor` and `audit_trail`. The assembled system prompt is printed line-by-line so the depth-first expansion order is visible, and a skill-less robot is built alongside it for size comparison.
294
+
295
+ **Requires:** Ollama
176
296
 
177
297
  ### 19 — Token & Cost Tracking
178
298
 
179
- Track token usage across runs using `result.input_tokens` / `result.output_tokens` for per-run counts and `robot.total_input_tokens` / `robot.total_output_tokens` for running totals. Demonstrates `reset_token_totals` to start a fresh batch and includes a simple cost estimate using per-provider pricing constants.
299
+ Track token usage across runs using `result.input_tokens` / `result.output_tokens` for per-run counts and `robot.total_input_tokens` / `robot.total_output_tokens` for running totals. Demonstrates `reset_token_totals` to start a fresh batch. Local inference is free, so cost is reported as `$0.00000 (local)`; set `RATE_INPUT_CPM` / `RATE_OUTPUT_CPM` to price the same traffic against a hosted provider.
180
300
 
181
- **Requires:** LLM API key
301
+ **Requires:** Ollama
182
302
 
183
303
  ### 20 — Tool Loop Circuit Breaker
184
304
 
185
305
  Guards against runaway tool call loops using `max_tool_rounds:`. A step processor tool is designed to always return "more steps remain", which would loop indefinitely without a guard. The circuit breaker fires after the configured limit and raises `RobotLab::ToolLoopError`. Shows how to rescue the error gracefully and confirms the robot is fully reusable after a breaker trip.
186
306
 
187
- **Requires:** LLM API key
307
+ **Requires:** Ollama
188
308
 
189
309
  ### 21 — Learning Accumulation Loop
190
310
 
191
311
  Builds up cross-run observations with `robot.learn(text)`. A code reviewer accumulates one key insight after each review. On subsequent runs, learnings are automatically prepended to the user message as a "LEARNINGS FROM PREVIOUS RUNS:" block. Demonstrates bidirectional substring deduplication (broader learnings replace narrower ones), the `robot.learnings` accessor, and how learnings survive a robot rebuild via the shared `Memory` object.
192
312
 
193
- **Requires:** LLM API key
313
+ **Requires:** Ollama
194
314
 
195
315
  ### 22 — Context Window Compression
196
316
 
@@ -198,23 +318,17 @@ Demonstrates `robot.compress_history()` for reducing token usage in long convers
198
318
 
199
319
  **Requires:** `gem 'classifier', '~> 2.3'` in your Gemfile (no LLM calls in the demo itself)
200
320
 
201
- ### 24 — Structured Delegation
202
-
203
- A manager robot delegates sub-tasks to a summarizer and an analyst. Each `delegate()` call returns a `RobotResult` annotated with `delegated_by`, `duration`, and token counts. Includes a comparison table of when to use delegation vs. bus messaging vs. pipelines.
204
-
205
- **Requires:** LLM API key
206
-
207
321
  ### 23 — Debate Convergence Detection
208
322
 
209
- Demonstrates `RobotLab::Convergence` for detecting when two independent agents have reached the same conclusion. Scores pairs of texts from identical → semantically similar → partially related → unrelated, showing how the similarity metric varies. Includes the router fast-path pattern: when two verifier robots agree above a threshold, the expensive reconciler LLM call is skipped entirely.
323
+ Demonstrates `RobotLab::Convergence` for detecting when two independent agents have reached the same conclusion. Scores pairs of texts from identical → semantically similar → partially related → unrelated, showing how the similarity metric varies. Includes the fast-path pattern: a gate robot compares two verifiers' replies and only activates the expensive reconciler via SimpleFlow optional-task activation — when they disagree.
210
324
 
211
325
  **Requires:** `gem 'classifier', '~> 2.3'` in your Gemfile (no LLM calls in the demo itself)
212
326
 
213
327
  ### 24 — Structured Delegation
214
328
 
215
- Demonstrates `robot.delegate(to:, task:)` for synchronous and asynchronous inter-robot delegation. The manager robot delegates document analysis to a summarizer and an analyst. Shows synchronous (sequential, blocking) and asynchronous (parallel fan-out, `DelegationFuture`) modes with wall-time comparison.
329
+ Demonstrates `robot.delegate(to:, task:)` for synchronous and asynchronous inter-robot delegation. The manager robot delegates document analysis to a summarizer and an analyst. Shows synchronous (sequential, blocking) and asynchronous (parallel fan-out, `DelegationFuture`) modes with wall-time comparison. Each result carries `delegated_by`, `duration`, and token counts.
216
330
 
217
- **Requires:** LLM API key
331
+ **Requires:** Ollama
218
332
 
219
333
  ### 25 — Chat History Search
220
334
 
@@ -248,7 +362,7 @@ bundle exec ruby examples/27_incident_response/incident_response.rb
248
362
  bundle exec rake examples:run[27]
249
363
  ```
250
364
 
251
- **Requires:** LLM API key
365
+ **Requires:** Ollama
252
366
 
253
367
  ### 28 — MCP Server Discovery
254
368
 
@@ -296,7 +410,7 @@ and the `pipeline.step_dependencies` dependency graph inspection.
296
410
 
297
411
  **Part 3** — Live LLM run (enabled automatically when `ANTHROPIC_API_KEY` is set).
298
412
 
299
- **Requires:** None for Parts 1 & 2. LLM API key for Part 3.
413
+ **Requires:** None for Parts 1 & 2. `RUN_LIVE=1` plus Ollama for Part 3 (expected to fail — ruby_llm is not Ractor-safe yet).
300
414
 
301
415
  ### 31 — Product Launch Assessment (Concurrency Cap)
302
416
 
@@ -304,7 +418,37 @@ Six specialist robots evaluate a product launch simultaneously: market, competit
304
418
 
305
419
  Demonstrates: `max_concurrent_robots:` on `RunConfig`, `Async::Semaphore` back-pressure via `simple_flow`, six parallel `depends_on: :none` tasks, shared memory writes and blocking reads.
306
420
 
307
- **Requires:** LLM API key
421
+ **Requires:** Ollama
422
+
423
+ ### 32 — Newsletter Reader
424
+
425
+ A plain utility script that fetches unprocessed issues from Ruby newsletter RSS feeds and saves them as Markdown. **It uses no part of RobotLab** — it lives here as a content feeder for other experiments, not as a capability demo. Set `CLIPPINGS_DIR` to choose the output folder.
426
+
427
+ **Requires:** `html2markdown` on `PATH`
428
+
429
+ ### 33 — Durable Cross-Session Learning
430
+
431
+ `33_stock_generator.rb` publishes synthetic XYZZY prices to a Redis channel using geometric Brownian motion. `33_stock_predictor.rb` consumes them, predicts each window's high/low with an SMA + EMA ensemble, and after every window asks a tuner robot to adjust the predictor's parameters.
432
+
433
+ The tuner uses `robot_lab-durable`. Note the API: `learn:` / `learn_domain:` are **not** constructor parameters (`Robot#initialize` takes a fixed keyword list with no `**rest`, so passing them raises `ArgumentError`). Call `robot.setup_durable_learning(domain:)` after `build`, which seeds `robot.learnings` from `~/.robot_lab/durable/<domain>.yml` and appends the `RecallKnowledge` / `RecordKnowledge` tools. Core `Robot#run` does not invoke the reflector, so the caller drives `robot.run_reflector` to promote learnings back to the store.
434
+
435
+ Run the two scripts in separate terminals.
436
+
437
+ **Requires:** Ollama, Redis on localhost:6379, `robot_lab-durable`
438
+
439
+ ### 34 — AgentSkills.io Integration
440
+
441
+ Skills declared as `skills: [:code_reviewer]` are resolved from `~/.prompts/skills/<name>/SKILL.md` and matched per-run by embedding similarity — a code-review question activates the skill, an unrelated question does not. Run the two queries and compare.
442
+
443
+ **Requires:** Ollama, `robot_lab-document_store` gem, a `SKILL.md` at `~/.prompts/skills/code_reviewer/`
444
+
445
+ ### 35 — Hooks Architecture
446
+
447
+ Loads `xyzzy.rb`, a single-file `RobotLab::Hook` subclass that registers for every hook and logs each callback with the context it receives. Demonstrates robot run, LLM generation, tool call, network run, task, and error hooks. Stubs `robot.chat.ask` for the deterministic sections; the LLM-generation section makes four real calls to show `around_llm_generation` serving two of them from a cache.
448
+
449
+ `RobotLab::Hook` has no logger accessor — handlers own their output. `xyzzy.rb` freezes its log destination from `XYZZY_LOG_PATH` at load time, so the env var must be set before the `require`.
450
+
451
+ **Requires:** Ollama (four calls in the LLM-loop section)
308
452
 
309
453
  ### 18 — Rails Integration Demo
310
454
 
@@ -319,13 +463,15 @@ A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails
319
463
 
320
464
  **No Redis, no Solid Queue, no asset pipeline.** Uses `:async` adapters for both ActiveJob and ActionCable. Turbo JS loaded via importmap from CDN (`@hotwired/turbo-rails`).
321
465
 
466
+ **Two API details worth copying:** the job's superclass is `RobotLab::RailsIntegration::Job` (there is no `RobotLab::Job` alias), and the controller enqueues with `tools: :inherit` so that keyword reaches `robot.run` — otherwise `TimeTool` is never offered and the model guesses the time.
467
+
322
468
  ```bash
323
469
  cd examples/18_rails
324
470
  bin/setup # bundle install + db:create + db:migrate
325
471
  bin/dev # starts Puma on http://localhost:3000
326
472
  ```
327
473
 
328
- **Requires:** LLM API key, Ruby 3.2+
474
+ **Requires:** Ollama, Ruby 3.2+
329
475
 
330
476
  ## Prompt Templates
331
477
 
@@ -370,3 +516,18 @@ Front matter keys like `model`, `temperature`, `top_p`, `max_tokens` are applied
370
516
  | `open_mic_comic.md` | 14 | Observational comedian with self-modification |
371
517
  | `open_mic_heckler.md` | 14 | Tough audience heckler (can stay silent or counter-joke) |
372
518
  | `open_mic_scout.md` | 14 | Talent scout with analyst recruitment |
519
+ | `os_advocate.md` | 15 | Operating-system advocacy writer |
520
+ | `os_editor.md` | 15 | Synthesizes the three advocacy drafts |
521
+ | `os_chief.md` | 15 | Editor-in-chief (APPROVED / REVISE) |
522
+ | `writer.md` | 16 | Self-organizing novella writer |
523
+ | `screenplay_writer.md` | 16 | Self-organizing screenplay writer |
524
+ | `incident_responder.md` | 17 | SRE on-call analyst (main template) |
525
+ | `runbook_protocol.md` | 17 | Flat skill: 5-step incident protocol |
526
+ | `structured_output.md` | 17 | Flat skill: JSON response format |
527
+ | `sre_compliance.md` | 17 | Recursive skill: bundles the two leaves below |
528
+ | `pii_redactor.md` | 17 | Leaf skill: redact PII |
529
+ | `audit_trail.md` | 17 | Leaf skill: audit metadata |
530
+
531
+ Templates matching `*_test.md` are fixtures for the test suite, not examples.
532
+
533
+ Templates for 14, 15 and 16 live in those examples' own `prompts/` directories; each entrypoint sets `ROBOT_LAB_TEMPLATE_PATH` before requiring `common.rb`.