Class: LLM::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/llm/agent.rb

Overview

LLM::Agent is the recommended entry point for most use-cases. It provides a class-level DSL for defining reusable, preconfigured assistants with defaults for model, tools, schema, and instructions.

It wraps the same stateful runtime surface as LLM::Context: message history, usage, persistence, streaming parameters, and provider-backed requests still flow through an underlying context. The defining behavior of an agent is that it automatically resolves pending tool calls for you during talk, instead of leaving tool loops to the caller.

Notes:

  • Instructions are injected once unless a system message is already present.
  • An agent automatically executes tool loops (unlike LLM::Context).
  • The automatic tool loop enables the wrapped context's guard by default. The built-in LLM::Guard::Loop detects repeated tool-call patterns and blocks stuck execution before more tool work is queued.
  • The tool loop can be bounded with tool_budget. Once the budget is spent, the agent sends an in-band advisory message back through the model and keeps the loop in-band. By default no budget is set (nil), so the feature is disabled.
  • Tool loop execution can be configured with concurrency :sequential, :thread, :async, :fiber, :fork, or :ractor.

Examples:

Subclass with defaults

class SystemAdmin < LLM::Agent
  set model: "gpt-4.1-nano",
      instructions: "You are a Linux system admin",
      tools: [Shell],
      schema: Result
end

llm = LLM.openai(key: ENV["KEY"])
agent = SystemAdmin.new(llm)
agent.talk("Run 'date'")

Direct instance

llm = LLM.deepseek(key: ENV["KEY"])
agent = LLM::Agent.new(llm, stream: $stdout)
agent.talk "Hello world"

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(llm, params = {}) ⇒ Agent

Returns a new instance of Agent.

Parameters:

  • llm (LLM::Provider)

    A provider

  • params (Hash) (defaults to: {})

    The parameters to maintain throughout the conversation. Any parameter the provider supports can be included and not only those listed here.

Options Hash (params):

  • :model (String)

    Defaults to the provider's default model

  • :tools (Array<LLM::Function>, nil)

    Defaults to nil

  • :skills (Array<String>, nil)

    Defaults to nil

  • :schema (#to_json, nil)

    Defaults to nil

  • :stream (Object, Proc, nil)

    Optional stream override for this agent instance

  • :tracer (LLM::Tracer, Proc, nil)

    Optional tracer override for this agent instance

  • :concurrency (Symbol, Array<Symbol>, nil)

    Defaults to the agent class concurrency



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/llm/agent.rb', line 402

def initialize(llm, params = {})
  params = {}.merge!(params)
  @llm = llm
  fields, fields_ivar = FIELDS, IVARS
  fields.each do |field|
    resolvable = params.key?(field) ? params.delete(field) : (self.class.respond_to?(field) ? self.class.public_send(field) : nil)
    resolve_symbol = !%i[concurrency].include?(field)
    resolved = resolvable != nil ? resolve_option(self, resolvable, resolve_symbol:) : resolvable
    resolved = [*resolved].map(&:to_s) if field == :confirm && resolved
    if field == :model
      params[field] = resolved unless resolved.nil? || params.key?(field)
    elsif resolved && !fields_ivar.include?(field)
      params[field] ||= resolved
    elsif fields_ivar.include?(field)
      instance_variable_set(:"@#{field}", resolved)
    end
  end
  @ctx = LLM::Context.new(llm, {guard: LLM::Guard::Loop}.merge(params))
  @path and File.readable?(@path) ? @ctx.restore(path:) : nil
end

Instance Attribute Details

#llmLLM::Provider (readonly)

Returns a provider

Returns:



88
89
90
# File 'lib/llm/agent.rb', line 88

def llm
  @llm
end

Class Method Details

.concurrency(concurrency = nil) ⇒ Symbol, ...

Set or get the tool execution concurrency.

Parameters:

  • concurrency (Symbol, Array<Symbol>, nil) (defaults to: nil)

    Controls how pending tool loops are executed:

    • :sequential: sequential calls
    • :thread: concurrent threads
    • :async: concurrent async tasks
    • :fiber: concurrent scheduler-backed fibers
    • :fork: forked child processes
    • :ractor: concurrent Ruby ractors for class-based tools; MCP tools are not supported, and this mode is especially useful for CPU-bound tool work Usually pass a single strategy. Arrays are only for advanced mixed-work cases and are not needed for normal queued stream tool loops.

Returns:

  • (Symbol, Array<Symbol>, nil)


281
282
283
284
# File 'lib/llm/agent.rb', line 281

def self.concurrency(concurrency = nil)
  return @concurrency if concurrency.nil?
  @concurrency = concurrency
end

.confirm(*tool_names, &block) ⇒ Array<String>, ...

Set or get the tool names that require confirmation before they can run.

When a single Symbol is given, it is stored as-is and resolved at initialization time by calling the method with that name on the agent instance. This allows dynamic tool confirmation lists.

Examples:

class MyAgent < LLM::Agent
  confirm :tools_that_need_confirmation

  def tools_that_need_confirmation
    some_condition ? %w[delete destroy] : %w[delete]
  end
end

Parameters:

  • tool_names (String, Symbol, Array<String, Symbol>, Proc)

    One or more tool names.

  • block (Proc)

    An optional, lazy-evaluated Proc

Returns:

  • (Array<String>, Proc, Symbol, nil)


244
245
246
247
248
249
250
251
252
253
# File 'lib/llm/agent.rb', line 244

def self.confirm(*tool_names, &block)
  return @confirm if tool_names.empty? and !block
  if block
    @confirm = block
  elsif single_callable?(tool_names)
    @confirm = tool_names.first
  else
    @confirm = tool_names.flatten.map(&:to_s)
  end
end

.description(desc = UNDEFINED, &block) ⇒ String?

Note:

This method serves as a self-documenting string. It is optional but recommended.

Set or get an agent's description

Parameters:

  • desc (String) (defaults to: UNDEFINED)

    The agent's description

Returns:

  • (String, nil)

    Returns the agent's description



159
160
161
162
163
164
165
# File 'lib/llm/agent.rb', line 159

def self.description(desc = UNDEFINED, &block)
  if desc.equal?(UNDEFINED)
    @desc
  else
    @desc = block || desc
  end
end

.instructions(instructions = nil) ⇒ String?

Set or get the default instructions

Parameters:

  • instructions (String, nil) (defaults to: nil)

    The system instructions

Returns:

  • (String, nil)

    Returns the current instructions when no argument is provided



261
262
263
264
# File 'lib/llm/agent.rb', line 261

def self.instructions(instructions = nil)
  return @instructions if instructions.nil?
  @instructions = instructions
end

.model(model = nil, &block) ⇒ String?

Set or get the default model

Parameters:

  • model (String, nil) (defaults to: nil)

    The model identifier

Returns:

  • (String, nil)

    Returns the current model when no argument is provided



173
174
175
176
# File 'lib/llm/agent.rb', line 173

def self.model(model = nil, &block)
  return @model if model.nil? and !block
  @model = block || model
end

.name(name = UNDEFINED, &block) ⇒ String

Note:

This method serves as a self-documenting string and it is used by LLM::Repl. It is optional but recommended.

Set or get an agent's name

Parameters:

  • name (String) (defaults to: UNDEFINED)

    The agent name

Returns:

  • (String)

    Return's the agents name



137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/llm/agent.rb', line 137

def self.name(name = UNDEFINED, &block)
  if name.equal?(UNDEFINED)
    if @name.nil?
      name  = to_s.split("::").last
      @name = name.gsub(CASE_PATTERN, "-").downcase
    else
      @name
    end
  else
    @name = block || name
  end
end

.path(path = UNDEFINED, &block) ⇒ String?

Set the file path where an agent's memory can be restored from, and written to.

Parameters:

  • path (String) (defaults to: UNDEFINED)

    The path to a file

Returns:

  • (String, nil)


332
333
334
335
336
337
338
# File 'lib/llm/agent.rb', line 332

def self.path(path = UNDEFINED, &block)
  if path.equal?(UNDEFINED)
    @path
  else
    @path = path || block
  end
end

.retry_budget(budget = UNDEFINED) ⇒ Integer?

Sets or returns the retry budget for the agent.

The retry budget is the maximum number of times a rate-limited request will be retried before giving up. Each retry sleeps a growing interval, so an exhausted budget surfaces the rate-limit error instead of blocking indefinitely. Enabled (5) by default; a raw Context disables it (0) unless configured.

Parameters:

  • budget (Integer) (defaults to: UNDEFINED)

    The maximum number of rate-limit retries in a turn.

Returns:

  • (Integer, nil)


373
374
375
376
377
378
379
# File 'lib/llm/agent.rb', line 373

def self.retry_budget(budget = UNDEFINED)
  if budget.equal?(UNDEFINED)
    @retry_budget.nil? ? 5 : @retry_budget
  else
    @retry_budget = budget
  end
end

.schema(schema = nil, &block) ⇒ #to_json?

Set or get the default schema

Parameters:

  • schema (#to_json, nil) (defaults to: nil)

    The schema

Returns:

  • (#to_json, nil)

    Returns the current schema when no argument is provided



184
185
186
187
# File 'lib/llm/agent.rb', line 184

def self.schema(schema = nil, &block)
  return @schema if schema.nil? and !block
  @schema = block || schema
end

.set(properties) ⇒ void

This method returns an undefined value.

Bulk-assign class-level agent defaults from a Hash.

Each key is resolved by calling the corresponding class method on the agent subclass. An error is raised for unknown keys so that typos are caught early.

Examples:

class AdminAgent < LLM::Agent
  set name: "admin",
      instructions: "You are a system administrator",
      model: "gpt-4.1-nano",
      tools: [Shell, ReadFile]
end

Parameters:

  • properties (Hash)

Options Hash (properties):

  • :instructions (String)
  • :model (String)
  • :tools (Array<LLM::Function>)
  • :skills (Array<String>)
  • :schema (#to_json)
  • :concurrency (Symbol, Array<Symbol>)
  • :tracer (LLM::Tracer, Proc)
  • :stream (Object, Proc)
  • :confirm (String, Symbol, Array<String, Symbol>, Proc)

Raises:

  • (KeyError)

    when a property key does not match a class-level accessor



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

def self.set(properties)
  properties.each do
    if respond_to?(_1)
      public_send(_1, _2)
    else
      raise KeyError, "key not found: #{_1}"
    end
  end
end

.skills(*skills, &block) ⇒ Array<String>?

Set or get the default skills

Parameters:

  • skills (Array<String>, nil)

    One or more skill directories

Returns:

  • (Array<String>, nil)

    Returns the current skills when no argument is provided



212
213
214
215
216
217
218
219
220
221
# File 'lib/llm/agent.rb', line 212

def self.skills(*skills, &block)
  return @skills if skills.empty? and !block
  if block
    @skills = block
  elsif single_callable?(skills)
    @skills = skills.first
  else
    @skills = skills.flatten
  end
end

.stream(stream = nil, &block) ⇒ Object, ...

Set or get the default stream.

When a block is provided, it is stored and evaluated lazily against the agent instance during initialization so it can build a fresh stream for each agent.

Examples:

class Agent < LLM::Agent
  stream { MyStream.new }
end

Parameters:

  • stream (Object, Proc, nil) (defaults to: nil)

Yield Returns:

Returns:



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

def self.stream(stream = nil, &block)
  return @stream if stream.nil? && !block
  @stream = block || stream
end

.tool_budget(budget = UNDEFINED, &block) ⇒ Integer?

Note:

By default this feature is disabled (set to nil).

Set or get the maximum number of tool calls that are allowed in a single turn. Once the budget is spent, we will return an in-band message that informs the model it has spent its tool call budget - and usually a model will change course afterwards.

Parameters:

  • budget (Integer) (defaults to: UNDEFINED)

    The maximum number of tool calls to allow in a single turn.

Returns:

  • (Integer, nil)


354
355
356
357
358
359
360
# File 'lib/llm/agent.rb', line 354

def self.tool_budget(budget = UNDEFINED, &block)
  if budget.equal?(UNDEFINED)
    @tool_budget
  else
    @tool_budget = budget || block
  end
end

.tools(*tools, &block) ⇒ Array<LLM::Function>

Set or get the default tools

Parameters:

Returns:

  • (Array<LLM::Function>)

    Returns the current tools when no argument is provided



195
196
197
198
199
200
201
202
203
204
# File 'lib/llm/agent.rb', line 195

def self.tools(*tools, &block)
  return @tools || [] if tools.empty? and !block
  if block
    @tools = block
  elsif single_callable?(tools)
    @tools = tools.first
  else
    @tools = tools.flatten
  end
end

.tracer(tracer = nil, &block) ⇒ LLM::Tracer, ...

Set or get the default tracer.

When a block is provided, it is stored and evaluated lazily against the agent instance during initialization so it can build a tracer from the resolved provider.

Examples:

class Agent < LLM::Agent
  tracer { LLM::Tracer::Logger.new(llm, io: $stdout) }
end

Parameters:

Yield Returns:

Returns:



301
302
303
304
# File 'lib/llm/agent.rb', line 301

def self.tracer(tracer = nil, &block)
  return @tracer if tracer.nil? && !block
  @tracer = block || tracer
end

Instance Method Details

#ask(prompt, params = {}) ⇒ Object

See Also:



477
478
479
480
481
# File 'lib/llm/agent.rb', line 477

def ask(prompt, params = {})
  res = run_loop(prompt, params, :ask)
  path ? @ctx.save(path:) : nil
  res
end

#compacted?Boolean

Returns:

  • (Boolean)

See Also:



642
643
644
# File 'lib/llm/agent.rb', line 642

def compacted?
  @ctx.compacted?
end

#concurrencySymbol, ...

Returns the configured tool execution concurrency.

Returns:

  • (Symbol, Array<Symbol>, nil)


614
615
616
# File 'lib/llm/agent.rb', line 614

def concurrency
  @concurrency
end

#context_usageRational?

Returns:

  • (Rational, nil)

    Returns the fraction of the context window currently used. For example: Rational(100, 10_000), or nil when unknown



527
528
529
# File 'lib/llm/agent.rb', line 527

def context_usage
  @ctx.context_usage
end

#context_usedInteger?

Returns:

  • (Integer, nil)

    Returns the live context size (in tokens) of the most recent assistant message.



520
521
522
# File 'lib/llm/agent.rb', line 520

def context_used
  @ctx.context_used
end

#context_windowInteger

Returns:

  • (Integer)

See Also:



628
629
630
# File 'lib/llm/agent.rb', line 628

def context_window
  @ctx.context_window
end

#costLLM::Cost

Returns:

See Also:



621
622
623
# File 'lib/llm/agent.rb', line 621

def cost
  @ctx.cost
end

#descriptionString?

Returns the agent's description

Returns:

  • (String, nil)


448
449
450
# File 'lib/llm/agent.rb', line 448

def description
  @description
end

#deserialize(**kw) ⇒ LLM::Agent Also known as: restore

Returns:



725
726
727
728
# File 'lib/llm/agent.rb', line 725

def deserialize(**kw)
  @ctx.deserialize(**kw)
  self
end

#image_url(url) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • url (String)

    The URL

Returns:



553
554
555
# File 'lib/llm/agent.rb', line 553

def image_url(url)
  @ctx.image_url(url)
end

#inspectString

Returns:

  • (String)


709
710
711
712
# File 'lib/llm/agent.rb', line 709

def inspect
  "#<#{LLM::Utils.object_id(self)} " \
  "@llm=#{@llm.class}, @mode=#{mode.inspect}, @messages=#{messages.inspect}>"
end

#interrupt!nil Also known as: cancel!

Interrupt the active request, if any.

Returns:

  • (nil)


534
535
536
# File 'lib/llm/agent.rb', line 534

def interrupt!
  @ctx.interrupt!
end

#local_file(path) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • path (String)

    The path

Returns:



562
563
564
# File 'lib/llm/agent.rb', line 562

def local_file(path)
  @ctx.local_file(path)
end

#messagesLLM::Buffer<LLM::Message>



485
486
487
# File 'lib/llm/agent.rb', line 485

def messages
  @ctx.messages
end

#modeSymbol

Returns:

  • (Symbol)


607
608
609
# File 'lib/llm/agent.rb', line 607

def mode
  @ctx.mode
end

#modelString

Returns the model an Agent is actively using

Returns:

  • (String)


601
602
603
# File 'lib/llm/agent.rb', line 601

def model
  @ctx.model
end

#nameString

Returns the agent's name

Returns:

  • (String)


426
427
428
# File 'lib/llm/agent.rb', line 426

def name
  @name
end

#on_tool_confirmation(fn, strategy) ⇒ LLM::Function::Return

This method is called when confirmation is required before a tool can run.

Parameters:

  • fn (LLM::Function)

    The pending function call. It can be cancelled through the Function#cancel method.

  • strategy (Symbol, Array<Symbol>)

    The execution strategy that would be used for the tool call.

Returns:

  • (LLM::Function::Return)

    Return either fn.task(strategy).wait to approve execution or fn.cancel(...) to cancel the call.



742
743
744
# File 'lib/llm/agent.rb', line 742

def on_tool_confirmation(fn, strategy)
  fn.cancel
end

#paramsHash

Returns:

  • (Hash)

See Also:



690
691
692
# File 'lib/llm/agent.rb', line 690

def params
  @ctx.params
end

#pathString?

Returns a file path where an agent's memory is restored from, and written to after each turn.

Returns:

  • (String, nil)


434
435
436
# File 'lib/llm/agent.rb', line 434

def path
  @path
end

#pending_functionsArray<LLM::Function>

Returns:



491
492
493
# File 'lib/llm/agent.rb', line 491

def pending_functions
  @tracer ? @llm.with_tracer(@tracer) { @ctx.pending_functions } : @ctx.pending_functions
end

#prompt(&b) ⇒ LLM::Prompt Also known as: build_prompt

Parameters:

  • b (Proc)

    A block that composes messages. If it takes one argument, it receives the prompt object. Otherwise it runs in prompt context.

Returns:

See Also:



543
544
545
# File 'lib/llm/agent.rb', line 543

def prompt(&b)
  @ctx.prompt(&b)
end

#recordObject?

Returns the ORM record this agent is bound to, or nil.

Returns:



441
442
443
# File 'lib/llm/agent.rb', line 441

def record
  @record
end

#registryLLM::Registry

Returns:

See Also:



635
636
637
# File 'lib/llm/agent.rb', line 635

def registry
  @ctx.registry
end

#remote_file(res) ⇒ LLM::Object

Returns a tagged object

Parameters:

Returns:



571
572
573
# File 'lib/llm/agent.rb', line 571

def remote_file(res)
  @ctx.remote_file(res)
end

#repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil) ⇒ void

Note:

By default this method disables the tracer for the duration of the repl session, and restores it afterwards.

This method returns an undefined value.

Start a minimalist repl that can interact with the agent and its current state. This method requires the 'curses' gem to be installed and available to require.

Parameters:

  • name (String) (defaults to: self.name)

    The agent's name. Defaults to #name.

  • path (String) (defaults to: nil)

    The path to a file where runtime state is read from, and written to

  • tools (Array<LLM::Tool>) (defaults to: [])

    Extra tools to attach for the repl session

  • skills (Array<String>) (defaults to: [])

    Extra skills to attach for the repl session

  • tracer (Boolean) (defaults to: false)

    When true, the tracer is kept alive during the repl session. Default is false.



670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# File 'lib/llm/agent.rb', line 670

def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
  if trace != nil
    warn "llm.rb: trace option is deprecated, use tracer instead"
    tracer = trace
  end
  if !tracer
    previous    = self.tracer
    self.tracer = nil
  end
  require_relative "repl" unless defined?(::LLM::Repl)
  LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
ensure
  if !tracer
    self.tracer = previous
  end
end

#returnsArray<LLM::Function::Return>

Returns:

See Also:



498
499
500
# File 'lib/llm/agent.rb', line 498

def returns
  @ctx.returns
end

#serialize(**kw) ⇒ void Also known as: save

This method returns an undefined value.



717
718
719
# File 'lib/llm/agent.rb', line 717

def serialize(**kw)
  @ctx.serialize(**kw)
end

#streamLLM::Stream, ...

Returns a stream object, or nil

Returns:

  • (LLM::Stream, #<<, nil)

    Returns a stream object, or nil



594
595
596
# File 'lib/llm/agent.rb', line 594

def stream
  @ctx.stream
end

#talk(prompt, params = {}) ⇒ LLM::Response

Maintain a conversation via the chat completions API. This method immediately sends a request to the LLM and returns the response.

Examples:

llm = LLM.openai(key: ENV["KEY"])
agent = LLM::Agent.new(llm)
response = agent.talk("Hello, what is your name?")
puts response.choices[0].content

Parameters:

  • params (Hash) (defaults to: {})

    The params passed to the provider, including optional :stream, :tools, :schema etc.

  • prompt (String)

    The input prompt to be completed

Options Hash (params):

  • :tool_budget (Integer)

    The maximum number of tool calls that can be made in a single turn before the agent sends an in-band advisory message that tells the model it has spent its tool call budget - and usually the model will change course after that. By default this feature is disabled (set to nil).

Returns:



469
470
471
472
473
# File 'lib/llm/agent.rb', line 469

def talk(prompt, params = {})
  res = run_loop(prompt, params, :talk)
  path ? @ctx.save(path:) : nil
  res
end

#to_hHash

Returns:

  • (Hash)

See Also:



697
698
699
# File 'lib/llm/agent.rb', line 697

def to_h
  @ctx.to_h
end

#to_jsonString

Returns:

  • (String)


703
704
705
# File 'lib/llm/agent.rb', line 703

def to_json(...)
  LLM.json.dump(to_h, ...)
end

#token_usageLLM::Usage Also known as: usage

Returns:



512
513
514
# File 'lib/llm/agent.rb', line 512

def token_usage
  @ctx.token_usage
end

#tracerLLM::Tracer

Returns an LLM tracer

Returns:



578
579
580
# File 'lib/llm/agent.rb', line 578

def tracer
  @tracer || @ctx.tracer
end

#tracer=(other) ⇒ void

This method returns an undefined value.

Parameters:



586
587
588
589
# File 'lib/llm/agent.rb', line 586

def tracer=(other)
  @ctx.tracer = other
  @tracer = other
end

#waitArray<LLM::Function::Return>

Returns:

See Also:



505
506
507
# File 'lib/llm/agent.rb', line 505

def wait(...)
  @tracer ? @llm.with_tracer(@tracer) { @ctx.wait(...) } : @ctx.wait(...)
end