ruby_llm 2.1.0 → 2.2.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 (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -14,6 +14,7 @@ module RubyLLM
14
14
  # their content is needed.
15
15
  class Resource
16
16
  include Support::Inspectable
17
+ include Files::Saving
17
18
 
18
19
  # The URI that identifies the resource on its server.
19
20
  attr_reader :uri
@@ -61,17 +62,19 @@ module RubyLLM
61
62
  content.b
62
63
  end
63
64
 
65
+ ##
66
+ # :method: save
67
+ # :call-seq:
68
+ # save(path)
69
+ #
64
70
  # Writes the content to +path+, expanding it first. Returns +path+.
65
- def save(path)
66
- File.binwrite(File.expand_path(path), to_blob)
67
- path
68
- end
69
71
 
70
72
  # Returns the resource as an Attachment, which is how chats take it
71
- # through +with:+.
73
+ # through +with:+. The attachment has the MIME type the server reports
74
+ # when the URI has no file extension.
72
75
  def to_attachment
73
76
  filename = Content.filename(uri)
74
- Attachment.new(StringIO.new(to_blob), filename: filename.empty? ? name : filename)
77
+ Content.file(to_blob, mime_type, filename.empty? ? name : filename)
75
78
  end
76
79
 
77
80
  private
@@ -56,9 +56,10 @@ module RubyLLM
56
56
  def self.expand_variable(spec, operator, separator, reserved, variables) # :nodoc:
57
57
  explode = spec.end_with?('*')
58
58
  name, length = spec.delete_suffix('*').split(':')
59
- return unless variables.key?(name)
59
+ values = Array(variables[name])
60
+ return if values.empty?
60
61
 
61
- parts = Array(variables[name]).map { |part| encode(length ? part.to_s[0, length.to_i] : part.to_s, reserved) }
62
+ parts = values.map { |part| encode(length ? part.to_s[0, length.to_i] : part.to_s, reserved) }
62
63
  named = NAMED_OPERATORS.include?(operator)
63
64
  return parts.map { |part| named ? "#{name}=#{part}" : part }.join(separator) if explode
64
65
 
@@ -4,7 +4,15 @@ module RubyLLM
4
4
  class MCP
5
5
  # Raised by a transport when the session of a server that predates
6
6
  # 2026-07-28 has ended, so the client starts a new one: the server
7
- # answered 404, or its stdio process exited.
8
- class SessionExpired < Error; end # :nodoc:
7
+ # answered 404, or its stdio process exited. +session+ is the session
8
+ # that ended, as the transport's +session+ reader returned it.
9
+ class SessionExpired < Error # :nodoc:
10
+ attr_reader :session
11
+
12
+ def initialize(message = nil, session: nil, **)
13
+ @session = session
14
+ super(message, **)
15
+ end
16
+ end
9
17
  end
10
18
  end
@@ -12,6 +12,11 @@ module RubyLLM
12
12
  # time. Its stderr is the parent's. A server that predates 2026-07-28
13
13
  # keeps its session for the life of its process, so after a restart its
14
14
  # requests raise SessionExpired until the client initializes it again.
15
+ # The process inherits only INHERITED_ENV from the application, and +env+
16
+ # adds to and overrides those: a Hash, or a callable that returns one,
17
+ # called each time the process starts. Where the platform has process
18
+ # groups the server leads its own, so stopping it reaches the processes
19
+ # it started.
15
20
  #
16
21
  # Subscriptions share the channel, so whichever thread reads a message
17
22
  # that belongs to one hands it to the subscription's listener: a
@@ -19,9 +24,20 @@ module RubyLLM
19
24
  # request reads. A listener stops when CancelledError is raised in its
20
25
  # thread, so it reads and writes with that deferred.
21
26
  class Stdio # :nodoc:
27
+ # Raised when the server's process exits or does not answer in time.
28
+ class Unavailable < Error # :nodoc:
29
+ end
30
+
22
31
  SHUTDOWN_GRACE = 2
32
+ SHUTDOWN_CHECK = 0.05
23
33
  CHECK_INTERVAL = 0.5
24
34
  SUBSCRIPTION_ID = 'io.modelcontextprotocol/subscriptionId'
35
+ INHERITED_ENV = %w[
36
+ GEM_HOME GEM_PATH HOME LANG LC_ALL LC_CTYPE LOGNAME PATH SHELL TERM TMPDIR USER
37
+ APPDATA COMSPEC HOMEDRIVE HOMEPATH LOCALAPPDATA PATHEXT PROCESSOR_ARCHITECTURE
38
+ PROGRAMDATA PROGRAMFILES PROGRAMFILES(X86) PROGRAMW6432 SYSTEMDRIVE SYSTEMROOT
39
+ TEMP TMP USERNAME USERPROFILE WINDIR
40
+ ].freeze
25
41
 
26
42
  # The messages of the subscription opened by request +id+, or with no
27
43
  # +id+, the changes a server that predates subscriptions announces.
@@ -39,9 +55,12 @@ module RubyLLM
39
55
  end
40
56
  end
41
57
 
58
+ # The session of a server that predates 2026-07-28: the process it was initialized in, or +nil+.
59
+ attr_reader :session
60
+
42
61
  def initialize(command, env: {}, directory: nil, timeout: nil, config: RubyLLM.config)
43
62
  @command = Array(command).map(&:to_s)
44
- @env = env.to_h { |key, value| [key.to_s, value.to_s] }
63
+ @env = env
45
64
  @directory = directory&.to_s
46
65
  @timeout = timeout || config.request_timeout
47
66
  @lock = Mutex.new
@@ -76,12 +95,10 @@ module RubyLLM
76
95
 
77
96
  yield reply
78
97
  end
79
- raise Error, "#{name} exited" unless @process.equal?(process)
98
+ raise Unavailable, "#{name} exited" unless @process.equal?(process)
80
99
 
81
100
  receive(subscription)
82
101
  end
83
- rescue IOError
84
- raise Error, "#{name} exited"
85
102
  ensure
86
103
  uninterrupted { @lock.synchronize { @subscriptions.delete(subscription) } }
87
104
  end
@@ -95,11 +112,11 @@ module RubyLLM
95
112
  def check_session(version)
96
113
  return if version.nil? || version == Client::VERSION || (@process&.alive? && @process.equal?(@session))
97
114
 
98
- raise SessionExpired, "#{name} exited"
115
+ raise SessionExpired.new("#{name} exited", session: @session)
99
116
  end
100
117
 
101
118
  def subscribe(subscription, message)
102
- raise Error, "#{name} exited" unless message || @process&.alive?
119
+ raise Unavailable, "#{name} exited" unless message || @process&.alive?
103
120
 
104
121
  @subscriptions << subscription
105
122
  write(message) if message
@@ -120,6 +137,8 @@ module RubyLLM
120
137
  return sleep(CHECK_INTERVAL) unless read
121
138
 
122
139
  @stdout.wait_readable(CHECK_INTERVAL) if subscription.messages.empty?
140
+ rescue IOError
141
+ raise Unavailable, "#{name} exited"
123
142
  end
124
143
 
125
144
  def drain
@@ -164,11 +183,13 @@ module RubyLLM
164
183
 
165
184
  def write(message)
166
185
  start unless @process&.alive?
167
- @stdin.puts JSON.generate(message)
168
- @stdin.flush
169
- rescue Errno::EPIPE, IOError
170
- stop
171
- raise Error, "#{name} exited"
186
+ begin
187
+ @stdin.puts JSON.generate(message)
188
+ @stdin.flush
189
+ rescue Errno::EPIPE, IOError
190
+ stop
191
+ raise Unavailable, "#{name} exited"
192
+ end
172
193
  end
173
194
 
174
195
  def read(deadline)
@@ -194,7 +215,7 @@ module RubyLLM
194
215
  return line if line
195
216
 
196
217
  remaining = deadline - monotonic_now
197
- raise Error, "#{name} did not answer in time" unless remaining.positive?
218
+ raise Unavailable, "#{name} did not answer in time" unless remaining.positive?
198
219
 
199
220
  Support::Cancellation.check
200
221
  next unless @stdout.wait_readable([remaining, CHECK_INTERVAL].min)
@@ -209,7 +230,7 @@ module RubyLLM
209
230
 
210
231
  def exited
211
232
  stop
212
- raise Error, "#{name} exited"
233
+ raise Unavailable, "#{name} exited"
213
234
  end
214
235
 
215
236
  def answer(request)
@@ -218,26 +239,53 @@ module RubyLLM
218
239
 
219
240
  def start
220
241
  options = @directory ? { chdir: @directory } : {}
221
- @stdin, @stdout, @process = Open3.popen2(@env, *@command, **options)
242
+ options[:pgroup] = true if groups?
243
+ env = ENV.slice(*INHERITED_ENV).merge(environment)
244
+ @stdin, @stdout, @process = Open3.popen2(env, *@command, unsetenv_others: true, **options)
222
245
  @buffer = +''
223
246
  end
224
247
 
248
+ def environment
249
+ variables = @env.respond_to?(:call) ? @env.call : @env
250
+ variables.to_h { |key, value| [key.to_s, value.to_s] }
251
+ end
252
+
225
253
  def stop
226
254
  return unless @process
227
255
 
228
256
  @stdin.close unless @stdin.closed?
229
257
  @stdout.close unless @stdout.closed?
230
- terminate unless @process.join(SHUTDOWN_GRACE)
258
+ @process.join(SHUTDOWN_GRACE)
259
+ terminate if groups? || @process.alive?
231
260
  @process = nil
232
261
  end
233
262
 
234
263
  def terminate
235
- Process.kill('TERM', @process.pid)
236
- Process.kill('KILL', @process.pid) unless @process.join(SHUTDOWN_GRACE)
237
- rescue Errno::ESRCH
264
+ signal('TERM')
265
+ signal('KILL') unless exited?
266
+ rescue Errno::ESRCH, Errno::EPERM
238
267
  nil
239
268
  end
240
269
 
270
+ def exited?
271
+ deadline = monotonic_now + SHUTDOWN_GRACE
272
+ while monotonic_now < deadline
273
+ signal(0)
274
+ sleep SHUTDOWN_CHECK
275
+ end
276
+ false
277
+ rescue Errno::ESRCH, Errno::EPERM
278
+ true
279
+ end
280
+
281
+ def signal(signal)
282
+ Process.kill(signal, groups? ? -@process.pid : @process.pid)
283
+ end
284
+
285
+ def groups?
286
+ !Gem.win_platform?
287
+ end
288
+
241
289
  def name
242
290
  File.basename(@command.first.to_s)
243
291
  end
@@ -18,30 +18,22 @@ module RubyLLM
18
18
  # task.poll_interval # => 5.0
19
19
  #
20
20
  # MCP#call waits for the task of a tool you call directly.
21
- class Task
22
- include Support::Inspectable
23
-
24
- DONE = %i[completed failed cancelled].freeze
25
- private_constant :DONE
26
-
21
+ class Task < RubyLLM::Task
27
22
  # The task's id on its server.
28
23
  attr_reader :id
29
24
 
30
- # The ToolCall paused on the task, when it came from a chat.
31
- attr_reader :tool_call
32
-
33
- attr_reader :data, :answered # :nodoc:
25
+ attr_reader :answered # :nodoc:
34
26
 
35
27
  def self.load(mcp, state, tool_call: nil) # :nodoc:
36
28
  new(mcp, state['task'], answered: state['answered'], tool_call:)
37
29
  end
38
30
 
39
31
  def initialize(mcp, data, answered: nil, tool_call: nil) # :nodoc:
32
+ super(tool_call:)
40
33
  @mcp = mcp
41
34
  @data = data
42
35
  @id = data['taskId']
43
36
  @answered = Array(answered)
44
- @tool_call = tool_call
45
37
  end
46
38
 
47
39
  # The task's state: +:working+, +:input_required+, +:completed+,
@@ -68,43 +60,23 @@ module RubyLLM
68
60
  Time.iso8601(data['createdAt']) + (data['ttlMs'] / 1000.0) if data['ttlMs'] && data['createdAt']
69
61
  end
70
62
 
71
- # Returns +true+ once the task completed, failed, or was cancelled.
72
- def done?
73
- DONE.include?(status)
74
- end
75
-
76
- # Returns +true+ when the task finished with a #result.
77
- def completed?
78
- status == :completed
79
- end
80
-
81
- # Returns +true+ when the task failed with an error.
82
- def failed?
83
- status == :failed
84
- end
85
-
86
- # Returns +true+ when the task was cancelled before it finished.
87
- def cancelled?
88
- status == :cancelled
89
- end
90
-
91
- # Returns the MCP::Result once the task #completed?, and +nil+ until
63
+ # Returns the MCP::Result once the task #succeeded?, and +nil+ until
92
64
  # then. Raises MCP::Error when the task failed or was cancelled.
93
65
  def result
94
66
  raise error if failed? || cancelled?
95
67
 
96
- Result.new(data['result']) if completed?
68
+ Result.new(data['result']) if succeeded?
97
69
  end
98
70
 
99
71
  # Checks on the task once and returns +self+, which now tells whether
100
- # the task is #done? and how soon to check again. Does nothing once
101
- # the task is #done?.
72
+ # the task is #complete? and how soon to check again. Does nothing once
73
+ # the task is #complete?.
102
74
  def refresh
103
- @data = connected.poll_task(id) unless done?
75
+ @data = connected.poll_task(id) unless complete?
104
76
  self
105
77
  end
106
78
 
107
- # Checks on the task until it is #done?, sleeping #poll_interval
79
+ # Checks on the task until it is #complete?, sleeping #poll_interval
108
80
  # seconds in between, and returns +self+. Answers the server's
109
81
  # requests for input with MCP.before_input_request callbacks.
110
82
  # +timeout+ defaults to the server's MCP.timeout, and the task is
@@ -119,14 +91,15 @@ module RubyLLM
119
91
  # Asks the server to cancel the task, and returns +self+. The server
120
92
  # may still finish it, so check with #refresh.
121
93
  def cancel
122
- connected.cancel_task(id) unless done?
94
+ connected.cancel_task(id) unless complete?
123
95
  self
124
96
  end
125
97
 
126
98
  def error # :nodoc:
127
99
  details = data['error'] || {}
128
- Error.new(details['message'] || status_message || "Task #{id} was #{status}",
129
- code: details['code'], data: details['data'])
100
+ reported = details['message'] || status_message
101
+ Error.new(("Task #{id} was #{status}" unless reported), provider_message: reported,
102
+ code: details['code'], data: details['data'])
130
103
  end
131
104
 
132
105
  def record_answers(keys) # :nodoc:
@@ -45,6 +45,11 @@ module RubyLLM
45
45
 
46
46
  attr_reader :fixed_arguments, :wrap # :nodoc:
47
47
 
48
+ # The fixed arguments, with lambdas called on the MCP instance.
49
+ def resolved_fixed_arguments # :nodoc:
50
+ fixed_arguments.transform_values { |value| value.is_a?(Proc) ? @mcp.instance_exec(&value) : value }
51
+ end
52
+
48
53
  def initialize(mcp, definition, prefix: nil, as: nil, description: nil, fixed_arguments: {}, wrap: nil) # :nodoc:
49
54
  super()
50
55
  @mcp = mcp
@@ -53,7 +58,7 @@ module RubyLLM
53
58
  @description = description || definition['description']
54
59
  @fixed_arguments = fixed_arguments.transform_keys(&:to_sym)
55
60
  @wrap = wrap
56
- @annotations = definition['annotations'] || {}
61
+ @annotations = definition['annotations'].is_a?(Hash) ? definition['annotations'] : {}
57
62
  @meta = definition['_meta'] || {}
58
63
  @ui_uri = Apps.uri(meta)
59
64
  @visibility = Apps.visibility(meta)
@@ -73,7 +78,8 @@ module RubyLLM
73
78
  end
74
79
 
75
80
  # Returns whether calling the tool again with the same arguments has
76
- # no further effect.
81
+ # no further effect. Only these calls are sent again when a stream
82
+ # breaks before the answer.
77
83
  def idempotent?
78
84
  @annotations['idempotentHint'] == true
79
85
  end
@@ -85,32 +91,49 @@ module RubyLLM
85
91
 
86
92
  # Returns whether the tool pauses for approval, as declared with
87
93
  # MCP.requires_approval.
88
- def requires_approval?
94
+ def requires_approval?(_tool_call = nil)
89
95
  @mcp.requires_approval?(self)
90
96
  end
91
97
 
92
98
  # Calls the tool on the server and returns its MCP::Result, what the
93
99
  # +wrap:+ method made of it, or <tt>{ error: }</tt> when the tool
94
- # failed. A chat sends the model the result's content. When the
95
- # server runs the call as a task, returns the MCP::Task without
96
- # waiting, and a chat pauses the tool call until the task is done.
100
+ # failed, or when the server failed the call: it could not be
101
+ # reached, answered HTTP 429 or a 5xx status, or answered with a
102
+ # JSON-RPC error. A server that could not be reached sits out the
103
+ # chat's next requests, as when listing its tools. A refusal, such as
104
+ # missing credentials, still raises. A tool with a UI returns its
105
+ # failed MCP::Result instead, so your app can still render the UI. A
106
+ # chat sends the model the result's content. When the server runs
107
+ # the call as a task, returns the MCP::Task without waiting, and a
108
+ # chat pauses the tool call until the task is done.
97
109
  # Raises MCP::InputRequiredError when the server needs input that no
98
110
  # MCP.before_input_request callback gave.
99
111
  def call(**arguments)
100
112
  @mcp.run(self, arguments.except(:tool_call))
101
113
  end
102
114
 
103
- # Resumes a call that paused on input requests, now answered, or on a
104
- # task, which it checks on once.
105
- def resume(input, arguments) # :nodoc:
106
- @mcp.run(self, arguments, input:)
115
+ # Resumes a call that paused on +task+, which it checks on once.
116
+ def resume(task, **arguments)
117
+ @mcp.run(self, arguments, input: task.to_h)
107
118
  end
108
119
 
109
120
  # Returns the MCP::Task a call paused on, from its saved state.
110
- def task(state, tool_call: nil) # :nodoc:
121
+ def task(state, tool_call: nil)
111
122
  Task.load(@mcp, state, tool_call:)
112
123
  end
113
124
 
125
+ def resume_call(state, arguments, **) # :nodoc:
126
+ @mcp.run(self, arguments, input: state)
127
+ end
128
+
129
+ # Runs the handler the MCP class declares for +exception+, or else
130
+ # one declared on this class.
131
+ def rescue_with_handler(exception) # :nodoc:
132
+ return super unless @mcp.class.rescue_handler_for(exception)
133
+
134
+ @mcp.rescue_with_handler(exception)
135
+ end
136
+
114
137
  private
115
138
 
116
139
  def model_schema(schema)