activeagents-telemetry-ruby_llm 0.1.0 → 0.3.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/README.md +44 -1
- data/lib/activeagents/telemetry/ruby_llm/version.rb +1 -1
- data/lib/activeagents/telemetry/ruby_llm.rb +107 -16
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5abf7d85a5deec9d008170f9c9a3e50d619024b7bad8c1f01eea3eda3d3befc6
|
|
4
|
+
data.tar.gz: ea10cb9f3cb45700fe8ad0642c72eb3c6e01576c1e4353d274998ead5f3f7243
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67e5b7d06a3de8510dead200b6ec9a4da44172d24b46c3ea02646cfd4fad1fc3c5acb960798e17a5957f61d8dcd4bd7efc392ae08f8e46ef19b084014e357073
|
|
7
|
+
data.tar.gz: 16dde44b30870de36415b0958b85eabff49caf639238baf140107e6b2a986effe21bf0eea529c78b5810d90816098cac8c35f09800304c3c5cddd3c7ab6b3c22
|
data/README.md
CHANGED
|
@@ -48,7 +48,9 @@ enclosing event, 2.x drives a flat `step until complete?` loop whose rounds
|
|
|
48
48
|
are siblings with tool calls between them. Rounds are accumulated and flushed
|
|
49
49
|
on the round that ends the turn, so both produce the same trace.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Prompts, completions, and tool arguments/results are sent only when the
|
|
52
|
+
configuration's `capture_bodies` is enabled (off by default, truncated to
|
|
53
|
+
4,000 characters); error messages are truncated.
|
|
52
54
|
|
|
53
55
|
## Naming the traffic
|
|
54
56
|
|
|
@@ -90,3 +92,44 @@ an explicit `flush!`.
|
|
|
90
92
|
```bash
|
|
91
93
|
bundle exec rake test
|
|
92
94
|
```
|
|
95
|
+
|
|
96
|
+
## Correlating evaluation traces
|
|
97
|
+
|
|
98
|
+
Use a separate identity for judge calls and attach stable evaluation identifiers
|
|
99
|
+
without changing the application's default agent resolver:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
trace_ids = []
|
|
103
|
+
ActiveAgents::Telemetry::RubyLLM.with_agent(
|
|
104
|
+
"EvaluationJudge", action: "score",
|
|
105
|
+
attributes: { "eval.run_id" => run_id, "eval.result_id" => result_id },
|
|
106
|
+
on_trace: ->(trace) { trace_ids << trace.trace_id },
|
|
107
|
+
synchronous: true
|
|
108
|
+
) do
|
|
109
|
+
judge_chat.ask(prompt)
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The callback receives each trace the reporter accepted for delivery, so an
|
|
114
|
+
evaluation result can retain the trace ID of that delivery attempt. Acceptance is
|
|
115
|
+
not ingestion: the trace passed the enabled, configured and sampling checks, but
|
|
116
|
+
a delivery that then fails is logged by the reporter rather than announced here,
|
|
117
|
+
and an asynchronous delivery can outlive a process that exits right away. A trace
|
|
118
|
+
dropped by `sample_rate` or by a disabled configuration is never announced.
|
|
119
|
+
`synchronous: true` delivers in the calling thread for this scope only, through
|
|
120
|
+
the same sampling and configuration checks as ordinary delivery; it does not
|
|
121
|
+
mutate the shared async configuration. Normal reporter error logging still
|
|
122
|
+
applies: synchronous delivery does not turn telemetry failures into application
|
|
123
|
+
exceptions. A turn keeps the scope it started under, so a turn left open by a
|
|
124
|
+
pending tool call and closed later by `flush!` still reports with this agent,
|
|
125
|
+
attributes and callback, and a turn that started outside any scope never adopts
|
|
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.
|
|
131
|
+
|
|
132
|
+
Report publication and trace ingestion are separate operations. Persist each run
|
|
133
|
+
and result ID in the evaluation report and attach the same IDs to its response and
|
|
134
|
+
judge trace attributes. The application chooses whether to publish full report
|
|
135
|
+
content; this API does not enable body capture or upload reports automatically.
|
|
@@ -27,7 +27,9 @@ module ActiveAgents
|
|
|
27
27
|
# Tokens are summed per round from the assistant messages that round added,
|
|
28
28
|
# so a repeated event-level count is never double counted.
|
|
29
29
|
#
|
|
30
|
-
#
|
|
30
|
+
# Prompts, completions, and tool arguments/results are not sent unless the
|
|
31
|
+
# configuration's capture_bodies is enabled (they may contain sensitive
|
|
32
|
+
# data); error messages are truncated.
|
|
31
33
|
module RubyLLM
|
|
32
34
|
AGENT_KEY = :activeagents_telemetry_ruby_llm_agent
|
|
33
35
|
STATE_KEY = :activeagents_telemetry_ruby_llm_state
|
|
@@ -36,10 +38,12 @@ module ActiveAgents
|
|
|
36
38
|
# A turn that never reaches a final round (a halted tool call, or an app
|
|
37
39
|
# driving RubyLLM 2.x's `step` by hand) would otherwise accumulate forever.
|
|
38
40
|
MAX_TURN_SECONDS = 600
|
|
41
|
+
# Captured prompt/completion/tool content is truncated to this many characters.
|
|
42
|
+
CONTENT_LIMIT = 4_000
|
|
39
43
|
|
|
40
44
|
DEFAULT_AGENT = { name: "RubyLLM::Chat", action: "chat" }.freeze
|
|
41
45
|
|
|
42
|
-
State = Struct.new(:depth, :started_at, :tool_spans, :rounds, :tokens, :chat_key)
|
|
46
|
+
State = Struct.new(:depth, :started_at, :tool_spans, :rounds, :tokens, :chat_key, :agent)
|
|
43
47
|
|
|
44
48
|
class << self
|
|
45
49
|
# Subscribes to RubyLLM's instrumentation.
|
|
@@ -87,17 +91,30 @@ module ActiveAgents
|
|
|
87
91
|
|
|
88
92
|
attr_writer :reporter
|
|
89
93
|
|
|
90
|
-
# Attributes traces inside the block to a named agent/action.
|
|
91
|
-
|
|
94
|
+
# Attributes traces inside the block to a named agent/action. Correlation
|
|
95
|
+
# attributes and the callback apply to this scope only. Short-lived
|
|
96
|
+
# evaluation commands can deliver synchronously without changing the
|
|
97
|
+
# application's shared reporter configuration.
|
|
98
|
+
#
|
|
99
|
+
# A turn keeps the scope it started under, so a turn left open by a
|
|
100
|
+
# pending tool call and closed later by `flush!` still reports as this
|
|
101
|
+
# agent, and a turn that started outside any scope never adopts one.
|
|
102
|
+
# `on_trace` runs only for a trace the reporter accepted, which means
|
|
103
|
+
# it passed the enabled, configured and sampling checks: a delivery
|
|
104
|
+
# that then fails is logged by the reporter, not announced here.
|
|
105
|
+
def with_agent(name, action: "chat", attributes: {}, on_trace: nil, synchronous: false)
|
|
92
106
|
previous = Thread.current[AGENT_KEY]
|
|
93
|
-
Thread.current[AGENT_KEY] = {
|
|
107
|
+
Thread.current[AGENT_KEY] = {
|
|
108
|
+
name: name, action: action, attributes: attributes.to_h.transform_keys(&:to_s),
|
|
109
|
+
on_trace: on_trace, synchronous: synchronous
|
|
110
|
+
}
|
|
94
111
|
yield
|
|
95
112
|
ensure
|
|
96
113
|
Thread.current[AGENT_KEY] = previous
|
|
97
114
|
end
|
|
98
115
|
|
|
99
116
|
def state
|
|
100
|
-
Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil)
|
|
117
|
+
Thread.current[STATE_KEY] ||= State.new(0, nil, [], 0, Span::ZERO_TOKENS.dup, nil, nil)
|
|
101
118
|
end
|
|
102
119
|
|
|
103
120
|
def clear_state
|
|
@@ -122,7 +139,13 @@ module ActiveAgents
|
|
|
122
139
|
flush! if turn.rounds.positive? && (turn.chat_key != chat_key || turn_expired?(turn))
|
|
123
140
|
turn = state
|
|
124
141
|
turn.chat_key = chat_key
|
|
125
|
-
turn.started_at
|
|
142
|
+
if turn.started_at.nil?
|
|
143
|
+
turn.started_at = Time.now
|
|
144
|
+
# Captured once, on the turn's first round, whether or not a scope
|
|
145
|
+
# is active: a turn that started unscoped stays unscoped even when
|
|
146
|
+
# a later scope's chat is what flushes it.
|
|
147
|
+
turn.agent = Thread.current[AGENT_KEY]
|
|
148
|
+
end
|
|
126
149
|
end
|
|
127
150
|
turn.depth += 1
|
|
128
151
|
end
|
|
@@ -139,11 +162,16 @@ module ActiveAgents
|
|
|
139
162
|
|
|
140
163
|
def build_tool_span(payload, started_at, finished_at)
|
|
141
164
|
error = payload[:exception_object]
|
|
165
|
+
attributes = { "tool.name" => payload[:tool_name].to_s, "tool.call_id" => payload[:tool_call_id].to_s }
|
|
166
|
+
if configuration.capture_bodies?
|
|
167
|
+
attributes["tool.arguments"] = tool_io_json(payload[:tool_arguments])
|
|
168
|
+
attributes["tool.result"] = tool_io_json(payload[:result_content]) unless error
|
|
169
|
+
end
|
|
142
170
|
span = Span.new(
|
|
143
171
|
"tool.#{payload[:tool_name]}",
|
|
144
172
|
type: "tool",
|
|
145
173
|
start_time: started_at,
|
|
146
|
-
attributes:
|
|
174
|
+
attributes: attributes
|
|
147
175
|
)
|
|
148
176
|
span.record_error(error, message_limit: configuration.error_message_limit) if error
|
|
149
177
|
span.finish(at: finished_at)
|
|
@@ -152,7 +180,7 @@ module ActiveAgents
|
|
|
152
180
|
private
|
|
153
181
|
|
|
154
182
|
def report_turn(payload, turn)
|
|
155
|
-
agent =
|
|
183
|
+
agent = turn.agent || resolve_agent(payload) || DEFAULT_AGENT
|
|
156
184
|
started_at = turn.started_at || Time.now
|
|
157
185
|
finished_at = Time.now
|
|
158
186
|
error = payload[:exception_object]
|
|
@@ -163,14 +191,17 @@ module ActiveAgents
|
|
|
163
191
|
resource_attributes: configuration.resource_attributes
|
|
164
192
|
)
|
|
165
193
|
|
|
194
|
+
root_attributes = (agent[:attributes] || {}).merge(
|
|
195
|
+
"agent.class" => agent[:name],
|
|
196
|
+
"agent.action" => agent[:action],
|
|
197
|
+
"agent.provider" => payload[:provider].to_s,
|
|
198
|
+
"agent.model" => payload[:model].to_s
|
|
199
|
+
)
|
|
200
|
+
root_attributes.merge!(conversation_attributes(payload)) if configuration.capture_bodies?
|
|
201
|
+
|
|
166
202
|
root = trace.span(
|
|
167
203
|
"#{agent[:name]}.#{agent[:action]}", type: "root", start_time: started_at,
|
|
168
|
-
attributes:
|
|
169
|
-
"agent.class" => agent[:name],
|
|
170
|
-
"agent.action" => agent[:action],
|
|
171
|
-
"agent.provider" => payload[:provider].to_s,
|
|
172
|
-
"agent.model" => payload[:model].to_s
|
|
173
|
-
}
|
|
204
|
+
attributes: root_attributes
|
|
174
205
|
)
|
|
175
206
|
|
|
176
207
|
llm = trace.span(
|
|
@@ -194,7 +225,14 @@ module ActiveAgents
|
|
|
194
225
|
trace.add_span(tool_span)
|
|
195
226
|
end
|
|
196
227
|
|
|
197
|
-
reporter.report(trace)
|
|
228
|
+
accepted = agent[:synchronous] ? reporter.report(trace, sync: true) : reporter.report(trace)
|
|
229
|
+
notify_trace(agent[:on_trace], trace) if accepted
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def notify_trace(callback, trace)
|
|
233
|
+
callback&.call(trace)
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
warn "[#{SDK_NAME}] on_trace failed: #{e.class}"
|
|
198
236
|
end
|
|
199
237
|
|
|
200
238
|
def turn_expired?(turn)
|
|
@@ -215,6 +253,59 @@ module ActiveAgents
|
|
|
215
253
|
nil
|
|
216
254
|
end
|
|
217
255
|
|
|
256
|
+
# The prompt that opened the turn and the answer that closed it — the two
|
|
257
|
+
# ends a trace is otherwise missing. System instructions are included:
|
|
258
|
+
# they are the most common cause of a surprising answer.
|
|
259
|
+
def conversation_attributes(payload)
|
|
260
|
+
input = Array(payload[:input_messages])
|
|
261
|
+
new_messages = Array(payload[:messages_after])[input.size..] || []
|
|
262
|
+
|
|
263
|
+
attributes = {}
|
|
264
|
+
if (prompt = last_message_text(input, "user"))
|
|
265
|
+
attributes["llm.prompt"] = truncate_captured(prompt)
|
|
266
|
+
end
|
|
267
|
+
if (instructions = system_instructions(input))
|
|
268
|
+
attributes["llm.instructions"] = truncate_captured(instructions)
|
|
269
|
+
end
|
|
270
|
+
if (completion = last_message_text(new_messages, "assistant"))
|
|
271
|
+
attributes["llm.completion"] = truncate_captured(completion)
|
|
272
|
+
end
|
|
273
|
+
attributes
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# RubyLLM's `with_instructions` appends by default, so a chat can carry
|
|
277
|
+
# several system messages and the model sees all of them. Join rather
|
|
278
|
+
# than taking the last, or an app that layers a base prompt with a
|
|
279
|
+
# per-request one would report only the fragment.
|
|
280
|
+
def system_instructions(messages)
|
|
281
|
+
texts = messages.select do |candidate|
|
|
282
|
+
candidate.respond_to?(:role) && candidate.role.to_s == "system" &&
|
|
283
|
+
candidate.respond_to?(:content) && !candidate.content.to_s.empty?
|
|
284
|
+
end.map { |message| message.content.to_s }
|
|
285
|
+
|
|
286
|
+
texts.empty? ? nil : texts.join("\n\n")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def last_message_text(messages, role)
|
|
290
|
+
message = messages.reverse.find do |candidate|
|
|
291
|
+
candidate.respond_to?(:role) && candidate.role.to_s == role &&
|
|
292
|
+
candidate.respond_to?(:content) && !candidate.content.to_s.empty?
|
|
293
|
+
end
|
|
294
|
+
text = message&.content.to_s
|
|
295
|
+
text.empty? ? nil : text
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def tool_io_json(value)
|
|
299
|
+
json = value.is_a?(String) ? value : JSON.generate(value)
|
|
300
|
+
truncate_captured(json)
|
|
301
|
+
rescue StandardError
|
|
302
|
+
value.inspect[0, CONTENT_LIMIT]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def truncate_captured(text)
|
|
306
|
+
text.to_s[0, CONTENT_LIMIT]
|
|
307
|
+
end
|
|
308
|
+
|
|
218
309
|
def token_totals(payload)
|
|
219
310
|
initial_count = Array(payload[:input_messages]).size
|
|
220
311
|
new_messages = Array(payload[:messages_after])[initial_count..] || []
|
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.
|
|
4
|
+
version: 0.3.0
|
|
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-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeagents-telemetry
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
19
|
+
version: '0.3'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.
|
|
26
|
+
version: '0.3'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: activesupport
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|