riffer 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agents/architecture.md +18 -11
- data/.agents/code-style.md +1 -1
- data/.agents/rbs-inline.md +2 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +19 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +87 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +17 -17
- data/docs/08_MESSAGES.md +25 -32
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +7 -18
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/06_MOCK_PROVIDER.md +2 -1
- data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
- data/docs/providers/08_GEMINI.md +2 -2
- data/docs/providers/09_OPENROUTER.md +242 -0
- data/lib/riffer/agent/config.rb +173 -0
- data/lib/riffer/agent/context.rb +125 -0
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +268 -0
- data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
- data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
- data/lib/riffer/agent.rb +234 -923
- data/lib/riffer/config.rb +14 -7
- data/lib/riffer/evals/evaluator.rb +13 -3
- data/lib/riffer/evals/judge.rb +2 -2
- data/lib/riffer/evals/run_result.rb +2 -1
- data/lib/riffer/evals/scenario_result.rb +2 -1
- data/lib/riffer/guardrails/runner.rb +3 -2
- data/lib/riffer/helpers/call_or_value.rb +16 -0
- data/lib/riffer/helpers.rb +0 -1
- data/lib/riffer/mcp/authenticated_tool.rb +4 -0
- data/lib/riffer/mcp/client.rb +1 -1
- data/lib/riffer/mcp/registration.rb +2 -3
- data/lib/riffer/mcp/registry.rb +3 -1
- data/lib/riffer/mcp/tool_factory.rb +5 -0
- data/lib/riffer/messages/assistant.rb +9 -3
- data/lib/riffer/messages/base.rb +22 -0
- data/lib/riffer/messages/converter.rb +6 -6
- data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
- data/lib/riffer/messages/tool.rb +1 -1
- data/lib/riffer/messages/user.rb +4 -4
- data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
- data/lib/riffer/{param.rb → params/param.rb} +6 -6
- data/lib/riffer/params.rb +27 -21
- data/lib/riffer/providers/amazon_bedrock.rb +19 -20
- data/lib/riffer/providers/anthropic.rb +27 -28
- data/lib/riffer/providers/base.rb +10 -9
- data/lib/riffer/providers/gemini.rb +15 -12
- data/lib/riffer/providers/mock.rb +41 -13
- data/lib/riffer/providers/open_ai.rb +24 -22
- data/lib/riffer/providers/open_router.rb +318 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
- data/lib/riffer/providers.rb +1 -0
- data/lib/riffer/runner/fibers.rb +4 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +1 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +1 -1
- data/lib/riffer/skills/context.rb +3 -3
- data/lib/riffer/skills/filesystem_backend.rb +7 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +1 -1
- data/lib/riffer/stream_events/token_usage_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +1 -1
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
- data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
- data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
- data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -1
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +91 -0
- data/sig/generated/riffer/agent/run.rbs +144 -0
- data/sig/generated/riffer/agent/session/repair.rbs +51 -0
- data/sig/generated/riffer/agent/session.rbs +145 -0
- data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
- data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
- data/sig/generated/riffer/agent.rbs +143 -342
- data/sig/generated/riffer/config.rbs +17 -5
- data/sig/generated/riffer/evals/judge.rbs +2 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/messages/assistant.rbs +7 -3
- data/sig/generated/riffer/messages/base.rbs +18 -0
- data/sig/generated/riffer/messages/converter.rbs +4 -4
- data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
- data/sig/generated/riffer/messages/user.rbs +4 -4
- data/sig/generated/riffer/params/boolean.rbs +10 -0
- data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
- data/sig/generated/riffer/params.rbs +15 -15
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +10 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +25 -5
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +85 -0
- data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
- data/sig/generated/riffer/providers.rbs +1 -0
- data/sig/generated/riffer/runner/fibers.rbs +2 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +2 -2
- data/sig/generated/riffer/runner.rbs +2 -2
- data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
- data/sig/generated/riffer/skills/config.rbs +1 -1
- data/sig/generated/riffer/skills/context.rbs +2 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
- data/sig/stubs/agent_ivars.rbs +7 -0
- data/sig/stubs/async.rbs +24 -0
- data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/stubs/extend_self.rbs +11 -0
- data/sig/stubs/lib_ivars.rbs +101 -0
- data/sig/stubs/mcp_sdk.rbs +22 -0
- data/sig/stubs/provider_ivars.rbs +36 -0
- data/sig/stubs/provider_sdk_methods.rbs +50 -0
- data/sig/stubs/zeitwerk.rbs +12 -0
- metadata +54 -33
- data/lib/riffer/core.rb +0 -28
- data/lib/riffer/helpers/validations.rb +0 -18
- data/sig/generated/riffer/boolean.rbs +0 -10
- data/sig/generated/riffer/core.rbs +0 -19
- data/sig/generated/riffer/helpers/validations.rbs +0 -12
data/lib/riffer/agent.rb
CHANGED
|
@@ -21,40 +21,32 @@ require "json"
|
|
|
21
21
|
class Riffer::Agent
|
|
22
22
|
include Riffer::Messages::Converter
|
|
23
23
|
extend Riffer::Helpers::ClassNameConverter
|
|
24
|
-
extend Riffer::Helpers::Validations
|
|
25
24
|
|
|
26
|
-
DEFAULT_MAX_STEPS = 16 #: Integer
|
|
27
25
|
INTERRUPT_MAX_STEPS = :max_steps #: Symbol
|
|
28
26
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
# Returns the per-class Riffer::Agent::Config value object holding every
|
|
28
|
+
# DSL setting. Lazily initialized on first read; each subclass has its own.
|
|
29
|
+
#
|
|
30
|
+
#--
|
|
31
|
+
#: () -> Riffer::Agent::Config
|
|
32
|
+
def self.config
|
|
33
|
+
@config ||= Riffer::Agent::Config.new
|
|
34
|
+
end
|
|
35
35
|
|
|
36
36
|
# Gets or sets the agent identifier.
|
|
37
37
|
#
|
|
38
38
|
#--
|
|
39
39
|
#: (?String?) -> String
|
|
40
40
|
def self.identifier(value = nil)
|
|
41
|
-
|
|
42
|
-
@identifier = value.to_s
|
|
41
|
+
value.nil? ? (config.identifier || class_name_to_path(name)) : (config.identifier = value)
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
# Gets or sets the model string (e.g., "openai/gpt-4o") or Proc.
|
|
46
45
|
#
|
|
47
46
|
#--
|
|
48
47
|
#: (?(String | Proc)?) -> (String | Proc)?
|
|
49
|
-
def self.model(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if model_string_or_proc.is_a?(Proc)
|
|
53
|
-
@model = model_string_or_proc
|
|
54
|
-
else
|
|
55
|
-
validate_is_string!(model_string_or_proc, "model")
|
|
56
|
-
@model = model_string_or_proc
|
|
57
|
-
end
|
|
48
|
+
def self.model(value = nil)
|
|
49
|
+
value.nil? ? config.model : (config.model = value)
|
|
58
50
|
end
|
|
59
51
|
|
|
60
52
|
# Gets or sets the agent instructions.
|
|
@@ -71,15 +63,8 @@ class Riffer::Agent
|
|
|
71
63
|
#
|
|
72
64
|
#--
|
|
73
65
|
#: (?(String | Proc)?) -> (String | Proc)?
|
|
74
|
-
def self.instructions(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if instructions_or_proc.is_a?(Proc)
|
|
78
|
-
@instructions = instructions_or_proc
|
|
79
|
-
else
|
|
80
|
-
validate_is_string!(instructions_or_proc, "instructions")
|
|
81
|
-
@instructions = instructions_or_proc
|
|
82
|
-
end
|
|
66
|
+
def self.instructions(value = nil)
|
|
67
|
+
value.nil? ? config.instructions : (config.instructions = value)
|
|
83
68
|
end
|
|
84
69
|
|
|
85
70
|
# Gets or sets provider options passed to the provider client.
|
|
@@ -87,8 +72,7 @@ class Riffer::Agent
|
|
|
87
72
|
#--
|
|
88
73
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
89
74
|
def self.provider_options(options = nil)
|
|
90
|
-
|
|
91
|
-
@provider_options = options
|
|
75
|
+
options.nil? ? config.provider_options : (config.provider_options = options)
|
|
92
76
|
end
|
|
93
77
|
|
|
94
78
|
# Gets or sets model options passed to generate_text/stream_text.
|
|
@@ -96,8 +80,7 @@ class Riffer::Agent
|
|
|
96
80
|
#--
|
|
97
81
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
98
82
|
def self.model_options(options = nil)
|
|
99
|
-
|
|
100
|
-
@model_options = options
|
|
83
|
+
options.nil? ? config.model_options : (config.model_options = options)
|
|
101
84
|
end
|
|
102
85
|
|
|
103
86
|
# Gets or sets the structured output schema for this agent.
|
|
@@ -105,90 +88,35 @@ class Riffer::Agent
|
|
|
105
88
|
# Accepts a Riffer::Params instance or a block evaluated against a new Params.
|
|
106
89
|
#
|
|
107
90
|
#--
|
|
108
|
-
#: (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
|
|
91
|
+
#: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
109
92
|
def self.structured_output(params = nil, &block)
|
|
110
93
|
if block
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
elsif params.nil?
|
|
114
|
-
@structured_output
|
|
115
|
-
else
|
|
116
|
-
raise Riffer::ArgumentError, "structured_output must be a Riffer::Params" unless params.is_a?(Riffer::Params)
|
|
117
|
-
@structured_output = params
|
|
94
|
+
params = Riffer::Params.new
|
|
95
|
+
params.instance_eval(&block)
|
|
118
96
|
end
|
|
97
|
+
config.structured_output = params if params
|
|
98
|
+
config.structured_output
|
|
119
99
|
end
|
|
120
100
|
|
|
121
101
|
# Gets or sets the maximum number of LLM call steps in the tool-use loop.
|
|
122
102
|
#
|
|
123
|
-
# Defaults to DEFAULT_MAX_STEPS (16). Set to
|
|
124
|
-
# unlimited steps.
|
|
103
|
+
# Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
|
|
104
|
+
# +Float::INFINITY+ for unlimited steps.
|
|
125
105
|
#
|
|
126
106
|
#--
|
|
127
107
|
#: (?Numeric?) -> Numeric
|
|
128
108
|
def self.max_steps(value = nil)
|
|
129
|
-
|
|
130
|
-
@max_steps = value
|
|
109
|
+
value.nil? ? config.max_steps : (config.max_steps = value)
|
|
131
110
|
end
|
|
132
111
|
|
|
133
112
|
# Gets or sets the tools used by this agent.
|
|
134
113
|
#
|
|
135
114
|
#--
|
|
136
115
|
#: (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
|
|
137
|
-
def self.uses_tools(
|
|
138
|
-
|
|
139
|
-
@tools_config = tools_or_lambda
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Returns the tool classes the LLM should see for this agent.
|
|
143
|
-
#
|
|
144
|
-
# Class-level companion to the instance #resolved_tools. Resolves the
|
|
145
|
-
# Proc form of +uses_tools+ and appends the skill activation tool when
|
|
146
|
-
# a +skills+ block is configured. Does not read the skills backend —
|
|
147
|
-
# the LLM-facing tool schema reflects class-level intent, not the
|
|
148
|
-
# runtime state of any backend.
|
|
149
|
-
#
|
|
150
|
-
# When +uses_tools+ is a Proc, +context+ is forwarded to it.
|
|
151
|
-
#
|
|
152
|
-
# The activation tool class is resolved from the agent's
|
|
153
|
-
# <tt>skills do; activate_tool ...; end</tt> override when set, otherwise
|
|
154
|
-
# from <tt>Riffer.config.skills.default_activate_tool</tt>.
|
|
155
|
-
#
|
|
156
|
-
# Each returned tool class is validated via +validate_as_tool!+, so
|
|
157
|
-
# callers serializing this list to a provider can rely on every entry
|
|
158
|
-
# having the metadata required for tool use (name + description).
|
|
159
|
-
#
|
|
160
|
-
# Raises Riffer::ArgumentError on tool name conflicts with the skill
|
|
161
|
-
# activation tool, or when a tool class fails +validate_as_tool!+.
|
|
162
|
-
#
|
|
163
|
-
#--
|
|
164
|
-
#: (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
165
|
-
def self.resolved_tool_classes(context: nil)
|
|
166
|
-
base = resolve_uses_tools_config(context)
|
|
167
|
-
|
|
168
|
-
tools = if skills
|
|
169
|
-
skill_activate_tool_class = skills.activate_tool || Riffer.config.skills.default_activate_tool
|
|
170
|
-
if base.any? { |t| t.name == skill_activate_tool_class.name }
|
|
171
|
-
raise Riffer::ArgumentError, "Tool name conflict with skill tools: #{skill_activate_tool_class.name}"
|
|
172
|
-
end
|
|
173
|
-
base + [skill_activate_tool_class]
|
|
174
|
-
else
|
|
175
|
-
base
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
tools.each(&:validate_as_tool!)
|
|
179
|
-
tools
|
|
116
|
+
def self.uses_tools(value = nil)
|
|
117
|
+
value.nil? ? config.tools_config : (config.tools_config = value)
|
|
180
118
|
end
|
|
181
119
|
|
|
182
|
-
#--
|
|
183
|
-
#: (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
184
|
-
def self.resolve_uses_tools_config(context)
|
|
185
|
-
config = uses_tools
|
|
186
|
-
return [] if config.nil?
|
|
187
|
-
return config unless config.is_a?(Proc)
|
|
188
|
-
config.arity.zero? ? config.call : config.call(context)
|
|
189
|
-
end
|
|
190
|
-
private_class_method :resolve_uses_tools_config
|
|
191
|
-
|
|
192
120
|
# Opts this agent into tools from all MCP registrations that share any of
|
|
193
121
|
# the given tag(s).
|
|
194
122
|
#
|
|
@@ -196,34 +124,25 @@ class Riffer::Agent
|
|
|
196
124
|
#
|
|
197
125
|
#: (String | Symbol) -> void
|
|
198
126
|
def self.use_mcp(tag)
|
|
199
|
-
|
|
200
|
-
@mcp_configs << {tags: [tag.to_sym]}
|
|
127
|
+
config.add_mcp(tag)
|
|
201
128
|
end
|
|
202
129
|
|
|
203
130
|
# Returns the accumulated +use_mcp+ configurations for this agent class.
|
|
204
131
|
#
|
|
205
132
|
#: () -> Array[Hash[Symbol, untyped]]
|
|
206
133
|
def self.mcp_configs
|
|
207
|
-
|
|
134
|
+
config.mcp_configs
|
|
208
135
|
end
|
|
209
136
|
|
|
210
137
|
# Gets or sets the tool runtime for this agent.
|
|
211
138
|
#
|
|
212
|
-
# Accepts a Riffer::
|
|
213
|
-
# or a Proc.
|
|
214
|
-
#
|
|
215
|
-
# Inherited by subclasses. When unset, walks the ancestor chain and
|
|
216
|
-
# falls back to the global <tt>Riffer.config.tool_runtime</tt>.
|
|
139
|
+
# Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
|
|
140
|
+
# or a Proc. Defaults to <tt>Riffer.config.tool_runtime</tt> when unset.
|
|
217
141
|
#
|
|
218
142
|
#--
|
|
219
|
-
#: (?(singleton(Riffer::
|
|
143
|
+
#: (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
220
144
|
def self.tool_runtime(value = nil)
|
|
221
|
-
|
|
222
|
-
return @tool_runtime if instance_variable_defined?(:@tool_runtime)
|
|
223
|
-
superclass.respond_to?(:tool_runtime) ? superclass.tool_runtime : nil
|
|
224
|
-
else
|
|
225
|
-
@tool_runtime = value
|
|
226
|
-
end
|
|
145
|
+
value.nil? ? config.tool_runtime : (config.tool_runtime = value)
|
|
227
146
|
end
|
|
228
147
|
|
|
229
148
|
# Configures skills for this agent via a block DSL.
|
|
@@ -237,13 +156,14 @@ class Riffer::Agent
|
|
|
237
156
|
# end
|
|
238
157
|
#
|
|
239
158
|
#--
|
|
240
|
-
#: () ?{ () -> void } -> Riffer::Skills::Config?
|
|
159
|
+
#: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
|
|
241
160
|
def self.skills(&block)
|
|
242
161
|
if block
|
|
243
|
-
|
|
244
|
-
|
|
162
|
+
skills_config = Riffer::Skills::Config.new
|
|
163
|
+
skills_config.instance_eval(&block)
|
|
164
|
+
config.skills_config = skills_config
|
|
245
165
|
end
|
|
246
|
-
|
|
166
|
+
config.skills_config
|
|
247
167
|
end
|
|
248
168
|
|
|
249
169
|
# Finds an agent class by identifier.
|
|
@@ -264,22 +184,24 @@ class Riffer::Agent
|
|
|
264
184
|
|
|
265
185
|
# Generates a response using a new agent instance.
|
|
266
186
|
#
|
|
267
|
-
#
|
|
187
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
188
|
+
# to +#generate+.
|
|
268
189
|
#
|
|
269
190
|
#--
|
|
270
|
-
#: (
|
|
271
|
-
def self.generate(
|
|
272
|
-
new.generate(
|
|
191
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
|
|
192
|
+
def self.generate(prompt = nil, files: nil, context: nil)
|
|
193
|
+
new(context: context).generate(prompt, files: files)
|
|
273
194
|
end
|
|
274
195
|
|
|
275
196
|
# Streams a response using a new agent instance.
|
|
276
197
|
#
|
|
277
|
-
#
|
|
198
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
199
|
+
# to +#stream+.
|
|
278
200
|
#
|
|
279
201
|
#--
|
|
280
|
-
#: (
|
|
281
|
-
def self.stream(
|
|
282
|
-
new.stream(
|
|
202
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
203
|
+
def self.stream(prompt = nil, files: nil, context: nil)
|
|
204
|
+
new(context: context).stream(prompt, files: files)
|
|
283
205
|
end
|
|
284
206
|
|
|
285
207
|
# Registers a guardrail for input, output, or both phases.
|
|
@@ -292,22 +214,7 @@ class Riffer::Agent
|
|
|
292
214
|
#--
|
|
293
215
|
#: (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void
|
|
294
216
|
def self.guardrail(phase, with:, **options)
|
|
295
|
-
|
|
296
|
-
raise Riffer::ArgumentError, "Invalid guardrail phase: #{phase}" unless valid_phases.include?(phase)
|
|
297
|
-
raise Riffer::ArgumentError, "Guardrail must be a Riffer::Guardrail subclass" unless with.is_a?(Class) && with <= Riffer::Guardrail
|
|
298
|
-
|
|
299
|
-
@guardrails ||= {before: [], after: []}
|
|
300
|
-
config = {class: with, options: options}
|
|
301
|
-
|
|
302
|
-
case phase
|
|
303
|
-
when :before
|
|
304
|
-
@guardrails[:before] << config
|
|
305
|
-
when :after
|
|
306
|
-
@guardrails[:after] << config
|
|
307
|
-
when :around
|
|
308
|
-
@guardrails[:before] << config
|
|
309
|
-
@guardrails[:after] << config
|
|
310
|
-
end
|
|
217
|
+
config.add_guardrail(phase, klass: with, options: options)
|
|
311
218
|
end
|
|
312
219
|
|
|
313
220
|
# Returns the registered guardrail configs for a given phase.
|
|
@@ -317,127 +224,136 @@ class Riffer::Agent
|
|
|
317
224
|
#--
|
|
318
225
|
#: (Symbol) -> Array[Hash[Symbol, untyped]]
|
|
319
226
|
def self.guardrails_for(phase)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
#
|
|
328
|
-
attr_reader :
|
|
227
|
+
config.guardrails_for(phase)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# The conversation handle. See Riffer::Agent::Session.
|
|
231
|
+
attr_reader :session #: Riffer::Agent::Session
|
|
232
|
+
|
|
233
|
+
# The per-instance Riffer::Agent::Config. Either the class-level default or
|
|
234
|
+
# an explicit Config passed to +Agent.new(config:)+.
|
|
235
|
+
attr_reader :config #: Riffer::Agent::Config
|
|
236
|
+
|
|
237
|
+
# The system message built from the configured +instructions+, or +nil+
|
|
238
|
+
# when no instructions are configured. Built once at +Agent.new+ using the
|
|
239
|
+
# constructor +context:+ and cached. Useful for persistence flows.
|
|
240
|
+
attr_reader :instruction_message #: Riffer::Messages::System?
|
|
241
|
+
|
|
242
|
+
# The system message describing the configured skills catalog, or +nil+
|
|
243
|
+
# when skills are unconfigured or the catalog is empty. Built once at
|
|
244
|
+
# +Agent.new+ and cached.
|
|
245
|
+
attr_reader :skills_message #: Riffer::Messages::System?
|
|
246
|
+
|
|
247
|
+
# The mutable runtime context, a +Riffer::Agent::Context+ value object
|
|
248
|
+
# threaded into every Proc-based DSL setting, guardrail, tool runtime,
|
|
249
|
+
# and skills resolution, and shared with every +Riffer::Agent::Run+
|
|
250
|
+
# this agent executes. Exposes:
|
|
251
|
+
#
|
|
252
|
+
# - +context.skills+ — the resolved +Riffer::Skills::Context+ (when
|
|
253
|
+
# skills are configured), set at +Agent.new+ time.
|
|
254
|
+
# - +context.token_usage+ — the cumulative +Riffer::Providers::TokenUsage+,
|
|
255
|
+
# updated by each Run as the loop progresses.
|
|
256
|
+
# - +context[:key]+ / <tt>context.dig(:key)</tt> — Hash-style reads for
|
|
257
|
+
# caller-provided keys (e.g. <tt>context[:agent]</tt>,
|
|
258
|
+
# <tt>context[:tenant]</tt>). +:skills+ and +:token_usage+ are
|
|
259
|
+
# reserved and cannot be passed by the caller.
|
|
260
|
+
attr_reader :context #: Riffer::Agent::Context
|
|
261
|
+
|
|
262
|
+
# The resolved model name (the part after "provider/"), used as the model
|
|
263
|
+
# argument on every LLM call. Resolved eagerly at +Agent.new+.
|
|
264
|
+
attr_reader :model_name #: String
|
|
265
|
+
|
|
266
|
+
# The provider client. Built eagerly at +Agent.new+ from the configured
|
|
267
|
+
# provider class and +Config#provider_options+, then handed to every
|
|
268
|
+
# +Riffer::Agent::Run+ this agent executes. Public so tests can pre-queue
|
|
269
|
+
# responses on +Riffer::Providers::Mock+ before calling +#generate+.
|
|
270
|
+
attr_reader :provider #: Riffer::Providers::Base
|
|
271
|
+
|
|
272
|
+
# The +Riffer::Agent::StructuredOutput+ wrapping the configured schema, or +nil+
|
|
273
|
+
# when structured output is not configured. Resolved eagerly at +Agent.new+.
|
|
274
|
+
attr_reader :structured_output #: Riffer::Agent::StructuredOutput?
|
|
275
|
+
|
|
276
|
+
# The tool classes the LLM sees on every call this agent makes. Resolved
|
|
277
|
+
# eagerly at +Agent.new+ (Proc-form +uses_tools+ is called against
|
|
278
|
+
# +context+ once; MCP tools and the skill_activate tool are merged in).
|
|
279
|
+
attr_reader :tools #: Array[singleton(Riffer::Tool)]
|
|
280
|
+
|
|
281
|
+
# The tool runtime instance used to execute tool calls. Resolved eagerly
|
|
282
|
+
# at +Agent.new+ (Proc-form +tool_runtime+ is called against +context+ once).
|
|
283
|
+
attr_reader :tool_runtime #: Riffer::Tools::Runtime
|
|
329
284
|
|
|
330
285
|
# Initializes a new agent.
|
|
331
286
|
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
@token_usage = nil
|
|
341
|
-
@interrupted = false
|
|
342
|
-
@model_config = self.class.model
|
|
343
|
-
@instructions_config = self.class.instructions
|
|
344
|
-
|
|
345
|
-
if @model_config.is_a?(Proc)
|
|
346
|
-
@provider_name = nil
|
|
347
|
-
@model_name = nil
|
|
348
|
-
else
|
|
349
|
-
parse_model_string!(@model_config)
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Generates a response from the agent.
|
|
287
|
+
# When +session:+ is omitted, a fresh +Riffer::Agent::Session+ is built and seeded
|
|
288
|
+
# with the system instruction message and skills catalog (when configured),
|
|
289
|
+
# using +context:+. When +session:+ is provided, the agent uses it as-is —
|
|
290
|
+
# the caller is responsible for the session's contents (typical use case:
|
|
291
|
+
# cross-process resume from persisted history). With
|
|
292
|
+
# +Riffer.config.experimental_history_healing+ on, a provided session is
|
|
293
|
+
# healed at construction time so the +tool_use+ ↔ +tool_result+ invariant
|
|
294
|
+
# holds before the next inference call.
|
|
354
295
|
#
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
296
|
+
# +context:+ flows through Proc-based instructions, model, skills resolution,
|
|
297
|
+
# tool resolution, guardrails, and tool runtime. It is fixed for the
|
|
298
|
+
# lifetime of the agent.
|
|
358
299
|
#
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def generate(prompt_or_messages, files: nil, context: nil)
|
|
362
|
-
@context = context
|
|
363
|
-
prepare_run
|
|
364
|
-
@structured_output = resolve_structured_output
|
|
365
|
-
initialize_messages(prompt_or_messages, files: files)
|
|
366
|
-
|
|
367
|
-
all_modifications = [] #: Array[Riffer::Guardrails::Modification]
|
|
368
|
-
|
|
369
|
-
tripwire, modifications = run_before_guardrails
|
|
370
|
-
all_modifications.concat(modifications)
|
|
371
|
-
return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire
|
|
372
|
-
|
|
373
|
-
run_generate_loop(all_modifications)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Streams a response from the agent.
|
|
377
|
-
#
|
|
378
|
-
# Raises Riffer::ArgumentError if structured output is configured.
|
|
379
|
-
#
|
|
380
|
-
# See +#generate+ for the +experimental_history_healing+ behavior on
|
|
381
|
-
# seeded arrays.
|
|
300
|
+
# Raises Riffer::ArgumentError if the configured model string is invalid
|
|
301
|
+
# (must be "provider/model" format).
|
|
382
302
|
#
|
|
383
303
|
#--
|
|
384
|
-
#: (
|
|
385
|
-
def
|
|
386
|
-
|
|
304
|
+
#: (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void
|
|
305
|
+
def initialize(session: nil, context: nil, config: nil)
|
|
306
|
+
@config = config || self.class.config
|
|
307
|
+
@context = Riffer::Agent::Context.new(context || {})
|
|
387
308
|
|
|
388
|
-
@
|
|
389
|
-
|
|
390
|
-
initialize_messages(prompt_or_messages, files: files)
|
|
309
|
+
provider_class, @model_name = resolve_provider_and_model
|
|
310
|
+
@provider = provider_class.new(**@config.provider_options)
|
|
391
311
|
|
|
392
|
-
|
|
393
|
-
tripwire, modifications = run_before_guardrails
|
|
394
|
-
modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }
|
|
312
|
+
@context.skills = resolve_skills(provider_class)
|
|
395
313
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
end
|
|
314
|
+
@structured_output = resolve_structured_output
|
|
315
|
+
@tools = resolve_tools
|
|
316
|
+
@tool_runtime = resolve_tool_runtime
|
|
400
317
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
end
|
|
318
|
+
@instruction_message = build_instruction_message
|
|
319
|
+
@skills_message = build_skills_message
|
|
404
320
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
# Raises Riffer::ArgumentError if no block is given.
|
|
408
|
-
#
|
|
409
|
-
#--
|
|
410
|
-
#: () { (Riffer::Messages::Base) -> void } -> self
|
|
411
|
-
def on_message(&block)
|
|
412
|
-
raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
|
|
413
|
-
@message_callbacks << block
|
|
414
|
-
self
|
|
321
|
+
@session = session || Riffer::Agent::Session.new(messages: [@instruction_message, @skills_message].compact)
|
|
322
|
+
@session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
|
|
415
323
|
end
|
|
416
324
|
|
|
417
|
-
# Generates
|
|
325
|
+
# Generates a response from the agent.
|
|
418
326
|
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
327
|
+
# Runs the inference loop via +Riffer::Agent::Run.generate+. When +prompt+
|
|
328
|
+
# is given, a new +Riffer::Messages::User+ is appended to the session
|
|
329
|
+
# (silently — +on_message+ does not fire for user inputs) and then the
|
|
330
|
+
# loop runs. When +prompt+ is omitted, the loop runs against the current
|
|
331
|
+
# session — useful for resuming a persisted conversation whose last turn
|
|
332
|
+
# is already a user message, or for picking up pending tool calls after
|
|
333
|
+
# an interrupt.
|
|
421
334
|
#
|
|
422
|
-
#
|
|
335
|
+
# +files:+ requires +prompt+. Pass files to attach to the new user message.
|
|
423
336
|
#
|
|
424
337
|
#--
|
|
425
|
-
#: (?
|
|
426
|
-
def
|
|
427
|
-
|
|
338
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
|
|
339
|
+
def generate(prompt = nil, files: nil)
|
|
340
|
+
Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files)
|
|
428
341
|
end
|
|
429
342
|
|
|
430
|
-
#
|
|
343
|
+
# Streams a response from the agent.
|
|
344
|
+
#
|
|
345
|
+
# Runs the inference loop via +Riffer::Agent::Run.stream+, returning an
|
|
346
|
+
# +Enumerator+ of +Riffer::StreamEvents+.
|
|
431
347
|
#
|
|
432
|
-
#
|
|
433
|
-
# need to be stored independently.
|
|
348
|
+
# Raises Riffer::ArgumentError if structured output is configured.
|
|
434
349
|
#
|
|
435
|
-
#
|
|
350
|
+
# See +#generate+ for prompt/files semantics.
|
|
436
351
|
#
|
|
437
352
|
#--
|
|
438
|
-
#: (?
|
|
439
|
-
def
|
|
440
|
-
|
|
353
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
354
|
+
def stream(prompt = nil, files: nil)
|
|
355
|
+
raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if @structured_output
|
|
356
|
+
Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files)
|
|
441
357
|
end
|
|
442
358
|
|
|
443
359
|
# Interrupts the agent loop.
|
|
@@ -458,599 +374,135 @@ class Riffer::Agent
|
|
|
458
374
|
throw :riffer_interrupt, reason
|
|
459
375
|
end
|
|
460
376
|
|
|
461
|
-
|
|
462
|
-
#
|
|
463
|
-
#--
|
|
464
|
-
#: (String) -> Riffer::Messages::Base?
|
|
465
|
-
def message_by_id(id)
|
|
466
|
-
@messages.find { |m| m.id == id }
|
|
467
|
-
end
|
|
377
|
+
private
|
|
468
378
|
|
|
469
|
-
# Returns the +Riffer::Messages::Tool+ message that satisfies +tool_call_id+,
|
|
470
|
-
# or +nil+ when no such tool result exists in history.
|
|
471
|
-
#
|
|
472
379
|
#--
|
|
473
|
-
#: (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
380
|
+
#: () -> Riffer::Messages::System?
|
|
381
|
+
def build_instruction_message
|
|
382
|
+
content = Riffer::Helpers::CallOrValue.resolve(@config.instructions, context: @context)
|
|
383
|
+
return nil if content.nil? || content.empty?
|
|
384
|
+
Riffer::Messages::System.new(content)
|
|
478
385
|
end
|
|
479
|
-
# rubocop:enable Style/ReverseFind
|
|
480
386
|
|
|
481
|
-
# Returns the most recent +Riffer::Messages::Assistant+ in history, or
|
|
482
|
-
# +nil+ when no assistant message has been recorded yet.
|
|
483
|
-
#
|
|
484
387
|
#--
|
|
485
|
-
#: () -> Riffer::Messages::
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
388
|
+
#: () -> Riffer::Messages::System?
|
|
389
|
+
def build_skills_message
|
|
390
|
+
skills = @context.skills
|
|
391
|
+
return nil unless skills&.system_prompt
|
|
392
|
+
Riffer::Messages::System.new(skills.system_prompt)
|
|
490
393
|
end
|
|
491
|
-
# rubocop:enable Style/ReverseFind
|
|
492
394
|
|
|
493
|
-
#
|
|
494
|
-
#
|
|
395
|
+
# Resolves +Config#model+ to a "provider/model" string (calling the Proc
|
|
396
|
+
# form against +@context+), parses it, and looks up the provider class.
|
|
495
397
|
#
|
|
496
|
-
#
|
|
497
|
-
#
|
|
398
|
+
# Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
|
|
399
|
+
# an invalid model string or an unregistered provider.
|
|
498
400
|
#
|
|
499
401
|
#--
|
|
500
|
-
#: () ->
|
|
501
|
-
def
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
next [] unless m.is_a?(Riffer::Messages::Assistant)
|
|
505
|
-
m.tool_calls.reject { |tc| result_ids.include?(tc.call_id) }.map(&:call_id)
|
|
506
|
-
}
|
|
507
|
-
end
|
|
402
|
+
#: () -> [singleton(Riffer::Providers::Base), String]
|
|
403
|
+
def resolve_provider_and_model
|
|
404
|
+
model_string = Riffer::Helpers::CallOrValue.resolve(@config.model, context: @context)
|
|
405
|
+
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless model_string.is_a?(String)
|
|
508
406
|
|
|
509
|
-
|
|
510
|
-
# +tool_calls+, +token_usage+, and +structured_output+. Lookup is by id.
|
|
511
|
-
#
|
|
512
|
-
# When +content+ is empty, delegates to +remove_message+ — including the
|
|
513
|
-
# cascade that drops dependent +Riffer::Messages::Tool+ children.
|
|
514
|
-
#
|
|
515
|
-
# Raises Riffer::ArgumentError when no assistant message has the given id.
|
|
516
|
-
#
|
|
517
|
-
#--
|
|
518
|
-
#: (id: String, content: String) -> Riffer::Messages::Base?
|
|
519
|
-
def replace_assistant_content(id:, content:)
|
|
520
|
-
return remove_message(id: id) if content.empty?
|
|
521
|
-
|
|
522
|
-
idx = @messages.index { |m| m.is_a?(Riffer::Messages::Assistant) && m.id == id }
|
|
523
|
-
raise Riffer::ArgumentError, "no assistant message with id #{id.inspect}" unless idx
|
|
524
|
-
|
|
525
|
-
old = @messages[idx] #: Riffer::Messages::Assistant
|
|
526
|
-
replacement = Riffer::Messages::Assistant.new(
|
|
527
|
-
content,
|
|
528
|
-
id: old.id,
|
|
529
|
-
tool_calls: old.tool_calls,
|
|
530
|
-
token_usage: old.token_usage,
|
|
531
|
-
structured_output: old.structured_output
|
|
532
|
-
)
|
|
533
|
-
@messages[idx] = replacement
|
|
534
|
-
replacement
|
|
535
|
-
end
|
|
407
|
+
provider_name, model_name = model_string.split("/", 2)
|
|
536
408
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
# +tool_call_id+ matches one of those calls is removed atomically — keeping
|
|
540
|
-
# the +tool_use+ ↔ +tool_result+ invariant intact.
|
|
541
|
-
#
|
|
542
|
-
# Raises Riffer::ArgumentError when called on a +Riffer::Messages::Tool+
|
|
543
|
-
# message — that would orphan the parent's +tool_use+. Use
|
|
544
|
-
# +replace_tool_result+ instead.
|
|
545
|
-
#
|
|
546
|
-
# Returns the removed message, or +nil+ when no message has the given id
|
|
547
|
-
# (idempotent).
|
|
548
|
-
#
|
|
549
|
-
#--
|
|
550
|
-
#: (id: String) -> Riffer::Messages::Base?
|
|
551
|
-
def remove_message(id:)
|
|
552
|
-
idx = @messages.index { |m| m.id == id }
|
|
553
|
-
return nil unless idx
|
|
554
|
-
|
|
555
|
-
target = @messages[idx]
|
|
556
|
-
if target.is_a?(Riffer::Messages::Tool)
|
|
557
|
-
raise Riffer::ArgumentError,
|
|
558
|
-
"remove_message cannot drop a Tool message (would orphan the parent's tool_use); use replace_tool_result instead"
|
|
409
|
+
unless provider_name.is_a?(String) && !provider_name.strip.empty? && model_name.is_a?(String) && !model_name.strip.empty?
|
|
410
|
+
raise Riffer::ArgumentError, "Invalid model string: #{model_string}"
|
|
559
411
|
end
|
|
560
412
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
@messages.reject! { |m| m.is_a?(Riffer::Messages::Tool) && child_ids.include?(m.tool_call_id) }
|
|
564
|
-
@messages.delete(target)
|
|
565
|
-
else
|
|
566
|
-
@messages.delete_at(idx)
|
|
567
|
-
end
|
|
568
|
-
target
|
|
569
|
-
end
|
|
413
|
+
provider_class = Riffer::Providers::Repository.find(provider_name)
|
|
414
|
+
raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
|
|
570
415
|
|
|
571
|
-
|
|
572
|
-
# Lookup is by +tool_call_id+. Preserves the existing message's +name+ and
|
|
573
|
-
# +id+.
|
|
574
|
-
#
|
|
575
|
-
# Raises Riffer::ArgumentError when no Tool message exists for the given
|
|
576
|
-
# +tool_call_id+.
|
|
577
|
-
#
|
|
578
|
-
#--
|
|
579
|
-
#: (tool_call_id: String, content: String, ?error: String?, ?error_type: Symbol?) -> Riffer::Messages::Tool
|
|
580
|
-
def replace_tool_result(tool_call_id:, content:, error: nil, error_type: nil)
|
|
581
|
-
idx = @messages.index { |m| m.is_a?(Riffer::Messages::Tool) && m.tool_call_id == tool_call_id }
|
|
582
|
-
raise Riffer::ArgumentError, "no tool result for tool_call_id #{tool_call_id.inspect}" unless idx
|
|
583
|
-
|
|
584
|
-
old = @messages[idx] #: Riffer::Messages::Tool
|
|
585
|
-
replacement = Riffer::Messages::Tool.new(
|
|
586
|
-
content,
|
|
587
|
-
id: old.id,
|
|
588
|
-
tool_call_id: old.tool_call_id,
|
|
589
|
-
name: old.name,
|
|
590
|
-
error: error,
|
|
591
|
-
error_type: error_type
|
|
592
|
-
)
|
|
593
|
-
@messages[idx] = replacement
|
|
594
|
-
replacement
|
|
416
|
+
[provider_class, model_name]
|
|
595
417
|
end
|
|
596
418
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
# Fills any orphaned +tool_use+ in history with the +HEALING_PLACEHOLDER+
|
|
600
|
-
# response. Each placeholder Tool message is inserted immediately after
|
|
601
|
-
# its parent assistant message. Returns the array of call_ids that were
|
|
602
|
-
# filled, in order; +[]+ when there are no orphans.
|
|
603
|
-
#
|
|
604
|
-
# Bypasses +on_message+ — placeholders are not inference output.
|
|
605
|
-
# Consumers learn that healing happened via the structured
|
|
606
|
-
# +Riffer::Agent::Response#healed_tool_call_ids+ field (and the streaming
|
|
607
|
-
# +Interrupt+ event's matching field).
|
|
419
|
+
# Resolves the skills backend, lists skills, and selects an adapter.
|
|
420
|
+
# Returns nil if skills are unconfigured or the backend is empty.
|
|
608
421
|
#
|
|
609
422
|
#--
|
|
610
|
-
#: () ->
|
|
611
|
-
def
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
new_messages = [] #: Array[Riffer::Messages::Base]
|
|
615
|
-
|
|
616
|
-
@messages.each do |m|
|
|
617
|
-
new_messages << m
|
|
618
|
-
next unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
|
|
619
|
-
|
|
620
|
-
m.tool_calls.each do |tc|
|
|
621
|
-
next if result_ids.include?(tc.call_id)
|
|
622
|
-
|
|
623
|
-
response = HEALING_PLACEHOLDER.call(tc)
|
|
624
|
-
new_messages << Riffer::Messages::Tool.new(
|
|
625
|
-
response.content,
|
|
626
|
-
tool_call_id: tc.call_id,
|
|
627
|
-
name: tc.name,
|
|
628
|
-
error: response.error_message,
|
|
629
|
-
error_type: response.error_type
|
|
630
|
-
)
|
|
631
|
-
healed << tc.call_id
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
@messages = new_messages
|
|
636
|
-
healed
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
#--
|
|
640
|
-
#: (?Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
|
|
641
|
-
def run_generate_loop(all_modifications = [])
|
|
642
|
-
step = count_assistant_messages
|
|
643
|
-
|
|
644
|
-
reason = catch(:riffer_interrupt) do
|
|
645
|
-
execute_pending_tool_calls
|
|
646
|
-
|
|
647
|
-
loop do
|
|
648
|
-
response = call_llm
|
|
649
|
-
step += 1
|
|
423
|
+
#: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
|
|
424
|
+
def resolve_skills(provider_class)
|
|
425
|
+
skills_config = @config.skills_config
|
|
426
|
+
return nil unless skills_config
|
|
650
427
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
processed_response, tripwire, modifications = run_after_guardrails(response)
|
|
654
|
-
all_modifications.concat(modifications)
|
|
655
|
-
|
|
656
|
-
return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire
|
|
657
|
-
|
|
658
|
-
add_message(processed_response)
|
|
428
|
+
backend = skills_config.backend || Riffer.config.skills.default_backend
|
|
429
|
+
return nil unless backend
|
|
659
430
|
|
|
660
|
-
|
|
431
|
+
backend = Riffer::Helpers::CallOrValue.resolve(backend, context: @context)
|
|
432
|
+
return nil if backend.list_skills.empty?
|
|
661
433
|
|
|
662
|
-
|
|
434
|
+
skills = backend.list_skills.to_h { |s| [s.name, s] }
|
|
435
|
+
adapter_class = skills_config.adapter || provider_class.skills_adapter(@model_name)
|
|
436
|
+
skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
|
|
663
437
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
438
|
+
skills_context = Riffer::Skills::Context.new(
|
|
439
|
+
backend: backend,
|
|
440
|
+
skills: skills,
|
|
441
|
+
adapter: adapter_class.new(skill_activate_tool: skill_activate_tool_class)
|
|
442
|
+
)
|
|
668
443
|
|
|
669
|
-
|
|
444
|
+
if skills_config.activate
|
|
445
|
+
names = Array(Riffer::Helpers::CallOrValue.resolve(skills_config.activate, context: @context))
|
|
446
|
+
names.each { |name| skills_context.activate(name) }
|
|
670
447
|
end
|
|
671
448
|
|
|
672
|
-
|
|
673
|
-
# the return above exits on the successful (non-interrupted) path.
|
|
674
|
-
@interrupted = true
|
|
675
|
-
healed = Riffer.config.experimental_history_healing ? heal_orphan_tool_calls : []
|
|
676
|
-
response = extract_final_response
|
|
677
|
-
|
|
678
|
-
build_response(response&.content || "", modifications: all_modifications, interrupted: true, interrupt_reason: reason, structured_output: validate_structured_output(response), healed_tool_call_ids: healed)
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
#--
|
|
682
|
-
#: (Riffer::Messages::Base) -> void
|
|
683
|
-
def add_message(message)
|
|
684
|
-
@messages << message
|
|
685
|
-
@message_callbacks.each { |callback| callback.call(message) }
|
|
686
|
-
end
|
|
687
|
-
|
|
688
|
-
#--
|
|
689
|
-
#: (Riffer::TokenUsage?) -> void
|
|
690
|
-
def track_token_usage(usage)
|
|
691
|
-
return unless usage
|
|
692
|
-
|
|
693
|
-
@token_usage = @token_usage ? @token_usage + usage : usage
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
#--
|
|
697
|
-
#: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
|
|
698
|
-
def initialize_messages(prompt_or_messages, files: nil)
|
|
699
|
-
if prompt_or_messages.is_a?(Array)
|
|
700
|
-
raise Riffer::ArgumentError, "cannot pass an array of messages on an agent with existing messages; use a string to continue the conversation or a new agent instance to start fresh" if @messages.any?
|
|
701
|
-
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead" if files && !files.empty?
|
|
702
|
-
validate_seed_ids!(prompt_or_messages)
|
|
703
|
-
converted = prompt_or_messages.map { |item| convert_to_message_object(item) }
|
|
704
|
-
@messages = Riffer.config.experimental_history_healing ? heal_seeded_history(converted) : converted
|
|
705
|
-
elsif @messages.any?
|
|
706
|
-
file_parts = (files || []).map { |f| convert_to_file_part(f) }
|
|
707
|
-
@messages << Riffer::Messages::User.new(prompt_or_messages, files: file_parts)
|
|
708
|
-
else
|
|
709
|
-
@messages = []
|
|
710
|
-
sys = build_instruction_message
|
|
711
|
-
@messages << sys if sys
|
|
712
|
-
skills = build_skills_message
|
|
713
|
-
@messages << skills if skills
|
|
714
|
-
file_parts = (files || []).map { |f| convert_to_file_part(f) }
|
|
715
|
-
@messages << Riffer::Messages::User.new(prompt_or_messages, files: file_parts)
|
|
716
|
-
end
|
|
449
|
+
skills_context
|
|
717
450
|
end
|
|
718
451
|
|
|
719
452
|
#--
|
|
720
|
-
#: (
|
|
721
|
-
def
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
items.each_with_index do |item, idx|
|
|
726
|
-
raw_id = case item
|
|
727
|
-
when Hash then item[:id]
|
|
728
|
-
when Riffer::Messages::Base then item.id
|
|
729
|
-
else next # type errors surface later via convert_to_message_object
|
|
730
|
-
end
|
|
731
|
-
next unless raw_id.nil?
|
|
732
|
-
raise Riffer::ArgumentError,
|
|
733
|
-
"seeded message at index #{idx} is missing :id (required when Riffer.config.message_id_strategy = #{strategy.inspect})"
|
|
734
|
-
end
|
|
453
|
+
#: () -> Riffer::Agent::StructuredOutput?
|
|
454
|
+
def resolve_structured_output
|
|
455
|
+
params = @config.structured_output
|
|
456
|
+
params ? Riffer::Agent::StructuredOutput.new(params) : nil
|
|
735
457
|
end
|
|
736
458
|
|
|
737
|
-
#
|
|
738
|
-
# invariant holds. Drops orphaned tool exchanges (assistant +tool_call+
|
|
739
|
-
# with no matching Tool result) and parentless Tool messages. Returns a
|
|
740
|
-
# new array; the input is not mutated.
|
|
459
|
+
# Resolves the full tool catalog for the agent:
|
|
741
460
|
#
|
|
742
|
-
#
|
|
743
|
-
#
|
|
744
|
-
#
|
|
745
|
-
#
|
|
461
|
+
# - The configured +uses_tools+ value (Proc-form resolved against +context+).
|
|
462
|
+
# - The skill activation tool, when a +skills+ block is configured. The
|
|
463
|
+
# activation tool class comes from the per-agent +skills do; activate_tool ...; end+
|
|
464
|
+
# override when set, otherwise from +Riffer.config.skills.default_activate_tool+.
|
|
465
|
+
# - All MCP tools matching any +use_mcp+ tag, optionally wrapped in
|
|
466
|
+
# AuthenticatedTool when +Riffer.config.mcp.credentials+ is configured.
|
|
746
467
|
#
|
|
747
|
-
#
|
|
468
|
+
# Raises Riffer::ArgumentError on tool name conflicts with the skill
|
|
469
|
+
# activation tool, on duplicate tool names across sources, or on tool
|
|
470
|
+
# classes missing required metadata (description, params).
|
|
748
471
|
#
|
|
749
472
|
#--
|
|
750
|
-
#: (
|
|
751
|
-
def
|
|
752
|
-
|
|
753
|
-
m = messages[idx]
|
|
754
|
-
m.is_a?(Riffer::Messages::Assistant) &&
|
|
755
|
-
messages[(idx + 1)..].all? { |later| later.is_a?(Riffer::Messages::Tool) }
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
|
|
759
|
-
parent_ids = messages.flat_map { |m|
|
|
760
|
-
m.is_a?(Riffer::Messages::Assistant) ? m.tool_calls.map(&:call_id) : []
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
strip_offenders = messages.each_with_index.flat_map { |m, idx|
|
|
764
|
-
next [] unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
|
|
765
|
-
next [] if idx == resume_boundary # preserve pending exchange
|
|
766
|
-
next [] if m.tool_calls.all? { |tc| result_ids.include?(tc.call_id) }
|
|
767
|
-
m.tool_calls.map(&:call_id)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
messages.reject { |m|
|
|
771
|
-
case m
|
|
772
|
-
when Riffer::Messages::Assistant
|
|
773
|
-
!m.tool_calls.empty? && m.tool_calls.any? { |tc| strip_offenders.include?(tc.call_id) }
|
|
774
|
-
when Riffer::Messages::Tool
|
|
775
|
-
strip_offenders.include?(m.tool_call_id) || !parent_ids.include?(m.tool_call_id)
|
|
776
|
-
else
|
|
777
|
-
false
|
|
778
|
-
end
|
|
779
|
-
}
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
#--
|
|
783
|
-
#: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
|
|
784
|
-
def build_instruction_message(context = @context)
|
|
785
|
-
content = generate_instructions(context)
|
|
786
|
-
return nil if content.nil? || content.empty?
|
|
787
|
-
Riffer::Messages::System.new(content)
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
#--
|
|
791
|
-
#: (?Riffer::Skills::Context?) -> Riffer::Messages::System?
|
|
792
|
-
def build_skills_message(skills_state = @skills_state)
|
|
793
|
-
content = skills_state&.system_prompt
|
|
794
|
-
return nil if content.nil? || content.empty?
|
|
795
|
-
Riffer::Messages::System.new(content)
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
#--
|
|
799
|
-
#: () -> Integer
|
|
800
|
-
def count_assistant_messages
|
|
801
|
-
@messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
802
|
-
end
|
|
473
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
474
|
+
def resolve_tools
|
|
475
|
+
tools = Riffer::Helpers::CallOrValue.resolve(@config.tools_config, context: @context, default: [])
|
|
803
476
|
|
|
804
|
-
|
|
805
|
-
#: (Enumerator::Yielder) -> void
|
|
806
|
-
def run_stream_loop(yielder)
|
|
807
|
-
step = count_assistant_messages
|
|
477
|
+
skills_config = @config.skills_config
|
|
808
478
|
|
|
809
|
-
if
|
|
810
|
-
|
|
811
|
-
end
|
|
479
|
+
if skills_config
|
|
480
|
+
skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
|
|
812
481
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
loop do
|
|
817
|
-
accumulated_content = ""
|
|
818
|
-
accumulated_tool_calls = []
|
|
819
|
-
accumulated_token_usage = nil
|
|
820
|
-
current_tool_call = nil
|
|
821
|
-
|
|
822
|
-
call_llm_stream.each do |event|
|
|
823
|
-
yielder << event
|
|
824
|
-
|
|
825
|
-
case event
|
|
826
|
-
when Riffer::StreamEvents::TextDelta
|
|
827
|
-
accumulated_content += event.content
|
|
828
|
-
when Riffer::StreamEvents::TextDone
|
|
829
|
-
accumulated_content = event.content
|
|
830
|
-
when Riffer::StreamEvents::ToolCallDelta
|
|
831
|
-
current_tool_call ||= {item_id: event.item_id, name: event.name, arguments: ""}
|
|
832
|
-
current_tool_call[:arguments] += event.arguments_delta
|
|
833
|
-
current_tool_call[:name] ||= event.name
|
|
834
|
-
when Riffer::StreamEvents::ToolCallDone
|
|
835
|
-
accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
836
|
-
call_id: event.call_id,
|
|
837
|
-
name: event.name,
|
|
838
|
-
arguments: event.arguments
|
|
839
|
-
)
|
|
840
|
-
current_tool_call = nil
|
|
841
|
-
when Riffer::StreamEvents::TokenUsageDone
|
|
842
|
-
accumulated_token_usage = event.token_usage
|
|
843
|
-
end
|
|
844
|
-
end
|
|
845
|
-
|
|
846
|
-
response = Riffer::Messages::Assistant.new(
|
|
847
|
-
accumulated_content,
|
|
848
|
-
tool_calls: accumulated_tool_calls,
|
|
849
|
-
token_usage: accumulated_token_usage
|
|
850
|
-
)
|
|
851
|
-
|
|
852
|
-
track_token_usage(accumulated_token_usage)
|
|
853
|
-
step += 1
|
|
854
|
-
|
|
855
|
-
processed_response, tripwire, modifications = run_after_guardrails(response)
|
|
856
|
-
modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }
|
|
857
|
-
|
|
858
|
-
if tripwire
|
|
859
|
-
yielder << Riffer::StreamEvents::GuardrailTripwire.new(tripwire)
|
|
860
|
-
break
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
add_message(processed_response)
|
|
864
|
-
|
|
865
|
-
break unless has_tool_calls?(processed_response)
|
|
866
|
-
|
|
867
|
-
throw :riffer_interrupt, INTERRUPT_MAX_STEPS if step >= self.class.max_steps
|
|
868
|
-
|
|
869
|
-
execute_tool_calls(processed_response)
|
|
482
|
+
if tools.any? { |t| t.name == skill_activate_tool_class.name }
|
|
483
|
+
raise Riffer::ArgumentError, "Tool name conflict with skill tools: #{skill_activate_tool_class.name}"
|
|
870
484
|
end
|
|
871
|
-
:completed
|
|
872
|
-
end
|
|
873
485
|
|
|
874
|
-
|
|
875
|
-
@interrupted = true
|
|
876
|
-
healed = Riffer.config.experimental_history_healing ? heal_orphan_tool_calls : []
|
|
877
|
-
yielder << Riffer::StreamEvents::Interrupt.new(reason: completed, healed_tool_call_ids: healed)
|
|
486
|
+
tools += [skill_activate_tool_class]
|
|
878
487
|
end
|
|
879
|
-
end
|
|
880
488
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
messages: @messages,
|
|
886
|
-
model: @model_name,
|
|
887
|
-
tools: resolved_tools,
|
|
888
|
-
**merged_model_options
|
|
889
|
-
)
|
|
890
|
-
end
|
|
891
|
-
|
|
892
|
-
#--
|
|
893
|
-
#: () -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
894
|
-
def call_llm_stream
|
|
895
|
-
provider_instance.stream_text(
|
|
896
|
-
messages: @messages,
|
|
897
|
-
model: @model_name,
|
|
898
|
-
tools: resolved_tools,
|
|
899
|
-
**merged_model_options
|
|
900
|
-
)
|
|
901
|
-
end
|
|
902
|
-
|
|
903
|
-
#--
|
|
904
|
-
#: () -> Riffer::Providers::Base
|
|
905
|
-
def provider_instance
|
|
906
|
-
@provider_instance ||= provider_class.new(**self.class.provider_options)
|
|
907
|
-
end
|
|
908
|
-
|
|
909
|
-
#--
|
|
910
|
-
#: (Riffer::Messages::Assistant) -> bool
|
|
911
|
-
def has_tool_calls?(response)
|
|
912
|
-
response.is_a?(Riffer::Messages::Assistant) && !response.tool_calls.empty?
|
|
913
|
-
end
|
|
914
|
-
|
|
915
|
-
#--
|
|
916
|
-
#: (Riffer::Messages::Assistant) -> void
|
|
917
|
-
def execute_tool_calls(response)
|
|
918
|
-
runtime = resolve_tool_runtime
|
|
919
|
-
results = runtime.execute(response.tool_calls, tools: resolved_tools, context: @context, assistant_message: response)
|
|
920
|
-
|
|
921
|
-
results.each do |tool_call, result|
|
|
922
|
-
add_message(Riffer::Messages::Tool.new(
|
|
923
|
-
result.content,
|
|
924
|
-
tool_call_id: tool_call.call_id,
|
|
925
|
-
name: tool_call.name,
|
|
926
|
-
error: result.error_message,
|
|
927
|
-
error_type: result.error_type
|
|
928
|
-
))
|
|
929
|
-
end
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
# Executes tool calls left unfinished by a prior interrupt.
|
|
933
|
-
#
|
|
934
|
-
# When an interrupt fires mid-way through tool execution, some tool calls
|
|
935
|
-
# from the last assistant message may not have been executed yet. This
|
|
936
|
-
# method detects those gaps by comparing the tool call ids requested by the
|
|
937
|
-
# last assistant message against the tool result messages that follow it,
|
|
938
|
-
# then executes any that are missing.
|
|
939
|
-
#
|
|
940
|
-
#--
|
|
941
|
-
#: () -> void
|
|
942
|
-
# Executes tool calls from the last assistant message that don't yet
|
|
943
|
-
# have a corresponding tool result. Safe to call unconditionally —
|
|
944
|
-
# returns immediately when there is nothing pending.
|
|
945
|
-
def execute_pending_tool_calls
|
|
946
|
-
assistant, pending = pending_tool_calls
|
|
947
|
-
return if pending.empty?
|
|
948
|
-
|
|
949
|
-
runtime = resolve_tool_runtime
|
|
950
|
-
results = runtime.execute(pending, tools: resolved_tools, context: @context, assistant_message: assistant)
|
|
951
|
-
|
|
952
|
-
results.each do |tool_call, result|
|
|
953
|
-
add_message(Riffer::Messages::Tool.new(
|
|
954
|
-
result.content,
|
|
955
|
-
tool_call_id: tool_call.call_id,
|
|
956
|
-
name: tool_call.name,
|
|
957
|
-
error: result.error_message,
|
|
958
|
-
error_type: result.error_type
|
|
959
|
-
))
|
|
960
|
-
end
|
|
961
|
-
end
|
|
962
|
-
|
|
963
|
-
# Returns +[assistant, pending_tool_calls]+ for the last assistant message.
|
|
964
|
-
# When there is no assistant message or no pending calls, the second
|
|
965
|
-
# element is an empty array.
|
|
966
|
-
#
|
|
967
|
-
#--
|
|
968
|
-
#: () -> [untyped, Array[Riffer::Messages::Assistant::ToolCall]]
|
|
969
|
-
def pending_tool_calls
|
|
970
|
-
last_assistant_idx = @messages.rindex { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
971
|
-
return [nil, []] unless last_assistant_idx
|
|
972
|
-
|
|
973
|
-
assistant = @messages[last_assistant_idx]
|
|
974
|
-
return [assistant, []] if assistant.tool_calls.empty?
|
|
975
|
-
|
|
976
|
-
executed_ids = @messages[(last_assistant_idx + 1)..].select { |m|
|
|
977
|
-
m.is_a?(Riffer::Messages::Tool)
|
|
978
|
-
}.map(&:tool_call_id)
|
|
979
|
-
|
|
980
|
-
[assistant, assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }]
|
|
981
|
-
end
|
|
982
|
-
|
|
983
|
-
#--
|
|
984
|
-
#: () -> void
|
|
985
|
-
def prepare_run
|
|
986
|
-
@resolved_tools = nil
|
|
987
|
-
@resolved_tool_runtime = nil
|
|
988
|
-
clear_resolved_model
|
|
989
|
-
@interrupted = false
|
|
990
|
-
resolve_model
|
|
991
|
-
@skills_state = resolve_skills
|
|
992
|
-
@context = (@context || {}).merge(skills: @skills_state) if @skills_state
|
|
993
|
-
end
|
|
994
|
-
|
|
995
|
-
#--
|
|
996
|
-
#: (untyped) -> void
|
|
997
|
-
def parse_model_string!(model_string)
|
|
998
|
-
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless model_string.is_a?(String)
|
|
999
|
-
provider_name, model_name = model_string.split("/", 2)
|
|
1000
|
-
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
|
|
1001
|
-
@provider_name = provider_name
|
|
1002
|
-
@model_name = model_name
|
|
1003
|
-
end
|
|
1004
|
-
|
|
1005
|
-
#--
|
|
1006
|
-
#: () -> void
|
|
1007
|
-
def clear_resolved_model
|
|
1008
|
-
@resolved_model = nil
|
|
1009
|
-
@provider_instance = nil if @model_config.is_a?(Proc)
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
#--
|
|
1013
|
-
#: (?Hash[Symbol, untyped]?) -> String?
|
|
1014
|
-
def generate_instructions(context = @context)
|
|
1015
|
-
if @instructions_config.is_a?(Proc)
|
|
1016
|
-
(@instructions_config.arity == 0) ? @instructions_config.call : @instructions_config.call(context)
|
|
1017
|
-
else
|
|
1018
|
-
@instructions_config
|
|
1019
|
-
end
|
|
1020
|
-
end
|
|
1021
|
-
|
|
1022
|
-
attr_reader :resolved_model #: String?
|
|
1023
|
-
|
|
1024
|
-
#--
|
|
1025
|
-
#: () -> String
|
|
1026
|
-
def resolve_model
|
|
1027
|
-
@resolved_model ||= if @model_config.is_a?(Proc)
|
|
1028
|
-
model_string = (@model_config.arity == 0) ? @model_config.call : @model_config.call(@context)
|
|
1029
|
-
parse_model_string!(model_string)
|
|
1030
|
-
model_string
|
|
1031
|
-
else
|
|
1032
|
-
@model_config
|
|
1033
|
-
end
|
|
489
|
+
tools += resolve_mcp_tool_classes
|
|
490
|
+
assert_distinct_tool_names!(tools)
|
|
491
|
+
tools.each(&:validate_as_tool!)
|
|
492
|
+
tools
|
|
1034
493
|
end
|
|
1035
494
|
|
|
1036
495
|
#--
|
|
1037
|
-
#: () ->
|
|
1038
|
-
def
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if config.nil?
|
|
1042
|
-
[]
|
|
1043
|
-
elsif config.is_a?(Proc)
|
|
1044
|
-
(config.arity == 0) ? config.call : config.call(@context)
|
|
1045
|
-
else
|
|
1046
|
-
config
|
|
1047
|
-
end
|
|
496
|
+
#: () -> Riffer::Tools::Runtime
|
|
497
|
+
def resolve_tool_runtime
|
|
498
|
+
runtime = Riffer::Helpers::CallOrValue.resolve(@config.tool_runtime, context: @context)
|
|
499
|
+
runtime.is_a?(Class) ? runtime.new : runtime
|
|
1048
500
|
end
|
|
1049
501
|
|
|
1050
502
|
#--
|
|
1051
503
|
#: () -> Array[singleton(Riffer::Tool)]
|
|
1052
504
|
def resolve_mcp_tool_classes
|
|
1053
|
-
configs =
|
|
505
|
+
configs = @config.mcp_configs
|
|
1054
506
|
return [] if configs.empty?
|
|
1055
507
|
|
|
1056
508
|
cred = Riffer.config.mcp.credentials
|
|
@@ -1065,7 +517,7 @@ class Riffer::Agent
|
|
|
1065
517
|
#
|
|
1066
518
|
#: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
1067
519
|
def gather_mcp_registrations_with_tags(configs)
|
|
1068
|
-
by_reg = {}
|
|
520
|
+
by_reg = {} #: Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
1069
521
|
configs.each do |cfg|
|
|
1070
522
|
Riffer::Mcp::Registry.find_by_tags(cfg[:tags]).each do |reg|
|
|
1071
523
|
(by_reg[reg] ||= []).concat(cfg[:tags] & reg.manifest.tags)
|
|
@@ -1074,7 +526,7 @@ class Riffer::Agent
|
|
|
1074
526
|
by_reg
|
|
1075
527
|
end
|
|
1076
528
|
|
|
1077
|
-
#: (Riffer::Mcp::Registration, Array[Symbol],
|
|
529
|
+
#: (Riffer::Mcp::Registration, Array[Symbol], (^(manifest: Riffer::Mcp::Manifest, matched_tags: Array[Symbol], context: Riffer::Agent::Context) -> Hash[Symbol, untyped]?)?, Riffer::Agent::Context) -> Array[singleton(Riffer::Tool)]
|
|
1078
530
|
def mcp_tools_for_registration(reg, matched_tags, cred, ctx)
|
|
1079
531
|
return reg.tools unless cred
|
|
1080
532
|
return [] if cred.call(manifest: reg.manifest, matched_tags: matched_tags, context: ctx).nil?
|
|
@@ -1092,145 +544,4 @@ class Riffer::Agent
|
|
|
1092
544
|
|
|
1093
545
|
raise Riffer::ArgumentError, "Duplicate tool names: #{dupes.sort.join(", ")}"
|
|
1094
546
|
end
|
|
1095
|
-
|
|
1096
|
-
#: () -> Array[singleton(Riffer::Tool)]
|
|
1097
|
-
def resolved_tools
|
|
1098
|
-
@resolved_tools ||= begin
|
|
1099
|
-
tools = self.class.resolved_tool_classes(context: @context) + resolve_mcp_tool_classes
|
|
1100
|
-
assert_distinct_tool_names!(tools)
|
|
1101
|
-
tools.each(&:validate_as_tool!)
|
|
1102
|
-
tools
|
|
1103
|
-
end
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
#--
|
|
1107
|
-
#: () -> Riffer::ToolRuntime
|
|
1108
|
-
def resolve_tool_runtime
|
|
1109
|
-
@resolved_tool_runtime ||= begin
|
|
1110
|
-
config = self.class.tool_runtime || Riffer.config.tool_runtime
|
|
1111
|
-
|
|
1112
|
-
runtime = if config.is_a?(Proc)
|
|
1113
|
-
(config.arity == 0) ? config.call : config.call(@context)
|
|
1114
|
-
else
|
|
1115
|
-
config
|
|
1116
|
-
end
|
|
1117
|
-
|
|
1118
|
-
case runtime
|
|
1119
|
-
when Class then runtime.new
|
|
1120
|
-
when Riffer::ToolRuntime then runtime
|
|
1121
|
-
else raise Riffer::ArgumentError, "Invalid tool_runtime: #{runtime.inspect}"
|
|
1122
|
-
end
|
|
1123
|
-
end
|
|
1124
|
-
end
|
|
1125
|
-
|
|
1126
|
-
# Resolves the skills backend, lists skills, and selects an adapter.
|
|
1127
|
-
#
|
|
1128
|
-
# Returns nil if skills are not configured or empty.
|
|
1129
|
-
# Does not mutate instance state — callers are responsible for
|
|
1130
|
-
# assigning the returned context.
|
|
1131
|
-
#
|
|
1132
|
-
#--
|
|
1133
|
-
#: (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
|
|
1134
|
-
def resolve_skills(context = @context)
|
|
1135
|
-
return nil unless self.class.skills
|
|
1136
|
-
|
|
1137
|
-
backend = self.class.skills.backend || Riffer.config.skills.default_backend
|
|
1138
|
-
return nil unless backend
|
|
1139
|
-
|
|
1140
|
-
backend = backend.is_a?(Proc) ? backend.call(context) : backend
|
|
1141
|
-
skills_list = backend.list_skills
|
|
1142
|
-
return nil if skills_list.empty?
|
|
1143
|
-
|
|
1144
|
-
skills = skills_list.to_h { |s| [s.name, s] }
|
|
1145
|
-
adapter_class = self.class.skills.adapter || provider_class.skills_adapter(@model_name)
|
|
1146
|
-
skill_activate_tool_class = self.class.skills.activate_tool || Riffer.config.skills.default_activate_tool
|
|
1147
|
-
|
|
1148
|
-
skills_context = Riffer::Skills::Context.new(
|
|
1149
|
-
backend: backend,
|
|
1150
|
-
skills: skills,
|
|
1151
|
-
adapter: adapter_class.new(skill_activate_tool: skill_activate_tool_class)
|
|
1152
|
-
)
|
|
1153
|
-
ctx = (context || {}).merge(skills: skills_context)
|
|
1154
|
-
|
|
1155
|
-
activate = self.class.skills.activate
|
|
1156
|
-
if activate
|
|
1157
|
-
names = activate.is_a?(Proc) ? activate.call(ctx) : Array(activate)
|
|
1158
|
-
names.each { |name| skills_context.activate(name) }
|
|
1159
|
-
end
|
|
1160
|
-
|
|
1161
|
-
skills_context
|
|
1162
|
-
end
|
|
1163
|
-
|
|
1164
|
-
#--
|
|
1165
|
-
#: () -> singleton(Riffer::Providers::Base)
|
|
1166
|
-
def provider_class
|
|
1167
|
-
klass = Riffer::Providers::Repository.find(@provider_name)
|
|
1168
|
-
raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless klass
|
|
1169
|
-
klass
|
|
1170
|
-
end
|
|
1171
|
-
|
|
1172
|
-
#--
|
|
1173
|
-
#: () -> Riffer::Messages::Assistant?
|
|
1174
|
-
def extract_final_response
|
|
1175
|
-
# TODO: Replace with rfind when minimum Ruby is 4.0+
|
|
1176
|
-
# rubocop:disable Style/ReverseFind
|
|
1177
|
-
@messages.reverse.find { |msg| msg.is_a?(Riffer::Messages::Assistant) } #: Riffer::Messages::Assistant?
|
|
1178
|
-
# rubocop:enable Style/ReverseFind
|
|
1179
|
-
end
|
|
1180
|
-
|
|
1181
|
-
#--
|
|
1182
|
-
#: () -> [Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
|
|
1183
|
-
def run_before_guardrails
|
|
1184
|
-
guardrails = self.class.guardrails_for(:before)
|
|
1185
|
-
return [nil, []] if guardrails.empty?
|
|
1186
|
-
|
|
1187
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :before, context: @context)
|
|
1188
|
-
processed_messages, tripwire, modifications = runner.run(@messages)
|
|
1189
|
-
@messages = processed_messages unless tripwire
|
|
1190
|
-
[tripwire, modifications]
|
|
1191
|
-
end
|
|
1192
|
-
|
|
1193
|
-
#--
|
|
1194
|
-
#: (Riffer::Messages::Assistant) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
|
|
1195
|
-
def run_after_guardrails(response)
|
|
1196
|
-
guardrails = self.class.guardrails_for(:after)
|
|
1197
|
-
return [response, nil, []] if guardrails.empty?
|
|
1198
|
-
|
|
1199
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :after, context: @context)
|
|
1200
|
-
processed_response, tripwire, modifications = runner.run(response, messages: @messages)
|
|
1201
|
-
|
|
1202
|
-
response_index = @messages.length
|
|
1203
|
-
modifications.each { |m| m.message_indices.map! { response_index } }
|
|
1204
|
-
|
|
1205
|
-
[processed_response, tripwire, modifications]
|
|
1206
|
-
end
|
|
1207
|
-
|
|
1208
|
-
#--
|
|
1209
|
-
#: (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
|
|
1210
|
-
def validate_structured_output(response)
|
|
1211
|
-
return unless response&.structured_output? && @structured_output
|
|
1212
|
-
|
|
1213
|
-
@structured_output.parse_and_validate(response.content).object
|
|
1214
|
-
end
|
|
1215
|
-
|
|
1216
|
-
#--
|
|
1217
|
-
#: () -> Riffer::StructuredOutput?
|
|
1218
|
-
def resolve_structured_output
|
|
1219
|
-
params = self.class.structured_output
|
|
1220
|
-
params ? Riffer::StructuredOutput.new(params) : nil
|
|
1221
|
-
end
|
|
1222
|
-
|
|
1223
|
-
#--
|
|
1224
|
-
#: () -> Hash[Symbol, untyped]
|
|
1225
|
-
def merged_model_options
|
|
1226
|
-
opts = self.class.model_options.dup
|
|
1227
|
-
opts[:structured_output] = @structured_output if @structured_output
|
|
1228
|
-
opts
|
|
1229
|
-
end
|
|
1230
|
-
|
|
1231
|
-
#--
|
|
1232
|
-
#: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?healed_tool_call_ids: Array[String]) -> Riffer::Agent::Response
|
|
1233
|
-
def build_response(content, tripwire: nil, modifications: [], interrupted: false, interrupt_reason: nil, structured_output: nil, healed_tool_call_ids: [])
|
|
1234
|
-
Riffer::Agent::Response.new(content, tripwire: tripwire, modifications: modifications, interrupted: interrupted, interrupt_reason: interrupt_reason, structured_output: structured_output, messages: @messages.frozen? ? @messages : @messages.dup.freeze, healed_tool_call_ids: healed_tool_call_ids)
|
|
1235
|
-
end
|
|
1236
547
|
end
|