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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +18 -0
- data/README.md +0 -1
- data/docs/01_OVERVIEW.md +0 -1
- data/docs/03_AGENTS.md +40 -0
- data/docs/10_CONFIGURATION.md +0 -17
- data/docs/16_TRACING.md +4 -0
- data/lib/riffer/agent/run.rb +79 -70
- data/lib/riffer/agent.rb +20 -13
- data/lib/riffer/config.rb +0 -45
- data/lib/riffer/guardrails/runner.rb +14 -35
- data/lib/riffer/providers/amazon_bedrock.rb +7 -1
- data/lib/riffer/providers/anthropic.rb +8 -1
- data/lib/riffer/providers/base.rb +16 -68
- data/lib/riffer/providers/gemini.rb +5 -1
- data/lib/riffer/providers/open_ai.rb +10 -1
- data/lib/riffer/providers/open_router.rb +10 -1
- data/lib/riffer/tools/runtime.rb +23 -34
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/opentelemetry.rbs +3 -6
- data/sig/generated/riffer/agent/run.rbs +41 -28
- data/sig/generated/riffer/agent.rbs +16 -9
- data/sig/generated/riffer/config.rbs +0 -33
- data/sig/generated/riffer/guardrails/runner.rbs +5 -10
- data/sig/generated/riffer/providers/base.rbs +4 -16
- data/sig/generated/riffer/tools/runtime.rbs +12 -10
- metadata +5 -16
- data/docs/17_METRICS.md +0 -153
- data/lib/riffer/metrics/instruments.rb +0 -31
- data/lib/riffer/metrics/no_op.rb +0 -14
- data/lib/riffer/metrics/otel.rb +0 -80
- data/lib/riffer/metrics.rb +0 -94
- data/sig/generated/riffer/metrics/instruments.rbs +0 -15
- data/sig/generated/riffer/metrics/no_op.rbs +0 -10
- data/sig/generated/riffer/metrics/otel.rbs +0 -48
- data/sig/generated/riffer/metrics.rbs +0 -72
- data/sig/manual/riffer/metrics/no_op.rbs +0 -5
- 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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
239
|
-
#
|
|
240
|
-
|
|
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
|
|
277
|
-
def
|
|
278
|
-
|
|
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
|
-
|
|
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
|
data/lib/riffer/tools/runtime.rb
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
#: (
|
|
141
|
-
def
|
|
142
|
-
|
|
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 —
|
data/lib/riffer/version.rb
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
# Minimal signatures for the `opentelemetry-api`
|
|
2
|
-
#
|
|
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
|
|
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
|
-
# : (
|
|
110
|
-
def
|
|
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
|
# --
|