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



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

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.



308
309
310
# File 'lib/riffer/agent.rb', line 308

def messages
  @messages
end

#token_usageObject (readonly)

Cumulative token usage across all LLM calls.



311
312
313
# File 'lib/riffer/agent.rb', line 311

def token_usage
  @token_usage
end

Class Method Details

.allObject

Returns all agent subclasses.

– : () -> Array



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

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

.find(identifier) ⇒ Object

Finds an agent class by identifier.

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



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

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



254
255
256
# File 'lib/riffer/agent.rb', line 254

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



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/riffer/agent.rb', line 277

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]]



302
303
304
305
# File 'lib/riffer/agent.rb', line 302

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

.mcp_configsObject

Returns the accumulated use_mcp configurations for this agent class.

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



189
190
191
# File 'lib/riffer/agent.rb', line 189

def self.mcp_configs
  @mcp_configs || []
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?



224
225
226
227
228
229
230
# File 'lib/riffer/agent.rb', line 224

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]



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

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



203
204
205
206
207
208
209
210
# File 'lib/riffer/agent.rb', line 203

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

.use_mcp(tag) ⇒ Object

Opts this agent into tools from all MCP registrations that share any of the given tag(s).

tag - a String or Symbol; matched against registration manifest tags.

: (String | Symbol) -> void



181
182
183
184
# File 'lib/riffer/agent.rb', line 181

def self.use_mcp(tag)
  @mcp_configs ||= []
  @mcp_configs << {tags: [tag.to_sym]}
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



340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/riffer/agent.rb', line 340

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?



402
403
404
# File 'lib/riffer/agent.rb', line 402

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?



415
416
417
# File 'lib/riffer/agent.rb', line 415

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



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

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



387
388
389
390
391
# File 'lib/riffer/agent.rb', line 387

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]



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/riffer/agent.rb', line 361

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