riffer 0.47.1 → 0.48.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/rbs-inline.md +5 -1
  3. data/.release-please-manifest.json +1 -1
  4. data/CHANGELOG.md +31 -0
  5. data/Rakefile +1 -1
  6. data/Steepfile +1 -15
  7. data/docs/AGENTS.md +37 -3
  8. data/docs/AGENT_LIFECYCLE.md +1 -0
  9. data/docs/CONFIGURATION.md +1 -1
  10. data/docs/EVALS.md +2 -1
  11. data/docs/MCP.md +0 -4
  12. data/docs/MESSAGES.md +85 -17
  13. data/docs/STREAM_EVENTS.md +31 -4
  14. data/docs/TRACING.md +1 -1
  15. data/docs/providers/AMAZON_BEDROCK.md +1 -1
  16. data/docs/providers/CUSTOM_PROVIDERS.md +58 -4
  17. data/docs/providers/GEMINI.md +1 -1
  18. data/docs/providers/MOCK_PROVIDER.md +17 -0
  19. data/lib/riffer/agent/config.rb +40 -16
  20. data/lib/riffer/agent/outcome.rb +2 -2
  21. data/lib/riffer/agent/response.rb +16 -9
  22. data/lib/riffer/agent/run.rb +24 -9
  23. data/lib/riffer/agent/session.rb +2 -1
  24. data/lib/riffer/agent/structured_output/result.rb +2 -2
  25. data/lib/riffer/agent/structured_output.rb +1 -1
  26. data/lib/riffer/agent.rb +40 -16
  27. data/lib/riffer/config.rb +31 -31
  28. data/lib/riffer/evals/evaluator.rb +29 -2
  29. data/lib/riffer/evals/judge.rb +13 -8
  30. data/lib/riffer/evals/result.rb +6 -6
  31. data/lib/riffer/evals/run_result.rb +1 -1
  32. data/lib/riffer/evals/scenario_result.rb +6 -6
  33. data/lib/riffer/files/resolver.rb +3 -3
  34. data/lib/riffer/guardrails/modification.rb +3 -3
  35. data/lib/riffer/guardrails/result.rb +3 -3
  36. data/lib/riffer/guardrails/runner.rb +4 -4
  37. data/lib/riffer/guardrails/tripwire.rb +4 -4
  38. data/lib/riffer/helpers/deep_dup.rb +42 -0
  39. data/lib/riffer/mcp/manifest.rb +5 -5
  40. data/lib/riffer/mcp/registration.rb +1 -1
  41. data/lib/riffer/mcp/search_tool.rb +1 -1
  42. data/lib/riffer/messages/assistant/reasoning_part.rb +90 -0
  43. data/lib/riffer/messages/assistant/tool_call.rb +63 -0
  44. data/lib/riffer/messages/assistant.rb +55 -7
  45. data/lib/riffer/messages/base.rb +7 -25
  46. data/lib/riffer/messages/system.rb +10 -0
  47. data/lib/riffer/messages/tool.rb +21 -4
  48. data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +18 -17
  49. data/lib/riffer/messages/user.rb +13 -2
  50. data/lib/riffer/params/param.rb +20 -8
  51. data/lib/riffer/params.rb +10 -1
  52. data/lib/riffer/providers/amazon_bedrock.rb +34 -6
  53. data/lib/riffer/providers/anthropic.rb +9 -2
  54. data/lib/riffer/providers/base.rb +28 -10
  55. data/lib/riffer/providers/finish_reason.rb +2 -2
  56. data/lib/riffer/providers/gemini.rb +2 -2
  57. data/lib/riffer/providers/mock.rb +19 -3
  58. data/lib/riffer/providers/open_ai.rb +14 -5
  59. data/lib/riffer/providers/open_router.rb +6 -5
  60. data/lib/riffer/providers/repository.rb +0 -4
  61. data/lib/riffer/providers/token_usage.rb +21 -5
  62. data/lib/riffer/skills/adapter.rb +1 -1
  63. data/lib/riffer/skills/config.rb +12 -0
  64. data/lib/riffer/skills/context.rb +4 -7
  65. data/lib/riffer/skills/filesystem_backend.rb +1 -2
  66. data/lib/riffer/skills/frontmatter.rb +4 -4
  67. data/lib/riffer/skills/xml_adapter.rb +1 -1
  68. data/lib/riffer/stream_events/base.rb +1 -1
  69. data/lib/riffer/stream_events/finish_reason_done.rb +2 -2
  70. data/lib/riffer/stream_events/guardrail_modification.rb +1 -1
  71. data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -1
  72. data/lib/riffer/stream_events/interrupt.rb +2 -2
  73. data/lib/riffer/stream_events/reasoning_delta.rb +1 -1
  74. data/lib/riffer/stream_events/reasoning_done.rb +10 -8
  75. data/lib/riffer/stream_events/skill_activation.rb +1 -1
  76. data/lib/riffer/stream_events/text_delta.rb +1 -1
  77. data/lib/riffer/stream_events/text_done.rb +1 -1
  78. data/lib/riffer/stream_events/token_usage_done.rb +1 -1
  79. data/lib/riffer/stream_events/tool_call_delta.rb +3 -3
  80. data/lib/riffer/stream_events/tool_call_done.rb +4 -4
  81. data/lib/riffer/stream_events/web_search_done.rb +2 -2
  82. data/lib/riffer/stream_events/web_search_status.rb +3 -3
  83. data/lib/riffer/tools/response.rb +4 -4
  84. data/lib/riffer/tools/runtime.rb +3 -4
  85. data/lib/riffer/tracing/capture.rb +2 -4
  86. data/lib/riffer/tracing/stream_recorder.rb +6 -6
  87. data/lib/riffer/version.rb +1 -1
  88. data/lib/riffer.rb +7 -0
  89. data/rbs_collection.lock.yaml +320 -0
  90. data/rbs_collection.yaml +12 -0
  91. data/sig/_private/anthropic.rbs +5 -4
  92. data/sig/_private/aws-sdk-core/event_error.rbs +15 -0
  93. data/sig/generated/riffer/agent/config.rbs +19 -5
  94. data/sig/generated/riffer/agent/response.rbs +6 -1
  95. data/sig/generated/riffer/agent/run.rbs +15 -8
  96. data/sig/generated/riffer/agent.rbs +25 -9
  97. data/sig/generated/riffer/config.rbs +1 -1
  98. data/sig/generated/riffer/evals/evaluator.rbs +20 -1
  99. data/sig/generated/riffer/evals/judge.rbs +5 -2
  100. data/sig/generated/riffer/files/resolver.rbs +6 -6
  101. data/sig/generated/riffer/helpers/deep_dup.rbs +21 -0
  102. data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +58 -0
  103. data/sig/generated/riffer/messages/assistant/tool_call.rbs +42 -0
  104. data/sig/generated/riffer/messages/assistant.rbs +24 -10
  105. data/sig/generated/riffer/messages/system.rbs +6 -0
  106. data/sig/generated/riffer/messages/tool.rbs +6 -0
  107. data/sig/generated/riffer/messages/{file_part.rbs → user/file_part.rbs} +11 -6
  108. data/sig/generated/riffer/messages/user.rbs +9 -3
  109. data/sig/generated/riffer/params/param.rbs +7 -0
  110. data/sig/generated/riffer/params.rbs +6 -0
  111. data/sig/generated/riffer/providers/amazon_bedrock.rbs +14 -5
  112. data/sig/generated/riffer/providers/anthropic.rbs +2 -2
  113. data/sig/generated/riffer/providers/base.rbs +22 -10
  114. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  115. data/sig/generated/riffer/providers/mock.rbs +7 -2
  116. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  117. data/sig/generated/riffer/providers/open_router.rbs +4 -4
  118. data/sig/generated/riffer/providers/repository.rbs +0 -2
  119. data/sig/generated/riffer/providers/token_usage.rbs +6 -0
  120. data/sig/generated/riffer/skills/config.rbs +9 -0
  121. data/sig/generated/riffer/stream_events/reasoning_done.rbs +8 -6
  122. data/sig/generated/riffer/tools/runtime.rbs +2 -2
  123. data/sig/generated/riffer/tracing/capture.rbs +4 -4
  124. data/sig/generated/riffer.rbs +8 -0
  125. data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
  126. metadata +13 -5
  127. data/sig/_private/async.rbs +0 -28
  128. data/sig/_private/minitest.rbs +0 -9
@@ -35,7 +35,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
35
35
  end
36
36
 
37
37
  #--
38
- #: (Riffer::Messages::FilePart) -> Symbol
38
+ #: (Riffer::Messages::User::FilePart) -> Symbol
39
39
  def file_delivery(file)
40
40
  file.image? ? :url : :base64
41
41
  end
@@ -80,7 +80,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
80
80
  } #: Hash[Symbol, untyped]
81
81
 
82
82
  unless tags.empty?
83
- params[:metadata] = tags
83
+ # Merged over any metadata set in model_options; a tag wins on a shared key.
84
+ params[:metadata] = (params[:metadata] || {}).merge(tags)
84
85
  # The reserved user_id also maps to the native safety identifier while
85
86
  # staying in metadata as an ordinary tag.
86
87
  user_id = tags["user_id"]
@@ -218,6 +219,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
218
219
  tool_info: {},
219
220
  } #: Hash[Symbol, untyped]
220
221
 
222
+ stream_completed = false
223
+
221
224
  stream = client.responses.stream(params)
222
225
  begin
223
226
  stream.each do |event|
@@ -247,6 +250,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
247
250
  when :"response.output_item.done"
248
251
  handle_output_item_done_web_search(event, yielder: yielder) if event.item&.type == :web_search_call
249
252
  when :"response.completed", :"response.incomplete", :"response.failed"
253
+ stream_completed = true
250
254
  handle_response_finished(event, state: current_state, yielder: yielder)
251
255
  end
252
256
  end
@@ -256,6 +260,11 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
256
260
  # socket leaks until GC. close is idempotent and a no-op after EOF.
257
261
  stream.close
258
262
  end
263
+
264
+ return if stream_completed
265
+
266
+ raise Riffer::IncompleteStreamError,
267
+ "OpenAI Responses stream ended without a response.completed, response.incomplete, or response.failed event"
259
268
  end
260
269
 
261
270
  #--
@@ -288,7 +297,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
288
297
  #--
289
298
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
290
299
  def handle_reasoning_summary_text_done(event, state:, yielder:)
291
- yielder << Riffer::StreamEvents::ReasoningDone.new(event.text)
300
+ yield_reasoning_done(yielder, event.text)
292
301
  end
293
302
 
294
303
  #--
@@ -394,7 +403,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
394
403
  type: "function_call",
395
404
  call_id: tc.call_id,
396
405
  name: encode_tool_name(tc.name),
397
- arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json,
406
+ arguments: tc.arguments,
398
407
  }
399
408
  end
400
409
  items
@@ -402,7 +411,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
402
411
  end
403
412
 
404
413
  #--
405
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
414
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
406
415
  def convert_file_part_to_openai_format(file)
407
416
  if file.image?
408
417
  image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
@@ -34,7 +34,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
34
34
  end
35
35
 
36
36
  #--
37
- #: (Riffer::Messages::FilePart) -> Symbol
37
+ #: (Riffer::Messages::User::FilePart) -> Symbol
38
38
  def file_delivery(file)
39
39
  file.image? ? :url : :base64
40
40
  end
@@ -74,7 +74,8 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
74
74
  } #: Hash[Symbol, untyped]
75
75
 
76
76
  unless tags.empty?
77
- params[:metadata] = tags
77
+ # Merged over any metadata set in model_options; a tag wins on a shared key.
78
+ params[:metadata] = (params[:metadata] || {}).merge(tags)
78
79
  # OpenRouter exposes the legacy Chat Completions user field rather than
79
80
  # safety_identifier; the reserved user_id maps there and stays in metadata.
80
81
  user = tags["user_id"]
@@ -223,7 +224,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
223
224
  emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
224
225
 
225
226
  yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
226
- yielder << Riffer::StreamEvents::ReasoningDone.new(state[:reasoning]) unless state[:reasoning].empty?
227
+ yield_reasoning_done(yielder, state[:reasoning]) unless state[:reasoning].empty?
227
228
  yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
228
229
  end
229
230
 
@@ -362,7 +363,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
362
363
  type: "function",
363
364
  function: {
364
365
  name: encode_tool_name(tc.name),
365
- arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json,
366
+ arguments: tc.arguments,
366
367
  },
367
368
  }
368
369
  end
@@ -372,7 +373,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
372
373
  end
373
374
 
374
375
  #--
375
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
376
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
376
377
  def convert_file_part_to_chat_completions_format(file)
377
378
  if file.image?
378
379
  image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
@@ -27,13 +27,9 @@ module Riffer::Providers::Repository
27
27
  #
28
28
  # Riffer::Providers::Repository.register(:jane) { MyApp::JaneProvider }
29
29
  #
30
- # Raises Riffer::ArgumentError when called without a block.
31
- #
32
30
  #--
33
31
  #: ((String | Symbol)) { () -> singleton(Riffer::Providers::Base) } -> void
34
32
  def register(identifier, &factory)
35
- raise Riffer::ArgumentError, "register requires a block returning a provider class" unless factory
36
-
37
33
  @registrations[identifier.to_sym] = factory
38
34
  @key_for = nil
39
35
  end
@@ -4,20 +4,36 @@
4
4
  # Normalized token usage for an LLM API call. Buckets carry the same
5
5
  # meaning for every provider.
6
6
  class Riffer::Providers::TokenUsage
7
+ # Builds a TokenUsage from a hash, or returns +usage+ unchanged when it is
8
+ # already a TokenUsage.
9
+ #--
10
+ #: ((Hash[Symbol, untyped] | Riffer::Providers::TokenUsage)) -> Riffer::Providers::TokenUsage
11
+ def self.from_hash(usage)
12
+ return usage if usage.is_a?(Riffer::Providers::TokenUsage)
13
+
14
+ new(
15
+ input_tokens: usage[:input_tokens],
16
+ output_tokens: usage[:output_tokens],
17
+ cache_write_tokens: usage[:cache_write_tokens],
18
+ cache_read_tokens: usage[:cache_read_tokens],
19
+ cost: usage[:cost],
20
+ )
21
+ end
22
+
7
23
  # Number of tokens entering the context window, including cache reads and writes.
8
- attr_reader :input_tokens #: Integer
24
+ attr_reader :input_tokens #: Integer # @dynamic input_tokens
9
25
 
10
26
  # Number of tokens generated by the model, including reasoning/thinking tokens.
11
- attr_reader :output_tokens #: Integer
27
+ attr_reader :output_tokens #: Integer # @dynamic output_tokens
12
28
 
13
29
  # Subset of +input_tokens+ written to the provider's prompt cache, when the provider reports it.
14
- attr_reader :cache_write_tokens #: Integer?
30
+ attr_reader :cache_write_tokens #: Integer? # @dynamic cache_write_tokens
15
31
 
16
32
  # Subset of +input_tokens+ read from the provider's prompt cache, when the provider reports it.
17
- attr_reader :cache_read_tokens #: Integer?
33
+ attr_reader :cache_read_tokens #: Integer? # @dynamic cache_read_tokens
18
34
 
19
35
  # Cost of the call, set when the model is priced. For observability, not billing.
20
- attr_reader :cost #: Float?
36
+ attr_reader :cost #: Float? # @dynamic cost
21
37
 
22
38
  #--
23
39
  #: (input_tokens: Integer, output_tokens: Integer, ?cache_write_tokens: Integer?, ?cache_read_tokens: Integer?, ?cost: Float?) -> void
@@ -7,7 +7,7 @@
7
7
  # for the rendered output.
8
8
  class Riffer::Skills::Adapter
9
9
  # The activation tool class for this adapter.
10
- attr_reader :skill_activate_tool #: singleton(Riffer::Tool)
10
+ attr_reader :skill_activate_tool #: singleton(Riffer::Tool) # @dynamic skill_activate_tool
11
11
 
12
12
  #--
13
13
  #: (skill_activate_tool: singleton(Riffer::Tool)) -> void
@@ -67,4 +67,16 @@ class Riffer::Skills::Config
67
67
 
68
68
  @activate_tool = value
69
69
  end
70
+
71
+ private
72
+
73
+ # +dup+ would leave the copy sharing this one's activation list, so activating
74
+ # a skill on either would reach the other. DeepDup rebuilds the list and
75
+ # returns a Proc as-is, which has nothing to alias.
76
+ #--
77
+ #: (Riffer::Skills::Config) -> void
78
+ def initialize_copy(source)
79
+ super
80
+ @activate = Riffer::Helpers::DeepDup.call(source.activate)
81
+ end
70
82
  end
@@ -11,13 +11,13 @@ class Riffer::Skills::Context
11
11
  # @rbs @preactivated: Array[String]
12
12
 
13
13
  # Skill catalog indexed by name.
14
- attr_reader :skills #: Hash[String, Riffer::Skills::Frontmatter]
14
+ attr_reader :skills #: Hash[String, Riffer::Skills::Frontmatter] # @dynamic skills
15
15
 
16
16
  # The skill adapter used for this context.
17
- attr_reader :adapter #: Riffer::Skills::Adapter
17
+ attr_reader :adapter #: Riffer::Skills::Adapter # @dynamic adapter
18
18
 
19
19
  # Optional callback invoked when a skill is first activated.
20
- attr_accessor :on_activate #: (^(String) -> void)?
20
+ attr_accessor :on_activate #: (^(String) -> void)? # @dynamic on_activate, on_activate=
21
21
 
22
22
  #--
23
23
  #: (backend: Riffer::Skills::Backend, skills: Hash[String, Riffer::Skills::Frontmatter], adapter: Riffer::Skills::Adapter) -> void
@@ -106,10 +106,7 @@ class Riffer::Skills::Context
106
106
  #--
107
107
  #: (String) -> bool
108
108
  def model_invocable?(name)
109
- skill = skills[name]
110
- return false unless skill
111
-
112
- !skill.disable_model_invocation
109
+ skills.key?(name) && !skills.fetch(name).disable_model_invocation
113
110
  end
114
111
 
115
112
  # Returns whether any skill is available for the model to activate.
@@ -53,8 +53,7 @@ class Riffer::Skills::FilesystemBackend < Riffer::Skills::Backend
53
53
  def read_skill(name)
54
54
  list_skills unless @skills_cache
55
55
  cache = @skills_cache #: Hash[String, String]
56
- dir = cache[name]
57
- raise Riffer::ArgumentError, "Skill not found: '#{name}'" unless dir
56
+ dir = cache.fetch(name) { raise Riffer::ArgumentError, "Skill not found: '#{name}'" }
58
57
 
59
58
  _, body = Riffer::Skills::Frontmatter.parse(File.read(File.join(dir, SKILL_FILENAME)))
60
59
  body
@@ -13,19 +13,19 @@ class Riffer::Skills::Frontmatter
13
13
  MAX_DESCRIPTION_LENGTH = 1024 #: Integer
14
14
 
15
15
  # The skill name (1-64 chars, lowercase alphanumeric and hyphens).
16
- attr_reader :name #: String
16
+ attr_reader :name #: String # @dynamic name
17
17
 
18
18
  # The skill description (1-1024 chars).
19
- attr_reader :description #: String
19
+ attr_reader :description #: String # @dynamic description
20
20
 
21
21
  # Whether the skill opts out of model-driven activation. Hidden from the
22
22
  # catalog and rejected at model activation; still reachable via programmatic
23
23
  # activation.
24
- attr_reader :disable_model_invocation #: bool
24
+ attr_reader :disable_model_invocation #: bool # @dynamic disable_model_invocation
25
25
 
26
26
  # Metadata from the spec's +metadata+ field plus any unrecognized top-level
27
27
  # keys.
28
- attr_reader :metadata #: Hash[Symbol, untyped]
28
+ attr_reader :metadata #: Hash[Symbol, untyped] # @dynamic metadata
29
29
 
30
30
  # Parses a raw SKILL.md string into a +[Frontmatter, body]+ pair — public so
31
31
  # custom backends needn't reimplement parsing. Raises Riffer::ArgumentError
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- require "cgi"
4
+ require "cgi/escape"
5
5
 
6
6
  # Renders a skill catalog as XML for the system prompt, optimized for
7
7
  # Anthropic/Claude.
@@ -4,7 +4,7 @@
4
4
  # Base class for all streaming events. Subclasses must implement +to_h+.
5
5
  class Riffer::StreamEvents::Base
6
6
  # The message role (typically :assistant).
7
- attr_reader :role #: Symbol
7
+ attr_reader :role #: Symbol # @dynamic role
8
8
 
9
9
  #--
10
10
  #: (?role: Symbol) -> void
@@ -5,10 +5,10 @@
5
5
  # stream; no ordering guarantee relative to TokenUsageDone.
6
6
  class Riffer::StreamEvents::FinishReasonDone < Riffer::StreamEvents::Base
7
7
  # The normalized finish reason (see <tt>Riffer::Providers::FinishReason::VALUES</tt>).
8
- attr_reader :finish_reason #: Symbol
8
+ attr_reader :finish_reason #: Symbol # @dynamic finish_reason
9
9
 
10
10
  # The provider's raw finish-reason value, when one exists on the wire.
11
- attr_reader :raw_finish_reason #: String?
11
+ attr_reader :raw_finish_reason #: String? # @dynamic raw_finish_reason
12
12
 
13
13
  # Raises Riffer::ArgumentError when +finish_reason+ is outside the
14
14
  # normalized vocabulary.
@@ -4,7 +4,7 @@
4
4
  # Emitted when a guardrail transforms data during streaming.
5
5
  class Riffer::StreamEvents::GuardrailModification < Riffer::StreamEvents::Base
6
6
  # The modification record.
7
- attr_reader :modification #: Riffer::Guardrails::Modification
7
+ attr_reader :modification #: Riffer::Guardrails::Modification # @dynamic modification
8
8
 
9
9
  #--
10
10
  #: (Riffer::Guardrails::Modification, ?role: Symbol) -> void
@@ -4,7 +4,7 @@
4
4
  # Emitted when a guardrail blocks execution during streaming.
5
5
  class Riffer::StreamEvents::GuardrailTripwire < Riffer::StreamEvents::Base
6
6
  # The tripwire containing block details.
7
- attr_reader :tripwire #: Riffer::Guardrails::Tripwire
7
+ attr_reader :tripwire #: Riffer::Guardrails::Tripwire # @dynamic tripwire
8
8
 
9
9
  #--
10
10
  #: (Riffer::Guardrails::Tripwire, ?role: Symbol) -> void
@@ -5,11 +5,11 @@
5
5
  # +:riffer_interrupt+.
6
6
  class Riffer::StreamEvents::Interrupt < Riffer::StreamEvents::Base
7
7
  # The reason provided with the interrupt, if any.
8
- attr_reader :reason #: (String | Symbol)?
8
+ attr_reader :reason #: (String | Symbol)? # @dynamic reason
9
9
 
10
10
  # Call ids of tool_use blocks riffer filled with placeholder results when the
11
11
  # interrupt fired (only when history healing is on).
12
- attr_reader :healed_tool_call_ids #: Array[String]
12
+ attr_reader :healed_tool_call_ids #: Array[String] # @dynamic healed_tool_call_ids
13
13
 
14
14
  #--
15
15
  #: (?reason: (String | Symbol)?, ?healed_tool_call_ids: Array[String]) -> void
@@ -5,7 +5,7 @@
5
5
  # providers that support reasoning (e.g. OpenAI with the reasoning option).
6
6
  class Riffer::StreamEvents::ReasoningDelta < Riffer::StreamEvents::Base
7
7
  # The incremental reasoning content.
8
- attr_reader :content #: String
8
+ attr_reader :content #: String # @dynamic content
9
9
 
10
10
  #--
11
11
  #: (String, ?role: Symbol) -> void
@@ -1,22 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Represents completed reasoning during streaming; only emitted by providers
5
- # that support reasoning (e.g. OpenAI with the reasoning option).
4
+ # Represents one completed reasoning block during streaming; only emitted by
5
+ # providers that support reasoning (e.g. OpenAI with the reasoning option).
6
6
  class Riffer::StreamEvents::ReasoningDone < Riffer::StreamEvents::Base
7
- # The complete reasoning content.
8
- attr_reader :content #: String
7
+ # The reasoning block, which the agent loop accumulates onto the assistant
8
+ # message. Its +text+ is the content the preceding ReasoningDelta events
9
+ # added up to.
10
+ attr_reader :part #: Riffer::Messages::Assistant::ReasoningPart # @dynamic part
9
11
 
10
12
  #--
11
- #: (String, ?role: Symbol) -> void
12
- def initialize(content, role: :assistant)
13
+ #: (Riffer::Messages::Assistant::ReasoningPart, ?role: Symbol) -> void
14
+ def initialize(part, role: :assistant)
13
15
  super(role: role)
14
- @content = content
16
+ @part = part
15
17
  end
16
18
 
17
19
  #--
18
20
  #: () -> Hash[Symbol, untyped]
19
21
  def to_h
20
- { role: @role, content: @content }
22
+ { role: @role, part: part.to_h }
21
23
  end
22
24
  end
@@ -5,7 +5,7 @@
5
5
  # callback when the LLM calls the activation tool.
6
6
  class Riffer::StreamEvents::SkillActivation < Riffer::StreamEvents::Base
7
7
  # The activated skill name.
8
- attr_reader :name #: String
8
+ attr_reader :name #: String # @dynamic name
9
9
 
10
10
  #--
11
11
  #: (String, ?role: Symbol) -> void
@@ -4,7 +4,7 @@
4
4
  # Represents an incremental text chunk during streaming.
5
5
  class Riffer::StreamEvents::TextDelta < Riffer::StreamEvents::Base
6
6
  # The incremental text content.
7
- attr_reader :content #: String
7
+ attr_reader :content #: String # @dynamic content
8
8
 
9
9
  #--
10
10
  #: (String, ?role: Symbol) -> void
@@ -4,7 +4,7 @@
4
4
  # Represents completed text generation during streaming.
5
5
  class Riffer::StreamEvents::TextDone < Riffer::StreamEvents::Base
6
6
  # The complete text content.
7
- attr_reader :content #: String
7
+ attr_reader :content #: String # @dynamic content
8
8
 
9
9
  #--
10
10
  #: (String, ?role: Symbol) -> void
@@ -4,7 +4,7 @@
4
4
  # Final token usage for the response, emitted when the LLM finishes.
5
5
  class Riffer::StreamEvents::TokenUsageDone < Riffer::StreamEvents::Base
6
6
  # The token usage data for this response.
7
- attr_reader :token_usage #: Riffer::Providers::TokenUsage
7
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage # @dynamic token_usage
8
8
 
9
9
  #--
10
10
  #: (token_usage: Riffer::Providers::TokenUsage, ?role: Symbol) -> void
@@ -5,13 +5,13 @@
5
5
  # streaming.
6
6
  class Riffer::StreamEvents::ToolCallDelta < Riffer::StreamEvents::Base
7
7
  # The tool call item identifier.
8
- attr_reader :item_id #: String
8
+ attr_reader :item_id #: String # @dynamic item_id
9
9
 
10
10
  # The tool name (may only be present in first delta).
11
- attr_reader :name #: String?
11
+ attr_reader :name #: String? # @dynamic name
12
12
 
13
13
  # The incremental arguments JSON fragment.
14
- attr_reader :arguments_delta #: String
14
+ attr_reader :arguments_delta #: String # @dynamic arguments_delta
15
15
 
16
16
  #--
17
17
  #: (item_id: String, arguments_delta: String, ?name: String?, ?role: Symbol) -> void
@@ -4,16 +4,16 @@
4
4
  # Represents a completed tool call during streaming.
5
5
  class Riffer::StreamEvents::ToolCallDone < Riffer::StreamEvents::Base
6
6
  # The tool call item identifier.
7
- attr_reader :item_id #: String
7
+ attr_reader :item_id #: String # @dynamic item_id
8
8
 
9
9
  # The call identifier for response matching.
10
- attr_reader :call_id #: String
10
+ attr_reader :call_id #: String # @dynamic call_id
11
11
 
12
12
  # The tool name.
13
- attr_reader :name #: String
13
+ attr_reader :name #: String # @dynamic name
14
14
 
15
15
  # The complete arguments JSON string.
16
- attr_reader :arguments #: String
16
+ attr_reader :arguments #: String # @dynamic arguments
17
17
 
18
18
  #--
19
19
  #: (item_id: String, call_id: String, name: String, arguments: String, ?role: Symbol) -> void
@@ -4,10 +4,10 @@
4
4
  # The result of a completed server-side web search during streaming.
5
5
  class Riffer::StreamEvents::WebSearchDone < Riffer::StreamEvents::Base
6
6
  # The search query used.
7
- attr_reader :query #: String
7
+ attr_reader :query #: String # @dynamic query
8
8
 
9
9
  # The search result sources with title and url.
10
- attr_reader :sources #: Array[Hash[Symbol, String?]]
10
+ attr_reader :sources #: Array[Hash[Symbol, String?]] # @dynamic sources
11
11
 
12
12
  #--
13
13
  #: (String, ?sources: Array[Hash[Symbol, String?]], ?role: Symbol) -> void
@@ -5,13 +5,13 @@
5
5
  # progresses.
6
6
  class Riffer::StreamEvents::WebSearchStatus < Riffer::StreamEvents::Base
7
7
  # The web search status ("in_progress", "searching", "completed", "open_page").
8
- attr_reader :status #: String
8
+ attr_reader :status #: String # @dynamic status
9
9
 
10
10
  # The URL being fetched (present for "open_page" status).
11
- attr_reader :url #: String?
11
+ attr_reader :url #: String? # @dynamic url
12
12
 
13
13
  # The search query (present when available during status changes).
14
- attr_reader :query #: String?
14
+ attr_reader :query #: String? # @dynamic query
15
15
 
16
16
  #--
17
17
  #: (String, ?url: String?, ?query: String?, ?role: Symbol) -> void
@@ -20,17 +20,17 @@ class Riffer::Tools::Response
20
20
  VALID_FORMATS = %i[text json].freeze #: Array[Symbol]
21
21
 
22
22
  # The response content.
23
- attr_reader :content #: String
23
+ attr_reader :content #: String # @dynamic content
24
24
 
25
25
  # The error message, or +nil+ on success.
26
- attr_reader :error_message #: String?
26
+ attr_reader :error_message #: String? # @dynamic error_message
27
27
 
28
28
  # The error type, or +nil+ on success.
29
- attr_reader :error_type #: Symbol?
29
+ attr_reader :error_type #: Symbol? # @dynamic error_type
30
30
 
31
31
  # The exception an unhandled failure was folded from, or +nil+. Kept out of
32
32
  # every serialized form so it never reaches an LLM or a message payload.
33
- attr_reader :exception #: Exception?
33
+ attr_reader :exception #: Exception? # @dynamic exception
34
34
 
35
35
  # Creates a success response.
36
36
  #
@@ -101,9 +101,9 @@ class Riffer::Tools::Runtime
101
101
  end
102
102
 
103
103
  #--
104
- #: (String?) -> untyped
104
+ #: (String) -> untyped
105
105
  def parse_arguments(arguments)
106
- return {} if arguments.nil? || arguments.empty?
106
+ return {} if arguments.empty?
107
107
 
108
108
  JSON.parse(arguments, symbolize_names: true)
109
109
  end
@@ -168,8 +168,7 @@ class Riffer::Tools::Runtime
168
168
  def capture_tool_arguments(span, tool_call)
169
169
  return unless capture_tool_content?(span)
170
170
 
171
- arguments = tool_call.arguments
172
- span.set_attribute("gen_ai.tool.call.arguments", arguments) if arguments
171
+ span.set_attribute("gen_ai.tool.call.arguments", tool_call.arguments)
173
172
  end
174
173
 
175
174
  #--
@@ -71,7 +71,7 @@ module Riffer::Tracing::Capture # :nodoc: all
71
71
  end
72
72
 
73
73
  #--
74
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
74
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
75
75
  def file_part(file)
76
76
  part = { type: "file", media_type: file.media_type } #: Hash[Symbol, untyped]
77
77
  part[:name] = file.filename if file.filename
@@ -81,10 +81,8 @@ module Riffer::Tracing::Capture # :nodoc: all
81
81
  # Semconv's tool_call part carries arguments as a JSON object; riffer holds
82
82
  # them as a string — parse so the captured payload isn't double-encoded.
83
83
  #--
84
- #: (untyped) -> untyped
84
+ #: (String) -> untyped
85
85
  def parse_arguments(arguments)
86
- return arguments unless arguments.is_a?(String)
87
-
88
86
  JSON.parse(arguments)
89
87
  rescue JSON::ParserError
90
88
  arguments
@@ -8,17 +8,17 @@ class Riffer::Tracing::StreamRecorder # :nodoc: all
8
8
  # @rbs @clock: ^() -> Float
9
9
  # @rbs @started_at: Float
10
10
 
11
- attr_reader :token_usage #: Riffer::Providers::TokenUsage?
11
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
12
12
 
13
- attr_reader :time_to_first_chunk #: Float?
13
+ attr_reader :time_to_first_chunk #: Float? # @dynamic time_to_first_chunk
14
14
 
15
- attr_reader :finish_reason #: Symbol?
15
+ attr_reader :finish_reason #: Symbol? # @dynamic finish_reason
16
16
 
17
- attr_reader :raw_finish_reason #: String?
17
+ attr_reader :raw_finish_reason #: String? # @dynamic raw_finish_reason
18
18
 
19
- attr_reader :content #: String?
19
+ attr_reader :content #: String? # @dynamic content
20
20
 
21
- attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
21
+ attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall] # @dynamic tool_calls
22
22
 
23
23
  #--
24
24
  #: (Enumerator::Yielder, ?clock: ^() -> Float) -> void
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.47.1" #: String
5
+ VERSION = "0.48.0" #: String
6
6
  end
data/lib/riffer.rb CHANGED
@@ -41,6 +41,13 @@ module Riffer
41
41
  # Raised when a tool encounters an expected execution error.
42
42
  class ToolExecutionError < Error; end
43
43
 
44
+ # Raised when a provider's stream ends before its terminal event (Bedrock's
45
+ # messageStop, Anthropic's message_stop, OpenAI's response.completed /
46
+ # incomplete / failed), so a truncated or empty response is never returned
47
+ # as a complete message. Events already yielded were delivered; the
48
+ # request is safe to retry.
49
+ class IncompleteStreamError < Error; end
50
+
44
51
  # Base error for a file attachment that can't be resolved for the provider.
45
52
  class FileError < Error; end
46
53