Module: ActiveAgent::Telemetry::Instrumentation::GenerationInstrumentation

Defined in:
lib/active_agent/telemetry/instrumentation.rb

Overview

Module prepended to intercept generation methods.

Instance Method Summary collapse

Instance Method Details

#process_embedObject

Wraps process_embed with telemetry tracing.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/active_agent/telemetry/instrumentation.rb', line 224

def process_embed
  return super unless Telemetry.enabled?

  Telemetry.trace("#{self.class.name}.embed", span_type: :embedding) do |span|
    span.set_attribute("agent.class", self.class.name)
    span.set_attribute("agent.action", "embed")
    span.set_attribute("agent.provider", provider_name)

    begin
      result = super

      if result.respond_to?(:usage) && result.usage.present?
        usage = result.usage
        input_tokens = (usage.input_tokens rescue 0) || 0
        span.set_tokens(input: input_tokens.to_i)
      end

      span.set_status(:ok)
      result
    rescue StandardError => e
      span.record_error(e)
      raise
    end
  end
end

#process_promptObject

Wraps process_prompt with telemetry tracing.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/active_agent/telemetry/instrumentation.rb', line 41

def process_prompt
  return super unless Telemetry.enabled?

  # Reuse (or mint) the generation's trace id so the telemetry trace
  # shares one id with everything else that reads
  # prompt_options[:trace_id] — the generation request parameters and
  # e.g. solid_agent's persisted generation records. Without this the
  # tracer generates its own id and traces can't be correlated with
  # the records they describe.
  trace_id = nil
  if respond_to?(:prompt_options) && prompt_options.is_a?(Hash)
    trace_id = (prompt_options[:trace_id] ||= SecureRandom.uuid)
  end

  Telemetry.trace("#{self.class.name}.#{action_name}", span_type: :root, **{ trace_id: trace_id }.compact) do |span|
    span.set_attribute("agent.class", self.class.name)
    span.set_attribute("agent.action", action_name.to_s)
    span.set_attribute("agent.provider", provider_name)
    span.set_attribute("agent.model", model_name)

    # Add prompt span, carrying the prompt contents (instructions +
    # outbound messages) so dashboards can show what was sent.
    prompt_span = span.add_span("agent.prompt", span_type: :prompt)
    if (message_stack = prompt_options[:messages]).respond_to?(:size)
      prompt_span.set_attribute("messages.count", message_stack.size)
    end
    # Attribute order is display order in dashboards: the system
    # message first, then the tool roster, then the (often long)
    # message history.
    if prompt_options.is_a?(Hash)
      rendered_instructions = begin
        prompt_view_instructions(prompt_options[:instructions]) if respond_to?(:prompt_view_instructions)
      rescue StandardError
        prompt_options[:instructions].is_a?(String) ? prompt_options[:instructions] : nil
      end
      if rendered_instructions.present?
        prompt_span.set_attribute("prompt.input.instructions", telemetry_truncate(Array(rendered_instructions).join("\n\n")))
      end

      if (tools = prompt_options[:tools]).present?
        roster = Array(tools).filter_map { |tool|
          next unless tool.is_a?(Hash)

          name = tool[:name] || tool["name"]
          description = tool[:description] || tool["description"]
          parameters = tool[:parameters] || tool["parameters"] || tool[:input_schema] || tool["input_schema"]
          properties = parameters.is_a?(Hash) ? (parameters[:properties] || parameters["properties"]) : nil
          {
            name: name,
            description: telemetry_truncate(description),
            parameters: properties.is_a?(Hash) ? properties.keys : []
          }.compact
        }
        prompt_span.set_attribute("prompt.input.tools", JSON.generate(roster)) if roster.any?
      end

      if (outbound = prompt_options[:messages]).present?
        serialized = Array(outbound).map { |message|
          if message.is_a?(Hash)
            role = message[:role] || message["role"] || "user"
            content = message[:content] || message["content"]
            { role: role.to_s, content: telemetry_truncate(content) }
          else
            { role: "user", content: telemetry_truncate(message) }
          end
        }
        prompt_span.set_attribute("prompt.input.messages", JSON.generate(serialized))
      end
    end
    prompt_span.finish

    # Execute generation with LLM span
    llm_span = span.add_span("llm.generate", span_type: :llm)
    llm_span.set_attribute("llm.provider", provider_name)
    llm_span.set_attribute("llm.model", model_name)

    # Providers run tools through tools_function mid-generation; the
    # wrapped proc (see below) hangs timed tool spans off this span.
    @_telemetry_llm_span = llm_span

    begin
      result = super

      # Record token usage from response
      if result.respond_to?(:usage) && result.usage.present?
        usage = result.usage
        # Usage model uses methods, not hash access
        input_tokens = (usage.input_tokens rescue 0) || 0
        output_tokens = (usage.output_tokens rescue 0) || 0
        reasoning_tokens = (usage.reasoning_tokens rescue 0) || 0

        llm_span.set_tokens(
          input: input_tokens.to_i,
          output: output_tokens.to_i,
          thinking: reasoning_tokens.to_i
        )
        span.set_tokens(
          input: input_tokens.to_i,
          output: output_tokens.to_i,
          thinking: reasoning_tokens.to_i
        )
      end

      # Record tool calls if present
      if result.respond_to?(:tool_calls) && result.tool_calls.present?
        result.tool_calls.each do |tool_call|
          tool_span = span.add_span("tool.#{tool_call[:name]}", span_type: :tool)
          tool_span.set_attribute("tool.name", tool_call[:name])
          tool_span.set_attribute("tool.id", tool_call[:id]) if tool_call[:id]
          tool_span.finish
        end
      end

      # The span was tagged with the configured model (often unset →
      # "unknown"); the provider's raw response knows what actually
      # served the request.
      if result.respond_to?(:raw_response) && result.raw_response.is_a?(Hash)
        served_model = result.raw_response["model"] || result.raw_response[:model]
        llm_span.set_attribute("llm.model", served_model.to_s) if served_model.present?
      end

      # Carry the generation contents so dashboards can show what
      # came back, not just how many tokens it cost.
      if result.respond_to?(:message) && result.message.respond_to?(:content) && result.message.content.present?
        llm_span.set_attribute("llm.output.message", telemetry_truncate(result.message.content))
      end
      if result.respond_to?(:finish_reason) && result.finish_reason.present?
        llm_span.set_attribute("llm.finish_reason", result.finish_reason.to_s)
      end

      llm_span.set_status(:ok)
      llm_span.finish
      span.set_status(:ok)

      result
    rescue StandardError => e
      llm_span.record_error(e)
      llm_span.finish
      span.record_error(e)
      raise
    ensure
      @_telemetry_llm_span = nil
    end
  end
end

#tools_functionObject

Providers invoke this proc for every tool call during generation. Wrapping it is what makes tool telemetry real: each call gets a timed span with its arguments and result — the post-hoc result.tool_calls path below never fires on 1.x responses, which don't expose tool calls.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/active_agent/telemetry/instrumentation.rb', line 192

def tools_function
  base = super
  return base unless Telemetry.enabled?

  agent = self
  proc do |tool_name, *args, **kwargs|
    parent = agent.instance_variable_get(:@_telemetry_llm_span)
    unless parent
      next base.call(tool_name, *args, **kwargs)
    end

    tool_span = parent.add_span("tool.#{tool_name}", span_type: :tool)
    tool_span.set_attribute("tool.name", tool_name.to_s)
    arguments = kwargs.presence || (args.length == 1 ? args.first : args.presence)
    if arguments.present?
      tool_span.set_attribute("tool.input.args", agent.send(:telemetry_truncate, JSON.generate(arguments)))
    end
    begin
      result = base.call(tool_name, *args, **kwargs)
      tool_span.set_attribute("tool.output.result", agent.send(:telemetry_truncate, result))
      tool_span.set_status(:ok)
      result
    rescue StandardError => e
      tool_span.record_error(e)
      raise
    ensure
      tool_span.finish
    end
  end
end