riffer 0.34.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/docs/03_AGENTS.md +44 -0
- data/docs/16_TRACING.md +4 -0
- data/docs/17_METRICS.md +5 -3
- data/lib/riffer/agent/run.rb +81 -48
- data/lib/riffer/agent.rb +20 -13
- data/lib/riffer/guardrails/runner.rb +7 -3
- data/lib/riffer/providers/amazon_bedrock.rb +7 -1
- data/lib/riffer/providers/anthropic.rb +8 -1
- data/lib/riffer/providers/base.rb +32 -20
- 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 +26 -16
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent/run.rbs +45 -28
- data/sig/generated/riffer/agent.rbs +16 -9
- data/sig/generated/riffer/guardrails/runner.rbs +5 -2
- data/sig/generated/riffer/providers/base.rbs +14 -8
- data/sig/generated/riffer/tools/runtime.rbs +16 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acfc9a0c002c97b79d515172d71aa419359e27fac0a3809934c30057931e66ef
|
|
4
|
+
data.tar.gz: 322aaebffb66c41824fdd3a3c43d08ae6bb806f8e2e06ca5ee1834648704e1d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba3554a4a135ae43f5d5a2d65928275e4acfcc27c03609f2578d5304e8825c3f42f05b7229fe5881fdc7c663372ef6ca3591af02b2719167bc7711fc7754e7e6
|
|
7
|
+
data.tar.gz: 49c2b41f0cb606ec9e86b72777d55b3b3222220d879b5e05ed211a07094d9561c7e1c054ce2689d3b81b6ff872d981f6e1f24306b916cf12a697f7df23f91a69
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.35.0](https://github.com/janeapp/riffer/compare/riffer/v0.34.0...riffer/v0.35.0) (2026-06-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* per-call tags: for Agent#generate and #stream ([#339](https://github.com/janeapp/riffer/issues/339)) ([ddfda89](https://github.com/janeapp/riffer/commit/ddfda8929ca3b594415f827957d8c91104387317))
|
|
14
|
+
|
|
8
15
|
## [0.34.0](https://github.com/janeapp/riffer/compare/riffer/v0.33.0...riffer/v0.34.0) (2026-06-22)
|
|
9
16
|
|
|
10
17
|
|
data/docs/03_AGENTS.md
CHANGED
|
@@ -317,6 +317,50 @@ agent.generate('Hello')
|
|
|
317
317
|
|
|
318
318
|
When `config:` is supplied, the class-level configuration is ignored for that instance.
|
|
319
319
|
|
|
320
|
+
## Per-Call Tags
|
|
321
|
+
|
|
322
|
+
`#generate` and `#stream` accept an optional `tags:` hash — a flat map of attribution labels scoped to that single call (for cost/usage attribution, filtering audit logs, slicing telemetry). It is **per-call only**.
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
agent.generate("Summarize this ticket.",
|
|
326
|
+
tags: {team: "growth", feature: "summarizer", user_id: "u_abc123"})
|
|
327
|
+
|
|
328
|
+
agent.stream("...", tags: {team: "growth", environment: "production"})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Keys and values may be `String` or `Symbol`; both are stringified, and entries with a `nil` value are dropped. Passing a non-`Hash` raises `Riffer::ArgumentError`. An omitted or empty `tags:` is a complete no-op.
|
|
332
|
+
|
|
333
|
+
Tags propagate to **two** places:
|
|
334
|
+
|
|
335
|
+
1. The provider's native per-request metadata field (see the mapping below).
|
|
336
|
+
2. Observability — stamped as `riffer.tag.<key>` on **every** span and metric the call emits (`invoke_agent`, `chat`, `execute_tool`, `execute_guardrail` spans, and the duration/token-usage/cost metrics). See [Tracing](16_TRACING.md) and [Metrics](17_METRICS.md).
|
|
337
|
+
|
|
338
|
+
### Reserved key: `user_id`
|
|
339
|
+
|
|
340
|
+
`user_id` is a reserved tag. Beyond appearing like any other tag, it maps to the provider's native end-user identifier where one exists (see the table).
|
|
341
|
+
|
|
342
|
+
### Provider mapping
|
|
343
|
+
|
|
344
|
+
| Provider | Native request field | `user_id` handling |
|
|
345
|
+
| --------------------- | ----------------------------- | --------------------------------------- |
|
|
346
|
+
| Amazon Bedrock | `request_metadata` (all tags) | Ordinary entry; no dedicated user field |
|
|
347
|
+
| OpenAI / Azure OpenAI | `metadata` (all tags) | Also mapped to `safety_identifier` |
|
|
348
|
+
| OpenRouter | `metadata` (all tags) | Also mapped to `user` |
|
|
349
|
+
| Anthropic | `metadata.user_id` **only** | The only tag forwarded |
|
|
350
|
+
| Gemini | _(none — observability only)_ | Tag only; no request field |
|
|
351
|
+
|
|
352
|
+
**Anthropic silently drops non-`user_id` tags.** The Messages API has no free-form request-metadata field — only `metadata.user_id`. So for Anthropic, `user_id` is forwarded as `metadata: {user_id: …}` and **every other tag is dropped from the request** (it still appears on spans and metrics). This is intentional.
|
|
353
|
+
|
|
354
|
+
**Gemini is observability-only.** Riffer's Gemini adapter targets the Gemini Developer API (`generativelanguage.googleapis.com`), whose `generateContent` request has **no** `labels` field — sending unknown fields is rejected. So tags are **not** added to the Gemini request; they propagate to spans and metrics only. Native request labels (`labels`, lowercase `[a-z0-9_-]`, ≤63 chars each) are a Vertex AI feature and would arrive with a future Vertex adapter.
|
|
355
|
+
|
|
356
|
+
### Provider limits (not enforced)
|
|
357
|
+
|
|
358
|
+
Riffer does not validate tag count, key/value length, or charset — it forwards what you give it and lets the provider reject anything out of bounds. Known limits: Bedrock `requestMetadata` ≤16 entries, key/value ≤256 chars; OpenAI / OpenRouter `metadata` ≤16 pairs (OpenRouter: 64-char keys, 512-char values).
|
|
359
|
+
|
|
360
|
+
### Metric cardinality
|
|
361
|
+
|
|
362
|
+
Tags are promoted onto metric dimensions verbatim — there is no allow-list. High-cardinality values (e.g. a unique `user_id` per request) will create a correspondingly large number of metric series in your backend. Keep metric-bound tags low-cardinality.
|
|
363
|
+
|
|
320
364
|
## Expand Your Agent
|
|
321
365
|
|
|
322
366
|
| Goal | Feature | Guide |
|
data/docs/16_TRACING.md
CHANGED
|
@@ -93,6 +93,10 @@ Every attribute a span can carry is listed below, including the conditional ones
|
|
|
93
93
|
|
|
94
94
|
The contract promise is: **when present**, a key carries the documented meaning and type. It is _not_ a promise that every key appears on every span.
|
|
95
95
|
|
|
96
|
+
### Per-call tags (`riffer.tag.*`)
|
|
97
|
+
|
|
98
|
+
Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four** span types as `riffer.tag.<key>` (string), so the per-span tables below omit them. They appear on every span the tagged call emits and are absent otherwise. Example: `tags: {team: "growth"}` adds `riffer.tag.team` → `"growth"` to the `invoke_agent`, `chat`, `execute_tool`, and `execute_guardrail` spans. See [Per-Call Tags](03_AGENTS.md#per-call-tags) for the full surface and the per-provider request-metadata mapping.
|
|
99
|
+
|
|
96
100
|
## `invoke_agent {agent}` — the run span
|
|
97
101
|
|
|
98
102
|
`INTERNAL`. One per call to `Agent#generate` or `Agent#stream`. The span name suffix is the agent's identifier (e.g. `invoke_agent weather-agent`).
|
data/docs/17_METRICS.md
CHANGED
|
@@ -74,6 +74,8 @@ OpenTelemetry.meter_provider.add_view(
|
|
|
74
74
|
|
|
75
75
|
Each instrument is documented here as a row carrying its name, instrument type, unit, and attribute set.
|
|
76
76
|
|
|
77
|
+
> **Per-call tags become dimensions.** Any tags passed to `#generate` / `#stream` via `tags:` are added as `riffer.tag.<key>` attributes to **every** instrument below — `operation.duration` (each operation), `token.usage` (each point), `cost`, and `guardrail.duration` — so the attribute lists omit them. Tags are promoted verbatim with no allow-list — a high-cardinality value like a unique `user_id` per request multiplies time series accordingly; drop it with a [View](https://opentelemetry.io/docs/specs/otel/metrics/sdk/#view) or keep metric-bound tags low-cardinality. See [Per-Call Tags](03_AGENTS.md#per-call-tags).
|
|
78
|
+
|
|
77
79
|
### `gen_ai.client.operation.duration`
|
|
78
80
|
|
|
79
81
|
Histogram, unit `s`. The latency of a single GenAI operation, recorded around the same wrap as the matching [span](16_TRACING.md) on both the success and error paths and timed with a monotonic clock. Recording is independent of tracing — the metric fires even with `config.tracing.enabled = false`. Tell the three operations apart by `gen_ai.operation.name`.
|
|
@@ -123,9 +125,9 @@ Histogram, unit `s`. The latency of a single guardrail execution, recorded aroun
|
|
|
123
125
|
|
|
124
126
|
This instrument is Riffer-owned (`riffer.*`, not `gen_ai.*`) by the same reasoning as its span: a guardrail is not a GenAI semantic-convention operation, so the `execute_guardrail` span deliberately carries no `gen_ai.operation.name` and lives in riffer's own `riffer.guardrail.*` namespace (see [Tracing](16_TRACING.md)). Folding the metric into `gen_ai.client.operation.duration` would contradict that, so the duration is its own riffer-owned histogram instead; see [Stability](#stability).
|
|
125
127
|
|
|
126
|
-
| Value
|
|
127
|
-
|
|
|
128
|
-
| duration of the guardrail execution
|
|
128
|
+
| Value | Attributes |
|
|
129
|
+
| ----------------------------------- | -------------------------------------------------------------------------- |
|
|
130
|
+
| duration of the guardrail execution | `riffer.guardrail.name`, `riffer.guardrail.phase`, `error.type` (on raise) |
|
|
129
131
|
|
|
130
132
|
`riffer.guardrail.phase` is `before` or `after`. A pass, transform, or block is a **handled** outcome and records no `error.type` — that attribute carries the exception class only when a guardrail raises, mirroring the span. One time series exists per `riffer.guardrail.name` × `riffer.guardrail.phase`; guardrail names are bounded by the guardrails you configure, so cardinality is safe — unlike the dynamic tool names on `gen_ai.client.operation.duration`.
|
|
131
133
|
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -10,37 +10,45 @@ module Riffer::Agent::Run
|
|
|
10
10
|
# for prompt/files semantics.
|
|
11
11
|
#
|
|
12
12
|
#--
|
|
13
|
-
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
|
|
14
|
-
def generate(agent:, prompt: nil, files: nil)
|
|
13
|
+
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
14
|
+
def generate(agent:, prompt: nil, files: nil, tags: {})
|
|
15
15
|
append_user_message(agent, prompt, files: files)
|
|
16
|
-
run_loop(agent)
|
|
16
|
+
run_loop(agent, tags: tags)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Runs the streaming loop for the given agent. See Riffer::Agent#stream
|
|
20
20
|
# for prompt/files semantics.
|
|
21
21
|
#
|
|
22
22
|
#--
|
|
23
|
-
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
24
|
-
def stream(agent:, prompt: nil, files: nil)
|
|
23
|
+
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
24
|
+
def stream(agent:, prompt: nil, files: nil, tags: {})
|
|
25
25
|
append_user_message(agent, prompt, files: files)
|
|
26
26
|
# The enumerator body runs in its own fiber, where the fiber-local OTEL
|
|
27
|
-
# context is empty — capture here so the run span parents to the caller's
|
|
27
|
+
# context is empty — capture here so the run span parents to the caller's
|
|
28
|
+
# trace. tags ride as an ordinary argument captured in the closure, so they
|
|
29
|
+
# cross the fiber boundary without any re-propagation.
|
|
28
30
|
trace_context = Riffer::Tracing.current_context
|
|
29
31
|
Enumerator.new do |stream_yielder|
|
|
30
|
-
Riffer::Tracing.with_context(trace_context) { run_loop(agent, stream_yielder: stream_yielder) }
|
|
32
|
+
Riffer::Tracing.with_context(trace_context) { run_loop(agent, tags: tags, stream_yielder: stream_yielder) }
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
38
|
+
# Both +generate+ and +stream+ funnel here, so this is the single place raw
|
|
39
|
+
# +tags+ are normalized. The clean <tt>String => String</tt> map is then
|
|
40
|
+
# threaded to every span/metric builder in the run as +riffer.tag.*+ and to
|
|
41
|
+
# each provider call (via +merged_model_options+) for native request-metadata
|
|
42
|
+
# mapping.
|
|
36
43
|
#--
|
|
37
|
-
#: (Riffer::Agent, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
|
|
38
|
-
def run_loop(agent, stream_yielder: nil)
|
|
44
|
+
#: (Riffer::Agent, ?tags: Hash[(String | Symbol), untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
|
|
45
|
+
def run_loop(agent, tags: {}, stream_yielder: nil)
|
|
46
|
+
tags = normalize_tags(tags)
|
|
39
47
|
start = Riffer::Metrics.monotonic_now
|
|
40
48
|
error_type = nil #: String?
|
|
41
49
|
begin
|
|
42
|
-
Riffer::Tracing.in_span("invoke_agent #{agent.class.identifier}", attributes: run_span_attributes(agent), kind: :internal) do |span|
|
|
43
|
-
response = execute_run(agent, stream_yielder)
|
|
50
|
+
Riffer::Tracing.in_span("invoke_agent #{agent.class.identifier}", attributes: run_span_attributes(agent, tags), kind: :internal) do |span|
|
|
51
|
+
response = execute_run(agent, stream_yielder, tags)
|
|
44
52
|
record_run_outcome(span, response)
|
|
45
53
|
response
|
|
46
54
|
rescue => error
|
|
@@ -55,18 +63,18 @@ module Riffer::Agent::Run
|
|
|
55
63
|
error_type = error.class.name #: String?
|
|
56
64
|
raise
|
|
57
65
|
ensure
|
|
58
|
-
Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: run_metric_attributes(agent, error_type))
|
|
66
|
+
Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: run_metric_attributes(agent, error_type, tags))
|
|
59
67
|
end
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
#--
|
|
63
|
-
#: (Riffer::Agent, Enumerator::Yielder?) -> Riffer::Agent::Response
|
|
64
|
-
def execute_run(agent, stream_yielder)
|
|
71
|
+
#: (Riffer::Agent, Enumerator::Yielder?, ?Hash[String, String]) -> Riffer::Agent::Response
|
|
72
|
+
def execute_run(agent, stream_yielder, tags = {})
|
|
65
73
|
all_modifications = [] #: Array[Riffer::Guardrails::Modification]
|
|
66
74
|
run_usage = nil #: Riffer::Providers::TokenUsage?
|
|
67
75
|
run_steps = 0
|
|
68
76
|
|
|
69
|
-
run_before_guardrails(agent, stream_yielder, all_modifications) do |tripwire|
|
|
77
|
+
run_before_guardrails(agent, stream_yielder, all_modifications, tags) do |tripwire|
|
|
70
78
|
return tripwire_response(agent, stream_yielder, tripwire, all_modifications, steps: run_steps)
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -84,16 +92,16 @@ module Riffer::Agent::Run
|
|
|
84
92
|
step = agent.session.steps
|
|
85
93
|
|
|
86
94
|
reason = catch(:riffer_interrupt) do
|
|
87
|
-
execute_pending_tool_calls(agent)
|
|
95
|
+
execute_pending_tool_calls(agent, tags)
|
|
88
96
|
|
|
89
97
|
loop do
|
|
90
|
-
response = stream_yielder ? accumulate_streamed_response(agent, stream_yielder) : call_llm(agent)
|
|
98
|
+
response = stream_yielder ? accumulate_streamed_response(agent, stream_yielder, tags) : call_llm(agent, tags)
|
|
91
99
|
step += 1
|
|
92
100
|
run_steps += 1
|
|
93
101
|
track_token_usage(agent, response.token_usage)
|
|
94
102
|
run_usage = sum_usage(run_usage, response.token_usage)
|
|
95
103
|
|
|
96
|
-
processed_response = run_after_guardrails(agent, response, stream_yielder, all_modifications) do |tripwire|
|
|
104
|
+
processed_response = run_after_guardrails(agent, response, stream_yielder, all_modifications, tags) do |tripwire|
|
|
97
105
|
return tripwire_response(agent, stream_yielder, tripwire, all_modifications, token_usage: run_usage, steps: run_steps)
|
|
98
106
|
end
|
|
99
107
|
|
|
@@ -104,7 +112,7 @@ module Riffer::Agent::Run
|
|
|
104
112
|
max_steps = agent.config.max_steps
|
|
105
113
|
throw :riffer_interrupt, Riffer::Agent::INTERRUPT_MAX_STEPS if max_steps && step >= max_steps
|
|
106
114
|
|
|
107
|
-
execute_tool_calls(agent, processed_response)
|
|
115
|
+
execute_tool_calls(agent, processed_response, tags: tags)
|
|
108
116
|
end
|
|
109
117
|
|
|
110
118
|
return final_response(agent, all_modifications, token_usage: run_usage, steps: run_steps)
|
|
@@ -122,14 +130,14 @@ module Riffer::Agent::Run
|
|
|
122
130
|
end
|
|
123
131
|
|
|
124
132
|
#--
|
|
125
|
-
#: (Riffer::Agent, Enumerator::Yielder) -> Riffer::Messages::Assistant
|
|
126
|
-
def accumulate_streamed_response(agent, stream_yielder)
|
|
133
|
+
#: (Riffer::Agent, Enumerator::Yielder, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
134
|
+
def accumulate_streamed_response(agent, stream_yielder, tags = {})
|
|
127
135
|
accumulated_content = ""
|
|
128
136
|
accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
129
137
|
accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
|
|
130
138
|
accumulated_finish_reason = nil #: Symbol?
|
|
131
139
|
|
|
132
|
-
call_llm_stream(agent).each do |event|
|
|
140
|
+
call_llm_stream(agent, tags).each do |event|
|
|
133
141
|
stream_yielder << event
|
|
134
142
|
|
|
135
143
|
case event
|
|
@@ -180,33 +188,33 @@ module Riffer::Agent::Run
|
|
|
180
188
|
end
|
|
181
189
|
|
|
182
190
|
#--
|
|
183
|
-
#: (Riffer::Agent) -> Riffer::Messages::Assistant
|
|
184
|
-
def call_llm(agent)
|
|
191
|
+
#: (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
192
|
+
def call_llm(agent, tags = {})
|
|
185
193
|
agent.provider.generate_text(
|
|
186
194
|
messages: agent.session.messages,
|
|
187
195
|
model: agent.model_name,
|
|
188
196
|
tools: effective_tools(agent),
|
|
189
|
-
**merged_model_options(agent)
|
|
197
|
+
**merged_model_options(agent, tags)
|
|
190
198
|
)
|
|
191
199
|
end
|
|
192
200
|
|
|
193
201
|
#--
|
|
194
|
-
#: (Riffer::Agent) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
195
|
-
def call_llm_stream(agent)
|
|
202
|
+
#: (Riffer::Agent, ?Hash[String, String]) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
203
|
+
def call_llm_stream(agent, tags = {})
|
|
196
204
|
agent.provider.stream_text(
|
|
197
205
|
messages: agent.session.messages,
|
|
198
206
|
model: agent.model_name,
|
|
199
207
|
tools: effective_tools(agent),
|
|
200
|
-
**merged_model_options(agent)
|
|
208
|
+
**merged_model_options(agent, tags)
|
|
201
209
|
)
|
|
202
210
|
end
|
|
203
211
|
|
|
204
212
|
#--
|
|
205
|
-
#: (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall]) -> void
|
|
206
|
-
def execute_tool_calls(agent, assistant_message, tool_calls: assistant_message.tool_calls)
|
|
213
|
+
#: (Riffer::Agent, Riffer::Messages::Assistant, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?tags: Hash[String, String]) -> void
|
|
214
|
+
def execute_tool_calls(agent, assistant_message, tool_calls: assistant_message.tool_calls, tags: {})
|
|
207
215
|
return if tool_calls.empty?
|
|
208
216
|
|
|
209
|
-
results = agent.tool_runtime.execute(tool_calls, tools: effective_tools(agent), context: agent.context, assistant_message: assistant_message)
|
|
217
|
+
results = agent.tool_runtime.execute(tool_calls, tools: effective_tools(agent), context: agent.context, assistant_message: assistant_message, tags: tags)
|
|
210
218
|
|
|
211
219
|
inject_discovered_tools(agent, results)
|
|
212
220
|
|
|
@@ -233,19 +241,19 @@ module Riffer::Agent::Run
|
|
|
233
241
|
end
|
|
234
242
|
|
|
235
243
|
#--
|
|
236
|
-
#: (Riffer::Agent) -> void
|
|
237
|
-
def execute_pending_tool_calls(agent)
|
|
244
|
+
#: (Riffer::Agent, ?Hash[String, String]) -> void
|
|
245
|
+
def execute_pending_tool_calls(agent, tags = {})
|
|
238
246
|
assistant_message, pending = agent.session.pending_tool_calls
|
|
239
|
-
execute_tool_calls(agent, assistant_message, tool_calls: pending) if assistant_message
|
|
247
|
+
execute_tool_calls(agent, assistant_message, tool_calls: pending, tags: tags) if assistant_message
|
|
240
248
|
end
|
|
241
249
|
|
|
242
250
|
#--
|
|
243
|
-
#: (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> void
|
|
244
|
-
def run_before_guardrails(agent, stream_yielder, all_modifications)
|
|
251
|
+
#: (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> void
|
|
252
|
+
def run_before_guardrails(agent, stream_yielder, all_modifications, tags = {})
|
|
245
253
|
guardrails = agent.config.guardrails_for(:before)
|
|
246
254
|
return if guardrails.empty?
|
|
247
255
|
|
|
248
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :before, context: agent.context)
|
|
256
|
+
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :before, context: agent.context, tags: tags)
|
|
249
257
|
processed_messages, tripwire, modifications = runner.run(agent.session.messages)
|
|
250
258
|
agent.session.set(processed_messages) unless tripwire
|
|
251
259
|
record_modifications!(stream_yielder, all_modifications, modifications)
|
|
@@ -253,12 +261,12 @@ module Riffer::Agent::Run
|
|
|
253
261
|
end
|
|
254
262
|
|
|
255
263
|
#--
|
|
256
|
-
#: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
|
|
257
|
-
def run_after_guardrails(agent, response, stream_yielder, all_modifications)
|
|
264
|
+
#: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
|
|
265
|
+
def run_after_guardrails(agent, response, stream_yielder, all_modifications, tags = {})
|
|
258
266
|
guardrails = agent.config.guardrails_for(:after)
|
|
259
267
|
return response if guardrails.empty?
|
|
260
268
|
|
|
261
|
-
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :after, context: agent.context)
|
|
269
|
+
runner = Riffer::Guardrails::Runner.new(guardrails, phase: :after, context: agent.context, tags: tags)
|
|
262
270
|
processed_response, tripwire, modifications = runner.run(response, messages: agent.session.messages)
|
|
263
271
|
|
|
264
272
|
response_index = agent.session.messages.length
|
|
@@ -285,11 +293,16 @@ module Riffer::Agent::Run
|
|
|
285
293
|
discovered.empty? ? agent.tools : agent.tools + discovered
|
|
286
294
|
end
|
|
287
295
|
|
|
296
|
+
# +tags+ rides in the options hash as a curated key the providers extract for
|
|
297
|
+
# native request-metadata mapping (alongside +:structured_output+); it never
|
|
298
|
+
# reaches an SDK call verbatim. Span/metric tagging is threaded separately to
|
|
299
|
+
# each builder.
|
|
288
300
|
#--
|
|
289
|
-
#: (Riffer::Agent) -> Hash[Symbol, untyped]
|
|
290
|
-
def merged_model_options(agent)
|
|
301
|
+
#: (Riffer::Agent, ?Hash[String, String]) -> Hash[Symbol, untyped]
|
|
302
|
+
def merged_model_options(agent, tags = {})
|
|
291
303
|
opts = agent.config.model_options.dup
|
|
292
304
|
opts[:structured_output] = agent.structured_output if agent.structured_output
|
|
305
|
+
opts[:tags] = tags unless tags.empty?
|
|
293
306
|
opts
|
|
294
307
|
end
|
|
295
308
|
|
|
@@ -329,19 +342,19 @@ module Riffer::Agent::Run
|
|
|
329
342
|
end
|
|
330
343
|
|
|
331
344
|
#--
|
|
332
|
-
#: (Riffer::Agent) -> Hash[String, untyped]
|
|
333
|
-
def run_span_attributes(agent)
|
|
345
|
+
#: (Riffer::Agent, ?Hash[String, String]) -> Hash[String, untyped]
|
|
346
|
+
def run_span_attributes(agent, tags = {})
|
|
334
347
|
{
|
|
335
348
|
"gen_ai.operation.name" => "invoke_agent",
|
|
336
349
|
"gen_ai.agent.name" => agent.class.identifier,
|
|
337
350
|
"gen_ai.provider.name" => agent.provider.class.semconv_provider_name,
|
|
338
351
|
"gen_ai.request.model" => agent.model_name
|
|
339
|
-
}
|
|
352
|
+
}.merge(tag_attributes(tags))
|
|
340
353
|
end
|
|
341
354
|
|
|
342
355
|
#--
|
|
343
|
-
#: (Riffer::Agent, String?) -> Hash[String, untyped]
|
|
344
|
-
def run_metric_attributes(agent, error_type)
|
|
356
|
+
#: (Riffer::Agent, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
357
|
+
def run_metric_attributes(agent, error_type, tags = {})
|
|
345
358
|
attributes = {
|
|
346
359
|
"gen_ai.operation.name" => "invoke_agent",
|
|
347
360
|
"gen_ai.provider.name" => agent.provider.class.semconv_provider_name,
|
|
@@ -349,7 +362,27 @@ module Riffer::Agent::Run
|
|
|
349
362
|
"gen_ai.agent.name" => agent.class.identifier
|
|
350
363
|
} #: Hash[String, untyped]
|
|
351
364
|
attributes["error.type"] = error_type if error_type
|
|
352
|
-
attributes
|
|
365
|
+
attributes.merge(tag_attributes(tags))
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
#--
|
|
369
|
+
#: (Hash[(String | Symbol), untyped]?) -> Hash[String, String]
|
|
370
|
+
def normalize_tags(tags)
|
|
371
|
+
return {} if tags.nil?
|
|
372
|
+
raise Riffer::ArgumentError, "tags: must be a Hash, got #{tags.class}" unless tags.is_a?(Hash)
|
|
373
|
+
|
|
374
|
+
result = {} #: Hash[String, String]
|
|
375
|
+
tags.each do |key, value|
|
|
376
|
+
next if value.nil?
|
|
377
|
+
result[key.to_s] = value.to_s
|
|
378
|
+
end
|
|
379
|
+
result
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
#--
|
|
383
|
+
#: (Hash[String, String]) -> Hash[String, String]
|
|
384
|
+
def tag_attributes(tags)
|
|
385
|
+
tags.transform_keys { |key| "riffer.tag.#{key}" }
|
|
353
386
|
end
|
|
354
387
|
|
|
355
388
|
#--
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -169,16 +169,16 @@ class Riffer::Agent
|
|
|
169
169
|
|
|
170
170
|
# Generates a response using a new agent instance.
|
|
171
171
|
#--
|
|
172
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
|
|
173
|
-
def self.generate(prompt = nil, files: nil, context: nil)
|
|
174
|
-
new(context: context).generate(prompt, files: files)
|
|
172
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
173
|
+
def self.generate(prompt = nil, files: nil, context: nil, tags: {})
|
|
174
|
+
new(context: context).generate(prompt, files: files, tags: tags)
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
# Streams a response using a new agent instance.
|
|
178
178
|
#--
|
|
179
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
180
|
-
def self.stream(prompt = nil, files: nil, context: nil)
|
|
181
|
-
new(context: context).stream(prompt, files: files)
|
|
179
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
180
|
+
def self.stream(prompt = nil, files: nil, context: nil, tags: {})
|
|
181
|
+
new(context: context).stream(prompt, files: files, tags: tags)
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
# Reconstructs a runnable agent from a wire hash produced by +#to_h+.
|
|
@@ -288,22 +288,29 @@ class Riffer::Agent
|
|
|
288
288
|
# against the current session, resuming a persisted conversation or pending
|
|
289
289
|
# tool calls. +files:+ requires +prompt+.
|
|
290
290
|
#
|
|
291
|
+
# +tags:+ is an optional flat hash of attribution tags applied to this single
|
|
292
|
+
# call: they propagate to the provider's native request-metadata field, and
|
|
293
|
+
# are stamped as +riffer.tag.*+ on every span and metric the call emits. See
|
|
294
|
+
# +docs/03_AGENTS.md+ for the per-provider mapping. The reserved key
|
|
295
|
+
# +user_id+ also maps to the provider's native user identifier where one
|
|
296
|
+
# exists.
|
|
297
|
+
#
|
|
291
298
|
#--
|
|
292
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
|
|
293
|
-
def generate(prompt = nil, files: nil)
|
|
294
|
-
Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files)
|
|
299
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
300
|
+
def generate(prompt = nil, files: nil, tags: {})
|
|
301
|
+
Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files, tags: tags)
|
|
295
302
|
end
|
|
296
303
|
|
|
297
304
|
# Streams a response from the agent, returning an +Enumerator+ of
|
|
298
|
-
# +Riffer::StreamEvents+. See +#generate+ for prompt/files semantics.
|
|
305
|
+
# +Riffer::StreamEvents+. See +#generate+ for prompt/files/tags semantics.
|
|
299
306
|
#
|
|
300
307
|
# Raises Riffer::ArgumentError if structured output is configured.
|
|
301
308
|
#
|
|
302
309
|
#--
|
|
303
|
-
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
304
|
-
def stream(prompt = nil, files: nil)
|
|
310
|
+
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
311
|
+
def stream(prompt = nil, files: nil, tags: {})
|
|
305
312
|
raise Riffer::ArgumentError, "Structured output is not supported with streaming. Use #generate instead." if @structured_output
|
|
306
|
-
Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files)
|
|
313
|
+
Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files, tags: tags)
|
|
307
314
|
end
|
|
308
315
|
|
|
309
316
|
# Interrupts the agent loop from an +on_message+ callback. Equivalent to
|
|
@@ -13,12 +13,16 @@ class Riffer::Guardrails::Runner
|
|
|
13
13
|
# The context passed to guardrails.
|
|
14
14
|
attr_reader :context #: untyped
|
|
15
15
|
|
|
16
|
+
# The normalized per-call tags, stamped as +riffer.tag.*+ on guardrail spans.
|
|
17
|
+
attr_reader :tags #: Hash[String, String]
|
|
18
|
+
|
|
16
19
|
#--
|
|
17
|
-
#: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped) -> void
|
|
18
|
-
def initialize(guardrail_configs, phase:, context: nil)
|
|
20
|
+
#: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void
|
|
21
|
+
def initialize(guardrail_configs, phase:, context: nil, tags: {})
|
|
19
22
|
@guardrail_configs = guardrail_configs
|
|
20
23
|
@phase = phase
|
|
21
24
|
@context = context
|
|
25
|
+
@tags = tags
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
# Runs the guardrails sequentially. For the +:before+ phase +data+ is the
|
|
@@ -129,7 +133,7 @@ class Riffer::Guardrails::Runner
|
|
|
129
133
|
{
|
|
130
134
|
"riffer.guardrail.name" => guardrail.name,
|
|
131
135
|
"riffer.guardrail.phase" => phase.to_s
|
|
132
|
-
}
|
|
136
|
+
}.merge(tags.transform_keys { |key| "riffer.tag.#{key}" })
|
|
133
137
|
end
|
|
134
138
|
|
|
135
139
|
#--
|
|
@@ -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/metrics.
|
|
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
|
|
|
@@ -46,6 +46,7 @@ class Riffer::Providers::Base
|
|
|
46
46
|
validate_normalized_messages!(messages)
|
|
47
47
|
messages = merge_consecutive_messages(messages)
|
|
48
48
|
params = build_request_params(messages, model, options)
|
|
49
|
+
tags = options[:tags] || {}
|
|
49
50
|
|
|
50
51
|
in_chat_span(model, messages, options) do |span|
|
|
51
52
|
response = execute_generate(params)
|
|
@@ -57,8 +58,8 @@ class Riffer::Providers::Base
|
|
|
57
58
|
structured_output = parse_structured_output(content) if options[:structured_output] && tool_calls.empty?
|
|
58
59
|
|
|
59
60
|
Riffer::Tracing.record_usage(span, token_usage)
|
|
60
|
-
record_token_usage_metric(model, token_usage)
|
|
61
|
-
record_cost_metric(model, token_usage)
|
|
61
|
+
record_token_usage_metric(model, token_usage, tags)
|
|
62
|
+
record_cost_metric(model, token_usage, tags)
|
|
62
63
|
record_finish_reason(span, finish_reason&.reason, finish_reason&.raw)
|
|
63
64
|
capture_output(span, content: content, tool_calls: tool_calls, finish_reason: finish_reason&.reason)
|
|
64
65
|
|
|
@@ -84,10 +85,12 @@ class Riffer::Providers::Base
|
|
|
84
85
|
validate_normalized_messages!(messages)
|
|
85
86
|
messages = merge_consecutive_messages(messages)
|
|
86
87
|
params = build_request_params(messages, model, options)
|
|
88
|
+
tags = options[:tags] || {}
|
|
87
89
|
|
|
88
90
|
# 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
|
-
#
|
|
91
|
+
# context is empty — capture here so the chat span parents to the caller's
|
|
92
|
+
# trace. tags are an ordinary local captured in the closure, so they reach
|
|
93
|
+
# the span/metric builders without re-propagation.
|
|
91
94
|
trace_context = Riffer::Tracing.current_context
|
|
92
95
|
Enumerator.new do |yielder|
|
|
93
96
|
Riffer::Tracing.with_context(trace_context) do
|
|
@@ -99,8 +102,8 @@ class Riffer::Providers::Base
|
|
|
99
102
|
execute_stream(params, sink)
|
|
100
103
|
if sink.is_a?(Riffer::Tracing::StreamRecorder)
|
|
101
104
|
record_stream_outcome(span, sink)
|
|
102
|
-
record_token_usage_metric(model, sink.token_usage)
|
|
103
|
-
record_cost_metric(model, sink.token_usage)
|
|
105
|
+
record_token_usage_metric(model, sink.token_usage, tags)
|
|
106
|
+
record_cost_metric(model, sink.token_usage, tags)
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
end
|
|
@@ -224,6 +227,7 @@ class Riffer::Providers::Base
|
|
|
224
227
|
def in_chat_span(model, messages, options)
|
|
225
228
|
start = Riffer::Metrics.monotonic_now
|
|
226
229
|
error_type = nil #: String?
|
|
230
|
+
tags = options[:tags] || {} #: Hash[String, String]
|
|
227
231
|
begin
|
|
228
232
|
Riffer::Tracing.in_span(model ? "chat #{model}" : "chat", attributes: chat_span_attributes(model, options), kind: :client) do |span|
|
|
229
233
|
capture_input(span, messages)
|
|
@@ -240,7 +244,7 @@ class Riffer::Providers::Base
|
|
|
240
244
|
error_type = error.class.name #: String?
|
|
241
245
|
raise
|
|
242
246
|
ensure
|
|
243
|
-
Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: chat_metric_attributes(model, error_type))
|
|
247
|
+
Riffer::Metrics::Instruments::OPERATION_DURATION.record(Riffer::Metrics.monotonic_now - start, attributes: chat_metric_attributes(model, error_type, tags))
|
|
244
248
|
end
|
|
245
249
|
end
|
|
246
250
|
|
|
@@ -258,47 +262,55 @@ class Riffer::Providers::Base
|
|
|
258
262
|
attributes[attribute] = value unless value.nil?
|
|
259
263
|
end
|
|
260
264
|
|
|
261
|
-
attributes
|
|
265
|
+
attributes.merge(tag_attributes(options[:tags] || {}))
|
|
262
266
|
end
|
|
263
267
|
|
|
264
268
|
#--
|
|
265
|
-
#: (String?) -> Hash[String, untyped]
|
|
266
|
-
def chat_metric_base_attributes(model)
|
|
269
|
+
#: (String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
270
|
+
def chat_metric_base_attributes(model, tags = {})
|
|
267
271
|
attributes = {
|
|
268
272
|
"gen_ai.operation.name" => "chat",
|
|
269
273
|
"gen_ai.provider.name" => self.class.semconv_provider_name
|
|
270
274
|
} #: Hash[String, untyped]
|
|
271
275
|
attributes["gen_ai.request.model"] = model if model
|
|
272
|
-
attributes
|
|
276
|
+
attributes.merge(tag_attributes(tags))
|
|
273
277
|
end
|
|
274
278
|
|
|
275
279
|
#--
|
|
276
|
-
#: (String?, String?) -> Hash[String, untyped]
|
|
277
|
-
def chat_metric_attributes(model, error_type)
|
|
278
|
-
attributes = chat_metric_base_attributes(model)
|
|
280
|
+
#: (String?, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
281
|
+
def chat_metric_attributes(model, error_type, tags = {})
|
|
282
|
+
attributes = chat_metric_base_attributes(model, tags)
|
|
279
283
|
attributes["error.type"] = error_type if error_type
|
|
280
284
|
attributes
|
|
281
285
|
end
|
|
282
286
|
|
|
287
|
+
# Maps normalized tags to their namespaced span/metric attribute form. An
|
|
288
|
+
# empty map yields an empty hash, so merging it is a no-op.
|
|
289
|
+
#--
|
|
290
|
+
#: (Hash[String, String]) -> Hash[String, String]
|
|
291
|
+
def tag_attributes(tags)
|
|
292
|
+
tags.transform_keys { |key| "riffer.tag.#{key}" }
|
|
293
|
+
end
|
|
294
|
+
|
|
283
295
|
# Per-call only — the run level would double-count an aggregate.
|
|
284
296
|
#--
|
|
285
|
-
#: (String?, Riffer::Providers::TokenUsage?) -> void
|
|
286
|
-
def record_token_usage_metric(model, usage)
|
|
297
|
+
#: (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
298
|
+
def record_token_usage_metric(model, usage, tags = {})
|
|
287
299
|
return unless usage
|
|
288
300
|
|
|
289
|
-
base = chat_metric_base_attributes(model)
|
|
301
|
+
base = chat_metric_base_attributes(model, tags)
|
|
290
302
|
Riffer::Metrics::Instruments::TOKEN_USAGE.record(usage.input_tokens, attributes: base.merge("gen_ai.token.type" => "input"))
|
|
291
303
|
Riffer::Metrics::Instruments::TOKEN_USAGE.record(usage.output_tokens, attributes: base.merge("gen_ai.token.type" => "output"))
|
|
292
304
|
end
|
|
293
305
|
|
|
294
306
|
# Per-call only — the run level would double-count an aggregate.
|
|
295
307
|
#--
|
|
296
|
-
#: (String?, Riffer::Providers::TokenUsage?) -> void
|
|
297
|
-
def record_cost_metric(model, usage)
|
|
308
|
+
#: (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
309
|
+
def record_cost_metric(model, usage, tags = {})
|
|
298
310
|
cost = usage&.cost
|
|
299
311
|
return unless cost
|
|
300
312
|
|
|
301
|
-
Riffer::Metrics::Instruments::COST.record(cost, attributes: chat_metric_base_attributes(model))
|
|
313
|
+
Riffer::Metrics::Instruments::COST.record(cost, attributes: chat_metric_base_attributes(model, tags))
|
|
302
314
|
end
|
|
303
315
|
|
|
304
316
|
#--
|
|
@@ -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/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::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|
|
|
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,24 +129,32 @@ 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 —
|
data/lib/riffer/version.rb
CHANGED
|
@@ -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,12 +111,20 @@ 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]
|
|
120
|
+
|
|
121
|
+
# --
|
|
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]
|
|
111
128
|
|
|
112
129
|
# --
|
|
113
130
|
# : (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 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>.
|
|
@@ -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
|
|
@@ -99,22 +99,28 @@ class Riffer::Providers::Base
|
|
|
99
99
|
def chat_span_attributes: (String?, Hash[Symbol, untyped]) -> Hash[String, untyped]
|
|
100
100
|
|
|
101
101
|
# --
|
|
102
|
-
# : (String?) -> Hash[String, untyped]
|
|
103
|
-
def chat_metric_base_attributes: (String?) -> Hash[String, untyped]
|
|
102
|
+
# : (String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
103
|
+
def chat_metric_base_attributes: (String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
104
104
|
|
|
105
105
|
# --
|
|
106
|
-
# : (String?, String?) -> Hash[String, untyped]
|
|
107
|
-
def chat_metric_attributes: (String?, String?) -> Hash[String, untyped]
|
|
106
|
+
# : (String?, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
107
|
+
def chat_metric_attributes: (String?, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
108
|
+
|
|
109
|
+
# Maps normalized tags to their namespaced span/metric attribute form. An
|
|
110
|
+
# empty map yields an empty hash, so merging it is a no-op.
|
|
111
|
+
# --
|
|
112
|
+
# : (Hash[String, String]) -> Hash[String, String]
|
|
113
|
+
def tag_attributes: (Hash[String, String]) -> Hash[String, String]
|
|
108
114
|
|
|
109
115
|
# Per-call only — the run level would double-count an aggregate.
|
|
110
116
|
# --
|
|
111
|
-
# : (String?, Riffer::Providers::TokenUsage?) -> void
|
|
112
|
-
def record_token_usage_metric: (String?, Riffer::Providers::TokenUsage?) -> void
|
|
117
|
+
# : (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
118
|
+
def record_token_usage_metric: (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
113
119
|
|
|
114
120
|
# Per-call only — the run level would double-count an aggregate.
|
|
115
121
|
# --
|
|
116
|
-
# : (String?, Riffer::Providers::TokenUsage?) -> void
|
|
117
|
-
def record_cost_metric: (String?, Riffer::Providers::TokenUsage?) -> void
|
|
122
|
+
# : (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
123
|
+
def record_cost_metric: (String?, Riffer::Providers::TokenUsage?, ?Hash[String, String]) -> void
|
|
118
124
|
|
|
119
125
|
# --
|
|
120
126
|
# : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Symbol?, String?) -> void
|
|
@@ -12,8 +12,8 @@ class Riffer::Tools::Runtime
|
|
|
12
12
|
|
|
13
13
|
# Executes a batch of tool calls, returning <tt>[tool_call, response]</tt> pairs.
|
|
14
14
|
# --
|
|
15
|
-
# : (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]]
|
|
16
|
-
def execute: (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 ]]
|
|
15
|
+
# : (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]]
|
|
16
|
+
def execute: (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 ]]
|
|
17
17
|
|
|
18
18
|
# Hook wrapping each tool call; override in subclasses to instrument or
|
|
19
19
|
# customize. Must +yield+ to continue.
|
|
@@ -36,8 +36,8 @@ class Riffer::Tools::Runtime
|
|
|
36
36
|
private
|
|
37
37
|
|
|
38
38
|
# --
|
|
39
|
-
# : (Riffer::Messages::Assistant::ToolCall) { () -> Riffer::Tools::Response } -> [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
|
|
40
|
-
def instrument_tool_call: (Riffer::Messages::Assistant::ToolCall) { () -> Riffer::Tools::Response } -> [ Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response ]
|
|
39
|
+
# : (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { () -> Riffer::Tools::Response } -> [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
|
|
40
|
+
def instrument_tool_call: (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { () -> Riffer::Tools::Response } -> [ Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response ]
|
|
41
41
|
|
|
42
42
|
# --
|
|
43
43
|
# : (Riffer::Messages::Assistant::ToolCall, tools: Array[singleton(Riffer::Tool)], context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) -> Riffer::Tools::Response
|
|
@@ -49,16 +49,22 @@ class Riffer::Tools::Runtime
|
|
|
49
49
|
|
|
50
50
|
# Emitted outside +around_tool_call+ so host enrichment spans nest beneath it.
|
|
51
51
|
# --
|
|
52
|
-
# : [R] (Riffer::Messages::Assistant::ToolCall) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> R } -> R
|
|
53
|
-
def in_tool_span: [R] (Riffer::Messages::Assistant::ToolCall) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R
|
|
52
|
+
# : [R] (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> R } -> R
|
|
53
|
+
def in_tool_span: [R] (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R
|
|
54
54
|
|
|
55
55
|
# --
|
|
56
|
-
# : (Riffer::Messages::Assistant::ToolCall) -> Hash[String, untyped]
|
|
57
|
-
def tool_span_attributes: (Riffer::Messages::Assistant::ToolCall) -> Hash[String, untyped]
|
|
56
|
+
# : (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) -> Hash[String, untyped]
|
|
57
|
+
def tool_span_attributes: (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) -> Hash[String, untyped]
|
|
58
58
|
|
|
59
59
|
# --
|
|
60
|
-
# : (Riffer::Messages::Assistant::ToolCall, String?) -> Hash[String, untyped]
|
|
61
|
-
def tool_metric_attributes: (Riffer::Messages::Assistant::ToolCall, String?) -> Hash[String, untyped]
|
|
60
|
+
# : (Riffer::Messages::Assistant::ToolCall, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
61
|
+
def tool_metric_attributes: (Riffer::Messages::Assistant::ToolCall, String?, ?Hash[String, String]) -> Hash[String, untyped]
|
|
62
|
+
|
|
63
|
+
# Maps normalized tags to their namespaced span/metric attribute form. An
|
|
64
|
+
# empty map yields an empty hash, so merging it is a no-op.
|
|
65
|
+
# --
|
|
66
|
+
# : (Hash[String, String]) -> Hash[String, String]
|
|
67
|
+
def tag_attributes: (Hash[String, String]) -> Hash[String, String]
|
|
62
68
|
|
|
63
69
|
# A returned error Response is a handled outcome, so its status stays unset —
|
|
64
70
|
# an error span status is reserved for a raised exception.
|