riffer 0.34.0 → 0.36.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +0 -1
  5. data/docs/01_OVERVIEW.md +0 -1
  6. data/docs/03_AGENTS.md +40 -0
  7. data/docs/10_CONFIGURATION.md +0 -17
  8. data/docs/16_TRACING.md +4 -0
  9. data/lib/riffer/agent/run.rb +79 -70
  10. data/lib/riffer/agent.rb +20 -13
  11. data/lib/riffer/config.rb +0 -45
  12. data/lib/riffer/guardrails/runner.rb +14 -35
  13. data/lib/riffer/providers/amazon_bedrock.rb +7 -1
  14. data/lib/riffer/providers/anthropic.rb +8 -1
  15. data/lib/riffer/providers/base.rb +16 -68
  16. data/lib/riffer/providers/gemini.rb +5 -1
  17. data/lib/riffer/providers/open_ai.rb +10 -1
  18. data/lib/riffer/providers/open_router.rb +10 -1
  19. data/lib/riffer/tools/runtime.rb +23 -34
  20. data/lib/riffer/version.rb +1 -1
  21. data/sig/_private/opentelemetry.rbs +3 -6
  22. data/sig/generated/riffer/agent/run.rbs +41 -28
  23. data/sig/generated/riffer/agent.rbs +16 -9
  24. data/sig/generated/riffer/config.rbs +0 -33
  25. data/sig/generated/riffer/guardrails/runner.rbs +5 -10
  26. data/sig/generated/riffer/providers/base.rbs +4 -16
  27. data/sig/generated/riffer/tools/runtime.rbs +12 -10
  28. metadata +5 -16
  29. data/docs/17_METRICS.md +0 -153
  30. data/lib/riffer/metrics/instruments.rb +0 -31
  31. data/lib/riffer/metrics/no_op.rb +0 -14
  32. data/lib/riffer/metrics/otel.rb +0 -80
  33. data/lib/riffer/metrics.rb +0 -94
  34. data/sig/generated/riffer/metrics/instruments.rbs +0 -15
  35. data/sig/generated/riffer/metrics/no_op.rbs +0 -10
  36. data/sig/generated/riffer/metrics/otel.rbs +0 -48
  37. data/sig/generated/riffer/metrics.rbs +0 -72
  38. data/sig/manual/riffer/metrics/no_op.rbs +0 -5
  39. data/sig/manual/riffer/metrics.rbs +0 -5
@@ -64,14 +64,20 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
64
64
  tools = options[:tools]
65
65
  structured_output = options[:structured_output]
66
66
  cache_control = options[:cache_control]
67
+ tags = options[:tags] || {}
67
68
 
68
69
  params = {
69
70
  model_id: model,
70
71
  system: partitioned_messages[:system],
71
72
  messages: partitioned_messages[:conversation],
72
- **options.except(:tools, :structured_output, :cache_control)
73
+ **options.except(:tools, :structured_output, :cache_control, :tags)
73
74
  } #: Hash[Symbol, untyped]
74
75
 
76
+ # requestMetadata is a flat String=>String map used to filter invocation
77
+ # logs; every tag (including the reserved user_id) rides along, since
78
+ # Converse has no dedicated end-user field.
79
+ params[:request_metadata] = tags unless tags.empty?
80
+
75
81
  if tools && !tools.empty?
76
82
  params[:tool_config] = {
77
83
  tools: tools.map { |t| convert_tool_to_bedrock_format(t) }
@@ -48,6 +48,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
48
48
  tools = options[:tools]
49
49
  structured_output = options[:structured_output]
50
50
  web_search = options[:web_search]
51
+ tags = options[:tags] || {}
51
52
 
52
53
  max_tokens = options.fetch(:max_tokens, 4096)
53
54
 
@@ -55,11 +56,17 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
55
56
  model: model,
56
57
  messages: partitioned_messages[:conversation],
57
58
  max_tokens: max_tokens,
58
- **options.except(:tools, :max_tokens, :structured_output, :web_search)
59
+ **options.except(:tools, :max_tokens, :structured_output, :web_search, :tags)
59
60
  } #: Hash[Symbol, untyped]
60
61
 
61
62
  params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
62
63
 
64
+ # Anthropic's only request-metadata field is metadata.user_id (opaque, no
65
+ # PII). It carries the reserved user_id tag; all other tags are dropped
66
+ # here and survive only on spans.
67
+ user_id = tags["user_id"]
68
+ params[:metadata] = {user_id: user_id} if user_id
69
+
63
70
  anthropic_tools = [] #: Array[Hash[Symbol, untyped]]
64
71
  anthropic_tools.concat(tools.map { |t| convert_tool_to_anthropic_format(t) }) if tools && !tools.empty?
65
72
 
@@ -57,8 +57,6 @@ class Riffer::Providers::Base
57
57
  structured_output = parse_structured_output(content) if options[:structured_output] && tool_calls.empty?
58
58
 
59
59
  Riffer::Tracing.record_usage(span, token_usage)
60
- record_token_usage_metric(model, token_usage)
61
- record_cost_metric(model, token_usage)
62
60
  record_finish_reason(span, finish_reason&.reason, finish_reason&.raw)
63
61
  capture_output(span, content: content, tool_calls: tool_calls, finish_reason: finish_reason&.reason)
64
62
 
@@ -86,22 +84,15 @@ class Riffer::Providers::Base
86
84
  params = build_request_params(messages, model, options)
87
85
 
88
86
  # The enumerator body runs in its own fiber, where the fiber-local OTEL
89
- # context is empty — capture here so the chat span parents to the
90
- # caller's trace.
87
+ # context is empty — capture here so the chat span parents to the caller's
88
+ # trace.
91
89
  trace_context = Riffer::Tracing.current_context
92
90
  Enumerator.new do |yielder|
93
91
  Riffer::Tracing.with_context(trace_context) do
94
92
  in_chat_span(model, messages, options) do |span|
95
- # The recorder feeds both the span and the token-usage metric, so build
96
- # it whenever either is live — metrics fire even with tracing off.
97
- observe = span.recording? || Riffer::Metrics.recording?
98
- sink = observe ? Riffer::Tracing::StreamRecorder.new(yielder) : yielder
93
+ sink = span.recording? ? Riffer::Tracing::StreamRecorder.new(yielder) : yielder
99
94
  execute_stream(params, sink)
100
- if sink.is_a?(Riffer::Tracing::StreamRecorder)
101
- record_stream_outcome(span, sink)
102
- record_token_usage_metric(model, sink.token_usage)
103
- record_cost_metric(model, sink.token_usage)
104
- end
95
+ record_stream_outcome(span, sink) if sink.is_a?(Riffer::Tracing::StreamRecorder)
105
96
  end
106
97
  end
107
98
  end
@@ -222,25 +213,14 @@ class Riffer::Providers::Base
222
213
  #--
223
214
  #: [R] (String?, Array[Riffer::Messages::Base], Hash[Symbol, untyped]) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R
224
215
  def in_chat_span(model, messages, options)
225
- start = Riffer::Metrics.monotonic_now
226
- error_type = nil #: String?
227
- begin
228
- Riffer::Tracing.in_span(model ? "chat #{model}" : "chat", attributes: chat_span_attributes(model, options), kind: :client) do |span|
229
- capture_input(span, messages)
230
- yield span
231
- rescue => error
232
- # The backend records the exception and error status on the re-raise;
233
- # error.type is the one semconv attribute it doesn't set.
234
- span.set_attribute("error.type", error.class.name)
235
- raise
236
- end
216
+ Riffer::Tracing.in_span(model ? "chat #{model}" : "chat", attributes: chat_span_attributes(model, options), kind: :client) do |span|
217
+ capture_input(span, messages)
218
+ yield span
237
219
  rescue => error
238
- # The inner rescue tags the span; capture error.type here too, at method
239
- # scope, where the ensure can read it onto the metric.
240
- error_type = error.class.name #: String?
220
+ # The backend records the exception and error status on the re-raise;
221
+ # error.type is the one semconv attribute it doesn't set.
222
+ span.set_attribute("error.type", error.class.name)
241
223
  raise
242
- ensure
243
- Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: chat_metric_attributes(model, error_type))
244
224
  end
245
225
  end
246
226
 
@@ -258,47 +238,15 @@ class Riffer::Providers::Base
258
238
  attributes[attribute] = value unless value.nil?
259
239
  end
260
240
 
261
- attributes
262
- end
263
-
264
- #--
265
- #: (String?) -> Hash[String, untyped]
266
- def chat_metric_base_attributes(model)
267
- attributes = {
268
- "gen_ai.operation.name" => "chat",
269
- "gen_ai.provider.name" => self.class.semconv_provider_name
270
- } #: Hash[String, untyped]
271
- attributes["gen_ai.request.model"] = model if model
272
- attributes
241
+ attributes.merge(tag_attributes(options[:tags] || {}))
273
242
  end
274
243
 
244
+ # Maps normalized tags to their namespaced span attribute form. An empty map
245
+ # yields an empty hash, so merging it is a no-op.
275
246
  #--
276
- #: (String?, String?) -> Hash[String, untyped]
277
- def chat_metric_attributes(model, error_type)
278
- attributes = chat_metric_base_attributes(model)
279
- attributes["error.type"] = error_type if error_type
280
- attributes
281
- end
282
-
283
- # Per-call only — the run level would double-count an aggregate.
284
- #--
285
- #: (String?, Riffer::Providers::TokenUsage?) -> void
286
- def record_token_usage_metric(model, usage)
287
- return unless usage
288
-
289
- base = chat_metric_base_attributes(model)
290
- Riffer::Metrics::Instruments::TOKEN_USAGE.record(usage.input_tokens, attributes: base.merge("gen_ai.token.type" => "input"))
291
- Riffer::Metrics::Instruments::TOKEN_USAGE.record(usage.output_tokens, attributes: base.merge("gen_ai.token.type" => "output"))
292
- end
293
-
294
- # Per-call only — the run level would double-count an aggregate.
295
- #--
296
- #: (String?, Riffer::Providers::TokenUsage?) -> void
297
- def record_cost_metric(model, usage)
298
- cost = usage&.cost
299
- return unless cost
300
-
301
- Riffer::Metrics::Instruments::COST.record(cost, attributes: chat_metric_base_attributes(model))
247
+ #: (Hash[String, String]) -> Hash[String, String]
248
+ def tag_attributes(tags)
249
+ tags.transform_keys { |key| "riffer.tag.#{key}" }
302
250
  end
303
251
 
304
252
  #--
@@ -67,7 +67,11 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
67
67
  }]
68
68
  end
69
69
 
70
- generation_config = options.except(:tools, :structured_output)
70
+ # tags propagate to observability only: the Gemini Developer API has no
71
+ # request labels field (unknown body fields are rejected), so :tags is
72
+ # stripped here rather than mapped. Native labels would arrive with a Vertex
73
+ # adapter. See docs/10_CONFIGURATION.md.
74
+ generation_config = options.except(:tools, :structured_output, :tags)
71
75
 
72
76
  if structured_output
73
77
  generation_config[:responseMimeType] = "application/json"
@@ -30,6 +30,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
30
30
  tools = options[:tools]
31
31
  structured_output = options[:structured_output]
32
32
  web_search = options[:web_search]
33
+ tags = options[:tags] || {}
33
34
 
34
35
  params = {
35
36
  input: convert_messages_to_openai_format(messages),
@@ -38,9 +39,17 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
38
39
  effort: reasoning,
39
40
  summary: "auto"
40
41
  },
41
- **options.except(:reasoning, :tools, :structured_output, :web_search)
42
+ **options.except(:reasoning, :tools, :structured_output, :web_search, :tags)
42
43
  } #: Hash[Symbol, untyped]
43
44
 
45
+ unless tags.empty?
46
+ params[:metadata] = tags
47
+ # The reserved user_id also maps to the native safety identifier while
48
+ # staying in metadata as an ordinary tag.
49
+ user_id = tags["user_id"]
50
+ params[:safety_identifier] = user_id if user_id
51
+ end
52
+
44
53
  openai_tools = [] #: Array[Hash[Symbol, untyped]]
45
54
  openai_tools.concat(tools.map { |t| convert_tool_to_openai_format(t) }) if tools && !tools.empty?
46
55
 
@@ -44,13 +44,22 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
44
44
  reasoning = options[:reasoning]
45
45
  tools = options[:tools]
46
46
  structured_output = options[:structured_output]
47
+ tags = options[:tags] || {}
47
48
 
48
49
  params = {
49
50
  model: model,
50
51
  messages: convert_messages_to_chat_completions_format(messages),
51
- **options.except(:reasoning, :tools, :structured_output)
52
+ **options.except(:reasoning, :tools, :structured_output, :tags)
52
53
  } #: Hash[Symbol, untyped]
53
54
 
55
+ unless tags.empty?
56
+ params[:metadata] = tags
57
+ # OpenRouter exposes the legacy Chat Completions user field rather than
58
+ # safety_identifier; the reserved user_id maps there and stays in metadata.
59
+ user = tags["user_id"]
60
+ params[:user] = user if user
61
+ end
62
+
54
63
  if reasoning
55
64
  params[:reasoning] = reasoning.is_a?(String) ? {effort: reasoning} : reasoning
56
65
  end
@@ -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 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,24 +59,14 @@ 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
- start = Riffer::Metrics.monotonic_now
63
- error_type = nil #: String?
64
- begin
65
- result = in_tool_span(tool_call) do |span|
66
- response = yield
67
- record_tool_outcome(span, response)
68
- response
69
- end
70
- error_type = result.error_type&.to_s
71
- [tool_call, result] #: [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
72
- rescue => error
73
- error_type = error.class.name #: String?
74
- raise
75
- ensure
76
- Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: tool_metric_attributes(tool_call, error_type))
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 = {})
64
+ result = in_tool_span(tool_call, tags) do |span|
65
+ response = yield
66
+ record_tool_outcome(span, response)
67
+ response
77
68
  end
69
+ [tool_call, result] #: [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
78
70
  end
79
71
 
80
72
  #--
@@ -113,9 +105,9 @@ class Riffer::Tools::Runtime
113
105
 
114
106
  # Emitted outside +around_tool_call+ so host enrichment spans nest beneath it.
115
107
  #--
116
- #: [R] (Riffer::Messages::Assistant::ToolCall) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::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|
108
+ #: [R] (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> R } -> R
109
+ def in_tool_span(tool_call, tags = {})
110
+ Riffer::Tracing.in_span("execute_tool #{tool_call.name}", attributes: tool_span_attributes(tool_call, tags), kind: :internal) do |span|
119
111
  capture_tool_arguments(span, tool_call)
120
112
  yield span
121
113
  rescue => error
@@ -127,24 +119,21 @@ class Riffer::Tools::Runtime
127
119
  end
128
120
 
129
121
  #--
130
- #: (Riffer::Messages::Assistant::ToolCall) -> Hash[String, untyped]
131
- def tool_span_attributes(tool_call)
122
+ #: (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) -> Hash[String, untyped]
123
+ def tool_span_attributes(tool_call, tags = {})
132
124
  {
133
125
  "gen_ai.operation.name" => "execute_tool",
134
126
  "gen_ai.tool.name" => tool_call.name,
135
127
  "gen_ai.tool.call.id" => tool_call.call_id
136
- }
128
+ }.merge(tag_attributes(tags))
137
129
  end
138
130
 
131
+ # Maps normalized tags to their namespaced span attribute form. An empty map
132
+ # yields an empty hash, so merging it is a no-op.
139
133
  #--
140
- #: (Riffer::Messages::Assistant::ToolCall, String?) -> Hash[String, untyped]
141
- def tool_metric_attributes(tool_call, error_type)
142
- attributes = {
143
- "gen_ai.operation.name" => "execute_tool",
144
- "gen_ai.tool.name" => tool_call.name
145
- } #: Hash[String, untyped]
146
- attributes["error.type"] = error_type if error_type
147
- attributes
134
+ #: (Hash[String, String]) -> Hash[String, String]
135
+ def tag_attributes(tags)
136
+ tags.transform_keys { |key| "riffer.tag.#{key}" }
148
137
  end
149
138
 
150
139
  # A returned error Response is a handled outcome, so its status stays unset —
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.34.0" #: String
5
+ VERSION = "0.36.0" #: String
6
6
  end
@@ -1,13 +1,10 @@
1
- # Minimal signatures for the `opentelemetry-api` and `opentelemetry-metrics-api`
2
- # gems (undeclared, runtime detected), which ship no RBS.
1
+ # Minimal signatures for the `opentelemetry-api` gem (undeclared, runtime
2
+ # detected), which ships no RBS.
3
3
  #
4
- # Only the surface used by Riffer::Tracing::Otel and Riffer::Metrics::Otel is
5
- # declared.
4
+ # Only the surface used by Riffer::Tracing::Otel is declared.
6
5
  module OpenTelemetry
7
6
  def self.tracer_provider: () -> untyped
8
7
 
9
- def self.meter_provider: () -> untyped
10
-
11
8
  class Context
12
9
  def self.current: () -> OpenTelemetry::Context
13
10
 
@@ -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 builder in the run as +riffer.tag.*+ and to each
25
+ # 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 tagging is threaded separately to each
90
+ # 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,12 +111,16 @@ 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
+ # : (Hash[(String | Symbol), untyped]?) -> Hash[String, String]
119
+ def normalize_tags: (Hash[String | Symbol, untyped]?) -> Hash[String, String]
120
+
121
+ # --
122
+ # : (Hash[String, String]) -> Hash[String, String]
123
+ def tag_attributes: (Hash[String, String]) -> Hash[String, String]
111
124
 
112
125
  # --
113
126
  # : (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Agent::Response) -> void
@@ -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 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>.
@@ -137,36 +137,6 @@ class Riffer::Config
137
137
  def backend=: (untyped) -> void
138
138
  end
139
139
 
140
- # Metrics-related global configuration, independent of +config.tracing+ so a
141
- # host can run one signal without the other.
142
- class Metrics
143
- # Whether riffer records OTEL metric instruments; defaults to +true+, a
144
- # no-op until a host wires an OTEL metrics SDK.
145
- attr_reader enabled: bool
146
-
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
-
151
- # --
152
- # : () -> void
153
- def initialize: () -> void
154
-
155
- # Sets the enabled flag, coercing boolean-ish values so an env-var
156
- # +"false"+ (truthy in Ruby) doesn't silently keep metrics on. Raises
157
- # Riffer::ArgumentError on an unrecognized value.
158
- # --
159
- # : (untyped) -> void
160
- def enabled=: (untyped) -> void
161
-
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
- # --
166
- # : (untyped) -> void
167
- def backend=: (untyped) -> void
168
- end
169
-
170
140
  # Consumer-configured token pricing, keyed by +provider/model+ id. Riffer
171
141
  # ships no price table, so an unconfigured model carries no cost.
172
142
  class Pricing
@@ -276,9 +246,6 @@ class Riffer::Config
276
246
  # Tracing-related global configuration.
277
247
  attr_reader tracing: Riffer::Config::Tracing
278
248
 
279
- # Metrics-related global configuration.
280
- attr_reader metrics: Riffer::Config::Metrics
281
-
282
249
  # Consumer-configured per-model token pricing.
283
250
  attr_reader pricing: Riffer::Config::Pricing
284
251
 
@@ -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,10 +40,6 @@ 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
 
40
- # --
41
- # : (Riffer::Guardrail) { () -> Riffer::Guardrails::Result } -> Riffer::Guardrails::Result
42
- def instrument_guardrail: (Riffer::Guardrail) { () -> Riffer::Guardrails::Result } -> Riffer::Guardrails::Result
43
-
44
43
  # --
45
44
  # : (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
46
45
  def run_guardrail_phase: (Riffer::Guardrail, untyped, messages: Array[Riffer::Messages::Base]?) -> Riffer::Guardrails::Result
@@ -49,10 +48,6 @@ class Riffer::Guardrails::Runner
49
48
  # : (Riffer::Guardrail) -> Hash[String, untyped]
50
49
  def guardrail_span_attributes: (Riffer::Guardrail) -> Hash[String, untyped]
51
50
 
52
- # --
53
- # : (Riffer::Guardrail, String?) -> Hash[String, untyped]
54
- def guardrail_metric_attributes: (Riffer::Guardrail, String?) -> Hash[String, untyped]
55
-
56
51
  # A block is a handled outcome, so its span status stays unset — an error
57
52
  # span status is reserved for a raised exception.
58
53
  # --