robot_lab 0.2.8 → 0.3.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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. data/tool_manifest_plan.md +0 -155
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de87b78206bc424703b465d3a1f236db723ea38fe4235c07413466ab5b469ead
4
- data.tar.gz: 6a7db14e4530e615e258415491422f297567a2f640983035f1542d88e7d200e5
3
+ metadata.gz: 8ae306b218ea8c7c5d68b9de5c0bce088e40a9f1c482278fd54557624bc0ad72
4
+ data.tar.gz: 92206741b254631a7e09da7247db35702eb12a70409622aa3634c594e83207a0
5
5
  SHA512:
6
- metadata.gz: f6814c9a8b86b8e6d1df2adcc2d9b4ab84c176c67211424e8404e9d83c1e08ffd353587523e4c45efec295d0e47bbd731b0cdc6f3e62754cfd4f1ce2a33d7655
7
- data.tar.gz: 37682af098203ae333cc83fa6e66a95f1883aae0bed8411524a05d6795bb47b4a99f231353fe3546b2fdae8a5c098d0f45b863e531edaf237aab33e4e1bc3859
6
+ metadata.gz: 36c52283dfd0edc88cfc71e358a82e463ddfad501a69097ef2aaa28b37f7c5f5d08938d39b1c6312c2b1a3aaf19494895d3bcfd6f74870b4b770942f3ae60d3e
7
+ data.tar.gz: 1477bf01cb700d41bab40950cd650960676f1589a5f2f4dc8747b1f940e9f21a5b3ff53a15ca4aa2ef01c493b38c1f957406ddc1b365f1592a21b1e6bda1d992
data/CHANGELOG.md CHANGED
@@ -6,6 +6,71 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [Unreleased]
10
+
11
+ ### Changed
12
+
13
+ - **Minimum Ruby raised to 4.0.9.** `required_ruby_version` is now `>= 4.0.9` (was `>= 3.2.0`), and a `.ruby-version` file pins development to 4.0.9.
14
+ - **Slimmer gem.** The gemspec now whitelists what ships in the `.gem`: `lib/`, `sig/`, `exe/`, README, CHANGELOG, and LICENSE. Previous releases packaged everything tracked by git — the docs site, examples with generated output, tooling config, and local design notes — which made 0.3.0 a 1.1 MB gem of 300+ files. 0.3.1 is ~130 KB and 65 files; the docs and examples remain on GitHub.
15
+ - Verified against ruby_llm 2.1.1 (the newest release the `~> 2.0` constraint admits) in addition to 2.0.0; the full suite passes on both.
16
+
17
+ ### Fixed
18
+
19
+ - `MCP::Transports::StreamableHTTP#connect` no longer raises inside its Async task when the initialize handshake returns no result; the session id is simply left unset.
20
+
21
+ ## [0.3.0] - 2026-09-18
22
+
23
+ The ruby_llm 2.0 release. Core moves from ruby_llm 1.x to the official 2.0.0
24
+ release; every `robot_lab-*` extension gem releases in lockstep at 0.3.0 and
25
+ now requires `robot_lab ~> 0.3.0`.
26
+
27
+ ### Changed (BREAKING)
28
+ - **ruby_llm 1.x → 2.0.** The gemspec now requires `ruby_llm "~> 2.0"`
29
+ (official release). Robot tools written with the 1.x DSL must migrate:
30
+ `param` → `parameter`, `desc:` → `description:`; `halt` is gone (the caller
31
+ controls the loop). Token counts moved to `response.tokens.input/.output`,
32
+ and `finish_reason` (Symbol) replaces `stop_reason`. The `claude-sonnet-4`
33
+ registry alias no longer resolves; the default models are now
34
+ `claude-sonnet-4-6` (production) and `claude-haiku-4-5` (test).
35
+ - **Dropped hard dependencies** on `ruby_llm-mcp`, `ruby_llm-schema`, and
36
+ `ruby_llm-semantic_cache`. Semantic caching is now optional: `Memory#cache`
37
+ returns `nil` when `ruby_llm-semantic_cache` is not installed and Memory
38
+ runs with caching disabled. (Its released versions still pin ruby_llm 1.x,
39
+ so it cannot co-resolve with 2.0 yet.)
40
+
41
+ ### Added
42
+ - `ruby_llm-providers-apfel` and `ruby_llm-providers-lms` (`~> 0.2.1`) as
43
+ runtime dependencies, consumed as released gems — the local `path:`
44
+ overrides for sibling checkouts are gone from the Gemfiles.
45
+ - `Robot::ResultBuilding` — extracted module that adapts a ruby_llm response
46
+ into a `RobotResult` (token accounting, stop-reason normalization, message
47
+ coercion).
48
+ - `examples/run_all.rb` — runs every executable `NN_*.rb` demo serially with
49
+ a banner between each; works from any cwd, and Ctrl-C terminates only the
50
+ demo currently running (the runner notes it and moves on).
51
+ - READMEs for the subdirectory demos: `15_memory_network_and_bus`,
52
+ `26_document_store` (corpus), and `27_incident_response`.
53
+ - Fasterer quality gate now actually runs: `fasterer` added to the
54
+ development group, with a `.fasterer.yml` documenting the disabled
55
+ speed-over-readability checks.
56
+
57
+ ### Changed
58
+ - Robot, Tool, RunConfig, and the hook system adapted to the ruby_llm 2.0
59
+ API; docs and examples synced to match.
60
+ - Examples run against a local LM Studio server through the `:lms` provider;
61
+ `examples/common.rb` anchors `BUNDLE_GEMFILE` to the gem root so demos run
62
+ from any directory, and `examples/README.md` notes that demo speed and
63
+ answer quality depend on the provider/model in use.
64
+ - `.envrc` no longer exports `BUNDLE_GEMFILE`; it is inherited from the
65
+ project root (`asgard dev` / `asgard prod`).
66
+
67
+ ### Fixed
68
+ - amazing_print 3.0 crash at demo startup (`undefined method 'try' for
69
+ module ActiveSupport`): robot_lab's dependencies load parts of
70
+ ActiveSupport, which tricked amazing_print into loading its Rails
71
+ extension; `examples/common.rb` now preloads
72
+ `active_support/core_ext/object/try` and `active_support/log_subscriber`.
73
+
9
74
  ## [0.2.8] - 2026-09-09
10
75
 
11
76
  ### Added
data/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
22
22
  - <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
23
23
  - <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
24
- - <strong>Local LLM Providers</strong> - Ollama and GPUStack via provider passthrough<br>
24
+ - <strong>Local LLM Providers</strong> - LM Studio (via ruby_llm-providers-lms), Ollama, and GPUStack via provider passthrough<br>
25
25
  - <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
26
26
  - <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
27
27
  - <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
@@ -55,7 +55,7 @@ gem install robot_lab
55
55
 
56
56
  ### Requirements
57
57
 
58
- - Ruby >= 3.2
58
+ - Ruby >= 4.0.9
59
59
  - [One or more API Keys for LLM providers supported by RubyLLM](https://rubyllm.com/configuration/#provider-configuration)
60
60
 
61
61
  For comprehensive guides and API documentation, visit **[https://madbomber.github.io/robot_lab](https://madbomber.github.io/robot_lab)**
@@ -82,17 +82,15 @@ puts result.last_text_content
82
82
 
83
83
  ### Local LLM Providers
84
84
 
85
- For local LLM providers, use the `provider:` parameter. RubyLLM's provider list is the authority on valid values — `:ollama` and `:gpustack` are the local ones (alongside `:anthropic`, `:azure`, `:bedrock`, `:deepseek`, `:gemini`, `:mistral`, `:openai`, `:openrouter`, `:perplexity`, `:vertexai`, and `:xai`). The provider's API base must be configured first, or building the robot raises `RubyLLM::ConfigurationError`:
86
-
87
- ```bash
88
- export ROBOT_LAB_RUBY_LLM__OLLAMA_API_BASE=http://localhost:11434/v1
89
- ```
85
+ For local LLM providers, use the `provider:` parameter. RubyLLM's provider list is the authority on valid values — `:ollama` and `:gpustack` are built in, and provider gems add more (alongside `:anthropic`, `:azure`, `:bedrock`, `:deepseek`, `:gemini`, `:mistral`, `:openai`, `:openrouter`, `:perplexity`, `:vertexai`, and `:xai`). The examples in this repo use LM Studio through the [ruby_llm-providers-lms](https://github.com/madbomber/ruby_llm-providers-lms) gem, which registers the `:lms` provider (defaults to `http://localhost:1234/v1`, no API key needed):
90
86
 
91
87
  ```ruby
88
+ require "ruby_llm/providers/lms"
89
+
92
90
  robot = RobotLab.build(
93
91
  name: "local_bot",
94
- model: "llama3.2",
95
- provider: :ollama,
92
+ model: "qwen/qwen3.8-27b", # complex work; use "openai/gpt-oss-20b" for simple chat
93
+ provider: :lms,
96
94
  system_prompt: "You are a helpful assistant."
97
95
  )
98
96
  ```
@@ -113,12 +111,12 @@ RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layer
113
111
  # Set API keys via environment variables (double underscore for nesting)
114
112
  export ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
115
113
  export ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
116
- export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
114
+ export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4-6
117
115
  ```
118
116
 
119
117
  ```ruby
120
118
  # Access configuration values
121
- RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
119
+ RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
122
120
  RobotLab.config.ruby_llm.request_timeout #=> 120
123
121
  ```
124
122
 
@@ -126,7 +124,7 @@ Or create a project config file at `./config/robot_lab.yml`:
126
124
 
127
125
  ```yaml
128
126
  ruby_llm:
129
- model: claude-sonnet-4
127
+ model: claude-sonnet-4-6
130
128
  anthropic_api_key: sk-ant-...
131
129
  request_timeout: 180
132
130
  ```
@@ -202,7 +200,7 @@ mcp:
202
200
  type: stdio
203
201
  command: npx
204
202
  args: ["-y", "@modelcontextprotocol/server-github"]
205
- model: claude-sonnet-4
203
+ model: claude-sonnet-4-6
206
204
  ---
207
205
  You are a GitHub assistant. Use available tools to help with repository tasks.
208
206
  ```
@@ -265,7 +263,7 @@ robot = RobotLab.build(
265
263
  ```ruby
266
264
  # Create a shared config
267
265
  shared = RobotLab::RunConfig.new(
268
- model: "claude-sonnet-4",
266
+ model: "claude-sonnet-4-6",
269
267
  temperature: 0.7,
270
268
  max_tokens: 2000
271
269
  )
@@ -302,16 +300,16 @@ RunConfig supports keyword construction, block DSL, and merge semantics:
302
300
  ```ruby
303
301
  # Block DSL
304
302
  config = RobotLab::RunConfig.new do |c|
305
- c.model "claude-sonnet-4"
303
+ c.model "claude-sonnet-4-6"
306
304
  c.temperature 0.7
307
305
  end
308
306
 
309
307
  # Merge (more-specific wins)
310
- network_config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
308
+ network_config = RobotLab::RunConfig.new(model: "claude-sonnet-4-6", temperature: 0.5)
311
309
  robot_config = RobotLab::RunConfig.new(temperature: 0.9)
312
310
  effective = network_config.merge(robot_config)
313
311
  effective.temperature #=> 0.9
314
- effective.model #=> "claude-sonnet-4"
312
+ effective.model #=> "claude-sonnet-4-6"
315
313
  ```
316
314
 
317
315
  ### Chaining Configuration
@@ -323,7 +321,7 @@ robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer
323
321
 
324
322
  result = robot
325
323
  .with_temperature(0.9)
326
- .with_model("claude-sonnet-4")
324
+ .with_model("claude-sonnet-4-6")
327
325
  .run("Write a haiku about Ruby programming")
328
326
  ```
329
327
 
@@ -33,9 +33,9 @@ module RobotLab
33
33
  #
34
34
  class AskUser < Tool
35
35
  description "Ask the user a question and wait for their typed response"
36
- param :question, type: "string", desc: "The question to ask the user"
37
- param :choices, type: "array", desc: "Optional list of choices to present", required: false
38
- param :default, type: "string", desc: "Default value if user presses Enter", required: false
36
+ parameter :question, type: "string", description: "The question to ask the user"
37
+ parameter :choices, type: "array", description: "Optional list of choices to present", required: false
38
+ parameter :default, type: "string", description: "Default value if user presses Enter", required: false
39
39
 
40
40
  # :reek:FeatureEnvy -- rendering and resolving the caller-supplied choices list is this tool's whole job.
41
41
  # :reek:TooManyStatements -- linear prompt/read/resolve terminal interaction.
@@ -41,13 +41,13 @@ defaults:
41
41
  with_model:
42
42
  provider: null # chat-specific provider
43
43
  model: null # chat-specific model
44
- assume_exists: null # assume the model exists primarily for local providers
44
+ assume_model_exists: null # assume the model exists primarily for local providers
45
45
  with_temperature: 0.7 # Controls randomness (0.0-2.0, null = model default)
46
+ with_max_output_tokens: null # Maximum tokens in response
46
47
  with_tools: null # ?? not sure about this one.
47
- with_params:
48
+ with_provider_options: # merged into the request payload (ruby_llm 2.0)
48
49
  top_p: null # Nucleus sampling threshold (0.0-1.0)
49
50
  top_k: null # Top-k sampling (integer, provider-specific)
50
- max_tokens: null # Maximum tokens in response
51
51
  presence_penalty: null # Penalize new tokens based on presence (-2.0 to 2.0)
52
52
  frequency_penalty: null # Penalize new tokens based on frequency (-2.0 to 2.0)
53
53
  stop: null # Stop sequences (string or array of strings)
@@ -55,7 +55,7 @@ defaults:
55
55
  # RubyLLM Configuration Section
56
56
  ruby_llm:
57
57
  provider: :anthropic
58
- model: claude-sonnet-4
58
+ model: claude-sonnet-4-6
59
59
  assume_model_exists: false # (dep of assume_exists in chat section) set true for Ollama and other local LLM providers
60
60
  # Provider API Keys (null = use env vars directly)
61
61
  anthropic_api_key: null
@@ -117,7 +117,7 @@ test:
117
117
  max_iterations: 3
118
118
  streaming_enabled: false
119
119
  ruby_llm:
120
- model: claude-3-haiku-20240307
120
+ model: claude-haiku-4-5
121
121
  request_timeout: 30
122
122
  max_retries: 1
123
123
  log_level: :warn
@@ -14,7 +14,7 @@ module RobotLab
14
14
  # - Automatic RubyLLM configuration application
15
15
  #
16
16
  # @example Access configuration values
17
- # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
17
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
18
18
  # RobotLab.config.ruby_llm.request_timeout #=> 120
19
19
  # RobotLab.config.development? #=> true
20
20
  #
@@ -28,7 +28,7 @@ module RobotLab
28
28
  # # defaults.yml. This file is NOT run through ERB, so keep secrets in
29
29
  # # environment variables or in ./config/robot_lab.yml (which is).
30
30
  # ruby_llm:
31
- # model: claude-sonnet-4
31
+ # model: claude-sonnet-4-6
32
32
  # request_timeout: 120
33
33
  #
34
34
  class Config < MywayConfig::Base
@@ -50,7 +50,7 @@ module RobotLab
50
50
 
51
51
  # Initialize MCP protocol
52
52
  result = send_initialize
53
- @session_id ||= result.dig(:serverInfo, :sessionId)
53
+ @session_id ||= result&.dig(:serverInfo, :sessionId)
54
54
  end
55
55
 
56
56
  self
@@ -116,9 +116,7 @@ module RobotLab
116
116
  # Returns the session identifier.
117
117
  #
118
118
  # @return [String, nil] the session ID
119
- def session_id
120
- @session_id
121
- end
119
+ attr_reader :session_id
122
120
 
123
121
  private
124
122
 
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ruby_llm/semantic_cache"
3
+ # Optional: ruby_llm-semantic_cache has not shipped a ruby_llm 2.0-compatible
4
+ # release. When the gem is absent, Memory#cache returns nil and semantic
5
+ # caching is silently disabled.
6
+ begin
7
+ require "ruby_llm/semantic_cache"
8
+ rescue LoadError
9
+ # Semantic caching unavailable; Memory runs without it.
10
+ end
4
11
 
5
12
  module RobotLab
6
13
  # Raised when a blocking get times out
@@ -106,7 +113,7 @@ module RobotLab
106
113
  set_internal(:results, Array(results))
107
114
  set_internal(:messages, Array(messages).map { |m| normalize_message(m) })
108
115
  set_internal(:session_id, session_id)
109
- set_internal(:cache, @enable_cache ? RubyLLM::SemanticCache : nil)
116
+ set_internal(:cache, @enable_cache ? create_semantic_cache : nil)
110
117
 
111
118
  # Data proxy for method-style access
112
119
  @data = nil
@@ -215,9 +222,11 @@ module RobotLab
215
222
 
216
223
  # Get the semantic cache module
217
224
  #
218
- # The cache is always active and provides semantic similarity matching
219
- # for LLM responses, reducing costs and latency by returning cached
220
- # responses for semantically equivalent queries.
225
+ # When the optional ruby_llm-semantic_cache gem is installed (and
226
+ # enable_cache is true), provides semantic similarity matching for LLM
227
+ # responses, reducing costs and latency by returning cached responses
228
+ # for semantically equivalent queries. Returns nil when the gem is
229
+ # absent or caching is disabled.
221
230
  #
222
231
  # @example Using the cache with fetch
223
232
  # response = memory.cache.fetch("What is Ruby?") do
@@ -228,7 +237,7 @@ module RobotLab
228
237
  # chat = memory.cache.wrap(RubyLLM.chat(model: "gpt-4"))
229
238
  # chat.ask("What is Ruby?") # Cached on semantic similarity
230
239
  #
231
- # @return [RubyLLM::SemanticCache] the semantic cache module
240
+ # @return [RubyLLM::SemanticCache, nil] the semantic cache module, or nil when unavailable
232
241
  #
233
242
  def cache
234
243
  get_internal(:cache)
@@ -702,7 +711,7 @@ module RobotLab
702
711
  end
703
712
 
704
713
  def create_semantic_cache
705
- RubyLLM::SemanticCache
714
+ defined?(RubyLLM::SemanticCache) ? RubyLLM::SemanticCache : nil
706
715
  end
707
716
 
708
717
  # :reek:ControlParameter -- factory method; the preference symbol is exactly what selects the backend.
@@ -54,6 +54,32 @@ module RobotLab
54
54
  def hook_registries(network = nil)
55
55
  [RobotLab.hooks, network&.hooks, @hooks]
56
56
  end
57
+
58
+ # Arm the per-run circuit breaker checked by the chat's before_tool_call
59
+ # dispatcher (see Robot#register_chat_callbacks). Raises ToolLoopError
60
+ # once tool calls exceed @config.max_tool_rounds. ruby_llm 2.0 callbacks
61
+ # are additive and cannot be removed, so the breaker toggles a flag the
62
+ # permanent dispatcher consults instead of swapping callbacks per run.
63
+ def install_circuit_breaker
64
+ @circuit_breaker_call_count = 0
65
+ @circuit_breaker_armed = true
66
+ end
67
+
68
+ # Disarm the circuit breaker after a run.
69
+ def restore_tool_call_callback
70
+ @circuit_breaker_armed = false
71
+ end
72
+
73
+ # Count a tool call against max_tool_rounds and raise once exceeded.
74
+ def enforce_circuit_breaker!
75
+ max = @config.max_tool_rounds
76
+ @circuit_breaker_call_count += 1
77
+ return if @circuit_breaker_call_count <= max
78
+
79
+ raise ToolLoopError,
80
+ "Circuit breaker triggered: #{@circuit_breaker_call_count} tool calls exceeded " \
81
+ "max_tool_rounds (#{max})"
82
+ end
57
83
  end
58
84
  end
59
85
  end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ class Robot < RubyLLM::Agent
5
+ # Adapts a ruby_llm response into a RobotResult, including token
6
+ # accounting, stop-reason normalization, and message coercion helpers.
7
+ #
8
+ # Owns: nothing (pure adapters over the response and @chat)
9
+ # Reads: @chat, @name; Writes: @total_input_tokens, @total_output_tokens
10
+ module ResultBuilding
11
+ private
12
+
13
+ # :reek:TooManyStatements :reek:FeatureEnvy -- adapting a provider response's many optional fields into
14
+ # a RobotResult is inherently response-centric.
15
+ def build_result(response, _memory)
16
+ text = result_text(response)
17
+ output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
18
+
19
+ tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
20
+
21
+ input_toks, output_toks = extract_token_counts(response)
22
+ @total_input_tokens += input_toks
23
+ @total_output_tokens += output_toks
24
+
25
+ RobotResult.new(
26
+ robot_name: @name,
27
+ output: output,
28
+ tool_calls: normalize_tool_calls(tool_calls),
29
+ stop_reason: extract_stop_reason(response),
30
+ raw: response,
31
+ input_tokens: input_toks,
32
+ output_tokens: output_toks
33
+ )
34
+ end
35
+
36
+ # Token usage from the response. ruby_llm 2.0 nests counts under
37
+ # response.tokens; duck-typed responses may still answer input_tokens.
38
+ # :reek:FeatureEnvy -- reading the response's optional token fields is the extraction itself.
39
+ def extract_token_counts(response)
40
+ if response.respond_to?(:tokens) && (tokens = response.tokens)
41
+ [tokens.input.to_i, tokens.output.to_i]
42
+ elsif response.respond_to?(:input_tokens)
43
+ [response.input_tokens.to_i,
44
+ response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0]
45
+ else
46
+ [0, 0]
47
+ end
48
+ end
49
+
50
+ # ruby_llm 2.0's add_message accepts a Message, an attribute Hash, or a
51
+ # record responding to to_llm. Compression summaries (and tests) hand us
52
+ # plain role/content value objects; convert those to attribute hashes so
53
+ # the chat can coerce them.
54
+ def coerce_replacement_message(message)
55
+ if message.is_a?(RubyLLM::Message) || message.is_a?(Hash) || message.respond_to?(:to_llm)
56
+ message
57
+ else
58
+ { role: message.role, content: message.content }
59
+ end
60
+ end
61
+
62
+ # The response's normalized stop reason. ruby_llm 2.0 exposes it as
63
+ # finish_reason (a Symbol such as :stop or :tool_calls); older or
64
+ # duck-typed responses may still answer stop_reason.
65
+ def extract_stop_reason(response)
66
+ return response.finish_reason if response.respond_to?(:finish_reason)
67
+
68
+ response.respond_to?(:stop_reason) ? response.stop_reason : nil
69
+ end
70
+
71
+ # Text for the result's output. Prefers the final response's content, then
72
+ # falls back in order to: (1) thinking text for models that route all output
73
+ # through reasoning_content (e.g. qwen3 on Ollama), (2) the most recent
74
+ # assistant text within the current turn for models that end on a tool call
75
+ # with no trailing text.
76
+ #
77
+ # The chat-history fallback is scoped to messages AFTER the last user message
78
+ # (the current turn) to prevent a previous turn's response from being returned
79
+ # when a thinking-mode model emits nothing in response.content.
80
+ # :reek:TooManyStatements :reek:FeatureEnvy -- documented fallback chain over the response's optional content/thinking/history fields.
81
+ def result_text(response)
82
+ content = response.content if response.respond_to?(:content)
83
+ return content if content && !content.to_s.empty?
84
+
85
+ # Ollama routes qwen3's reasoning to reasoning_content, which ruby_llm
86
+ # surfaces as response.thinking (a RubyLLM::Thinking object). When content
87
+ # is nil and thinking is present, the thinking IS the response for that turn.
88
+ if response.respond_to?(:thinking) && (thinking = response.thinking)
89
+ thinking_text = thinking.respond_to?(:text) ? thinking.text.to_s : thinking.to_s
90
+ return thinking_text unless thinking_text.empty?
91
+ end
92
+
93
+ return nil unless @chat.respond_to?(:messages)
94
+
95
+ messages = @chat.messages
96
+ last_user_idx = messages.rindex { |m| m.role == :user } || -1
97
+ current_turn = messages[(last_user_idx + 1)..]
98
+
99
+ last = current_turn.rfind { |m| m.role == :assistant && m.content && !m.content.to_s.empty? }
100
+ last&.content
101
+ end
102
+
103
+ def normalize_tool_calls(tool_calls)
104
+ return [] unless tool_calls
105
+
106
+ tool_calls.map do |tc|
107
+ if tc.is_a?(Hash)
108
+ ToolResultMessage.new(
109
+ tool: tc,
110
+ content: tc[:result] || tc['result']
111
+ )
112
+ else
113
+ tc
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end