Class: Riffer::Agent

Inherits:
Object
  • Object
show all
Extended by:
Helpers::ClassNameConverter, Helpers::Validations
Includes:
Messages::Converter
Defined in:
lib/riffer/agent.rb

Overview

Riffer::Agent is the base class for all agents in the Riffer framework.

Provides orchestration for LLM calls, tool use, and message management. Subclass this to create your own agents.

See Riffer::Messages and Riffer::Providers.

class MyAgent < Riffer::Agent
  model 'openai/gpt-4o'
  instructions 'You are a helpful assistant.'
end

agent = MyAgent.new
agent.generate('Hello!')

Defined Under Namespace

Classes: Response

Constant Summary collapse

DEFAULT_MAX_STEPS =

: Integer

16
INTERRUPT_MAX_STEPS =

: Symbol

:max_steps

Constants included from Helpers::ClassNameConverter

Helpers::ClassNameConverter::DEFAULT_SEPARATOR

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers::ClassNameConverter

class_name_to_path

Methods included from Helpers::Validations

validate_is_string!

Methods included from Messages::Converter

#convert_to_file_part, #convert_to_message_object

Constructor Details

#initializeAgent

Initializes a new agent.

Raises Riffer::ArgumentError if the configured model string is invalid (must be “provider/model” format).

– : () -> void



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/riffer/agent.rb', line 302

def initialize
  @messages = []
  @message_callbacks = []
  @token_usage = nil
  @interrupted = false
  @model_config = self.class.model
  @instructions_config = self.class.instructions

  if @model_config.is_a?(Proc)
    @provider_name = nil
    @model_name = nil
  else
    parse_model_string!(@model_config)
  end
end

Instance Attribute Details

#messagesObject (readonly)

The message history for the agent.



290
291
292
# File 'lib/riffer/agent.rb', line 290

def messages
  @messages
end

#token_usageObject (readonly)

Cumulative token usage across all LLM calls.



293
294
295
# File 'lib/riffer/agent.rb', line 293

def token_usage
  @token_usage
end

Class Method Details

.allObject

Returns all agent subclasses.

– : () -> Array



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

def self.all
  subclasses #: Array[singleton(Riffer::Agent)]
end

.find(identifier) ⇒ Object

Finds an agent class by identifier.

– : (String) -> singleton(Riffer::Agent)?



218
219
220
# File 'lib/riffer/agent.rb', line 218

def self.find(identifier)
  all.find { |agent_class| agent_class.identifier == identifier.to_s }
end

.generateObject

Generates a response using a new agent instance.

See #generate for parameters and return value.

– : (*untyped, **untyped) -> Riffer::Agent::Response



236
237
238
# File 'lib/riffer/agent.rb', line 236

def self.generate(...)
  new.generate(...)
end

.guardrail(phase, with:, **options) ⇒ Object

Registers a guardrail for input, output, or both phases.

phase

:before, :after, or :around.

with

the guardrail class (must be subclass of Riffer::Guardrail).

options

additional options passed to the guardrail.

Raises Riffer::ArgumentError if phase is invalid or guardrail is not a Guardrail class. – : (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/riffer/agent.rb', line 259

def self.guardrail(phase, with:, **options)
  valid_phases = [*Riffer::Guardrails::PHASES, :around]
  raise Riffer::ArgumentError, "Invalid guardrail phase: #{phase}" unless valid_phases.include?(phase)
  raise Riffer::ArgumentError, "Guardrail must be a Riffer::Guardrail subclass" unless with.is_a?(Class) && with <= Riffer::Guardrail

  @guardrails ||= {before: [], after: []}
  config = {class: with, options: options}

  case phase
  when :before
    @guardrails[:before] << config
  when :after
    @guardrails[:after] << config
  when :around
    @guardrails[:before] << config
    @guardrails[:after] << config
  end
end

.guardrails_for(phase) ⇒ Object

Returns the registered guardrail configs for a given phase.

phase

:before or :after.

– : (Symbol) -> Array[Hash[Symbol, untyped]]



284
285
286
287
# File 'lib/riffer/agent.rb', line 284

def self.guardrails_for(phase)
  @guardrails ||= {before: [], after: []}
  @guardrails[phase] || []
end

.identifier(value = nil) ⇒ Object

Gets or sets the agent identifier.

– : (?String?) -> String



33
34
35
36
# File 'lib/riffer/agent.rb', line 33

def self.identifier(value = nil)
  return @identifier || class_name_to_path(name) if value.nil?
  @identifier = value.to_s
end

.instructions(instructions_or_proc = nil) ⇒ Object

Gets or sets the agent instructions.

Accepts a static string or a Proc for dynamic instructions. When a Proc is given, it is called at generate time and receives the context hash (which may be nil).

instructions "You are a helpful assistant."

instructions -> (context) {
  "You are assisting #{context[:name]}"
}

– : (?(String | Proc)?) -> (String | Proc)?



67
68
69
70
71
72
73
74
75
76
# File 'lib/riffer/agent.rb', line 67

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

  if instructions_or_proc.is_a?(Proc)
    @instructions = instructions_or_proc
  else
    validate_is_string!(instructions_or_proc, "instructions")
    @instructions = instructions_or_proc
  end
end

.max_steps(value = nil) ⇒ Object

Gets or sets the maximum number of LLM call steps in the tool-use loop.

Defaults to DEFAULT_MAX_STEPS (16). Set to Float::INFINITY for unlimited steps.

– : (?Numeric?) -> Numeric



121
122
123
124
# File 'lib/riffer/agent.rb', line 121

def self.max_steps(value = nil)
  return @max_steps || DEFAULT_MAX_STEPS if value.nil?
  @max_steps = value
end

.model(model_string_or_proc = nil) ⇒ Object

Gets or sets the model string (e.g., “openai/gpt-4o”) or Proc.

– : (?(String | Proc)?) -> (String | Proc)?



42
43
44
45
46
47
48
49
50
51
# File 'lib/riffer/agent.rb', line 42

def self.model(model_string_or_proc = nil)
  return @model if model_string_or_proc.nil?

  if model_string_or_proc.is_a?(Proc)
    @model = model_string_or_proc
  else
    validate_is_string!(model_string_or_proc, "model")
    @model = model_string_or_proc
  end
end

.model_options(options = nil) ⇒ Object

Gets or sets model options passed to generate_text/stream_text.

– : (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]



91
92
93
94
# File 'lib/riffer/agent.rb', line 91

def self.model_options(options = nil)
  return @model_options || {} if options.nil?
  @model_options = options
end

.provider_options(options = nil) ⇒ Object

Gets or sets provider options passed to the provider client.

– : (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]



82
83
84
85
# File 'lib/riffer/agent.rb', line 82

def self.provider_options(options = nil)
  return @provider_options || {} if options.nil?
  @provider_options = options
end

.resolved_tool_classes(context: nil) ⇒ Object

Returns the tool classes the LLM should see for this agent.

Class-level companion to the instance #resolved_tools. Resolves the Proc form of uses_tools and appends the skill activation tool when a skills block is configured. Does not read the skills backend —the LLM-facing tool schema reflects class-level intent, not the runtime state of any backend.

When uses_tools is a Proc, context is forwarded to it.

The activation tool class is resolved from the agent’s skills do; activate_tool ...; end override when set, otherwise from Riffer.config.skills.default_activate_tool.

Raises Riffer::ArgumentError on tool name conflicts with the skill activation tool.

– : (?context: Hash[Symbol, untyped]?) -> Array



154
155
156
157
158
159
160
161
162
163
# File 'lib/riffer/agent.rb', line 154

def self.resolved_tool_classes(context: nil)
  base = resolve_uses_tools_config(context)
  return base unless skills

  skill_activate_tool_class = skills.activate_tool || Riffer.config.skills.default_activate_tool
  if base.any? { |t| t.name == skill_activate_tool_class.name }
    raise Riffer::ArgumentError, "Tool name conflict with skill tools: #{skill_activate_tool_class.name}"
  end
  base + [skill_activate_tool_class]
end

.skills(&block) ⇒ Object

Configures skills for this agent via a block DSL.

Returns the current Riffer::Skills::Config when called without a block.

skills do
  backend Riffer::Skills::FilesystemBackend.new(".skills")
  adapter Riffer::Skills::XmlAdapter
  activate ["code-review"]
end

– : () ?{ () -> void } -> Riffer::Skills::Config?



206
207
208
209
210
211
212
# File 'lib/riffer/agent.rb', line 206

def self.skills(&block)
  if block
    @skills_config = Riffer::Skills::Config.new
    @skills_config.instance_eval(&block)
  end
  @skills_config
end

.streamObject

Streams a response using a new agent instance.

See #stream for parameters and return value.

– : (*untyped, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]



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

def self.stream(...)
  new.stream(...)
end

.structured_output(params = nil, &block) ⇒ Object

Gets or sets the structured output schema for this agent.

Accepts a Riffer::Params instance or a block evaluated against a new Params.

– : (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/riffer/agent.rb', line 102

def self.structured_output(params = nil, &block)
  if block
    @structured_output = Riffer::Params.new
    @structured_output.instance_eval(&block)
  elsif params.nil?
    @structured_output
  else
    raise Riffer::ArgumentError, "structured_output must be a Riffer::Params" unless params.is_a?(Riffer::Params)
    @structured_output = params
  end
end

.tool_runtime(value = nil) ⇒ Object

Gets or sets the tool runtime for this agent.

Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance, or a Proc.

Inherited by subclasses. When unset, walks the ancestor chain and falls back to the global Riffer.config.tool_runtime.

– : (?(singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?) -> (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?



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

def self.tool_runtime(value = nil)
  if value.nil?
    return @tool_runtime if instance_variable_defined?(:@tool_runtime)
    superclass.respond_to?(:tool_runtime) ? superclass.tool_runtime : nil
  else
    @tool_runtime = value
  end
end

.uses_tools(tools_or_lambda = nil) ⇒ Object

Gets or sets the tools used by this agent.

– : (?(Array | Proc)?) -> (Array | Proc)?



130
131
132
133
# File 'lib/riffer/agent.rb', line 130

def self.uses_tools(tools_or_lambda = nil)
  return @tools_config if tools_or_lambda.nil?
  @tools_config = tools_or_lambda
end

Instance Method Details

#generate(prompt_or_messages, files: nil, context: nil) ⇒ Object

Generates a response from the agent.

– : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/riffer/agent.rb', line 322

def generate(prompt_or_messages, files: nil, context: nil)
  @context = context
  prepare_run
  @structured_output = resolve_structured_output
  initialize_messages(prompt_or_messages, files: files)

  all_modifications = [] #: Array[Riffer::Guardrails::Modification]

  tripwire, modifications = run_before_guardrails
  all_modifications.concat(modifications)
  return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire

  run_generate_loop(all_modifications)
end

#generate_instruction_message(context: nil) ⇒ Object

Generates the instruction system message for this agent.

Useful for database persistence workflows where the system messages need to be stored independently.

Returns nil when no instructions are configured.

– : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?



384
385
386
# File 'lib/riffer/agent.rb', line 384

def generate_instruction_message(context: nil)
  build_instruction_message(context)
end

#generate_skills_message(context: nil) ⇒ Object

Generates the skills catalog system message for this agent.

Useful for database persistence workflows where the system messages need to be stored independently.

Returns nil when no skills are configured or the catalog is empty.

– : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?



397
398
399
# File 'lib/riffer/agent.rb', line 397

def generate_skills_message(context: nil)
  build_skills_message(resolve_skills(context))
end

#interrupt!(reason = nil) ⇒ Object

Interrupts the agent loop.

Call from an on_message callback to cleanly interrupt the loop. Equivalent to throw :riffer_interrupt, reason.

– : (?(String | Symbol)?) -> void



408
409
410
# File 'lib/riffer/agent.rb', line 408

def interrupt!(reason = nil)
  throw :riffer_interrupt, reason
end

#on_message(&block) ⇒ Object

Registers a callback to be invoked when messages are added during generation.

Raises Riffer::ArgumentError if no block is given.

– : () { (Riffer::Messages::Base) -> void } -> self



369
370
371
372
373
# File 'lib/riffer/agent.rb', line 369

def on_message(&block)
  raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
  @message_callbacks << block
  self
end

#stream(prompt_or_messages, files: nil, context: nil) ⇒ Object

Streams a response from the agent.

Raises Riffer::ArgumentError if structured output is configured.

– : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/riffer/agent.rb', line 343

def stream(prompt_or_messages, files: nil, context: nil)
  raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if self.class.structured_output

  @context = context
  prepare_run
  initialize_messages(prompt_or_messages, files: files)

  Enumerator.new do |yielder|
    tripwire, modifications = run_before_guardrails
    modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }

    if tripwire
      yielder << Riffer::StreamEvents::GuardrailTripwire.new(tripwire)
      next
    end

    run_stream_loop(yielder)
  end
end