riffer 0.27.2 → 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 +31 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +134 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +23 -19
- data/docs/08_MESSAGES.md +28 -31
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +25 -15
- 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/response.rb +11 -2
- 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 +246 -684
- data/lib/riffer/config.rb +56 -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 +10 -3
- 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} +21 -15
- 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/response.rbs +10 -2
- 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 +154 -225
- data/sig/generated/riffer/config.rbs +50 -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/interrupt.rbs +7 -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} +19 -13
- 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,33 +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
|
|
|
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
|
+
|
|
29
36
|
# Gets or sets the agent identifier.
|
|
30
37
|
#
|
|
31
38
|
#--
|
|
32
39
|
#: (?String?) -> String
|
|
33
40
|
def self.identifier(value = nil)
|
|
34
|
-
|
|
35
|
-
@identifier = value.to_s
|
|
41
|
+
value.nil? ? (config.identifier || class_name_to_path(name)) : (config.identifier = value)
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
# Gets or sets the model string (e.g., "openai/gpt-4o") or Proc.
|
|
39
45
|
#
|
|
40
46
|
#--
|
|
41
47
|
#: (?(String | Proc)?) -> (String | Proc)?
|
|
42
|
-
def self.model(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if model_string_or_proc.is_a?(Proc)
|
|
46
|
-
@model = model_string_or_proc
|
|
47
|
-
else
|
|
48
|
-
validate_is_string!(model_string_or_proc, "model")
|
|
49
|
-
@model = model_string_or_proc
|
|
50
|
-
end
|
|
48
|
+
def self.model(value = nil)
|
|
49
|
+
value.nil? ? config.model : (config.model = value)
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
# Gets or sets the agent instructions.
|
|
@@ -64,15 +63,8 @@ class Riffer::Agent
|
|
|
64
63
|
#
|
|
65
64
|
#--
|
|
66
65
|
#: (?(String | Proc)?) -> (String | Proc)?
|
|
67
|
-
def self.instructions(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if instructions_or_proc.is_a?(Proc)
|
|
71
|
-
@instructions = instructions_or_proc
|
|
72
|
-
else
|
|
73
|
-
validate_is_string!(instructions_or_proc, "instructions")
|
|
74
|
-
@instructions = instructions_or_proc
|
|
75
|
-
end
|
|
66
|
+
def self.instructions(value = nil)
|
|
67
|
+
value.nil? ? config.instructions : (config.instructions = value)
|
|
76
68
|
end
|
|
77
69
|
|
|
78
70
|
# Gets or sets provider options passed to the provider client.
|
|
@@ -80,8 +72,7 @@ class Riffer::Agent
|
|
|
80
72
|
#--
|
|
81
73
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
82
74
|
def self.provider_options(options = nil)
|
|
83
|
-
|
|
84
|
-
@provider_options = options
|
|
75
|
+
options.nil? ? config.provider_options : (config.provider_options = options)
|
|
85
76
|
end
|
|
86
77
|
|
|
87
78
|
# Gets or sets model options passed to generate_text/stream_text.
|
|
@@ -89,8 +80,7 @@ class Riffer::Agent
|
|
|
89
80
|
#--
|
|
90
81
|
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
91
82
|
def self.model_options(options = nil)
|
|
92
|
-
|
|
93
|
-
@model_options = options
|
|
83
|
+
options.nil? ? config.model_options : (config.model_options = options)
|
|
94
84
|
end
|
|
95
85
|
|
|
96
86
|
# Gets or sets the structured output schema for this agent.
|
|
@@ -98,90 +88,35 @@ class Riffer::Agent
|
|
|
98
88
|
# Accepts a Riffer::Params instance or a block evaluated against a new Params.
|
|
99
89
|
#
|
|
100
90
|
#--
|
|
101
|
-
#: (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
|
|
91
|
+
#: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
102
92
|
def self.structured_output(params = nil, &block)
|
|
103
93
|
if block
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
elsif params.nil?
|
|
107
|
-
@structured_output
|
|
108
|
-
else
|
|
109
|
-
raise Riffer::ArgumentError, "structured_output must be a Riffer::Params" unless params.is_a?(Riffer::Params)
|
|
110
|
-
@structured_output = params
|
|
94
|
+
params = Riffer::Params.new
|
|
95
|
+
params.instance_eval(&block)
|
|
111
96
|
end
|
|
97
|
+
config.structured_output = params if params
|
|
98
|
+
config.structured_output
|
|
112
99
|
end
|
|
113
100
|
|
|
114
101
|
# Gets or sets the maximum number of LLM call steps in the tool-use loop.
|
|
115
102
|
#
|
|
116
|
-
# Defaults to DEFAULT_MAX_STEPS (16). Set to
|
|
117
|
-
# unlimited steps.
|
|
103
|
+
# Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
|
|
104
|
+
# +Float::INFINITY+ for unlimited steps.
|
|
118
105
|
#
|
|
119
106
|
#--
|
|
120
107
|
#: (?Numeric?) -> Numeric
|
|
121
108
|
def self.max_steps(value = nil)
|
|
122
|
-
|
|
123
|
-
@max_steps = value
|
|
109
|
+
value.nil? ? config.max_steps : (config.max_steps = value)
|
|
124
110
|
end
|
|
125
111
|
|
|
126
112
|
# Gets or sets the tools used by this agent.
|
|
127
113
|
#
|
|
128
114
|
#--
|
|
129
115
|
#: (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
|
|
130
|
-
def self.uses_tools(
|
|
131
|
-
|
|
132
|
-
@tools_config = tools_or_lambda
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Returns the tool classes the LLM should see for this agent.
|
|
136
|
-
#
|
|
137
|
-
# Class-level companion to the instance #resolved_tools. Resolves the
|
|
138
|
-
# Proc form of +uses_tools+ and appends the skill activation tool when
|
|
139
|
-
# a +skills+ block is configured. Does not read the skills backend —
|
|
140
|
-
# the LLM-facing tool schema reflects class-level intent, not the
|
|
141
|
-
# runtime state of any backend.
|
|
142
|
-
#
|
|
143
|
-
# When +uses_tools+ is a Proc, +context+ is forwarded to it.
|
|
144
|
-
#
|
|
145
|
-
# The activation tool class is resolved from the agent's
|
|
146
|
-
# <tt>skills do; activate_tool ...; end</tt> override when set, otherwise
|
|
147
|
-
# from <tt>Riffer.config.skills.default_activate_tool</tt>.
|
|
148
|
-
#
|
|
149
|
-
# Each returned tool class is validated via +validate_as_tool!+, so
|
|
150
|
-
# callers serializing this list to a provider can rely on every entry
|
|
151
|
-
# having the metadata required for tool use (name + description).
|
|
152
|
-
#
|
|
153
|
-
# Raises Riffer::ArgumentError on tool name conflicts with the skill
|
|
154
|
-
# activation tool, or when a tool class fails +validate_as_tool!+.
|
|
155
|
-
#
|
|
156
|
-
#--
|
|
157
|
-
#: (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
158
|
-
def self.resolved_tool_classes(context: nil)
|
|
159
|
-
base = resolve_uses_tools_config(context)
|
|
160
|
-
|
|
161
|
-
tools = if skills
|
|
162
|
-
skill_activate_tool_class = skills.activate_tool || Riffer.config.skills.default_activate_tool
|
|
163
|
-
if base.any? { |t| t.name == skill_activate_tool_class.name }
|
|
164
|
-
raise Riffer::ArgumentError, "Tool name conflict with skill tools: #{skill_activate_tool_class.name}"
|
|
165
|
-
end
|
|
166
|
-
base + [skill_activate_tool_class]
|
|
167
|
-
else
|
|
168
|
-
base
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
tools.each(&:validate_as_tool!)
|
|
172
|
-
tools
|
|
116
|
+
def self.uses_tools(value = nil)
|
|
117
|
+
value.nil? ? config.tools_config : (config.tools_config = value)
|
|
173
118
|
end
|
|
174
119
|
|
|
175
|
-
#--
|
|
176
|
-
#: (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
|
|
177
|
-
def self.resolve_uses_tools_config(context)
|
|
178
|
-
config = uses_tools
|
|
179
|
-
return [] if config.nil?
|
|
180
|
-
return config unless config.is_a?(Proc)
|
|
181
|
-
config.arity.zero? ? config.call : config.call(context)
|
|
182
|
-
end
|
|
183
|
-
private_class_method :resolve_uses_tools_config
|
|
184
|
-
|
|
185
120
|
# Opts this agent into tools from all MCP registrations that share any of
|
|
186
121
|
# the given tag(s).
|
|
187
122
|
#
|
|
@@ -189,34 +124,25 @@ class Riffer::Agent
|
|
|
189
124
|
#
|
|
190
125
|
#: (String | Symbol) -> void
|
|
191
126
|
def self.use_mcp(tag)
|
|
192
|
-
|
|
193
|
-
@mcp_configs << {tags: [tag.to_sym]}
|
|
127
|
+
config.add_mcp(tag)
|
|
194
128
|
end
|
|
195
129
|
|
|
196
130
|
# Returns the accumulated +use_mcp+ configurations for this agent class.
|
|
197
131
|
#
|
|
198
132
|
#: () -> Array[Hash[Symbol, untyped]]
|
|
199
133
|
def self.mcp_configs
|
|
200
|
-
|
|
134
|
+
config.mcp_configs
|
|
201
135
|
end
|
|
202
136
|
|
|
203
137
|
# Gets or sets the tool runtime for this agent.
|
|
204
138
|
#
|
|
205
|
-
# Accepts a Riffer::
|
|
206
|
-
# or a Proc.
|
|
207
|
-
#
|
|
208
|
-
# Inherited by subclasses. When unset, walks the ancestor chain and
|
|
209
|
-
# 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.
|
|
210
141
|
#
|
|
211
142
|
#--
|
|
212
|
-
#: (?(singleton(Riffer::
|
|
143
|
+
#: (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
213
144
|
def self.tool_runtime(value = nil)
|
|
214
|
-
|
|
215
|
-
return @tool_runtime if instance_variable_defined?(:@tool_runtime)
|
|
216
|
-
superclass.respond_to?(:tool_runtime) ? superclass.tool_runtime : nil
|
|
217
|
-
else
|
|
218
|
-
@tool_runtime = value
|
|
219
|
-
end
|
|
145
|
+
value.nil? ? config.tool_runtime : (config.tool_runtime = value)
|
|
220
146
|
end
|
|
221
147
|
|
|
222
148
|
# Configures skills for this agent via a block DSL.
|
|
@@ -230,13 +156,14 @@ class Riffer::Agent
|
|
|
230
156
|
# end
|
|
231
157
|
#
|
|
232
158
|
#--
|
|
233
|
-
#: () ?{ () -> void } -> Riffer::Skills::Config?
|
|
159
|
+
#: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
|
|
234
160
|
def self.skills(&block)
|
|
235
161
|
if block
|
|
236
|
-
|
|
237
|
-
|
|
162
|
+
skills_config = Riffer::Skills::Config.new
|
|
163
|
+
skills_config.instance_eval(&block)
|
|
164
|
+
config.skills_config = skills_config
|
|
238
165
|
end
|
|
239
|
-
|
|
166
|
+
config.skills_config
|
|
240
167
|
end
|
|
241
168
|
|
|
242
169
|
# Finds an agent class by identifier.
|
|
@@ -257,22 +184,24 @@ class Riffer::Agent
|
|
|
257
184
|
|
|
258
185
|
# Generates a response using a new agent instance.
|
|
259
186
|
#
|
|
260
|
-
#
|
|
187
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
188
|
+
# to +#generate+.
|
|
261
189
|
#
|
|
262
190
|
#--
|
|
263
|
-
#: (
|
|
264
|
-
def self.generate(
|
|
265
|
-
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)
|
|
266
194
|
end
|
|
267
195
|
|
|
268
196
|
# Streams a response using a new agent instance.
|
|
269
197
|
#
|
|
270
|
-
#
|
|
198
|
+
# +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
|
|
199
|
+
# to +#stream+.
|
|
271
200
|
#
|
|
272
201
|
#--
|
|
273
|
-
#: (
|
|
274
|
-
def self.stream(
|
|
275
|
-
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)
|
|
276
205
|
end
|
|
277
206
|
|
|
278
207
|
# Registers a guardrail for input, output, or both phases.
|
|
@@ -285,22 +214,7 @@ class Riffer::Agent
|
|
|
285
214
|
#--
|
|
286
215
|
#: (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void
|
|
287
216
|
def self.guardrail(phase, with:, **options)
|
|
288
|
-
|
|
289
|
-
raise Riffer::ArgumentError, "Invalid guardrail phase: #{phase}" unless valid_phases.include?(phase)
|
|
290
|
-
raise Riffer::ArgumentError, "Guardrail must be a Riffer::Guardrail subclass" unless with.is_a?(Class) && with <= Riffer::Guardrail
|
|
291
|
-
|
|
292
|
-
@guardrails ||= {before: [], after: []}
|
|
293
|
-
config = {class: with, options: options}
|
|
294
|
-
|
|
295
|
-
case phase
|
|
296
|
-
when :before
|
|
297
|
-
@guardrails[:before] << config
|
|
298
|
-
when :after
|
|
299
|
-
@guardrails[:after] << config
|
|
300
|
-
when :around
|
|
301
|
-
@guardrails[:before] << config
|
|
302
|
-
@guardrails[:after] << config
|
|
303
|
-
end
|
|
217
|
+
config.add_guardrail(phase, klass: with, options: options)
|
|
304
218
|
end
|
|
305
219
|
|
|
306
220
|
# Returns the registered guardrail configs for a given phase.
|
|
@@ -310,120 +224,136 @@ class Riffer::Agent
|
|
|
310
224
|
#--
|
|
311
225
|
#: (Symbol) -> Array[Hash[Symbol, untyped]]
|
|
312
226
|
def self.guardrails_for(phase)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
#
|
|
321
|
-
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
|
|
322
284
|
|
|
323
285
|
# Initializes a new agent.
|
|
324
286
|
#
|
|
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.
|
|
295
|
+
#
|
|
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.
|
|
299
|
+
#
|
|
325
300
|
# Raises Riffer::ArgumentError if the configured model string is invalid
|
|
326
301
|
# (must be "provider/model" format).
|
|
327
302
|
#
|
|
328
303
|
#--
|
|
329
|
-
#: () -> void
|
|
330
|
-
def initialize
|
|
331
|
-
@
|
|
332
|
-
@
|
|
333
|
-
@token_usage = nil
|
|
334
|
-
@interrupted = false
|
|
335
|
-
@model_config = self.class.model
|
|
336
|
-
@instructions_config = self.class.instructions
|
|
337
|
-
|
|
338
|
-
if @model_config.is_a?(Proc)
|
|
339
|
-
@provider_name = nil
|
|
340
|
-
@model_name = nil
|
|
341
|
-
else
|
|
342
|
-
parse_model_string!(@model_config)
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
# Generates a response from the agent.
|
|
347
|
-
#
|
|
348
|
-
#--
|
|
349
|
-
#: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
|
|
350
|
-
def generate(prompt_or_messages, files: nil, context: nil)
|
|
351
|
-
@context = context
|
|
352
|
-
prepare_run
|
|
353
|
-
@structured_output = resolve_structured_output
|
|
354
|
-
initialize_messages(prompt_or_messages, files: files)
|
|
355
|
-
|
|
356
|
-
all_modifications = [] #: Array[Riffer::Guardrails::Modification]
|
|
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 || {})
|
|
357
308
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire
|
|
309
|
+
provider_class, @model_name = resolve_provider_and_model
|
|
310
|
+
@provider = provider_class.new(**@config.provider_options)
|
|
361
311
|
|
|
362
|
-
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# Streams a response from the agent.
|
|
366
|
-
#
|
|
367
|
-
# Raises Riffer::ArgumentError if structured output is configured.
|
|
368
|
-
#
|
|
369
|
-
#--
|
|
370
|
-
#: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
371
|
-
def stream(prompt_or_messages, files: nil, context: nil)
|
|
372
|
-
raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if self.class.structured_output
|
|
312
|
+
@context.skills = resolve_skills(provider_class)
|
|
373
313
|
|
|
374
|
-
@
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
Enumerator.new do |yielder|
|
|
379
|
-
tripwire, modifications = run_before_guardrails
|
|
380
|
-
modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }
|
|
314
|
+
@structured_output = resolve_structured_output
|
|
315
|
+
@tools = resolve_tools
|
|
316
|
+
@tool_runtime = resolve_tool_runtime
|
|
381
317
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
next
|
|
385
|
-
end
|
|
318
|
+
@instruction_message = build_instruction_message
|
|
319
|
+
@skills_message = build_skills_message
|
|
386
320
|
|
|
387
|
-
|
|
388
|
-
|
|
321
|
+
@session = session || Riffer::Agent::Session.new(messages: [@instruction_message, @skills_message].compact)
|
|
322
|
+
@session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
|
|
389
323
|
end
|
|
390
324
|
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
# Raises Riffer::ArgumentError if no block is given.
|
|
394
|
-
#
|
|
395
|
-
#--
|
|
396
|
-
#: () { (Riffer::Messages::Base) -> void } -> self
|
|
397
|
-
def on_message(&block)
|
|
398
|
-
raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
|
|
399
|
-
@message_callbacks << block
|
|
400
|
-
self
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# Generates the instruction system message for this agent.
|
|
325
|
+
# Generates a response from the agent.
|
|
404
326
|
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
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.
|
|
407
334
|
#
|
|
408
|
-
#
|
|
335
|
+
# +files:+ requires +prompt+. Pass files to attach to the new user message.
|
|
409
336
|
#
|
|
410
337
|
#--
|
|
411
|
-
#: (?
|
|
412
|
-
def
|
|
413
|
-
|
|
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)
|
|
414
341
|
end
|
|
415
342
|
|
|
416
|
-
#
|
|
343
|
+
# Streams a response from the agent.
|
|
417
344
|
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
345
|
+
# Runs the inference loop via +Riffer::Agent::Run.stream+, returning an
|
|
346
|
+
# +Enumerator+ of +Riffer::StreamEvents+.
|
|
420
347
|
#
|
|
421
|
-
#
|
|
348
|
+
# Raises Riffer::ArgumentError if structured output is configured.
|
|
349
|
+
#
|
|
350
|
+
# See +#generate+ for prompt/files semantics.
|
|
422
351
|
#
|
|
423
352
|
#--
|
|
424
|
-
#: (?
|
|
425
|
-
def
|
|
426
|
-
|
|
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)
|
|
427
357
|
end
|
|
428
358
|
|
|
429
359
|
# Interrupts the agent loop.
|
|
@@ -431,6 +361,13 @@ class Riffer::Agent
|
|
|
431
361
|
# Call from an +on_message+ callback to cleanly interrupt the loop.
|
|
432
362
|
# Equivalent to <tt>throw :riffer_interrupt, reason</tt>.
|
|
433
363
|
#
|
|
364
|
+
# When +Riffer.config.experimental_history_healing+ is enabled, riffer
|
|
365
|
+
# fills any orphaned +tool_use+ on the way out with a placeholder
|
|
366
|
+
# +Riffer::Messages::Tool+ carrying +error_type: :interrupted+. The
|
|
367
|
+
# filled call_ids are exposed on
|
|
368
|
+
# +Riffer::Agent::Response#healed_tool_call_ids+ (and the streaming
|
|
369
|
+
# +Riffer::StreamEvents::Interrupt+ event).
|
|
370
|
+
#
|
|
434
371
|
#--
|
|
435
372
|
#: (?(String | Symbol)?) -> void
|
|
436
373
|
def interrupt!(reason = nil)
|
|
@@ -440,366 +377,132 @@ class Riffer::Agent
|
|
|
440
377
|
private
|
|
441
378
|
|
|
442
379
|
#--
|
|
443
|
-
#: (
|
|
444
|
-
def
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
execute_pending_tool_calls
|
|
449
|
-
|
|
450
|
-
loop do
|
|
451
|
-
response = call_llm
|
|
452
|
-
step += 1
|
|
453
|
-
|
|
454
|
-
track_token_usage(response.token_usage)
|
|
455
|
-
|
|
456
|
-
processed_response, tripwire, modifications = run_after_guardrails(response)
|
|
457
|
-
all_modifications.concat(modifications)
|
|
458
|
-
|
|
459
|
-
return build_response("", tripwire: tripwire, modifications: all_modifications) if tripwire
|
|
460
|
-
|
|
461
|
-
add_message(processed_response)
|
|
462
|
-
|
|
463
|
-
break unless has_tool_calls?(processed_response)
|
|
464
|
-
|
|
465
|
-
throw :riffer_interrupt, INTERRUPT_MAX_STEPS if step >= self.class.max_steps
|
|
466
|
-
|
|
467
|
-
execute_tool_calls(processed_response)
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
response = extract_final_response
|
|
471
|
-
|
|
472
|
-
return build_response(response&.content || "", modifications: all_modifications, structured_output: validate_structured_output(response))
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# catch returns the thrown value when throw :riffer_interrupt fires;
|
|
476
|
-
# the return above exits on the successful (non-interrupted) path.
|
|
477
|
-
@interrupted = true
|
|
478
|
-
response = extract_final_response
|
|
479
|
-
|
|
480
|
-
build_response(response&.content || "", modifications: all_modifications, interrupted: true, interrupt_reason: reason, structured_output: validate_structured_output(response))
|
|
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)
|
|
481
385
|
end
|
|
482
386
|
|
|
483
387
|
#--
|
|
484
|
-
#: (Riffer::Messages::
|
|
485
|
-
def
|
|
486
|
-
|
|
487
|
-
|
|
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)
|
|
488
393
|
end
|
|
489
394
|
|
|
395
|
+
# Resolves +Config#model+ to a "provider/model" string (calling the Proc
|
|
396
|
+
# form against +@context+), parses it, and looks up the provider class.
|
|
397
|
+
#
|
|
398
|
+
# Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
|
|
399
|
+
# an invalid model string or an unregistered provider.
|
|
400
|
+
#
|
|
490
401
|
#--
|
|
491
|
-
#: (Riffer::
|
|
492
|
-
def
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
@token_usage = @token_usage ? @token_usage + usage : usage
|
|
496
|
-
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)
|
|
497
406
|
|
|
498
|
-
|
|
499
|
-
#: ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
|
|
500
|
-
def initialize_messages(prompt_or_messages, files: nil)
|
|
501
|
-
if prompt_or_messages.is_a?(Array)
|
|
502
|
-
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?
|
|
503
|
-
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead" if files && !files.empty?
|
|
504
|
-
validate_seed_ids!(prompt_or_messages)
|
|
505
|
-
@messages = prompt_or_messages.map { |item| convert_to_message_object(item) }
|
|
506
|
-
elsif @messages.any?
|
|
507
|
-
file_parts = (files || []).map { |f| convert_to_file_part(f) }
|
|
508
|
-
@messages << Riffer::Messages::User.new(prompt_or_messages, files: file_parts)
|
|
509
|
-
else
|
|
510
|
-
@messages = []
|
|
511
|
-
sys = build_instruction_message
|
|
512
|
-
@messages << sys if sys
|
|
513
|
-
skills = build_skills_message
|
|
514
|
-
@messages << skills if skills
|
|
515
|
-
file_parts = (files || []).map { |f| convert_to_file_part(f) }
|
|
516
|
-
@messages << Riffer::Messages::User.new(prompt_or_messages, files: file_parts)
|
|
517
|
-
end
|
|
518
|
-
end
|
|
407
|
+
provider_name, model_name = model_string.split("/", 2)
|
|
519
408
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def validate_seed_ids!(items)
|
|
523
|
-
strategy = Riffer.config.message_id_strategy
|
|
524
|
-
return if strategy == :none
|
|
525
|
-
|
|
526
|
-
items.each_with_index do |item, idx|
|
|
527
|
-
raw_id = case item
|
|
528
|
-
when Hash then item[:id]
|
|
529
|
-
when Riffer::Messages::Base then item.id
|
|
530
|
-
else next # type errors surface later via convert_to_message_object
|
|
531
|
-
end
|
|
532
|
-
next unless raw_id.nil?
|
|
533
|
-
raise Riffer::ArgumentError,
|
|
534
|
-
"seeded message at index #{idx} is missing :id (required when Riffer.config.message_id_strategy = #{strategy.inspect})"
|
|
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}"
|
|
535
411
|
end
|
|
536
|
-
end
|
|
537
412
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def build_instruction_message(context = @context)
|
|
541
|
-
content = generate_instructions(context)
|
|
542
|
-
return nil if content.nil? || content.empty?
|
|
543
|
-
Riffer::Messages::System.new(content)
|
|
544
|
-
end
|
|
413
|
+
provider_class = Riffer::Providers::Repository.find(provider_name)
|
|
414
|
+
raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
|
|
545
415
|
|
|
546
|
-
|
|
547
|
-
#: (?Riffer::Skills::Context?) -> Riffer::Messages::System?
|
|
548
|
-
def build_skills_message(skills_state = @skills_state)
|
|
549
|
-
content = skills_state&.system_prompt
|
|
550
|
-
return nil if content.nil? || content.empty?
|
|
551
|
-
Riffer::Messages::System.new(content)
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
#--
|
|
555
|
-
#: () -> Integer
|
|
556
|
-
def count_assistant_messages
|
|
557
|
-
@messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
416
|
+
[provider_class, model_name]
|
|
558
417
|
end
|
|
559
418
|
|
|
419
|
+
# Resolves the skills backend, lists skills, and selects an adapter.
|
|
420
|
+
# Returns nil if skills are unconfigured or the backend is empty.
|
|
421
|
+
#
|
|
560
422
|
#--
|
|
561
|
-
#: (
|
|
562
|
-
def
|
|
563
|
-
|
|
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
|
|
564
427
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
completed = catch(:riffer_interrupt) do
|
|
570
|
-
execute_pending_tool_calls
|
|
571
|
-
|
|
572
|
-
loop do
|
|
573
|
-
accumulated_content = ""
|
|
574
|
-
accumulated_tool_calls = []
|
|
575
|
-
accumulated_token_usage = nil
|
|
576
|
-
current_tool_call = nil
|
|
577
|
-
|
|
578
|
-
call_llm_stream.each do |event|
|
|
579
|
-
yielder << event
|
|
580
|
-
|
|
581
|
-
case event
|
|
582
|
-
when Riffer::StreamEvents::TextDelta
|
|
583
|
-
accumulated_content += event.content
|
|
584
|
-
when Riffer::StreamEvents::TextDone
|
|
585
|
-
accumulated_content = event.content
|
|
586
|
-
when Riffer::StreamEvents::ToolCallDelta
|
|
587
|
-
current_tool_call ||= {item_id: event.item_id, name: event.name, arguments: ""}
|
|
588
|
-
current_tool_call[:arguments] += event.arguments_delta
|
|
589
|
-
current_tool_call[:name] ||= event.name
|
|
590
|
-
when Riffer::StreamEvents::ToolCallDone
|
|
591
|
-
accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
592
|
-
call_id: event.call_id,
|
|
593
|
-
name: event.name,
|
|
594
|
-
arguments: event.arguments
|
|
595
|
-
)
|
|
596
|
-
current_tool_call = nil
|
|
597
|
-
when Riffer::StreamEvents::TokenUsageDone
|
|
598
|
-
accumulated_token_usage = event.token_usage
|
|
599
|
-
end
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
response = Riffer::Messages::Assistant.new(
|
|
603
|
-
accumulated_content,
|
|
604
|
-
tool_calls: accumulated_tool_calls,
|
|
605
|
-
token_usage: accumulated_token_usage
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
track_token_usage(accumulated_token_usage)
|
|
609
|
-
step += 1
|
|
610
|
-
|
|
611
|
-
processed_response, tripwire, modifications = run_after_guardrails(response)
|
|
612
|
-
modifications.each { |m| yielder << Riffer::StreamEvents::GuardrailModification.new(m) }
|
|
613
|
-
|
|
614
|
-
if tripwire
|
|
615
|
-
yielder << Riffer::StreamEvents::GuardrailTripwire.new(tripwire)
|
|
616
|
-
break
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
add_message(processed_response)
|
|
620
|
-
|
|
621
|
-
break unless has_tool_calls?(processed_response)
|
|
622
|
-
|
|
623
|
-
throw :riffer_interrupt, INTERRUPT_MAX_STEPS if step >= self.class.max_steps
|
|
624
|
-
|
|
625
|
-
execute_tool_calls(processed_response)
|
|
626
|
-
end
|
|
627
|
-
:completed
|
|
628
|
-
end
|
|
428
|
+
backend = skills_config.backend || Riffer.config.skills.default_backend
|
|
429
|
+
return nil unless backend
|
|
629
430
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
yielder << Riffer::StreamEvents::Interrupt.new(reason: completed)
|
|
633
|
-
end
|
|
634
|
-
end
|
|
431
|
+
backend = Riffer::Helpers::CallOrValue.resolve(backend, context: @context)
|
|
432
|
+
return nil if backend.list_skills.empty?
|
|
635
433
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
provider_instance.generate_text(
|
|
640
|
-
messages: @messages,
|
|
641
|
-
model: @model_name,
|
|
642
|
-
tools: resolved_tools,
|
|
643
|
-
**merged_model_options
|
|
644
|
-
)
|
|
645
|
-
end
|
|
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
|
|
646
437
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
messages: @messages,
|
|
652
|
-
model: @model_name,
|
|
653
|
-
tools: resolved_tools,
|
|
654
|
-
**merged_model_options
|
|
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)
|
|
655
442
|
)
|
|
656
|
-
end
|
|
657
443
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
end
|
|
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) }
|
|
447
|
+
end
|
|
663
448
|
|
|
664
|
-
|
|
665
|
-
#: (Riffer::Messages::Assistant) -> bool
|
|
666
|
-
def has_tool_calls?(response)
|
|
667
|
-
response.is_a?(Riffer::Messages::Assistant) && !response.tool_calls.empty?
|
|
449
|
+
skills_context
|
|
668
450
|
end
|
|
669
451
|
|
|
670
452
|
#--
|
|
671
|
-
#: (
|
|
672
|
-
def
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
results.each do |tool_call, result|
|
|
677
|
-
add_message(Riffer::Messages::Tool.new(
|
|
678
|
-
result.content,
|
|
679
|
-
tool_call_id: tool_call.call_id,
|
|
680
|
-
name: tool_call.name,
|
|
681
|
-
error: result.error_message,
|
|
682
|
-
error_type: result.error_type
|
|
683
|
-
))
|
|
684
|
-
end
|
|
453
|
+
#: () -> Riffer::Agent::StructuredOutput?
|
|
454
|
+
def resolve_structured_output
|
|
455
|
+
params = @config.structured_output
|
|
456
|
+
params ? Riffer::Agent::StructuredOutput.new(params) : nil
|
|
685
457
|
end
|
|
686
458
|
|
|
687
|
-
#
|
|
459
|
+
# Resolves the full tool catalog for the agent:
|
|
688
460
|
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
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.
|
|
467
|
+
#
|
|
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).
|
|
694
471
|
#
|
|
695
472
|
#--
|
|
696
|
-
#: () ->
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
# returns immediately when there is nothing pending.
|
|
700
|
-
def execute_pending_tool_calls
|
|
701
|
-
pending = pending_tool_calls
|
|
702
|
-
return if pending.empty?
|
|
703
|
-
|
|
704
|
-
runtime = resolve_tool_runtime
|
|
705
|
-
results = runtime.execute(pending, tools: resolved_tools, context: @context)
|
|
706
|
-
|
|
707
|
-
results.each do |tool_call, result|
|
|
708
|
-
add_message(Riffer::Messages::Tool.new(
|
|
709
|
-
result.content,
|
|
710
|
-
tool_call_id: tool_call.call_id,
|
|
711
|
-
name: tool_call.name,
|
|
712
|
-
error: result.error_message,
|
|
713
|
-
error_type: result.error_type
|
|
714
|
-
))
|
|
715
|
-
end
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
def pending_tool_calls
|
|
719
|
-
last_assistant_idx = @messages.rindex { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
720
|
-
return [] unless last_assistant_idx
|
|
721
|
-
|
|
722
|
-
assistant = @messages[last_assistant_idx]
|
|
723
|
-
return [] if assistant.tool_calls.empty?
|
|
724
|
-
|
|
725
|
-
executed_ids = @messages[(last_assistant_idx + 1)..].select { |m|
|
|
726
|
-
m.is_a?(Riffer::Messages::Tool)
|
|
727
|
-
}.map(&:tool_call_id)
|
|
728
|
-
|
|
729
|
-
assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }
|
|
730
|
-
end
|
|
473
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
474
|
+
def resolve_tools
|
|
475
|
+
tools = Riffer::Helpers::CallOrValue.resolve(@config.tools_config, context: @context, default: [])
|
|
731
476
|
|
|
732
|
-
|
|
733
|
-
#: () -> void
|
|
734
|
-
def prepare_run
|
|
735
|
-
@resolved_tools = nil
|
|
736
|
-
@resolved_tool_runtime = nil
|
|
737
|
-
clear_resolved_model
|
|
738
|
-
@interrupted = false
|
|
739
|
-
resolve_model
|
|
740
|
-
@skills_state = resolve_skills
|
|
741
|
-
@context = (@context || {}).merge(skills: @skills_state) if @skills_state
|
|
742
|
-
end
|
|
477
|
+
skills_config = @config.skills_config
|
|
743
478
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def parse_model_string!(model_string)
|
|
747
|
-
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless model_string.is_a?(String)
|
|
748
|
-
provider_name, model_name = model_string.split("/", 2)
|
|
749
|
-
raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
|
|
750
|
-
@provider_name = provider_name
|
|
751
|
-
@model_name = model_name
|
|
752
|
-
end
|
|
479
|
+
if skills_config
|
|
480
|
+
skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
|
|
753
481
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
@resolved_model = nil
|
|
758
|
-
@provider_instance = nil if @model_config.is_a?(Proc)
|
|
759
|
-
end
|
|
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}"
|
|
484
|
+
end
|
|
760
485
|
|
|
761
|
-
|
|
762
|
-
#: (?Hash[Symbol, untyped]?) -> String?
|
|
763
|
-
def generate_instructions(context = @context)
|
|
764
|
-
if @instructions_config.is_a?(Proc)
|
|
765
|
-
(@instructions_config.arity == 0) ? @instructions_config.call : @instructions_config.call(context)
|
|
766
|
-
else
|
|
767
|
-
@instructions_config
|
|
486
|
+
tools += [skill_activate_tool_class]
|
|
768
487
|
end
|
|
769
|
-
end
|
|
770
488
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
def resolve_model
|
|
776
|
-
@resolved_model ||= if @model_config.is_a?(Proc)
|
|
777
|
-
model_string = (@model_config.arity == 0) ? @model_config.call : @model_config.call(@context)
|
|
778
|
-
parse_model_string!(model_string)
|
|
779
|
-
model_string
|
|
780
|
-
else
|
|
781
|
-
@model_config
|
|
782
|
-
end
|
|
489
|
+
tools += resolve_mcp_tool_classes
|
|
490
|
+
assert_distinct_tool_names!(tools)
|
|
491
|
+
tools.each(&:validate_as_tool!)
|
|
492
|
+
tools
|
|
783
493
|
end
|
|
784
494
|
|
|
785
495
|
#--
|
|
786
|
-
#: () ->
|
|
787
|
-
def
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if config.nil?
|
|
791
|
-
[]
|
|
792
|
-
elsif config.is_a?(Proc)
|
|
793
|
-
(config.arity == 0) ? config.call : config.call(@context)
|
|
794
|
-
else
|
|
795
|
-
config
|
|
796
|
-
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
|
|
797
500
|
end
|
|
798
501
|
|
|
799
502
|
#--
|
|
800
503
|
#: () -> Array[singleton(Riffer::Tool)]
|
|
801
504
|
def resolve_mcp_tool_classes
|
|
802
|
-
configs =
|
|
505
|
+
configs = @config.mcp_configs
|
|
803
506
|
return [] if configs.empty?
|
|
804
507
|
|
|
805
508
|
cred = Riffer.config.mcp.credentials
|
|
@@ -814,7 +517,7 @@ class Riffer::Agent
|
|
|
814
517
|
#
|
|
815
518
|
#: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
816
519
|
def gather_mcp_registrations_with_tags(configs)
|
|
817
|
-
by_reg = {}
|
|
520
|
+
by_reg = {} #: Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
818
521
|
configs.each do |cfg|
|
|
819
522
|
Riffer::Mcp::Registry.find_by_tags(cfg[:tags]).each do |reg|
|
|
820
523
|
(by_reg[reg] ||= []).concat(cfg[:tags] & reg.manifest.tags)
|
|
@@ -823,7 +526,7 @@ class Riffer::Agent
|
|
|
823
526
|
by_reg
|
|
824
527
|
end
|
|
825
528
|
|
|
826
|
-
#: (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)]
|
|
827
530
|
def mcp_tools_for_registration(reg, matched_tags, cred, ctx)
|
|
828
531
|
return reg.tools unless cred
|
|
829
532
|
return [] if cred.call(manifest: reg.manifest, matched_tags: matched_tags, context: ctx).nil?
|
|
@@ -841,145 +544,4 @@ class Riffer::Agent
|
|
|
841
544
|
|
|
842
545
|
raise Riffer::ArgumentError, "Duplicate tool names: #{dupes.sort.join(", ")}"
|
|
843
546
|
end
|
|
844
|
-
|
|
845
|
-
#: () -> Array[singleton(Riffer::Tool)]
|
|
846
|
-
def resolved_tools
|
|
847
|
-
@resolved_tools ||= begin
|
|
848
|
-
tools = self.class.resolved_tool_classes(context: @context) + resolve_mcp_tool_classes
|
|
849
|
-
assert_distinct_tool_names!(tools)
|
|
850
|
-
tools.each(&:validate_as_tool!)
|
|
851
|
-
tools
|
|
852
|
-
end
|
|
853
|
-
end
|
|
854
|
-
|
|
855
|
-
#--
|
|
856
|
-
#: () -> Riffer::ToolRuntime
|
|
857
|
-
def resolve_tool_runtime
|
|
858
|
-
@resolved_tool_runtime ||= begin
|
|
859
|
-
config = self.class.tool_runtime || Riffer.config.tool_runtime
|
|
860
|
-
|
|
861
|
-
runtime = if config.is_a?(Proc)
|
|
862
|
-
(config.arity == 0) ? config.call : config.call(@context)
|
|
863
|
-
else
|
|
864
|
-
config
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
case runtime
|
|
868
|
-
when Class then runtime.new
|
|
869
|
-
when Riffer::ToolRuntime then runtime
|
|
870
|
-
else raise Riffer::ArgumentError, "Invalid tool_runtime: #{runtime.inspect}"
|
|
871
|
-
end
|
|
872
|
-
end
|
|
873
|
-
end
|
|
874
|
-
|
|
875
|
-
# Resolves the skills backend, lists skills, and selects an adapter.
|
|
876
|
-
#
|
|
877
|
-
# Returns nil if skills are not configured or empty.
|
|
878
|
-
# Does not mutate instance state — callers are responsible for
|
|
879
|
-
# assigning the returned context.
|
|
880
|
-
#
|
|
881
|
-
#--
|
|
882
|
-
#: (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
|
|
883
|
-
def resolve_skills(context = @context)
|
|
884
|
-
return nil unless self.class.skills
|
|
885
|
-
|
|
886
|
-
backend = self.class.skills.backend || Riffer.config.skills.default_backend
|
|
887
|
-
return nil unless backend
|
|
888
|
-
|
|
889
|
-
backend = backend.is_a?(Proc) ? backend.call(context) : backend
|
|
890
|
-
skills_list = backend.list_skills
|
|
891
|
-
return nil if skills_list.empty?
|
|
892
|
-
|
|
893
|
-
skills = skills_list.to_h { |s| [s.name, s] }
|
|
894
|
-
adapter_class = self.class.skills.adapter || provider_class.skills_adapter(@model_name)
|
|
895
|
-
skill_activate_tool_class = self.class.skills.activate_tool || Riffer.config.skills.default_activate_tool
|
|
896
|
-
|
|
897
|
-
skills_context = Riffer::Skills::Context.new(
|
|
898
|
-
backend: backend,
|
|
899
|
-
skills: skills,
|
|
900
|
-
adapter: adapter_class.new(skill_activate_tool: skill_activate_tool_class)
|
|
901
|
-
)
|
|
902
|
-
ctx = (context || {}).merge(skills: skills_context)
|
|
903
|
-
|
|
904
|
-
activate = self.class.skills.activate
|
|
905
|
-
if activate
|
|
906
|
-
names = activate.is_a?(Proc) ? activate.call(ctx) : Array(activate)
|
|
907
|
-
names.each { |name| skills_context.activate(name) }
|
|
908
|
-
end
|
|
909
|
-
|
|
910
|
-
skills_context
|
|
911
|
-
end
|
|
912
|
-
|
|
913
|
-
#--
|
|
914
|
-
#: () -> singleton(Riffer::Providers::Base)
|
|
915
|
-
def provider_class
|
|
916
|
-
klass = Riffer::Providers::Repository.find(@provider_name)
|
|
917
|
-
raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless klass
|
|
918
|
-
klass
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
#--
|
|
922
|
-
#: () -> Riffer::Messages::Assistant?
|
|
923
|
-
def extract_final_response
|
|
924
|
-
# TODO: Replace with rfind when minimum Ruby is 4.0+
|
|
925
|
-
# rubocop:disable Style/ReverseFind
|
|
926
|
-
@messages.reverse.find { |msg| msg.is_a?(Riffer::Messages::Assistant) } #: Riffer::Messages::Assistant?
|
|
927
|
-
# rubocop:enable Style/ReverseFind
|
|
928
|
-
end
|
|
929
|
-
|
|
930
|
-
#--
|
|
931
|
-
#: () -> [Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
|
|
932
|
-
def run_before_guardrails
|
|
933
|
-
guardrails = self.class.guardrails_for(:before)
|
|
934
|
-
return [nil, []] if guardrails.empty?
|
|
935
|
-
|
|
936
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :before, context: @context)
|
|
937
|
-
processed_messages, tripwire, modifications = runner.run(@messages)
|
|
938
|
-
@messages = processed_messages unless tripwire
|
|
939
|
-
[tripwire, modifications]
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
#--
|
|
943
|
-
#: (Riffer::Messages::Assistant) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
|
|
944
|
-
def run_after_guardrails(response)
|
|
945
|
-
guardrails = self.class.guardrails_for(:after)
|
|
946
|
-
return [response, nil, []] if guardrails.empty?
|
|
947
|
-
|
|
948
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :after, context: @context)
|
|
949
|
-
processed_response, tripwire, modifications = runner.run(response, messages: @messages)
|
|
950
|
-
|
|
951
|
-
response_index = @messages.length
|
|
952
|
-
modifications.each { |m| m.message_indices.map! { response_index } }
|
|
953
|
-
|
|
954
|
-
[processed_response, tripwire, modifications]
|
|
955
|
-
end
|
|
956
|
-
|
|
957
|
-
#--
|
|
958
|
-
#: (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
|
|
959
|
-
def validate_structured_output(response)
|
|
960
|
-
return unless response&.structured_output? && @structured_output
|
|
961
|
-
|
|
962
|
-
@structured_output.parse_and_validate(response.content).object
|
|
963
|
-
end
|
|
964
|
-
|
|
965
|
-
#--
|
|
966
|
-
#: () -> Riffer::StructuredOutput?
|
|
967
|
-
def resolve_structured_output
|
|
968
|
-
params = self.class.structured_output
|
|
969
|
-
params ? Riffer::StructuredOutput.new(params) : nil
|
|
970
|
-
end
|
|
971
|
-
|
|
972
|
-
#--
|
|
973
|
-
#: () -> Hash[Symbol, untyped]
|
|
974
|
-
def merged_model_options
|
|
975
|
-
opts = self.class.model_options.dup
|
|
976
|
-
opts[:structured_output] = @structured_output if @structured_output
|
|
977
|
-
opts
|
|
978
|
-
end
|
|
979
|
-
|
|
980
|
-
#--
|
|
981
|
-
#: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
|
|
982
|
-
def build_response(content, tripwire: nil, modifications: [], interrupted: false, interrupt_reason: nil, structured_output: nil)
|
|
983
|
-
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)
|
|
984
|
-
end
|
|
985
547
|
end
|