activeagent 1.0.3 → 1.1.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/CHANGELOG.md +71 -0
- data/README.md +26 -0
- data/lib/active_agent/base.rb +4 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +27 -6
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +11 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +15 -12
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +27 -7
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +9 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +18 -2
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +15 -3
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +3 -1
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +4 -4
- data/lib/active_agent/dashboard/config/routes.rb +5 -64
- data/lib/active_agent/dashboard/engine.rb +19 -15
- data/lib/active_agent/dashboard.rb +13 -3
- data/lib/active_agent/model_capabilities.rb +89 -0
- data/lib/active_agent/providers/_base_provider.rb +23 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
- data/lib/active_agent/providers/errors.rb +140 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
- data/lib/active_agent/railtie.rb +5 -0
- data/lib/active_agent/telemetry/configuration.rb +112 -172
- data/lib/active_agent/telemetry/instrumentation.rb +137 -14
- data/lib/active_agent/telemetry/reporter.rb +12 -165
- data/lib/active_agent/telemetry/span.rb +18 -245
- data/lib/active_agent/telemetry/tracer.rb +67 -70
- data/lib/active_agent/telemetry.rb +1 -2
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +5 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +30 -2
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +41 -4
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +20 -4
- metadata +17 -11
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
|
@@ -23,11 +23,6 @@ module ActiveAgent
|
|
|
23
23
|
module Instrumentation
|
|
24
24
|
extend ActiveSupport::Concern
|
|
25
25
|
|
|
26
|
-
included do
|
|
27
|
-
# Hook into generation lifecycle
|
|
28
|
-
around_generate :trace_generation if respond_to?(:around_generate)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
26
|
class_methods do
|
|
32
27
|
# Installs instrumentation on the agent class.
|
|
33
28
|
#
|
|
@@ -46,21 +41,82 @@ module ActiveAgent
|
|
|
46
41
|
def process_prompt
|
|
47
42
|
return super unless Telemetry.enabled?
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
# Reuse (or mint) the generation's trace id so the telemetry trace
|
|
45
|
+
# shares one id with everything else that reads
|
|
46
|
+
# prompt_options[:trace_id] — the generation request parameters and
|
|
47
|
+
# e.g. solid_agent's persisted generation records. Without this the
|
|
48
|
+
# tracer generates its own id and traces can't be correlated with
|
|
49
|
+
# the records they describe.
|
|
50
|
+
trace_id = nil
|
|
51
|
+
if respond_to?(:prompt_options) && prompt_options.is_a?(Hash)
|
|
52
|
+
trace_id = (prompt_options[:trace_id] ||= SecureRandom.uuid)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Telemetry.trace("#{self.class.name}.#{action_name}", span_type: :root, **{ trace_id: trace_id }.compact) do |span|
|
|
50
56
|
span.set_attribute("agent.class", self.class.name)
|
|
51
57
|
span.set_attribute("agent.action", action_name.to_s)
|
|
52
|
-
span.set_attribute("agent.provider", provider_name)
|
|
53
|
-
span.set_attribute("agent.model", model_name)
|
|
58
|
+
span.set_attribute("agent.provider", provider_name)
|
|
59
|
+
span.set_attribute("agent.model", model_name)
|
|
54
60
|
|
|
55
|
-
# Add prompt span
|
|
61
|
+
# Add prompt span, carrying the prompt contents (instructions +
|
|
62
|
+
# outbound messages) so dashboards can show what was sent.
|
|
56
63
|
prompt_span = span.add_span("agent.prompt", span_type: :prompt)
|
|
57
|
-
|
|
64
|
+
if (message_stack = prompt_options[:messages]).respond_to?(:size)
|
|
65
|
+
prompt_span.set_attribute("messages.count", message_stack.size)
|
|
66
|
+
end
|
|
67
|
+
# Attribute order is display order in dashboards: the system
|
|
68
|
+
# message first, then the tool roster, then the (often long)
|
|
69
|
+
# message history.
|
|
70
|
+
if prompt_options.is_a?(Hash)
|
|
71
|
+
rendered_instructions = begin
|
|
72
|
+
prompt_view_instructions(prompt_options[:instructions]) if respond_to?(:prompt_view_instructions)
|
|
73
|
+
rescue StandardError
|
|
74
|
+
prompt_options[:instructions].is_a?(String) ? prompt_options[:instructions] : nil
|
|
75
|
+
end
|
|
76
|
+
if rendered_instructions.present?
|
|
77
|
+
prompt_span.set_attribute("prompt.input.instructions", telemetry_truncate(Array(rendered_instructions).join("\n\n")))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if (tools = prompt_options[:tools]).present?
|
|
81
|
+
roster = Array(tools).filter_map { |tool|
|
|
82
|
+
next unless tool.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
name = tool[:name] || tool["name"]
|
|
85
|
+
description = tool[:description] || tool["description"]
|
|
86
|
+
parameters = tool[:parameters] || tool["parameters"] || tool[:input_schema] || tool["input_schema"]
|
|
87
|
+
properties = parameters.is_a?(Hash) ? (parameters[:properties] || parameters["properties"]) : nil
|
|
88
|
+
{
|
|
89
|
+
name: name,
|
|
90
|
+
description: telemetry_truncate(description),
|
|
91
|
+
parameters: properties.is_a?(Hash) ? properties.keys : []
|
|
92
|
+
}.compact
|
|
93
|
+
}
|
|
94
|
+
prompt_span.set_attribute("prompt.input.tools", JSON.generate(roster)) if roster.any?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if (outbound = prompt_options[:messages]).present?
|
|
98
|
+
serialized = Array(outbound).map { |message|
|
|
99
|
+
if message.is_a?(Hash)
|
|
100
|
+
role = message[:role] || message["role"] || "user"
|
|
101
|
+
content = message[:content] || message["content"]
|
|
102
|
+
{ role: role.to_s, content: telemetry_truncate(content) }
|
|
103
|
+
else
|
|
104
|
+
{ role: "user", content: telemetry_truncate(message) }
|
|
105
|
+
end
|
|
106
|
+
}
|
|
107
|
+
prompt_span.set_attribute("prompt.input.messages", JSON.generate(serialized))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
58
110
|
prompt_span.finish
|
|
59
111
|
|
|
60
112
|
# Execute generation with LLM span
|
|
61
113
|
llm_span = span.add_span("llm.generate", span_type: :llm)
|
|
62
|
-
llm_span.set_attribute("llm.provider", provider_name)
|
|
63
|
-
llm_span.set_attribute("llm.model", model_name)
|
|
114
|
+
llm_span.set_attribute("llm.provider", provider_name)
|
|
115
|
+
llm_span.set_attribute("llm.model", model_name)
|
|
116
|
+
|
|
117
|
+
# Providers run tools through tools_function mid-generation; the
|
|
118
|
+
# wrapped proc (see below) hangs timed tool spans off this span.
|
|
119
|
+
@_telemetry_llm_span = llm_span
|
|
64
120
|
|
|
65
121
|
begin
|
|
66
122
|
result = super
|
|
@@ -95,6 +151,23 @@ module ActiveAgent
|
|
|
95
151
|
end
|
|
96
152
|
end
|
|
97
153
|
|
|
154
|
+
# The span was tagged with the configured model (often unset →
|
|
155
|
+
# "unknown"); the provider's raw response knows what actually
|
|
156
|
+
# served the request.
|
|
157
|
+
if result.respond_to?(:raw_response) && result.raw_response.is_a?(Hash)
|
|
158
|
+
served_model = result.raw_response["model"] || result.raw_response[:model]
|
|
159
|
+
llm_span.set_attribute("llm.model", served_model.to_s) if served_model.present?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Carry the generation contents so dashboards can show what
|
|
163
|
+
# came back, not just how many tokens it cost.
|
|
164
|
+
if result.respond_to?(:message) && result.message.respond_to?(:content) && result.message.content.present?
|
|
165
|
+
llm_span.set_attribute("llm.output.message", telemetry_truncate(result.message.content))
|
|
166
|
+
end
|
|
167
|
+
if result.respond_to?(:finish_reason) && result.finish_reason.present?
|
|
168
|
+
llm_span.set_attribute("llm.finish_reason", result.finish_reason.to_s)
|
|
169
|
+
end
|
|
170
|
+
|
|
98
171
|
llm_span.set_status(:ok)
|
|
99
172
|
llm_span.finish
|
|
100
173
|
span.set_status(:ok)
|
|
@@ -105,6 +178,44 @@ module ActiveAgent
|
|
|
105
178
|
llm_span.finish
|
|
106
179
|
span.record_error(e)
|
|
107
180
|
raise
|
|
181
|
+
ensure
|
|
182
|
+
@_telemetry_llm_span = nil
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Providers invoke this proc for every tool call during generation.
|
|
188
|
+
# Wrapping it is what makes tool telemetry real: each call gets a
|
|
189
|
+
# timed span with its arguments and result — the post-hoc
|
|
190
|
+
# result.tool_calls path below never fires on 1.x responses, which
|
|
191
|
+
# don't expose tool calls.
|
|
192
|
+
def tools_function
|
|
193
|
+
base = super
|
|
194
|
+
return base unless Telemetry.enabled?
|
|
195
|
+
|
|
196
|
+
agent = self
|
|
197
|
+
proc do |tool_name, *args, **kwargs|
|
|
198
|
+
parent = agent.instance_variable_get(:@_telemetry_llm_span)
|
|
199
|
+
unless parent
|
|
200
|
+
next base.call(tool_name, *args, **kwargs)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
tool_span = parent.add_span("tool.#{tool_name}", span_type: :tool)
|
|
204
|
+
tool_span.set_attribute("tool.name", tool_name.to_s)
|
|
205
|
+
arguments = kwargs.presence || (args.length == 1 ? args.first : args.presence)
|
|
206
|
+
if arguments.present?
|
|
207
|
+
tool_span.set_attribute("tool.input.args", agent.send(:telemetry_truncate, JSON.generate(arguments)))
|
|
208
|
+
end
|
|
209
|
+
begin
|
|
210
|
+
result = base.call(tool_name, *args, **kwargs)
|
|
211
|
+
tool_span.set_attribute("tool.output.result", agent.send(:telemetry_truncate, result))
|
|
212
|
+
tool_span.set_status(:ok)
|
|
213
|
+
result
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
tool_span.record_error(e)
|
|
216
|
+
raise
|
|
217
|
+
ensure
|
|
218
|
+
tool_span.finish
|
|
108
219
|
end
|
|
109
220
|
end
|
|
110
221
|
end
|
|
@@ -116,7 +227,7 @@ module ActiveAgent
|
|
|
116
227
|
Telemetry.trace("#{self.class.name}.embed", span_type: :embedding) do |span|
|
|
117
228
|
span.set_attribute("agent.class", self.class.name)
|
|
118
229
|
span.set_attribute("agent.action", "embed")
|
|
119
|
-
span.set_attribute("agent.provider", provider_name)
|
|
230
|
+
span.set_attribute("agent.provider", provider_name)
|
|
120
231
|
|
|
121
232
|
begin
|
|
122
233
|
result = super
|
|
@@ -138,8 +249,20 @@ module ActiveAgent
|
|
|
138
249
|
|
|
139
250
|
private
|
|
140
251
|
|
|
252
|
+
# Content attributes are capped so a large prompt (e.g. a 100k-token
|
|
253
|
+
# tool loop) can't bloat the trace payload.
|
|
254
|
+
TELEMETRY_ATTRIBUTE_MAX_CHARS = 4_000
|
|
255
|
+
|
|
256
|
+
def telemetry_truncate(value)
|
|
257
|
+
text = value.to_s
|
|
258
|
+
return text if text.length <= TELEMETRY_ATTRIBUTE_MAX_CHARS
|
|
259
|
+
|
|
260
|
+
"#{text[0, TELEMETRY_ATTRIBUTE_MAX_CHARS]}… (truncated, #{text.length} chars total)"
|
|
261
|
+
end
|
|
262
|
+
|
|
141
263
|
def provider_name
|
|
142
|
-
|
|
264
|
+
klass = prompt_provider_klass
|
|
265
|
+
klass.respond_to?(:tag_name) ? klass.tag_name : "unknown"
|
|
143
266
|
rescue StandardError
|
|
144
267
|
"unknown"
|
|
145
268
|
end
|
|
@@ -1,175 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "json"
|
|
5
|
-
require "uri"
|
|
6
|
-
|
|
7
3
|
module ActiveAgent
|
|
8
4
|
module Telemetry
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
5
|
+
# The framework's reporter, now provided by the activeagents-telemetry
|
|
6
|
+
# gem's BatchingReporter: buffered delivery on batch_size/flush_interval,
|
|
7
|
+
# a blocking flush, and a shutdown that waits out in-flight sends so
|
|
8
|
+
# traces reported near process exit are delivered rather than dropped.
|
|
13
9
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# reporter.report(trace_payload)
|
|
17
|
-
# reporter.flush # Send immediately
|
|
18
|
-
# reporter.shutdown # Clean shutdown
|
|
10
|
+
# Local storage is handled by Configuration#local_store, which routes
|
|
11
|
+
# traces through the dashboard's trace model instead of HTTP.
|
|
19
12
|
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
attr_reader :configuration
|
|
23
|
-
|
|
13
|
+
# @see ActiveAgents::Telemetry::BatchingReporter
|
|
14
|
+
class Reporter < ActiveAgents::Telemetry::BatchingReporter
|
|
24
15
|
def initialize(configuration)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@thread = nil
|
|
30
|
-
@shutdown = false
|
|
31
|
-
|
|
32
|
-
start_flush_thread if configuration.enabled?
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Adds a trace to the buffer for transmission.
|
|
36
|
-
#
|
|
37
|
-
# @param trace [Hash] Trace payload
|
|
38
|
-
# @return [void]
|
|
39
|
-
def report(trace)
|
|
40
|
-
return unless configuration.enabled?
|
|
41
|
-
|
|
42
|
-
@mutex.synchronize do
|
|
43
|
-
@buffer << trace
|
|
44
|
-
|
|
45
|
-
# Flush immediately if buffer is full
|
|
46
|
-
if @buffer.size >= configuration.batch_size
|
|
47
|
-
flush_buffer
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Flushes all buffered traces immediately.
|
|
53
|
-
#
|
|
54
|
-
# @return [void]
|
|
55
|
-
def flush
|
|
56
|
-
@mutex.synchronize do
|
|
57
|
-
flush_buffer
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Shuts down the reporter, flushing remaining traces.
|
|
62
|
-
#
|
|
63
|
-
# @return [void]
|
|
64
|
-
def shutdown
|
|
65
|
-
@shutdown = true
|
|
66
|
-
flush
|
|
67
|
-
@thread&.join(5) # Wait up to 5 seconds for thread to finish
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
# Starts the background flush thread.
|
|
73
|
-
def start_flush_thread
|
|
74
|
-
@running = true
|
|
75
|
-
@thread = Thread.new do
|
|
76
|
-
Thread.current.name = "activeagent-telemetry-reporter"
|
|
77
|
-
|
|
78
|
-
while @running && !@shutdown
|
|
79
|
-
sleep(configuration.flush_interval)
|
|
80
|
-
|
|
81
|
-
@mutex.synchronize do
|
|
82
|
-
flush_buffer if @buffer.any?
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Flushes the buffer by sending traces to the endpoint.
|
|
89
|
-
#
|
|
90
|
-
# Must be called within @mutex synchronization.
|
|
91
|
-
def flush_buffer
|
|
92
|
-
return if @buffer.empty?
|
|
93
|
-
|
|
94
|
-
traces = @buffer.dup
|
|
95
|
-
@buffer.clear
|
|
96
|
-
|
|
97
|
-
Thread.new { send_traces(traces) }
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Sends traces to the configured endpoint.
|
|
101
|
-
#
|
|
102
|
-
# @param traces [Array<Hash>] Traces to send
|
|
103
|
-
def send_traces(traces)
|
|
104
|
-
# Use direct database storage for local mode
|
|
105
|
-
if configuration.local_storage?
|
|
106
|
-
store_traces_locally(traces)
|
|
107
|
-
return
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
uri = URI.parse(configuration.endpoint)
|
|
111
|
-
|
|
112
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
113
|
-
http.use_ssl = uri.scheme == "https"
|
|
114
|
-
http.open_timeout = configuration.timeout
|
|
115
|
-
http.read_timeout = configuration.timeout
|
|
116
|
-
|
|
117
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
118
|
-
request["Content-Type"] = "application/json"
|
|
119
|
-
request["Authorization"] = "Bearer #{configuration.api_key}"
|
|
120
|
-
request["User-Agent"] = "ActiveAgent/#{ActiveAgent::VERSION} Ruby/#{RUBY_VERSION}"
|
|
121
|
-
request["X-Service-Name"] = configuration.resolved_service_name
|
|
122
|
-
request["X-Environment"] = configuration.environment
|
|
123
|
-
|
|
124
|
-
payload = {
|
|
125
|
-
traces: traces,
|
|
126
|
-
sdk: {
|
|
127
|
-
name: "activeagent",
|
|
128
|
-
version: ActiveAgent::VERSION,
|
|
129
|
-
language: "ruby",
|
|
130
|
-
runtime_version: RUBY_VERSION
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
request.body = JSON.generate(payload)
|
|
135
|
-
|
|
136
|
-
response = http.request(request)
|
|
137
|
-
|
|
138
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
139
|
-
log_error("Failed to send traces: #{response.code} #{response.message}")
|
|
140
|
-
end
|
|
141
|
-
rescue StandardError => e
|
|
142
|
-
log_error("Error sending traces: #{e.class} - #{e.message}")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Stores traces directly in the local database.
|
|
146
|
-
#
|
|
147
|
-
# @param traces [Array<Hash>] Traces to store
|
|
148
|
-
def store_traces_locally(traces)
|
|
149
|
-
sdk_info = {
|
|
150
|
-
name: "activeagent",
|
|
151
|
-
version: ActiveAgent::VERSION,
|
|
152
|
-
language: "ruby",
|
|
153
|
-
runtime_version: RUBY_VERSION
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
traces.each do |trace|
|
|
157
|
-
# Skip if trace already exists (idempotency)
|
|
158
|
-
next if ActiveAgent::TelemetryTrace.exists?(trace_id: trace["trace_id"])
|
|
159
|
-
|
|
160
|
-
ActiveAgent::TelemetryTrace.create_from_payload(trace, sdk_info)
|
|
161
|
-
rescue StandardError => e
|
|
162
|
-
log_error("Failed to store trace locally: #{e.class} - #{e.message}")
|
|
163
|
-
end
|
|
164
|
-
rescue StandardError => e
|
|
165
|
-
log_error("Error storing traces locally: #{e.class} - #{e.message}")
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Logs an error message.
|
|
169
|
-
#
|
|
170
|
-
# @param message [String] Error message
|
|
171
|
-
def log_error(message)
|
|
172
|
-
configuration.resolved_logger.error("[ActiveAgent::Telemetry] #{message}")
|
|
16
|
+
# sample: false — the Tracer applies head-based sampling at trace
|
|
17
|
+
# creation, before any span is built; sampling again here would
|
|
18
|
+
# compound the rate to rate².
|
|
19
|
+
super(configuration, sdk_name: "activeagent", sdk_version: ActiveAgent::VERSION, sample: false)
|
|
173
20
|
end
|
|
174
21
|
end
|
|
175
22
|
end
|
|
@@ -2,103 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAgent
|
|
4
4
|
module Telemetry
|
|
5
|
-
#
|
|
5
|
+
# The framework's span, now provided by the activeagents-telemetry gem.
|
|
6
|
+
# This subclass translates the framework's historical constructor
|
|
7
|
+
# (span_type: keyword, attributes as a keyword splat) onto the shared
|
|
8
|
+
# class; everything else — set_attribute, set_tokens, set_status,
|
|
9
|
+
# record_error, add_span, measure, finish — is inherited.
|
|
6
10
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# and can have child spans.
|
|
11
|
+
# One deliberate change rides along from the shared core: record_error
|
|
12
|
+
# truncates the message and no longer puts a backtrace on the wire.
|
|
10
13
|
#
|
|
11
|
-
# @
|
|
12
|
-
|
|
13
|
-
# span.set_attribute("provider", "anthropic")
|
|
14
|
-
# span.set_attribute("model", "claude-3-5-sonnet")
|
|
15
|
-
# span.set_tokens(input: 100, output: 50)
|
|
16
|
-
# span.finish
|
|
17
|
-
#
|
|
18
|
-
class Span
|
|
19
|
-
# Span types for categorization
|
|
20
|
-
TYPES = {
|
|
21
|
-
root: "root", # Root span for entire generation
|
|
22
|
-
prompt: "prompt", # Prompt preparation/rendering
|
|
23
|
-
llm: "llm", # LLM API call
|
|
24
|
-
tool: "tool", # Tool invocation
|
|
25
|
-
thinking: "thinking", # Extended thinking (Anthropic)
|
|
26
|
-
embedding: "embedding", # Embedding generation
|
|
27
|
-
error: "error" # Error handling
|
|
28
|
-
}.freeze
|
|
29
|
-
|
|
30
|
-
# Span status codes
|
|
31
|
-
STATUS = {
|
|
32
|
-
unset: "UNSET",
|
|
33
|
-
ok: "OK",
|
|
34
|
-
error: "ERROR"
|
|
35
|
-
}.freeze
|
|
36
|
-
|
|
37
|
-
# @return [String] Unique identifier for this span
|
|
38
|
-
attr_reader :span_id
|
|
39
|
-
|
|
40
|
-
# @return [String] Trace ID this span belongs to
|
|
41
|
-
attr_reader :trace_id
|
|
42
|
-
|
|
43
|
-
# @return [String, nil] Parent span ID
|
|
44
|
-
attr_reader :parent_span_id
|
|
45
|
-
|
|
46
|
-
# @return [String] Span name (e.g., "llm.generate", "tool.get_weather")
|
|
47
|
-
attr_reader :name
|
|
48
|
-
|
|
49
|
-
# @return [String] Span type from TYPES
|
|
50
|
-
attr_reader :span_type
|
|
51
|
-
|
|
52
|
-
# @return [Time] When the span started
|
|
53
|
-
attr_reader :start_time
|
|
54
|
-
|
|
55
|
-
# @return [Time, nil] When the span ended
|
|
56
|
-
attr_reader :end_time
|
|
57
|
-
|
|
58
|
-
# @return [Hash] Span attributes
|
|
59
|
-
attr_reader :attributes
|
|
60
|
-
|
|
61
|
-
# @return [Array<Span>] Child spans
|
|
62
|
-
attr_reader :children
|
|
63
|
-
|
|
64
|
-
# @return [String] Status code from STATUS
|
|
65
|
-
attr_reader :status
|
|
66
|
-
|
|
67
|
-
# @return [String, nil] Status message
|
|
68
|
-
attr_reader :status_message
|
|
69
|
-
|
|
70
|
-
# @return [Array<Hash>] Events recorded during the span
|
|
71
|
-
attr_reader :events
|
|
72
|
-
|
|
73
|
-
# Creates a new span.
|
|
74
|
-
#
|
|
75
|
-
# @param name [String] Span name
|
|
76
|
-
# @param trace_id [String] Parent trace ID
|
|
77
|
-
# @param parent_span_id [String, nil] Parent span ID
|
|
78
|
-
# @param span_type [Symbol] Type of span
|
|
79
|
-
# @param attributes [Hash] Initial attributes
|
|
14
|
+
# @see ActiveAgents::Telemetry::Span
|
|
15
|
+
class Span < ActiveAgents::Telemetry::Span
|
|
80
16
|
def initialize(name, trace_id:, parent_span_id: nil, span_type: :root, **attributes)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@attributes = attributes.transform_keys(&:to_s)
|
|
89
|
-
@children = []
|
|
90
|
-
@status = STATUS[:unset]
|
|
91
|
-
@status_message = nil
|
|
92
|
-
@events = []
|
|
93
|
-
@tokens = { input: 0, output: 0, thinking: 0, total: 0 }
|
|
17
|
+
super(
|
|
18
|
+
name,
|
|
19
|
+
type: span_type,
|
|
20
|
+
trace_id: trace_id,
|
|
21
|
+
parent_span_id: parent_span_id,
|
|
22
|
+
attributes: attributes
|
|
23
|
+
)
|
|
94
24
|
end
|
|
95
25
|
|
|
96
|
-
# Creates a child span.
|
|
97
|
-
#
|
|
98
|
-
# @param name [String] Child span name
|
|
99
|
-
# @param span_type [Symbol] Type of span
|
|
100
|
-
# @param attributes [Hash] Span attributes
|
|
101
|
-
# @return [Span] The child span
|
|
26
|
+
# Creates a child span, keeping the framework's keyword shape.
|
|
102
27
|
def add_span(name, span_type: :root, **attributes)
|
|
103
28
|
child = Span.new(
|
|
104
29
|
name,
|
|
@@ -107,161 +32,9 @@ module ActiveAgent
|
|
|
107
32
|
span_type: span_type,
|
|
108
33
|
**attributes
|
|
109
34
|
)
|
|
110
|
-
|
|
35
|
+
children << child
|
|
111
36
|
child
|
|
112
37
|
end
|
|
113
|
-
|
|
114
|
-
# Sets a single attribute.
|
|
115
|
-
#
|
|
116
|
-
# @param key [String, Symbol] Attribute key
|
|
117
|
-
# @param value [Object] Attribute value
|
|
118
|
-
# @return [self]
|
|
119
|
-
def set_attribute(key, value)
|
|
120
|
-
@attributes[key.to_s] = value
|
|
121
|
-
self
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Sets multiple attributes at once.
|
|
125
|
-
#
|
|
126
|
-
# @param attrs [Hash] Attributes to set
|
|
127
|
-
# @return [self]
|
|
128
|
-
def set_attributes(attrs)
|
|
129
|
-
attrs.each { |k, v| set_attribute(k, v) }
|
|
130
|
-
self
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Sets token usage for LLM spans.
|
|
134
|
-
#
|
|
135
|
-
# @param input [Integer] Input token count
|
|
136
|
-
# @param output [Integer] Output token count
|
|
137
|
-
# @param thinking [Integer] Thinking token count (Anthropic extended thinking)
|
|
138
|
-
# @return [self]
|
|
139
|
-
def set_tokens(input: 0, output: 0, thinking: 0)
|
|
140
|
-
@tokens = {
|
|
141
|
-
input: input,
|
|
142
|
-
output: output,
|
|
143
|
-
thinking: thinking,
|
|
144
|
-
total: input + output + thinking
|
|
145
|
-
}
|
|
146
|
-
set_attribute("tokens.input", input)
|
|
147
|
-
set_attribute("tokens.output", output)
|
|
148
|
-
set_attribute("tokens.thinking", thinking) if thinking > 0
|
|
149
|
-
set_attribute("tokens.total", @tokens[:total])
|
|
150
|
-
self
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Returns token usage.
|
|
154
|
-
#
|
|
155
|
-
# @return [Hash] Token counts
|
|
156
|
-
def tokens
|
|
157
|
-
@tokens.dup
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Sets the span status.
|
|
161
|
-
#
|
|
162
|
-
# @param code [Symbol] Status code (:ok, :error, :unset)
|
|
163
|
-
# @param message [String, nil] Optional status message
|
|
164
|
-
# @return [self]
|
|
165
|
-
def set_status(code, message = nil)
|
|
166
|
-
@status = STATUS[code] || STATUS[:unset]
|
|
167
|
-
@status_message = message
|
|
168
|
-
self
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Records an error on the span.
|
|
172
|
-
#
|
|
173
|
-
# @param error [Exception] The error to record
|
|
174
|
-
# @return [self]
|
|
175
|
-
def record_error(error)
|
|
176
|
-
set_status(:error, error.message)
|
|
177
|
-
set_attribute("error.type", error.class.name)
|
|
178
|
-
set_attribute("error.message", error.message)
|
|
179
|
-
set_attribute("error.backtrace", error.backtrace&.first(10)&.join("\n"))
|
|
180
|
-
|
|
181
|
-
add_event("exception", {
|
|
182
|
-
"exception.type" => error.class.name,
|
|
183
|
-
"exception.message" => error.message,
|
|
184
|
-
"exception.stacktrace" => error.backtrace&.join("\n")
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
self
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Adds an event to the span.
|
|
191
|
-
#
|
|
192
|
-
# @param name [String] Event name
|
|
193
|
-
# @param attributes [Hash] Event attributes
|
|
194
|
-
# @return [self]
|
|
195
|
-
def add_event(name, attributes = {})
|
|
196
|
-
@events << {
|
|
197
|
-
name: name,
|
|
198
|
-
timestamp: Time.current.iso8601(6),
|
|
199
|
-
attributes: attributes.transform_keys(&:to_s)
|
|
200
|
-
}
|
|
201
|
-
self
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Marks the span as finished.
|
|
205
|
-
#
|
|
206
|
-
# @return [self]
|
|
207
|
-
def finish
|
|
208
|
-
@end_time = Time.current
|
|
209
|
-
set_status(:ok) if @status == STATUS[:unset]
|
|
210
|
-
self
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Returns whether the span is finished.
|
|
214
|
-
#
|
|
215
|
-
# @return [Boolean]
|
|
216
|
-
def finished?
|
|
217
|
-
!@end_time.nil?
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Returns the duration in milliseconds.
|
|
221
|
-
#
|
|
222
|
-
# @return [Float, nil] Duration or nil if not finished
|
|
223
|
-
def duration_ms
|
|
224
|
-
return nil unless finished?
|
|
225
|
-
|
|
226
|
-
((@end_time - @start_time) * 1000).round(2)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Serializes the span for transmission.
|
|
230
|
-
#
|
|
231
|
-
# @return [Hash] Serialized span data
|
|
232
|
-
def to_h
|
|
233
|
-
{
|
|
234
|
-
span_id: span_id,
|
|
235
|
-
trace_id: trace_id,
|
|
236
|
-
parent_span_id: parent_span_id,
|
|
237
|
-
name: name,
|
|
238
|
-
type: span_type,
|
|
239
|
-
start_time: start_time.iso8601(6),
|
|
240
|
-
end_time: end_time&.iso8601(6),
|
|
241
|
-
duration_ms: duration_ms,
|
|
242
|
-
status: status,
|
|
243
|
-
status_message: status_message,
|
|
244
|
-
attributes: attributes,
|
|
245
|
-
tokens: tokens,
|
|
246
|
-
events: events,
|
|
247
|
-
children: children.map(&:to_h)
|
|
248
|
-
}
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Executes a block and records timing/errors.
|
|
252
|
-
#
|
|
253
|
-
# @yield Block to execute within the span
|
|
254
|
-
# @return [Object] Result of the block
|
|
255
|
-
def measure
|
|
256
|
-
result = yield
|
|
257
|
-
set_status(:ok)
|
|
258
|
-
result
|
|
259
|
-
rescue StandardError => e
|
|
260
|
-
record_error(e)
|
|
261
|
-
raise
|
|
262
|
-
ensure
|
|
263
|
-
finish
|
|
264
|
-
end
|
|
265
38
|
end
|
|
266
39
|
end
|
|
267
40
|
end
|