riffer 0.45.0 → 0.46.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +25 -0
- data/docs/AGENTS.md +13 -0
- data/docs/AGENT_LIFECYCLE.md +47 -14
- data/docs/AGENT_LOOP.md +8 -9
- data/docs/CONFIGURATION.md +24 -17
- data/docs/GUARDRAILS.md +4 -4
- data/docs/MESSAGES.md +4 -3
- data/docs/STREAM_EVENTS.md +1 -1
- data/docs/TOOLS.md +2 -0
- data/docs/TOOL_ADVANCED.md +2 -0
- data/docs/TRACING.md +3 -1
- data/docs/providers/MOCK_PROVIDER.md +18 -11
- data/lib/riffer/agent/outcome.rb +52 -0
- data/lib/riffer/agent/response.rb +12 -33
- data/lib/riffer/agent/run.rb +55 -17
- data/lib/riffer/agent/session.rb +2 -0
- data/lib/riffer/agent.rb +5 -4
- data/lib/riffer/messages/assistant.rb +24 -2
- data/lib/riffer/messages/base.rb +11 -19
- data/lib/riffer/params/param.rb +2 -0
- data/lib/riffer/params.rb +30 -6
- data/lib/riffer/providers/anthropic.rb +5 -0
- data/lib/riffer/providers/base.rb +1 -0
- data/lib/riffer/runner/fibers.rb +12 -8
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/async.rbs +4 -0
- data/sig/generated/riffer/agent/outcome.rbs +41 -0
- data/sig/generated/riffer/agent/response.rbs +11 -26
- data/sig/generated/riffer/agent/run.rbs +20 -9
- data/sig/generated/riffer/agent.rbs +7 -6
- data/sig/generated/riffer/messages/assistant.rbs +14 -2
- data/sig/generated/riffer/params.rbs +16 -2
- data/sig/generated/riffer/runner/fibers.rbs +2 -0
- metadata +3 -1
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Wraps an agent generation response.
|
|
5
|
-
# +content+ is empty and +tripwire+ carries the
|
|
4
|
+
# Wraps an agent generation response. +outcome+ says how the run ended; when a
|
|
5
|
+
# guardrail blocks execution, +content+ is empty and +tripwire+ carries the
|
|
6
|
+
# block details.
|
|
6
7
|
#
|
|
7
8
|
# response = agent.generate("Hello")
|
|
8
|
-
# if response.
|
|
9
|
-
# puts "Blocked: #{response.tripwire.reason}"
|
|
10
|
-
# else
|
|
9
|
+
# if response.outcome.success?
|
|
11
10
|
# puts response.content
|
|
11
|
+
# else
|
|
12
|
+
# puts "#{response.outcome.reason}: #{response.outcome.detail}"
|
|
12
13
|
# end
|
|
13
14
|
class Riffer::Agent::Response
|
|
14
|
-
# @rbs @interrupted: bool
|
|
15
|
-
|
|
16
15
|
# The response content.
|
|
17
16
|
attr_reader :content #: String
|
|
18
17
|
|
|
18
|
+
# How the run ended.
|
|
19
|
+
attr_reader :outcome #: Riffer::Agent::Outcome
|
|
20
|
+
|
|
19
21
|
# The tripwire if execution was blocked.
|
|
20
22
|
attr_reader :tripwire #: Riffer::Guardrails::Tripwire?
|
|
21
23
|
|
|
22
24
|
# The modifications made by guardrails during processing.
|
|
23
25
|
attr_reader :modifications #: Array[Riffer::Guardrails::Modification]
|
|
24
26
|
|
|
25
|
-
# The reason provided with the interrupt, if any.
|
|
26
|
-
attr_reader :interrupt_reason #: (String | Symbol)?
|
|
27
|
-
|
|
28
27
|
# The parsed structured output, if structured output was configured.
|
|
29
28
|
attr_reader :structured_output #: Hash[Symbol, untyped]?
|
|
30
29
|
|
|
@@ -45,10 +44,9 @@ class Riffer::Agent::Response
|
|
|
45
44
|
#--
|
|
46
45
|
#: (
|
|
47
46
|
# String,
|
|
47
|
+
# outcome: Riffer::Agent::Outcome,
|
|
48
48
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
49
49
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
50
|
-
# ?interrupted: bool,
|
|
51
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
52
50
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
53
51
|
# ?messages: Array[Riffer::Messages::Base],
|
|
54
52
|
# ?healed_tool_call_ids: Array[String],
|
|
@@ -57,10 +55,9 @@ class Riffer::Agent::Response
|
|
|
57
55
|
# ) -> void
|
|
58
56
|
def initialize(
|
|
59
57
|
content,
|
|
58
|
+
outcome:,
|
|
60
59
|
tripwire: nil,
|
|
61
60
|
modifications: [],
|
|
62
|
-
interrupted: false,
|
|
63
|
-
interrupt_reason: nil,
|
|
64
61
|
structured_output: nil,
|
|
65
62
|
messages: [],
|
|
66
63
|
healed_tool_call_ids: [],
|
|
@@ -68,10 +65,9 @@ class Riffer::Agent::Response
|
|
|
68
65
|
steps: 0
|
|
69
66
|
)
|
|
70
67
|
@content = content
|
|
68
|
+
@outcome = outcome
|
|
71
69
|
@tripwire = tripwire
|
|
72
70
|
@modifications = modifications
|
|
73
|
-
@interrupted = interrupted
|
|
74
|
-
@interrupt_reason = interrupt_reason
|
|
75
71
|
@structured_output = structured_output
|
|
76
72
|
@messages = messages
|
|
77
73
|
@healed_tool_call_ids = healed_tool_call_ids
|
|
@@ -79,14 +75,6 @@ class Riffer::Agent::Response
|
|
|
79
75
|
@steps = steps
|
|
80
76
|
end
|
|
81
77
|
|
|
82
|
-
# Returns true if the response was blocked by a guardrail.
|
|
83
|
-
#
|
|
84
|
-
#--
|
|
85
|
-
#: () -> bool
|
|
86
|
-
def blocked?
|
|
87
|
-
!tripwire.nil?
|
|
88
|
-
end
|
|
89
|
-
|
|
90
78
|
# Returns true if any guardrail modified data during processing.
|
|
91
79
|
#
|
|
92
80
|
#--
|
|
@@ -94,13 +82,4 @@ class Riffer::Agent::Response
|
|
|
94
82
|
def modified?
|
|
95
83
|
modifications.any?
|
|
96
84
|
end
|
|
97
|
-
|
|
98
|
-
# Returns true if the agent loop was interrupted by a callback
|
|
99
|
-
# via <tt>throw :riffer_interrupt</tt>.
|
|
100
|
-
#
|
|
101
|
-
#--
|
|
102
|
-
#: () -> bool
|
|
103
|
-
def interrupted?
|
|
104
|
-
@interrupted
|
|
105
|
-
end
|
|
106
85
|
end
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Riffer::Agent::Run
|
|
|
20
20
|
# for prompt/files semantics.
|
|
21
21
|
#
|
|
22
22
|
#--
|
|
23
|
-
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base,
|
|
23
|
+
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
|
|
24
24
|
def stream(agent:, prompt: nil, files: nil, tags: {})
|
|
25
25
|
append_user_message(agent, prompt, files: files)
|
|
26
26
|
# The enumerator body runs in its own fiber, where the fiber-local OTEL
|
|
@@ -155,6 +155,7 @@ module Riffer::Agent::Run
|
|
|
155
155
|
accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
156
156
|
accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
|
|
157
157
|
accumulated_finish_reason = nil #: Symbol?
|
|
158
|
+
accumulated_finish_reason_raw = nil #: String?
|
|
158
159
|
|
|
159
160
|
call_llm_stream(agent, tags).each do |event|
|
|
160
161
|
stream_yielder << event
|
|
@@ -178,6 +179,7 @@ module Riffer::Agent::Run
|
|
|
178
179
|
accumulated_token_usage = event.token_usage
|
|
179
180
|
when Riffer::StreamEvents::FinishReasonDone
|
|
180
181
|
accumulated_finish_reason = event.finish_reason
|
|
182
|
+
accumulated_finish_reason_raw = event.raw_finish_reason
|
|
181
183
|
end
|
|
182
184
|
end
|
|
183
185
|
|
|
@@ -186,6 +188,7 @@ module Riffer::Agent::Run
|
|
|
186
188
|
tool_calls: accumulated_tool_calls,
|
|
187
189
|
token_usage: accumulated_token_usage,
|
|
188
190
|
finish_reason: accumulated_finish_reason,
|
|
191
|
+
finish_reason_raw: accumulated_finish_reason_raw,
|
|
189
192
|
)
|
|
190
193
|
end
|
|
191
194
|
|
|
@@ -207,6 +210,7 @@ module Riffer::Agent::Run
|
|
|
207
210
|
build_response(
|
|
208
211
|
agent,
|
|
209
212
|
"",
|
|
213
|
+
outcome: Riffer::Agent::Outcome.new(reason: :guardrail_blocked, detail: tripwire.reason),
|
|
210
214
|
tripwire: tripwire,
|
|
211
215
|
modifications: all_modifications,
|
|
212
216
|
token_usage: token_usage,
|
|
@@ -215,18 +219,41 @@ module Riffer::Agent::Run
|
|
|
215
219
|
end
|
|
216
220
|
|
|
217
221
|
#--
|
|
218
|
-
#: (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
|
|
219
|
-
def final_response(agent, all_modifications, **extra)
|
|
220
|
-
|
|
222
|
+
#: (Riffer::Agent, Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, **untyped) -> Riffer::Agent::Response
|
|
223
|
+
def final_response(agent, all_modifications, interrupted: false, interrupt_reason: nil, **extra)
|
|
224
|
+
message = agent.session.final_assistant_message
|
|
225
|
+
result = agent.structured_output && structured_output_result(agent, message)
|
|
221
226
|
build_response(
|
|
222
227
|
agent,
|
|
223
|
-
|
|
228
|
+
message&.content || "",
|
|
229
|
+
outcome: final_outcome(message, result, interrupted: interrupted, interrupt_reason: interrupt_reason),
|
|
224
230
|
modifications: all_modifications,
|
|
225
|
-
structured_output:
|
|
231
|
+
structured_output: result&.object,
|
|
226
232
|
**extra,
|
|
227
233
|
)
|
|
228
234
|
end
|
|
229
235
|
|
|
236
|
+
# Checked in the order things happened. The loop being stopped (max_steps or
|
|
237
|
+
# an interrupt) beats the provider's finish reason, which beats riffer's own
|
|
238
|
+
# validation of the content. A truncated response that also fails the schema
|
|
239
|
+
# therefore reports :length, not :invalid_structured_output.
|
|
240
|
+
#--
|
|
241
|
+
#: (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
|
|
242
|
+
def final_outcome(message, result, interrupted:, interrupt_reason:)
|
|
243
|
+
finish_reason = message&.finish_reason
|
|
244
|
+
if interrupted && interrupt_reason == Riffer::Agent::INTERRUPT_MAX_STEPS
|
|
245
|
+
Riffer::Agent::Outcome.new(reason: :max_steps)
|
|
246
|
+
elsif interrupted
|
|
247
|
+
Riffer::Agent::Outcome.new(reason: :interrupted, detail: interrupt_reason&.to_s)
|
|
248
|
+
elsif finish_reason && !Riffer::Agent::Outcome::NORMAL_FINISH_REASONS.include?(finish_reason)
|
|
249
|
+
Riffer::Agent::Outcome.new(reason: finish_reason, detail: message&.finish_reason_raw)
|
|
250
|
+
elsif result&.failure?
|
|
251
|
+
Riffer::Agent::Outcome.new(reason: :invalid_structured_output, detail: result.error)
|
|
252
|
+
else
|
|
253
|
+
Riffer::Agent::Outcome.new(reason: :completed)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
230
257
|
#--
|
|
231
258
|
#: (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
232
259
|
def call_llm(agent, tags = {})
|
|
@@ -327,11 +354,11 @@ module Riffer::Agent::Run
|
|
|
327
354
|
end
|
|
328
355
|
|
|
329
356
|
#--
|
|
330
|
-
#: (Riffer::Agent, Riffer::Messages::Assistant?) ->
|
|
331
|
-
def
|
|
332
|
-
return unless
|
|
357
|
+
#: (Riffer::Agent, Riffer::Messages::Assistant?) -> Riffer::Agent::StructuredOutput::Result?
|
|
358
|
+
def structured_output_result(agent, message)
|
|
359
|
+
return unless message
|
|
333
360
|
|
|
334
|
-
agent.structured_output
|
|
361
|
+
agent.structured_output&.parse_and_validate(message.content)
|
|
335
362
|
end
|
|
336
363
|
|
|
337
364
|
#--
|
|
@@ -358,10 +385,9 @@ module Riffer::Agent::Run
|
|
|
358
385
|
#: (
|
|
359
386
|
# Riffer::Agent,
|
|
360
387
|
# String,
|
|
388
|
+
# outcome: Riffer::Agent::Outcome,
|
|
361
389
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
362
390
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
363
|
-
# ?interrupted: bool,
|
|
364
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
365
391
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
366
392
|
# ?healed_tool_call_ids: Array[String],
|
|
367
393
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
@@ -370,10 +396,9 @@ module Riffer::Agent::Run
|
|
|
370
396
|
def build_response(
|
|
371
397
|
agent,
|
|
372
398
|
content,
|
|
399
|
+
outcome:,
|
|
373
400
|
tripwire: nil,
|
|
374
401
|
modifications: [],
|
|
375
|
-
interrupted: false,
|
|
376
|
-
interrupt_reason: nil,
|
|
377
402
|
structured_output: nil,
|
|
378
403
|
healed_tool_call_ids: [],
|
|
379
404
|
token_usage: nil,
|
|
@@ -382,10 +407,9 @@ module Riffer::Agent::Run
|
|
|
382
407
|
messages = agent.session.messages
|
|
383
408
|
Riffer::Agent::Response.new(
|
|
384
409
|
content,
|
|
410
|
+
outcome: outcome,
|
|
385
411
|
tripwire: tripwire,
|
|
386
412
|
modifications: modifications,
|
|
387
|
-
interrupted: interrupted,
|
|
388
|
-
interrupt_reason: interrupt_reason,
|
|
389
413
|
structured_output: structured_output,
|
|
390
414
|
messages: messages.frozen? ? messages : messages.dup.freeze,
|
|
391
415
|
healed_tool_call_ids: healed_tool_call_ids,
|
|
@@ -460,7 +484,12 @@ module Riffer::Agent::Run
|
|
|
460
484
|
span.set_attribute("riffer.steps", response.steps)
|
|
461
485
|
Riffer::Tracing.record_usage(span, response.token_usage)
|
|
462
486
|
|
|
463
|
-
|
|
487
|
+
outcome = response.outcome
|
|
488
|
+
span.set_attribute("riffer.outcome.reason", outcome.reason.to_s)
|
|
489
|
+
detail = outcome.detail
|
|
490
|
+
span.set_attribute("riffer.outcome.detail", detail) if detail
|
|
491
|
+
interrupt_reason = interrupt_reason_attribute(outcome)
|
|
492
|
+
span.set_attribute("riffer.interrupt.reason", interrupt_reason) if interrupt_reason
|
|
464
493
|
|
|
465
494
|
tripwire = response.tripwire
|
|
466
495
|
return unless tripwire
|
|
@@ -470,4 +499,13 @@ module Riffer::Agent::Run
|
|
|
470
499
|
span.set_attribute("riffer.tripwire.reason", tripwire.reason)
|
|
471
500
|
span.set_attribute("riffer.tripwire.phase", tripwire.phase.to_s)
|
|
472
501
|
end
|
|
502
|
+
|
|
503
|
+
#--
|
|
504
|
+
#: (Riffer::Agent::Outcome) -> String?
|
|
505
|
+
def interrupt_reason_attribute(outcome)
|
|
506
|
+
case outcome.reason
|
|
507
|
+
when :max_steps then Riffer::Agent::INTERRUPT_MAX_STEPS.to_s
|
|
508
|
+
when :interrupted then outcome.detail
|
|
509
|
+
end
|
|
510
|
+
end
|
|
473
511
|
end
|
data/lib/riffer/agent/session.rb
CHANGED
|
@@ -203,6 +203,8 @@ class Riffer::Agent::Session
|
|
|
203
203
|
tool_calls: attrs.fetch(:tool_calls, old.tool_calls),
|
|
204
204
|
token_usage: attrs.fetch(:token_usage, old.token_usage),
|
|
205
205
|
structured_output: attrs.fetch(:structured_output, old.structured_output),
|
|
206
|
+
finish_reason: attrs.fetch(:finish_reason, old.finish_reason),
|
|
207
|
+
finish_reason_raw: attrs.fetch(:finish_reason_raw, old.finish_reason_raw),
|
|
206
208
|
)
|
|
207
209
|
when Riffer::Messages::Tool
|
|
208
210
|
Riffer::Messages::Tool.new(
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -155,7 +155,7 @@ class Riffer::Agent
|
|
|
155
155
|
|
|
156
156
|
# Streams a response using a new agent instance.
|
|
157
157
|
#--
|
|
158
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base,
|
|
158
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
|
|
159
159
|
def self.stream(prompt = nil, files: nil, context: nil, tags: {})
|
|
160
160
|
new(context: context).stream(prompt, files: files, tags: tags)
|
|
161
161
|
end
|
|
@@ -304,13 +304,14 @@ class Riffer::Agent
|
|
|
304
304
|
Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files, tags: tags)
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
-
# Streams a response from the agent
|
|
308
|
-
# +Riffer::StreamEvents
|
|
307
|
+
# Streams a response from the agent as an +Enumerator+ of
|
|
308
|
+
# +Riffer::StreamEvents+ whose block-form +each+ returns the final
|
|
309
|
+
# Riffer::Agent::Response. See +#generate+ for prompt/files/tags semantics.
|
|
309
310
|
#
|
|
310
311
|
# Raises Riffer::ArgumentError if structured output is configured.
|
|
311
312
|
#
|
|
312
313
|
#--
|
|
313
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base,
|
|
314
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
|
|
314
315
|
def stream(prompt = nil, files: nil, tags: {})
|
|
315
316
|
if @structured_output
|
|
316
317
|
raise Riffer::ArgumentError,
|
|
@@ -19,11 +19,31 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
19
19
|
# <tt>Riffer::Providers::FinishReason::VALUES</tt>).
|
|
20
20
|
attr_reader :finish_reason #: Symbol?
|
|
21
21
|
|
|
22
|
+
# The provider's raw finish-reason value behind +finish_reason+, when one
|
|
23
|
+
# exists on the wire.
|
|
24
|
+
attr_reader :finish_reason_raw #: String?
|
|
25
|
+
|
|
22
26
|
# Raises Riffer::ArgumentError when +finish_reason+ is outside the
|
|
23
27
|
# normalized vocabulary.
|
|
24
28
|
#--
|
|
25
|
-
#: (
|
|
26
|
-
|
|
29
|
+
#: (
|
|
30
|
+
# String,
|
|
31
|
+
# ?id: String?,
|
|
32
|
+
# ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall],
|
|
33
|
+
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
34
|
+
# ?structured_output: Hash[Symbol, untyped]?,
|
|
35
|
+
# ?finish_reason: Symbol?,
|
|
36
|
+
# ?finish_reason_raw: String?
|
|
37
|
+
# ) -> void
|
|
38
|
+
def initialize(
|
|
39
|
+
content,
|
|
40
|
+
id: nil,
|
|
41
|
+
tool_calls: [],
|
|
42
|
+
token_usage: nil,
|
|
43
|
+
structured_output: nil,
|
|
44
|
+
finish_reason: nil,
|
|
45
|
+
finish_reason_raw: nil
|
|
46
|
+
)
|
|
27
47
|
if finish_reason && !Riffer::Providers::FinishReason::VALUES.include?(finish_reason)
|
|
28
48
|
values = Riffer::Providers::FinishReason::VALUES.inspect
|
|
29
49
|
raise Riffer::ArgumentError, "finish_reason must be one of #{values}, got #{finish_reason.inspect}"
|
|
@@ -34,6 +54,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
34
54
|
@token_usage = token_usage
|
|
35
55
|
@structured_output = structured_output
|
|
36
56
|
@finish_reason = finish_reason
|
|
57
|
+
@finish_reason_raw = finish_reason_raw
|
|
37
58
|
end
|
|
38
59
|
|
|
39
60
|
#--
|
|
@@ -71,6 +92,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
71
92
|
hash[:token_usage] = token_usage.to_h if token_usage
|
|
72
93
|
hash[:structured_output] = structured_output if structured_output?
|
|
73
94
|
hash[:finish_reason] = finish_reason if finish_reason
|
|
95
|
+
hash[:finish_reason_raw] = finish_reason_raw if finish_reason_raw
|
|
74
96
|
hash
|
|
75
97
|
end
|
|
76
98
|
end
|
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -14,38 +14,30 @@ class Riffer::Messages::Base
|
|
|
14
14
|
|
|
15
15
|
raise Riffer::ArgumentError, "Message must be a Hash or Message object, got #{msg.class}" unless msg.is_a?(Hash)
|
|
16
16
|
|
|
17
|
-
role
|
|
18
|
-
content = msg[:content]
|
|
17
|
+
raise Riffer::ArgumentError, "Message hash must include a 'role' key" if msg[:role].nil? || msg[:role].empty?
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
id = msg[:id]
|
|
23
|
-
|
|
24
|
-
case role.to_sym
|
|
19
|
+
case msg[:role].to_sym
|
|
25
20
|
when :user
|
|
26
21
|
files = (msg[:files] || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
27
|
-
Riffer::Messages::User.new(content, id: id, files: files)
|
|
22
|
+
Riffer::Messages::User.new(msg[:content], id: msg[:id], files: files)
|
|
28
23
|
when :assistant
|
|
29
24
|
tool_calls = (msg[:tool_calls] || []).map do |tc|
|
|
30
25
|
tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
|
|
31
26
|
end
|
|
32
|
-
structured_output = msg[:structured_output]
|
|
33
|
-
finish_reason = msg[:finish_reason]&.to_sym
|
|
34
27
|
Riffer::Messages::Assistant.new(
|
|
35
|
-
content,
|
|
36
|
-
id: id,
|
|
28
|
+
msg[:content],
|
|
29
|
+
id: msg[:id],
|
|
37
30
|
tool_calls: tool_calls,
|
|
38
|
-
structured_output: structured_output,
|
|
39
|
-
finish_reason: finish_reason,
|
|
31
|
+
structured_output: msg[:structured_output],
|
|
32
|
+
finish_reason: msg[:finish_reason]&.to_sym,
|
|
33
|
+
finish_reason_raw: msg[:finish_reason_raw],
|
|
40
34
|
)
|
|
41
35
|
when :system
|
|
42
|
-
Riffer::Messages::System.new(content, id: id)
|
|
36
|
+
Riffer::Messages::System.new(msg[:content], id: msg[:id])
|
|
43
37
|
when :tool
|
|
44
|
-
|
|
45
|
-
name = msg[:name]
|
|
46
|
-
Riffer::Messages::Tool.new(content, id: id, tool_call_id: tool_call_id, name: name)
|
|
38
|
+
Riffer::Messages::Tool.new(msg[:content], id: msg[:id], tool_call_id: msg[:tool_call_id], name: msg[:name])
|
|
47
39
|
else
|
|
48
|
-
raise Riffer::ArgumentError, "Unknown message role: #{role}"
|
|
40
|
+
raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
49
41
|
end
|
|
50
42
|
end
|
|
51
43
|
|
data/lib/riffer/params/param.rb
CHANGED
data/lib/riffer/params.rb
CHANGED
|
@@ -79,6 +79,11 @@ class Riffer::Params
|
|
|
79
79
|
|
|
80
80
|
# Validates arguments against parameter definitions.
|
|
81
81
|
#
|
|
82
|
+
# A Float param accepts an Integer (JSON Schema <tt>"number"</tt> covers
|
|
83
|
+
# integers) and its value is coerced with +to_f+, so callers always get a
|
|
84
|
+
# Float. The same holds for the items of an <tt>of: Float</tt> array. No other
|
|
85
|
+
# type is coerced.
|
|
86
|
+
#
|
|
82
87
|
# Raises Riffer::ValidationError if validation fails.
|
|
83
88
|
#
|
|
84
89
|
#--
|
|
@@ -112,7 +117,7 @@ class Riffer::Params
|
|
|
112
117
|
|
|
113
118
|
value = validate_nested(param, value, errors)
|
|
114
119
|
|
|
115
|
-
validated[param.name] = value
|
|
120
|
+
validated[param.name] = coerce_value(param.type, value)
|
|
116
121
|
end
|
|
117
122
|
|
|
118
123
|
raise Riffer::ValidationError, errors.join("; ") if errors.any?
|
|
@@ -178,7 +183,6 @@ class Riffer::Params
|
|
|
178
183
|
validate_nested_array_of_objects(param, value, errors)
|
|
179
184
|
elsif param.type == Array && param.item_type
|
|
180
185
|
validate_typed_array(param, value, errors)
|
|
181
|
-
value
|
|
182
186
|
else
|
|
183
187
|
value
|
|
184
188
|
end
|
|
@@ -218,20 +222,40 @@ class Riffer::Params
|
|
|
218
222
|
end
|
|
219
223
|
end
|
|
220
224
|
|
|
225
|
+
# Returns the array with its valid items coerced by +coerce_value+.
|
|
221
226
|
#--
|
|
222
|
-
#: (Riffer::Params::Param, Array[untyped], Array[String]) ->
|
|
227
|
+
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
223
228
|
def validate_typed_array(param, value, errors)
|
|
224
229
|
item_type = param.item_type
|
|
225
|
-
return unless item_type
|
|
230
|
+
return value unless item_type
|
|
226
231
|
|
|
227
232
|
type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
|
|
228
233
|
valid_item = if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(item_type)
|
|
229
234
|
->(item) { [true, false].include?(item) }
|
|
235
|
+
elsif item_type == Float
|
|
236
|
+
->(item) { item.is_a?(Numeric) }
|
|
230
237
|
else
|
|
231
238
|
->(item) { item.is_a?(item_type) }
|
|
232
239
|
end
|
|
233
|
-
value.
|
|
234
|
-
|
|
240
|
+
value.map.with_index do |item, i|
|
|
241
|
+
unless valid_item.call(item)
|
|
242
|
+
errors << "#{param.name}[#{i}] must be a #{type_name}"
|
|
243
|
+
next item
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
coerce_value(item_type, item)
|
|
235
247
|
end
|
|
236
248
|
end
|
|
249
|
+
|
|
250
|
+
# Coerces an already-validated value to the Ruby type its param declares.
|
|
251
|
+
# Only Float coerces today, because JSON Schema "number" accepts integers and
|
|
252
|
+
# callers should not get a type that depends on whether the model wrote a
|
|
253
|
+
# decimal point. Add a branch here rather than inline at a call site.
|
|
254
|
+
#--
|
|
255
|
+
#: (Module, untyped) -> untyped
|
|
256
|
+
def coerce_value(type, value)
|
|
257
|
+
return value.to_f if type == Float
|
|
258
|
+
|
|
259
|
+
value
|
|
260
|
+
end
|
|
237
261
|
end
|
|
@@ -96,7 +96,12 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
96
96
|
# Use strict schema to make optional fields nullable. Without this,
|
|
97
97
|
# Anthropic may return empty strings or whitespace instead of null
|
|
98
98
|
# for optional fields that the model has no value for.
|
|
99
|
+
#
|
|
100
|
+
# Merged over any caller-supplied output_config (e.g. effort) so those
|
|
101
|
+
# keys survive; the structured-output format wins because the run loop
|
|
102
|
+
# validates the response against it.
|
|
99
103
|
params[:output_config] = {
|
|
104
|
+
**(params[:output_config] || {}),
|
|
100
105
|
format: {
|
|
101
106
|
type: "json_schema",
|
|
102
107
|
schema: structured_output.json_schema(strict: true),
|
data/lib/riffer/runner/fibers.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# +max_concurrency+ caps simultaneous fibers via an <tt>Async::Semaphore</tt>.
|
|
6
6
|
# If multiple fibers raise, only the first exception is re-raised after all
|
|
7
7
|
# finish.
|
|
8
|
+
# Joins the current reactor task when one is already running, and otherwise
|
|
9
|
+
# starts its own.
|
|
8
10
|
class Riffer::Runner::Fibers < Riffer::Runner
|
|
9
11
|
# @rbs @max_concurrency: Integer?
|
|
10
12
|
|
|
@@ -25,15 +27,15 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
25
27
|
results = Array.new(items.size)
|
|
26
28
|
errors = Array.new(items.size)
|
|
27
29
|
|
|
28
|
-
Async
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
30
|
+
barrier = Async::Barrier.new
|
|
31
|
+
max = @max_concurrency
|
|
32
|
+
parent = if max
|
|
33
|
+
Async::Semaphore.new(max, parent: barrier)
|
|
34
|
+
else
|
|
35
|
+
barrier
|
|
36
|
+
end
|
|
36
37
|
|
|
38
|
+
Sync do
|
|
37
39
|
items.each_with_index do |item, index|
|
|
38
40
|
parent.async do
|
|
39
41
|
results[index] = yield(item)
|
|
@@ -43,6 +45,8 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
barrier.wait
|
|
48
|
+
ensure
|
|
49
|
+
barrier.stop
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
first_error = errors.compact.first
|
data/lib/riffer/version.rb
CHANGED
data/sig/_private/async.rbs
CHANGED
|
@@ -10,6 +10,8 @@ module Async
|
|
|
10
10
|
def async: () { () -> void } -> untyped
|
|
11
11
|
|
|
12
12
|
def wait: () -> void
|
|
13
|
+
|
|
14
|
+
def stop: () -> void
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
class Semaphore
|
|
@@ -21,4 +23,6 @@ end
|
|
|
21
23
|
|
|
22
24
|
module Kernel
|
|
23
25
|
def Async: () { () -> void } -> untyped
|
|
26
|
+
|
|
27
|
+
def Sync: () { () -> void } -> untyped
|
|
24
28
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated from lib/riffer/agent/outcome.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# How a run ended — the single place to read whether the agent completed
|
|
4
|
+
# normally and, if not, why. +detail+ carries the specifics when there are any:
|
|
5
|
+
# the tripwire reason, the interrupt reason, the provider's raw finish value,
|
|
6
|
+
# or the structured output parse/validation error.
|
|
7
|
+
#
|
|
8
|
+
# response = agent.generate("Analyze this")
|
|
9
|
+
# case response.outcome.reason
|
|
10
|
+
# when :completed then puts response.structured_output
|
|
11
|
+
# when :invalid_structured_output then warn response.outcome.detail
|
|
12
|
+
# end
|
|
13
|
+
class Riffer::Agent::Outcome
|
|
14
|
+
# Finish reasons that end a turn normally; every other finish reason means the
|
|
15
|
+
# provider cut the turn short and surfaces as the run's outcome verbatim.
|
|
16
|
+
NORMAL_FINISH_REASONS: Array[Symbol]
|
|
17
|
+
|
|
18
|
+
# Derived from the provider vocabulary so a new finish reason becomes an
|
|
19
|
+
# outcome without a second list to update.
|
|
20
|
+
PROVIDER_STOP_REASONS: Array[Symbol]
|
|
21
|
+
|
|
22
|
+
# The vocabulary every run ends in.
|
|
23
|
+
VALUES: Array[Symbol]
|
|
24
|
+
|
|
25
|
+
# Why the run ended.
|
|
26
|
+
attr_reader reason: Symbol
|
|
27
|
+
|
|
28
|
+
# Human-readable specifics for +reason+, when there are any.
|
|
29
|
+
attr_reader detail: String?
|
|
30
|
+
|
|
31
|
+
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
32
|
+
# --
|
|
33
|
+
# : (reason: Symbol, ?detail: String?) -> void
|
|
34
|
+
def initialize: (reason: Symbol, ?detail: String?) -> void
|
|
35
|
+
|
|
36
|
+
# Returns true when the run completed normally.
|
|
37
|
+
#
|
|
38
|
+
# --
|
|
39
|
+
# : () -> bool
|
|
40
|
+
def success?: () -> bool
|
|
41
|
+
end
|