robot_lab 0.3.0 → 0.5.0

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. data/tool_manifest_plan.md +0 -155
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RobotLabResult < ApplicationRecord
4
- belongs_to :thread,
5
- class_name: "RobotLabThread",
6
- foreign_key: :session_id,
7
- primary_key: :session_id
8
-
9
- validates :session_id, presence: true
10
- validates :robot_name, presence: true
11
- validates :sequence_number, presence: true,
12
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
13
-
14
- default_scope { order(sequence_number: :asc) }
15
-
16
- def output_messages
17
- (output_data || []).map do |data|
18
- RobotLab::Message.from_hash(data.symbolize_keys)
19
- end
20
- end
21
-
22
- def tool_call_messages
23
- (tool_calls_data || []).map do |data|
24
- RobotLab::Message.from_hash(data.symbolize_keys)
25
- end
26
- end
27
-
28
- def to_robot_result
29
- RobotLab::RobotResult.new(
30
- robot_name: robot_name,
31
- output: output_messages,
32
- tool_calls: tool_call_messages,
33
- stop_reason: stop_reason
34
- )
35
- end
36
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RobotLabThread < ApplicationRecord
4
- has_many :results,
5
- class_name: "RobotLabResult",
6
- foreign_key: :session_id,
7
- primary_key: :session_id,
8
- dependent: :destroy
9
-
10
- validates :session_id, presence: true, uniqueness: true
11
-
12
- def self.find_or_create_by_session_id(id)
13
- find_or_create_by(session_id: id)
14
- end
15
-
16
- def last_result
17
- results.order(sequence_number: :desc).first
18
- end
19
-
20
- def message_count
21
- results.count
22
- end
23
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Factory for the chat robot. RobotLab::RailsIntegration::Job calls
4
- # ChatRobot.build(on_content:, on_tool_call:) to wire Turbo streaming.
5
- #
6
- # The model is served locally by Ollama. Ollama models are absent from
7
- # RubyLLM's registry, so provider: must accompany model: — that is what makes
8
- # RubyLLM skip the registry lookup.
9
- class ChatRobot
10
- SYSTEM_PROMPT = "You are a friendly assistant. Be concise."
11
-
12
- PROVIDER = ENV.fetch("ROBOT_LAB_PROVIDER", "ollama")
13
- MODEL = ENV.fetch("ROBOT_LAB_MODEL", "qwen3.6:latest")
14
-
15
- def self.build(**options)
16
- RobotLab.build(
17
- name: "chat",
18
- provider: PROVIDER,
19
- model: MODEL,
20
- system_prompt: SYSTEM_PROMPT,
21
- local_tools: [TimeTool],
22
- **options
23
- )
24
- end
25
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class TimeTool < RobotLab::Tool
4
- description "Get the current date and time"
5
-
6
- def execute
7
- Time.current.strftime("%Y-%m-%d %H:%M:%S %Z")
8
- end
9
- end
@@ -1 +0,0 @@
1
- <div class="message user"><%= message %></div>
@@ -1,67 +0,0 @@
1
- <h1>RobotLab Chat</h1>
2
-
3
- <%= turbo_stream_from "robot_lab_thread_#{@thread_id}" %>
4
-
5
- <%= form_with url: chat_path, method: :post, class: "chat-form" do |f| %>
6
- <%= f.text_field :message, placeholder: "Type a message...", autofocus: true, autocomplete: "off" %>
7
- <%= f.submit "Send" %>
8
- <% end %>
9
-
10
- <div id="chat-scroll">
11
- <div id="messages">
12
- <% @results.each do |result| %>
13
- <% if result.robot_name == "user" %>
14
- <% result.output_messages.each do |msg| %>
15
- <div class="message user"><%= msg.content %></div>
16
- <% end %>
17
- <% else %>
18
- <% result.output_messages.each do |msg| %>
19
- <div class="message assistant"><%= msg.content %></div>
20
- <% end %>
21
- <% unless result.tool_call_messages.empty? %>
22
- <div>
23
- <% result.tool_call_messages.each do |tc| %>
24
- <span class="tool-badge">Used: <%= tc.content %></span>
25
- <% end %>
26
- </div>
27
- <% end %>
28
- <% end %>
29
- <% end %>
30
- </div>
31
-
32
- <div id="robot_tools"></div>
33
- <div id="robot_response" class="message assistant" style="display:none;"></div>
34
- <div id="robot_errors"></div>
35
- <div id="robot_status"></div>
36
- </div>
37
-
38
- <script>
39
- const scrollContainer = document.getElementById('chat-scroll');
40
- const responseEl = document.getElementById('robot_response');
41
-
42
- function scrollToBottom() {
43
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
44
- }
45
-
46
- // Auto-scroll on any mutation inside the scroll container
47
- const observer = new MutationObserver(function() {
48
- if (responseEl.textContent.trim().length > 0) {
49
- responseEl.style.display = 'block';
50
- }
51
- scrollToBottom();
52
- });
53
-
54
- observer.observe(scrollContainer, { childList: true, characterData: true, subtree: true });
55
-
56
- // Clear the input field after form submission
57
- document.addEventListener('turbo:submit-end', function(event) {
58
- const input = event.target.querySelector('input[name="message"]');
59
- if (input) {
60
- input.value = '';
61
- input.focus();
62
- }
63
- });
64
-
65
- // Scroll to bottom on initial load if there's history
66
- scrollToBottom();
67
- </script>
@@ -1,49 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>RobotLab Rails Demo</title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <%= action_cable_meta_tag %>
7
- <script type="importmap">
8
- {
9
- "imports": {
10
- "@hotwired/turbo": "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm",
11
- "@hotwired/turbo-rails": "https://cdn.jsdelivr.net/npm/@hotwired/turbo-rails@8.0.12/+esm",
12
- "@rails/actioncable": "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.0.200/+esm"
13
- }
14
- }
15
- </script>
16
- <script type="module">
17
- import "@hotwired/turbo-rails"
18
- </script>
19
- <style>
20
- * { box-sizing: border-box; margin: 0; padding: 0; }
21
- body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; align-items: center; overflow: hidden; }
22
- .container { max-width: 720px; width: 100%; padding: 2rem 1rem; display: flex; flex-direction: column; height: 100%; overflow: hidden; }
23
- #chat-scroll { flex: 1; overflow-y: auto; scroll-behavior: smooth; padding-right: 0.25rem; }
24
- #chat-scroll::-webkit-scrollbar { width: 6px; }
25
- #chat-scroll::-webkit-scrollbar-track { background: transparent; }
26
- #chat-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
27
- h1 { color: #7c83ff; margin-bottom: 1.5rem; font-size: 1.5rem; }
28
- .chat-form { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
29
- .chat-form input[type="text"] { flex: 1; padding: 0.75rem; border-radius: 8px; border: 1px solid #333; background: #16213e; color: #e0e0e0; font-size: 1rem; }
30
- .chat-form input[type="text"]:focus { outline: none; border-color: #7c83ff; }
31
- .chat-form button { padding: 0.75rem 1.5rem; border-radius: 8px; border: none; background: #7c83ff; color: #fff; font-size: 1rem; cursor: pointer; }
32
- .chat-form button:hover { background: #6a70e0; }
33
- .message { padding: 0.75rem 1rem; margin-bottom: 0.5rem; border-radius: 8px; line-height: 1.5; white-space: pre-wrap; }
34
- .message.user { background: #1f4068; margin-left: 4rem; }
35
- .message.assistant { background: #16213e; margin-right: 4rem; border: 1px solid #2a2a4a; }
36
- .tool-badge { display: inline-block; background: #2d1b69; color: #b8b0ff; padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; margin: 0.25rem 0.25rem 0.25rem 0; }
37
- .thinking { color: #7c83ff; font-style: italic; }
38
- .complete { color: #4ade80; font-size: 0.85rem; }
39
- .error { color: #f87171; background: #2d1320; padding: 0.5rem 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; }
40
- #robot_response { white-space: pre-wrap; }
41
- .history-label { color: #666; font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.75rem; letter-spacing: 0.05em; }
42
- </style>
43
- </head>
44
- <body>
45
- <div class="container">
46
- <%= yield %>
47
- </div>
48
- </body>
49
- </html>
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- cd "$(dirname "$0")/.."
5
-
6
- echo "Starting Puma on http://localhost:3000 ..."
7
- bundle exec rails server -p 3000
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- APP_PATH = File.expand_path("../config/application", __dir__)
5
- require_relative "../config/application"
6
- require "rails/commands"
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- cd "$(dirname "$0")/.."
5
-
6
- echo "==> Installing gems..."
7
- bundle install
8
-
9
- echo "==> Creating database..."
10
- bundle exec rails db:create
11
-
12
- echo "==> Running migrations..."
13
- bundle exec rails db:migrate
14
-
15
- echo "==> Done! Run bin/dev to start the server."
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
-
5
- require "rails"
6
- require "active_record/railtie"
7
- require "active_job/railtie"
8
- require "action_controller/railtie"
9
- require "action_view/railtie"
10
- require "action_cable/engine"
11
-
12
- Bundler.require(*Rails.groups)
13
-
14
- module RobotLabDemo
15
- class Application < Rails::Application
16
- config.load_defaults 8.0
17
-
18
- config.eager_load = false
19
- config.consider_all_requests_local = true
20
- config.secret_key_base = "demo-secret-key-for-development-only"
21
-
22
- # Use async adapter for jobs — no external process needed
23
- config.active_job.queue_adapter = :async
24
-
25
- # Autoload app/robots and app/tools
26
- config.autoload_paths << root.join("app", "robots")
27
- config.autoload_paths << root.join("app", "tools")
28
-
29
- # Disable unused middleware
30
- config.api_only = false
31
- config.hosts.clear
32
- end
33
- end
@@ -1,2 +0,0 @@
1
- development:
2
- adapter: async
@@ -1,5 +0,0 @@
1
- development:
2
- adapter: sqlite3
3
- database: db/development.sqlite3
4
- pool: 5
5
- timeout: 5000
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "application"
4
- Rails.application.initialize!
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RobotLab.config.logger = Rails.logger
4
-
5
- # Runs against a local Ollama server — no API keys required.
6
- # ollama serve
7
- # ollama pull qwen3.6
8
- RubyLLM.configure do |c|
9
- c.logger = Rails.logger
10
- c.ollama_api_base = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434/v1")
11
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Rails.application.routes.draw do
4
- root "chat#index"
5
- post "chat", to: "chat#create"
6
- end
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "config/environment"
4
- run Rails.application
@@ -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,138 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 19: Per-Robot Token / Cost Tracking
5
- #
6
- # Demonstrates per-run and cumulative token tracking:
7
- # - result.input_tokens / result.output_tokens — tokens used in that run
8
- # - robot.total_input_tokens / total_output_tokens — running totals on the robot
9
- # - robot.reset_token_totals — reset the accounting counter (not the chat)
10
- #
11
- # Key distinction:
12
- # reset_token_totals resets the robot's *accounting counter* only.
13
- # The underlying chat history keeps growing, so input_tokens per run
14
- # naturally increase as conversation context accumulates.
15
- # Use a fresh robot.build when you need a genuinely fresh context.
16
- #
17
- # LM Studio reports usage counts like the hosted providers do, so the tracking
18
- # API behaves identically — only the price per token differs. Token counts are
19
- # zero for providers that don't report usage data.
20
- #
21
- # Usage:
22
- # ruby examples/19_token_tracking.rb
23
-
24
- require_relative "common"
25
-
26
- # Cost model. A local LM Studio model bills nothing, so the interesting number is
27
- # what the same traffic WOULD have cost on a hosted model — set RATE_INPUT_CPM
28
- # and RATE_OUTPUT_CPM to your provider's $-per-1M-token rates to see it.
29
- # Defaults are zero: local inference is free.
30
- RATE_INPUT_CPM = ENV.fetch("RATE_INPUT_CPM", "0").to_f
31
- RATE_OUTPUT_CPM = ENV.fetch("RATE_OUTPUT_CPM", "0").to_f
32
-
33
- def token_summary(input, output)
34
- "in=#{input} out=#{output} total=#{input + output}"
35
- end
36
-
37
- def run_cost(input, output)
38
- return "$0.00000 (local)" if RATE_INPUT_CPM.zero? && RATE_OUTPUT_CPM.zero?
39
-
40
- dollars = (input * RATE_INPUT_CPM + output * RATE_OUTPUT_CPM) / 1_000_000.0
41
- "$#{"%.5f" % dollars}"
42
- end
43
-
44
- def first_line(text)
45
- text&.strip&.lines&.first&.strip || ""
46
- end
47
-
48
- banner "Per-Robot Token & Cost Tracking"
49
-
50
- puts "Model: #{LLM[:default].provider}/#{LLM[:default].model}"
51
- if RATE_INPUT_CPM.zero? && RATE_OUTPUT_CPM.zero?
52
- puts "Rates: none set — local inference is free."
53
- puts " Set RATE_INPUT_CPM / RATE_OUTPUT_CPM to price the same traffic"
54
- puts " against a hosted provider."
55
- else
56
- puts "Rates: $#{RATE_INPUT_CPM}/1M in, $#{RATE_OUTPUT_CPM}/1M out"
57
- end
58
- puts
59
-
60
- robot = RobotLab.build(
61
- **llm_opts,
62
- name: "analyst",
63
- system_prompt: "You are a concise technical analyst. Keep every reply under 40 words."
64
- )
65
-
66
- prompts = [
67
- "What is the difference between a stack and a queue?",
68
- "Name three Ruby gems that every Rails project should consider.",
69
- "Why does database indexing improve query performance?"
70
- ]
71
-
72
- puts "Running #{prompts.size} prompts...\n\n"
73
-
74
- prompts.each_with_index do |prompt, i|
75
- result = robot.run(prompt)
76
-
77
- puts "Run #{i + 1}: #{prompt}"
78
- puts " Reply: #{first_line(result.reply)}"
79
- puts " This run: #{token_summary(result.input_tokens, result.output_tokens)}"
80
- puts " Cost: #{run_cost(result.input_tokens, result.output_tokens)}"
81
- puts " Cumulative: #{token_summary(robot.total_input_tokens, robot.total_output_tokens)}"
82
- puts
83
- end
84
-
85
- hr
86
- puts "After #{prompts.size} runs:"
87
- puts " Total tokens: #{token_summary(robot.total_input_tokens, robot.total_output_tokens)}"
88
- puts " Total cost: #{run_cost(robot.total_input_tokens, robot.total_output_tokens)}"
89
- puts
90
-
91
- # ---------------------------------------------------------------
92
- # reset_token_totals: resets the accounting counter only.
93
- # Useful when you want to measure cost for a specific task batch
94
- # while keeping the robot alive for the next batch.
95
- #
96
- # Note: input_tokens will still reflect the full chat history sent
97
- # to the API — that's the real cost, not an error.
98
- # ---------------------------------------------------------------
99
-
100
- puts "Resetting token totals (batch 1 complete, starting batch 2)..."
101
- robot.reset_token_totals
102
- puts " Counter now: #{token_summary(robot.total_input_tokens, robot.total_output_tokens)}"
103
- puts
104
- puts " Note: the next run's input_tokens will be larger than run 1's"
105
- puts " because the LLM receives the full accumulated chat context."
106
- puts " The counter reset tracks *accounting* — it doesn't clear the chat."
107
- puts
108
-
109
- result = robot.run("What is memoization?")
110
- puts "Batch 2 — Run 1: What is memoization?"
111
- puts " Reply: #{first_line(result.reply)}"
112
- puts " This run: #{token_summary(result.input_tokens, result.output_tokens)}"
113
- puts " Batch total: #{token_summary(robot.total_input_tokens, robot.total_output_tokens)}"
114
- puts
115
-
116
- # ---------------------------------------------------------------
117
- # To start truly fresh (new context, new counter), build a new robot.
118
- # ---------------------------------------------------------------
119
-
120
- hr
121
- puts "Fresh robot — genuinely zero context:"
122
- puts
123
-
124
- fresh = RobotLab.build(
125
- **llm_opts,
126
- name: "analyst2",
127
- system_prompt: "You are a concise technical analyst. Keep every reply under 40 words."
128
- )
129
-
130
- result = fresh.run("What is memoization?")
131
- puts "Run 1 (fresh robot): What is memoization?"
132
- puts " Reply: #{first_line(result.reply)}"
133
- puts " This run: #{token_summary(result.input_tokens, result.output_tokens)}"
134
- puts " Cumulative: #{token_summary(fresh.total_input_tokens, fresh.total_output_tokens)}"
135
- puts
136
-
137
- hr
138
- puts "Token tracking demo complete."
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 20: Tool Loop Circuit Breaker
5
- #
6
- # Demonstrates max_tool_rounds to prevent runaway tool call loops.
7
- #
8
- # A "process runner" robot is given a step tool whose return value always
9
- # instructs the LLM to call it again — it would loop forever without a guard.
10
- # The circuit breaker fires after max_tool_rounds tool calls and raises
11
- # RobotLab::ToolLoopError.
12
- #
13
- # Key behaviour when the breaker fires:
14
- # The chat history contains a dangling tool_use with no tool_result.
15
- # Most providers reject any subsequent request with that
16
- # broken history. You MUST call robot.clear_messages before reusing the
17
- # same robot instance — or simply build a fresh robot.
18
- #
19
- # Demonstrates:
20
- # - max_tool_rounds: N on RobotLab.build
21
- # - ToolLoopError raised when the limit is exceeded
22
- # - clear_messages to flush the corrupted chat and recover the robot
23
- # - Contrast: robot without a circuit breaker on a task that terminates
24
- #
25
- # Usage:
26
- # ruby examples/20_circuit_breaker.rb
27
-
28
- require_relative "common"
29
-
30
- # -------------------------------------------------------------------------
31
- # A tool that always says "more steps remain" — designed to induce looping
32
- # -------------------------------------------------------------------------
33
-
34
- class MultiStepProcessor < RubyLLM::Tool
35
- description <<~DESC
36
- Executes one step of a sequential batch process.
37
- Returns the result of that step and whether more steps remain.
38
- You MUST call this tool again for each remaining step until
39
- the status is "complete".
40
- DESC
41
-
42
- parameter :step_number,
43
- type: "integer",
44
- description: "Which step to execute. Start at 1, increment by 1 each call."
45
-
46
- TOTAL_STEPS = 50 # far more than any sensible max_tool_rounds
47
-
48
- def execute(step_number:)
49
- remaining = TOTAL_STEPS - step_number
50
- if remaining <= 0
51
- { status: "complete", step: step_number, message: "All steps finished." }
52
- else
53
- {
54
- status: "in_progress",
55
- step_completed: step_number,
56
- remaining_steps: remaining,
57
- instruction: "Call this tool again with step_number: #{step_number + 1}"
58
- }
59
- end
60
- end
61
- end
62
-
63
- banner "Tool Loop Circuit Breaker"
64
-
65
- TASK = "Run the batch process from step 1 using the MultiStepProcessor tool. " \
66
- "Execute every step sequentially until the process reports 'complete'."
67
-
68
- # -------------------------------------------------------------------------
69
- # Part 1: Circuit breaker fires — rescue ToolLoopError, then recover
70
- #
71
- # After ToolLoopError the chat contains a dangling tool_use block with no
72
- # matching tool_result. Providers reject any follow-up request with that
73
- # history. Call clear_messages to flush the broken context before reuse.
74
- # -------------------------------------------------------------------------
75
-
76
- section "Part 1: Circuit Breaker Fires (max_tool_rounds: 5)"
77
-
78
- robot = RobotLab.build(
79
- **llm_opts,
80
- name: "process_runner",
81
- system_prompt: "You are a process runner. Execute tasks exactly as instructed.",
82
- local_tools: [MultiStepProcessor],
83
- max_tool_rounds: 5
84
- )
85
-
86
- begin
87
- # tools: :inherit — run() defaults to :none, which sends the provider an
88
- # empty tool list. With no tool to loop on there would be nothing for the
89
- # breaker to trip.
90
- robot.run(TASK, tools: :inherit)
91
- puts "Process completed (unexpected for this demo)."
92
- rescue RobotLab::ToolLoopError => e
93
- puts "Circuit breaker fired!"
94
- puts " #{e.message}"
95
- end
96
-
97
- puts
98
-
99
- # -------------------------------------------------------------------------
100
- # Part 2: Recover by flushing the corrupted chat with clear_messages
101
- #
102
- # The robot retains its config (system prompt, tools, max_tool_rounds).
103
- # Only the conversation history is cleared — the robot is then reusable.
104
- # -------------------------------------------------------------------------
105
-
106
- section "Part 2: Recover with clear_messages"
107
-
108
- robot.clear_messages
109
- puts "Chat flushed. Robot config (tools, max_tool_rounds) is unchanged."
110
- puts " max_tool_rounds still: #{robot.config.max_tool_rounds}"
111
- puts
112
-
113
- result = robot.run("What is 3 + 4? Answer in one sentence.")
114
- puts "Follow-up after recovery: #{result.reply&.strip}"
115
- puts
116
-
117
- # -------------------------------------------------------------------------
118
- # Part 3: Robot without a circuit breaker on a task that terminates quickly
119
- #
120
- # Normal tool use works fine with no guard — the breaker is a safety net,
121
- # not a tax on well-behaved tool interactions.
122
- # -------------------------------------------------------------------------
123
-
124
- section "Part 3: No Circuit Breaker — Task Terminates Naturally"
125
-
126
- class SingleStep < RubyLLM::Tool
127
- description "Doubles a number and returns the result immediately."
128
- parameter :value, type: "integer", description: "The number to double"
129
-
130
- def execute(value:)
131
- { result: value * 2, status: "complete" }
132
- end
133
- end
134
-
135
- unguarded = RobotLab.build(
136
- **llm_opts,
137
- name: "calculator",
138
- system_prompt: "Use the provided tool to answer questions.",
139
- local_tools: [SingleStep]
140
- )
141
-
142
- result = unguarded.run("Double the number 21 using the tool.", tools: :inherit)
143
- puts "Result: #{result.reply&.strip}"
144
- puts
145
-
146
- hr
147
- puts "Circuit breaker demo complete."