activeagents-telemetry-ruby_llm 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74f798054c3b023ac181d541e9ce36d907733511e4abfd1a43d1565cbff09c3e
4
- data.tar.gz: c651f96ca0fafc447d5e49090da2f84907fb4a04d33256ad1b87fee1d0e6d0d7
3
+ metadata.gz: cf9f1bb148ecabe70a08facf887f60c55dd08f6683370bcb03cb33d1abad0dd1
4
+ data.tar.gz: 81d06e3973b08667dbb972fe1a013d1be3785b9d9c2b3804a4dfbe5ae67fbaa5
5
5
  SHA512:
6
- metadata.gz: 066163ce9683532ee6f2f3220592ede5a85c8a58208bdac182e169c32ccbe5a8c00682e1ac79c936da353399d6aeb024d132253b329d6dcf6e7b2227a0421577
7
- data.tar.gz: d1ee5d365abac91ad386ff734095b21aa1b77ef1b0722c4a9d37e2742ab91cc9c9b5898db709f82cda59e7fd1663e224c19fa1691d34c095e6987f4e3d6783b3
6
+ metadata.gz: 999b5249ec348dc4b5280aeddfb2c905af13b29948a85aed5b81ddfdfd2f920fd5d0607b0fd79313f5fad732de271e0db2d1ab23b7ce04e575bf93f90c967040
7
+ data.tar.gz: 4ec2de8a1cb63605724e7497632b7463ee3e317c133d3bc05cc8131aa8ea1e9b379fdb0da172bf509809bb2ac193c2164bc3780b8844ed32c4c7225220475fb7
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
- Tool arguments and results are never sent; error messages are truncated.
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
 
@@ -3,7 +3,7 @@
3
3
  module ActiveAgents
4
4
  module Telemetry
5
5
  module RubyLLM
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
9
9
  end
@@ -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
- # Tool arguments and results are never sent; error messages are truncated.
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,6 +38,8 @@ 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
 
@@ -139,11 +143,16 @@ module ActiveAgents
139
143
 
140
144
  def build_tool_span(payload, started_at, finished_at)
141
145
  error = payload[:exception_object]
146
+ attributes = { "tool.name" => payload[:tool_name].to_s, "tool.call_id" => payload[:tool_call_id].to_s }
147
+ if configuration.capture_bodies?
148
+ attributes["tool.arguments"] = tool_io_json(payload[:tool_arguments])
149
+ attributes["tool.result"] = tool_io_json(payload[:result_content]) unless error
150
+ end
142
151
  span = Span.new(
143
152
  "tool.#{payload[:tool_name]}",
144
153
  type: "tool",
145
154
  start_time: started_at,
146
- attributes: { "tool.name" => payload[:tool_name].to_s, "tool.call_id" => payload[:tool_call_id].to_s }
155
+ attributes: attributes
147
156
  )
148
157
  span.record_error(error, message_limit: configuration.error_message_limit) if error
149
158
  span.finish(at: finished_at)
@@ -163,14 +172,17 @@ module ActiveAgents
163
172
  resource_attributes: configuration.resource_attributes
164
173
  )
165
174
 
175
+ root_attributes = {
176
+ "agent.class" => agent[:name],
177
+ "agent.action" => agent[:action],
178
+ "agent.provider" => payload[:provider].to_s,
179
+ "agent.model" => payload[:model].to_s
180
+ }
181
+ root_attributes.merge!(conversation_attributes(payload)) if configuration.capture_bodies?
182
+
166
183
  root = trace.span(
167
184
  "#{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
- }
185
+ attributes: root_attributes
174
186
  )
175
187
 
176
188
  llm = trace.span(
@@ -215,6 +227,59 @@ module ActiveAgents
215
227
  nil
216
228
  end
217
229
 
230
+ # The prompt that opened the turn and the answer that closed it — the two
231
+ # ends a trace is otherwise missing. System instructions are included:
232
+ # they are the most common cause of a surprising answer.
233
+ def conversation_attributes(payload)
234
+ input = Array(payload[:input_messages])
235
+ new_messages = Array(payload[:messages_after])[input.size..] || []
236
+
237
+ attributes = {}
238
+ if (prompt = last_message_text(input, "user"))
239
+ attributes["llm.prompt"] = truncate_captured(prompt)
240
+ end
241
+ if (instructions = system_instructions(input))
242
+ attributes["llm.instructions"] = truncate_captured(instructions)
243
+ end
244
+ if (completion = last_message_text(new_messages, "assistant"))
245
+ attributes["llm.completion"] = truncate_captured(completion)
246
+ end
247
+ attributes
248
+ end
249
+
250
+ # RubyLLM's `with_instructions` appends by default, so a chat can carry
251
+ # several system messages and the model sees all of them. Join rather
252
+ # than taking the last, or an app that layers a base prompt with a
253
+ # per-request one would report only the fragment.
254
+ def system_instructions(messages)
255
+ texts = messages.select do |candidate|
256
+ candidate.respond_to?(:role) && candidate.role.to_s == "system" &&
257
+ candidate.respond_to?(:content) && !candidate.content.to_s.empty?
258
+ end.map { |message| message.content.to_s }
259
+
260
+ texts.empty? ? nil : texts.join("\n\n")
261
+ end
262
+
263
+ def last_message_text(messages, role)
264
+ message = messages.reverse.find do |candidate|
265
+ candidate.respond_to?(:role) && candidate.role.to_s == role &&
266
+ candidate.respond_to?(:content) && !candidate.content.to_s.empty?
267
+ end
268
+ text = message&.content.to_s
269
+ text.empty? ? nil : text
270
+ end
271
+
272
+ def tool_io_json(value)
273
+ json = value.is_a?(String) ? value : JSON.generate(value)
274
+ truncate_captured(json)
275
+ rescue StandardError
276
+ value.inspect[0, CONTENT_LIMIT]
277
+ end
278
+
279
+ def truncate_captured(text)
280
+ text.to_s[0, CONTENT_LIMIT]
281
+ end
282
+
218
283
  def token_totals(payload)
219
284
  initial_count = Array(payload[:input_messages]).size
220
285
  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.1.0
4
+ version: 0.2.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-08-10 00:00:00.000000000 Z
11
+ date: 2026-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeagents-telemetry