robot_lab 0.0.12 → 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 (243) 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 +108 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -45
  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/api/messages/index.md +21 -0
  19. data/docs/examples/index.md +37 -2
  20. data/docs/getting-started/configuration.md +20 -7
  21. data/docs/guides/creating-networks.md +23 -0
  22. data/docs/guides/index.md +16 -16
  23. data/docs/guides/knowledge.md +7 -1
  24. data/docs/guides/observability.md +132 -0
  25. data/docs/index.md +30 -3
  26. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  27. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  28. data/examples/.envrc +1 -0
  29. data/examples/01_simple_robot.rb +5 -9
  30. data/examples/02_tools.rb +5 -9
  31. data/examples/03_network.rb +8 -9
  32. data/examples/04_mcp.rb +21 -29
  33. data/examples/05_streaming.rb +12 -18
  34. data/examples/06_prompt_templates.rb +11 -19
  35. data/examples/07_network_memory.rb +16 -31
  36. data/examples/08_llm_config.rb +10 -22
  37. data/examples/09_chaining.rb +16 -27
  38. data/examples/10_memory.rb +12 -28
  39. data/examples/11_network_introspection.rb +15 -29
  40. data/examples/12_message_bus.rb +5 -12
  41. data/examples/13_spawn.rb +5 -10
  42. data/examples/14_rusty_circuit/.envrc +1 -0
  43. data/examples/14_rusty_circuit/comic.rb +2 -0
  44. data/examples/14_rusty_circuit/heckler.rb +1 -1
  45. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  46. data/examples/14_rusty_circuit/scout.rb +2 -0
  47. data/examples/15_memory_network_and_bus/.envrc +1 -0
  48. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  49. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  50. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  51. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  52. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  53. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  54. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  55. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  56. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  57. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  58. data/examples/16_writers_room/.envrc +1 -0
  59. data/examples/16_writers_room/writers_room.rb +2 -4
  60. data/examples/17_skills.rb +8 -17
  61. data/examples/18_rails/Gemfile +1 -0
  62. data/examples/18_rails/app/jobs/robot_run_job.rb +15 -75
  63. data/examples/19_token_tracking.rb +9 -15
  64. data/examples/20_circuit_breaker.rb +10 -19
  65. data/examples/21_learning_loop.rb +11 -20
  66. data/examples/22_context_compression.rb +6 -13
  67. data/examples/23_convergence.rb +6 -17
  68. data/examples/24_structured_delegation.rb +11 -15
  69. data/examples/25_history_search.rb +5 -12
  70. data/examples/26_document_store.rb +6 -13
  71. data/examples/27_incident_response/incident_response.rb +4 -5
  72. data/examples/28_mcp_discovery.rb +8 -11
  73. data/examples/29_ractor_tools.rb +4 -9
  74. data/examples/30_ractor_network.rb +10 -19
  75. data/examples/31_launch_assessment.rb +235 -0
  76. data/examples/32_newsletter_reader.rb +188 -0
  77. data/examples/33_stock_generator.rb +80 -0
  78. data/examples/33_stock_predictor.rb +306 -0
  79. data/examples/34_agentskills.rb +72 -0
  80. data/examples/README.md +10 -1
  81. data/examples/common.rb +76 -0
  82. data/examples/ruboruby.md +423 -0
  83. data/examples/temp.md +51 -0
  84. data/lib/robot_lab/agent_skill.rb +63 -0
  85. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  86. data/lib/robot_lab/ask_user.rb +2 -2
  87. data/lib/robot_lab/bus_poller.rb +12 -5
  88. data/lib/robot_lab/config.rb +1 -12
  89. data/lib/robot_lab/delegation_future.rb +1 -1
  90. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  91. data/lib/robot_lab/history_compressor.rb +4 -10
  92. data/lib/robot_lab/mcp/client.rb +1 -2
  93. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  94. data/lib/robot_lab/mcp/server.rb +1 -1
  95. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  96. data/lib/robot_lab/memory.rb +32 -27
  97. data/lib/robot_lab/memory_change.rb +2 -2
  98. data/lib/robot_lab/message.rb +5 -5
  99. data/lib/robot_lab/network.rb +12 -7
  100. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  101. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  102. data/lib/robot_lab/robot/history_search.rb +4 -1
  103. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  104. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  105. data/lib/robot_lab/robot.rb +323 -206
  106. data/lib/robot_lab/robot_result.rb +6 -5
  107. data/lib/robot_lab/run_config.rb +5 -11
  108. data/lib/robot_lab/script_tool.rb +76 -0
  109. data/lib/robot_lab/state_proxy.rb +7 -5
  110. data/lib/robot_lab/tool.rb +3 -3
  111. data/lib/robot_lab/tool_config.rb +1 -1
  112. data/lib/robot_lab/tool_manifest.rb +5 -7
  113. data/lib/robot_lab/user_message.rb +2 -2
  114. data/lib/robot_lab/version.rb +1 -1
  115. data/lib/robot_lab/waiter.rb +1 -1
  116. data/lib/robot_lab.rb +41 -48
  117. data/logfile +8 -0
  118. data/mkdocs.yml +2 -3
  119. data/robot_concurrency.md +38 -0
  120. data/simple_acp_review.md +298 -0
  121. data/site/404.html +2300 -0
  122. data/site/api/core/index.html +2706 -0
  123. data/site/api/core/memory/index.html +3793 -0
  124. data/site/api/core/network/index.html +3500 -0
  125. data/site/api/core/robot/index.html +4566 -0
  126. data/site/api/core/state/index.html +3390 -0
  127. data/site/api/core/tool/index.html +3843 -0
  128. data/site/api/index.html +2635 -0
  129. data/site/api/mcp/client/index.html +3435 -0
  130. data/site/api/mcp/index.html +2783 -0
  131. data/site/api/mcp/server/index.html +3252 -0
  132. data/site/api/mcp/transports/index.html +3352 -0
  133. data/site/api/messages/index.html +2641 -0
  134. data/site/api/messages/text-message/index.html +3087 -0
  135. data/site/api/messages/tool-call-message/index.html +3159 -0
  136. data/site/api/messages/tool-result-message/index.html +3252 -0
  137. data/site/api/messages/user-message/index.html +3212 -0
  138. data/site/api/streaming/context/index.html +3282 -0
  139. data/site/api/streaming/events/index.html +3347 -0
  140. data/site/api/streaming/index.html +2738 -0
  141. data/site/architecture/core-concepts/index.html +3757 -0
  142. data/site/architecture/index.html +2797 -0
  143. data/site/architecture/message-flow/index.html +3238 -0
  144. data/site/architecture/network-orchestration/index.html +3433 -0
  145. data/site/architecture/robot-execution/index.html +3140 -0
  146. data/site/architecture/state-management/index.html +3498 -0
  147. data/site/assets/css/custom.css +56 -0
  148. data/site/assets/images/favicon.png +0 -0
  149. data/site/assets/images/robot_lab.jpg +0 -0
  150. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  151. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  152. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  153. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  154. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  157. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  158. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  159. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  162. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  163. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  168. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  169. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  170. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  173. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  174. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  178. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  179. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  181. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  182. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  183. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  184. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  185. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  186. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  187. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  188. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  189. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  190. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  191. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  192. data/site/concepts/index.html +3455 -0
  193. data/site/examples/basic-chat/index.html +2880 -0
  194. data/site/examples/index.html +2907 -0
  195. data/site/examples/mcp-server/index.html +3018 -0
  196. data/site/examples/multi-robot-network/index.html +3131 -0
  197. data/site/examples/rails-application/index.html +3329 -0
  198. data/site/examples/tool-usage/index.html +3085 -0
  199. data/site/getting-started/configuration/index.html +3745 -0
  200. data/site/getting-started/index.html +2572 -0
  201. data/site/getting-started/installation/index.html +2981 -0
  202. data/site/getting-started/quick-start/index.html +2942 -0
  203. data/site/guides/building-robots/index.html +4290 -0
  204. data/site/guides/creating-networks/index.html +3858 -0
  205. data/site/guides/index.html +2586 -0
  206. data/site/guides/mcp-integration/index.html +3581 -0
  207. data/site/guides/memory/index.html +3586 -0
  208. data/site/guides/rails-integration/index.html +4019 -0
  209. data/site/guides/streaming/index.html +3157 -0
  210. data/site/guides/using-tools/index.html +3802 -0
  211. data/site/index.html +2671 -0
  212. data/site/search/search_index.json +1 -0
  213. data/site/sitemap.xml +183 -0
  214. data/site/sitemap.xml.gz +0 -0
  215. data/site/tags.json +1 -0
  216. data/temp.md +6 -0
  217. data/tool_manifest_plan.md +155 -0
  218. metadata +155 -90
  219. data/.github/workflows/deploy-yard-docs.yml +0 -52
  220. data/docs/examples/rails-application.md +0 -419
  221. data/docs/guides/ractor-parallelism.md +0 -364
  222. data/docs/guides/rails-integration.md +0 -642
  223. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  224. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  225. data/lib/generators/robot_lab/install_generator.rb +0 -90
  226. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  227. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  228. data/lib/generators/robot_lab/templates/job.rb.tt +0 -92
  229. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  230. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  231. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  232. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  233. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  234. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  235. data/lib/robot_lab/document_store.rb +0 -155
  236. data/lib/robot_lab/ractor_boundary.rb +0 -42
  237. data/lib/robot_lab/ractor_job.rb +0 -37
  238. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  239. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  240. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  241. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  242. data/lib/robot_lab/rails_integration/railtie.rb +0 -42
  243. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 32: Newsletter Issue Retriever
5
+ #
6
+ # Fetches all unprocessed issues from multiple Ruby newsletters via RSS
7
+ # and saves each as a Markdown file in the Obsidian Clippings folder.
8
+ #
9
+ # - Processes oldest unprocessed issue first (across all feeds)
10
+ # - Filename: <newsletter-name>_YYYYMMDD.md
11
+ # - Includes YAML frontmatter with source URL (compatible with Clippings workflow)
12
+ # - Tracks processed issue URLs in ~/.robot_lab/newsletter_processed.yaml
13
+ #
14
+ # Usage:
15
+ # ruby examples/32_newsletter_reader.rb
16
+
17
+ require "net/http"
18
+ require "open3"
19
+ require "time"
20
+ require "uri"
21
+ require "yaml"
22
+ require "fileutils"
23
+ require "rexml/document"
24
+ require "rexml/xpath"
25
+
26
+ NEWSLETTER_RSS_URLS = [
27
+ "https://rss.beehiiv.com/feeds/MTJunJRFxo.xml", # Ruby AI News
28
+ "https://cprss.s3.amazonaws.com/rubyweekly.com.xml" # Ruby Weekly
29
+ ].freeze
30
+
31
+ CLIPPINGS_DIR = File.expand_path("/Users/dewayne/Documents/obsidian_order_intelligence/PKM/Clippings")
32
+ PROCESSED_STATE_FILE = File.join(Dir.home, ".robot_lab", "newsletter_processed.yaml")
33
+
34
+ # Tracks which newsletter issue URLs have been saved.
35
+ class ProcessedIssues
36
+ def initialize(path: PROCESSED_STATE_FILE)
37
+ @path = path
38
+ @urls = load_urls
39
+ end
40
+
41
+ def processed?(url)
42
+ @urls.include?(url)
43
+ end
44
+
45
+ def mark_processed(url)
46
+ @urls << url
47
+ FileUtils.mkdir_p(File.dirname(@path))
48
+ File.write(@path, YAML.dump(@urls.uniq))
49
+ end
50
+
51
+ def count = @urls.size
52
+
53
+ private
54
+
55
+ def load_urls
56
+ return [] unless File.exist?(@path)
57
+ Array(YAML.safe_load(File.read(@path)) || [])
58
+ end
59
+ end
60
+
61
+ # Fetches a single RSS feed URL and returns its items (unsorted).
62
+ # Each item: { title:, url:, pub_date:, published_at:, html:, channel_name: }
63
+ def fetch_rss_items(url)
64
+ uri = URI(url)
65
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.get(uri.request_uri) }
66
+ raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
67
+
68
+ doc = REXML::Document.new(response.body)
69
+ ns = { "content" => "http://purl.org/rss/1.0/modules/content/" }
70
+
71
+ channel_title = REXML::XPath.first(doc, "//channel/title")&.text&.strip || "newsletter"
72
+
73
+ REXML::XPath.match(doc, "//channel/item").filter_map do |item|
74
+ item_url = REXML::XPath.first(item, "link")&.text&.strip
75
+ title = REXML::XPath.first(item, "title")&.text&.strip
76
+ pub_date = REXML::XPath.first(item, "pubDate")&.text&.strip
77
+ html = REXML::XPath.first(item, "content:encoded", ns)&.text ||
78
+ REXML::XPath.first(item, "description")&.text || ""
79
+
80
+ next unless item_url && title && pub_date
81
+
82
+ {
83
+ title: title,
84
+ url: item_url,
85
+ pub_date: pub_date,
86
+ published_at: Time.parse(pub_date),
87
+ html: html,
88
+ channel_name: channel_title
89
+ }
90
+ end
91
+ end
92
+
93
+ # Fetches all configured RSS feeds and returns items sorted oldest-first.
94
+ def fetch_all_rss_items
95
+ NEWSLETTER_RSS_URLS.flat_map do |feed_url|
96
+ print " #{feed_url}... "
97
+ items = fetch_rss_items(feed_url)
98
+ puts "#{items.size} issues"
99
+ items
100
+ end.sort_by { |item| item[:published_at] }
101
+ end
102
+
103
+ # Converts HTML to Markdown via html2markdown CLI, stripping UTM params.
104
+ def html_to_markdown(html)
105
+ md, = Open3.capture3(
106
+ "html2markdown",
107
+ stdin_data: html
108
+ )
109
+ md.gsub(/\]\(([^)]+)\)/) do
110
+ url = $1
111
+ if url.include?("?")
112
+ base, query = url.split("?", 2)
113
+ kept = query.split("&").reject { |p| p.start_with?("utm_") }
114
+ "](#{kept.empty? ? base : "#{base}?#{kept.join("&")}"})"
115
+ else
116
+ "](#{url})"
117
+ end
118
+ end
119
+ end
120
+
121
+ # Builds the output filename: <newsletter-name>_YYYYMMDD.md
122
+ def output_filename(channel_name, published_at)
123
+ safe_name = channel_name.downcase.gsub(/[^a-z0-9]+/, "_").delete_suffix("_")
124
+ date_str = published_at.strftime("%Y%m%d")
125
+ "#{safe_name}_#{date_str}.md"
126
+ end
127
+
128
+ # Wraps markdown content in YAML frontmatter compatible with the Clippings workflow.
129
+ def wrap_with_frontmatter(title:, url:, pub_date:, body:)
130
+ date = Time.parse(pub_date).strftime("%Y-%m-%d")
131
+ <<~MD
132
+ ---
133
+ source: #{url}
134
+ title: "#{title.gsub('"', '\\"')}"
135
+ date: #{date}
136
+ ---
137
+
138
+ # #{title}
139
+
140
+ #{body.strip}
141
+ MD
142
+ end
143
+
144
+ # ── Main ──────────────────────────────────────────────────────────────────────
145
+
146
+ puts "=" * 60
147
+ puts "Example 32: Newsletter Issue Retriever"
148
+ puts "=" * 60
149
+ puts
150
+
151
+ FileUtils.mkdir_p(CLIPPINGS_DIR)
152
+ state = ProcessedIssues.new
153
+
154
+ puts "Fetching #{NEWSLETTER_RSS_URLS.size} RSS feeds..."
155
+ all_items = fetch_all_rss_items
156
+ pending = all_items.reject { |item| state.processed?(item[:url]) }
157
+ puts "#{all_items.size} total issues found across all feeds."
158
+
159
+ if pending.empty?
160
+ puts "All issues already saved. Nothing to do."
161
+ exit
162
+ end
163
+
164
+ puts "#{pending.size} unprocessed (#{state.count} already done). Saving oldest-first."
165
+ puts
166
+
167
+ pending.each_with_index do |item, idx|
168
+ filename = output_filename(item[:channel_name], item[:published_at])
169
+ filepath = File.join(CLIPPINGS_DIR, filename)
170
+
171
+ print "[#{idx + 1}/#{pending.size}] #{item[:title]} (#{item[:published_at].strftime("%Y-%m-%d")})... "
172
+
173
+ body = html_to_markdown(item[:html])
174
+ content = wrap_with_frontmatter(
175
+ title: item[:title],
176
+ url: item[:url],
177
+ pub_date: item[:pub_date],
178
+ body: body
179
+ )
180
+
181
+ File.write(filepath, content)
182
+ state.mark_processed(item[:url])
183
+
184
+ puts "saved → #{filename}"
185
+ end
186
+
187
+ puts
188
+ puts "Done. #{pending.size} issue(s) saved to #{CLIPPINGS_DIR}"
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 33: XYZZY Stock Price Generator
5
+ #
6
+ # Publishes fake streaming prices for ticker XYZZY to a Redis channel
7
+ # using Geometric Brownian Motion with occasional volatility regime shifts.
8
+ #
9
+ # Prerequisites:
10
+ # gem install redis
11
+ # Redis server running on localhost:6379
12
+ #
13
+ # Usage:
14
+ # ruby examples/33_stock_generator.rb
15
+
16
+ require "redis"
17
+ require "json"
18
+ require "time"
19
+
20
+ CHANNEL = "stock:xyzzy"
21
+ START_PRICE = 100.0
22
+ BASE_VOL = 0.008 # baseline volatility per tick (~0.8%)
23
+ DRIFT = 0.0001 # slight upward drift per tick
24
+
25
+ # Box-Muller transform — standard normal sample
26
+ def randn
27
+ Math.sqrt(-2.0 * Math.log(rand)) * Math.cos(2.0 * Math::PI * rand)
28
+ end
29
+
30
+ # Occasionally shift the volatility regime to create interesting price dynamics
31
+ def current_volatility(tick)
32
+ case tick % 60
33
+ when 0..10 then BASE_VOL * 2.0 # high volatility burst
34
+ when 30..35 then BASE_VOL * 0.4 # low volatility squeeze
35
+ else BASE_VOL
36
+ end
37
+ end
38
+
39
+ # ── Main ──────────────────────────────────────────────────────────────────────
40
+
41
+ redis = Redis.new
42
+ price = START_PRICE
43
+ tick = 0
44
+
45
+ trap("INT") { puts "\nGenerator stopped."; exit }
46
+
47
+ puts "=" * 50
48
+ puts "XYZZY Stock Generator"
49
+ puts "=" * 50
50
+ puts "Channel : #{CHANNEL}"
51
+ puts "Interval: 5 seconds per tick"
52
+ puts "Model : Geometric Brownian Motion"
53
+ puts "Press Ctrl-C to stop."
54
+ puts "-" * 50
55
+
56
+ loop do
57
+ tick += 1
58
+
59
+ vol = current_volatility(tick)
60
+ price = (price * Math.exp((DRIFT - 0.5 * vol**2) + vol * randn)).round(2)
61
+ price = [price, 1.0].max # floor at $1.00
62
+
63
+ regime = case vol
64
+ when BASE_VOL * 2.0 then " [HIGH VOL]"
65
+ when BASE_VOL * 0.4 then " [low vol]"
66
+ else ""
67
+ end
68
+
69
+ payload = JSON.generate(
70
+ ticker: "XYZZY",
71
+ price: price,
72
+ tick: tick,
73
+ timestamp: Time.now.iso8601
74
+ )
75
+
76
+ redis.publish(CHANNEL, payload)
77
+ puts "Tick %5d $%8.2f vol=%.3f%s" % [tick, price, vol, regime]
78
+
79
+ sleep 5
80
+ end
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 33: XYZZY Stock Price Predictor
5
+ #
6
+ # Consumes fake streaming prices for ticker XYZZY from a Redis channel,
7
+ # predicts the high and low over the next price window using an SMA + EMA
8
+ # ensemble, and uses a RobotLab learning robot to tune predictor parameters
9
+ # after each window closes.
10
+ #
11
+ # Run alongside:
12
+ # ruby examples/33_stock_generator.rb (in a separate terminal)
13
+ #
14
+ # Prerequisites:
15
+ # gem install redis
16
+ # Redis server running on localhost:6379
17
+ #
18
+ # Usage:
19
+ # ruby examples/33_stock_predictor.rb
20
+
21
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
22
+ require "robot_lab"
23
+ require "robot_lab/durable"
24
+ require "redis"
25
+ require "json"
26
+
27
+ CHANNEL = "stock:xyzzy"
28
+ WINDOW_SIZE = 12 # ticks per prediction window
29
+
30
+ # ── Mutable predictor parameters ──────────────────────────────────────────────
31
+
32
+ module PredictorConfig
33
+ @sma_window = 10
34
+ @sma_std_multiplier = 1.5
35
+ @ema_alpha = 0.2
36
+ @ema_vol_multiplier = 2.0
37
+ @sma_weight = 0.5
38
+
39
+ class << self
40
+ attr_accessor :sma_window, :sma_std_multiplier, :ema_alpha,
41
+ :ema_vol_multiplier, :sma_weight
42
+
43
+ def summary
44
+ format(
45
+ "sma_window=%d sma_std=%.2f ema_alpha=%.2f ema_vol=%.2f sma_weight=%.2f",
46
+ sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ # ── SMA predictor ──────────────────────────────────────────────────────────────
53
+
54
+ module SMAPredictor
55
+ def self.predict(prices)
56
+ window = prices.last(PredictorConfig.sma_window)
57
+ mean = window.sum / window.size.to_f
58
+ var = window.sum { |p| (p - mean)**2 } / window.size.to_f
59
+ std = Math.sqrt(var)
60
+ mult = PredictorConfig.sma_std_multiplier
61
+
62
+ {
63
+ high: (mean + mult * std).round(2),
64
+ low: [mean - mult * std, 1.0].max.round(2)
65
+ }
66
+ end
67
+ end
68
+
69
+ # ── EMA predictor (stateful — updated every tick) ──────────────────────────────
70
+
71
+ module EMAPredictor
72
+ @ema = nil
73
+ @var_ema = nil
74
+
75
+ class << self
76
+ def update(price)
77
+ alpha = PredictorConfig.ema_alpha
78
+ if @ema.nil?
79
+ @ema = price
80
+ @var_ema = 0.0
81
+ else
82
+ delta = price - @ema
83
+ @ema = alpha * price + (1 - alpha) * @ema
84
+ @var_ema = alpha * delta**2 + (1 - alpha) * @var_ema
85
+ end
86
+ end
87
+
88
+ def predict
89
+ return nil if @ema.nil?
90
+
91
+ vol = Math.sqrt(@var_ema) * PredictorConfig.ema_vol_multiplier
92
+ {
93
+ high: (@ema + vol).round(2),
94
+ low: [@ema - vol, 1.0].max.round(2)
95
+ }
96
+ end
97
+ end
98
+ end
99
+
100
+ # ── Ensemble predictor ─────────────────────────────────────────────────────────
101
+
102
+ module EnsemblePredictor
103
+ def self.predict(prices)
104
+ sma = SMAPredictor.predict(prices)
105
+ ema = EMAPredictor.predict
106
+ return sma unless ema
107
+
108
+ w = PredictorConfig.sma_weight
109
+ {
110
+ high: (w * sma[:high] + (1 - w) * ema[:high]).round(2),
111
+ low: (w * sma[:low] + (1 - w) * ema[:low]).round(2)
112
+ }
113
+ end
114
+ end
115
+
116
+ # ── AdjustParameters tool ──────────────────────────────────────────────────────
117
+
118
+ class AdjustParameters < RobotLab::Tool
119
+ description "Adjust one predictor parameter to improve future prediction accuracy. " \
120
+ "Make at most one or two targeted changes per window."
121
+
122
+ param :parameter, type: "string",
123
+ desc: "Parameter to adjust: sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight"
124
+ param :value, type: "number",
125
+ desc: "New value (sma_window: 3-30 int; std/vol multipliers: 0.5-4.0; ema_alpha: 0.05-0.5; sma_weight: 0.0-1.0)"
126
+ param :reasoning, type: "string",
127
+ desc: "Why this change should reduce prediction error"
128
+
129
+ LIMITS = {
130
+ "sma_window" => { min: 3, max: 30, integer: true },
131
+ "sma_std_multiplier" => { min: 0.5, max: 4.0, integer: false },
132
+ "ema_alpha" => { min: 0.05, max: 0.5, integer: false },
133
+ "ema_vol_multiplier" => { min: 0.5, max: 4.0, integer: false },
134
+ "sma_weight" => { min: 0.0, max: 1.0, integer: false }
135
+ }.freeze
136
+
137
+ def execute(parameter:, value:, reasoning:)
138
+ spec = LIMITS[parameter]
139
+ return "Unknown parameter '#{parameter}'. Valid: #{LIMITS.keys.join(", ")}" unless spec
140
+
141
+ clamped = value.to_f.clamp(spec[:min], spec[:max])
142
+ clamped = clamped.round if spec[:integer]
143
+
144
+ PredictorConfig.send(:"#{parameter}=", clamped)
145
+
146
+ "Set #{parameter} = #{clamped}. #{reasoning}"
147
+ end
148
+ end
149
+
150
+ # ── Error metrics ──────────────────────────────────────────────────────────────
151
+
152
+ WindowResult = Data.define(
153
+ :window_num,
154
+ :predicted_high, :predicted_low,
155
+ :actual_high, :actual_low,
156
+ :high_err, :low_err, :mean_err
157
+ )
158
+
159
+ def evaluate_window(window_num, predicted, actuals)
160
+ actual_high = actuals.max.round(2)
161
+ actual_low = actuals.min.round(2)
162
+ high_err = (predicted[:high] - actual_high).abs.round(2)
163
+ low_err = (predicted[:low] - actual_low).abs.round(2)
164
+ mean_err = ((high_err + low_err) / 2.0).round(2)
165
+
166
+ WindowResult.new(
167
+ window_num:,
168
+ predicted_high: predicted[:high], predicted_low: predicted[:low],
169
+ actual_high:, actual_low:,
170
+ high_err:, low_err:, mean_err:
171
+ )
172
+ end
173
+
174
+ def tuner_prompt(result)
175
+ <<~PROMPT
176
+ Window #{result.window_num} just closed.
177
+
178
+ Prediction vs Actual:
179
+ Predicted: high=$#{result.predicted_high} low=$#{result.predicted_low}
180
+ Actual: high=$#{result.actual_high} low=$#{result.actual_low}
181
+ Error: high_err=$#{result.high_err} low_err=$#{result.low_err} mean_err=$#{result.mean_err}
182
+
183
+ Current parameters:
184
+ #{PredictorConfig.summary}
185
+
186
+ Window size: #{WINDOW_SIZE} ticks.
187
+
188
+ First call RecallKnowledge to check what has worked before.
189
+ Then decide whether to adjust a parameter via AdjustParameters.
190
+ If the error is acceptable or you are uncertain, do nothing.
191
+ If you notice a clear pattern worth preserving, call RecordKnowledge.
192
+ PROMPT
193
+ end
194
+
195
+ # ── Main ──────────────────────────────────────────────────────────────────────
196
+
197
+ puts "=" * 60
198
+ puts "XYZZY Stock Predictor"
199
+ puts "=" * 60
200
+ puts "Channel : #{CHANNEL}"
201
+ puts "Window : #{WINDOW_SIZE} ticks"
202
+ puts "Model : SMA + EMA Ensemble with Durable Learning"
203
+ puts "Warmup : #{PredictorConfig.sma_window} ticks"
204
+ puts "Press Ctrl-C to stop."
205
+ puts "-" * 60
206
+
207
+ redis = Redis.new
208
+ prices = []
209
+ robot = RobotLab.build(
210
+ model: "gpt-5.4",
211
+ name: "predictor_tuner",
212
+ system_prompt: <<~PROMPT,
213
+ You are a quantitative analyst tuning an ensemble stock price range
214
+ predictor for ticker XYZZY. Each prediction covers the high and low
215
+ price over the next #{WINDOW_SIZE} ticks.
216
+
217
+ The ensemble combines a Simple Moving Average (SMA) band and an
218
+ Exponential Moving Average (EMA) band. Adjustable parameters:
219
+
220
+ sma_window (3-30 int) — lookback period for SMA
221
+ sma_std_multiplier (0.5-4.0) — band width relative to SMA stddev
222
+ ema_alpha (0.05-0.5) — EMA smoothing (higher = more reactive)
223
+ ema_vol_multiplier (0.5-4.0) — band width relative to EMA volatility
224
+ sma_weight (0.0-1.0) — SMA share in ensemble (EMA = 1 - weight)
225
+
226
+ Workflow per window:
227
+ 1. Call RecallKnowledge to check past findings before acting.
228
+ 2. If the error is clearly too high/low in one direction, adjust the
229
+ relevant band multiplier via AdjustParameters.
230
+ 3. Make at most two adjustments per window to isolate cause and effect.
231
+ 4. If you observe a reliable pattern, call RecordKnowledge to preserve it.
232
+ 5. When uncertain, do nothing rather than guess.
233
+ PROMPT
234
+ local_tools: [AdjustParameters],
235
+ learn: true,
236
+ learn_domain: "xyzzy stock prediction"
237
+ )
238
+
239
+ warmed_up = false
240
+ pending_pred = nil # { prediction: {high:, low:}, window_prices: [] }
241
+ window_num = 0
242
+
243
+ trap("INT") { puts "\nPredictor stopped."; exit }
244
+
245
+ puts "Connecting to Redis and subscribing to #{CHANNEL}..."
246
+
247
+ redis.subscribe(CHANNEL) do |on|
248
+ on.message do |_channel, payload|
249
+ data = JSON.parse(payload, symbolize_names: true)
250
+ tick = data[:tick]
251
+ price = data[:price].to_f
252
+
253
+ EMAPredictor.update(price)
254
+ prices << price
255
+
256
+ # ── Warmup phase ──────────────────────────────────────────────
257
+ unless warmed_up
258
+ if prices.size < PredictorConfig.sma_window
259
+ puts "Tick %5d $%8.2f [warming up %d/%d]" % [tick, price, prices.size, PredictorConfig.sma_window]
260
+ next
261
+ end
262
+
263
+ warmed_up = true
264
+ pred = EnsemblePredictor.predict(prices)
265
+ pending_pred = { prediction: pred, window_prices: [] }
266
+
267
+ puts "Tick %5d $%8.2f [warmup done]" % [tick, price]
268
+ puts " First prediction → high=$#{pred[:high]} low=$#{pred[:low]}"
269
+ next
270
+ end
271
+
272
+ # ── Accumulate current window ──────────────────────────────────
273
+ pending_pred[:window_prices] << price
274
+ progress = pending_pred[:window_prices].size
275
+ pred = pending_pred[:prediction]
276
+
277
+ puts "Tick %5d $%8.2f [%2d/#{WINDOW_SIZE}] (pred high=$#{pred[:high]} low=$#{pred[:low]})" %
278
+ [tick, price, progress]
279
+
280
+ next unless progress >= WINDOW_SIZE
281
+
282
+ # ── Window closed — evaluate ───────────────────────────────────
283
+ window_num += 1
284
+ result = evaluate_window(window_num, pred, pending_pred[:window_prices])
285
+
286
+ puts "\n#{"─" * 60}"
287
+ puts " Window #{result.window_num} result:"
288
+ puts " Predicted high=$%-8.2f low=$%-.2f" % [result.predicted_high, result.predicted_low]
289
+ puts " Actual high=$%-8.2f low=$%-.2f" % [result.actual_high, result.actual_low]
290
+ puts " Error high=%-8.2f low=%-8.2f mean=%.2f" % [result.high_err, result.low_err, result.mean_err]
291
+ puts "#{"─" * 60}"
292
+
293
+ print " [tuner] analyzing window #{window_num}..."
294
+ tuner_response = robot.run(tuner_prompt(result))
295
+ tuner_line = tuner_response.reply.lines.first&.chomp || "(no response)"
296
+ puts "\r [tuner] #{tuner_line}#{" " * 20}"
297
+ puts " Params: #{PredictorConfig.summary}"
298
+ puts
299
+
300
+ # ── Start next window ──────────────────────────────────────────
301
+ new_pred = EnsemblePredictor.predict(prices)
302
+ pending_pred = { prediction: new_pred, window_prices: [] }
303
+ puts " Next prediction → high=$#{new_pred[:high]} low=$#{new_pred[:low]}"
304
+ puts
305
+ end
306
+ end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 34: AgentSkills.io Integration
5
+ #
6
+ # Demonstrates the unified skills: param detecting AgentSkills folder format.
7
+ # Skills in ~/.prompts/skills/ are matched at runtime via embedding similarity
8
+ # before each run() call — only relevant skills are injected.
9
+ #
10
+ # Usage:
11
+ # mkdir -p ~/.prompts/skills/code_reviewer
12
+ # # (create SKILL.md as shown in the example header)
13
+ # ANTHROPIC_API_KEY=your_key ruby examples/34_agentskills.rb
14
+
15
+ require_relative "common"
16
+
17
+ require "logger"
18
+ log_file = File.join(__dir__, "34.log")
19
+ RobotLab.config.logger = Logger.new(log_file)
20
+ RubyLLM.configure { |c| c.logger = Logger.new(log_file) }
21
+
22
+ banner "RobotLab — AgentSkills.io Integration Demo"
23
+
24
+ # Check if the skill is installed
25
+ skill_path = File.expand_path("~/.prompts/skills/code_reviewer/SKILL.md")
26
+ unless File.exist?(skill_path)
27
+ puts "Demo skill not found at #{skill_path}"
28
+ puts "Create it with:"
29
+ puts " mkdir -p ~/.prompts/skills/code_reviewer"
30
+ puts " # Then add SKILL.md with name: code_reviewer"
31
+ exit 1
32
+ end
33
+
34
+ # Build a robot that lists code_reviewer as a candidate skill.
35
+ # At runtime, if the user message is semantically similar to
36
+ # "Review Ruby code for quality, style, and potential bugs",
37
+ # the skill's instructions are injected into the system prompt.
38
+ robot = RobotLab.build(
39
+ model: LLM[:default].model,
40
+ name: "assistant",
41
+ system_prompt: "You are a helpful Ruby programming assistant.",
42
+ skills: [:code_reviewer]
43
+ )
44
+
45
+ puts "Pending AgentSkills: #{robot.instance_variable_get(:@pending_agent_skills).map(&:name).inspect}"
46
+ puts
47
+
48
+ # Message semantically related to code review — skill should activate
49
+ code_question = <<~MSG
50
+ Please review this Ruby method for quality issues:
51
+
52
+ def process(data)
53
+ begin
54
+ result = data.map { |item| transform(item) }
55
+ save(result)
56
+ rescue => e
57
+ puts e.message
58
+ end
59
+ end
60
+ MSG
61
+
62
+ puts "Query: code review (skill should activate)"
63
+ result = robot.run(code_question)
64
+ puts result.reply
65
+ puts
66
+ hr
67
+
68
+ # Message unrelated to code review — skill should NOT activate
69
+ puts "Query: general question (skill should NOT activate)"
70
+ result = robot.run("What is the capital of France?")
71
+ puts result.reply
72
+ puts
data/examples/README.md CHANGED
@@ -57,6 +57,7 @@ examples/
57
57
  26_document_store.rb # Embedding-based document store (RAG) via fastembed
58
58
  29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
59
59
  30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
60
+ 31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
60
61
  18_rails/ # Minimal Rails 8 demo app (full integration)
61
62
  app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
62
63
  app/tools/time_tool.rb # Custom RobotLab::Tool subclass
@@ -225,7 +226,7 @@ Demonstrates `robot.search_history(query, limit:)` — semantic search over accu
225
226
 
226
227
  Demonstrates `memory.store_document(key, text)` and `memory.search_documents(query, limit:)` — a lightweight RAG store using `fastembed` (BAAI/bge-small-en-v1.5). Documents are embedded once; queries are compared by cosine similarity at search time. Includes the `RobotLab::DocumentStore` standalone API and a RAG pattern sketch showing how to pass retrieved context to a robot.
227
228
 
228
- **Requires:** `fastembed` gem (already a core dependency); downloads the ~23 MB ONNX model on first run (cached in `~/.cache/fastembed/`)
229
+ **Requires:** `robot_lab-document_store` gem (`gem "robot_lab-document_store"` in your Gemfile); downloads the ~23 MB ONNX model on first run (cached in `~/.cache/fastembed/`)
229
230
 
230
231
  ### 27 — Production Incident War Room
231
232
 
@@ -297,6 +298,14 @@ and the `pipeline.step_dependencies` dependency graph inspection.
297
298
 
298
299
  **Requires:** None for Parts 1 & 2. LLM API key for Part 3.
299
300
 
301
+ ### 31 — Product Launch Assessment (Concurrency Cap)
302
+
303
+ Six specialist robots evaluate a product launch simultaneously: market, competitive, technical, risk, financial, and legal analysts. `RunConfig.new(max_concurrent_robots: 4)` caps the `Async::Semaphore` at 4 in-flight LLM calls — robots 5 and 6 queue until a slot opens. A `LaunchDirector` reads all six findings from shared reactive memory and issues a GO / NO-GO recommendation. Start timestamps in the output make the semaphore behavior visible.
304
+
305
+ 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
+
307
+ **Requires:** LLM API key
308
+
300
309
  ### 18 — Rails Integration Demo
301
310
 
302
311
  A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails integration end-to-end. No `rails new` — every file is hand-crafted for minimum size.