Class: RubyLLM::Agent

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Enumerable
Defined in:
lib/ruby_llm/agent.rb

Overview

An Agent is a reusable chat configuration defined as a class. Subclasses declare a model, instructions, tools, and other settings once, then build configured chats wherever they are needed.

class SupportAgent < RubyLLM::Agent
model "gpt-5.6-luna"
instructions "You are a concise support assistant."
tools SearchDocs, LookupAccount
end

SupportAgent.new.ask "How do I reset my API key?"

::chat returns a configured Chat. When ::chat_model names an ActiveRecord chat class, ::create, ::create!, and ::find return configured records of that class instead.

Configuration that depends on runtime state goes in blocks or lambdas. They are evaluated when a chat is built, with chat and any declared ::inputs available as methods:

class WorkAssistant < RubyLLM::Agent
inputs :workspace

instructions { "You are helping #{workspace.name}" }
end

WorkAssistant.chat(workspace: workspace)

Agent instances delegate Chat's conversation API (#ask, #complete, #with_tools, and so on) to the wrapped chat, which is available via #chat. Direct transcript replacement stays on the wrapped chat because Rails-backed chat models own their message association. Agents are enumerable over their messages.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs) ⇒ Agent

Returns a new agent wrapping chat:, or wrapping a newly built chat when chat: is nil. Applies the agent's configuration either way. Keywords matching declared inputs (and the inputs: hash) become runtime inputs; the rest are forwarded to RubyLLM.chat when the agent builds its own chat. Pass persist_instructions: false to apply instructions at runtime only, without persisting them on a Rails-backed record.

agent = WorkAssistant.new
agent.ask "Hello"

record = Chat.find(params[:id])
WorkAssistant.new(chat: record)


849
850
851
852
853
854
# File 'lib/ruby_llm/agent.rb', line 849

def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
  input_values, chat_options = self.class.partition_inputs(kwargs)
  input_values = input_values.merge(inputs || {})
  @chat = chat || self.class.build_chat(inputs: input_values, options: chat_options)
  self.class.apply_configuration(@chat, input_values:, persist_instructions:)
end

Instance Attribute Details

#chatObject (readonly)

The wrapped Chat, or the chat record in Rails mode.



857
858
859
# File 'lib/ruby_llm/agent.rb', line 857

def chat
  @chat
end

Class Method Details

.apply_configuration(chat, input_values:, persist_instructions:) ⇒ Object

:nodoc:



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/ruby_llm/agent.rb', line 566

def apply_configuration(chat, input_values:, persist_instructions:) # :nodoc:
  runtime = runtime_context(chat:, inputs: input_values)
  apply_chat_options(chat)
  apply_context(chat)
  apply_instructions(chat, runtime, inputs: input_values, persist: persist_instructions)
  apply_tools(chat, runtime)
  apply_passthrough_options(chat)
  apply_thinking(chat)
  apply_citations(chat)
  apply_end_user(chat, runtime)
  apply_caching(chat, runtime)
  apply_compaction(chat)
  apply_provider_options(chat, runtime)
  apply_headers(chat, runtime)
  apply_schema(chat, runtime)
  apply_fallbacks(chat)
end

.build_chat(inputs:, options:) ⇒ Object

:nodoc:



463
464
465
# File 'lib/ruby_llm/agent.rb', line 463

def build_chat(inputs:, options:) # :nodoc:
  (context || RubyLLM).chat(**resolved_chat_kwargs(inputs:), **options)
end

.caching(enabled = true, **options, &block) ⇒ Object

Enables prompt caching for chats this agent builds, applied via Chat#with_caching. With no options, the provider's default behavior applies. Pass false to stop RubyLLM from sending cache controls. A provider may still cache prompts implicitly. A block defers evaluation until the chat is built. Accepts keywords or an options Hash.

caching
caching false
caching ttl: "1h"
caching { { ttl: workspace.cache_ttl } }

Raises:



294
295
296
297
298
299
300
301
302
# File 'lib/ruby_llm/agent.rb', line 294

def caching(enabled = true, **options, &block) # rubocop:disable Metrics/PerceivedComplexity, Style/OptionalBooleanParameter
  return caching(**enabled.transform_keys(&:to_sym), **options, &block) if enabled.is_a?(Hash)

  raise ArgumentError, 'caching accepts false or caching options' unless [true, false].include?(enabled)
  raise ArgumentError, 'caching accepts options or a block, not both' if options.any? && block
  raise ArgumentError, 'caching false does not accept options or a block' if !enabled && (options.any? || block)

  @caching = block || (enabled ? options : false)
end

.chat(**kwargs) ⇒ Object

Builds a Chat configured with this agent's declarations and returns it. Keywords matching declared ::inputs become runtime inputs; the rest are forwarded to RubyLLM.chat.

chat = WorkAssistant.chat
chat.ask "Hello"


474
475
476
477
478
479
# File 'lib/ruby_llm/agent.rb', line 474

def chat(**kwargs)
  input_values, chat_options = partition_inputs(kwargs)
  chat = build_chat(inputs: input_values, options: chat_options)
  apply_configuration(chat, input_values:, persist_instructions: true)
  chat
end

.chat_kwargsObject

:nodoc:



452
453
454
# File 'lib/ruby_llm/agent.rb', line 452

def chat_kwargs # :nodoc:
  @chat_kwargs || {}
end

.chat_model(value = nil) ⇒ Object

Sets the ActiveRecord chat class this agent creates and finds, activating Rails mode (::create, ::create!, ::find, and ::sync_instructions). Accepts the class or its name as a string. Called with no argument, returns the configured value.

chat_model Chat


381
382
383
384
385
386
# File 'lib/ruby_llm/agent.rb', line 381

def chat_model(value = nil)
  return @chat_model if value.nil?

  @chat_model = value
  remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
end

.citations(value = true) ⇒ Object

Enables citations for chats this agent builds, applied via Chat#with_citations. Pass false to disable them.

citations
citations false

Raises:



277
278
279
280
281
# File 'lib/ruby_llm/agent.rb', line 277

def citations(value = true) # rubocop:disable Style/OptionalBooleanParameter
  raise ArgumentError, 'citations accepts true or false' unless [true, false].include?(value)

  @citations = value
end

.compaction(options = {}) ⇒ Object

Enables context compaction for chats this agent builds, applied via Chat#with_compaction. With no options, the provider's own defaults apply. Pass false to disable it.

compaction
compaction false
compaction at: 50_000


248
249
250
251
252
253
254
255
# File 'lib/ruby_llm/agent.rb', line 248

def compaction(options = {})
  options = {} if options == true
  unless options == false || options.is_a?(Hash)
    raise ArgumentError, 'compaction accepts true, false, or compaction options'
  end

  @compaction = options
end

.context(value = nil) ⇒ Object

Sets a Context whose configuration chats this agent builds should use, applied via Chat#with_context. Called with no argument, returns the configured context.



368
369
370
371
372
# File 'lib/ruby_llm/agent.rb', line 368

def context(value = nil)
  return @context if value.nil?

  @context = value
end

.create(**kwargs) ⇒ Object

Creates a ::chat_model record, applies this agent's configuration to it, and returns it. Keywords matching declared ::inputs become runtime inputs; the rest are forwarded to the model's create.

chat = WorkAssistant.create(user: current_user)

Raises ArgumentError if ::chat_model is not configured.



488
489
490
# File 'lib/ruby_llm/agent.rb', line 488

def create(**kwargs)
  with_rails_chat_record(:create, **kwargs)
end

.create!(**kwargs) ⇒ Object

Like ::create, but calls the model's create!, raising if the record is invalid.

chat = WorkAssistant.create!(user: current_user)


497
498
499
# File 'lib/ruby_llm/agent.rb', line 497

def create!(**kwargs)
  with_rails_chat_record(:create!, **kwargs)
end

.end_user(value = nil, &block) ⇒ Object

Sets the safety identifier for chats this agent builds, applied via Chat#with_end_user. A block defers evaluation until the chat is built, so the id can come from the agent's inputs. Called with no arguments, returns the configured value.

end_user "tenant-42"
end_user { workspace.public_id }


265
266
267
268
269
# File 'lib/ruby_llm/agent.rb', line 265

def end_user(value = nil, &block)
  return @end_user if value.nil? && !block_given?

  @end_user = block || value
end

.fallback_optionsObject



359
360
361
# File 'lib/ruby_llm/agent.rb', line 359

def fallback_options
  @fallback_options || {}
end

.fallbacks(*models, **options) ⇒ Object

Sets fallback models for chats this agent builds, applied via Chat#with_fallbacks. Called with no arguments, returns the configured models.

fallbacks "gpt-4.1-mini", "claude-haiku-4-5"
fallbacks "gpt-4.1-mini", on: [RubyLLM::RateLimitError]

Raises:



351
352
353
354
355
356
357
# File 'lib/ruby_llm/agent.rb', line 351

def fallbacks(*models, **options)
  return @fallbacks || [] if models.empty? && options.empty?
  raise ArgumentError, 'To set fallback options, provide at least one fallback model' if models.empty?

  @fallbacks = models.flatten.compact
  @fallback_options = options
end

.find(id, **kwargs) ⇒ Object

Finds the ::chat_model record with id and applies this agent's configuration at runtime, without persisting instructions. Returns the record.

chat = WorkAssistant.find(params[:id])

Raises ArgumentError if ::chat_model is not configured.

Raises:



508
509
510
511
512
513
514
515
516
# File 'lib/ruby_llm/agent.rb', line 508

def find(id, **kwargs)
  raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model

  input_values, = partition_inputs(kwargs)
  record = resolved_chat_model.find(id)
  apply_configuration(record, input_values:, persist_instructions: false)

  record
end

.headers(**headers, &block) ⇒ Object

Sets custom HTTP headers for chats this agent builds, applied via Chat#with_headers. A block defers evaluation until the chat is built. Called with no arguments, returns the configured value.



320
321
322
323
324
# File 'lib/ruby_llm/agent.rb', line 320

def headers(**headers, &block)
  return @headers || {} if headers.empty? && !block_given?

  @headers = block_given? ? block : headers
end

.inherited(subclass) ⇒ Object

:nodoc:



99
100
101
102
# File 'lib/ruby_llm/agent.rb', line 99

def inherited(subclass) # :nodoc:
  super
  copy_inherited_config_to(subclass)
end

.inputs(*names) ⇒ Object

Declares named runtime inputs. Matching keyword arguments passed to ::chat, ::create, ::create!, ::find, or ::new become methods inside lazy configuration blocks. Called with no arguments, returns the declared names.

inputs :workspace


395
396
397
398
399
# File 'lib/ruby_llm/agent.rb', line 395

def inputs(*names)
  return @input_names || [] if names.empty?

  @input_names = names.flatten.map(&:to_sym)
end

.instructions(text = nil, append: false, persist: true, cache_until_here: false, **prompt_locals, &block) ⇒ Object

Adds system instructions for chats this agent builds. Accepts a string, a block evaluated when the chat is built, or keyword locals for the agent's conventional prompt template (for a WorkAssistant agent, app/prompts/work_assistant/instructions.txt.erb). Multiple declarations are applied in order.

instructions "You are a helpful assistant."
instructions { "You are helping #{workspace.name}" }
instructions display_name: -> { chat.user.display_name_or_email }
instructions append: true, persist: false do
"Today is #{Date.current}"
end

The class's own declarations take precedence over its conventional template. Inherited declarations are used only when neither exists. In Rails mode, declarations persist when the record is created unless persist: false; ::find always reapplies them without rewriting history. Called with no arguments, returns the declarations.



185
186
187
188
189
190
191
192
193
194
# File 'lib/ruby_llm/agent.rb', line 185

def instructions(text = nil, append: false, persist: true, cache_until_here: false, **prompt_locals, &block)
  return instruction_declarations if text.nil? && prompt_locals.empty? && !block_given?

  (@instruction_declarations ||= []) << {
    value: block || text || { prompt: 'instructions', locals: prompt_locals },
    append: append,
    persist: persist,
    cache_until_here: cache_until_here
  }
end

.model(model_id = nil, **options, &block) ⇒ Object

Sets the model used by chats this agent builds. Extra options are forwarded to RubyLLM.chat, including provider: to disambiguate the model and protocol: to override its wire protocol. A block picks the model when the chat is built, with the declared ::inputs available as methods. Called with no arguments, returns the configured chat keywords.

model "gpt-5.6-luna"
model "gpt-5.6", provider: :openai, protocol: :responses
model { quality == :high ? "gpt-5.6" : "gpt-5.6-luna" }

The block runs before the chat exists, so it can read inputs but not chat.



117
118
119
120
121
122
123
# File 'lib/ruby_llm/agent.rb', line 117

def model(model_id = nil, **options, &block)
  return @chat_kwargs || {} if model_id.nil? && options.empty? && !block_given?

  model_value = block || model_id
  options[:model] = model_value unless model_value.nil?
  @chat_kwargs = options
end

.optionObject

:method: max_output_tokens :call-seq: max_output_tokens(value = nil)

Caps the number of tokens chats this agent builds may generate. Called with no argument, returns the configured value.

max_output_tokens 1000


214
215
216
217
218
219
220
# File 'lib/ruby_llm/agent.rb', line 214

PASSTHROUGH_OPTIONS.each do |option|
  define_method(option) do |value = nil|
    return instance_variable_get(:"@#{option}") if value.nil?

    instance_variable_set(:"@#{option}", value)
  end
end

.partition_inputs(kwargs) ⇒ Object

:nodoc:



550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/ruby_llm/agent.rb', line 550

def partition_inputs(kwargs) # :nodoc:
  input_values = {}
  chat_options = {}

  kwargs.each do |key, value|
    symbolized_key = key.to_sym
    if inputs.include?(symbolized_key)
      input_values[symbolized_key] = value
    else
      chat_options[symbolized_key] = value
    end
  end

  [input_values, chat_options]
end

.provider_options(**provider_options, &block) ⇒ Object

Sets options in the provider's request vocabulary for chats this agent builds, applied via Chat#with_provider_options. A block defers evaluation until the chat is built. Called with no arguments, returns the configured value.

provider_options service_tier: "flex"


311
312
313
314
315
# File 'lib/ruby_llm/agent.rb', line 311

def provider_options(**provider_options, &block)
  return @provider_options || {} if provider_options.empty? && !block_given?

  @provider_options = block_given? ? block : provider_options
end

.provider_tools(*tools, **tools_with_options, &block) ⇒ Object

Enables provider-executed tools for chats this agent builds, applied via Chat#with_provider_tools. Accepts the same aliases, options, and raw Hashes; a block defers evaluation until the chat is built. Called with no arguments, returns the declared entries.

provider_tools :web_search
provider_tools web_search: { allowed_domains: ["ruby-lang.org"] }


160
161
162
163
164
# File 'lib/ruby_llm/agent.rb', line 160

def provider_tools(*tools, **tools_with_options, &block)
  return @provider_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?

  @provider_tools = block_given? ? block : RubyLLM::Tools::ProviderTools.normalize(tools, tools_with_options)
end

.render_prompt(name, chat:, inputs:, locals:) ⇒ Object

:nodoc:



545
546
547
548
# File 'lib/ruby_llm/agent.rb', line 545

def render_prompt(name, chat:, inputs:, locals:) # :nodoc:
  resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
  RubyLLM.render_prompt("#{prompt_agent_path}/#{name}", **resolved_locals)
end

.rescue_from(*exception_classes, with: nil, &block) ⇒ Object

Registers a handler for exceptions raised by the chat operations of this agent's instances: #ask, #say, #ask_later, #complete, #generate, #run_tools, #step, #count_tokens, and #compact. Name the handler with with: or pass a block; either runs on the agent instance, so the agent's #chat, inputs, and class name are available for instrumentation.

class ApplicationAgent < RubyLLM::Agent
rescue_from RubyLLM::RateLimitError, Faraday::TimeoutError, with: :handle_transient
rescue_from RubyLLM::BadRequestError do |error|
  error_tracker.notify(error)
  raise
end

private

def handle_transient(error)
  metrics.increment("llm.api_error", type: "transient")
  raise
end
end

Handlers are searched in reverse declaration order, so the last matching one wins. Re-raise inside a handler to let the caller see the exception; otherwise the handler's return value becomes the operation's return value. Exceptions no handler matches are re-raised. Subclasses inherit the handlers declared when they are defined.

Exception classes may be named as Strings, which defers constant lookup until an exception is raised.

Raises:



431
432
433
434
435
436
437
438
# File 'lib/ruby_llm/agent.rb', line 431

def rescue_from(*exception_classes, with: nil, &block)
  raise ArgumentError, 'rescue_from needs a handler: pass with: or a block' unless with || block
  raise ArgumentError, 'rescue_from takes with: or a block, not both' if with && block

  exception_classes.flatten.each do |exception_class|
    rescue_handlers << [rescue_handler_key(exception_class), with || block]
  end
end

.rescue_handler_for(exception) ⇒ Object

:nodoc:



444
445
446
447
448
449
450
# File 'lib/ruby_llm/agent.rb', line 444

def rescue_handler_for(exception) # :nodoc:
  rescue_handlers.reverse_each do |class_name, handler|
    exception_class = rescue_handler_class(class_name)
    return handler if exception_class && exception.is_a?(exception_class)
  end
  nil
end

.rescue_handlersObject

:nodoc:



440
441
442
# File 'lib/ruby_llm/agent.rb', line 440

def rescue_handlers # :nodoc:
  @rescue_handlers ||= []
end

.resolved_chat_kwargs(inputs: {}) ⇒ Object

:nodoc:



456
457
458
459
460
461
# File 'lib/ruby_llm/agent.rb', line 456

def resolved_chat_kwargs(inputs: {}) # :nodoc:
  kwargs = chat_kwargs
  return kwargs unless kwargs[:model].is_a?(Proc)

  kwargs.merge(model: evaluate(kwargs[:model], runtime_context(chat: nil, inputs: inputs)))
end

.schema(value = nil, &block) ⇒ Object

Sets the structured output schema for chats this agent builds, applied via Chat#with_schema. Accepts a schema class, a JSON schema hash, or a block. A plain block is built with the Schematist::Schema DSL; a lambda is evaluated when the chat is built. Called with no arguments, returns the configured value.

schema PersonSchema
schema do
string :verdict, enum: ["pass", "revise"]
string :feedback
end


338
339
340
341
342
# File 'lib/ruby_llm/agent.rb', line 338

def schema(value = nil, &block)
  return @schema if value.nil? && !block_given?

  @schema = block_given? ? block : value
end

.sync_instructions(chat_or_id, **kwargs) ⇒ Object

Re-renders this agent's instructions and persists them on the given ::chat_model record (or the record found by that id). Keywords matching declared ::inputs become runtime inputs. Returns the record.

WorkAssistant.sync_instructions(chat)

Raises ArgumentError if ::chat_model is not configured.

Raises:



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/ruby_llm/agent.rb', line 526

def sync_instructions(chat_or_id, **kwargs)
  raise ArgumentError, 'chat_model must be configured to use sync_instructions' unless resolved_chat_model

  input_values, = partition_inputs(kwargs)
  record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
  apply_assume_model_exists(record)
  apply_protocol(record)
  apply_context(record)
  runtime = runtime_context(chat: record, inputs: input_values)
  apply_instructions(
    record,
    runtime,
    inputs: input_values,
    persist: true,
    persistent_only: true
  )
  record
end

.thinking(enabled = true, **options) ⇒ Object

Enables thinking for chats this agent builds, applied via Chat#with_thinking. With no options, RubyLLM chooses from the model's registered controls. Accepts keywords or an options Hash. Pass false to disable it. Passing nil raises ArgumentError.

thinking
thinking false
thinking effort: :low
thinking budget: 10_000
thinking display: :summarized


233
234
235
236
237
238
# File 'lib/ruby_llm/agent.rb', line 233

def thinking(enabled = true, **options) # rubocop:disable Style/OptionalBooleanParameter
  return thinking(**enabled.transform_keys(&:to_sym), **options) if enabled.is_a?(Hash)

  validate_thinking_options(enabled, options)
  @thinking = enabled ? options : false
end

.tool_options(**options, &block) ⇒ Object

Sets how chats this agent builds use their tools, applied via Chat#with_tool_options. Accepts choice:, calls:, and concurrency:. A block defers evaluation until the chat is built. Called with no arguments, returns the configured options.

tool_options choice: :required, calls: :one


146
147
148
149
150
# File 'lib/ruby_llm/agent.rb', line 146

def tool_options(**options, &block)
  return @tool_options || {} if options.empty? && !block_given?

  @tool_options = block_given? ? block : options
end

.tools(*tools, &block) ⇒ Object

Declares the tools for chats this agent builds. A block defers construction until the chat is built. Configure how the model uses them with ::tool_options. Called with no arguments, returns the declared tools.

tools SearchDocs, LookupAccount
tools { [TodoTool.new(chat: chat)] }


133
134
135
136
137
# File 'lib/ruby_llm/agent.rb', line 133

def tools(*tools, &block)
  return @tools || [] if tools.empty? && !block_given?

  @tools = block_given? ? block : tools.flatten
end

Instance Method Details

#operationObject

:method: compact :call-seq: compact

Compacts the model context through Chat#compact and returns its Message. Exceptions use ::rescue_from handlers.



1261
1262
1263
1264
1265
1266
1267
# File 'lib/ruby_llm/agent.rb', line 1261

GUARDED_OPERATIONS.each do |operation|
  define_method(operation) do |*args, **kwargs, &block|
    chat.public_send(operation, *args, **kwargs, &block)
  rescue StandardError => e
    rescue_with_handler(e)
  end
end

#rescue_with_handler(exception) ⇒ Object

Runs the ::rescue_from handler matching exception and returns its value, or re-raises when no handler matches. The chat operations call this for you.



1272
1273
1274
1275
1276
1277
1278
1279
1280
# File 'lib/ruby_llm/agent.rb', line 1272

def rescue_with_handler(exception)
  handler = self.class.rescue_handler_for(exception)
  raise exception unless handler

  return instance_exec(exception, &handler) unless handler.is_a?(Symbol)

  handler_method = method(handler)
  handler_method.arity.zero? ? handler_method.call : handler_method.call(exception)
end