class RubyLLM::Chat
A Chat is a conversation with an AI model. It holds the messages exchanged so far, the tools the model may call, and the settings applied to each request. RubyLLM.chat is the usual way to create one.
chat = RubyLLM.chat chat.ask "What's the best way to learn Ruby?"
Configuration methods return self, so calls chain:
chat = RubyLLM.chat(model: 'claude-sonnet-5') chat.with_instructions("Be terse.").with_tools(Weather)
ask runs the conversation loop, executing tools until the model answers or a call needs approval. ask_later, generate, run_tools, and step expose individual operations. Resume an approval pause with approve or deny followed by complete. queue adds a message from anywhere, even while the loop runs, and the loop takes it between two moves.
A Chat is Enumerable over its messages.
Constants
- COMPACTION_OPTIONS
-
The provider-neutral options
with_compactionaccepts.
Attributes
Hooks installed by the Rails integration.
The prompt caching options set with with_caching, false when explicitly disabled, or nil when not configured.
Hooks installed by the Rails integration.
Whether with_citations asked the provider for citations.
The code execution options set with with_code_execution, false when explicitly disabled, or nil when not configured.
The context compaction options set with with_compaction, false when explicitly disabled, or nil when not configured.
The tool concurrency mode, or nil when tools run sequentially.
The Context this chat sends requests through, or nil for the global configuration.
The opaque per-user identifier set with with_end_user, or nil.
The Fallback models tried in order when generation fails.
Extra HTTP headers set with with_headers.
Hooks installed by the Rails integration.
Hooks installed by the Rails integration.
Hooks installed by the Rails integration.
The output cap set with with_max_output_tokens, or nil.
Hooks installed by the Rails integration.
The Message objects exchanged so far, including system instructions.
The Provider instance handling requests for the current model.
Extra request options set with with_provider_options, expressed in the provider’s request vocabulary.
The provider tools enabled with with_provider_tools, as an array of normalized entry Hashes.
Hooks installed by the Rails integration.
Hooks installed by the Rails integration.
The normalized structured output schema set with with_schema, or nil.
The Skill objects given with with_skills, empty without skills.
The sampling temperature set with with_temperature, or nil to let the model use its default.
Hooks installed by the Rails integration.
Hooks installed by the Rails integration.
Hooks installed by the Rails integration.
Public Class Methods
# File lib/ruby_llm/chat.rb, line 125 def initialize(model: nil, provider: nil, protocol: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider raise ArgumentError, 'Provider must be specified if assume_model_exists is true' end @context = context @config = context&.config || RubyLLM.config with_model(model, provider: provider, protocol: protocol, assume_model_exists: assume_model_exists) @temperature = nil @max_output_tokens = nil @messages = [] @usage_entries = [] @tools = {} @mcp = MCP::Collection.new @tool_deferrals = {} @mcp_deferrals = {} @provider_tools = [] @tool_prefs = { choice: nil, calls: nil } @concurrency = normalize_tool_concurrency(@config.tool_concurrency) @provider_options = {} @headers = {} @schema = nil @thinking = nil @citations = false @caching = nil @compaction = nil @code_execution = nil @code_execution_tool = nil @skills = [] @skill_loader = build_skill_loader @file_references = nil @file_tools = [] @end_user = nil @fallbacks = [] @fallback_errors = Fallback::DEFAULT_ERRORS @interruptions = [] @callbacks = Hash.new { |callbacks, name| callbacks[name] = [] } @attachment_replacements = {}.compare_by_identity @cancelled = false @cancellation_checker = nil @tool_call_decisions = {} @remembered_tools = [] @used_escalations = [] @approval_checker = nil @remembered_checker = nil @tool_call_inputs = {} @input_checker = nil @input_recorder = nil @tool_call_claimer = nil @tool_call_releaser = nil @running_checker = nil @contested_tool_calls = [] @message_queue = build_message_queue end
Creates a chat with model:, or with the configured default model when model: is nil. Most code calls RubyLLM.chat instead.
A model is identified by its name, an optional provider:, and an optional protocol:. Pass provider: to disambiguate models available from several providers, and protocol: to override the wire protocol the provider would otherwise pick for the model. With assume_model_exists: true the registry lookup is skipped, which requires provider:. Pass a Context as context: to use its configuration instead of the global one.
Public Instance Methods
# File lib/ruby_llm/chat.rb, line 1381 def add_message(message_or_attributes) message = prepare_message(message_or_attributes) messages << message message end
Source
# File lib/ruby_llm/chat.rb, line 1186 def after_dequeue(&) add_callback(:after_dequeue, &) end
Registers a callback that receives each queued message as it joins the conversation: the user Message it became, then the QueuedMessage. It runs in the loop’s thread, before the model is asked to answer it. Returns self.
chat.after_dequeue { |message, queued| puts "Joined: #{message.content}" }
Source
# File lib/ruby_llm/chat.rb, line 1245 def after_fallback(&) add_callback(:after_fallback, &) end
Registers a callback that receives the Fallback attempt once it has succeeded or failed. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 1232 def after_file_write(&) add_callback(:after_file_write, &) end
Registers a callback that receives each StoredFile the conversation writes to the chat’s FileStore (see with_file_store): attachments as they are kept, long tool results, and the files tools and code write.
Source
# File lib/ruby_llm/chat.rb, line 1166 def after_message(&) add_callback(:after_message, &) end
Registers a callback that receives each assistant response and each tool result message once it has been appended. Returns self.
chat.after_message { |message| puts message.content }
Source
# File lib/ruby_llm/chat.rb, line 1175 def after_queue(&) add_callback(:after_queue, &) end
Registers a callback that receives each QueuedMessage once queue has added it. It runs in the thread that called queue. Returns self.
chat.after_queue { |queued| puts "Queued: #{queued.content}" }
Source
# File lib/ruby_llm/chat.rb, line 1225 def after_tool_progress(&) add_callback(:after_tool_progress, &) end
Registers a callback that receives the ToolCall and a Progress each time a running local tool reports what it is doing, with Tool#progress or, for MCP tools, through the server’s progress notifications. Returns self.
chat.after_tool_progress do |tool_call, progress| puts "#{tool_call.name}: #{progress.message}" end
The callback runs in the thread or fiber that reports, before the tool’s result. On Ruby 3.2 and later, that includes threads and fibers the tool starts. With concurrent tool execution, callbacks for different tool calls can run at the same time.
Source
# File lib/ruby_llm/chat.rb, line 1208 def after_tool_result(&) add_callback(:after_tool_result, &) end
Registers a callback that receives each local tool’s result after execution. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 1193 def after_unqueue(&) add_callback(:after_unqueue, &) end
Registers a callback that receives each QueuedMessage unqueue takes out of the queue. It runs in the thread that called unqueue. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 554 def answer(request, **values) settle_input(request) { |pending| pending.answer(**values) } end
Answers request, an MCP::InputRequest from pending_inputs, with values for a form, or none to accept a URL request. The next complete resumes the call once its requests are settled. Returns self.
# File lib/ruby_llm/chat.rb, line 429 def approve(tool_call, remember: false) name = remembered_tool_name(tool_call) if remember record_tool_call_decision(tool_call, true) @remembered_tools |= [name] if name self end
Records approval for tool_call, a ToolCall or its id, so the next complete or run_tools executes a local tool or records permission for the provider to execute a remote tool on the next request. Returns self.
chat.approve(tool_call) chat.complete
With remember: true, later calls to the same tool run without asking for the rest of the chat.
chat.approve(tool_call, remember: true)
A run_code call that asks for another sandbox is approved one call at a time: passing remember: true for it raises ArgumentError, and the approval never applies to another call.
# File lib/ruby_llm/chat.rb, line 231 def ask(message = nil, with: nil, &) ask_later(message, with: with) complete(&) end
Adds message as a user message and runs the conversation loop, executing tools until the model answers or a call needs approval. Returns the latest assistant Message; check waiting? before treating it as a final answer. Attach files with with:. An MCP::Prompt adds its messages instead. Messages still in the queue (see queue) join the conversation first, in order. A given block receives streamed Chunk objects as they arrive.
String attachments read local paths or fetch URLs. Only pass trusted, authorized sources; validate user uploads before calling this method. See Attachment.new.
chat.ask "What's the best way to learn Ruby?" chat.ask "What's in this image?", with: "ruby_conf.jpg" chat.ask "Analyze these files", with: ["diagram.png", "report.pdf"] chat.ask("Tell me a story") { |chunk| print chunk.content } chat.ask github.prompt(:code_review, code: diff)
# File lib/ruby_llm/chat.rb, line 252 def ask_later(message = nil, with: nil) raise_if_pending_tool_calls! take_queued_messages if message.is_a?(MCP::Prompt) message.messages.each { |prompt_message| add_message(prompt_message) } else add_message role: :user, content: message, attachments: with end self end
Stages message as a user message without requesting a completion, leaving the chat ready for complete, a single step, or a provider-side batch via RubyLLM.batch. Accepts attachments with with: like ask. Returns self.
chats = tickets.map { |t| RubyLLM.chat.ask_later(t.body) } RubyLLM.batch(chats)
Every message still in the queue (see queue) joins the conversation first, in order, whether it steers or not.
Raises PendingToolCallsError while the last response has unanswered tool calls: finish the round first, recording approve or deny decisions for calls that require approval.
Source
# File lib/ruby_llm/chat.rb, line 474 def awaiting_approval? waiting? && pending_approvals.any? end
Returns whether the conversation can make no progress without an approval decision: every remaining pending tool call requires approval and has none recorded, or waits on input. While true, complete returns without executing them; record decisions with approve or deny, then call complete again. Tool calls that need no approval still execute before the loop pauses.
Consults each pending tool’s approval resolver when one is declared, so resolvers must be idempotent reads.
Source
# File lib/ruby_llm/chat.rb, line 495 def awaiting_input? waiting? && pending_inputs.any? end
Returns whether the conversation can make no progress until the user answers an MCP server: every remaining pending tool call waits on input or an approval decision, and at least one waits on input.
Source
# File lib/ruby_llm/chat.rb, line 527 def awaiting_tasks? waiting? && pending_tasks.any? end
Returns whether the conversation can make no progress until tasks finish: every remaining pending tool call waits on a Task, input, or an approval decision, and at least one waits on a Task. complete resumes the calls again.
chat.ask "Render the quarterly report" chat.awaiting_tasks? # => true sleep chat.pending_tasks.first.poll_interval chat.complete
Source
# File lib/ruby_llm/chat.rb, line 1239 def before_fallback(&) add_callback(:before_fallback, &) end
Registers a callback that receives the Fallback attempt after the current model fails and before the fallback model is tried. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 1157 def before_message(&) add_callback(:before_message, &) end
Registers a callback that runs before each assistant response or tool result is appended to the conversation. Callbacks are additive: every registered block runs. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 1255 def before_request(&) add_callback(:before_request, &) end
Registers a callback that receives the fully rendered request payload before it is sent and may mutate it in place. Runs after all RubyLLM formatting and with_provider_options merging. Returns self.
chat.before_request { |payload| logger.debug payload }
Source
# File lib/ruby_llm/chat.rb, line 1202 def before_tool_call(&) add_callback(:before_tool_call, &) end
Registers a callback that receives each local ToolCall before the tool executes. Returns self.
chat.before_tool_call { |tool_call| puts tool_call.name }
Source
# File lib/ruby_llm/chat.rb, line 1404 def cache_until_here(ttl: nil) message = messages.last raise ArgumentError, 'No messages to cache' unless message message.cache_until_here(ttl:) self end
Marks the latest message as an explicit prompt cache boundary, asking the provider to cache everything up to this point. Pass ttl: to give this boundary its own cache lifetime, as Message#cache_until_here describes. Returns self.
chat.with_instructions(policy).cache_until_here(ttl: "1h")
Raises ArgumentError if the chat has no messages.
Source
# File lib/ruby_llm/chat.rb, line 569 def cancel @cancelled = true self end
Cancels the current in-flight chat operation. Every cancellation checkpoint raises CancelledError until the operation ends, so tool calls running concurrently all stop, and the flag is cleared then so the chat can be reused. The tasks that pending tool calls wait on are cancelled then too; see pending_tasks.
Source
# File lib/ruby_llm/chat.rb, line 575 def cancelled? @cancelled end
Returns whether this in-memory chat has been marked for cancellation.
Source
# File lib/ruby_llm/chat.rb, line 1340 def compact raise_if_cancelled! raise_if_pending_tool_calls! usage_start = usage_entries.length payload = instrumentation_payload(streaming: false) RubyLLM.instrument('compaction.ruby_llm', payload, config: @config) do |event| result = provider_compaction record_out_of_band_usage(result) if usage_entries.length == usage_start record_generated_message(result, usage_start) record_completion_event(event, result) result end rescue CancelledError @cancelled = false raise end
Compacts the conversation’s model context and returns an assistant Message. The message can have empty text and carries the provider’s compacted context internally. Every earlier message remains in messages, including on persisted Rails chats.
chat.ask "Remember these project requirements..." chat.compact chat.ask "Which requirement should we implement first?"
Uses the current instructions, headers, and request hooks. Records reported usage and runs the normal message callbacks. Raises Error when the provider has no manual compaction endpoint, and PendingToolCallsError until pending tool calls have been answered.
Source
# File lib/ruby_llm/chat.rb, line 333 def complete(&) run_tools if awaiting_tasks? continue_turn(steering_only: false, &) end
Runs the conversation loop until complete? or waiting? is true. Returns the last conversation Message, or nil for an empty chat. While the chat is waiting?, returns the assistant Message that holds the pending tool calls, even after other tools in the round have run. Used after ask_later; ask calls complete for you.
When a pending tool call requires approval and no decision has been recorded, or waits on input for an MCP server, the loop pauses. Record approve, deny, answer, or decline decisions, then call complete again to continue. A tool call that returns a Task pauses the loop too, and each complete resumes it once with Tool#resume, without waiting, and continues once it has a result. For an MCP tool, that checks on its MCP::Task.
The first time the loop could ask the model, every message in the queue joins the conversation, in order. After that, only messages that steer join, after each round of tool results and after the answer, so the turn goes on until the model has answered them. Messages queued with steer: false wait for the next complete. See queue.
Source
# File lib/ruby_llm/chat.rb, line 355 def complete? settled?(true) end
Returns whether the chat has no pending response or tool execution: nothing is staged, or the model answered without requesting tools, and no queued message steers the turn. Messages queued with steer: false don’t count, so the chat is complete while they wait for the next complete.
# File lib/ruby_llm/chat.rb, line 1271 def convert_unsupported_attachments(&) add_callback(:unsupported_attachment, &) end
Converts attachments the current protocol cannot render with the given block. Returns self. The block receives an Attachment and returns a replacement Attachment, or nil to keep the existing error. Blocks run in registration order until one returns a replacement. The transcript keeps the original file, even when the model changes. The chat converts each attachment once and reuses the replacement, and its upload, on later requests.
chat.convert_unsupported_attachments do |attachment| text = TextExtraction.call(attachment) Attachment.new(StringIO.new(text), filename: 'extracted.txt') if text end
Source
# File lib/ruby_llm/chat.rb, line 1296 def cost Cost.aggregate(usage_entries.map(&:cost), complete: usage_entries.all?(&:cost_available?)) end
Returns a Cost aggregating every provider attempt this chat has made, including retries and attempts that produced no message.
chat.cost.total
Source
# File lib/ruby_llm/chat.rb, line 1311 def count_tokens(message = nil) request_messages = messages.dup request_messages << coerce_message(role: :user, content: message) unless message.nil? @provider.count_tokens( preprocessed_messages(request_messages), model: @model, tools: request_tools, tool_prefs: @tool_prefs, thinking: resolved_thinking, schema: @schema, citations: @citations, caching: @caching, protocol: @protocol ) end
Counts input tokens for the conversation, including instructions, function tools, structured output, thinking, and attachments. Pass message to include it as a staged user message without mutating the chat. Returns an Integer.
chat.with_instructions("Be terse.").with_tools(Weather) chat.count_tokens("What's the weather in Berlin?")
Provider tools, provider_options, compaction, and before_request hooks are not included. Raises Error when the provider has no token counting endpoint.
Source
# File lib/ruby_llm/chat.rb, line 560 def decline(request) settle_input(request, &:decline) end
Declines request, an MCP::InputRequest from pending_inputs. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 658 def deferred_tools tools.slice(*deferred_tool_names) end
Returns the tools whose definitions stay out of the model’s context until the provider’s tool search loads them, as a Hash like tools. Providers without tool search receive them as ordinary tools.
chat.with_tools(Weather).with_mcp(GitHub, defer: true) chat.deferred_tools.keys # => [:search_issues, :create_issue, ...]
Source
# File lib/ruby_llm/chat.rb, line 441 def deny(tool_call) record_tool_call_decision(tool_call, false) end
Source
# File lib/ruby_llm/chat.rb, line 1278 def each(&) messages.each(&) end
Source
# File lib/ruby_llm/chat.rb, line 1080 def file_store @file_references&.store end
Returns the FileStore attached with with_file_store, or nil without one.
chat.file_store.list chat.file_store.read("notes/plan.md")
Source
# File lib/ruby_llm/chat.rb, line 268 def generate(&) raise_if_cancelled! return generate_once(&) if fallbacks.empty? with_model_restored { generate_with_fallbacks(&) } rescue CancelledError @cancelled = false raise end
Requests one completion from the model, appends the response to the conversation, and returns it as a Message. Honors the fallbacks configured with with_fallbacks. A given block receives streamed Chunk objects. Tool calls in the response are not executed; that is run_tools.
Source
# File lib/ruby_llm/chat.rb, line 1360 def messages=(new_messages) @messages = message_list(new_messages).map { |message| coerce_message(message) } end
Source
# File lib/ruby_llm/chat.rb, line 485 def pending_approvals response = pending_tool_response return [] unless response pending_tool_calls(response).values.select { |tool_call| approval_pending?(tool_call) } end
Returns the tool calls from the latest response that require approval and have no recorded decision, as an array of ToolCall objects. Pairs with approve and deny. ToolCall#remote? identifies provider-executed calls.
chat.pending_approvals.each { |tool_call| puts tool_call.name } chat.approve(chat.pending_approvals.first)
Source
# File lib/ruby_llm/chat.rb, line 506 def pending_inputs response = pending_tool_response return [] unless response pending_tool_calls(response).values.flat_map do |tool_call| Array(tool_call_input(tool_call)&.[]('requests')) .map { |data| MCP::InputRequest.from_h(data, tool_call:) } .reject(&:answered?) end end
Returns the unanswered requests for input that paused MCP tool calls, as MCP::InputRequest objects. Settle each with answer or decline, then call complete to resume the calls.
chat.pending_inputs.each { |request| puts request.message } chat.answer(chat.pending_inputs.first, environment: "staging")
Source
# File lib/ruby_llm/chat.rb, line 540 def pending_tasks response = pending_tool_response return [] unless response pending_tool_calls(response).values.filter_map do |tool_call| state = tool_call_input(tool_call) task_for(tool_call, state) if state&.key?('task') && !unanswered?(state) end end
Returns the Task objects that tool calls wait on, as they were when the chat last checked, with an MCP::Task for each MCP tool call. Task#refresh checks on one without resuming the chat; complete resumes every call with Tool#resume. A task that is Task#input_required? waits on a person, so there is no point in checking on it before they act.
chat.pending_tasks.each { |task| puts "#{task.tool_call.name}: #{task.status_message}" }
# File lib/ruby_llm/chat.rb, line 381 def queue(message = nil, with: nil, steer: true, owner: nil) queued = @message_queue.push(content: message, attachments: with, steer:, owner:) run_callbacks(:after_queue, queued) queued end
Adds message to the queue: a user message that waits outside the conversation until the loop takes it. Call it at any time, from any thread, even while complete runs in another one. Attach files with with:, as with ask. Returns the QueuedMessage.
chat.queue "Use the EU file instead" chat.queue "Then email it to me", steer: false chat.queue "Check this one too", with: "q3.pdf", owner: current_user
A message that steers, the default, joins the turn that is running: after the next round of tool results, or once the model finishes its answer, which then goes on to answer it too. Pass steer: false to wait for the turn to end instead: complete returns, the chat is complete?, and the next ask, ask_later, complete, or step takes the message. Steering messages can join before waiting ones queued earlier. Messages that join together become separate user messages, oldest first.
Nothing is taken while the chat is waiting?, and cancel leaves the queue as it is. Queueing never runs the loop: when nothing is running, call complete. owner is stored with the message and handed back on it without RubyLLM interpreting it.
Source
# File lib/ruby_llm/chat.rb, line 389 def queued_messages @message_queue.to_a end
Returns the messages waiting in the queue, as QueuedMessage objects, oldest first.
Source
# File lib/ruby_llm/chat.rb, line 1429 def render @provider.render( preprocessed_messages, tools: request_tools, provider_tools: @provider_tools, tool_prefs: @tool_prefs, temperature: @temperature, max_output_tokens: @max_output_tokens, model: @model, provider_options: Support::Utils.deep_dup(@provider_options), schema: @schema, thinking: resolved_thinking, citations: @citations, caching: @caching, compaction: @compaction, end_user: @end_user, protocol: @protocol, before_request: @callbacks[:before_request] ) end
Returns the request payload this chat would send to the provider for its next completion, with before_request hooks applied. Useful for inspecting and testing request output.
Source
# File lib/ruby_llm/chat.rb, line 289 def run_tools raise_if_cancelled! message = pending_tool_response execute_pending_tool_calls(message) if message self rescue CancelledError @cancelled = false cancel_tasks raise end
Executes the tool calls pending in the latest response and appends their result messages, without asking the model to respond. Tool calls that already have results are skipped, so a chat reloaded mid-round resumes with only the remaining tools. Calls whose tool was declared with Tool.requires_approval only execute once approve records a decision; denied calls receive a structured denial result, and undecided calls stay pending. Does nothing when no tool calls are pending. Calls paused on a Task resume once. The chat is then ready for the next generate, or the next batch round. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 309 def step(&) advance(steering_only: false, &) end
Advances the conversation by one move: runs the pending tool calls if any are unanswered, otherwise generates the next response. Returns the Message that move produced, and nil once there is nothing left to do or the chat is waiting?.
Unless tool calls are pending, every message in the queue (see queue) joins the conversation before the model is asked, whether it steers or not.
Source
# File lib/ruby_llm/chat.rb, line 856 def thinking config = @thinking&.resolve(@model, strict: false) return unless config { effort: config.effort, budget: config.budget, display: config.display, enabled: config.enabled }.compact end
Returns the thinking options resolved for the current model, or nil when thinking was not configured or needs no provider control. When the model registry has no thinking controls for the model, returns the request itself, { enabled: true } or { enabled: false }; the request then raises ArgumentError.
Source
# File lib/ruby_llm/chat.rb, line 1287 def tokens Tokens.aggregate(usage_entries.map(&:tokens)) end
Returns token usage aggregated across every provider attempt this chat has made, including retries and attempts that produced no message.
chat.tokens.input
Source
# File lib/ruby_llm/chat.rb, line 111 def tool_options { choice: tool_prefs[:choice], calls: tool_prefs[:calls], concurrency: concurrency } end
Returns the choice, calls, and concurrency set with with_tool_options, with nil for anything left at the default.
Source
# File lib/ruby_llm/chat.rb, line 638 def tools offered = @tools.reject { |_, tool| hidden_from_model?(tool) } built_in = [@code_execution_tool, (@skill_loader if skills.any?), *@file_tools, tool_search_tool] [*built_in, *mcp.flat_map(&:tools)].compact.each_with_object(offered) do |tool, tools| next if hidden_from_model?(tool) name = tool.name.to_sym raise ArgumentError, "Two tools are named #{name}. Rename one with `tool :#{name}, as:`" if tools.key?(name) tools[name] = tool end end
Returns the tools the model can call, as a Hash of tool name Symbols to Tool instances: those registered with with_tools and those of the MCP servers connected with with_mcp. MCP tools that only the UI of an MCP App may call are left out; see MCP::Tool#visibility.
Raises ArgumentError when two tools share a name.
Source
# File lib/ruby_llm/chat.rb, line 399 def unqueue(queued) removed = @message_queue.delete(queued) run_callbacks(:after_unqueue, removed) if removed !removed.nil? end
Takes queued, a QueuedMessage or its id, out of the queue before the loop takes it. Returns true, or false when it is no longer queued: the loop took it into the conversation, or it was unqueued already.
chat.unqueue(queued) # => true
Source
# File lib/ruby_llm/chat.rb, line 454 def waiting? response = pending_tool_response return false unless response pending = pending_tool_calls(response).values pending.any? && pending.all? do |tool_call| approval_pending?(tool_call) || paused?(tool_call) || running_elsewhere?(tool_call) end end
Returns whether the conversation can’t continue until something outside it happens: every remaining pending tool call waits on an approval decision, an answer to an MCP input request, a Task, or a run in another process. complete stops here, and awaiting_approval?, awaiting_input?, and awaiting_tasks? tell what it waits on. Stop a loop you drive with step here too:
chat.step until chat.complete? || chat.waiting?
Source
# File lib/ruby_llm/chat.rb, line 897 def with_caching(options = {}) options = {} if options == true unless options == false || options.is_a?(Hash) raise ArgumentError, 'with_caching accepts true, false, or caching options' end @caching = options == false ? false : options.transform_keys(&:to_sym).freeze self end
Enables provider prompt caching. With no arguments the provider’s default behavior applies; options such as ttl: apply where supported. Pass id: with a CachedContent (or its name) from RubyLLM.cache to attach an explicit content cache. Pass false to stop RubyLLM from sending cache controls or rendering explicit cache boundaries. A provider may still cache prompts implicitly. Passing nil raises ArgumentError. Returns self.
chat.with_caching chat.with_caching(ttl: "1h") chat.with_caching(id: cache) chat.with_caching(false)
# File lib/ruby_llm/chat.rb, line 876 def with_citations(enabled = true) raise ArgumentError, 'with_citations accepts true or false' unless [true, false].include?(enabled) @citations = enabled self end
Enables document citations, so the model backs its claims with quotes from attached files. Pass false to disable. Passing nil raises ArgumentError. Returns self.
chat.with_citations response = chat.ask "Who created Ruby?", with: "facts.txt" response.citations.each { |citation| puts citation.cited_text }
# File lib/ruby_llm/chat.rb, line 987 def with_code_execution(enabled = true, **options) options = CodeExecution.options(enabled, options) @code_execution_tool = if options CodeExecution.new( sandbox: options.fetch(:sandbox) { -> { @config.sandbox } }, attachments: -> { messages.flat_map(&:attachments) }, file_store: -> { file_store }, written: ->(file) { run_callbacks(:after_file_write, file) }, skills: -> { skills.to_h { |skill| [Skill::Loader.mount_path(skill), skill.path] } }, chat: self, config: -> { @config }, package_cache_dir: @config.package_cache_dir, approval: ->(tool_call) { escalation_approved?(tool_call) }, **options.except(:sandbox) ) end @code_execution = options self end
Lets the model run code in a Sandbox. The model gets one tool, run_code, that runs Ruby, Python, JavaScript, or a shell script in a fresh run directory, with read-only copies of the conversation’s files, and returns the output and every file the code writes there as attachments on the tool result. With with_file_store, runs work with the chat’s files instead: in the store’s folder when it has one (FileStore#local_root), otherwise on copies of the files the model names, with what the code writes saved back. The options are:
sandbox-
the sandbox every call runs in. Default:
Configuration#sandbox. escalate_to-
sandboxes the model may ask for, one call at a time, as an Array named after their classes (WorkspaceSandbox is
workspace) or a Hash of names to sandboxes. Each call that asks waits forapprove. languages-
the languages the model may use, from
:ruby,:python,:javascript, and:shell. Default: Ruby and shell, plus Python and JavaScript when their packages are declared. gems-
gems installed once and available to every Ruby run.
python-
Python packages installed once and available to every Python run.
npm-
npm packages installed once, without their install scripts, and available to every JavaScript run.
gem_home-
a GEM_HOME with gems already installed, used instead of
gems. virtualenv-
a Python virtualenv with packages already installed, used instead of
python. node_modules-
a
node_modulesdirectory with packages already installed, used instead ofnpm. mounts-
directories mounted read-only in each run directory, keyed by their path there, or a Proc called before each run that returns them.
Accepts keywords or an options Hash. Pass false to disable it. Passing nil raises ArgumentError. Returns self.
chat.with_code_execution chat.with_code_execution(gems: %w[prawn], python: %w[pandas matplotlib]) chat.with_code_execution(sandbox: ReadOnlySandbox, escalate_to: [WorkspaceSandbox, OnlineSandbox]) chat.with_code_execution(mounts: { "templates" => "app/templates/reports" }) chat.with_code_execution(false)
Raises ConfigurationError when no sandbox is configured. Code never runs outside a sandbox. To run code on the provider’s servers instead, use with_provider_tools(:code_execution).
# File lib/ruby_llm/chat.rb, line 931 def with_compaction(options = {}) options = {} if options == true unless options == false || options.is_a?(Hash) raise ArgumentError, 'with_compaction accepts true, false, or compaction options' end @compaction = options == false ? false : normalize_compaction(options) self end
Enables provider-side context compaction, so a long conversation keeps going instead of overflowing the context window. The provider condenses the earlier turns itself and returns a block that RubyLLM replays on later requests. With no arguments the provider’s own defaults apply. The options are provider-neutral:
at-
the input-token count that triggers compaction.
instructions-
a custom prompt for the summary the provider writes.
pause_after-
end the turn once compaction runs, instead of continuing straight into the answer.
Each provider applies the options it supports. Unsupported options are ignored with a debug log. Pass false to disable; passing nil raises ArgumentError. Returns self.
chat.with_compaction chat.with_compaction(at: 50_000) chat.with_compaction(at: 100_000, instructions: "Keep every decision.") chat.with_compaction(false)
What a provider does when the threshold is crossed differs. Anthropic and OpenAI summarize the compacted span into an opaque block that replaces it; OpenRouter drops messages from the middle of the conversation instead, and has no threshold of its own.
Source
# File lib/ruby_llm/chat.rb, line 1101 def with_context(context) @context = context @config = context&.config || RubyLLM.config with_model(@model.id, provider: @provider.slug, protocol: @protocol, assume_model_exists: true) self end
Rebinds the chat to context, a Context built with RubyLLM.context, so subsequent requests use its configuration. Pass nil to return to the global RubyLLM.config. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 1093 def with_end_user(end_user) @end_user = end_user self end
Identifies the end user behind the conversation for the provider’s abuse monitoring. Providers without an equivalent field omit it. Pass nil to remove it. Returns self.
chat.with_end_user("user-123").ask "Hello"
The value is sent as given, so use an opaque id such as a hash of your user id, never personal data.
# File lib/ruby_llm/chat.rb, line 787 def with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS) fallback_models = models.flatten.compact @fallbacks = fallback_models.map { |model| Fallback.build(model) } @fallback_errors = fallback_models.empty? ? Fallback::DEFAULT_ERRORS : Array(on).flatten.compact self end
Sets fallback models to try, in order, when generation fails. on: selects the error classes that trigger a fallback; the default covers transient provider and network errors. Cancelling the chat never triggers a fallback, and neither does another process taking over a persisted chat or an error your streaming block raises. Pass nil to remove all fallbacks and restore the default error classes. Returns self.
chat.with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5") chat.with_fallbacks(nil)
Source
# File lib/ruby_llm/chat.rb, line 1065 def with_file_store(store = RubyLLM.file_store.memory, spill_to_file: FileReferences::SPILL_TO_FILE) FileReferences.validate(store, spill_to_file:) unless store.nil? @file_references = if store FileReferences.new(store:, spill_to_file:, written: ->(file) { run_callbacks(:after_file_write, file) }) end @file_tools = store ? build_file_tools : [] self end
Attaches store, a FileStore, to the chat and gives the model five tools to work with it: list_files, read_file, search_files, write_file, and edit_file. Files stop filling the conversation:
-
Every attachment is kept in the store. The model sees the files attached to the latest question, and those it opens with
read_file, in full until the next question; after that, and for files other tools return, it sees one line naming the file. -
A tool result longer than
spill_to_filecharacters is saved underresults/, and the model gets its start and the file’s path. -
With
with_code_execution, code runs in the store’s folder when it has one, such asFileStore::Directory, changing the files in place. With other stores, each run copies in the files the model names, and what the code writes is saved back to the store.
Without store, the chat gets a new FileStore::Memory, or for a persisted chat, its files in Active Storage. spill_to_file is the characters a tool result may have before it is saved as a file, or false to send every result whole; tools can set their own with Tool.spill_to_file. Default: 10,000.
Pass nil to remove the store and its tools. Returns self.
chat.with_file_store chat.with_file_store(RubyLLM.file_store.directory("tmp/chats/42")) chat.with_file_store(spill_to_file: 20_000) chat.with_file_store(nil)
Text files are read as text; register converters, such as the OCR converter for PDFs and images, in Configuration#file_converters to read more.
Source
# File lib/ruby_llm/chat.rb, line 1124 def with_headers(headers) @headers = headers.to_h self end
Sets extra HTTP headers sent with completion requests, replacing any previously set headers; nil clears them. Returns self.
chat.with_headers('anthropic-beta' => 'fine-grained-tool-streaming-2025-05-14')
# File lib/ruby_llm/chat.rb, line 592 def with_instructions(instructions, append: false, cache_until_here: false) boundary = Message.cache_boundary_options(cache_until_here) if cache_until_here @messages.reject! { |message| message.role == :system } unless append @messages << Message.new(role: :system, content: instructions) unless instructions.nil? @messages.last.cache_until_here(**boundary) if instructions && boundary self end
Sets the system instructions for the conversation, replacing any existing system messages. With append: true the instructions are added alongside the existing ones. With cache_until_here: true the instruction becomes an explicit prompt cache boundary; pass cache_until_here: { ttl: "1h" } to give that boundary its own lifetime, as Message#cache_until_here describes. Pass nil to remove all system instructions. Returns self.
chat.with_instructions "You are a helpful Ruby tutor." chat.with_instructions "Use exactly one short paragraph.", append: true chat.with_instructions policy, cache_until_here: { ttl: "1h" } chat.with_instructions nil
# File lib/ruby_llm/chat.rb, line 810 def with_max_output_tokens(max_output_tokens) @max_output_tokens = max_output_tokens self end
Caps the number of tokens the model may generate. Pass nil to remove the limit. Returns self.
chat.with_max_output_tokens(1000)
# File lib/ruby_llm/chat.rb, line 680 def with_mcp(*servers, defer: nil) if servers == [nil] @mcp.disconnect(self) @mcp = MCP::Collection.new @mcp_deferrals.clear else servers.flatten.compact.each do |server| server = @mcp.connect(server, self) @mcp_deferrals[server.name.to_sym] = defer end end self end
Connects MCP servers, each an MCP instance or class, and gives the model their tools. The servers are contacted when the chat first needs their tools. Pass nil to disconnect them all. Returns self.
The chat closes the servers it built from a class when it disconnects them, or when another server takes their name. Instances you pass in stay open: close them with MCP#close.
chat.with_mcp(Linear.new(user: current_user), Files) chat.mcp.linear
Pass defer: true to keep every tool of these servers out of the model’s context until the provider’s tool search loads it, or defer: false to offer them up front despite MCP.defer. See deferred_tools.
chat.with_mcp(GitHub, defer: true)
# File lib/ruby_llm/chat.rb, line 768 def with_model(model_id, provider: nil, protocol: nil, assume_model_exists: false) model_id ||= @config.default_model @model, @provider = Models.resolve(model_id, provider:, assume_model_exists:, config: @config) @connection = @provider.connection @protocol = protocol self end
Switches the chat to model_id and its provider. Pass provider: to disambiguate, and assume_model_exists: true to skip registry validation for custom or private models. Pass nil to return to the configured default model. Returns self.
protocol: overrides the wire protocol the provider would pick for the model, such as :responses or :chat_completions for OpenAI. It stays nil by default, meaning the provider chooses the protocol for each request. A bare with_model resets the override to nil, just as it re-resolves the provider from the model.
Raises ModelNotFoundError if model_id is not in the registry and assume_model_exists: is false.
chat.with_model('claude-sonnet-5') chat.with_model('gpt-6.1-sol', protocol: :chat_completions)
# File lib/ruby_llm/chat.rb, line 1114 def with_provider_options(provider_options) @provider_options = provider_options.to_h self end
Sets options in the provider’s request vocabulary, merged into the request payload as-is and overriding RubyLLM’s defaults. Replaces any previously set provider options; nil clears them. Returns self.
chat.with_provider_options(service_tier: "flex")
# File lib/ruby_llm/chat.rb, line 715 def with_provider_tools(*tools, **tools_with_options) if tools == [nil] && tools_with_options.empty? @provider_tools = [] return self end additions = RubyLLM::Tools::ProviderTools.normalize(tools, tools_with_options) @provider_tools = RubyLLM::Tools::ProviderTools.merge(@provider_tools, additions) self end
Enables tools that run on the provider’s servers, such as web search or code execution. Accepts portable alias Symbols, alias-with-options keywords whose options use the provider’s own vocabulary, and raw Hashes passed to the provider verbatim, so provider tools RubyLLM has no alias for yet work without a gem update. Entries add to any tools enabled earlier. Enabling an alias again replaces its options, and an identical raw Hash or MCP server is not repeated, so a chat sends each tool once. Pass nil to clear them all. Returns self.
chat.with_provider_tools(:web_search) chat.with_provider_tools(:web_search, :code_execution) chat.with_provider_tools(web_search: { allowed_domains: ["ruby-lang.org"] }) chat.with_provider_tools({ type: "web_search_20260318", name: "web_search" })
The tool steps the model ran come back on Message#server_tool_calls, citations from search tools on Message#citations, and per-use billing counters on message.tokens.server_tool_use.
Raises UnsupportedServerToolError at request time when the provider has no provider-tool support or does not define a requested alias.
Source
# File lib/ruby_llm/chat.rb, line 1144 def with_schema(schema) schema_instance = schema.is_a?(Class) ? schema.new : schema @schema = normalize_schema_payload( schema_instance.respond_to?(:to_json_schema) ? schema_instance.to_json_schema : schema_instance ) self end
Sets the schema for structured output. Accepts a JSON Schema Hash, a Schematist::Schema class or instance, or any object responding to to_json_schema. Returns self.
class PersonSchema < Schematist::Schema string :name integer :age end chat.with_schema(PersonSchema) response = chat.ask("Generate a person named Alice who is 30 years old") response.parsed # => {"name" => "Alice", "age" => 30}
Pass nil to remove the schema, returning the chat to plain text responses.
Source
# File lib/ruby_llm/chat.rb, line 1028 def with_skills(*skills) selection = Skill.selection(skills) @skills = selection == false ? [] : Skill.merge(@skills, Skill.resolve(selection)).freeze self end
Gives the model skills: folders of instructions, scripts, and files in the open Agent Skills format. Called bare or with true, adds every skill directly under Skill.roots, app/skills by default. Symbols name folders under the roots, and Strings and Pathnames are paths to skill folders. Pass false to remove every skill. Passing nil raises ArgumentError. Returns self.
chat.with_skills chat.with_skills(:invoices, :reports) chat.with_skills(:invoices, "vendor/skills/pdf") chat.with_skills(false)
Every request lists each skill’s name and description, and the model loads a skill’s instructions and files with its load_skill tool. With with_code_execution, each skill folder is mounted read-only in every run at skills/<name>/, so the model can run the skill’s scripts.
Raises SkillNotFoundError for a missing skill, InvalidSkillError for a SKILL.md that breaks the specification, and ArgumentError when two skills share a name.
# File lib/ruby_llm/chat.rb, line 799 def with_temperature(temperature) @temperature = temperature self end
Sets the sampling temperature for subsequent requests. Pass nil to return to the model’s default sampling behavior. Returns self.
chat.with_temperature(0.2)
# File lib/ruby_llm/chat.rb, line 831 def with_thinking(enabled = true, **options) # rubocop:disable Metrics/PerceivedComplexity return with_thinking(**enabled.transform_keys(&:to_sym), **options) if enabled.is_a?(Hash) raise ArgumentError, 'with_thinking accepts false or thinking options' unless [true, false].include?(enabled) raise ArgumentError, 'with_thinking(false) does not accept options' if !enabled && options.any? raise ArgumentError, 'thinking options cannot be nil; pass false to disable' if options.value?(nil) if (unsupported = options.keys - THINKING_OPTIONS).any? raise ArgumentError, "with_thinking accepts #{format_option_keys(THINKING_OPTIONS)}, " \ "got #{format_option_keys(unsupported)}" end @thinking = if enabled options.empty? ? Thinking::Config.default : Thinking::Config.new(**options) else Thinking::Config.disabled end self end
Configures extended thinking for models that support it. With no arguments, RubyLLM uses the current model’s registered default. Pass false to disable thinking, or tune it with effort: (:low, :medium, :high, :none, or a provider-specific tier such as :minimal, :xhigh, or :max, passed through as-is), budget: (a token count), and display: (:summarized or :omitted, controlling whether providers that support it return readable thinking text). Accepts keywords or an options Hash. Passing nil raises ArgumentError. Returns self.
chat.with_thinking chat.with_thinking(false) chat.with_thinking(effort: :high) chat.with_thinking(budget: 10_000) chat.with_thinking(display: :summarized)
# File lib/ruby_llm/chat.rb, line 739 def with_tool_options(**options) options.each do |option, value| case option when :choice then apply_tool_choice(value) when :calls then @tool_prefs[:calls] = value.nil? ? nil : normalize_calls(value) when :concurrency then @concurrency = normalize_tool_concurrency(value.nil? ? @config.tool_concurrency : value) else raise ArgumentError, "Unknown tool option: #{option}. Valid options are: choice, calls, concurrency" end end self end
Configures how the model uses the registered tools. choice: constrains tool use to :auto, :none, :required, a tool name, or a Tool class. calls: limits how many tool calls one response may contain (:many or :one). concurrency: runs tool calls concurrently: true or :threads for threads, :fibers for fibers. An omitted option is left unchanged; passing nil explicitly resets that option (+concurrency: nil+ returns to the configured default). Returns self.
chat.with_tools(Weather, Search).with_tool_options(choice: :required) chat.with_tool_options(calls: :one, concurrency: :threads) chat.with_tool_options(choice: nil)
# File lib/ruby_llm/chat.rb, line 618 def with_tools(*tools, defer: nil) if tools == [nil] @tools.clear @tool_deferrals.clear end tools.flatten.compact.each do |tool| tool = tool.new if tool.is_a?(Class) name = tool.name.to_sym @tools[name] = tool @tool_deferrals[name] = defer.nil? ? tool.deferred? : defer end self end
Registers tools, each a Tool class or instance, for the model to call. Configure how the model uses them with with_tool_options. Pass nil to remove all registered tools. Returns self.
chat.with_tools(Weather, Search) chat.with_tools(Weather).with_tool_options(choice: :required)
Pass defer: true to keep the tools’ definitions out of the model’s context until the provider’s tool search loads the ones it needs, or defer: false to offer a tool declared with Tool.defer up front. See deferred_tools.
chat.with_tools(*Catalog.tools, defer: true)
To replace the registered tools, clear them first:
chat.with_tools(nil).with_tools(NewTool)