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
@@ -15,8 +15,12 @@ class Riffer::Evals::Evaluator
15
15
  # @rbs self.@instructions: String?
16
16
  # @rbs self.@higher_is_better: bool?
17
17
  # @rbs self.@judge_model: String?
18
+ # @rbs self.@identifier: String?
18
19
  # @rbs @judge: Riffer::Evals::Judge?
19
20
 
21
+ # The identifier for an anonymous evaluator class.
22
+ DEFAULT_IDENTIFIER = "riffer/judge" #: String
23
+
20
24
  class << self
21
25
  # Gets or sets the evaluation instructions (criteria and scoring rubric).
22
26
  #
@@ -51,6 +55,28 @@ class Riffer::Evals::Evaluator
51
55
 
52
56
  @judge_model = value.to_s
53
57
  end
58
+
59
+ # Gets or sets the evaluator identifier, sent as the +agent+ tag on judge
60
+ # calls. Defaults to the snake_cased class name, or DEFAULT_IDENTIFIER for
61
+ # an anonymous class.
62
+ #
63
+ #--
64
+ #: (?String?) -> String
65
+ def identifier(value = nil)
66
+ return @identifier = value.to_s if value
67
+
68
+ @identifier || derived_identifier
69
+ end
70
+
71
+ private
72
+
73
+ # Anonymous classes derive an empty identifier.
74
+ #--
75
+ #: () -> String
76
+ def derived_identifier
77
+ derived = Riffer::Helpers::Identifier.for(self)
78
+ derived.empty? ? DEFAULT_IDENTIFIER : derived
79
+ end
54
80
  end
55
81
 
56
82
  # Evaluates an input/output pair. The default calls the judge with the
@@ -94,7 +120,8 @@ class Riffer::Evals::Evaluator
94
120
 
95
121
  protected
96
122
 
97
- # Returns a Judge instance configured for this evaluator.
123
+ # Returns a Judge instance configured for this evaluator. Its calls carry
124
+ # the +kind+ (+"judge"+) and +agent+ (the evaluator identifier) tags.
98
125
  #
99
126
  #--
100
127
  #: () -> Riffer::Evals::Judge
@@ -106,7 +133,7 @@ class Riffer::Evals::Evaluator
106
133
  "No judge model configured. Set judge_model on the evaluator or Riffer.config.evals.judge_model"
107
134
  end
108
135
 
109
- Riffer::Evals::Judge.new(model: model)
136
+ Riffer::Evals::Judge.new(model: model, tags: { "kind" => "judge", "agent" => self.class.identifier })
110
137
  end
111
138
  end
112
139
 
@@ -28,18 +28,22 @@ class Riffer::Evals::Judge
28
28
  end
29
29
 
30
30
  # The model string (provider/model format).
31
- attr_reader :model #: String
31
+ attr_reader :model #: String # @dynamic model
32
+
33
+ # The tags passed to the provider on every call.
34
+ attr_reader :tags #: Hash[String, String] # @dynamic tags
32
35
 
33
36
  # Raises Riffer::ArgumentError unless +model+ is "provider/model" format.
34
37
  #--
35
- #: (model: String) -> void
36
- def initialize(model:)
38
+ #: (model: String, ?tags: Hash[String, String]) -> void
39
+ def initialize(model:, tags: {})
37
40
  provider_name, model_name = model.split("/", 2)
38
41
  unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
39
42
  raise Riffer::ArgumentError, "Invalid model string: #{model}"
40
43
  end
41
44
 
42
45
  @model = model
46
+ @tags = tags
43
47
  end
44
48
 
45
49
  # Evaluates an input/output pair using the configured LLM.
@@ -49,12 +53,14 @@ class Riffer::Evals::Judge
49
53
  system_message = build_system_message(instructions)
50
54
  user_message = build_user_message(input: input, output: output, ground_truth: ground_truth)
51
55
 
52
- response = provider_instance.generate_text(
56
+ options = {
53
57
  system: system_message,
54
58
  prompt: user_message,
55
59
  model: model_name,
56
60
  tools: [EvaluationTool],
57
- )
61
+ } #: Hash[Symbol, untyped]
62
+ options[:tags] = tags unless tags.empty?
63
+ response = provider_instance.generate_text(**options)
58
64
 
59
65
  parse_tool_response(response)
60
66
  end
@@ -109,10 +115,9 @@ class Riffer::Evals::Judge
109
115
  #--
110
116
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
111
117
  def parse_tool_response(response)
112
- tool_call = response.tool_calls.first
113
- raise Riffer::Error, "Invalid judge response: no tool call found" unless tool_call
118
+ tool_call = response.tool_calls.fetch(0) { raise Riffer::Error, "Invalid judge response: no tool call found" }
114
119
 
115
- parsed = JSON.parse(tool_call[:arguments], symbolize_names: true)
120
+ parsed = JSON.parse(tool_call.arguments, symbolize_names: true)
116
121
  score = parsed[:score]
117
122
  reason = parsed[:reason]
118
123
 
@@ -4,23 +4,23 @@
4
4
  # Represents the result of a single evaluation.
5
5
  class Riffer::Evals::Result
6
6
  # The evaluator class that produced this result.
7
- attr_reader :evaluator #: singleton(Riffer::Evals::Evaluator)
7
+ attr_reader :evaluator #: singleton(Riffer::Evals::Evaluator) # @dynamic evaluator
8
8
 
9
9
  # The evaluation score (0.0 to 1.0).
10
- attr_reader :score #: Float
10
+ attr_reader :score #: Float # @dynamic score
11
11
 
12
12
  # Human-readable explanation of the score.
13
- attr_reader :reason #: String?
13
+ attr_reader :reason #: String? # @dynamic reason
14
14
 
15
15
  # Additional metadata from the evaluation.
16
- attr_reader :metadata #: Hash[Symbol, untyped]
16
+ attr_reader :metadata #: Hash[Symbol, untyped] # @dynamic metadata
17
17
 
18
18
  # Whether higher scores are better for this evaluator.
19
- attr_reader :higher_is_better #: bool
19
+ attr_reader :higher_is_better #: bool # @dynamic higher_is_better
20
20
 
21
21
  # Token usage for the judge call that produced this result, when the
22
22
  # evaluator used an LLM. Nil for rule-based evaluators.
23
- attr_reader :token_usage #: Riffer::Providers::TokenUsage?
23
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
24
24
 
25
25
  # Raises Riffer::ArgumentError if +score+ is not between 0.0 and 1.0.
26
26
  #--
@@ -4,7 +4,7 @@
4
4
  # Represents the complete result of an evaluation run across multiple scenarios.
5
5
  class Riffer::Evals::RunResult
6
6
  # Per-scenario evaluation results.
7
- attr_reader :scenario_results #: Array[Riffer::Evals::ScenarioResult]
7
+ attr_reader :scenario_results #: Array[Riffer::Evals::ScenarioResult] # @dynamic scenario_results
8
8
 
9
9
  #--
10
10
  #: (scenario_results: Array[Riffer::Evals::ScenarioResult]) -> void
@@ -4,22 +4,22 @@
4
4
  # Represents the result of evaluating a single scenario.
5
5
  class Riffer::Evals::ScenarioResult
6
6
  # The input that was evaluated.
7
- attr_reader :input #: String
7
+ attr_reader :input #: String # @dynamic input
8
8
 
9
9
  # The agent output for this scenario.
10
- attr_reader :output #: String
10
+ attr_reader :output #: String # @dynamic output
11
11
 
12
12
  # The ground truth used during evaluation.
13
- attr_reader :ground_truth #: String?
13
+ attr_reader :ground_truth #: String? # @dynamic ground_truth
14
14
 
15
15
  # Individual evaluation results.
16
- attr_reader :results #: Array[Riffer::Evals::Result]
16
+ attr_reader :results #: Array[Riffer::Evals::Result] # @dynamic results
17
17
 
18
18
  # The full message history from the agent conversation.
19
- attr_reader :messages #: Array[Riffer::Messages::Base]
19
+ attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
20
20
 
21
21
  # Token usage the agent under test spent generating this scenario's output.
22
- attr_reader :token_usage #: Riffer::Providers::TokenUsage?
22
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
23
23
 
24
24
  #--
25
25
  #: (input: String, output: String, ground_truth: String?, results: Array[Riffer::Evals::Result], ?messages: Array[Riffer::Messages::Base], ?token_usage: Riffer::Providers::TokenUsage?) -> void
@@ -40,7 +40,7 @@ class Riffer::Files::Resolver
40
40
  raise Riffer::TooManyFilesError, "Too many files specified in user message"
41
41
  end
42
42
 
43
- #: (Riffer::Messages::FilePart) -> void
43
+ #: (Riffer::Messages::User::FilePart) -> void
44
44
  def resolve_file!(file)
45
45
  delivery = @provider.file_delivery(file)
46
46
  if delivery == :unsupported
@@ -62,7 +62,7 @@ class Riffer::Files::Resolver
62
62
  end
63
63
  end
64
64
 
65
- #: (Riffer::Messages::FilePart) -> void
65
+ #: (Riffer::Messages::User::FilePart) -> void
66
66
  def verify_inline!(file)
67
67
  return unless file.sha256
68
68
 
@@ -72,7 +72,7 @@ class Riffer::Files::Resolver
72
72
  # +cache:+ is false for a :url-delivery provider verifying a sha256 — the
73
73
  # request still sends the URL, never the downloaded bytes, so caching them
74
74
  # would hold memory nothing reads and let later turns skip re-verifying.
75
- #: (Riffer::Messages::FilePart, cache: (false | Symbol)) -> void
75
+ #: (Riffer::Messages::User::FilePart, cache: (false | Symbol)) -> void
76
76
  def download!(file, cache:)
77
77
  raise Riffer::FileDownloadsDisabledError, "File attachments are disabled" unless @config.allow_downloads
78
78
 
@@ -4,13 +4,13 @@
4
4
  # Records a guardrail transformation event.
5
5
  class Riffer::Guardrails::Modification
6
6
  # The guardrail class that transformed data.
7
- attr_reader :guardrail #: singleton(Riffer::Guardrail)
7
+ attr_reader :guardrail #: singleton(Riffer::Guardrail) # @dynamic guardrail
8
8
 
9
9
  # The phase when the transformation occurred (:before or :after).
10
- attr_reader :phase #: Symbol
10
+ attr_reader :phase #: Symbol # @dynamic phase
11
11
 
12
12
  # The indices of messages that were changed.
13
- attr_reader :message_indices #: Array[Integer]
13
+ attr_reader :message_indices #: Array[Integer] # @dynamic message_indices
14
14
 
15
15
  #--
16
16
  #: (guardrail: singleton(Riffer::Guardrail), phase: Symbol, message_indices: Array[Integer]) -> void
@@ -7,13 +7,13 @@ class Riffer::Guardrails::Result
7
7
  TYPES = %i[pass transform block].freeze #: Array[Symbol]
8
8
 
9
9
  # The result type (:pass, :transform, or :block).
10
- attr_reader :type #: Symbol
10
+ attr_reader :type #: Symbol # @dynamic type
11
11
 
12
12
  # The data (for pass/transform) or reason (for block).
13
- attr_reader :data #: untyped
13
+ attr_reader :data #: untyped # @dynamic data
14
14
 
15
15
  # Optional metadata for block results.
16
- attr_reader :metadata #: Hash[Symbol, untyped]?
16
+ attr_reader :metadata #: Hash[Symbol, untyped]? # @dynamic metadata
17
17
 
18
18
  class << self
19
19
  # Creates a pass result that continues with unchanged data.
@@ -5,16 +5,16 @@
5
5
  # any blocks, execution stops and a tripwire is returned.
6
6
  class Riffer::Guardrails::Runner
7
7
  # The guardrail configs to execute.
8
- attr_reader :guardrail_configs #: Array[Hash[Symbol, untyped]]
8
+ attr_reader :guardrail_configs #: Array[Hash[Symbol, untyped]] # @dynamic guardrail_configs
9
9
 
10
10
  # The execution phase (:before or :after).
11
- attr_reader :phase #: Symbol
11
+ attr_reader :phase #: Symbol # @dynamic phase
12
12
 
13
13
  # The context passed to guardrails.
14
- attr_reader :context #: untyped
14
+ attr_reader :context #: untyped # @dynamic context
15
15
 
16
16
  # The normalized per-call tags, stamped as +riffer.tag.*+ on guardrail spans.
17
- attr_reader :tags #: Hash[String, String]
17
+ attr_reader :tags #: Hash[String, String] # @dynamic tags
18
18
 
19
19
  #--
20
20
  #: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void
@@ -6,16 +6,16 @@ class Riffer::Guardrails::Tripwire
6
6
  PHASES = Riffer::Guardrails::PHASES #: Array[Symbol]
7
7
 
8
8
  # The reason for blocking.
9
- attr_reader :reason #: String
9
+ attr_reader :reason #: String # @dynamic reason
10
10
 
11
11
  # The guardrail class that triggered the block.
12
- attr_reader :guardrail #: singleton(Riffer::Guardrail)
12
+ attr_reader :guardrail #: singleton(Riffer::Guardrail) # @dynamic guardrail
13
13
 
14
14
  # The phase when the block occurred (:before or :after).
15
- attr_reader :phase #: Symbol
15
+ attr_reader :phase #: Symbol # @dynamic phase
16
16
 
17
17
  # Optional metadata about the block.
18
- attr_reader :metadata #: Hash[Symbol, untyped]?
18
+ attr_reader :metadata #: Hash[Symbol, untyped]? # @dynamic metadata
19
19
 
20
20
  # Raises Riffer::ArgumentError if +phase+ is invalid.
21
21
  #--
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Copying for the nested Hashes and Arrays a configuration object holds.
5
+ module Riffer::Helpers::DeepDup
6
+ extend self
7
+
8
+ # Returns +value+ with every Hash and Array rebuilt, so a copy shares no
9
+ # collection with its source.
10
+ #
11
+ # Anything else is returned as-is, which a Class or a Proc needs: +Class#dup+
12
+ # answers a new anonymous class. One source collection maps to one copy, so
13
+ # references shared within +value+ stay shared in the result — an +:around+
14
+ # guardrail registered under two phases is still one registration afterwards.
15
+ #--
16
+ #: (untyped) -> untyped
17
+ def call(value)
18
+ seen = {} #: Hash[untyped, untyped]
19
+
20
+ rebuild(value, seen.compare_by_identity)
21
+ end
22
+
23
+ private
24
+
25
+ #--
26
+ #: (untyped, Hash[untyped, untyped]) -> untyped
27
+ def rebuild(value, seen)
28
+ return seen[value] if seen.key?(value)
29
+
30
+ case value
31
+ when Hash
32
+ copy = seen[value] = {}
33
+ value.each { |key, entry| copy[key] = rebuild(entry, seen) }
34
+ copy
35
+ when Array
36
+ copy = seen[value] = []
37
+ value.each { |entry| copy << rebuild(entry, seen) }
38
+ copy
39
+ else value
40
+ end
41
+ end
42
+ end
@@ -6,20 +6,20 @@ require "uri"
6
6
  # Holds the configuration for a single MCP server.
7
7
  class Riffer::Mcp::Manifest
8
8
  # Identifier used as the registration key and generated-agent identifier.
9
- attr_reader :name #: String
9
+ attr_reader :name #: String # @dynamic name
10
10
 
11
11
  # Tags for matching +use_mcp+.
12
- attr_reader :tags #: Array[Symbol]
12
+ attr_reader :tags #: Array[Symbol] # @dynamic tags
13
13
 
14
14
  # HTTPS URL passed to the MCP transport.
15
- attr_reader :endpoint #: String
15
+ attr_reader :endpoint #: String # @dynamic endpoint
16
16
 
17
17
  # Headers (or a Proc) resolved once when building the discovery client.
18
- attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)?
18
+ attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)? # @dynamic discovery_headers
19
19
 
20
20
  # Optional hint (+:global+/+:tenant+/+:user+) for whether invocation
21
21
  # credentials depend on tenant/user keys in +context+.
22
- attr_reader :credentials_scope #: Symbol?
22
+ attr_reader :credentials_scope #: Symbol? # @dynamic credentials_scope
23
23
 
24
24
  # Raises Riffer::ArgumentError unless +name+ is present and +endpoint+ is a
25
25
  # valid HTTPS URL.
@@ -9,7 +9,7 @@ class Riffer::Mcp::Registration
9
9
  # @rbs @mutex: Thread::Mutex
10
10
 
11
11
  # The manifest that describes this server.
12
- attr_reader :manifest #: Riffer::Mcp::Manifest
12
+ attr_reader :manifest #: Riffer::Mcp::Manifest # @dynamic manifest
13
13
 
14
14
  # Generated Riffer::Mcp::Tool subclasses.
15
15
  #
@@ -8,7 +8,7 @@ class Riffer::Mcp::SearchTool < Riffer::Tool
8
8
  # Successful search response carrying the matched tool classes.
9
9
  class Result < Riffer::Tools::Response
10
10
  # Tool classes that matched the search query.
11
- attr_reader :discovered_tools #: Array[singleton(Riffer::Tool)]
11
+ attr_reader :discovered_tools #: Array[singleton(Riffer::Tool)] # @dynamic discovered_tools
12
12
 
13
13
  #--
14
14
  #: (String, Array[singleton(Riffer::Tool)]) -> void
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Represents one block of model reasoning attached to an assistant message.
5
+ # riffer stores and replays a part verbatim rather than interpreting it, so a
6
+ # provider that requires its own thinking blocks back gets them unchanged.
7
+ class Riffer::Messages::Assistant::ReasoningPart
8
+ TYPES = %i[text summary encrypted].freeze #: Array[Symbol]
9
+
10
+ # What the part carries: readable reasoning (+:text+), a provider-condensed
11
+ # digest (+:summary+), or an opaque payload (+:encrypted+).
12
+ attr_reader :type #: Symbol # @dynamic type
13
+
14
+ # The reasoning prose, for +:text+ and +:summary+ parts.
15
+ attr_reader :text #: String? # @dynamic text
16
+
17
+ # The opaque payload, for +:encrypted+ parts.
18
+ attr_reader :data #: String? # @dynamic data
19
+
20
+ # The provider's signature over the part, when it issues one.
21
+ attr_reader :signature #: String? # @dynamic signature
22
+
23
+ # The provider's identifier for the part, when it issues one.
24
+ attr_reader :id #: String? # @dynamic id
25
+
26
+ # The wire format of the part, owned by the provider adapter that produced it
27
+ # (e.g. <tt>"anthropic-claude-v1"</tt>). Adapters replay only the formats they
28
+ # recognize, so it is never validated here.
29
+ attr_reader :format #: String? # @dynamic format
30
+
31
+ # Builds a ReasoningPart from a hash, or returns +part+ unchanged when it is
32
+ # already a ReasoningPart.
33
+ #--
34
+ #: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart)) -> Riffer::Messages::Assistant::ReasoningPart
35
+ def self.from_hash(part)
36
+ return part if part.is_a?(Riffer::Messages::Assistant::ReasoningPart)
37
+
38
+ new(
39
+ type: part[:type].to_sym,
40
+ text: part[:text],
41
+ data: part[:data],
42
+ signature: part[:signature],
43
+ id: part[:id],
44
+ format: part[:format],
45
+ )
46
+ end
47
+
48
+ # Raises Riffer::ArgumentError on a +type+ outside TYPES.
49
+ #--
50
+ #: (type: Symbol, ?text: String?, ?data: String?, ?signature: String?, ?id: String?, ?format: String?) -> void
51
+ def initialize(type:, text: nil, data: nil, signature: nil, id: nil, format: nil)
52
+ unless TYPES.include?(type)
53
+ raise Riffer::ArgumentError,
54
+ "type must be one of #{TYPES.inspect}, got #{type.inspect}"
55
+ end
56
+
57
+ @type = type
58
+ @text = text
59
+ @data = data
60
+ @signature = signature
61
+ @id = id
62
+ @format = format
63
+ end
64
+
65
+ # Serializes the part to a hash, omitting the fields it doesn't carry.
66
+ #
67
+ #--
68
+ #: () -> Hash[Symbol, untyped]
69
+ def to_h
70
+ { type: type, text: text, data: data, signature: signature, id: id, format: format }.compact
71
+ end
72
+
73
+ #--
74
+ #: (untyped) -> bool
75
+ def ==(other)
76
+ other.is_a?(Riffer::Messages::Assistant::ReasoningPart) && to_h == other.to_h
77
+ end
78
+
79
+ #--
80
+ #: (untyped) -> bool
81
+ def eql?(other)
82
+ self == other
83
+ end
84
+
85
+ #--
86
+ #: () -> Integer
87
+ def hash
88
+ to_h.hash
89
+ end
90
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Represents one tool invocation the model requested on an assistant message.
5
+ class Riffer::Messages::Assistant::ToolCall
6
+ # The provider's identifier for the call, echoed back on the tool result.
7
+ attr_reader :call_id #: String # @dynamic call_id
8
+
9
+ # The name of the tool to invoke.
10
+ attr_reader :name #: String # @dynamic name
11
+
12
+ # The JSON-encoded arguments, exactly as the provider emitted them.
13
+ attr_reader :arguments #: String # @dynamic arguments
14
+
15
+ # Builds a ToolCall from a hash, or returns +call+ unchanged when it is
16
+ # already a ToolCall. Raises Riffer::ArgumentError when the hash is missing
17
+ # a field.
18
+ #--
19
+ #: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant::ToolCall)) -> Riffer::Messages::Assistant::ToolCall
20
+ def self.from_hash(call)
21
+ return call if call.is_a?(Riffer::Messages::Assistant::ToolCall)
22
+
23
+ if call.values_at(:call_id, :name, :arguments).any?(&:nil?)
24
+ raise Riffer::ArgumentError, "Tool call hash must include :call_id, :name, and :arguments"
25
+ end
26
+
27
+ new(call_id: call[:call_id], name: call[:name], arguments: call[:arguments])
28
+ end
29
+
30
+ #--
31
+ #: (call_id: String, name: String, arguments: String) -> void
32
+ def initialize(call_id:, name:, arguments:)
33
+ @call_id = call_id
34
+ @name = name
35
+ @arguments = arguments
36
+ end
37
+
38
+ # Serializes the call to a hash.
39
+ #
40
+ #--
41
+ #: () -> Hash[Symbol, untyped]
42
+ def to_h
43
+ { call_id: call_id, name: name, arguments: arguments }
44
+ end
45
+
46
+ #--
47
+ #: (untyped) -> bool
48
+ def ==(other)
49
+ other.is_a?(Riffer::Messages::Assistant::ToolCall) && to_h == other.to_h
50
+ end
51
+
52
+ #--
53
+ #: (untyped) -> bool
54
+ def eql?(other)
55
+ self == other
56
+ end
57
+
58
+ #--
59
+ #: () -> Integer
60
+ def hash
61
+ to_h.hash
62
+ end
63
+ end
@@ -4,24 +4,49 @@
4
4
  # Represents an assistant (LLM) message in a conversation; may include tool
5
5
  # calls when the LLM requests tool execution.
6
6
  class Riffer::Messages::Assistant < Riffer::Messages::Base
7
- ToolCall = Struct.new(:call_id, :name, :arguments)
7
+ # The reasoning part types +reasoning_text+ reads; the rest carry no prose.
8
+ REASONING_TEXT_TYPES = %i[text summary].freeze #: Array[Symbol]
9
+
10
+ # Builds an Assistant message from a hash, or returns +msg+ unchanged when it
11
+ # is already an Assistant message. Raises Riffer::ArgumentError on an invalid
12
+ # tool call, reasoning part, or +finish_reason+.
13
+ #--
14
+ #: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant)) -> Riffer::Messages::Assistant
15
+ def self.from_hash(msg)
16
+ return msg if msg.is_a?(Riffer::Messages::Assistant)
17
+
18
+ new(
19
+ msg[:content],
20
+ id: msg[:id],
21
+ tool_calls: (msg[:tool_calls] || []).map { |tc| Riffer::Messages::Assistant::ToolCall.from_hash(tc) },
22
+ reasoning: (msg[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
23
+ token_usage: msg[:token_usage] && Riffer::Providers::TokenUsage.from_hash(msg[:token_usage]),
24
+ structured_output: msg[:structured_output],
25
+ finish_reason: msg[:finish_reason]&.to_sym,
26
+ finish_reason_raw: msg[:finish_reason_raw],
27
+ )
28
+ end
8
29
 
9
30
  # Array of tool calls requested by the assistant.
10
- attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
31
+ attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall] # @dynamic tool_calls
32
+
33
+ # The model's reasoning blocks for this response, in the order the provider
34
+ # emitted them.
35
+ attr_reader :reasoning #: Array[Riffer::Messages::Assistant::ReasoningPart] # @dynamic reasoning
11
36
 
12
37
  # Token usage data for this response.
13
- attr_reader :token_usage #: Riffer::Providers::TokenUsage?
38
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
14
39
 
15
40
  # Parsed structured output hash, or nil when not applicable.
16
- attr_reader :structured_output #: Hash[Symbol, untyped]?
41
+ attr_reader :structured_output #: Hash[Symbol, untyped]? # @dynamic structured_output
17
42
 
18
43
  # Normalized reason the provider finished this response, when reported (see
19
44
  # <tt>Riffer::Providers::FinishReason::VALUES</tt>).
20
- attr_reader :finish_reason #: Symbol?
45
+ attr_reader :finish_reason #: Symbol? # @dynamic finish_reason
21
46
 
22
47
  # The provider's raw finish-reason value behind +finish_reason+, when one
23
48
  # exists on the wire.
24
- attr_reader :finish_reason_raw #: String?
49
+ attr_reader :finish_reason_raw #: String? # @dynamic finish_reason_raw
25
50
 
26
51
  # Raises Riffer::ArgumentError when +finish_reason+ is outside the
27
52
  # normalized vocabulary.
@@ -30,6 +55,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
30
55
  # String,
31
56
  # ?id: String?,
32
57
  # ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall],
58
+ # ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
33
59
  # ?token_usage: Riffer::Providers::TokenUsage?,
34
60
  # ?structured_output: Hash[Symbol, untyped]?,
35
61
  # ?finish_reason: Symbol?,
@@ -39,6 +65,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
39
65
  content,
40
66
  id: nil,
41
67
  tool_calls: [],
68
+ reasoning: [],
42
69
  token_usage: nil,
43
70
  structured_output: nil,
44
71
  finish_reason: nil,
@@ -51,6 +78,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
51
78
 
52
79
  super(content, id: id)
53
80
  @tool_calls = tool_calls
81
+ @reasoning = reasoning
54
82
  @token_usage = token_usage
55
83
  @structured_output = structured_output
56
84
  @finish_reason = finish_reason
@@ -75,10 +103,29 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
75
103
  !@tool_calls.empty?
76
104
  end
77
105
 
106
+ #--
107
+ #: () -> bool
108
+ def reasoning?
109
+ !@reasoning.empty?
110
+ end
111
+
112
+ # The readable reasoning across this message's +:text+ and +:summary+ parts,
113
+ # joined by blank lines, or nil when it carries none.
114
+ #--
115
+ #: () -> String?
116
+ def reasoning_text
117
+ texts = reasoning.filter_map { |part| part.text if REASONING_TEXT_TYPES.include?(part.type) }
118
+ texts.empty? ? nil : texts.join("\n\n")
119
+ end
120
+
78
121
  #--
79
122
  #: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
80
123
  def +(other)
81
- self.class.new("#{content}\n\n#{other.content}", tool_calls: tool_calls + other.tool_calls)
124
+ self.class.new(
125
+ "#{content}\n\n#{other.content}",
126
+ tool_calls: tool_calls + other.tool_calls,
127
+ reasoning: reasoning + other.reasoning,
128
+ )
82
129
  end
83
130
 
84
131
  # Converts the message to a hash.
@@ -89,6 +136,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
89
136
  hash = { role: role, content: content } #: Hash[Symbol, untyped]
90
137
  hash[:id] = id if id
91
138
  hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
139
+ hash[:reasoning] = reasoning.map(&:to_h) if reasoning?
92
140
  hash[:token_usage] = token_usage.to_h if token_usage
93
141
  hash[:structured_output] = structured_output if structured_output?
94
142
  hash[:finish_reason] = finish_reason if finish_reason