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
@@ -7,6 +7,7 @@ require_relative 'robot/history_search'
7
7
  require_relative 'robot/agent_skill_matching'
8
8
  require_relative 'robot/budget'
9
9
  require_relative 'robot/hooking'
10
+ require_relative 'robot/result_building'
10
11
 
11
12
  module RobotLab
12
13
  # LLM-powered robot built on RubyLLM::Agent
@@ -54,6 +55,7 @@ module RobotLab
54
55
  include Robot::HistorySearch
55
56
  include Robot::Budget
56
57
  include Robot::Hooking
58
+ include Robot::ResultBuilding
57
59
  include Runnable
58
60
  prepend Robot::AgentSkillMatching
59
61
 
@@ -367,9 +369,7 @@ module RobotLab
367
369
  # access to conversation state.
368
370
  #
369
371
  # @return [RubyLLM::Chat]
370
- def chat
371
- @chat
372
- end
372
+ attr_reader :chat
373
373
 
374
374
  # Return the conversation messages from the underlying chat.
375
375
  #
@@ -386,10 +386,10 @@ module RobotLab
386
386
  def clear_messages(keep_system: true)
387
387
  if keep_system
388
388
  system_msg = @chat.messages.find { |m| m.role == :system }
389
- @chat.reset_messages!
389
+ @chat.messages = []
390
390
  @chat.add_message(system_msg) if system_msg
391
391
  else
392
- @chat.reset_messages!
392
+ @chat.messages = []
393
393
  end
394
394
  self
395
395
  end
@@ -399,8 +399,8 @@ module RobotLab
399
399
  # @param messages [Array<RubyLLM::Message>] the messages to restore
400
400
  # @return [self]
401
401
  def replace_messages(messages)
402
- @chat.reset_messages!
403
- messages.each { |m| @chat.add_message(m) }
402
+ @chat.messages = []
403
+ messages.each { |m| @chat.add_message(coerce_replacement_message(m)) }
404
404
  self
405
405
  end
406
406
 
@@ -665,6 +665,8 @@ module RobotLab
665
665
  @learnings = []
666
666
  @hooks = HookRegistry.new
667
667
  @budget_ledger = build_budget_ledger
668
+ @circuit_breaker_armed = false
669
+ @circuit_breaker_call_count = 0
668
670
  end
669
671
 
670
672
  def initialize_memory
@@ -716,20 +718,33 @@ module RobotLab
716
718
 
717
719
  def apply_chat_params
718
720
  @chat.with_temperature(@config.temperature) if @config.temperature
721
+ @chat.with_max_output_tokens(@config.max_tokens) if @config.max_tokens
719
722
 
723
+ # ruby_llm 2.0 has no with_params; provider-specific sampling knobs are
724
+ # merged into the request payload via with_provider_options instead.
725
+ # with_provider_options replaces prior options, so merge with whatever
726
+ # the chat already carries (e.g. from template front matter).
720
727
  extra_params = {
721
728
  top_p: @config.top_p, top_k: @config.top_k,
722
- max_tokens: @config.max_tokens,
723
729
  presence_penalty: @config.presence_penalty,
724
730
  frequency_penalty: @config.frequency_penalty,
725
731
  stop: @config.stop
726
732
  }.compact
727
- @chat.with_params(**extra_params) if extra_params.any?
733
+ @chat.with_provider_options(@chat.provider_options.to_h.merge(extra_params)) if extra_params.any?
728
734
  end
729
735
 
736
+ # ruby_llm 2.0 callbacks (before_tool_call/after_tool_result) are additive
737
+ # and cannot be removed, so register a single dispatcher per chat that
738
+ # consults the robot's current state. The per-run circuit breaker arms and
739
+ # disarms a flag instead of swapping callbacks.
730
740
  def register_chat_callbacks
731
- @chat.on_tool_call(&@on_tool_call) if @on_tool_call
732
- @chat.on_tool_result(&@on_tool_result) if @on_tool_result
741
+ @chat.before_tool_call do |tool_call|
742
+ enforce_circuit_breaker! if @circuit_breaker_armed
743
+ @on_tool_call&.call(tool_call)
744
+ end
745
+ @chat.after_tool_result do |result|
746
+ @on_tool_result&.call(result)
747
+ end
733
748
  setup_bus_channel if @bus
734
749
  end
735
750
 
@@ -761,6 +776,7 @@ module RobotLab
761
776
  end
762
777
  end
763
778
 
779
+ # :reek:TooManyStatements -- resolve/discover/connect/filter/attach is one linear per-turn tool pipeline.
764
780
  def prepare_tools(message:, mcp:, tools:, network:, network_config:)
765
781
  resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
766
782
  resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
@@ -779,12 +795,16 @@ module RobotLab
779
795
  # runtime value before resolution to honor the zero-tools intent.
780
796
  filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(resolved_tools))
781
797
 
782
- # replace: true so the chat holds EXACTLY this turn's resolved+capped set.
783
- # RubyLLM's with_tools appends by default; on a persistent chat that lets
784
- # tools accumulate across turns, so a capped per-turn addition could still
785
- # push the chat's total past the provider limit. An explicit none clears
786
- # the chat's tools to zero (with_tools(replace: true) with no tools).
787
- @chat.with_tools(*filtered, replace: true) if filtered.any? || explicit_none_tools?(tools)
798
+ # Clear-then-add so the chat holds EXACTLY this turn's resolved+capped
799
+ # set. RubyLLM's with_tools appends; on a persistent chat that lets tools
800
+ # accumulate across turns, so a capped per-turn addition could still push
801
+ # the chat's total past the provider limit. with_tools(nil) is ruby_llm
802
+ # 2.0's "clear the tool set"; an explicit none stops there, sending zero
803
+ # tools this turn.
804
+ return unless filtered.any? || explicit_none_tools?(tools)
805
+
806
+ @chat.with_tools(nil)
807
+ @chat.with_tools(*filtered) if filtered.any?
788
808
  end
789
809
 
790
810
  # True when the runtime tools value explicitly requests zero tools — `:none`
@@ -902,85 +922,6 @@ module RobotLab
902
922
  merged
903
923
  end
904
924
 
905
- # :reek:TooManyStatements :reek:FeatureEnvy -- adapting a provider response's many optional fields into
906
- # a RobotResult is inherently response-centric.
907
- def build_result(response, _memory)
908
- text = result_text(response)
909
- output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
910
-
911
- tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
912
-
913
- # Extract token usage from the response
914
- input_toks = output_toks = 0
915
- if response.respond_to?(:tokens) && (tokens = response.tokens)
916
- input_toks = tokens.input.to_i
917
- output_toks = tokens.output.to_i
918
- elsif response.respond_to?(:input_tokens)
919
- input_toks = response.input_tokens.to_i
920
- output_toks = response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0
921
- end
922
-
923
- @total_input_tokens += input_toks
924
- @total_output_tokens += output_toks
925
-
926
- RobotResult.new(
927
- robot_name: @name,
928
- output: output,
929
- tool_calls: normalize_tool_calls(tool_calls),
930
- stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil,
931
- raw: response,
932
- input_tokens: input_toks,
933
- output_tokens: output_toks
934
- )
935
- end
936
-
937
- # Text for the result's output. Prefers the final response's content, then
938
- # falls back in order to: (1) thinking text for models that route all output
939
- # through reasoning_content (e.g. qwen3 on Ollama), (2) the most recent
940
- # assistant text within the current turn for models that end on a tool call
941
- # with no trailing text.
942
- #
943
- # The chat-history fallback is scoped to messages AFTER the last user message
944
- # (the current turn) to prevent a previous turn's response from being returned
945
- # when a thinking-mode model emits nothing in response.content.
946
- # :reek:TooManyStatements :reek:FeatureEnvy -- documented fallback chain over the response's optional content/thinking/history fields.
947
- def result_text(response)
948
- content = response.content if response.respond_to?(:content)
949
- return content if content && !content.to_s.empty?
950
-
951
- # Ollama routes qwen3's reasoning to reasoning_content, which ruby_llm
952
- # surfaces as response.thinking (a RubyLLM::Thinking object). When content
953
- # is nil and thinking is present, the thinking IS the response for that turn.
954
- if response.respond_to?(:thinking) && (thinking = response.thinking)
955
- thinking_text = thinking.respond_to?(:text) ? thinking.text.to_s : thinking.to_s
956
- return thinking_text unless thinking_text.empty?
957
- end
958
-
959
- return nil unless @chat.respond_to?(:messages)
960
-
961
- messages = @chat.messages
962
- last_user_idx = messages.rindex { |m| m.role == :user } || -1
963
- current_turn = messages[(last_user_idx + 1)..]
964
-
965
- last = current_turn.rfind { |m| m.role == :assistant && m.content && !m.content.to_s.empty? }
966
- last&.content
967
- end
968
-
969
- def normalize_tool_calls(tool_calls)
970
- return [] unless tool_calls
971
-
972
- tool_calls.map do |tc|
973
- if tc.is_a?(Hash)
974
- ToolResultMessage.new(
975
- tool: tc,
976
- content: tc[:result] || tc['result']
977
- )
978
- else
979
- tc
980
- end
981
- end
982
- end
983
-
984
925
  # Merge the stored on_content callback with a runtime streaming block.
985
926
  # If both exist, both fire (stored first, then runtime block).
986
927
  #
@@ -1124,29 +1065,5 @@ module RobotLab
1124
1065
  RobotLab.config.logger.warn("[#{@name}] auto_compact: #{e.message}; skipping compaction")
1125
1066
  end
1126
1067
  end
1127
-
1128
- # Install a per-run circuit breaker on the chat's on_tool_call hook.
1129
- # Raises ToolLoopError if tool calls exceed @config.max_tool_rounds.
1130
- # Stores the previous callback so restore_tool_call_callback can undo it.
1131
- def install_circuit_breaker
1132
- @circuit_breaker_call_count = 0
1133
- max = @config.max_tool_rounds
1134
- original = @on_tool_call
1135
-
1136
- @chat.on_tool_call do |tool_call|
1137
- @circuit_breaker_call_count += 1
1138
- if @circuit_breaker_call_count > max
1139
- raise ToolLoopError,
1140
- "Circuit breaker triggered: #{@circuit_breaker_call_count} tool calls exceeded " \
1141
- "max_tool_rounds (#{max})"
1142
- end
1143
- original&.call(tool_call)
1144
- end
1145
- end
1146
-
1147
- # Restore the original on_tool_call callback after a circuit-breaker run.
1148
- def restore_tool_call_callback
1149
- @chat.on_tool_call(&@on_tool_call)
1150
- end
1151
1068
  end
1152
1069
  end
@@ -12,20 +12,20 @@ module RobotLab
12
12
  # config's non-nil values win over the less-specific config.
13
13
  #
14
14
  # @example Keyword construction
15
- # config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
15
+ # config = RunConfig.new(model: "claude-sonnet-4-6", temperature: 0.7)
16
16
  #
17
17
  # @example Block DSL
18
18
  # config = RunConfig.new do |c|
19
- # c.model "claude-sonnet-4"
19
+ # c.model "claude-sonnet-4-6"
20
20
  # c.temperature 0.7
21
21
  # end
22
22
  #
23
23
  # @example Merge (more-specific wins)
24
- # network_config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
24
+ # network_config = RunConfig.new(model: "claude-sonnet-4-6", temperature: 0.5)
25
25
  # robot_config = RunConfig.new(temperature: 0.9)
26
26
  # effective = network_config.merge(robot_config)
27
27
  # effective.temperature #=> 0.9
28
- # effective.model #=> "claude-sonnet-4"
28
+ # effective.model #=> "claude-sonnet-4-6"
29
29
  #
30
30
  # :reek:RepeatedConditional -- `if value` guards in independent field loops; each skips unset fields.
31
31
  class RunConfig
@@ -106,6 +106,10 @@ module RobotLab
106
106
  self.class.new(**merged)
107
107
  end
108
108
 
109
+ # Fields that ruby_llm 2.0 accepts only through with_provider_options
110
+ # (merged into the request payload in the provider's own vocabulary).
111
+ PROVIDER_OPTION_FIELDS = %i[top_p top_k presence_penalty frequency_penalty stop].freeze
112
+
109
113
  # Applies LLM fields to a chat object via its with_* methods.
110
114
  #
111
115
  # +provider+/+assume_model_exists+ are threaded through to +with_model+
@@ -117,27 +121,26 @@ module RobotLab
117
121
  # raises ModelNotFoundError for any local-provider model (Ollama, etc.)
118
122
  # not in RubyLLM's bundled registry.
119
123
  #
124
+ # ruby_llm 2.0 mapping: +max_tokens+ applies via with_max_output_tokens,
125
+ # and the sampling knobs (top_p, top_k, penalties, stop) go through
126
+ # with_provider_options, merged with any options the chat already has.
127
+ #
120
128
  # @param chat [Object] a RubyLLM::Chat (or similar) that responds to with_model, with_temperature, etc.
121
129
  # @param provider [String, Symbol, nil] passed through to chat.with_model's provider: kwarg
122
- # @param assume_model_exists [Boolean] passed through to chat.with_model's assume_exists: kwarg
130
+ # @param assume_model_exists [Boolean] passed through to chat.with_model's assume_model_exists: kwarg
123
131
  # :reek:BooleanParameter -- assume_model_exists is a pass-through to RubyLLM's with_model kwarg.
124
132
  # :reek:FeatureEnvy -- configuring the chat object handed in is exactly what apply_to is for.
133
+ # :reek:TooManyStatements -- one guarded application per LLM field group.
125
134
  def apply_to(chat, provider: nil, assume_model_exists: false)
126
- LLM_FIELDS.each do |field|
127
- value = @fields[field]
128
- next unless value
129
-
130
- if field == :model && provider
131
- # Only take the provider-aware path when a provider was actually
132
- # given -- preserves the original single-arg call (and thus
133
- # compatibility with any chat-like object exposing only
134
- # `with_model(value)`) for the common case.
135
- chat.with_model(value, provider:, assume_exists: assume_model_exists) if chat.respond_to?(:with_model)
136
- else
137
- method = :"with_#{field}"
138
- chat.public_send(method, value) if chat.respond_to?(method)
139
- end
140
- end
135
+ apply_model_to(chat, provider: provider, assume_model_exists: assume_model_exists)
136
+
137
+ temperature = @fields[:temperature]
138
+ chat.with_temperature(temperature) if temperature && chat.respond_to?(:with_temperature)
139
+
140
+ max_tokens = @fields[:max_tokens]
141
+ chat.with_max_output_tokens(max_tokens) if max_tokens && chat.respond_to?(:with_max_output_tokens)
142
+
143
+ apply_provider_options_to(chat)
141
144
  end
142
145
 
143
146
  # Build a RunConfig from prompt_manager front matter metadata.
@@ -187,6 +190,35 @@ module RobotLab
187
190
 
188
191
  private
189
192
 
193
+ # Apply the model field, threading provider/assume_model_exists through
194
+ # only when a provider was actually given -- preserves the original
195
+ # single-arg call (and thus compatibility with any chat-like object
196
+ # exposing only `with_model(value)`) for the common case.
197
+ # :reek:BooleanParameter -- pass-through to RubyLLM's with_model kwarg.
198
+ # :reek:FeatureEnvy -- configuring the chat handed in is exactly what apply_to delegates here.
199
+ def apply_model_to(chat, provider:, assume_model_exists:)
200
+ model = @fields[:model]
201
+ return unless model && chat.respond_to?(:with_model)
202
+
203
+ if provider
204
+ chat.with_model(model, provider: provider, assume_model_exists: assume_model_exists)
205
+ else
206
+ chat.with_model(model)
207
+ end
208
+ end
209
+
210
+ # Merge the sampling knobs into the chat's provider options.
211
+ # with_provider_options replaces prior options wholesale, so merge with
212
+ # the chat's current set to keep options applied earlier.
213
+ # :reek:FeatureEnvy -- configuring the chat handed in is exactly what apply_to delegates here.
214
+ def apply_provider_options_to(chat)
215
+ options = @fields.slice(*PROVIDER_OPTION_FIELDS)
216
+ return if options.empty? || !chat.respond_to?(:with_provider_options)
217
+
218
+ current = chat.respond_to?(:provider_options) ? chat.provider_options.to_h : {}
219
+ chat.with_provider_options(current.merge(options))
220
+ end
221
+
190
222
  # Validates and stores a field value. Nil removes the key.
191
223
  def set(field, value)
192
224
  field = field.to_sym
@@ -9,7 +9,7 @@ module RobotLab
9
9
  #
10
10
  # class GetWeather < RobotLab::Tool
11
11
  # description "Get weather for a location"
12
- # param :location, type: "string", desc: "City name"
12
+ # parameter :location, type: "string", description: "City name"
13
13
  #
14
14
  # def execute(location:)
15
15
  # WeatherService.fetch(location)
@@ -93,10 +93,11 @@ module RobotLab
93
93
  #
94
94
  # For non-Ractor-safe tools: runs execute directly in the calling thread.
95
95
  #
96
+ # @param tool_call [RubyLLM::ToolCall, nil] the originating tool call (supplied by the chat)
96
97
  # @param args [Hash] the tool arguments from the LLM
97
98
  # @return [Object] the tool result or an error string
98
99
  # :reek:TooManyStatements -- hook-wrapped dispatch with per-error-class handling; the rescue clauses are the method.
99
- def call(args)
100
+ def call(tool_call: nil, **args)
100
101
  context = ToolCallHookContext.new(tool: self, tool_args: args, robot: @robot)
101
102
 
102
103
  RobotLab::Hooks.run(:tool_call, context, **tool_hook_options) do
@@ -157,15 +158,9 @@ module RobotLab
157
158
  tool_class = Class.new(self) do
158
159
  description(desc_text) if desc_text
159
160
 
160
- if params_hash.is_a?(Hash) && params_hash[:properties]
161
- required_list = Array(params_hash[:required]).map(&:to_s)
162
- params_hash[:properties].each do |pname, pdef|
163
- param pname.to_sym,
164
- type: pdef[:type] || "string",
165
- desc: pdef[:description],
166
- required: required_list.include?(pname.to_s)
167
- end
168
- end
161
+ # ruby_llm 2.0's parameters() accepts a raw JSON Schema hash, so the
162
+ # full schema (including nested items/objects) survives verbatim.
163
+ parameters(params_hash) if params_hash.is_a?(Hash) && params_hash[:properties]
169
164
 
170
165
  define_method(:execute) do |**args|
171
166
  block.call(args)
@@ -183,7 +178,7 @@ module RobotLab
183
178
  #
184
179
  # @return [Hash] JSON Schema representation
185
180
  def to_json_schema
186
- schema = params_schema || { "type" => "object", "properties" => {}, "required" => [] }
181
+ schema = parameters_schema || { "type" => "object", "properties" => {}, "required" => [] }
187
182
  {
188
183
  name: name,
189
184
  description: description,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.2.8"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/robot_lab.rb CHANGED
@@ -39,7 +39,7 @@ require 'typed_bus'
39
39
  # # See lib/robot_lab/config/defaults.yml for all options
40
40
  #
41
41
  # # Access configuration values:
42
- # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
42
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
43
43
  # RobotLab.config.ruby_llm.request_timeout #=> 120
44
44
  #
45
45
  module RobotLab
@@ -159,7 +159,7 @@ module RobotLab
159
159
  # @return [Config] the config instance
160
160
  #
161
161
  # @example
162
- # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
162
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
163
163
  # RobotLab.config.ruby_llm.request_timeout #=> 120
164
164
  # RobotLab.config.development? #=> true
165
165
  def config
@@ -173,7 +173,7 @@ module RobotLab
173
173
  #
174
174
  # @example
175
175
  # RobotLab.configure do |c|
176
- # c.default_model = "claude-sonnet-4"
176
+ # c.default_model = "claude-sonnet-4-6"
177
177
  # end
178
178
  def configure
179
179
  yield config