riffer 0.28.0 → 0.29.1
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 +53 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +26 -0
- data/README.md +17 -18
- data/Steepfile +8 -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 +127 -0
- data/lib/riffer/agent/response.rb +2 -0
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +270 -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 +236 -923
- data/lib/riffer/config.rb +14 -7
- data/lib/riffer/evals/evaluator.rb +18 -3
- data/lib/riffer/evals/judge.rb +7 -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 +3 -1
- data/lib/riffer/mcp/registration.rb +6 -3
- data/lib/riffer/mcp/registry.rb +6 -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} +7 -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 +37 -31
- data/lib/riffer/providers/anthropic.rb +39 -36
- data/lib/riffer/providers/base.rb +12 -9
- data/lib/riffer/providers/gemini.rb +19 -12
- data/lib/riffer/providers/mock.rb +45 -13
- data/lib/riffer/providers/open_ai.rb +34 -29
- data/lib/riffer/providers/open_router.rb +325 -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 +6 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +3 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +6 -1
- data/lib/riffer/skills/context.rb +6 -3
- data/lib/riffer/skills/filesystem_backend.rb +10 -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/tools/response.rb +2 -0
- 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} +11 -9
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +19 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +4 -1
- data/sig/_private/anthropic.rbs +16 -0
- data/sig/_private/async.rbs +24 -0
- data/sig/_private/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/_private/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/_private/mcp.rbs +22 -0
- data/sig/_private/openai.rbs +29 -0
- data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -0
- data/sig/_private/riffer/providers/anthropic.rbs +4 -0
- data/sig/_private/riffer/providers/open_ai.rbs +4 -0
- data/sig/_private/riffer/providers/open_router.rbs +4 -0
- data/sig/_private/zeitwerk.rbs +12 -0
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +93 -0
- data/sig/generated/riffer/agent/response.rbs +2 -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 +147 -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 +145 -342
- data/sig/generated/riffer/config.rbs +17 -5
- data/sig/generated/riffer/evals/evaluator.rbs +8 -0
- data/sig/generated/riffer/evals/judge.rbs +10 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/mcp/client.rbs +2 -0
- data/sig/generated/riffer/mcp/registration.rbs +6 -0
- data/sig/generated/riffer/mcp/registry.rbs +4 -0
- 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} +7 -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 +12 -12
- data/sig/generated/riffer/providers/base.rbs +12 -10
- data/sig/generated/riffer/providers/gemini.rbs +10 -4
- data/sig/generated/riffer/providers/mock.rbs +31 -5
- data/sig/generated/riffer/providers/open_ai.rbs +10 -10
- 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 +4 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +4 -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 +9 -1
- data/sig/generated/riffer/skills/context.rbs +6 -2
- data/sig/generated/riffer/skills/filesystem_backend.rbs +4 -0
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/tools/response.rbs +2 -0
- 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} +14 -12
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +18 -6
- data/sig/generated/riffer.rbs +2 -0
- data/sig/manifest.yaml +3 -0
- data/sig/manual/riffer/agent/run.rbs +5 -0
- data/sig/manual/riffer/helpers/call_or_value.rbs +5 -0
- data/sig/manual/riffer/tools/toolable.rbs +6 -0
- metadata +59 -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
|
@@ -19,42 +19,36 @@ require "json"
|
|
|
19
19
|
# agent.generate('Hello!')
|
|
20
20
|
#
|
|
21
21
|
class Riffer::Agent
|
|
22
|
+
# @rbs self.@config: Riffer::Agent::Config?
|
|
23
|
+
|
|
22
24
|
include Riffer::Messages::Converter
|
|
23
25
|
extend Riffer::Helpers::ClassNameConverter
|
|
24
|
-
extend Riffer::Helpers::Validations
|
|
25
26
|
|
|
26
|
-
DEFAULT_MAX_STEPS = 16 #: Integer
|
|
27
27
|
INTERRUPT_MAX_STEPS = :max_steps #: Symbol
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
# Returns the per-class Riffer::Agent::Config value object holding every
|
|
30
|
+
# DSL setting. Lazily initialized on first read; each subclass has its own.
|
|
31
|
+
#
|
|
32
|
+
#--
|
|
33
|
+
#: () -> Riffer::Agent::Config
|
|
34
|
+
def self.config
|
|
35
|
+
@config ||= Riffer::Agent::Config.new
|
|
36
|
+
end
|
|
35
37
|
|
|
36
38
|
# Gets or sets the agent identifier.
|
|
37
39
|
#
|
|
38
40
|
#--
|
|
39
41
|
#: (?String?) -> String
|
|
40
42
|
def self.identifier(value = nil)
|
|
41
|
-
|
|
42
|
-
@identifier = value.to_s
|
|
43
|
+
value.nil? ? (config.identifier || class_name_to_path(name)) : (config.identifier = value)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
# Gets or sets the model string (e.g., "openai/gpt-4o") or Proc.
|
|
46
47
|
#
|
|
47
48
|
#--
|
|
48
49
|
#: (?(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
|
|
50
|
+
def self.model(value = nil)
|
|
51
|
+
value.nil? ? config.model : (config.model = value)
|
|
58
52
|
end
|
|
59
53
|
|
|
60
54
|
# Gets or sets the agent instructions.
|
|
@@ -71,15 +65,8 @@ class Riffer::Agent
|
|
|
71
65
|
#
|
|
72
66
|
#--
|
|
73
67
|
#: (?(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
|
|
68
|
+
def self.instructions(value = nil)
|
|
69
|
+
value.nil? ? config.instructions : (config.instructions = value)
|
|
83
70
|
end
|
|
84
71
|
|
|
85
72
|
# Gets or sets provider options passed to the provider client.
|
|
@@ -87,8 +74,7 @@ class Riffer::Agent
|
|
|
87
74
|
#--
|
|
88
75
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
89
76
|
def self.provider_options(options = nil)
|
|
90
|
-
|
|
91
|
-
@provider_options = options
|
|
77
|
+
options.nil? ? config.provider_options : (config.provider_options = options)
|
|
92
78
|
end
|
|
93
79
|
|
|
94
80
|
# Gets or sets model options passed to generate_text/stream_text.
|
|
@@ -96,8 +82,7 @@ class Riffer::Agent
|
|
|
96
82
|
#--
|
|
97
83
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
98
84
|
def self.model_options(options = nil)
|
|
99
|
-
|
|
100
|
-
@model_options = options
|
|
85
|
+
options.nil? ? config.model_options : (config.model_options = options)
|
|
101
86
|
end
|
|
102
87
|
|
|
103
88
|
# Gets or sets the structured output schema for this agent.
|
|
@@ -105,89 +90,34 @@ class Riffer::Agent
|
|
|
105
90
|
# Accepts a Riffer::Params instance or a block evaluated against a new Params.
|
|
106
91
|
#
|
|
107
92
|
#--
|
|
108
|
-
#: (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
|
|
93
|
+
#: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
109
94
|
def self.structured_output(params = nil, &block)
|
|
110
95
|
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
|
|
96
|
+
params = Riffer::Params.new
|
|
97
|
+
params.instance_eval(&block)
|
|
118
98
|
end
|
|
99
|
+
config.structured_output = params if params
|
|
100
|
+
config.structured_output
|
|
119
101
|
end
|
|
120
102
|
|
|
121
103
|
# Gets or sets the maximum number of LLM call steps in the tool-use loop.
|
|
122
104
|
#
|
|
123
|
-
# Defaults to DEFAULT_MAX_STEPS (16). Set to
|
|
124
|
-
# unlimited steps.
|
|
105
|
+
# Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
|
|
106
|
+
# +Float::INFINITY+ for unlimited steps.
|
|
125
107
|
#
|
|
126
108
|
#--
|
|
127
109
|
#: (?Numeric?) -> Numeric
|
|
128
110
|
def self.max_steps(value = nil)
|
|
129
|
-
|
|
130
|
-
@max_steps = value
|
|
111
|
+
value.nil? ? config.max_steps : (config.max_steps = value)
|
|
131
112
|
end
|
|
132
113
|
|
|
133
114
|
# Gets or sets the tools used by this agent.
|
|
134
115
|
#
|
|
135
116
|
#--
|
|
136
117
|
#: (?(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
|
|
180
|
-
end
|
|
181
|
-
|
|
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)
|
|
118
|
+
def self.uses_tools(value = nil)
|
|
119
|
+
value.nil? ? config.tools_config : (config.tools_config = value)
|
|
189
120
|
end
|
|
190
|
-
private_class_method :resolve_uses_tools_config
|
|
191
121
|
|
|
192
122
|
# Opts this agent into tools from all MCP registrations that share any of
|
|
193
123
|
# the given tag(s).
|
|
@@ -196,34 +126,25 @@ class Riffer::Agent
|
|
|
196
126
|
#
|
|
197
127
|
#: (String | Symbol) -> void
|
|
198
128
|
def self.use_mcp(tag)
|
|
199
|
-
|
|
200
|
-
@mcp_configs << {tags: [tag.to_sym]}
|
|
129
|
+
config.add_mcp(tag)
|
|
201
130
|
end
|
|
202
131
|
|
|
203
132
|
# Returns the accumulated +use_mcp+ configurations for this agent class.
|
|
204
133
|
#
|
|
205
134
|
#: () -> Array[Hash[Symbol, untyped]]
|
|
206
135
|
def self.mcp_configs
|
|
207
|
-
|
|
136
|
+
config.mcp_configs
|
|
208
137
|
end
|
|
209
138
|
|
|
210
139
|
# Gets or sets the tool runtime for this agent.
|
|
211
140
|
#
|
|
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>.
|
|
141
|
+
# Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
|
|
142
|
+
# or a Proc. Defaults to <tt>Riffer.config.tool_runtime</tt> when unset.
|
|
217
143
|
#
|
|
218
144
|
#--
|
|
219
|
-
#: (?(singleton(Riffer::
|
|
145
|
+
#: (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
220
146
|
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
|
|
147
|
+
value.nil? ? config.tool_runtime : (config.tool_runtime = value)
|
|
227
148
|
end
|
|
228
149
|
|
|
229
150
|
# Configures skills for this agent via a block DSL.
|
|
@@ -237,13 +158,14 @@ class Riffer::Agent
|
|
|
237
158
|
# end
|
|
238
159
|
#
|
|
239
160
|
#--
|
|
240
|
-
#: () ?{ () -> void } -> Riffer::Skills::Config?
|
|
161
|
+
#: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
|
|
241
162
|
def self.skills(&block)
|
|
242
163
|
if block
|
|
243
|
-
|
|
244
|
-
|
|
164
|
+
skills_config = Riffer::Skills::Config.new
|
|
165
|
+
skills_config.instance_eval(&block)
|
|
166
|
+
config.skills_config = skills_config
|
|
245
167
|
end
|
|
246
|
-
|
|
168
|
+
config.skills_config
|
|
247
169
|
end
|
|
248
170
|
|
|
249
171
|
# Finds an agent class by identifier.
|
|
@@ -264,22 +186,24 @@ class Riffer::Agent
|
|
|
264
186
|
|
|
265
187
|
# Generates a response using a new agent instance.
|
|
266
188
|
#
|
|
267
|
-
#
|
|
189
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
190
|
+
# to +#generate+.
|
|
268
191
|
#
|
|
269
192
|
#--
|
|
270
|
-
#: (
|
|
271
|
-
def self.generate(
|
|
272
|
-
new.generate(
|
|
193
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
|
|
194
|
+
def self.generate(prompt = nil, files: nil, context: nil)
|
|
195
|
+
new(context: context).generate(prompt, files: files)
|
|
273
196
|
end
|
|
274
197
|
|
|
275
198
|
# Streams a response using a new agent instance.
|
|
276
199
|
#
|
|
277
|
-
#
|
|
200
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
201
|
+
# to +#stream+.
|
|
278
202
|
#
|
|
279
203
|
#--
|
|
280
|
-
#: (
|
|
281
|
-
def self.stream(
|
|
282
|
-
new.stream(
|
|
204
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
205
|
+
def self.stream(prompt = nil, files: nil, context: nil)
|
|
206
|
+
new(context: context).stream(prompt, files: files)
|
|
283
207
|
end
|
|
284
208
|
|
|
285
209
|
# Registers a guardrail for input, output, or both phases.
|
|
@@ -292,22 +216,7 @@ class Riffer::Agent
|
|
|
292
216
|
#--
|
|
293
217
|
#: (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void
|
|
294
218
|
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
|
|
219
|
+
config.add_guardrail(phase, klass: with, options: options)
|
|
311
220
|
end
|
|
312
221
|
|
|
313
222
|
# Returns the registered guardrail configs for a given phase.
|
|
@@ -317,127 +226,136 @@ class Riffer::Agent
|
|
|
317
226
|
#--
|
|
318
227
|
#: (Symbol) -> Array[Hash[Symbol, untyped]]
|
|
319
228
|
def self.guardrails_for(phase)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
#
|
|
328
|
-
attr_reader :
|
|
229
|
+
config.guardrails_for(phase)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The conversation handle. See Riffer::Agent::Session.
|
|
233
|
+
attr_reader :session #: Riffer::Agent::Session
|
|
234
|
+
|
|
235
|
+
# The per-instance Riffer::Agent::Config. Either the class-level default or
|
|
236
|
+
# an explicit Config passed to +Agent.new(config:)+.
|
|
237
|
+
attr_reader :config #: Riffer::Agent::Config
|
|
238
|
+
|
|
239
|
+
# The system message built from the configured +instructions+, or +nil+
|
|
240
|
+
# when no instructions are configured. Built once at +Agent.new+ using the
|
|
241
|
+
# constructor +context:+ and cached. Useful for persistence flows.
|
|
242
|
+
attr_reader :instruction_message #: Riffer::Messages::System?
|
|
243
|
+
|
|
244
|
+
# The system message describing the configured skills catalog, or +nil+
|
|
245
|
+
# when skills are unconfigured or the catalog is empty. Built once at
|
|
246
|
+
# +Agent.new+ and cached.
|
|
247
|
+
attr_reader :skills_message #: Riffer::Messages::System?
|
|
248
|
+
|
|
249
|
+
# The mutable runtime context, a +Riffer::Agent::Context+ value object
|
|
250
|
+
# threaded into every Proc-based DSL setting, guardrail, tool runtime,
|
|
251
|
+
# and skills resolution, and shared with every +Riffer::Agent::Run+
|
|
252
|
+
# this agent executes. Exposes:
|
|
253
|
+
#
|
|
254
|
+
# - +context.skills+ — the resolved +Riffer::Skills::Context+ (when
|
|
255
|
+
# skills are configured), set at +Agent.new+ time.
|
|
256
|
+
# - +context.token_usage+ — the cumulative +Riffer::Providers::TokenUsage+,
|
|
257
|
+
# updated by each Run as the loop progresses.
|
|
258
|
+
# - +context[:key]+ / <tt>context.dig(:key)</tt> — Hash-style reads for
|
|
259
|
+
# caller-provided keys (e.g. <tt>context[:agent]</tt>,
|
|
260
|
+
# <tt>context[:tenant]</tt>). +:skills+ and +:token_usage+ are
|
|
261
|
+
# reserved and cannot be passed by the caller.
|
|
262
|
+
attr_reader :context #: Riffer::Agent::Context
|
|
263
|
+
|
|
264
|
+
# The resolved model name (the part after "provider/"), used as the model
|
|
265
|
+
# argument on every LLM call. Resolved eagerly at +Agent.new+.
|
|
266
|
+
attr_reader :model_name #: String
|
|
267
|
+
|
|
268
|
+
# The provider client. Built eagerly at +Agent.new+ from the configured
|
|
269
|
+
# provider class and +Config#provider_options+, then handed to every
|
|
270
|
+
# +Riffer::Agent::Run+ this agent executes. Public so tests can pre-queue
|
|
271
|
+
# responses on +Riffer::Providers::Mock+ before calling +#generate+.
|
|
272
|
+
attr_reader :provider #: Riffer::Providers::Base
|
|
273
|
+
|
|
274
|
+
# The +Riffer::Agent::StructuredOutput+ wrapping the configured schema, or +nil+
|
|
275
|
+
# when structured output is not configured. Resolved eagerly at +Agent.new+.
|
|
276
|
+
attr_reader :structured_output #: Riffer::Agent::StructuredOutput?
|
|
277
|
+
|
|
278
|
+
# The tool classes the LLM sees on every call this agent makes. Resolved
|
|
279
|
+
# eagerly at +Agent.new+ (Proc-form +uses_tools+ is called against
|
|
280
|
+
# +context+ once; MCP tools and the skill_activate tool are merged in).
|
|
281
|
+
attr_reader :tools #: Array[singleton(Riffer::Tool)]
|
|
282
|
+
|
|
283
|
+
# The tool runtime instance used to execute tool calls. Resolved eagerly
|
|
284
|
+
# at +Agent.new+ (Proc-form +tool_runtime+ is called against +context+ once).
|
|
285
|
+
attr_reader :tool_runtime #: Riffer::Tools::Runtime
|
|
329
286
|
|
|
330
287
|
# Initializes a new agent.
|
|
331
288
|
#
|
|
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.
|
|
354
|
-
#
|
|
355
|
-
# When +Riffer.config.experimental_history_healing+ is enabled, seeded
|
|
356
|
-
# message arrays that violate the +tool_use+ ↔ +tool_result+ invariant
|
|
357
|
-
# are silently repaired before the run begins.
|
|
289
|
+
# When +session:+ is omitted, a fresh +Riffer::Agent::Session+ is built and seeded
|
|
290
|
+
# with the system instruction message and skills catalog (when configured),
|
|
291
|
+
# using +context:+. When +session:+ is provided, the agent uses it as-is —
|
|
292
|
+
# the caller is responsible for the session's contents (typical use case:
|
|
293
|
+
# cross-process resume from persisted history). With
|
|
294
|
+
# +Riffer.config.experimental_history_healing+ on, a provided session is
|
|
295
|
+
# healed at construction time so the +tool_use+ ↔ +tool_result+ invariant
|
|
296
|
+
# holds before the next inference call.
|
|
358
297
|
#
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
298
|
+
# +context:+ flows through Proc-based instructions, model, skills resolution,
|
|
299
|
+
# tool resolution, guardrails, and tool runtime. It is fixed for the
|
|
300
|
+
# lifetime of the agent.
|
|
379
301
|
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
302
|
+
# Raises Riffer::ArgumentError if the configured model string is invalid
|
|
303
|
+
# (must be "provider/model" format).
|
|
382
304
|
#
|
|
383
305
|
#--
|
|
384
|
-
#: (
|
|
385
|
-
def
|
|
386
|
-
|
|
306
|
+
#: (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void
|
|
307
|
+
def initialize(session: nil, context: nil, config: nil)
|
|
308
|
+
@config = config || self.class.config
|
|
309
|
+
@context = Riffer::Agent::Context.new(context || {})
|
|
387
310
|
|
|
388
|
-
@
|
|
389
|
-
|
|
390
|
-
initialize_messages(prompt_or_messages, files: files)
|
|
311
|
+
provider_class, @model_name = resolve_provider_and_model
|
|
312
|
+
@provider = provider_class.new(**@config.provider_options)
|
|
391
313
|
|
|
392
|
-
|
|
393
|
-
tripwire, modifications = run_before_guardrails
|
|
394
|
-
modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }
|
|
314
|
+
@context.skills = resolve_skills(provider_class)
|
|
395
315
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
end
|
|
316
|
+
@structured_output = resolve_structured_output
|
|
317
|
+
@tools = resolve_tools
|
|
318
|
+
@tool_runtime = resolve_tool_runtime
|
|
400
319
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
end
|
|
320
|
+
@instruction_message = build_instruction_message
|
|
321
|
+
@skills_message = build_skills_message
|
|
404
322
|
|
|
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
|
|
323
|
+
@session = session || Riffer::Agent::Session.new(messages: [@instruction_message, @skills_message].compact)
|
|
324
|
+
@session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
|
|
415
325
|
end
|
|
416
326
|
|
|
417
|
-
# Generates
|
|
327
|
+
# Generates a response from the agent.
|
|
418
328
|
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
329
|
+
# Runs the inference loop via +Riffer::Agent::Run.generate+. When +prompt+
|
|
330
|
+
# is given, a new +Riffer::Messages::User+ is appended to the session
|
|
331
|
+
# (silently — +on_message+ does not fire for user inputs) and then the
|
|
332
|
+
# loop runs. When +prompt+ is omitted, the loop runs against the current
|
|
333
|
+
# session — useful for resuming a persisted conversation whose last turn
|
|
334
|
+
# is already a user message, or for picking up pending tool calls after
|
|
335
|
+
# an interrupt.
|
|
421
336
|
#
|
|
422
|
-
#
|
|
337
|
+
# +files:+ requires +prompt+. Pass files to attach to the new user message.
|
|
423
338
|
#
|
|
424
339
|
#--
|
|
425
|
-
#: (?
|
|
426
|
-
def
|
|
427
|
-
|
|
340
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
|
|
341
|
+
def generate(prompt = nil, files: nil)
|
|
342
|
+
Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files)
|
|
428
343
|
end
|
|
429
344
|
|
|
430
|
-
#
|
|
345
|
+
# Streams a response from the agent.
|
|
431
346
|
#
|
|
432
|
-
#
|
|
433
|
-
#
|
|
347
|
+
# Runs the inference loop via +Riffer::Agent::Run.stream+, returning an
|
|
348
|
+
# +Enumerator+ of +Riffer::StreamEvents+.
|
|
349
|
+
#
|
|
350
|
+
# Raises Riffer::ArgumentError if structured output is configured.
|
|
434
351
|
#
|
|
435
|
-
#
|
|
352
|
+
# See +#generate+ for prompt/files semantics.
|
|
436
353
|
#
|
|
437
354
|
#--
|
|
438
|
-
#: (?
|
|
439
|
-
def
|
|
440
|
-
|
|
355
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
356
|
+
def stream(prompt = nil, files: nil)
|
|
357
|
+
raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if @structured_output
|
|
358
|
+
Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files)
|
|
441
359
|
end
|
|
442
360
|
|
|
443
361
|
# Interrupts the agent loop.
|
|
@@ -458,599 +376,135 @@ class Riffer::Agent
|
|
|
458
376
|
throw :riffer_interrupt, reason
|
|
459
377
|
end
|
|
460
378
|
|
|
461
|
-
|
|
462
|
-
#
|
|
463
|
-
#--
|
|
464
|
-
#: (String) -> Riffer::Messages::Base?
|
|
465
|
-
def message_by_id(id)
|
|
466
|
-
@messages.find { |m| m.id == id }
|
|
467
|
-
end
|
|
379
|
+
private
|
|
468
380
|
|
|
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
381
|
#--
|
|
473
|
-
#: (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
382
|
+
#: () -> Riffer::Messages::System?
|
|
383
|
+
def build_instruction_message
|
|
384
|
+
content = Riffer::Helpers::CallOrValue.resolve(@config.instructions, context: @context)
|
|
385
|
+
return nil if content.nil? || content.empty?
|
|
386
|
+
Riffer::Messages::System.new(content)
|
|
478
387
|
end
|
|
479
|
-
# rubocop:enable Style/ReverseFind
|
|
480
388
|
|
|
481
|
-
# Returns the most recent +Riffer::Messages::Assistant+ in history, or
|
|
482
|
-
# +nil+ when no assistant message has been recorded yet.
|
|
483
|
-
#
|
|
484
389
|
#--
|
|
485
|
-
#: () -> Riffer::Messages::
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
390
|
+
#: () -> Riffer::Messages::System?
|
|
391
|
+
def build_skills_message
|
|
392
|
+
skills = @context.skills
|
|
393
|
+
return nil unless skills&.system_prompt
|
|
394
|
+
Riffer::Messages::System.new(skills.system_prompt)
|
|
490
395
|
end
|
|
491
|
-
# rubocop:enable Style/ReverseFind
|
|
492
396
|
|
|
493
|
-
#
|
|
494
|
-
#
|
|
397
|
+
# Resolves +Config#model+ to a "provider/model" string (calling the Proc
|
|
398
|
+
# form against +@context+), parses it, and looks up the provider class.
|
|
495
399
|
#
|
|
496
|
-
#
|
|
497
|
-
#
|
|
400
|
+
# Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
|
|
401
|
+
# an invalid model string or an unregistered provider.
|
|
498
402
|
#
|
|
499
403
|
#--
|
|
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
|
|
404
|
+
#: () -> [singleton(Riffer::Providers::Base), String]
|
|
405
|
+
def resolve_provider_and_model
|
|
406
|
+
model_string = Riffer::Helpers::CallOrValue.resolve(@config.model, context: @context)
|
|
407
|
+
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless model_string.is_a?(String)
|
|
508
408
|
|
|
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
|
|
409
|
+
provider_name, model_name = model_string.split("/", 2)
|
|
536
410
|
|
|
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"
|
|
411
|
+
unless provider_name.is_a?(String) && !provider_name.strip.empty? && model_name.is_a?(String) && !model_name.strip.empty?
|
|
412
|
+
raise Riffer::ArgumentError, "Invalid model string: #{model_string}"
|
|
559
413
|
end
|
|
560
414
|
|
|
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
|
|
415
|
+
provider_class = Riffer::Providers::Repository.find(provider_name)
|
|
416
|
+
raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
|
|
570
417
|
|
|
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
|
|
418
|
+
[provider_class, model_name]
|
|
595
419
|
end
|
|
596
420
|
|
|
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).
|
|
421
|
+
# Resolves the skills backend, lists skills, and selects an adapter.
|
|
422
|
+
# Returns nil if skills are unconfigured or the backend is empty.
|
|
608
423
|
#
|
|
609
424
|
#--
|
|
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
|
|
425
|
+
#: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
|
|
426
|
+
def resolve_skills(provider_class)
|
|
427
|
+
skills_config = @config.skills_config
|
|
428
|
+
return nil unless skills_config
|
|
646
429
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
step += 1
|
|
650
|
-
|
|
651
|
-
track_token_usage(response.token_usage)
|
|
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)
|
|
659
|
-
|
|
660
|
-
break unless has_tool_calls?(processed_response)
|
|
430
|
+
backend = skills_config.backend || Riffer.config.skills.default_backend
|
|
431
|
+
return nil unless backend
|
|
661
432
|
|
|
662
|
-
|
|
433
|
+
backend = Riffer::Helpers::CallOrValue.resolve(backend, context: @context)
|
|
434
|
+
return nil if backend.list_skills.empty?
|
|
663
435
|
|
|
664
|
-
|
|
665
|
-
|
|
436
|
+
skills = backend.list_skills.to_h { |s| [s.name, s] }
|
|
437
|
+
adapter_class = skills_config.adapter || provider_class.skills_adapter(@model_name)
|
|
438
|
+
skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
|
|
666
439
|
|
|
667
|
-
|
|
440
|
+
skills_context = Riffer::Skills::Context.new(
|
|
441
|
+
backend: backend,
|
|
442
|
+
skills: skills,
|
|
443
|
+
adapter: adapter_class.new(skill_activate_tool: skill_activate_tool_class)
|
|
444
|
+
)
|
|
668
445
|
|
|
669
|
-
|
|
446
|
+
if skills_config.activate
|
|
447
|
+
names = Array(Riffer::Helpers::CallOrValue.resolve(skills_config.activate, context: @context))
|
|
448
|
+
names.each { |name| skills_context.activate(name) }
|
|
670
449
|
end
|
|
671
450
|
|
|
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
|
|
451
|
+
skills_context
|
|
717
452
|
end
|
|
718
453
|
|
|
719
454
|
#--
|
|
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
|
|
455
|
+
#: () -> Riffer::Agent::StructuredOutput?
|
|
456
|
+
def resolve_structured_output
|
|
457
|
+
params = @config.structured_output
|
|
458
|
+
params ? Riffer::Agent::StructuredOutput.new(params) : nil
|
|
735
459
|
end
|
|
736
460
|
|
|
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.
|
|
461
|
+
# Resolves the full tool catalog for the agent:
|
|
741
462
|
#
|
|
742
|
-
#
|
|
743
|
-
#
|
|
744
|
-
#
|
|
745
|
-
#
|
|
463
|
+
# - The configured +uses_tools+ value (Proc-form resolved against +context+).
|
|
464
|
+
# - The skill activation tool, when a +skills+ block is configured. The
|
|
465
|
+
# activation tool class comes from the per-agent +skills do; activate_tool ...; end+
|
|
466
|
+
# override when set, otherwise from +Riffer.config.skills.default_activate_tool+.
|
|
467
|
+
# - All MCP tools matching any +use_mcp+ tag, optionally wrapped in
|
|
468
|
+
# AuthenticatedTool when +Riffer.config.mcp.credentials+ is configured.
|
|
746
469
|
#
|
|
747
|
-
#
|
|
470
|
+
# Raises Riffer::ArgumentError on tool name conflicts with the skill
|
|
471
|
+
# activation tool, on duplicate tool names across sources, or on tool
|
|
472
|
+
# classes missing required metadata (description, params).
|
|
748
473
|
#
|
|
749
474
|
#--
|
|
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
|
|
475
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
476
|
+
def resolve_tools
|
|
477
|
+
tools = Riffer::Helpers::CallOrValue.resolve(@config.tools_config, context: @context, default: [])
|
|
803
478
|
|
|
804
|
-
|
|
805
|
-
#: (Enumerator::Yielder) -> void
|
|
806
|
-
def run_stream_loop(yielder)
|
|
807
|
-
step = count_assistant_messages
|
|
479
|
+
skills_config = @config.skills_config
|
|
808
480
|
|
|
809
|
-
if
|
|
810
|
-
|
|
811
|
-
end
|
|
481
|
+
if skills_config
|
|
482
|
+
skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
|
|
812
483
|
|
|
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)
|
|
484
|
+
if tools.any? { |t| t.name == skill_activate_tool_class.name }
|
|
485
|
+
raise Riffer::ArgumentError, "Tool name conflict with skill tools: #{skill_activate_tool_class.name}"
|
|
870
486
|
end
|
|
871
|
-
:completed
|
|
872
|
-
end
|
|
873
|
-
|
|
874
|
-
unless completed == :completed
|
|
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)
|
|
878
|
-
end
|
|
879
|
-
end
|
|
880
|
-
|
|
881
|
-
#--
|
|
882
|
-
#: () -> Riffer::Messages::Assistant
|
|
883
|
-
def call_llm
|
|
884
|
-
provider_instance.generate_text(
|
|
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
487
|
|
|
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
|
|
488
|
+
tools += [skill_activate_tool_class]
|
|
1019
489
|
end
|
|
1020
|
-
end
|
|
1021
490
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
491
|
+
tools += resolve_mcp_tool_classes
|
|
492
|
+
assert_distinct_tool_names!(tools)
|
|
493
|
+
tools.each(&:validate_as_tool!)
|
|
494
|
+
tools
|
|
1034
495
|
end
|
|
1035
496
|
|
|
1036
497
|
#--
|
|
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
|
|
498
|
+
#: () -> Riffer::Tools::Runtime
|
|
499
|
+
def resolve_tool_runtime
|
|
500
|
+
runtime = Riffer::Helpers::CallOrValue.resolve(@config.tool_runtime, context: @context)
|
|
501
|
+
runtime.is_a?(Class) ? runtime.new : runtime
|
|
1048
502
|
end
|
|
1049
503
|
|
|
1050
504
|
#--
|
|
1051
505
|
#: () -> Array[singleton(Riffer::Tool)]
|
|
1052
506
|
def resolve_mcp_tool_classes
|
|
1053
|
-
configs =
|
|
507
|
+
configs = @config.mcp_configs
|
|
1054
508
|
return [] if configs.empty?
|
|
1055
509
|
|
|
1056
510
|
cred = Riffer.config.mcp.credentials
|
|
@@ -1065,7 +519,7 @@ class Riffer::Agent
|
|
|
1065
519
|
#
|
|
1066
520
|
#: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
1067
521
|
def gather_mcp_registrations_with_tags(configs)
|
|
1068
|
-
by_reg = {}
|
|
522
|
+
by_reg = {} #: Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
1069
523
|
configs.each do |cfg|
|
|
1070
524
|
Riffer::Mcp::Registry.find_by_tags(cfg[:tags]).each do |reg|
|
|
1071
525
|
(by_reg[reg] ||= []).concat(cfg[:tags] & reg.manifest.tags)
|
|
@@ -1074,7 +528,7 @@ class Riffer::Agent
|
|
|
1074
528
|
by_reg
|
|
1075
529
|
end
|
|
1076
530
|
|
|
1077
|
-
#: (Riffer::Mcp::Registration, Array[Symbol],
|
|
531
|
+
#: (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
532
|
def mcp_tools_for_registration(reg, matched_tags, cred, ctx)
|
|
1079
533
|
return reg.tools unless cred
|
|
1080
534
|
return [] if cred.call(manifest: reg.manifest, matched_tags: matched_tags, context: ctx).nil?
|
|
@@ -1092,145 +546,4 @@ class Riffer::Agent
|
|
|
1092
546
|
|
|
1093
547
|
raise Riffer::ArgumentError, "Duplicate tool names: #{dupes.sort.join(", ")}"
|
|
1094
548
|
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
549
|
end
|