activeagents-telemetry-ruby_llm 0.3.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 255ca4c32e9f775c8b2e45ab431ae81ee410fa3002d66dce026ba06d948da4b6
4
- data.tar.gz: 1afbdf608524f69045341e10cd7a6f722854cc49977e27ff511cafcc8368fa72
3
+ metadata.gz: e97d04ad7846320b7c233f247ce4ecad0ea2f20dba9574a73d2c3f93d3a47bfd
4
+ data.tar.gz: 50e301417298577aad66eb5d8423689d2c24742e5cc91be7a5fd55f518695c22
5
5
  SHA512:
6
- metadata.gz: 5b316442b6a5bb9044df92b691d6d4dee824d12522c45f4367d252cf7041aeee2be2060ce3a604f9b0e36b814c04e78ef5280646ad67ae7f893c29f0a1799c2b
7
- data.tar.gz: 2fe33cb43e2a35659c2112decb662199b461f756e8ef63560e4e7b63151fde0fdcfa1a4f042c6f0419bead7e41787ec43ba51fdbcee58bc4cd1550014a3f56b5
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, and nested scopes use
128
- their own attributes. A callback error is logged by exception class without
129
- dropping the trace. Attribute redaction and content-capture settings continue to
130
- apply.
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
@@ -3,7 +3,7 @@
3
3
  module ActiveAgents
4
4
  module Telemetry
5
5
  module RubyLLM
6
- VERSION = "0.3.1"
6
+ VERSION = "0.3.2"
7
7
  end
8
8
  end
9
9
  end
@@ -105,17 +105,71 @@ module ActiveAgents
105
105
  # `on_trace` runs only for a trace the reporter accepted, which means
106
106
  # it passed the enabled, configured and sampling checks: a delivery
107
107
  # that then fails is logged by the reporter, not announced here.
108
- def with_agent(name, action: "chat", attributes: {}, on_trace: nil, synchronous: false)
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)
109
131
  previous = Thread.current[AGENT_KEY]
110
- Thread.current[AGENT_KEY] = {
111
- name: name, action: action, attributes: attributes.to_h.transform_keys(&:to_s),
112
- on_trace: on_trace, synchronous: synchronous
113
- }
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
+ )
114
136
  yield
115
137
  ensure
116
138
  Thread.current[AGENT_KEY] = previous
117
139
  end
118
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
+
119
173
  def state
120
174
  Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil, nil)
121
175
  end
@@ -182,6 +236,27 @@ module ActiveAgents
182
236
 
183
237
  private
184
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
+
185
260
  def report_turn(payload, turn)
186
261
  agent = turn.agent || resolve_agent(payload) || DEFAULT_AGENT
187
262
  started_at = turn.started_at || Time.now
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.1
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-09-21 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeagents-telemetry