riffer 0.33.0 → 0.35.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +19 -0
  4. data/docs/03_AGENTS.md +44 -0
  5. data/docs/10_CONFIGURATION.md +15 -15
  6. data/docs/16_TRACING.md +32 -4
  7. data/docs/17_METRICS.md +36 -4
  8. data/lib/riffer/agent/run.rb +82 -49
  9. data/lib/riffer/agent.rb +20 -13
  10. data/lib/riffer/config.rb +23 -24
  11. data/lib/riffer/guardrails/runner.rb +40 -11
  12. data/lib/riffer/metrics/instruments.rb +6 -0
  13. data/lib/riffer/metrics/{null.rb → no_op.rb} +1 -1
  14. data/lib/riffer/metrics/otel.rb +4 -3
  15. data/lib/riffer/metrics.rb +7 -6
  16. data/lib/riffer/providers/amazon_bedrock.rb +7 -1
  17. data/lib/riffer/providers/anthropic.rb +8 -1
  18. data/lib/riffer/providers/base.rb +38 -26
  19. data/lib/riffer/providers/gemini.rb +5 -1
  20. data/lib/riffer/providers/open_ai.rb +10 -1
  21. data/lib/riffer/providers/open_router.rb +10 -1
  22. data/lib/riffer/tools/runtime.rb +30 -20
  23. data/lib/riffer/tracing/{null.rb → no_op.rb} +3 -3
  24. data/lib/riffer/tracing/otel.rb +4 -3
  25. data/lib/riffer/tracing.rb +9 -9
  26. data/lib/riffer/version.rb +1 -1
  27. data/sig/generated/riffer/agent/run.rbs +47 -30
  28. data/sig/generated/riffer/agent.rbs +16 -9
  29. data/sig/generated/riffer/config.rbs +14 -14
  30. data/sig/generated/riffer/guardrails/runner.rbs +15 -4
  31. data/sig/generated/riffer/metrics/instruments.rbs +2 -0
  32. data/sig/generated/riffer/metrics/{null.rbs → no_op.rbs} +2 -2
  33. data/sig/generated/riffer/metrics/otel.rbs +4 -3
  34. data/sig/generated/riffer/metrics.rbs +7 -6
  35. data/sig/generated/riffer/providers/base.rbs +26 -20
  36. data/sig/generated/riffer/tools/runtime.rbs +24 -18
  37. data/sig/generated/riffer/tracing/{null.rbs → no_op.rbs} +5 -5
  38. data/sig/generated/riffer/tracing/otel.rbs +4 -3
  39. data/sig/generated/riffer/tracing.rbs +9 -9
  40. data/sig/manual/riffer/metrics/no_op.rbs +5 -0
  41. data/sig/manual/riffer/tracing/no_op.rbs +5 -0
  42. metadata +11 -11
  43. data/sig/manual/riffer/metrics/null.rbs +0 -5
  44. data/sig/manual/riffer/tracing/null.rbs +0 -5
@@ -18,14 +18,16 @@ class Riffer::Tools::Runtime
18
18
 
19
19
  # Executes a batch of tool calls, returning <tt>[tool_call, response]</tt> pairs.
20
20
  #--
21
- #: (Array[Riffer::Messages::Assistant::ToolCall], tools: Array[singleton(Riffer::Tool)], context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) -> Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]
22
- def execute(tool_calls, tools:, context:, assistant_message: nil)
21
+ #: (Array[Riffer::Messages::Assistant::ToolCall], tools: Array[singleton(Riffer::Tool)], context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?, ?tags: Hash[String, String]) -> Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]
22
+ def execute(tool_calls, tools:, context:, assistant_message: nil, tags: {})
23
23
  # Each Runner worker runs in its own thread/fiber, where the OTEL context
24
24
  # starts empty — capture here so the execute_tool span parents correctly.
25
+ # tags are an ordinary local captured in the block, so they reach the
26
+ # worker's span/metric without re-propagation.
25
27
  trace_context = Riffer::Tracing.current_context
26
28
  @runner.map(tool_calls, context: context) do |tool_call|
27
29
  Riffer::Tracing.with_context(trace_context) do
28
- instrument_tool_call(tool_call) do
30
+ instrument_tool_call(tool_call, tags) do
29
31
  around_tool_call(tool_call, context: context, assistant_message: assistant_message) do
30
32
  dispatch_tool_call(tool_call, tools: tools, context: context, assistant_message: assistant_message)
31
33
  end
@@ -57,12 +59,12 @@ class Riffer::Tools::Runtime
57
59
  private
58
60
 
59
61
  #--
60
- #: (Riffer::Messages::Assistant::ToolCall) { () -> Riffer::Tools::Response } -> [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
61
- def instrument_tool_call(tool_call)
62
+ #: (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { () -> Riffer::Tools::Response } -> [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
63
+ def instrument_tool_call(tool_call, tags = {})
62
64
  start = Riffer::Metrics.monotonic_now
63
65
  error_type = nil #: String?
64
66
  begin
65
- result = in_tool_span(tool_call) do |span|
67
+ result = in_tool_span(tool_call, tags) do |span|
66
68
  response = yield
67
69
  record_tool_outcome(span, response)
68
70
  response
@@ -73,7 +75,7 @@ class Riffer::Tools::Runtime
73
75
  error_type = error.class.name #: String?
74
76
  raise
75
77
  ensure
76
- Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: tool_metric_attributes(tool_call, error_type))
78
+ Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: tool_metric_attributes(tool_call, error_type, tags))
77
79
  end
78
80
  end
79
81
 
@@ -113,9 +115,9 @@ class Riffer::Tools::Runtime
113
115
 
114
116
  # Emitted outside +around_tool_call+ so host enrichment spans nest beneath it.
115
117
  #--
116
- #: [R] (Riffer::Messages::Assistant::ToolCall) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span)) -> R } -> R
117
- def in_tool_span(tool_call)
118
- Riffer::Tracing.in_span("execute_tool #{tool_call.name}", attributes: tool_span_attributes(tool_call), kind: :internal) do |span|
118
+ #: [R] (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> R } -> R
119
+ def in_tool_span(tool_call, tags = {})
120
+ Riffer::Tracing.in_span("execute_tool #{tool_call.name}", attributes: tool_span_attributes(tool_call, tags), kind: :internal) do |span|
119
121
  capture_tool_arguments(span, tool_call)
120
122
  yield span
121
123
  rescue => error
@@ -127,30 +129,38 @@ class Riffer::Tools::Runtime
127
129
  end
128
130
 
129
131
  #--
130
- #: (Riffer::Messages::Assistant::ToolCall) -> Hash[String, untyped]
131
- def tool_span_attributes(tool_call)
132
+ #: (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) -> Hash[String, untyped]
133
+ def tool_span_attributes(tool_call, tags = {})
132
134
  {
133
135
  "gen_ai.operation.name" => "execute_tool",
134
136
  "gen_ai.tool.name" => tool_call.name,
135
137
  "gen_ai.tool.call.id" => tool_call.call_id
136
- }
138
+ }.merge(tag_attributes(tags))
137
139
  end
138
140
 
139
141
  #--
140
- #: (Riffer::Messages::Assistant::ToolCall, String?) -> Hash[String, untyped]
141
- def tool_metric_attributes(tool_call, error_type)
142
+ #: (Riffer::Messages::Assistant::ToolCall, String?, ?Hash[String, String]) -> Hash[String, untyped]
143
+ def tool_metric_attributes(tool_call, error_type, tags = {})
142
144
  attributes = {
143
145
  "gen_ai.operation.name" => "execute_tool",
144
146
  "gen_ai.tool.name" => tool_call.name
145
147
  } #: Hash[String, untyped]
146
148
  attributes["error.type"] = error_type if error_type
147
- attributes
149
+ attributes.merge(tag_attributes(tags))
150
+ end
151
+
152
+ # Maps normalized tags to their namespaced span/metric attribute form. An
153
+ # empty map yields an empty hash, so merging it is a no-op.
154
+ #--
155
+ #: (Hash[String, String]) -> Hash[String, String]
156
+ def tag_attributes(tags)
157
+ tags.transform_keys { |key| "riffer.tag.#{key}" }
148
158
  end
149
159
 
150
160
  # A returned error Response is a handled outcome, so its status stays unset —
151
161
  # an error span status is reserved for a raised exception.
152
162
  #--
153
- #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span), Riffer::Tools::Response) -> void
163
+ #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void
154
164
  def record_tool_outcome(span, result)
155
165
  error_type = result.error_type
156
166
  span.set_attribute("error.type", error_type.to_s) if error_type
@@ -158,7 +168,7 @@ class Riffer::Tools::Runtime
158
168
  end
159
169
 
160
170
  #--
161
- #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span), Riffer::Messages::Assistant::ToolCall) -> void
171
+ #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Messages::Assistant::ToolCall) -> void
162
172
  def capture_tool_arguments(span, tool_call)
163
173
  return unless capture_tool_content?(span)
164
174
 
@@ -167,7 +177,7 @@ class Riffer::Tools::Runtime
167
177
  end
168
178
 
169
179
  #--
170
- #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span), Riffer::Tools::Response) -> void
180
+ #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void
171
181
  def capture_tool_result(span, result)
172
182
  return unless capture_tool_content?(span)
173
183
 
@@ -175,7 +185,7 @@ class Riffer::Tools::Runtime
175
185
  end
176
186
 
177
187
  #--
178
- #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span)) -> bool
188
+ #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> bool
179
189
  def capture_tool_content?(span)
180
190
  Riffer.config.tracing.capture_messages && span.recording?
181
191
  end
@@ -3,7 +3,7 @@
3
3
 
4
4
  # No-op tracing backend, used when OTEL is unavailable or tracing is
5
5
  # disabled.
6
- module Riffer::Tracing::Null # :nodoc: all
6
+ module Riffer::Tracing::NoOp # :nodoc: all
7
7
  extend self
8
8
 
9
9
  # No-op stand-in for a span; answers <tt>recording?</tt> with +false+ so
@@ -36,11 +36,11 @@ module Riffer::Tracing::Null # :nodoc: all
36
36
  end
37
37
  end
38
38
 
39
- SPAN = Span.new.freeze #: Riffer::Tracing::Null::Span
39
+ SPAN = Span.new.freeze #: Riffer::Tracing::NoOp::Span
40
40
 
41
41
  # Yields the no-op span, ignoring all span options.
42
42
  #--
43
- #: [R] (String, **untyped) { (Riffer::Tracing::Null::Span) -> R } -> R
43
+ #: [R] (String, **untyped) { (Riffer::Tracing::NoOp::Span) -> R } -> R
44
44
  def in_span(_name, **)
45
45
  yield SPAN
46
46
  end
@@ -52,10 +52,11 @@ class Riffer::Tracing::Otel # :nodoc: all
52
52
 
53
53
  class << self
54
54
  # Builds a backend when the OpenTelemetry API is loadable at a supported
55
- # version; returns +nil+ so resolution falls back to Null.
55
+ # version; returns +nil+ so resolution falls back to NoOp. +provider+
56
+ # defaults to the global <tt>OpenTelemetry.tracer_provider</tt>.
56
57
  #--
57
- #: (provider: untyped) -> Riffer::Tracing::Otel?
58
- def build(provider:)
58
+ #: (?provider: untyped) -> Riffer::Tracing::Otel?
59
+ def build(provider: nil)
59
60
  version = api_version
60
61
  return nil unless version
61
62
 
@@ -7,7 +7,7 @@
7
7
  module Riffer::Tracing # :nodoc: all
8
8
  extend self
9
9
 
10
- # @rbs @backend: (Riffer::Tracing::Otel | singleton(Riffer::Tracing::Null))?
10
+ # @rbs @backend: untyped
11
11
 
12
12
  MUTEX = Mutex.new #: Mutex
13
13
 
@@ -17,9 +17,9 @@ module Riffer::Tracing # :nodoc: all
17
17
 
18
18
  # Opens a span around the block, yielding the span.
19
19
  #--
20
- #: [R] (String, ?attributes: Hash[String, untyped]?, ?kind: Symbol) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span) -> R } -> R
20
+ #: [R] (String, ?attributes: Hash[String, untyped]?, ?kind: Symbol) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R
21
21
  def in_span(name, attributes: nil, kind: :internal, &block)
22
- return Null.in_span(name, &block) unless Riffer.config.tracing.enabled
22
+ return NoOp.in_span(name, &block) unless Riffer.config.tracing.enabled
23
23
  backend.in_span(name, attributes: attributes, kind: kind, &block)
24
24
  end
25
25
 
@@ -28,7 +28,7 @@ module Riffer::Tracing # :nodoc: all
28
28
  #--
29
29
  #: () -> untyped
30
30
  def current_context
31
- return Null.current_context unless Riffer.config.tracing.enabled
31
+ return NoOp.current_context unless Riffer.config.tracing.enabled
32
32
  backend.current_context
33
33
  end
34
34
 
@@ -37,14 +37,14 @@ module Riffer::Tracing # :nodoc: all
37
37
  #--
38
38
  #: [R] (untyped) { () -> R } -> R
39
39
  def with_context(context, &block)
40
- return Null.with_context(context, &block) unless Riffer.config.tracing.enabled
40
+ return NoOp.with_context(context, &block) unless Riffer.config.tracing.enabled
41
41
  backend.with_context(context, &block)
42
42
  end
43
43
 
44
44
  # Stamps token usage onto the span — the <tt>gen_ai.usage.*</tt> counts and,
45
45
  # when the model was priced, <tt>riffer.cost</tt>.
46
46
  #--
47
- #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span), Riffer::Providers::TokenUsage?) -> void
47
+ #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Providers::TokenUsage?) -> void
48
48
  def record_usage(span, usage)
49
49
  return unless usage
50
50
 
@@ -65,14 +65,14 @@ module Riffer::Tracing # :nodoc: all
65
65
  private
66
66
 
67
67
  #--
68
- #: () -> (Riffer::Tracing::Otel | singleton(Riffer::Tracing::Null))
68
+ #: () -> untyped
69
69
  def backend
70
70
  @backend || MUTEX.synchronize { @backend ||= resolve_backend }
71
71
  end
72
72
 
73
73
  #--
74
- #: () -> (Riffer::Tracing::Otel | singleton(Riffer::Tracing::Null))
74
+ #: () -> untyped
75
75
  def resolve_backend
76
- Otel.build(provider: Riffer.config.tracing.tracer_provider) || Null
76
+ Riffer.config.tracing.backend || NoOp
77
77
  end
78
78
  end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.33.0" #: String
5
+ VERSION = "0.35.0" #: String
6
6
  end
@@ -7,29 +7,34 @@ module Riffer::Agent::Run
7
7
  # for prompt/files semantics.
8
8
  #
9
9
  # --
10
- # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
11
- def generate: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
10
+ # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
11
+ def generate: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Riffer::Agent::Response
12
12
 
13
13
  # Runs the streaming loop for the given agent. See Riffer::Agent#stream
14
14
  # for prompt/files semantics.
15
15
  #
16
16
  # --
17
- # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
18
- def stream: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
17
+ # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
18
+ def stream: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
19
19
 
20
20
  private
21
21
 
22
+ # Both +generate+ and +stream+ funnel here, so this is the single place raw
23
+ # +tags+ are normalized. The clean <tt>String => String</tt> map is then
24
+ # threaded to every span/metric builder in the run as +riffer.tag.*+ and to
25
+ # each provider call (via +merged_model_options+) for native request-metadata
26
+ # mapping.
22
27
  # --
23
- # : (Riffer::Agent, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
24
- def run_loop: (Riffer::Agent, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
28
+ # : (Riffer::Agent, ?tags: Hash[(String | Symbol), untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
29
+ def run_loop: (Riffer::Agent, ?tags: Hash[String | Symbol, untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
25
30
 
26
31
  # --
27
- # : (Riffer::Agent, Enumerator::Yielder?) -> Riffer::Agent::Response
28
- def execute_run: (Riffer::Agent, Enumerator::Yielder?) -> Riffer::Agent::Response
32
+ # : (Riffer::Agent, Enumerator::Yielder?, ?Hash[String, String]) -> Riffer::Agent::Response
33
+ def execute_run: (Riffer::Agent, Enumerator::Yielder?, ?Hash[String, String]) -> Riffer::Agent::Response
29
34
 
30
35
  # --
31
- # : (Riffer::Agent, Enumerator::Yielder) -> Riffer::Messages::Assistant
32
- def accumulate_streamed_response: (Riffer::Agent, Enumerator::Yielder) -> Riffer::Messages::Assistant
36
+ # : (Riffer::Agent, Enumerator::Yielder, ?Hash[String, String]) -> Riffer::Messages::Assistant
37
+ def accumulate_streamed_response: (Riffer::Agent, Enumerator::Yielder, ?Hash[String, String]) -> Riffer::Messages::Assistant
33
38
 
34
39
  # --
35
40
  # : (Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], Array[Riffer::Guardrails::Modification]) -> void
@@ -44,32 +49,32 @@ module Riffer::Agent::Run
44
49
  def final_response: (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
45
50
 
46
51
  # --
47
- # : (Riffer::Agent) -> Riffer::Messages::Assistant
48
- def call_llm: (Riffer::Agent) -> Riffer::Messages::Assistant
52
+ # : (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
53
+ def call_llm: (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
49
54
 
50
55
  # --
51
- # : (Riffer::Agent) -> Enumerator[Riffer::StreamEvents::Base, void]
52
- def call_llm_stream: (Riffer::Agent) -> Enumerator[Riffer::StreamEvents::Base, void]
56
+ # : (Riffer::Agent, ?Hash[String, String]) -> Enumerator[Riffer::StreamEvents::Base, void]
57
+ def call_llm_stream: (Riffer::Agent, ?Hash[String, String]) -> Enumerator[Riffer::StreamEvents::Base, void]
53
58
 
54
59
  # --
55
- # : (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall]) -> void
56
- def execute_tool_calls: (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall]) -> void
60
+ # : (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?tags: Hash[String, String]) -> void
61
+ def execute_tool_calls: (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?tags: Hash[String, String]) -> void
57
62
 
58
63
  # --
59
64
  # : (Riffer::Agent, Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]) -> void
60
65
  def inject_discovered_tools: (Riffer::Agent, Array[[ Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response ]]) -> void
61
66
 
62
67
  # --
63
- # : (Riffer::Agent) -> void
64
- def execute_pending_tool_calls: (Riffer::Agent) -> void
68
+ # : (Riffer::Agent, ?Hash[String, String]) -> void
69
+ def execute_pending_tool_calls: (Riffer::Agent, ?Hash[String, String]) -> void
65
70
 
66
71
  # --
67
- # : (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> void
68
- def run_before_guardrails: (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> void
72
+ # : (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> void
73
+ def run_before_guardrails: (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> void
69
74
 
70
75
  # --
71
- # : (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
72
- def run_after_guardrails: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
76
+ # : (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
77
+ def run_after_guardrails: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
73
78
 
74
79
  # --
75
80
  # : (Riffer::Agent, Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
@@ -79,9 +84,13 @@ module Riffer::Agent::Run
79
84
  # : (Riffer::Agent) -> Array[singleton(Riffer::Tool)]
80
85
  def effective_tools: (Riffer::Agent) -> Array[singleton(Riffer::Tool)]
81
86
 
87
+ # +tags+ rides in the options hash as a curated key the providers extract for
88
+ # native request-metadata mapping (alongside +:structured_output+); it never
89
+ # reaches an SDK call verbatim. Span/metric tagging is threaded separately to
90
+ # each builder.
82
91
  # --
83
- # : (Riffer::Agent) -> Hash[Symbol, untyped]
84
- def merged_model_options: (Riffer::Agent) -> Hash[Symbol, untyped]
92
+ # : (Riffer::Agent, ?Hash[String, String]) -> Hash[Symbol, untyped]
93
+ def merged_model_options: (Riffer::Agent, ?Hash[String, String]) -> Hash[Symbol, untyped]
85
94
 
86
95
  # --
87
96
  # : (Riffer::Agent, String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?healed_tool_call_ids: Array[String], ?token_usage: Riffer::Providers::TokenUsage?, ?steps: Integer) -> Riffer::Agent::Response
@@ -102,14 +111,22 @@ module Riffer::Agent::Run
102
111
  def sum_usage: (Riffer::Providers::TokenUsage?, Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
103
112
 
104
113
  # --
105
- # : (Riffer::Agent) -> Hash[String, untyped]
106
- def run_span_attributes: (Riffer::Agent) -> Hash[String, untyped]
114
+ # : (Riffer::Agent, ?Hash[String, String]) -> Hash[String, untyped]
115
+ def run_span_attributes: (Riffer::Agent, ?Hash[String, String]) -> Hash[String, untyped]
107
116
 
108
117
  # --
109
- # : (Riffer::Agent, String?) -> Hash[String, untyped]
110
- def run_metric_attributes: (Riffer::Agent, String?) -> Hash[String, untyped]
118
+ # : (Riffer::Agent, String?, ?Hash[String, String]) -> Hash[String, untyped]
119
+ def run_metric_attributes: (Riffer::Agent, String?, ?Hash[String, String]) -> Hash[String, untyped]
111
120
 
112
121
  # --
113
- # : (Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span, Riffer::Agent::Response) -> void
114
- def record_run_outcome: (Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span, Riffer::Agent::Response) -> void
122
+ # : (Hash[(String | Symbol), untyped]?) -> Hash[String, String]
123
+ def normalize_tags: (Hash[String | Symbol, untyped]?) -> Hash[String, String]
124
+
125
+ # --
126
+ # : (Hash[String, String]) -> Hash[String, String]
127
+ def tag_attributes: (Hash[String, String]) -> Hash[String, String]
128
+
129
+ # --
130
+ # : (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Agent::Response) -> void
131
+ def record_run_outcome: (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Agent::Response) -> void
115
132
  end
@@ -124,13 +124,13 @@ class Riffer::Agent
124
124
 
125
125
  # Generates a response using a new agent instance.
126
126
  # --
127
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
128
- def self.generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
127
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
128
+ def self.generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[String | Symbol, untyped]) -> Riffer::Agent::Response
129
129
 
130
130
  # Streams a response using a new agent instance.
131
131
  # --
132
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
133
- def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
132
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
133
+ def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
134
134
 
135
135
  # Reconstructs a runnable agent from a wire hash produced by +#to_h+.
136
136
  # --
@@ -213,18 +213,25 @@ class Riffer::Agent
213
213
  # against the current session, resuming a persisted conversation or pending
214
214
  # tool calls. +files:+ requires +prompt+.
215
215
  #
216
+ # +tags:+ is an optional flat hash of attribution tags applied to this single
217
+ # call: they propagate to the provider's native request-metadata field, and
218
+ # are stamped as +riffer.tag.*+ on every span and metric the call emits. See
219
+ # +docs/03_AGENTS.md+ for the per-provider mapping. The reserved key
220
+ # +user_id+ also maps to the provider's native user identifier where one
221
+ # exists.
222
+ #
216
223
  # --
217
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
218
- def generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
224
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
225
+ def generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Riffer::Agent::Response
219
226
 
220
227
  # Streams a response from the agent, returning an +Enumerator+ of
221
- # +Riffer::StreamEvents+. See +#generate+ for prompt/files semantics.
228
+ # +Riffer::StreamEvents+. See +#generate+ for prompt/files/tags semantics.
222
229
  #
223
230
  # Raises Riffer::ArgumentError if structured output is configured.
224
231
  #
225
232
  # --
226
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
227
- def stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
233
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
234
+ def stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
228
235
 
229
236
  # Interrupts the agent loop from an +on_message+ callback. Equivalent to
230
237
  # <tt>throw :riffer_interrupt, reason</tt>.
@@ -107,9 +107,9 @@ class Riffer::Config
107
107
  # content routinely carries sensitive data.
108
108
  attr_reader capture_messages: bool
109
109
 
110
- # Explicit OTEL tracer provider; defaults to +nil+, which resolves the
111
- # global <tt>OpenTelemetry.tracer_provider</tt> at first span.
112
- attr_reader tracer_provider: untyped
110
+ # The backend riffer routes spans through; defaults to +nil+, a no-op.
111
+ # Riffer auto-detects no backend; assigning one is opt-in.
112
+ attr_reader backend: untyped
113
113
 
114
114
  # --
115
115
  # : () -> void
@@ -129,12 +129,12 @@ class Riffer::Config
129
129
  # : (untyped) -> void
130
130
  def capture_messages=: (untyped) -> void
131
131
 
132
- # Sets an explicit tracer provider, forcing the OTEL backend. Raises
133
- # Riffer::ArgumentError when the OpenTelemetry API gem isn't available
134
- # at a supported version.
132
+ # Sets the tracing backend riffer routes spans through. Raises
133
+ # Riffer::ArgumentError unless the value is +nil+ or responds to the full
134
+ # delegated contract: +in_span+, +current_context+, and +with_context+.
135
135
  # --
136
136
  # : (untyped) -> void
137
- def tracer_provider=: (untyped) -> void
137
+ def backend=: (untyped) -> void
138
138
  end
139
139
 
140
140
  # Metrics-related global configuration, independent of +config.tracing+ so a
@@ -144,9 +144,9 @@ class Riffer::Config
144
144
  # no-op until a host wires an OTEL metrics SDK.
145
145
  attr_reader enabled: bool
146
146
 
147
- # Explicit OTEL meter provider; defaults to +nil+, which resolves the
148
- # global <tt>OpenTelemetry.meter_provider</tt> at first record.
149
- attr_reader meter_provider: untyped
147
+ # The backend riffer routes measurements through; defaults to +nil+, a no-op.
148
+ # Riffer auto-detects no backend; assigning one is opt-in.
149
+ attr_reader backend: untyped
150
150
 
151
151
  # --
152
152
  # : () -> void
@@ -159,12 +159,12 @@ class Riffer::Config
159
159
  # : (untyped) -> void
160
160
  def enabled=: (untyped) -> void
161
161
 
162
- # Sets an explicit meter provider, forcing the OTEL backend. Raises
163
- # Riffer::ArgumentError when the OpenTelemetry metrics API gem isn't
164
- # available at a supported version.
162
+ # Sets the metrics backend riffer routes measurements through. Raises
163
+ # Riffer::ArgumentError unless the value is +nil+ or responds to
164
+ # +record_histogram+.
165
165
  # --
166
166
  # : (untyped) -> void
167
- def meter_provider=: (untyped) -> void
167
+ def backend=: (untyped) -> void
168
168
  end
169
169
 
170
170
  # Consumer-configured token pricing, keyed by +provider/model+ id. Riffer
@@ -12,9 +12,12 @@ class Riffer::Guardrails::Runner
12
12
  # The context passed to guardrails.
13
13
  attr_reader context: untyped
14
14
 
15
+ # The normalized per-call tags, stamped as +riffer.tag.*+ on guardrail spans.
16
+ attr_reader tags: Hash[String, String]
17
+
15
18
  # --
16
- # : (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped) -> void
17
- def initialize: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped) -> void
19
+ # : (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void
20
+ def initialize: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void
18
21
 
19
22
  # Runs the guardrails sequentially. For the +:before+ phase +data+ is the
20
23
  # messages array; for +:after+ it's the response (and +messages+ must be
@@ -37,6 +40,10 @@ class Riffer::Guardrails::Runner
37
40
  # : (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
38
41
  def execute_guardrail: (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
39
42
 
43
+ # --
44
+ # : (Riffer::Guardrail) { () -> Riffer::Guardrails::Result } -> Riffer::Guardrails::Result
45
+ def instrument_guardrail: (Riffer::Guardrail) { () -> Riffer::Guardrails::Result } -> Riffer::Guardrails::Result
46
+
40
47
  # --
41
48
  # : (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
42
49
  def run_guardrail_phase: (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
@@ -45,9 +52,13 @@ class Riffer::Guardrails::Runner
45
52
  # : (Riffer::Guardrail) -> Hash[String, untyped]
46
53
  def guardrail_span_attributes: (Riffer::Guardrail) -> Hash[String, untyped]
47
54
 
55
+ # --
56
+ # : (Riffer::Guardrail, String?) -> Hash[String, untyped]
57
+ def guardrail_metric_attributes: (Riffer::Guardrail, String?) -> Hash[String, untyped]
58
+
48
59
  # A block is a handled outcome, so its span status stays unset — an error
49
60
  # span status is reserved for a raised exception.
50
61
  # --
51
- # : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span), Riffer::Guardrails::Result) -> void
52
- def record_guardrail_outcome: (Riffer::Tracing::Otel::Span | Riffer::Tracing::Null::Span, Riffer::Guardrails::Result) -> void
62
+ # : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Guardrails::Result) -> void
63
+ def record_guardrail_outcome: (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Guardrails::Result) -> void
53
64
  end
@@ -10,4 +10,6 @@ module Riffer::Metrics::Instruments
10
10
  TOKEN_USAGE: Riffer::Metrics::Histogram
11
11
 
12
12
  COST: Riffer::Metrics::Histogram
13
+
14
+ GUARDRAIL_DURATION: Riffer::Metrics::Histogram
13
15
  end
@@ -1,8 +1,8 @@
1
- # Generated from lib/riffer/metrics/null.rb with RBS::Inline
1
+ # Generated from lib/riffer/metrics/no_op.rb with RBS::Inline
2
2
 
3
3
  # No-op metrics backend, used when the OpenTelemetry metrics API is unavailable
4
4
  # or metrics are disabled.
5
- module Riffer::Metrics::Null
5
+ module Riffer::Metrics::NoOp
6
6
  # Ignores the measurement; there is no meter without the OTEL metrics API.
7
7
  # --
8
8
  # : (String, Numeric, unit: String?, description: String?, attributes: Hash[String, untyped]?) -> void
@@ -8,10 +8,11 @@ class Riffer::Metrics::Otel
8
8
  SUPPORTED_API_VERSIONS: Gem::Requirement
9
9
 
10
10
  # Builds a backend when the OpenTelemetry metrics API is loadable at a
11
- # supported version; returns +nil+ so resolution falls back to Null.
11
+ # supported version; returns +nil+ so resolution falls back to NoOp.
12
+ # +provider+ defaults to the global <tt>OpenTelemetry.meter_provider</tt>.
12
13
  # --
13
- # : (provider: untyped) -> Riffer::Metrics::Otel?
14
- def self.build: (provider: untyped) -> Riffer::Metrics::Otel?
14
+ # : (?provider: untyped) -> Riffer::Metrics::Otel?
15
+ def self.build: (?provider: untyped) -> Riffer::Metrics::Otel?
15
16
 
16
17
  # Whether the OpenTelemetry metrics API gem is loadable at a supported
17
18
  # version.
@@ -4,7 +4,7 @@
4
4
  # the OpenTelemetry metrics API and no-ops otherwise, so riffer never declares
5
5
  # an OTEL dependency.
6
6
  module Riffer::Metrics
7
- @backend: (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))?
7
+ @backend: untyped
8
8
 
9
9
  MUTEX: Mutex
10
10
 
@@ -42,7 +42,8 @@ module Riffer::Metrics
42
42
  def record_histogram: (String, Numeric, ?unit: String?, ?description: String?, ?attributes: Hash[String, untyped]?) -> void
43
43
 
44
44
  # Mirrors a span's +recording?+ so a caller can skip work that exists only to
45
- # feed a metric.
45
+ # feed a metric. True for any live backend — the OTEL backend or a
46
+ # consumer-supplied one — so a custom metrics sink still gets fed.
46
47
  # --
47
48
  # : () -> bool
48
49
  def recording?: () -> bool
@@ -62,10 +63,10 @@ module Riffer::Metrics
62
63
  private
63
64
 
64
65
  # --
65
- # : () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
66
- def backend: () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
66
+ # : () -> untyped
67
+ def backend: () -> untyped
67
68
 
68
69
  # --
69
- # : () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
70
- def resolve_backend: () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
70
+ # : () -> untyped
71
+ def resolve_backend: () -> untyped
71
72
  end