activeagent 1.0.2 → 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +26 -0
  4. data/lib/active_agent/base.rb +4 -0
  5. data/lib/active_agent/concerns/view.rb +1 -1
  6. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +27 -6
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +11 -1
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +15 -12
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +27 -7
  10. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +9 -0
  11. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +1 -1
  12. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +18 -2
  13. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +15 -3
  14. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +3 -1
  15. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +4 -4
  16. data/lib/active_agent/dashboard/config/routes.rb +5 -64
  17. data/lib/active_agent/dashboard/engine.rb +19 -15
  18. data/lib/active_agent/dashboard.rb +13 -3
  19. data/lib/active_agent/model_capabilities.rb +89 -0
  20. data/lib/active_agent/providers/_base_provider.rb +23 -2
  21. data/lib/active_agent/providers/anthropic/request.rb +3 -1
  22. data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
  23. data/lib/active_agent/providers/bedrock_provider.rb +2 -2
  24. data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
  25. data/lib/active_agent/providers/errors.rb +140 -0
  26. data/lib/active_agent/providers/mock/request.rb +1 -4
  27. data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
  28. data/lib/active_agent/providers/open_ai/chat_provider.rb +3 -3
  29. data/lib/active_agent/providers/requesty/_types.rb +16 -0
  30. data/lib/active_agent/providers/requesty/options.rb +39 -0
  31. data/lib/active_agent/providers/requesty_provider.rb +63 -0
  32. data/lib/active_agent/railtie.rb +5 -0
  33. data/lib/active_agent/telemetry/configuration.rb +112 -172
  34. data/lib/active_agent/telemetry/instrumentation.rb +137 -14
  35. data/lib/active_agent/telemetry/reporter.rb +12 -165
  36. data/lib/active_agent/telemetry/span.rb +18 -245
  37. data/lib/active_agent/telemetry/tracer.rb +67 -70
  38. data/lib/active_agent/telemetry.rb +1 -2
  39. data/lib/active_agent/version.rb +1 -1
  40. data/lib/active_agent.rb +5 -0
  41. data/lib/generators/active_agent/dashboard/install_generator.rb +30 -2
  42. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +41 -4
  43. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +20 -4
  44. metadata +39 -16
  45. data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
  46. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
  47. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
  48. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
  49. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
  50. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
  51. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
  52. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
  53. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
  54. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
@@ -2,212 +2,152 @@
2
2
 
3
3
  module ActiveAgent
4
4
  module Telemetry
5
- # Configuration for telemetry collection and reporting.
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
- # Stores settings for endpoint, authentication, sampling, and batching.
8
- # Configuration can be set programmatically or loaded from YAML.
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
- # @example Programmatic configuration
11
- # ActiveAgent::Telemetry.configure do |config|
12
- # config.enabled = true
13
- # config.endpoint = "https://api.activeagents.ai/v1/traces"
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
- # @example YAML configuration (config/activeagent.yml)
19
- # telemetry:
20
- # enabled: true
21
- # endpoint: https://api.activeagents.ai/v1/traces
22
- # api_key: <%= ENV["ACTIVEAGENTS_API_KEY"] %>
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
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"
57
23
 
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
69
-
70
- # Default ActiveAgents.ai endpoint for hosted observability.
71
- DEFAULT_ENDPOINT = "https://api.activeagents.ai/v1/traces"
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
- @enabled = false
78
- @endpoint = DEFAULT_ENDPOINT
79
- @api_key = nil
80
- @sample_rate = 1.0
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
- # Returns whether telemetry collection is enabled.
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
- # Returns whether telemetry is properly configured.
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
- # Returns whether local storage mode is enabled.
110
- #
111
- # @return [Boolean]
112
- def local_storage?
113
- @local_storage == true
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
- if local_storage?
123
- LOCAL_ENDPOINT_PATH
124
- else
125
- endpoint
126
- end
55
+ local_storage? ? local_endpoint_path : endpoint
127
56
  end
128
57
 
129
- # Returns whether a trace should be sampled.
130
- #
131
- # Uses sample_rate to determine if trace should be collected.
132
- #
133
- # @return [Boolean]
134
- def should_sample?
135
- return true if sample_rate >= 1.0
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
- # Resolves the service name for traces.
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
- @service_name || rails_app_name || "activeagent"
70
+ value = super
71
+ value == "ruby" ? "activeagent" : value
148
72
  end
149
73
 
150
- # Returns the logger for telemetry operations.
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
- # Loads configuration from a hash (typically from YAML).
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
- # Returns configuration as a hash for serialization.
184
- #
185
- # @return [Hash]
186
- def to_h
187
- {
188
- enabled: enabled,
189
- endpoint: endpoint,
190
- api_key: api_key ? "[REDACTED]" : nil,
191
- sample_rate: sample_rate,
192
- batch_size: batch_size,
193
- flush_interval: flush_interval,
194
- timeout: timeout,
195
- capture_bodies: capture_bodies,
196
- service_name: resolved_service_name,
197
- environment: environment,
198
- local_storage: local_storage
199
- }
80
+ # Persists a trace through the dashboard's trace model, honoring
81
+ # ActiveAgent::Dashboard.trace_model_class overrides. Idempotent on
82
+ # trace_id, as HTTP ingest is.
83
+ def dashboard_store
84
+ @dashboard_store ||= lambda do |trace, sdk|
85
+ model = local_trace_model
86
+ unless model
87
+ resolved_logger.error(
88
+ "[ActiveAgent::Telemetry] local_storage is enabled but no trace model is available — " \
89
+ "run `rails generate active_agent:dashboard:install` first"
90
+ )
91
+ next
92
+ end
93
+
94
+ next if model.exists?(trace_id: trace["trace_id"])
95
+
96
+ model.create_from_payload(trace, sdk)
97
+ end
200
98
  end
201
99
 
202
- private
100
+ def local_trace_model
101
+ if defined?(ActiveAgent::Dashboard) && ActiveAgent::Dashboard.respond_to?(:trace_model)
102
+ ActiveAgent::Dashboard.trace_model
103
+ elsif defined?(ActiveAgent::TelemetryTrace)
104
+ ActiveAgent::TelemetryTrace
105
+ end
106
+ rescue NameError
107
+ nil
108
+ end
203
109
 
204
- def rails_app_name
205
- return nil unless defined?(Rails) && Rails.application
110
+ # The engine's mount point in the host app, found by locating the
111
+ # mounted engine in the host's route set. Works regardless of the
112
+ # helper name, so `mount ... => "/", as: :something_else` and
113
+ # constraint-wrapped mounts resolve correctly. Returns nil when the
114
+ # engine isn't mounted or no Rails app is booted; "" for a root mount.
115
+ def dashboard_mount_path
116
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
206
117
 
207
- Rails.application.class.module_parent_name.underscore
118
+ mount_path_in(::Rails.application.routes)
208
119
  rescue StandardError
209
120
  nil
210
121
  end
122
+
123
+ # The engine's mount path within a given route set, or nil when it
124
+ # isn't mounted there. Separate from #dashboard_mount_path so it can be
125
+ # exercised against a route set directly.
126
+ public def mount_path_in(route_set)
127
+ return nil unless defined?(::ActiveAgent::Dashboard::Engine)
128
+
129
+ route = route_set.routes.find do |candidate|
130
+ mounted_engine(candidate.app) == ::ActiveAgent::Dashboard::Engine
131
+ end
132
+ return nil unless route
133
+
134
+ # "/activeagents(.:format)" -> "/activeagents"; a root mount -> "".
135
+ route.path.spec.to_s.sub(/\(\.:format\)\z/, "").chomp("/")
136
+ end
137
+
138
+ # Unwraps the constraint layers Rails wraps a mounted engine in,
139
+ # returning the engine class (or nil for ordinary routes). Stops at the
140
+ # engine class itself — engines also respond to #app (their route set),
141
+ # so unwrapping blindly walks straight past them.
142
+ def mounted_engine(app)
143
+ 5.times do
144
+ return app if app.is_a?(Class) && app < ::Rails::Engine
145
+ break unless app.respond_to?(:app) && !app.app.equal?(app)
146
+
147
+ app = app.app
148
+ end
149
+ nil
150
+ end
211
151
  end
212
152
  end
213
153
  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
- Telemetry.trace("#{self.class.name}.#{action_name}", span_type: :root) do |span|
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) if respond_to?(:provider_name)
53
- span.set_attribute("agent.model", model_name) if respond_to?(: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
- prompt_span.set_attribute("messages.count", messages.size) if respond_to?(:messages)
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) if respond_to?(:provider_name)
63
- llm_span.set_attribute("llm.model", model_name) if respond_to?(: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) if respond_to?(: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
- self.class.generation_provider&.to_s || "unknown"
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