activeagents-telemetry-ruby_llm 0.3.0 → 0.3.2
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/README.md +60 -4
- data/lib/activeagents/telemetry/ruby_llm/version.rb +1 -1
- data/lib/activeagents/telemetry/ruby_llm.rb +109 -14
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e97d04ad7846320b7c233f247ce4ecad0ea2f20dba9574a73d2c3f93d3a47bfd
|
|
4
|
+
data.tar.gz: 50e301417298577aad66eb5d8423689d2c24742e5cc91be7a5fd55f518695c22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 759dd7ce43f5c74c4eef4003ec72e2254e618523a132fcd0e7d8c249416fb2c8a4d96170d2984d7a6c352ca95ff4240d1beff259d8d96b410807e02836660199
|
|
7
|
+
data.tar.gz: 2e49c0ec5f861e55fc42304ea30e3f27e5bcefe8043e841d54cb2bcb8bc39c95870793baa87d6128006ada7e5b8e12dc3aeeaa83ad5fd5d4b277c663b64e2d3c
|
data/README.md
CHANGED
|
@@ -124,10 +124,66 @@ exceptions. A turn keeps the scope it started under, so a turn left open by a
|
|
|
124
124
|
pending tool call and closed later by `flush!` still reports with this agent,
|
|
125
125
|
attributes and callback, and a turn that started outside any scope never adopts
|
|
126
126
|
one. Context
|
|
127
|
-
is restored after the block, including when it raises
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
is restored after the block, including when it raises. A callback error is
|
|
128
|
+
logged by exception class without dropping the trace. Attribute redaction and
|
|
129
|
+
content-capture settings continue to apply.
|
|
130
|
+
|
|
131
|
+
### Nested scopes
|
|
132
|
+
|
|
133
|
+
Scopes compose, so an evaluation can wrap code that names its own agent
|
|
134
|
+
without losing what the evaluation attached. When a `with_agent` block opens
|
|
135
|
+
inside another one:
|
|
136
|
+
|
|
137
|
+
| Setting | Nested scope inside a plain scope | Nested scope inside a `pin: true` scope |
|
|
138
|
+
|---------|-----------------------------------|------------------------------------------|
|
|
139
|
+
| `attributes` | Merged: enclosing first, nested keys win on collision | Merged the same way |
|
|
140
|
+
| `on_trace` | Inherited unless the nested call passes its own | The pinned scope's callback stays in force |
|
|
141
|
+
| `synchronous` | Inherited unless the nested call passes its own (`nil`, the default, inherits; an explicit `false` overrides an inherited `true`) | The pinned scope's setting stays in force |
|
|
142
|
+
| `name` / `action` | Replaced by the nested scope | The pinned scope's identity stays in force |
|
|
143
|
+
|
|
144
|
+
A pinned scope owns the turn's identity: while it is active, a nested
|
|
145
|
+
`with_agent` may only add attributes, and it stays pinned however deep the
|
|
146
|
+
nesting goes. The previous scope is restored exactly on exit, including when
|
|
147
|
+
the block raises. The `agent.*` identity keys are always written last, so an
|
|
148
|
+
attribute with one of those names never overrides the reported agent.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
ActiveAgents::Telemetry::RubyLLM.with_agent(
|
|
152
|
+
"SupportAgent", action: "replay",
|
|
153
|
+
attributes: { "eval.run_id" => run_id, "eval.result_id" => result_id },
|
|
154
|
+
on_trace: ->(trace) { result.trace_id = trace.trace_id },
|
|
155
|
+
synchronous: true, pin: true
|
|
156
|
+
) do
|
|
157
|
+
# The code under test names its own scope around the actual call. The
|
|
158
|
+
# trace reports as SupportAgent.replay with eval.* and sparkle.role, the
|
|
159
|
+
# evaluation's on_trace receives it, and it is delivered before this
|
|
160
|
+
# block returns.
|
|
161
|
+
ActiveAgents::Telemetry::RubyLLM.with_agent("SupportAgent", action: "respond",
|
|
162
|
+
attributes: { "sparkle.role" => "assistant" }) do
|
|
163
|
+
chat.ask(prompt)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Correlating with ActiveAgent evaluations
|
|
169
|
+
|
|
170
|
+
`ActiveAgent::Evals::Correlation` (activeagent >= 1.6.3) takes a `tracer:`
|
|
171
|
+
that it calls as `tracer.call(name, action:, attributes:, on_trace:) { ... }`
|
|
172
|
+
around each replay, and expects `on_trace` to receive something answering
|
|
173
|
+
`#trace_id`. `correlation_tracer` returns exactly that lambda, opening a
|
|
174
|
+
pinned, synchronous `with_agent` scope with the arguments it is given and
|
|
175
|
+
returning the block's value:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
ActiveAgent::Evals::Correlation.new(
|
|
179
|
+
agent_name: "SupportAgent",
|
|
180
|
+
tracer: ActiveAgents::Telemetry::RubyLLM.correlation_tracer
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`correlation_tracer(synchronous: false)` or `correlation_tracer(pin: false)`
|
|
185
|
+
relaxes either default. This gem does not depend on activeagent; the only
|
|
186
|
+
contract shared with it is the lambda's signature.
|
|
131
187
|
|
|
132
188
|
Report publication and trace ingestion are separate operations. Persist each run
|
|
133
189
|
and result ID in the evaluation report and attach the same IDs to its response and
|
|
@@ -43,6 +43,9 @@ module ActiveAgents
|
|
|
43
43
|
|
|
44
44
|
DEFAULT_AGENT = { name: "RubyLLM::Chat", action: "chat" }.freeze
|
|
45
45
|
|
|
46
|
+
# Span token keys and the reader that answers each on a RubyLLM 2.x +Tokens+.
|
|
47
|
+
TOKEN_READERS = { "input" => :input, "output" => :output, "thinking" => :thinking }.freeze
|
|
48
|
+
|
|
46
49
|
State = Struct.new(:depth, :started_at, :tool_spans, :rounds, :tokens, :chat_key, :agent)
|
|
47
50
|
|
|
48
51
|
class << self
|
|
@@ -102,17 +105,71 @@ module ActiveAgents
|
|
|
102
105
|
# `on_trace` runs only for a trace the reporter accepted, which means
|
|
103
106
|
# it passed the enabled, configured and sampling checks: a delivery
|
|
104
107
|
# that then fails is logged by the reporter, not announced here.
|
|
105
|
-
|
|
108
|
+
#
|
|
109
|
+
# Scopes compose. Inside an enclosing scope, a nested call merges its
|
|
110
|
+
# `attributes` onto the enclosing ones (a nested key wins on
|
|
111
|
+
# collision) and inherits `on_trace` and `synchronous` unless it
|
|
112
|
+
# passes its own; `synchronous: nil`, the default, means "inherit,
|
|
113
|
+
# else false", so an explicit `synchronous: false` still overrides an
|
|
114
|
+
# inherited `true`. Without `pin`, the nested scope replaces `name`
|
|
115
|
+
# and `action`, so a call site that names its own agent keeps doing
|
|
116
|
+
# so under an evaluation's scope. A scope opened with `pin: true` owns
|
|
117
|
+
# the turn's identity: while it is active a nested `with_agent` may
|
|
118
|
+
# only add attributes, and the pinned scope's `name`, `action`,
|
|
119
|
+
# `on_trace` and `synchronous` stay in force whatever the nested call
|
|
120
|
+
# passes. An evaluation replay pins its scope so the code under test,
|
|
121
|
+
# which opens its own scope around the actual `chat.ask`, cannot
|
|
122
|
+
# detach the trace from the evaluation's attributes, callback and
|
|
123
|
+
# synchronous delivery. The previous scope is restored on exit,
|
|
124
|
+
# including when the block raises.
|
|
125
|
+
#
|
|
126
|
+
# @param synchronous [Boolean, nil] nil inherits the enclosing scope's
|
|
127
|
+
# setting, or false at the top level.
|
|
128
|
+
# @param pin [Boolean] whether this scope keeps its identity, callback
|
|
129
|
+
# and delivery mode against nested scopes.
|
|
130
|
+
def with_agent(name, action: "chat", attributes: {}, on_trace: nil, synchronous: nil, pin: false)
|
|
106
131
|
previous = Thread.current[AGENT_KEY]
|
|
107
|
-
Thread.current[AGENT_KEY] =
|
|
108
|
-
name: name, action: action, attributes: attributes
|
|
109
|
-
on_trace: on_trace, synchronous: synchronous
|
|
110
|
-
|
|
132
|
+
Thread.current[AGENT_KEY] = compose_scope(
|
|
133
|
+
previous, name: name, action: action, attributes: attributes,
|
|
134
|
+
on_trace: on_trace, synchronous: synchronous, pin: pin
|
|
135
|
+
)
|
|
111
136
|
yield
|
|
112
137
|
ensure
|
|
113
138
|
Thread.current[AGENT_KEY] = previous
|
|
114
139
|
end
|
|
115
140
|
|
|
141
|
+
# A `tracer:` for `ActiveAgent::Evals::Correlation` (activeagent >=
|
|
142
|
+
# 1.6.3), which wraps each evaluation replay in
|
|
143
|
+
# `tracer.call(name, action:, attributes:, on_trace:) { ... }` and
|
|
144
|
+
# expects `on_trace` to receive something answering `#trace_id`. The
|
|
145
|
+
# returned lambda opens a pinned, synchronous `with_agent` scope with
|
|
146
|
+
# exactly those arguments, so the replayed code's own `with_agent`
|
|
147
|
+
# around `chat.ask` can add attributes but cannot replace the
|
|
148
|
+
# evaluation's identity, drop its `on_trace` or turn delivery
|
|
149
|
+
# asynchronous, and the trace id the callback receives is the one
|
|
150
|
+
# the reporter delivered before the replay returns. The block's value
|
|
151
|
+
# is returned. There is no dependency on activeagent: the contract is
|
|
152
|
+
# only the lambda's signature.
|
|
153
|
+
#
|
|
154
|
+
# ActiveAgent::Evals::Correlation.new(
|
|
155
|
+
# agent_name: "SupportAgent",
|
|
156
|
+
# tracer: ActiveAgents::Telemetry::RubyLLM.correlation_tracer
|
|
157
|
+
# )
|
|
158
|
+
#
|
|
159
|
+
# @param synchronous [Boolean] whether the scope delivers in the
|
|
160
|
+
# calling thread; true keeps the trace id available when the
|
|
161
|
+
# replay returns and before a short-lived process exits.
|
|
162
|
+
# @param pin [Boolean] whether nested scopes may only add attributes.
|
|
163
|
+
# @return [Proc] `->(name, action:, attributes:, on_trace:, &block)`
|
|
164
|
+
def correlation_tracer(synchronous: true, pin: true)
|
|
165
|
+
lambda do |name, action: "chat", attributes: {}, on_trace: nil, &block|
|
|
166
|
+
with_agent(
|
|
167
|
+
name, action: action, attributes: attributes, on_trace: on_trace,
|
|
168
|
+
synchronous: synchronous, pin: pin, &block
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
116
173
|
def state
|
|
117
174
|
Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil, nil)
|
|
118
175
|
end
|
|
@@ -179,6 +236,27 @@ module ActiveAgents
|
|
|
179
236
|
|
|
180
237
|
private
|
|
181
238
|
|
|
239
|
+
# The scope a nested `with_agent` installs, given the enclosing one.
|
|
240
|
+
# Attributes always merge with the nested keys winning; the rest
|
|
241
|
+
# depends on whether the enclosing scope is pinned.
|
|
242
|
+
def compose_scope(enclosing, name:, action:, attributes:, on_trace:, synchronous:, pin:)
|
|
243
|
+
own = attributes.to_h.transform_keys(&:to_s)
|
|
244
|
+
if enclosing.nil?
|
|
245
|
+
return { name: name, action: action, attributes: own, on_trace: on_trace,
|
|
246
|
+
synchronous: synchronous ? true : false, pin: pin ? true : false }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
merged = (enclosing[:attributes] || {}).merge(own)
|
|
250
|
+
return enclosing.merge(attributes: merged) if enclosing[:pin]
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
name: name, action: action, attributes: merged,
|
|
254
|
+
on_trace: on_trace.nil? ? enclosing[:on_trace] : on_trace,
|
|
255
|
+
synchronous: synchronous.nil? ? enclosing[:synchronous] : synchronous,
|
|
256
|
+
pin: pin ? true : false
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
182
260
|
def report_turn(payload, turn)
|
|
183
261
|
agent = turn.agent || resolve_agent(payload) || DEFAULT_AGENT
|
|
184
262
|
started_at = turn.started_at || Time.now
|
|
@@ -306,22 +384,39 @@ module ActiveAgents
|
|
|
306
384
|
text.to_s[0, CONTENT_LIMIT]
|
|
307
385
|
end
|
|
308
386
|
|
|
387
|
+
# Tokens for one round. RubyLLM 2.x puts the round's +Tokens+ on the
|
|
388
|
+
# payload and moves the per-message readers to +message.tokens+; 1.x
|
|
389
|
+
# has +input_tokens+ and friends on each assistant message and nothing
|
|
390
|
+
# on the payload. The payload is read first, then the messages the
|
|
391
|
+
# round added, with either generation's readers.
|
|
309
392
|
def token_totals(payload)
|
|
393
|
+
tokens = payload_tokens(payload[:tokens]) || message_tokens(payload)
|
|
394
|
+
tokens["total"] = tokens.values.sum
|
|
395
|
+
tokens
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def payload_tokens(tokens)
|
|
399
|
+
return unless TOKEN_READERS.values.all? { |reader| tokens.respond_to?(reader) }
|
|
400
|
+
|
|
401
|
+
TOKEN_READERS.to_h { |key, reader| [ key, tokens.public_send(reader).to_i ] }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def message_tokens(payload)
|
|
310
405
|
initial_count = Array(payload[:input_messages]).size
|
|
311
406
|
new_messages = Array(payload[:messages_after])[initial_count..] || []
|
|
312
407
|
assistant_messages = new_messages.select { |message| message.respond_to?(:role) && message.role.to_s == "assistant" }
|
|
313
408
|
|
|
314
|
-
|
|
315
|
-
"input" => sum_tokens(assistant_messages, :input_tokens),
|
|
316
|
-
"output" => sum_tokens(assistant_messages, :output_tokens),
|
|
317
|
-
"thinking" => sum_tokens(assistant_messages, :thinking_tokens)
|
|
318
|
-
}
|
|
319
|
-
tokens["total"] = tokens.values.sum
|
|
320
|
-
tokens
|
|
409
|
+
TOKEN_READERS.to_h { |key, reader| [ key, assistant_messages.sum { |message| message_token_count(message, key, reader) } ] }
|
|
321
410
|
end
|
|
322
411
|
|
|
323
|
-
|
|
324
|
-
|
|
412
|
+
# A 1.x message answers +input_tokens+; a 2.x message answers
|
|
413
|
+
# +tokens.input+.
|
|
414
|
+
def message_token_count(message, key, reader)
|
|
415
|
+
legacy_reader = :"#{key}_tokens"
|
|
416
|
+
return message.public_send(legacy_reader).to_i if message.respond_to?(legacy_reader)
|
|
417
|
+
|
|
418
|
+
tokens = message.tokens if message.respond_to?(:tokens)
|
|
419
|
+
tokens.respond_to?(reader) ? tokens.public_send(reader).to_i : 0
|
|
325
420
|
end
|
|
326
421
|
end
|
|
327
422
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeagents-telemetry-ruby_llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ActiveAgents
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeagents-telemetry
|