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::LoopGuard detects repeated tool-call patterns and blocks stuck execution before more tool work is queued.
  • The default tool attempt budget is 25. After that, the agent sends advisory tool errors back through the model and keeps the loop in-band. Set tool_attempts: nil to disable that advisory behavior.
  • 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



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/llm/agent.rb', line 328

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

Instance Attribute Details

#llmLLM::Provider (readonly)

Returns a provider

Returns:



68
69
70
# File 'lib/llm/agent.rb', line 68

def llm
  @llm
end

Class Method Details

.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



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

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

.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



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

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



153
154
155
156
# File 'lib/llm/agent.rb', line 153

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



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

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

.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



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

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

.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



194
195
196
197
# File 'lib/llm/agent.rb', line 194

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



205
206
207
208
# File 'lib/llm/agent.rb', line 205

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

.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)


225
226
227
228
# File 'lib/llm/agent.rb', line 225

def self.concurrency(concurrency = nil)
  return @concurrency if concurrency.nil?
  @concurrency = concurrency
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:



245
246
247
248
# File 'lib/llm/agent.rb', line 245

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



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

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


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

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

.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)


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

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

.set(properties)

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



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

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

Instance Method Details

#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.



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

def on_tool_confirmation(fn, strategy)
  fn.cancel
end

#nameString

Returns the agent's name

Returns:

  • (String)


353
354
355
# File 'lib/llm/agent.rb', line 353

def name
  @name
end

#pathString?

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

Returns:

  • (String, nil)


361
362
363
# File 'lib/llm/agent.rb', line 361

def path
  @path
end

#descriptionString?

Returns the agent's description

Returns:

  • (String, nil)


368
369
370
# File 'lib/llm/agent.rb', line 368

def description
  @description
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_attempts (Integer)

    The maxinum number of tool call iterations before the agent sends in-band advisory tool errors back through the model (default 25). Set to nil to disable advisory tool-limit returns.

Returns:



388
389
390
391
392
# File 'lib/llm/agent.rb', line 388

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

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

See Also:



396
397
398
399
400
# File 'lib/llm/agent.rb', line 396

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

#messagesLLM::Buffer<LLM::Message>



404
405
406
# File 'lib/llm/agent.rb', line 404

def messages
  @ctx.messages
end

#pending_functionsArray<LLM::Function>

Returns:



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

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

#returnsArray<LLM::Function::Return>

Returns:

See Also:



417
418
419
# File 'lib/llm/agent.rb', line 417

def returns
  @ctx.returns
end

#waitArray<LLM::Function::Return>

Returns:

See Also:



424
425
426
# File 'lib/llm/agent.rb', line 424

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

#usageLLM::Object

Returns:



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

def usage
  @ctx.usage
end

#interrupt!nil Also known as: cancel!

Interrupt the active request, if any.

Returns:

  • (nil)


437
438
439
# File 'lib/llm/agent.rb', line 437

def interrupt!
  @ctx.interrupt!
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:



446
447
448
# File 'lib/llm/agent.rb', line 446

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

#image_url(url) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • url (String)

    The URL

Returns:



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

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

#local_file(path) ⇒ LLM::Object

Returns a tagged object

Parameters:

  • path (String)

    The path

Returns:



465
466
467
# File 'lib/llm/agent.rb', line 465

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

#remote_file(res) ⇒ LLM::Object

Returns a tagged object

Parameters:

Returns:



474
475
476
# File 'lib/llm/agent.rb', line 474

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

#tracerLLM::Tracer

Returns an LLM tracer

Returns:



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

def tracer
  @tracer || @ctx.tracer
end

#tracer=(other)

This method returns an undefined value.

Parameters:



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

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

#streamLLM::Stream, ...

Returns a stream object, or nil

Returns:

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

    Returns a stream object, or nil



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

def stream
  @ctx.stream
end

#modelString

Returns the model an Agent is actively using

Returns:

  • (String)


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

def model
  @ctx.model
end

#modeSymbol

Returns:

  • (Symbol)


510
511
512
# File 'lib/llm/agent.rb', line 510

def mode
  @ctx.mode
end

#concurrencySymbol, ...

Returns the configured tool execution concurrency.

Returns:

  • (Symbol, Array<Symbol>, nil)


517
518
519
# File 'lib/llm/agent.rb', line 517

def concurrency
  @concurrency
end

#costLLM::Cost

Returns:

See Also:



524
525
526
# File 'lib/llm/agent.rb', line 524

def cost
  @ctx.cost
end

#context_windowInteger

Returns:

  • (Integer)

See Also:



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

def context_window
  @ctx.context_window
end

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

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.



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/llm/agent.rb', line 559

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

#paramsHash

Returns:

  • (Hash)

See Also:



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

def params
  @ctx.params
end

#to_hHash

Returns:

  • (Hash)

See Also:



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

def to_h
  @ctx.to_h
end

#to_jsonString

Returns:

  • (String)


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

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

#inspectString

Returns:

  • (String)


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

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

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

This method returns an undefined value.



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

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

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

Returns:



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

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