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
|
@@ -2,212 +2,156 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAgent
|
|
4
4
|
module Telemetry
|
|
5
|
-
#
|
|
5
|
+
# The framework's telemetry configuration, now provided by the
|
|
6
|
+
# activeagents-telemetry gem. This subclass keeps the framework's
|
|
7
|
+
# historical behavior on top of the shared core:
|
|
6
8
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
+
# * telemetry is opt-in (`enabled` defaults to false)
|
|
10
|
+
# * `local_storage: true` persists traces through the dashboard's trace
|
|
11
|
+
# model instead of HTTP
|
|
9
12
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# config.api_key = "your-api-key"
|
|
15
|
-
# config.sample_rate = 1.0
|
|
16
|
-
# end
|
|
13
|
+
# Every other option — endpoint, api_key, sample_rate, batch_size,
|
|
14
|
+
# flush_interval, capture_bodies, redact_attributes, service_name,
|
|
15
|
+
# environment, resource_attributes — lives on the shared class, so
|
|
16
|
+
# existing config/active_agent.yml files keep working unchanged.
|
|
17
17
|
#
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# sample_rate: 1.0
|
|
24
|
-
# batch_size: 100
|
|
25
|
-
# flush_interval: 5
|
|
26
|
-
#
|
|
27
|
-
class Configuration
|
|
28
|
-
# @return [Boolean] Whether telemetry is enabled (default: false)
|
|
29
|
-
attr_accessor :enabled
|
|
30
|
-
|
|
31
|
-
# @return [String] The endpoint URL for sending traces
|
|
32
|
-
attr_accessor :endpoint
|
|
33
|
-
|
|
34
|
-
# @return [String] API key for authentication
|
|
35
|
-
attr_accessor :api_key
|
|
36
|
-
|
|
37
|
-
# @return [Float] Sampling rate from 0.0 to 1.0 (default: 1.0)
|
|
38
|
-
attr_accessor :sample_rate
|
|
39
|
-
|
|
40
|
-
# @return [Integer] Number of traces to batch before sending (default: 100)
|
|
41
|
-
attr_accessor :batch_size
|
|
42
|
-
|
|
43
|
-
# @return [Integer] Seconds between automatic flushes (default: 5)
|
|
44
|
-
attr_accessor :flush_interval
|
|
45
|
-
|
|
46
|
-
# @return [Integer] HTTP timeout in seconds (default: 10)
|
|
47
|
-
attr_accessor :timeout
|
|
48
|
-
|
|
49
|
-
# @return [Boolean] Whether to capture request/response bodies (default: false)
|
|
50
|
-
attr_accessor :capture_bodies
|
|
51
|
-
|
|
52
|
-
# @return [Array<String>] Attributes to redact from traces
|
|
53
|
-
attr_accessor :redact_attributes
|
|
54
|
-
|
|
55
|
-
# @return [String] Service name for trace attribution
|
|
56
|
-
attr_accessor :service_name
|
|
57
|
-
|
|
58
|
-
# @return [String] Environment name (development, staging, production)
|
|
59
|
-
attr_accessor :environment
|
|
60
|
-
|
|
61
|
-
# @return [Hash] Additional resource attributes to include in all traces
|
|
62
|
-
attr_accessor :resource_attributes
|
|
63
|
-
|
|
64
|
-
# @return [Logger] Logger for telemetry operations
|
|
65
|
-
attr_accessor :logger
|
|
66
|
-
|
|
67
|
-
# @return [Boolean] Whether to store traces locally in the app's database
|
|
68
|
-
attr_accessor :local_storage
|
|
18
|
+
# @see ActiveAgents::Telemetry::Configuration
|
|
19
|
+
class Configuration < ActiveAgents::Telemetry::Configuration
|
|
20
|
+
# Fallback ingest path when the dashboard engine's mount point can't
|
|
21
|
+
# be resolved from the host's routes (e.g. engine not mounted).
|
|
22
|
+
LOCAL_ENDPOINT_PATH = "/activeagents/api/traces"
|
|
69
23
|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Local dashboard endpoint path (relative to app root)
|
|
74
|
-
LOCAL_ENDPOINT_PATH = "/active_agent/api/traces"
|
|
24
|
+
# @return [Boolean] Whether to store traces in the app's own database
|
|
25
|
+
attr_reader :local_storage
|
|
75
26
|
|
|
76
27
|
def initialize
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@batch_size = 100
|
|
82
|
-
@flush_interval = 5
|
|
83
|
-
@timeout = 10
|
|
84
|
-
@capture_bodies = false
|
|
85
|
-
@redact_attributes = %w[password secret token key credential api_key]
|
|
86
|
-
@service_name = nil
|
|
87
|
-
@environment = Rails.env if defined?(Rails)
|
|
88
|
-
@resource_attributes = {}
|
|
89
|
-
@logger = nil
|
|
28
|
+
super
|
|
29
|
+
# The framework predates the shared gem and has always been opt-in;
|
|
30
|
+
# adapters treat the presence of an api_key as the switch instead.
|
|
31
|
+
self.enabled = false
|
|
90
32
|
@local_storage = false
|
|
91
33
|
end
|
|
92
34
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# @return [Boolean]
|
|
96
|
-
def enabled?
|
|
97
|
-
@enabled == true
|
|
35
|
+
def local_storage=(value)
|
|
36
|
+
@local_storage = value == true
|
|
98
37
|
end
|
|
99
38
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Checks that endpoint and api_key are present, or local_storage is enabled.
|
|
103
|
-
#
|
|
104
|
-
# @return [Boolean]
|
|
105
|
-
def configured?
|
|
106
|
-
local_storage? || (endpoint.present? && api_key.present?)
|
|
39
|
+
def local_storage?
|
|
40
|
+
@local_storage
|
|
107
41
|
end
|
|
108
42
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
43
|
+
def local_store?
|
|
44
|
+
local_storage? || super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# When local storage is on, traces persist through the dashboard's
|
|
48
|
+
# trace model rather than leaving the process.
|
|
49
|
+
def local_store
|
|
50
|
+
super || (dashboard_store if local_storage?)
|
|
114
51
|
end
|
|
115
52
|
|
|
116
53
|
# Returns the resolved endpoint for trace reporting.
|
|
117
|
-
#
|
|
118
|
-
# Uses local endpoint when local_storage is enabled.
|
|
119
|
-
#
|
|
120
|
-
# @return [String]
|
|
121
54
|
def resolved_endpoint
|
|
122
|
-
|
|
123
|
-
LOCAL_ENDPOINT_PATH
|
|
124
|
-
else
|
|
125
|
-
endpoint
|
|
126
|
-
end
|
|
55
|
+
local_storage? ? local_endpoint_path : endpoint
|
|
127
56
|
end
|
|
128
57
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return false if sample_rate <= 0.0
|
|
137
|
-
|
|
138
|
-
rand < sample_rate
|
|
58
|
+
# The dashboard engine's ingest path, derived from wherever the host
|
|
59
|
+
# app actually mounted it — "/activeagents", "/observability", or "/"
|
|
60
|
+
# on a dedicated subdomain all work. Falls back to
|
|
61
|
+
# LOCAL_ENDPOINT_PATH when the engine isn't mounted.
|
|
62
|
+
def local_endpoint_path
|
|
63
|
+
mount = dashboard_mount_path
|
|
64
|
+
mount ? "#{mount}/api/traces" : LOCAL_ENDPOINT_PATH
|
|
139
65
|
end
|
|
140
66
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# Falls back to Rails application name or "activeagent".
|
|
144
|
-
#
|
|
145
|
-
# @return [String]
|
|
67
|
+
# The framework's historical fallback is "activeagent", not the shared
|
|
68
|
+
# core's generic "ruby".
|
|
146
69
|
def resolved_service_name
|
|
147
|
-
|
|
70
|
+
value = super
|
|
71
|
+
value == "ruby" ? "activeagent" : value
|
|
148
72
|
end
|
|
149
73
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# Falls back to Rails.logger or a null logger.
|
|
153
|
-
#
|
|
154
|
-
# @return [Logger]
|
|
155
|
-
def resolved_logger
|
|
156
|
-
@logger || (defined?(Rails) && Rails.logger) || Logger.new(File::NULL)
|
|
74
|
+
def to_h
|
|
75
|
+
super.merge(local_storage: local_storage)
|
|
157
76
|
end
|
|
158
77
|
|
|
159
|
-
|
|
160
|
-
#
|
|
161
|
-
# @param hash [Hash] Configuration hash
|
|
162
|
-
# @return [self]
|
|
163
|
-
def load_from_hash(hash)
|
|
164
|
-
hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
|
|
165
|
-
|
|
166
|
-
@enabled = hash[:enabled] if hash.key?(:enabled)
|
|
167
|
-
@endpoint = hash[:endpoint] if hash.key?(:endpoint)
|
|
168
|
-
@api_key = hash[:api_key] if hash.key?(:api_key)
|
|
169
|
-
@sample_rate = hash[:sample_rate].to_f if hash.key?(:sample_rate)
|
|
170
|
-
@batch_size = hash[:batch_size].to_i if hash.key?(:batch_size)
|
|
171
|
-
@flush_interval = hash[:flush_interval].to_i if hash.key?(:flush_interval)
|
|
172
|
-
@timeout = hash[:timeout].to_i if hash.key?(:timeout)
|
|
173
|
-
@capture_bodies = hash[:capture_bodies] if hash.key?(:capture_bodies)
|
|
174
|
-
@redact_attributes = hash[:redact_attributes] if hash.key?(:redact_attributes)
|
|
175
|
-
@service_name = hash[:service_name] if hash.key?(:service_name)
|
|
176
|
-
@environment = hash[:environment] if hash.key?(:environment)
|
|
177
|
-
@resource_attributes = hash[:resource_attributes] if hash.key?(:resource_attributes)
|
|
178
|
-
@local_storage = hash[:local_storage] if hash.key?(:local_storage)
|
|
179
|
-
|
|
180
|
-
self
|
|
181
|
-
end
|
|
78
|
+
private
|
|
182
79
|
|
|
183
|
-
#
|
|
80
|
+
# Persists a trace through the dashboard's trace model, honoring
|
|
81
|
+
# ActionAgent.trace_model_class overrides. Idempotent on trace_id, as
|
|
82
|
+
# HTTP ingest is.
|
|
184
83
|
#
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
84
|
+
# The dashboard is the `actionagent` gem, which the framework does not
|
|
85
|
+
# depend on — every reference to it here is guarded, so local_storage
|
|
86
|
+
# simply reports that it has nowhere to write when it isn't installed.
|
|
87
|
+
def dashboard_store
|
|
88
|
+
@dashboard_store ||= lambda do |trace, sdk|
|
|
89
|
+
model = local_trace_model
|
|
90
|
+
unless model
|
|
91
|
+
resolved_logger.error(
|
|
92
|
+
"[ActiveAgent::Telemetry] local_storage is enabled but no trace model is available — " \
|
|
93
|
+
"add the actionagent gem and run `rails generate action_agent:install` first"
|
|
94
|
+
)
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
next if model.exists?(trace_id: trace["trace_id"])
|
|
99
|
+
|
|
100
|
+
model.create_from_payload(trace, sdk)
|
|
101
|
+
end
|
|
200
102
|
end
|
|
201
103
|
|
|
202
|
-
|
|
104
|
+
def local_trace_model
|
|
105
|
+
if defined?(::ActionAgent) && ::ActionAgent.respond_to?(:trace_model)
|
|
106
|
+
::ActionAgent.trace_model
|
|
107
|
+
elsif defined?(::ActionAgent::TelemetryTrace)
|
|
108
|
+
::ActionAgent::TelemetryTrace
|
|
109
|
+
end
|
|
110
|
+
rescue NameError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
203
113
|
|
|
204
|
-
|
|
205
|
-
|
|
114
|
+
# The engine's mount point in the host app, found by locating the
|
|
115
|
+
# mounted engine in the host's route set. Works regardless of the
|
|
116
|
+
# helper name, so `mount ... => "/", as: :something_else` and
|
|
117
|
+
# constraint-wrapped mounts resolve correctly. Returns nil when the
|
|
118
|
+
# engine isn't mounted or no Rails app is booted; "" for a root mount.
|
|
119
|
+
def dashboard_mount_path
|
|
120
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
206
121
|
|
|
207
|
-
Rails.application.
|
|
122
|
+
mount_path_in(::Rails.application.routes)
|
|
208
123
|
rescue StandardError
|
|
209
124
|
nil
|
|
210
125
|
end
|
|
126
|
+
|
|
127
|
+
# The engine's mount path within a given route set, or nil when it
|
|
128
|
+
# isn't mounted there. Separate from #dashboard_mount_path so it can be
|
|
129
|
+
# exercised against a route set directly.
|
|
130
|
+
public def mount_path_in(route_set)
|
|
131
|
+
return nil unless defined?(::ActionAgent::Engine)
|
|
132
|
+
|
|
133
|
+
route = route_set.routes.find do |candidate|
|
|
134
|
+
mounted_engine(candidate.app) == ::ActionAgent::Engine
|
|
135
|
+
end
|
|
136
|
+
return nil unless route
|
|
137
|
+
|
|
138
|
+
# "/activeagents(.:format)" -> "/activeagents"; a root mount -> "".
|
|
139
|
+
route.path.spec.to_s.sub(/\(\.:format\)\z/, "").chomp("/")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Unwraps the constraint layers Rails wraps a mounted engine in,
|
|
143
|
+
# returning the engine class (or nil for ordinary routes). Stops at the
|
|
144
|
+
# engine class itself — engines also respond to #app (their route set),
|
|
145
|
+
# so unwrapping blindly walks straight past them.
|
|
146
|
+
def mounted_engine(app)
|
|
147
|
+
5.times do
|
|
148
|
+
return app if app.is_a?(Class) && app < ::Rails::Engine
|
|
149
|
+
break unless app.respond_to?(:app) && !app.app.equal?(app)
|
|
150
|
+
|
|
151
|
+
app = app.app
|
|
152
|
+
end
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
211
155
|
end
|
|
212
156
|
end
|
|
213
157
|
end
|
|
@@ -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
|
|
@@ -91,10 +147,28 @@ module ActiveAgent
|
|
|
91
147
|
tool_span = span.add_span("tool.#{tool_call[:name]}", span_type: :tool)
|
|
92
148
|
tool_span.set_attribute("tool.name", tool_call[:name])
|
|
93
149
|
tool_span.set_attribute("tool.id", tool_call[:id]) if tool_call[:id]
|
|
150
|
+
ToolOrigin.annotate(tool_span, tool_call[:name])
|
|
94
151
|
tool_span.finish
|
|
95
152
|
end
|
|
96
153
|
end
|
|
97
154
|
|
|
155
|
+
# The span was tagged with the configured model (often unset →
|
|
156
|
+
# "unknown"); the provider's raw response knows what actually
|
|
157
|
+
# served the request.
|
|
158
|
+
if result.respond_to?(:raw_response) && result.raw_response.is_a?(Hash)
|
|
159
|
+
served_model = result.raw_response["model"] || result.raw_response[:model]
|
|
160
|
+
llm_span.set_attribute("llm.model", served_model.to_s) if served_model.present?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Carry the generation contents so dashboards can show what
|
|
164
|
+
# came back, not just how many tokens it cost.
|
|
165
|
+
if result.respond_to?(:message) && result.message.respond_to?(:content) && result.message.content.present?
|
|
166
|
+
llm_span.set_attribute("llm.output.message", telemetry_truncate(result.message.content))
|
|
167
|
+
end
|
|
168
|
+
if result.respond_to?(:finish_reason) && result.finish_reason.present?
|
|
169
|
+
llm_span.set_attribute("llm.finish_reason", result.finish_reason.to_s)
|
|
170
|
+
end
|
|
171
|
+
|
|
98
172
|
llm_span.set_status(:ok)
|
|
99
173
|
llm_span.finish
|
|
100
174
|
span.set_status(:ok)
|
|
@@ -105,6 +179,47 @@ module ActiveAgent
|
|
|
105
179
|
llm_span.finish
|
|
106
180
|
span.record_error(e)
|
|
107
181
|
raise
|
|
182
|
+
ensure
|
|
183
|
+
@_telemetry_llm_span = nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Providers invoke this proc for every tool call during generation.
|
|
189
|
+
# Wrapping it is what makes tool telemetry real: each call gets a
|
|
190
|
+
# timed span with its arguments and result — the post-hoc
|
|
191
|
+
# result.tool_calls path below never fires on 1.x responses, which
|
|
192
|
+
# don't expose tool calls.
|
|
193
|
+
def tools_function
|
|
194
|
+
base = super
|
|
195
|
+
return base unless Telemetry.enabled?
|
|
196
|
+
|
|
197
|
+
agent = self
|
|
198
|
+
proc do |tool_name, *args, **kwargs|
|
|
199
|
+
parent = agent.instance_variable_get(:@_telemetry_llm_span)
|
|
200
|
+
unless parent
|
|
201
|
+
next base.call(tool_name, *args, **kwargs)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
tool_span = parent.add_span("tool.#{tool_name}", span_type: :tool)
|
|
205
|
+
tool_span.set_attribute("tool.name", tool_name.to_s)
|
|
206
|
+
# Records which MCP server (if any) serves this tool, so tool
|
|
207
|
+
# traffic can be grouped by service downstream.
|
|
208
|
+
ToolOrigin.annotate(tool_span, tool_name)
|
|
209
|
+
arguments = kwargs.presence || (args.length == 1 ? args.first : args.presence)
|
|
210
|
+
if arguments.present?
|
|
211
|
+
tool_span.set_attribute("tool.input.args", agent.send(:telemetry_truncate, JSON.generate(arguments)))
|
|
212
|
+
end
|
|
213
|
+
begin
|
|
214
|
+
result = base.call(tool_name, *args, **kwargs)
|
|
215
|
+
tool_span.set_attribute("tool.output.result", agent.send(:telemetry_truncate, result))
|
|
216
|
+
tool_span.set_status(:ok)
|
|
217
|
+
result
|
|
218
|
+
rescue StandardError => e
|
|
219
|
+
tool_span.record_error(e)
|
|
220
|
+
raise
|
|
221
|
+
ensure
|
|
222
|
+
tool_span.finish
|
|
108
223
|
end
|
|
109
224
|
end
|
|
110
225
|
end
|
|
@@ -116,7 +231,7 @@ module ActiveAgent
|
|
|
116
231
|
Telemetry.trace("#{self.class.name}.embed", span_type: :embedding) do |span|
|
|
117
232
|
span.set_attribute("agent.class", self.class.name)
|
|
118
233
|
span.set_attribute("agent.action", "embed")
|
|
119
|
-
span.set_attribute("agent.provider", provider_name)
|
|
234
|
+
span.set_attribute("agent.provider", provider_name)
|
|
120
235
|
|
|
121
236
|
begin
|
|
122
237
|
result = super
|
|
@@ -138,8 +253,20 @@ module ActiveAgent
|
|
|
138
253
|
|
|
139
254
|
private
|
|
140
255
|
|
|
256
|
+
# Content attributes are capped so a large prompt (e.g. a 100k-token
|
|
257
|
+
# tool loop) can't bloat the trace payload.
|
|
258
|
+
TELEMETRY_ATTRIBUTE_MAX_CHARS = 4_000
|
|
259
|
+
|
|
260
|
+
def telemetry_truncate(value)
|
|
261
|
+
text = value.to_s
|
|
262
|
+
return text if text.length <= TELEMETRY_ATTRIBUTE_MAX_CHARS
|
|
263
|
+
|
|
264
|
+
"#{text[0, TELEMETRY_ATTRIBUTE_MAX_CHARS]}… (truncated, #{text.length} chars total)"
|
|
265
|
+
end
|
|
266
|
+
|
|
141
267
|
def provider_name
|
|
142
|
-
|
|
268
|
+
klass = prompt_provider_klass
|
|
269
|
+
klass.respond_to?(:tag_name) ? klass.tag_name : "unknown"
|
|
143
270
|
rescue StandardError
|
|
144
271
|
"unknown"
|
|
145
272
|
end
|