activeagent 1.0.3 → 1.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 +4 -4
- data/CHANGELOG.md +132 -0
- data/README.md +39 -3
- data/lib/active_agent/base.rb +4 -0
- 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 +115 -171
- data/lib/active_agent/telemetry/instrumentation.rb +141 -14
- data/lib/active_agent/telemetry/reporter.rb +12 -165
- data/lib/active_agent/telemetry/span.rb +18 -245
- data/lib/active_agent/telemetry/tool_origin.rb +90 -0
- data/lib/active_agent/telemetry/tracer.rb +67 -70
- data/lib/active_agent/telemetry.rb +2 -2
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +1 -1
- metadata +38 -42
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -117
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -54
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -126
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -103
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -77
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -198
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -105
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -143
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
- data/lib/active_agent/dashboard/config/routes.rb +0 -78
- data/lib/active_agent/dashboard/engine.rb +0 -39
- data/lib/active_agent/dashboard.rb +0 -151
- 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
- data/lib/generators/active_agent/dashboard/install_generator.rb +0 -64
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -30
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -30
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Classifies where a tool call came from, so dashboards can group tool
|
|
6
|
+
# traffic by the service that serves it instead of showing a flat list
|
|
7
|
+
# of names.
|
|
8
|
+
#
|
|
9
|
+
# Telemetry only ever sees the name a provider used to invoke the tool,
|
|
10
|
+
# so the origin has to be recovered from naming conventions. The MCP
|
|
11
|
+
# ecosystem settled on a namespaced form — +mcp__<server>__<tool>+ —
|
|
12
|
+
# which most clients (Claude Code, Cursor, the Ruby MCP clients) emit
|
|
13
|
+
# verbatim, and that is the strongest signal available. Everything else
|
|
14
|
+
# falls back to "the agent class defines this method".
|
|
15
|
+
#
|
|
16
|
+
# Classification happens at instrumentation time rather than at read
|
|
17
|
+
# time so the attribution is recorded in the span itself: a trace stays
|
|
18
|
+
# self-describing, and consumers (the gem dashboard, activeagents.ai,
|
|
19
|
+
# any OTLP exporter) all read the same fields instead of each
|
|
20
|
+
# re-implementing the guess.
|
|
21
|
+
#
|
|
22
|
+
# @example Namespaced MCP tool
|
|
23
|
+
# ToolOrigin.classify("mcp__playwright__browser_navigate")
|
|
24
|
+
# # => { origin: "mcp", server: "playwright", tool: "browser_navigate" }
|
|
25
|
+
#
|
|
26
|
+
# @example Agent-defined method
|
|
27
|
+
# ToolOrigin.classify("lookup_order")
|
|
28
|
+
# # => { origin: "agent", server: nil, tool: "lookup_order" }
|
|
29
|
+
module ToolOrigin
|
|
30
|
+
# +mcp__<server>__<tool>+. The tool half may itself contain "__", so
|
|
31
|
+
# only the first two segments are structural.
|
|
32
|
+
MCP_PATTERN = /\Amcp__([^_]+(?:_[^_]+)*?)__(.+)\z/
|
|
33
|
+
|
|
34
|
+
# Origin values written to +tool.origin+.
|
|
35
|
+
MCP = "mcp"
|
|
36
|
+
AGENT = "agent"
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# Classifies a tool name.
|
|
41
|
+
#
|
|
42
|
+
# @param name [String, Symbol] the tool name as the provider invoked it
|
|
43
|
+
# @return [Hash] +:origin+ ("mcp" or "agent"), +:server+ (MCP server
|
|
44
|
+
# name or nil), and +:tool+ (the bare tool name with any namespace
|
|
45
|
+
# stripped)
|
|
46
|
+
def classify(name)
|
|
47
|
+
name = name.to_s
|
|
48
|
+
|
|
49
|
+
if (match = MCP_PATTERN.match(name))
|
|
50
|
+
{ origin: MCP, server: match[1], tool: match[2] }
|
|
51
|
+
else
|
|
52
|
+
{ origin: AGENT, server: nil, tool: name }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether a tool name is namespaced to an MCP server.
|
|
57
|
+
#
|
|
58
|
+
# @param name [String, Symbol]
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def mcp?(name)
|
|
61
|
+
MCP_PATTERN.match?(name.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The MCP server a tool belongs to, if any.
|
|
65
|
+
#
|
|
66
|
+
# @param name [String, Symbol]
|
|
67
|
+
# @return [String, nil]
|
|
68
|
+
def server_for(name)
|
|
69
|
+
classify(name)[:server]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Writes the classification onto a span. Only sets +tool.mcp_server+
|
|
73
|
+
# when there is a server to name, so agent-defined tools don't carry
|
|
74
|
+
# an empty attribute.
|
|
75
|
+
#
|
|
76
|
+
# @param span [ActiveAgent::Telemetry::Span]
|
|
77
|
+
# @param name [String, Symbol] the tool name
|
|
78
|
+
# @return [Hash] the classification, for callers that also want it
|
|
79
|
+
def annotate(span, name)
|
|
80
|
+
classification = classify(name)
|
|
81
|
+
span.set_attribute("tool.origin", classification[:origin])
|
|
82
|
+
if classification[:server]
|
|
83
|
+
span.set_attribute("tool.mcp_server", classification[:server])
|
|
84
|
+
span.set_attribute("tool.base_name", classification[:tool])
|
|
85
|
+
end
|
|
86
|
+
classification
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|