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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a1deba19c874b6abb36731bcee93bddf79bc193a3443a234cc811d9a4cc8383
4
- data.tar.gz: 6f8847ab0d5eebf0ce26a8425f7630c884f0b9af71384c67325e3c06a8b0a8ae
3
+ metadata.gz: f352ce28ead5e975e8d64c76c4a263c1ac7cac3b202ea6fef1a137f899e3aea0
4
+ data.tar.gz: d163e4360d38fdf440465ec0caf76dc9c196cab342de0de4cf67de020b31d0e2
5
5
  SHA512:
6
- metadata.gz: 767f039bd0af4f600a34becaac7bb2d04d2f8b768aef11242191f8a355b4a442c9c10690fe25a1a7f8e0da8b27e7216025c6f882c6d6002e5da9d0a9e876f1cb
7
- data.tar.gz: 30725fb2369bd3b8dc23aa9d41e7d8a9843c3c8dd0aa629ace4bbd4cc217077339684ae0925cc6ea330c1e018b444a66057bcf5854b5bebf2bcb6a14bf57a322
6
+ metadata.gz: 8aed9bb0e1d7bec5b5e178940e4e43c8a117c82d36db703d34cbef485b0492d29d628350bb956cf709ee504a81327e152d5510d3c1b81cf43d5ecc70f33ffae8
7
+ data.tar.gz: 6d8b3c9b6d6f7286985206a42db8949067dbb46bc39db67e4eb507ccc1a9dd405ed46720c06878c4d5ea748f8da184072ce4e9bf9951361d4fe5172ae65770d8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,77 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2026-08-12
9
+
10
+ ### Dashboard — self-hosted (enterprise) mount readiness
11
+
12
+ The engine can now be mounted in any Rails app as the self-hosted
13
+ observability surface (see `docs/framework/self-hosted-observability.md`):
14
+
15
+ - **One install generator**: the duplicate `active_agent:dashboard:install`
16
+ variant that copied eight migrations (agents, sandboxes, recordings —
17
+ tables for models with no shipped controllers or routes) is removed.
18
+ The surviving generator installs the telemetry traces table only and
19
+ gains `--skip_migrations` / `--skip_routes`; its initializer template now
20
+ covers authentication, `ingest_api_key`, and multi-tenant options.
21
+ - **Canonical mount path is `/activeagents`** (generator, dummy app and
22
+ docs updated). `Telemetry::Configuration#resolved_endpoint` now reports
23
+ the ingest path for wherever the engine is actually mounted — any mount
24
+ path, including `/` on a dedicated subdomain — instead of a hardcoded
25
+ constant, falling back to `LOCAL_ENDPOINT_PATH` when it isn't mounted.
26
+ Note this is informational: `local_storage` capture writes through the
27
+ trace model without HTTP, and remote apps set `endpoint:` explicitly.
28
+ - **`TracesController` honors configuration**: index/metrics/time-series
29
+ queries now go through `ActiveAgent::Dashboard.trace_model` (previously
30
+ only `show` did) and are scoped with `for_account(current_owner)`, so a
31
+ `trace_model_class` override and multi-tenant scoping apply everywhere.
32
+ - **Single-tenant ingest auth**: new `config.ingest_api_key` requires a
33
+ matching Bearer token on `POST <mount>/api/traces` when set. The
34
+ telemetry reporter and ruby_llm_telemetry already send their `api_key`
35
+ as a Bearer header, so remote apps need no changes.
36
+ - **Metrics page no longer 500s with data**: the per-agent stats table
37
+ read a grouped SQL alias through a model method that expected per-trace
38
+ token columns.
39
+ - **Mount detection is route-set based**: the ingest path is resolved by
40
+ locating the mounted engine in the host's routes rather than assuming
41
+ the default `active_agent_path` helper, so `mount ... => "/", as:
42
+ :something_else` and constraint-wrapped (subdomain) mounts resolve
43
+ correctly instead of silently falling back.
44
+ - Deprecated the never-consumed `base_controller_class` config attribute:
45
+ it remains a no-op accessor with its historical default so existing
46
+ initializers keep booting, and will be removed in the next major.
47
+
48
+ ### Dashboard & Telemetry — dev console readiness
49
+
50
+ The dashboard engine — Active Agent's local dev console — now works out of
51
+ the box (production observability is the hosted platform product):
52
+
53
+ - **Engine load paths fixed**: `Engine.find_root` now points at the
54
+ dashboard directory, so `ActiveAgent::TelemetryTrace`,
55
+ `ProcessTelemetryTracesJob`, the API controller, views and engine routes
56
+ are auto-discovered in host apps (previously they required manual
57
+ `require`s). The engine is also required eagerly with Rails, since
58
+ engines defined lazily miss initializer collection.
59
+ - **Routes now match shipped controllers**: the engine exposes traces,
60
+ metrics and the ingest API (`<mount>/api/traces`); routes to
61
+ never-shipped controllers (agents, sandboxes, templates, recordings,
62
+ api/v1) were removed. Engine root renders the traces index.
63
+ - **`local_storage` telemetry mode fixed**: tracer payloads are
64
+ symbol-keyed and were silently dropped by the string-keyed ingestion
65
+ normalizer; the reporter now stringifies and honors
66
+ `ActiveAgent::Dashboard.trace_model` overrides.
67
+ - **Token totals no longer double-count**: instrumentation mirrors LLM
68
+ token usage onto the root span; `TelemetryTrace.create_from_payload`
69
+ now counts child spans as the source of truth.
70
+ - **Span waterfall renders real offsets** (was pinned to 0ms), turbo-rails
71
+ is now optional (previously 500s without it), layout route helpers fixed,
72
+ `Agent.for_owner` scope added, synchronous ingest capped at 100
73
+ traces/request.
74
+ - **New docs** (`docs/framework/dashboard.md`, README section) covering
75
+ install, authentication (none by default — see docs), remote ingestion
76
+ and multi-tenant mode; dashboard engine test suite added
77
+ (`test/dashboard/`).
78
+
8
79
  ## [1.0.0] - 2025-11-21
9
80
 
10
81
  Major refactor with breaking changes. Complete provider rewrite. New modular architecture.
data/README.md CHANGED
@@ -127,6 +127,32 @@ development:
127
127
  service: "RubyLLM"
128
128
  ```
129
129
 
130
+ ## Dev Console & Observability
131
+
132
+ Active Agent includes a local dev console — traces with span waterfalls,
133
+ token usage, and per-agent metrics — as a mountable Rails engine, so you
134
+ can watch your agents while you build:
135
+
136
+ ```bash
137
+ rails generate active_agent:dashboard:install
138
+ rails db:migrate
139
+ ```
140
+
141
+ ```yaml
142
+ # config/active_agent.yml
143
+ telemetry:
144
+ enabled: true
145
+ local_storage: true
146
+ ```
147
+
148
+ Open `/activeagents` and every generation appears as a trace. See
149
+ [docs/framework/dashboard.md](docs/framework/dashboard.md) for
150
+ authentication, remote ingestion, and multi-tenant mode. For production
151
+ monitoring — evaluations, cost estimates, retention, team workspaces —
152
+ point telemetry at the hosted platform at
153
+ [activeagents.ai](https://activeagents.ai); every workspace starts with a
154
+ free low-volume trial.
155
+
130
156
  ## Features
131
157
 
132
158
  - **Agent-Oriented Programming**: Build AI applications using familiar Rails patterns
@@ -304,6 +304,10 @@ module ActiveAgent
304
304
  # Render out proc/lamda attributes before rendering templates
305
305
  parameters.deep_transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
306
306
 
307
+ # Strip parameters the target model rejects (e.g. temperature/top_p
308
+ # on thinking-first models) before they reach the provider.
309
+ ModelCapabilities.sanitize!(parameters)
310
+
307
311
  # Apply Callbacks
308
312
  parameters.merge!(
309
313
  trace_id: prompt_options[:trace_id] || SecureRandom.uuid,
@@ -142,7 +142,7 @@ module ActiveAgent
142
142
 
143
143
  return if !strict && !lookup_context.exists?(template_name, _prefixes, false, [], **options)
144
144
 
145
- template = lookup_context.find_template(template_name, _prefixes, false, [], **options)
145
+ template = lookup_context.find(template_name, _prefixes, false, [], **options)
146
146
 
147
147
  render_to_string(template: template.virtual_path, locals:, layout: false).chomp.presence
148
148
  end
@@ -9,11 +9,14 @@ module ActiveAgent
9
9
  # for analysis and visualization in the dashboard.
10
10
  #
11
11
  # Supports two modes:
12
- # - Local mode: No authentication, synchronous processing
13
- # - Multi-tenant mode: Bearer token auth, async processing via job
12
+ # - Local mode: synchronous processing; unauthenticated unless
13
+ # ActiveAgent::Dashboard.ingest_api_key is set (set it whenever the
14
+ # mount is reachable beyond your own machine)
15
+ # - Multi-tenant mode: per-account Bearer token auth, async processing
16
+ # via job
14
17
  #
15
18
  # @example Local mode request
16
- # POST /active_agent/api/traces
19
+ # POST <mount>/api/traces (e.g. /activeagents/api/traces)
17
20
  # Content-Type: application/json
18
21
  #
19
22
  # {
@@ -22,7 +25,7 @@ module ActiveAgent
22
25
  # }
23
26
  #
24
27
  # @example Multi-tenant mode request
25
- # POST /active_agent/api/traces
28
+ # POST <mount>/api/traces (e.g. /activeagents/api/traces)
26
29
  # Authorization: Bearer <api_key>
27
30
  # Content-Type: application/json
28
31
  #
@@ -33,10 +36,15 @@ module ActiveAgent
33
36
  #
34
37
  class TracesController < ActionController::API
35
38
  before_action :authenticate_api_key!, if: -> { ActiveAgent::Dashboard.multi_tenant? }
39
+ before_action :authenticate_ingest_key!, unless: -> { ActiveAgent::Dashboard.multi_tenant? }
36
40
 
37
- # POST /active_agent/api/traces
41
+ # Maximum traces accepted per request (mirrors
42
+ # ProcessTelemetryTracesJob::MAX_TRACES_PER_JOB).
43
+ MAX_TRACES_PER_REQUEST = 100
44
+
45
+ # POST <mount>/api/traces (e.g. /activeagents/api/traces)
38
46
  def create
39
- traces = params[:traces] || []
47
+ traces = Array(params[:traces]).take(MAX_TRACES_PER_REQUEST)
40
48
  sdk_info = params[:sdk] || {}
41
49
 
42
50
  return head :accepted if traces.empty?
@@ -86,6 +94,19 @@ module ActiveAgent
86
94
  @account.increment_telemetry_usage! if @account.respond_to?(:increment_telemetry_usage!)
87
95
  end
88
96
 
97
+ # Requires the configured single-tenant ingest key when one is set.
98
+ # The telemetry reporter and ruby_llm_telemetry both send their
99
+ # api_key as a Bearer header, so remote apps work unchanged.
100
+ def authenticate_ingest_key!
101
+ expected = ActiveAgent::Dashboard.ingest_api_key
102
+ return if expected.blank?
103
+
104
+ token = extract_bearer_token
105
+ return if token.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected)
106
+
107
+ render json: { error: "Invalid API key" }, status: :unauthorized
108
+ end
109
+
89
110
  # Extracts Bearer token from Authorization header.
90
111
  def extract_bearer_token
91
112
  auth_header = request.headers["Authorization"]
@@ -21,7 +21,17 @@ module ActiveAgent
21
21
  private
22
22
 
23
23
  def authenticate_dashboard!
24
- return if ActiveAgent::Dashboard.authentication_method.nil?
24
+ if ActiveAgent::Dashboard.authentication_method.nil?
25
+ # Traces contain prompts, outputs, and error backtraces. Refuse to
26
+ # serve them unauthenticated in production rather than exposing
27
+ # them to the internet by default.
28
+ if Rails.env.production?
29
+ render plain: "ActiveAgent Dashboard: set ActiveAgent::Dashboard.authentication_method " \
30
+ "(see docs/framework/dashboard.md) to enable access in production.",
31
+ status: :forbidden
32
+ end
33
+ return
34
+ end
25
35
 
26
36
  result = ActiveAgent::Dashboard.authentication_method.call(self)
27
37
  head :unauthorized unless result
@@ -6,23 +6,26 @@ module ActiveAgent
6
6
  #
7
7
  class DashboardController < ApplicationController
8
8
  def index
9
+ unless ActiveAgent::Dashboard.use_inertia && defined?(InertiaRails)
10
+ # No ERB overview ships yet — the traces list is the dashboard.
11
+ # Loading agent/run data here would also require the full
12
+ # dashboard tables, which telemetry-only installs don't have.
13
+ return redirect_to(traces_path)
14
+ end
15
+
9
16
  @agents = fetch_agents.limit(10)
10
17
  @recent_runs = fetch_recent_runs.limit(10)
11
18
  @recent_traces = fetch_recent_traces.limit(10)
12
19
  @metrics = calculate_metrics
13
20
 
14
- if ActiveAgent::Dashboard.use_inertia && defined?(InertiaRails)
15
- render inertia: "Dashboard", props: {
16
- agents: serialize_agents(@agents),
17
- recentRuns: serialize_runs(@recent_runs),
18
- recentTraces: serialize_traces(@recent_traces),
19
- metrics: @metrics,
20
- user: current_user_props,
21
- account: current_account_props
22
- }
23
- else
24
- render :index
25
- end
21
+ render inertia: "Dashboard", props: {
22
+ agents: serialize_agents(@agents),
23
+ recentRuns: serialize_runs(@recent_runs),
24
+ recentTraces: serialize_traces(@recent_traces),
25
+ metrics: @metrics,
26
+ user: current_user_props,
27
+ account: current_account_props
28
+ }
26
29
  end
27
30
 
28
31
  private
@@ -16,12 +16,14 @@ module ActiveAgent
16
16
 
17
17
  respond_to do |format|
18
18
  format.html
19
- format.turbo_stream
19
+ # turbo-rails is optional; responding to an unregistered MIME
20
+ # type raises in host apps without it.
21
+ format.turbo_stream if turbo_stream_available?
20
22
  end
21
23
  end
22
24
 
23
25
  def show
24
- @trace = ActiveAgent::TelemetryTrace.find(params[:id])
26
+ @trace = scoped_traces.find(params[:id])
25
27
  end
26
28
 
27
29
  def metrics
@@ -31,14 +33,32 @@ module ActiveAgent
31
33
 
32
34
  respond_to do |format|
33
35
  format.html
34
- format.turbo_stream
36
+ format.turbo_stream if turbo_stream_available?
35
37
  end
36
38
  end
37
39
 
38
40
  private
39
41
 
42
+ def turbo_stream_available?
43
+ Mime::Type.lookup_by_extension(:turbo_stream).present?
44
+ end
45
+
46
+ # All queries honor the configured trace model and, in multi-tenant
47
+ # mode, the current owner's account (for_account no-ops otherwise).
48
+ #
49
+ # In multi-tenant mode an unresolvable owner returns nothing rather
50
+ # than falling through: current_owner degrades to current_user when
51
+ # current_account_method is unset, which would scope by a user id
52
+ # against account_id — another tenant's traces.
53
+ def scoped_traces
54
+ model = ActiveAgent::Dashboard.trace_model
55
+ return model.none if ActiveAgent::Dashboard.multi_tenant? && current_owner.nil?
56
+
57
+ model.for_account(current_owner)
58
+ end
59
+
40
60
  def fetch_traces
41
- traces = ActiveAgent::TelemetryTrace.recent
61
+ traces = scoped_traces.recent
42
62
 
43
63
  traces = traces.for_agent(params[:agent]) if params[:agent].present?
44
64
  traces = traces.with_errors if params[:status] == "error"
@@ -55,7 +75,7 @@ module ActiveAgent
55
75
  end
56
76
 
57
77
  def calculate_metrics
58
- traces = ActiveAgent::TelemetryTrace.where(
78
+ traces = scoped_traces.where(
59
79
  "created_at > ?", 24.hours.ago
60
80
  )
61
81
 
@@ -77,7 +97,7 @@ module ActiveAgent
77
97
  end
78
98
 
79
99
  def agent_statistics
80
- ActiveAgent::TelemetryTrace
100
+ scoped_traces
81
101
  .where("created_at > ?", 24.hours.ago)
82
102
  .group(:agent_class)
83
103
  .select(
@@ -90,7 +110,7 @@ module ActiveAgent
90
110
  end
91
111
 
92
112
  def time_series_data
93
- ActiveAgent::TelemetryTrace
113
+ scoped_traces
94
114
  .where("created_at > ?", 1.hour.ago)
95
115
  .group_by_minute(:created_at)
96
116
  .count
@@ -39,7 +39,16 @@ module ActiveAgent
39
39
  return
40
40
  end
41
41
 
42
+ # Process in bounded slices; anything beyond the cap is re-enqueued
43
+ # rather than silently dropped (the client already received 202).
44
+ remainder = traces.drop(MAX_TRACES_PER_JOB)
42
45
  traces = traces.take(MAX_TRACES_PER_JOB)
46
+ if remainder.any?
47
+ self.class.perform_later(
48
+ account_id: account_id, traces: remainder,
49
+ sdk_info: sdk_info, received_at: received_at
50
+ )
51
+ end
43
52
 
44
53
  traces.each do |trace|
45
54
  process_trace(trace, sdk_info, account)
@@ -71,7 +71,7 @@ module ActiveAgent
71
71
  ].freeze
72
72
 
73
73
  # Available providers
74
- PROVIDERS = %w[openai anthropic ollama openrouter].freeze
74
+ PROVIDERS = %w[openai anthropic ollama openrouter requesty].freeze
75
75
 
76
76
  # Returns the configuration as a hash for versioning
77
77
  def configuration_snapshot
@@ -54,13 +54,20 @@ module ActiveAgent
54
54
  spans = trace["spans"] || []
55
55
  root_span = spans.find { |s| s["parent_span_id"].nil? } || spans.first || {}
56
56
 
57
- # Calculate totals from all spans
58
57
  total_duration = root_span["duration_ms"]
58
+
59
+ # Instrumentation mirrors LLM token usage onto the root span for
60
+ # display, so summing every span double-counts. When child spans
61
+ # carry token data, they are the source of truth; the root span only
62
+ # counts for single-span traces.
63
+ counted_spans = spans.reject { |s| s["parent_span_id"].nil? }
64
+ counted_spans = spans if counted_spans.none? { |s| span_token_sum(s).positive? }
65
+
59
66
  total_input = 0
60
67
  total_output = 0
61
68
  total_thinking = 0
62
69
 
63
- spans.each do |span|
70
+ counted_spans.each do |span|
64
71
  tokens = span["tokens"] || {}
65
72
  total_input += (tokens["input"] || 0)
66
73
  total_output += (tokens["output"] || 0)
@@ -100,6 +107,15 @@ module ActiveAgent
100
107
  create!(attrs)
101
108
  end
102
109
 
110
+ # Sums a span's token counts (used to decide which spans carry the
111
+ # authoritative token data during ingestion).
112
+ #
113
+ # @api private
114
+ def self.span_token_sum(span)
115
+ tokens = span["tokens"] || {}
116
+ tokens.fetch("input", 0).to_i + tokens.fetch("output", 0).to_i + tokens.fetch("thinking", 0).to_i
117
+ end
118
+
103
119
  # Returns the root span of this trace.
104
120
  #
105
121
  # @return [Hash, nil] The root span or nil
@@ -59,11 +59,23 @@
59
59
  <div class="mt-4">
60
60
  <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Span Timeline</h4>
61
61
  <div class="relative bg-gray-100 dark:bg-gray-800 rounded-lg p-4">
62
- <% total_duration = trace.total_duration_ms || 1 %>
62
+ <% total_duration = (trace.total_duration_ms || 1).to_f %>
63
+ <% total_duration = 1.0 if total_duration.zero? %>
64
+ <%
65
+ # Offsets relative to the root span's start. Span payloads carry
66
+ # absolute ISO8601 start_time values, not relative offsets.
67
+ root = trace.root_span || trace.spans.first || {}
68
+ root_start = Time.parse(root["start_time"]) rescue nil
69
+ %>
63
70
  <% trace.spans.each_with_index do |span, index| %>
64
71
  <%
65
- start_pct = ((span["start_time_relative_ms"] || 0) / total_duration * 100).clamp(0, 100)
66
- width_pct = ((span["duration_ms"] || 0) / total_duration * 100).clamp(0.5, 100 - start_pct)
72
+ relative_ms = span["start_time_relative_ms"]
73
+ if relative_ms.nil? && root_start && span["start_time"].present?
74
+ span_start = Time.parse(span["start_time"]) rescue nil
75
+ relative_ms = ((span_start - root_start) * 1000).round(2) if span_start
76
+ end
77
+ start_pct = ((relative_ms || 0).to_f / total_duration * 100).clamp(0, 100)
78
+ width_pct = ((span["duration_ms"] || 0).to_f / total_duration * 100).clamp(0.5, [ 100 - start_pct, 0.5 ].max)
67
79
  span_type = span["type"] || "unknown"
68
80
  colors = {
69
81
  "root" => "bg-gray-400",
@@ -114,7 +114,9 @@
114
114
  <%= number_with_delimiter(stat.trace_count) %>
115
115
  </td>
116
116
  <td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
117
- <%= number_to_human(stat.total_tokens || 0, precision: 1) %>
117
+ <%# stat is a grouped row: read the SQL alias, not the model
118
+ method (which sums per-trace token columns absent here) %>
119
+ <%= number_to_human(stat[:total_tokens] || 0, precision: 1) %>
118
120
  </td>
119
121
  <td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
120
122
  <% avg = stat.avg_duration.to_f %>
@@ -69,11 +69,11 @@
69
69
  </div>
70
70
  <div class="ml-10 flex items-baseline space-x-4">
71
71
  <%= link_to "Traces",
72
- active_agent_dashboard.traces_path,
73
- class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == active_agent_dashboard.traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
72
+ traces_path,
73
+ class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
74
74
  <%= link_to "Metrics",
75
- active_agent_dashboard.metrics_traces_path,
76
- class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == active_agent_dashboard.metrics_traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
75
+ metrics_traces_path,
76
+ class: "px-3 py-2 rounded-md text-sm font-medium #{request.path == metrics_traces_path ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'}" %>
77
77
  </div>
78
78
  </div>
79
79
  <div class="text-sm text-gray-500 dark:text-gray-400">
@@ -1,78 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ActiveAgent::Dashboard::Engine.routes.draw do
4
- # Dashboard root
5
- root to: "dashboard#index"
6
-
7
- # Dashboard
4
+ # The functional dashboard surface: traces + metrics.
5
+ root to: "traces#index"
8
6
  get "dashboard", to: "dashboard#index"
9
7
 
10
- # Traces
11
8
  resources :traces, only: [ :index, :show ] do
12
9
  collection do
13
10
  get :metrics
14
11
  end
15
12
  end
16
13
 
17
- # Agents
18
- resources :agents do
19
- member do
20
- post :execute
21
- post :test
22
- get :versions
23
- post :restore_version
24
- end
25
- resources :runs, controller: "agent_runs", only: [ :index, :show ] do
26
- member do
27
- post :cancel
28
- end
29
- end
30
- end
31
-
32
- # Agent Templates
33
- resources :templates, only: [ :index, :show ] do
34
- member do
35
- post :create_agent
36
- end
37
- end
38
-
39
- # Sandbox Sessions
40
- resources :sandboxes, controller: "sandbox_sessions" do
41
- member do
42
- post :provision
43
- post :execute
44
- post :expire
45
- end
46
- resources :runs, controller: "sandbox_runs", only: [ :index, :show ]
47
- end
48
-
49
- # Session Recordings
50
- resources :recordings, controller: "session_recordings", only: [ :index, :show ] do
51
- member do
52
- get :timeline
53
- get :playback
54
- end
55
- end
56
-
57
- # API namespace
14
+ # Telemetry ingestion, relative to wherever the engine is mounted:
15
+ # <mount>/api/traces (e.g. /activeagents/api/traces at the default mount).
58
16
  namespace :api do
59
- namespace :v1 do
60
- # Telemetry ingestion
61
- resources :traces, only: [ :create ]
62
-
63
- # Agent execution API
64
- resources :agents, only: [ :index, :show ] do
65
- member do
66
- post :execute
67
- end
68
- end
69
-
70
- # Sandbox API
71
- resources :sandboxes, only: [ :create, :show ] do
72
- member do
73
- post :execute
74
- end
75
- end
76
- end
17
+ resources :traces, only: [ :create ]
77
18
  end
78
19
  end
@@ -4,36 +4,40 @@ module ActiveAgent
4
4
  module Dashboard
5
5
  # Rails Engine for the ActiveAgent Dashboard.
6
6
  #
7
- # Provides a full-featured dashboard for managing agents, viewing telemetry,
8
- # running sandboxes, and recording sessions.
7
+ # Provides the free self-hosted dashboard: telemetry traces, metrics,
8
+ # and the local trace ingestion API.
9
9
  #
10
10
  # Mount in your routes:
11
- # mount ActiveAgent::Dashboard::Engine => "/active_agent"
11
+ # mount ActiveAgent::Dashboard::Engine => "/activeagents"
12
12
  #
13
13
  class Engine < ::Rails::Engine
14
- isolate_namespace ActiveAgent::Dashboard
15
-
16
- # Use a unique engine name to avoid conflicts
17
- engine_name "active_agent"
14
+ # The engine lives at lib/active_agent/dashboard rather than the gem
15
+ # root. Rails derives an engine's load paths (app/models,
16
+ # app/controllers, app/jobs, app/views, config/routes.rb) from
17
+ # find_root when +config+ is first materialized — which
18
+ # isolate_namespace below triggers via +routes+ — so this override
19
+ # must be defined before anything touches config. Overriding only
20
+ # +root+ leaves the engine's classes off host applications' autoload
21
+ # paths entirely.
22
+ def self.find_root(_from)
23
+ root
24
+ end
18
25
 
19
- # Override engine root to point to the dashboard directory
20
- # This must be set before paths are computed
21
26
  def self.root
22
27
  @root ||= Pathname.new(File.expand_path("..", __FILE__))
23
28
  end
24
29
 
30
+ isolate_namespace ActiveAgent::Dashboard
31
+
32
+ # Use a unique engine name to avoid conflicts
33
+ engine_name "active_agent"
34
+
25
35
  # Alias for consistency with existing code
26
36
  def self.dashboard_root
27
37
  root
28
38
  end
29
39
 
30
40
  config.active_agent_dashboard = ActiveSupport::OrderedOptions.new
31
-
32
- initializer "active_agent.dashboard.append_view_paths" do
33
- ActiveSupport.on_load(:action_controller) do
34
- prepend_view_path ActiveAgent::Dashboard::Engine.root.join("app", "views").to_s
35
- end
36
- end
37
41
  end
38
42
  end
39
43
  end
@@ -8,7 +8,7 @@ module ActiveAgent
8
8
  # Mount the engine in your routes to access the full dashboard:
9
9
  #
10
10
  # # config/routes.rb
11
- # mount ActiveAgent::Dashboard::Engine => "/active_agent"
11
+ # mount ActiveAgent::Dashboard::Engine => "/activeagents"
12
12
  #
13
13
  # The dashboard provides:
14
14
  # - Agent management: Create, edit, version, and execute agents
@@ -91,7 +91,16 @@ module ActiveAgent
91
91
  # @return [Object, nil] Object responding to #signed_url_for and #fetch_snapshot
92
92
  attr_accessor :storage_service
93
93
 
94
- # Base controller class for dashboard controllers
94
+ # Bearer token required by the ingest API in single-tenant mode. When
95
+ # unset the local ingest endpoint accepts unauthenticated posts, so set
96
+ # it whenever the mount is reachable beyond your own machine.
97
+ # (Multi-tenant mode authenticates per-account keys instead.)
98
+ # @return [String, nil]
99
+ attr_accessor :ingest_api_key
100
+
101
+ # @deprecated Never consumed — dashboard controllers inherit
102
+ # ActionController::Base. Retained as a no-op so existing
103
+ # initializers that set it keep booting; remove in the next major.
95
104
  # @return [String]
96
105
  attr_accessor :base_controller_class
97
106
 
@@ -141,7 +150,8 @@ module ActiveAgent
141
150
  @sandbox_service = :local
142
151
  @sandbox_limits = nil
143
152
  @storage_service = nil
144
- @base_controller_class = "ActionController::Base"
153
+ @ingest_api_key = nil
154
+ @base_controller_class = "ActionController::Base" # deprecated no-op
145
155
  end
146
156
  end
147
157