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



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/llm/agent.rb', line 352

def initialize(llm, params = {})
  params = {}.merge!(params)
  @llm = llm
  fields = %i[name description path tool_budget model skills schema tracer stream tools concurrency instructions confirm]
  fields_ivar = %i[name description path tool_budget tracer concurrency instructions confirm]
  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:



70
71
72
# File 'lib/llm/agent.rb', line 70

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)


227
228
229
230
# File 'lib/llm/agent.rb', line 227

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)


293
294
295
296
297
298
299
300
# File 'lib/llm/agent.rb', line 293

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



141
142
143
144
145
146
147
# File 'lib/llm/agent.rb', line 141

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



207
208
209
210
# File 'lib/llm/agent.rb', line 207

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



155
156
157
158
# File 'lib/llm/agent.rb', line 155

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



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/llm/agent.rb', line 119

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)


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

def self.path(path = UNDEFINED, &block)
  if path.equal?(UNDEFINED)
    @path
  else
    @path = path || block
  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



196
197
198
199
# File 'lib/llm/agent.rb', line 196

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



99
100
101
102
103
104
105
106
107
# File 'lib/llm/agent.rb', line 99

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



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

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:



267
268
269
270
# File 'lib/llm/agent.rb', line 267

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)


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

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



166
167
168
169
170
171
172
173
# File 'lib/llm/agent.rb', line 166

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:



247
248
249
250
# File 'lib/llm/agent.rb', line 247

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:



421
422
423
424
425
# File 'lib/llm/agent.rb', line 421

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

#compacted?Boolean

Returns:

  • (Boolean)

See Also:



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

def compacted?
  @ctx.compacted?
end

#concurrencySymbol, ...

Returns the configured tool execution concurrency.

Returns:

  • (Symbol, Array<Symbol>, nil)


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

def concurrency
  @concurrency
end

#context_windowInteger

Returns:

  • (Integer)

See Also:



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

def context_window
  @ctx.context_window
end

#costLLM::Cost

Returns:

See Also:



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

def cost
  @ctx.cost
end

#descriptionString?

Returns the agent's description

Returns:

  • (String, nil)


392
393
394
# File 'lib/llm/agent.rb', line 392

def description
  @description
end

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

Returns:



646
647
648
649
# File 'lib/llm/agent.rb', line 646

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

#image_url(url) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • url (String)

    The URL

Returns:



481
482
483
# File 'lib/llm/agent.rb', line 481

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

#inspectString

Returns:

  • (String)


630
631
632
633
# File 'lib/llm/agent.rb', line 630

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)


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

def interrupt!
  @ctx.interrupt!
end

#local_file(path) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • path (String)

    The path

Returns:



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

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

#messagesLLM::Buffer<LLM::Message>



429
430
431
# File 'lib/llm/agent.rb', line 429

def messages
  @ctx.messages
end

#modeSymbol

Returns:

  • (Symbol)


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

def mode
  @ctx.mode
end

#modelString

Returns the model an Agent is actively using

Returns:

  • (String)


529
530
531
# File 'lib/llm/agent.rb', line 529

def model
  @ctx.model
end

#nameString

Returns the agent's name

Returns:

  • (String)


377
378
379
# File 'lib/llm/agent.rb', line 377

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.



663
664
665
# File 'lib/llm/agent.rb', line 663

def on_tool_confirmation(fn, strategy)
  fn.cancel
end

#paramsHash

Returns:

  • (Hash)

See Also:



611
612
613
# File 'lib/llm/agent.rb', line 611

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)


385
386
387
# File 'lib/llm/agent.rb', line 385

def path
  @path
end

#pending_functionsArray<LLM::Function>

Returns:



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

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:



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

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

#remote_file(res) ⇒ LLM::Object

Returns a tagged object

Parameters:

Returns:



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

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.



591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/llm/agent.rb', line 591

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:



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

def returns
  @ctx.returns
end

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

This method returns an undefined value.



638
639
640
# File 'lib/llm/agent.rb', line 638

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



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

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:



413
414
415
416
417
# File 'lib/llm/agent.rb', line 413

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

#to_hHash

Returns:

  • (Hash)

See Also:



618
619
620
# File 'lib/llm/agent.rb', line 618

def to_h
  @ctx.to_h
end

#to_jsonString

Returns:

  • (String)


624
625
626
# File 'lib/llm/agent.rb', line 624

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

#tracerLLM::Tracer

Returns an LLM tracer

Returns:



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

def tracer
  @tracer || @ctx.tracer
end

#tracer=(other) ⇒ void

This method returns an undefined value.

Parameters:



514
515
516
517
# File 'lib/llm/agent.rb', line 514

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

#usageLLM::Object

Returns:



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

def usage
  @ctx.usage
end

#waitArray<LLM::Function::Return>

Returns:

See Also:



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

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