Class: Riffer::Agent

Inherits:
Object
  • Object
show all
Extended by:
Helpers::ClassNameConverter
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

Modules: Run, Serializer Classes: Config, Context, Response, Session, StructuredOutput

Constant Summary collapse

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 Messages::Converter

#convert_to_file_part, #convert_to_message_object

Constructor Details

#initialize(session: nil, context: nil, config: nil) ⇒ Agent

Initializes a new agent.

When session: is omitted, a fresh Riffer::Agent::Session is built and seeded with the system instruction message and skills catalog (when configured), using context:. When session: is provided, the agent uses it as-is —the caller is responsible for the session’s contents (typical use case: cross-process resume from persisted history). With Riffer.config.experimental_history_healing on, a provided session is healed at construction time so the tool_usetool_result invariant holds before the next inference call.

context: flows through Proc-based instructions, model, skills resolution, tool resolution, guardrails, and tool runtime. It is fixed for the lifetime of the agent.

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

– : (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void



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

def initialize(session: nil, context: nil, config: nil)
  @config = config || self.class.config
  @context = Riffer::Agent::Context.new(context || {})

  @provider_name, @model_name = resolve_provider_and_model
  @provider = build_provider

  @context.skills = resolve_skills

  @structured_output = resolve_structured_output
  @tools = resolve_tools
  @tool_runtime = resolve_tool_runtime

  @instruction_message = build_instruction_message
  @skills_message = build_skills_message

  @session = session || Riffer::Agent::Session.new(messages: [@instruction_message, @skills_message].compact)
  @session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
end

Instance Attribute Details

#configObject (readonly)

The per-instance Riffer::Agent::Config. Either the class-level default or an explicit Config passed to Agent.new(config:).



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

def config
  @config
end

#contextObject (readonly)

The mutable runtime context, a Riffer::Agent::Context value object threaded into every Proc-based DSL setting, guardrail, tool runtime, and skills resolution, and shared with every Riffer::Agent::Run this agent executes. Exposes:

  • context.skills — the resolved Riffer::Skills::Context (when skills are configured), set at Agent.new time.

  • context.token_usage — the cumulative Riffer::Providers::TokenUsage, updated by each Run as the loop progresses.

  • context[:key] / context.dig(:key) — Hash-style reads for caller-provided keys (e.g. context[:agent], context[:tenant]). :skills and :token_usage are reserved and cannot be passed by the caller.



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

def context
  @context
end

#instruction_messageObject (readonly)

The system message built from the configured instructions, or nil when no instructions are configured. Built once at Agent.new using the constructor context: and cached. Useful for persistence flows.



272
273
274
# File 'lib/riffer/agent.rb', line 272

def instruction_message
  @instruction_message
end

#model_nameObject (readonly)

The resolved model name (the part after “provider/”), used as the model argument on every LLM call. Resolved eagerly at Agent.new.



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

def model_name
  @model_name
end

#providerObject (readonly)

The provider client. Built eagerly at Agent.new from the configured provider class and Config#provider_options, then handed to every Riffer::Agent::Run this agent executes. Public so tests can pre-queue responses on Riffer::Providers::Mock before calling #generate.



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

def provider
  @provider
end

#provider_nameObject (readonly)

The resolved provider name (the part before “provider/”), e.g. “openai”. Resolved eagerly at Agent.new alongside model_name; together they form the provider-neutral model identifier the agent serializes.



297
298
299
# File 'lib/riffer/agent.rb', line 297

def provider_name
  @provider_name
end

#sessionObject (readonly)

The conversation handle. See Riffer::Agent::Session.



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

def session
  @session
end

#skills_messageObject (readonly)

The system message describing the configured skills catalog, or nil when skills are unconfigured or the catalog is empty. Built once at Agent.new and cached.



277
278
279
# File 'lib/riffer/agent.rb', line 277

def skills_message
  @skills_message
end

#structured_outputObject (readonly)

The Riffer::Agent::StructuredOutput wrapping the configured schema, or nil when structured output is not configured. Resolved eagerly at Agent.new.



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

def structured_output
  @structured_output
end

#tool_runtimeObject (readonly)

The tool runtime instance used to execute tool calls. Resolved eagerly at Agent.new (Proc-form tool_runtime is called against context once).



320
321
322
# File 'lib/riffer/agent.rb', line 320

def tool_runtime
  @tool_runtime
end

#toolsObject (readonly)

The tool classes the LLM sees on every call this agent makes. Resolved eagerly at Agent.new (Proc-form uses_tools is called against context once; MCP tools and the skill_activate tool are merged in).



316
317
318
# File 'lib/riffer/agent.rb', line 316

def tools
  @tools
end

Class Method Details

.allObject

Returns all agent subclasses.

– : () -> Array



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

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

.configObject

Returns the per-class Riffer::Agent::Config value object holding every DSL setting. Lazily initialized on first read; each subclass has its own.

– : () -> Riffer::Agent::Config



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

def self.config
  @config ||= Riffer::Agent::Config.new
end

.find(identifier) ⇒ Object

Finds an agent class by identifier.

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



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

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

.from_h(hash, context: nil, session: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil) ⇒ Object

Reconstructs a runnable agent from a wire hash produced by #to_h.

Delegates to Riffer::Agent::Serializer.from_h. See it for the session seed, the tool_resolver / tool_runtime injection points, and what does not transfer.

– : (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent



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

def self.from_h(hash, context: nil, session: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
  Riffer::Agent::Serializer.from_h(hash, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
end

.from_json(json, context: nil, session: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil) ⇒ Object

Reconstructs a runnable agent from a JSON string produced by #to_json.

Delegates to Riffer::Agent::Serializer.from_json, which parses the JSON (with symbol keys) for you. See Riffer::Agent::Serializer.from_h for the session seed and the tool_resolver / tool_runtime injection points.

– : (String, ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent



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

def self.from_json(json, context: nil, session: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
  Riffer::Agent::Serializer.from_json(json, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
end

.generate(prompt = nil, files: nil, context: nil) ⇒ Object

Generates a response using a new agent instance.

context: is threaded into new; prompt and files: are forwarded to #generate.

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



200
201
202
# File 'lib/riffer/agent.rb', line 200

def self.generate(prompt = nil, files: nil, context: nil)
  new(context: context).generate(prompt, files: files)
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



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

def self.guardrail(phase, with:, **options)
  config.add_guardrail(phase, klass: with, options: options)
end

.guardrails_for(phase) ⇒ Object

Returns the registered guardrail configs for a given phase.

phase

:before or :after.

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



258
259
260
# File 'lib/riffer/agent.rb', line 258

def self.guardrails_for(phase)
  config.guardrails_for(phase)
end

.identifier(value = nil) ⇒ Object

Gets or sets the agent identifier.

– : (?String?) -> String



42
43
44
# File 'lib/riffer/agent.rb', line 42

def self.identifier(value = nil)
  value.nil? ? (config.identifier || class_name_to_path(name)) : (config.identifier = value)
end

.instructions(value = 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)?



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

def self.instructions(value = nil)
  value.nil? ? config.instructions : (config.instructions = value)
end

.max_steps(*value) ⇒ Object

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

Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to nil for unlimited steps. The splat distinguishes a getter call (no argument) from setting the limit to nil.

max_steps        # reads the current limit
max_steps 8      # cap the loop at 8 steps
max_steps nil    # unlimited

– : (*Numeric?) -> Numeric?



115
116
117
118
# File 'lib/riffer/agent.rb', line 115

def self.max_steps(*value)
  return config.max_steps if value.empty?
  config.max_steps = value.first
end

.mcp_configsObject

Returns the accumulated use_mcp configurations for this agent class.

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



141
142
143
# File 'lib/riffer/agent.rb', line 141

def self.mcp_configs
  config.mcp_configs
end

.model(value = nil) ⇒ Object

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

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



50
51
52
# File 'lib/riffer/agent.rb', line 50

def self.model(value = nil)
  value.nil? ? config.model : (config.model = value)
end

.model_options(options = nil) ⇒ Object

Gets or sets model options passed to generate_text/stream_text.

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



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

def self.model_options(options = nil)
  options.nil? ? config.model_options : (config.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]



76
77
78
# File 'lib/riffer/agent.rb', line 76

def self.provider_options(options = nil)
  options.nil? ? config.provider_options : (config.provider_options = options)
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

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



168
169
170
171
172
173
174
175
# File 'lib/riffer/agent.rb', line 168

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

.stream(prompt = nil, files: nil, context: nil) ⇒ Object

Streams a response using a new agent instance.

context: is threaded into new; prompt and files: are forwarded to #stream.

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



211
212
213
# File 'lib/riffer/agent.rb', line 211

def self.stream(prompt = nil, files: nil, context: nil)
  new(context: context).stream(prompt, files: files)
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?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?



94
95
96
97
98
99
100
101
# File 'lib/riffer/agent.rb', line 94

def self.structured_output(params = nil, &block)
  if block
    params = Riffer::Params.new
    params.instance_eval(&block)
  end
  config.structured_output = params if params
  config.structured_output
end

.tool_runtime(value = nil) ⇒ Object

Gets or sets the tool runtime for this agent.

Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance, or a Proc. Defaults to Riffer.config.tool_runtime when unset.

– : (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)



152
153
154
# File 'lib/riffer/agent.rb', line 152

def self.tool_runtime(value = nil)
  value.nil? ? config.tool_runtime : (config.tool_runtime = value)
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



134
135
136
# File 'lib/riffer/agent.rb', line 134

def self.use_mcp(tag)
  config.add_mcp(tag)
end

.uses_tools(value = nil) ⇒ Object

Gets or sets the tools used by this agent.

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



124
125
126
# File 'lib/riffer/agent.rb', line 124

def self.uses_tools(value = nil)
  value.nil? ? config.tools_config : (config.tools_config = value)
end

Instance Method Details

#generate(prompt = nil, files: nil) ⇒ Object

Generates a response from the agent.

Runs the inference loop via Riffer::Agent::Run.generate. When prompt is given, a new Riffer::Messages::User is appended to the session (silently — on_message does not fire for user inputs) and then the loop runs. When prompt is omitted, the loop runs against the current session — useful for resuming a persisted conversation whose last turn is already a user message, or for picking up pending tool calls after an interrupt.

files: requires prompt. Pass files to attach to the new user message.

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



376
377
378
# File 'lib/riffer/agent.rb', line 376

def generate(prompt = nil, files: nil)
  Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files)
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.

When Riffer.config.experimental_history_healing is enabled, riffer fills any orphaned tool_use on the way out with a placeholder Riffer::Messages::Tool carrying error_type: :interrupted. The filled call_ids are exposed on Riffer::Agent::Response#healed_tool_call_ids (and the streaming Riffer::StreamEvents::Interrupt event).

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



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

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

#stream(prompt = nil, files: nil) ⇒ Object

Streams a response from the agent.

Runs the inference loop via Riffer::Agent::Run.stream, returning an Enumerator of Riffer::StreamEvents.

Raises Riffer::ArgumentError if structured output is configured.

See #generate for prompt/files semantics.

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



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

def stream(prompt = nil, files: nil)
  raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if @structured_output
  Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files)
end

#to_hObject

Snapshots this resolved agent into a self-contained, provider-neutral wire hash. Delegates to Riffer::Agent::Serializer.to_h.

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



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

def to_h
  Riffer::Agent::Serializer.to_h(agent: self)
end

#to_jsonObject

Snapshots this resolved agent into a wire JSON string. Delegates to Riffer::Agent::Serializer.to_json. The * absorbs the JSON generator state argument so JSON.generate(agent) works too.

– : (*untyped) -> String



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

def to_json(*)
  Riffer::Agent::Serializer.to_json(agent: self)
end