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



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/llm/agent.rb', line 387

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.public_send(field)
    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:



86
87
88
# File 'lib/llm/agent.rb', line 86

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)


243
244
245
246
# File 'lib/llm/agent.rb', line 243

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)


309
310
311
312
313
314
315
316
# File 'lib/llm/agent.rb', line 309

def self.confirm(*tool_names, &block)
  return @confirm if tool_names.empty? && !block
  if tool_names.size == 1 && tool_names.grep(Symbol).any?
    @confirm = tool_names.first
  else
    @confirm = block || 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



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

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



223
224
225
226
# File 'lib/llm/agent.rb', line 223

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



171
172
173
174
# File 'lib/llm/agent.rb', line 171

def self.model(model = nil, &block)
  return @model if model.nil? && !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



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

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)


324
325
326
327
328
329
330
# File 'lib/llm/agent.rb', line 324

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)


365
366
367
368
369
370
371
# File 'lib/llm/agent.rb', line 365

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



212
213
214
215
# File 'lib/llm/agent.rb', line 212

def self.schema(schema = nil, &block)
  return @schema if schema.nil? && !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



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

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



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

def self.skills(*skills, &block)
  return @skills if skills.empty? && !block
  if skills.size == 1 and skills.grep(Symbol).any?
    @skills = skills.first
  else
    @skills = block || 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:



283
284
285
286
# File 'lib/llm/agent.rb', line 283

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)


346
347
348
349
350
351
352
# File 'lib/llm/agent.rb', line 346

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



182
183
184
185
186
187
188
189
# File 'lib/llm/agent.rb', line 182

def self.tools(*tools, &block)
  return @tools || [] if tools.empty? && !block
  if tools.size == 1 and tools.grep(Symbol).any?
    @tools = tools.first
  else
    @tools = block || 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:



263
264
265
266
# File 'lib/llm/agent.rb', line 263

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:



455
456
457
458
459
# File 'lib/llm/agent.rb', line 455

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

#compacted?Boolean

Returns:

  • (Boolean)

See Also:



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

def compacted?
  @ctx.compacted?
end

#concurrencySymbol, ...

Returns the configured tool execution concurrency.

Returns:

  • (Symbol, Array<Symbol>, nil)


592
593
594
# File 'lib/llm/agent.rb', line 592

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



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

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.



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

def context_used
  @ctx.context_used
end

#context_windowInteger

Returns:

  • (Integer)

See Also:



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

def context_window
  @ctx.context_window
end

#costLLM::Cost

Returns:

See Also:



599
600
601
# File 'lib/llm/agent.rb', line 599

def cost
  @ctx.cost
end

#descriptionString?

Returns the agent's description

Returns:

  • (String, nil)


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

def description
  @description
end

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

Returns:



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

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

#image_url(url) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • url (String)

    The URL

Returns:



531
532
533
# File 'lib/llm/agent.rb', line 531

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

#inspectString

Returns:

  • (String)


687
688
689
690
# File 'lib/llm/agent.rb', line 687

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)


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

def interrupt!
  @ctx.interrupt!
end

#local_file(path) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • path (String)

    The path

Returns:



540
541
542
# File 'lib/llm/agent.rb', line 540

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

#messagesLLM::Buffer<LLM::Message>



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

def messages
  @ctx.messages
end

#modeSymbol

Returns:

  • (Symbol)


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

def mode
  @ctx.mode
end

#modelString

Returns the model an Agent is actively using

Returns:

  • (String)


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

def model
  @ctx.model
end

#nameString

Returns the agent's name

Returns:

  • (String)


411
412
413
# File 'lib/llm/agent.rb', line 411

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.



720
721
722
# File 'lib/llm/agent.rb', line 720

def on_tool_confirmation(fn, strategy)
  fn.cancel
end

#paramsHash

Returns:

  • (Hash)

See Also:



668
669
670
# File 'lib/llm/agent.rb', line 668

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)


419
420
421
# File 'lib/llm/agent.rb', line 419

def path
  @path
end

#pending_functionsArray<LLM::Function>

Returns:



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

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:



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

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

#registryLLM::Registry

Returns:

See Also:



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

def registry
  @ctx.registry
end

#remote_file(res) ⇒ LLM::Object

Returns a tagged object

Parameters:

Returns:



549
550
551
# File 'lib/llm/agent.rb', line 549

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.



648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
# File 'lib/llm/agent.rb', line 648

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:



476
477
478
# File 'lib/llm/agent.rb', line 476

def returns
  @ctx.returns
end

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

This method returns an undefined value.



695
696
697
# File 'lib/llm/agent.rb', line 695

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



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

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:



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

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

#to_hHash

Returns:

  • (Hash)

See Also:



675
676
677
# File 'lib/llm/agent.rb', line 675

def to_h
  @ctx.to_h
end

#to_jsonString

Returns:

  • (String)


681
682
683
# File 'lib/llm/agent.rb', line 681

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

#token_usageLLM::Usage Also known as: usage

Returns:



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

def token_usage
  @ctx.token_usage
end

#tracerLLM::Tracer

Returns an LLM tracer

Returns:



556
557
558
# File 'lib/llm/agent.rb', line 556

def tracer
  @tracer || @ctx.tracer
end

#tracer=(other) ⇒ void

This method returns an undefined value.

Parameters:



564
565
566
567
# File 'lib/llm/agent.rb', line 564

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

#waitArray<LLM::Function::Return>

Returns:

See Also:



483
484
485
# File 'lib/llm/agent.rb', line 483

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