actionagent 1.9.3 → 1.9.5
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/README.md +9 -1
- data/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +61 -61
- data/app/controllers/action_agent/api/agents_controller.rb +1 -17
- data/app/controllers/action_agent/api/evaluation_reports_controller.rb +11 -7
- data/app/controllers/action_agent/api/github_app_webhooks_controller.rb +98 -0
- data/app/controllers/action_agent/api/github_installations_controller.rb +9 -1
- data/app/controllers/action_agent/api/mcp_controller.rb +5 -2
- data/app/controllers/action_agent/api/projects_controller.rb +119 -40
- data/app/controllers/action_agent/dashboard_controller.rb +27 -0
- data/app/controllers/action_agent/traces_controller.rb +6 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +199 -24
- data/app/jobs/action_agent/github_webhook_job.rb +102 -0
- data/app/models/action_agent/github_connection.rb +21 -0
- data/app/models/action_agent/github_installation.rb +28 -1
- data/app/models/action_agent/github_webhook_delivery.rb +45 -0
- data/app/models/action_agent/provider_key.rb +8 -1
- data/app/models/action_agent/scenario_catalog.rb +8 -2
- data/app/serializers/action_agent/agent_version_serializer.rb +29 -0
- data/app/serializers/action_agent/evaluation_serializer.rb +9 -0
- data/app/services/action_agent/evaluation_report_import.rb +112 -19
- data/app/services/action_agent/github_client.rb +11 -2
- data/app/services/action_agent/local_sandbox_claude_login.rb +1 -1
- data/app/services/action_agent/project_capabilities.rb +6 -3
- data/app/views/action_agent/traces/_styles.html.erb +45 -0
- data/app/views/action_agent/traces/_summary.html.erb +8 -0
- data/app/views/action_agent/traces/_trace_detail.html.erb +33 -96
- data/app/views/action_agent/traces/index.html.erb +25 -134
- data/app/views/action_agent/traces/metrics.html.erb +22 -140
- data/app/views/action_agent/traces/show.html.erb +7 -36
- data/app/views/layouts/action_agent/application.html.erb +26 -86
- data/config/routes.rb +15 -0
- data/lib/action_agent/claude_login_process.rb +1 -1
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +47 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +10 -3
- data/lib/generators/action_agent/templates/migrations/029_create_active_agent_github_webhook_deliveries.rb.erb +23 -0
- metadata +9 -2
|
@@ -1,135 +1,26 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Avg Duration</div>
|
|
22
|
-
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
23
|
-
<%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
29
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Error Rate</div>
|
|
30
|
-
<div class="mt-1 text-2xl font-semibold <%= @metrics[:error_rate] > 5 ? 'text-red-600' : 'text-gray-900 dark:text-white' %>">
|
|
31
|
-
<%= @metrics[:error_rate] %>%
|
|
32
|
-
</div>
|
|
33
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
37
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Agents</div>
|
|
38
|
-
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
39
|
-
<%= @metrics[:unique_agents] %>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- Traces Table -->
|
|
46
|
-
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden" data-controller="traces">
|
|
47
|
-
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
|
48
|
-
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Recent Traces</h2>
|
|
49
|
-
<div class="flex gap-2">
|
|
50
|
-
<%= link_to "All", traces_path, class: "px-3 py-1 text-sm rounded #{params[:status].blank? ? 'bg-indigo-100 text-indigo-700' : 'text-gray-600 hover:bg-gray-100'}" %>
|
|
51
|
-
<%= link_to "Errors", traces_path(status: "error"), class: "px-3 py-1 text-sm rounded #{params[:status] == 'error' ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'}" %>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<% if @traces.any? %>
|
|
56
|
-
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
57
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
58
|
-
<tr>
|
|
59
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
|
|
60
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
61
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
62
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
|
63
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
|
64
|
-
</tr>
|
|
65
|
-
</thead>
|
|
66
|
-
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
67
|
-
<% @traces.each do |trace| %>
|
|
68
|
-
<!-- Main Row -->
|
|
69
|
-
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
|
70
|
-
data-trace-id="<%= trace.id %>"
|
|
71
|
-
data-action="click->traces#toggle">
|
|
72
|
-
<td class="px-4 py-3">
|
|
73
|
-
<div class="flex items-center">
|
|
74
|
-
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
75
|
-
<%= trace.agent_class || "Unknown" %>
|
|
76
|
-
</span>
|
|
77
|
-
<% if trace.agent_action %>
|
|
78
|
-
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
|
79
|
-
.<%= trace.agent_action %>
|
|
80
|
-
</span>
|
|
81
|
-
<% end %>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="text-xs text-gray-400 font-mono"><%= trace.trace_id&.first(8) %></div>
|
|
84
|
-
</td>
|
|
85
|
-
<td class="px-4 py-3">
|
|
86
|
-
<% if trace.error? %>
|
|
87
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
|
88
|
-
Error
|
|
89
|
-
</span>
|
|
90
|
-
<% else %>
|
|
91
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
92
|
-
OK
|
|
93
|
-
</span>
|
|
94
|
-
<% end %>
|
|
95
|
-
</td>
|
|
96
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
97
|
-
<%= trace.formatted_duration %>
|
|
98
|
-
</td>
|
|
99
|
-
<td class="px-4 py-3">
|
|
100
|
-
<div class="flex items-center gap-2 text-sm">
|
|
101
|
-
<span class="text-gray-900 dark:text-white"><%= trace.formatted_tokens %></span>
|
|
102
|
-
<% if trace.total_thinking_tokens.to_i > 0 %>
|
|
103
|
-
<span class="text-xs text-purple-600 dark:text-purple-400" title="Thinking tokens">
|
|
104
|
-
+<%= number_to_human(trace.total_thinking_tokens, precision: 1) %> thinking
|
|
105
|
-
</span>
|
|
106
|
-
<% end %>
|
|
107
|
-
</div>
|
|
108
|
-
</td>
|
|
109
|
-
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
110
|
-
<%= time_ago_in_words(trace.timestamp || trace.created_at) %> ago
|
|
111
|
-
</td>
|
|
112
|
-
</tr>
|
|
113
|
-
|
|
114
|
-
<!-- Expanded Detail Row -->
|
|
115
|
-
<tr id="detail-<%= trace.id %>" class="hidden bg-gray-50 dark:bg-gray-900">
|
|
116
|
-
<td colspan="5" class="px-4 py-4">
|
|
117
|
-
<%= render partial: "trace_detail", locals: { trace: trace } %>
|
|
118
|
-
</td>
|
|
119
|
-
</tr>
|
|
120
|
-
<% end %>
|
|
121
|
-
</tbody>
|
|
122
|
-
</table>
|
|
123
|
-
<% else %>
|
|
124
|
-
<div class="px-4 py-12 text-center">
|
|
125
|
-
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
126
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
127
|
-
</svg>
|
|
128
|
-
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No traces yet</h3>
|
|
129
|
-
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
130
|
-
Traces will appear here when your agents generate responses.
|
|
131
|
-
</p>
|
|
132
|
-
</div>
|
|
1
|
+
<h1>Traces</h1>
|
|
2
|
+
<%= render "summary" %>
|
|
3
|
+
<section class="panel" aria-label="Recent traces">
|
|
4
|
+
<h2>Recent Traces</h2>
|
|
5
|
+
<p><%= link_to "All", traces_path %> · <%= link_to "Errors", traces_path(status: "error") %></p>
|
|
6
|
+
<% if @traces.any? %>
|
|
7
|
+
<% @traces.each do |trace| %>
|
|
8
|
+
<details class="panel">
|
|
9
|
+
<summary>
|
|
10
|
+
<span class="trace-summary">
|
|
11
|
+
<strong><%= trace.display_name %></strong>
|
|
12
|
+
<span class="badge <%= trace.error? ? 'error' : 'success' %>"><%= trace.error? ? "Error" : "OK" %></span>
|
|
13
|
+
<span><%= trace.formatted_duration %></span>
|
|
14
|
+
<span><%= trace.formatted_tokens %> tokens</span>
|
|
15
|
+
<span class="muted"><%= time_ago_in_words(trace.timestamp || trace.created_at) %> ago</span>
|
|
16
|
+
</span>
|
|
17
|
+
</summary>
|
|
18
|
+
<%= link_to "Open trace", trace_path(trace) %>
|
|
19
|
+
<%= render "trace_detail", trace: trace %>
|
|
20
|
+
</details>
|
|
133
21
|
<% end %>
|
|
134
|
-
|
|
135
|
-
</
|
|
22
|
+
<% else %>
|
|
23
|
+
<h2>No traces yet</h2>
|
|
24
|
+
<p>Traces will appear here when your agents generate responses.</p>
|
|
25
|
+
<% end %>
|
|
26
|
+
</section>
|
|
@@ -1,145 +1,27 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</svg>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="ml-4">
|
|
14
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Traces</p>
|
|
15
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
16
|
-
<%= number_with_delimiter(@metrics[:total_traces]) %>
|
|
17
|
-
</p>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
23
|
-
<div class="flex items-center">
|
|
24
|
-
<div class="p-3 rounded-full bg-blue-100 dark:bg-blue-900">
|
|
25
|
-
<svg class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
26
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
27
|
-
</svg>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="ml-4">
|
|
30
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Tokens</p>
|
|
31
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
32
|
-
<%= number_to_human(@metrics[:total_tokens], precision: 2) %>
|
|
33
|
-
</p>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
39
|
-
<div class="flex items-center">
|
|
40
|
-
<div class="p-3 rounded-full bg-green-100 dark:bg-green-900">
|
|
41
|
-
<svg class="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
42
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
43
|
-
</svg>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="ml-4">
|
|
46
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Avg Duration</p>
|
|
47
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
48
|
-
<%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %>
|
|
49
|
-
</p>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
55
|
-
<div class="flex items-center">
|
|
56
|
-
<div class="p-3 rounded-full <%= @metrics[:error_rate] > 5 ? 'bg-red-100 dark:bg-red-900' : 'bg-gray-100 dark:bg-gray-700' %>">
|
|
57
|
-
<svg class="h-6 w-6 <%= @metrics[:error_rate] > 5 ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400' %>" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
58
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
59
|
-
</svg>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="ml-4">
|
|
62
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Error Rate</p>
|
|
63
|
-
<p class="text-2xl font-semibold <%= @metrics[:error_rate] > 5 ? 'text-red-600' : 'text-gray-900 dark:text-white' %>">
|
|
64
|
-
<%= @metrics[:error_rate] %>%
|
|
65
|
-
</p>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
71
|
-
<div class="flex items-center">
|
|
72
|
-
<div class="p-3 rounded-full bg-purple-100 dark:bg-purple-900">
|
|
73
|
-
<svg class="h-6 w-6 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
74
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
75
|
-
</svg>
|
|
76
|
-
</div>
|
|
77
|
-
<div class="ml-4">
|
|
78
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Agents</p>
|
|
79
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
80
|
-
<%= @metrics[:unique_agents] %>
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Agent Statistics -->
|
|
88
|
-
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden mb-8">
|
|
89
|
-
<div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
90
|
-
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Agent Statistics</h2>
|
|
91
|
-
<p class="text-sm text-gray-500 dark:text-gray-400">Performance breakdown by agent class (last 24h)</p>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
<% if @agent_stats.any? %>
|
|
95
|
-
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
96
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
97
|
-
<tr>
|
|
98
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
|
|
99
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Traces</th>
|
|
100
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
|
101
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Duration</th>
|
|
102
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Errors</th>
|
|
103
|
-
</tr>
|
|
104
|
-
</thead>
|
|
105
|
-
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
1
|
+
<h1>Metrics Overview</h1>
|
|
2
|
+
<%= render "summary" %>
|
|
3
|
+
<section class="panel">
|
|
4
|
+
<h2>Agent Statistics</h2>
|
|
5
|
+
<p class="muted">Performance breakdown by agent class (last 24h)</p>
|
|
6
|
+
<% if @agent_stats.any? %>
|
|
7
|
+
<div class="table-scroll">
|
|
8
|
+
<table>
|
|
9
|
+
<thead><tr><th scope="col">Agent</th><th scope="col">Traces</th><th scope="col">Tokens</th><th scope="col">Avg Duration</th><th scope="col">Errors</th></tr></thead>
|
|
10
|
+
<tbody>
|
|
106
11
|
<% @agent_stats.each do |stat| %>
|
|
107
|
-
<tr
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<td class="
|
|
114
|
-
<%= number_with_delimiter(stat.trace_count) %>
|
|
115
|
-
</td>
|
|
116
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
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) %>
|
|
120
|
-
</td>
|
|
121
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
122
|
-
<% avg = stat.avg_duration.to_f %>
|
|
123
|
-
<%= avg >= 1000 ? "#{(avg / 1000.0).round(2)}s" : "#{avg.round(0)}ms" %>
|
|
124
|
-
</td>
|
|
125
|
-
<td class="px-4 py-3">
|
|
126
|
-
<% error_count = stat.error_count.to_i %>
|
|
127
|
-
<% if error_count > 0 %>
|
|
128
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
|
129
|
-
<%= error_count %>
|
|
130
|
-
</span>
|
|
131
|
-
<% else %>
|
|
132
|
-
<span class="text-sm text-gray-400">0</span>
|
|
133
|
-
<% end %>
|
|
134
|
-
</td>
|
|
12
|
+
<tr>
|
|
13
|
+
<th scope="row"><%= stat.agent_class || "Unknown" %></th>
|
|
14
|
+
<td><%= number_with_delimiter(stat.trace_count) %></td>
|
|
15
|
+
<td><%= number_to_human(stat[:total_tokens] || 0, precision: 1) %></td>
|
|
16
|
+
<% avg = stat.avg_duration.to_f %>
|
|
17
|
+
<td><%= avg >= 1000 ? "#{(avg / 1000.0).round(2)}s" : "#{avg.round(0)}ms" %></td>
|
|
18
|
+
<td class="<%= 'error' if stat.error_count.to_i > 0 %>"><%= stat.error_count.to_i %></td>
|
|
135
19
|
</tr>
|
|
136
20
|
<% end %>
|
|
137
21
|
</tbody>
|
|
138
22
|
</table>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<% else %>
|
|
25
|
+
<p>No agent data available yet.</p>
|
|
26
|
+
<% end %>
|
|
27
|
+
</section>
|
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<div>
|
|
9
|
-
<h1 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
10
|
-
<%= @trace.display_name %>
|
|
11
|
-
</h1>
|
|
12
|
-
<p class="text-sm text-gray-500 dark:text-gray-400 font-mono">
|
|
13
|
-
<%= @trace.trace_id %>
|
|
14
|
-
</p>
|
|
15
|
-
</div>
|
|
16
|
-
<div class="flex items-center gap-4">
|
|
17
|
-
<% if @trace.error? %>
|
|
18
|
-
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
|
19
|
-
Error
|
|
20
|
-
</span>
|
|
21
|
-
<% else %>
|
|
22
|
-
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
23
|
-
Success
|
|
24
|
-
</span>
|
|
25
|
-
<% end %>
|
|
26
|
-
<span class="text-sm text-gray-500">
|
|
27
|
-
<%= @trace.timestamp&.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
28
|
-
</span>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<div class="p-4">
|
|
34
|
-
<%= render partial: "trace_detail", locals: { trace: @trace } %>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
1
|
+
<p><%= link_to "Back to traces", traces_path %></p>
|
|
2
|
+
<article class="panel">
|
|
3
|
+
<h1><%= @trace.display_name %></h1>
|
|
4
|
+
<p class="badge <%= @trace.error? ? 'error' : 'success' %>"><%= @trace.error? ? "Error" : "Success" %></p>
|
|
5
|
+
<p><time><%= @trace.timestamp&.strftime("%Y-%m-%d %H:%M:%S") %></time></p>
|
|
6
|
+
<%= render "trace_detail", trace: @trace %>
|
|
7
|
+
</article>
|
|
@@ -1,94 +1,34 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>ActiveAgent
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
[data-turbo-preview] { opacity: 0.5; }
|
|
12
|
-
.span-bar { transition: all 0.2s ease; }
|
|
13
|
-
.span-bar:hover { transform: scaleY(1.1); }
|
|
14
|
-
</style>
|
|
15
|
-
<script>
|
|
16
|
-
// Initialize Stimulus
|
|
17
|
-
const application = Stimulus.Application.start();
|
|
18
|
-
|
|
19
|
-
// Traces controller for expandable rows
|
|
20
|
-
application.register("traces", class extends Stimulus.Controller {
|
|
21
|
-
static targets = ["row", "detail"]
|
|
22
|
-
|
|
23
|
-
toggle(event) {
|
|
24
|
-
const traceId = event.currentTarget.dataset.traceId;
|
|
25
|
-
const detail = document.getElementById(`detail-${traceId}`);
|
|
26
|
-
if (detail) {
|
|
27
|
-
detail.classList.toggle("hidden");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Auto-refresh controller
|
|
33
|
-
application.register("refresh", class extends Stimulus.Controller {
|
|
34
|
-
static values = { interval: Number }
|
|
35
|
-
|
|
36
|
-
connect() {
|
|
37
|
-
if (this.intervalValue > 0) {
|
|
38
|
-
this.startRefresh();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
disconnect() {
|
|
43
|
-
this.stopRefresh();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
startRefresh() {
|
|
47
|
-
this.timer = setInterval(() => {
|
|
48
|
-
Turbo.visit(window.location.href, { action: "replace" });
|
|
49
|
-
}, this.intervalValue);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
stopRefresh() {
|
|
53
|
-
if (this.timer) clearInterval(this.timer);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
</script>
|
|
6
|
+
<title>ActiveAgent Trace Console</title>
|
|
7
|
+
<% if @refresh_interval %>
|
|
8
|
+
<meta http-equiv="refresh" content="<%= @refresh_interval %>">
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= render "action_agent/traces/styles" %>
|
|
57
11
|
</head>
|
|
58
|
-
<body class="
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<div class="flex-shrink-0">
|
|
66
|
-
<span class="text-xl font-bold text-indigo-600 dark:text-indigo-400">
|
|
67
|
-
ActiveAgent
|
|
68
|
-
</span>
|
|
69
|
-
</div>
|
|
70
|
-
<div class="ml-10 flex items-baseline space-x-4">
|
|
71
|
-
<%= link_to "Traces",
|
|
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
|
-
<%= link_to "Metrics",
|
|
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
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
80
|
-
Local Telemetry
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
12
|
+
<body class="trace-console">
|
|
13
|
+
<header>
|
|
14
|
+
<nav aria-label="Trace console">
|
|
15
|
+
<strong>ActiveAgent</strong>
|
|
16
|
+
<%= link_to "Traces", traces_path %>
|
|
17
|
+
<%= link_to "Metrics", metrics_traces_path %>
|
|
18
|
+
<span class="muted">Local Telemetry</span>
|
|
84
19
|
</nav>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
<%=
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
20
|
+
</header>
|
|
21
|
+
<main>
|
|
22
|
+
<div class="refresh-controls">
|
|
23
|
+
<% if @refresh_interval %>
|
|
24
|
+
<span>Refreshes every <%= @refresh_interval %> seconds.</span>
|
|
25
|
+
<%= link_to "Stop refreshing", "#{request.path}?#{request.query_parameters.except('refresh').to_query}" %>
|
|
26
|
+
<% else %>
|
|
27
|
+
<%= link_to "Auto-refresh", "#{request.path}?#{request.query_parameters.merge('refresh' => '30').to_query}" %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<%= link_to "Refresh now", request.path + (request.query_string.present? ? "?#{request.query_string}" : "") %>
|
|
30
|
+
</div>
|
|
31
|
+
<%= yield %>
|
|
32
|
+
</main>
|
|
93
33
|
</body>
|
|
94
34
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -134,6 +134,7 @@ ActionAgent::Engine.routes.draw do
|
|
|
134
134
|
resources :projects, only: [ :index, :show, :create, :update, :destroy ] do
|
|
135
135
|
collection do
|
|
136
136
|
get :capabilities
|
|
137
|
+
get :repositories
|
|
137
138
|
get :preflight
|
|
138
139
|
get :discover_secrets
|
|
139
140
|
end
|
|
@@ -318,6 +319,11 @@ ActionAgent::Engine.routes.draw do
|
|
|
318
319
|
end
|
|
319
320
|
end
|
|
320
321
|
|
|
322
|
+
# The GitHub App's webhook deliveries. Authenticated by GitHub's
|
|
323
|
+
# signature (github_app_webhook_secret), not a session; the App's
|
|
324
|
+
# Webhook URL is <mount>/api/github_app/webhooks.
|
|
325
|
+
post "github_app/webhooks", to: "github_app_webhooks#create", as: :github_app_webhooks
|
|
326
|
+
|
|
321
327
|
# Creating the GitHub App from a manifest on a self-hosted dashboard.
|
|
322
328
|
resource :github_app_manifest, only: [ :create ] do
|
|
323
329
|
get :callback
|
|
@@ -365,6 +371,15 @@ ActionAgent::Engine.routes.draw do
|
|
|
365
371
|
# data. See SessionPlayerController.
|
|
366
372
|
get "session_player", to: "session_player#show", as: :session_player
|
|
367
373
|
|
|
374
|
+
# Shareable evaluation links: an evaluation, one of its runs, or one
|
|
375
|
+
# scenario result of a run. The React app reads these paths like any other
|
|
376
|
+
# client-side route; they are matched here first so the page itself is
|
|
377
|
+
# checked against the caller's scope, and a link to an evaluation outside
|
|
378
|
+
# it answers 404 whether or not the evaluation exists.
|
|
379
|
+
get "evaluations/:evaluation_id(/runs/:run_id(/results/:result_id))", to: "dashboard#evaluation",
|
|
380
|
+
as: :evaluation_link, format: false,
|
|
381
|
+
constraints: { evaluation_id: /\d+/, run_id: /\d+/, result_id: /\d+/ }
|
|
382
|
+
|
|
368
383
|
# Everything else under the mount is a client-side route: render the
|
|
369
384
|
# dashboard and let the browser resolve it. Anchored last so it can only
|
|
370
385
|
# ever catch what the routes above did not, and refuses /api paths so a
|
|
@@ -76,7 +76,7 @@ begin
|
|
|
76
76
|
buffer << chunk.force_encoding("UTF-8").scrub
|
|
77
77
|
buffer = buffer[-32_768..] || buffer
|
|
78
78
|
clean = buffer.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
79
|
-
url = clean[%r{https://(?:claude\.ai|platform\.claude\.com)/oauth/authorize\?[^\s\e]+}]
|
|
79
|
+
url = clean[%r{https://(?:claude\.ai|claude\.com|platform\.claude\.com)/(?:cai/)?oauth/authorize\?[^\s\e\a]+}]
|
|
80
80
|
if url && URI.parse(url).query.to_s.include?("client_id=")
|
|
81
81
|
write_status.call("awaiting_code", url)
|
|
82
82
|
end
|
data/lib/action_agent/version.rb
CHANGED
data/lib/action_agent.rb
CHANGED
|
@@ -751,6 +751,16 @@ module ActionAgent
|
|
|
751
751
|
# @return [String, nil]
|
|
752
752
|
attr_accessor :sign_out_path
|
|
753
753
|
|
|
754
|
+
# Items the host app adds to the header's user menu, above "Sign out":
|
|
755
|
+
# its workspace and member pages, its account settings, and for platform
|
|
756
|
+
# admins its admin pages. A Proc receives the signed-in user and the
|
|
757
|
+
# current owner and returns an Array of { label:, href: } Hashes (an
|
|
758
|
+
# Array is used as is), so the host decides per user what to show. The
|
|
759
|
+
# engine has no pages of its own to link here; unset, or when the Proc
|
|
760
|
+
# raises (logged), the menu holds "Sign out" alone.
|
|
761
|
+
# @return [Proc, Array<Hash>, nil]
|
|
762
|
+
attr_accessor :user_menu_links
|
|
763
|
+
|
|
754
764
|
# Where a browser is sent when it asks for a dashboard page without a
|
|
755
765
|
# valid session — the host app's sign-in page. Unset, an unauthenticated
|
|
756
766
|
# page request gets a minimal session-expired page instead of a bare
|
|
@@ -857,6 +867,16 @@ module ActionAgent
|
|
|
857
867
|
attr_writer :github_app_id, :github_app_private_key, :github_app_slug,
|
|
858
868
|
:github_app_client_id, :github_app_client_secret
|
|
859
869
|
|
|
870
|
+
# The secret GitHub signs the App's webhook deliveries with, which
|
|
871
|
+
# POST <mount>/api/github_app/webhooks verifies. Falls back to
|
|
872
|
+
# GITHUB_APP_WEBHOOK_SECRET. Not one of the settings
|
|
873
|
+
# github_app_configured? needs: without it the endpoint answers 404 and
|
|
874
|
+
# the dashboard learns of installation changes only when a token mint is
|
|
875
|
+
# refused. Set the App's Webhook URL to <mount>/api/github_app/webhooks
|
|
876
|
+
# and subscribe it to Installation and Installation repositories.
|
|
877
|
+
# @return [String, nil]
|
|
878
|
+
attr_writer :github_app_webhook_secret
|
|
879
|
+
|
|
860
880
|
# MCP servers the host app itself serves or connects, appended to the
|
|
861
881
|
# built-in catalog (MCPCatalog) so the MCP Services view lists them and
|
|
862
882
|
# telemetry traffic attributes to them. Each entry is a hash shaped like
|
|
@@ -961,6 +981,10 @@ module ActionAgent
|
|
|
961
981
|
@github_app_client_secret.presence || ENV["GITHUB_APP_CLIENT_SECRET"].presence
|
|
962
982
|
end
|
|
963
983
|
|
|
984
|
+
def github_app_webhook_secret
|
|
985
|
+
@github_app_webhook_secret.presence || ENV["GITHUB_APP_WEBHOOK_SECRET"].presence
|
|
986
|
+
end
|
|
987
|
+
|
|
964
988
|
# Whether the GitHub App installation flow can run on this install: every
|
|
965
989
|
# github_app_* setting is present.
|
|
966
990
|
# @return [Boolean]
|
|
@@ -1183,6 +1207,27 @@ module ActionAgent
|
|
|
1183
1207
|
|
|
1184
1208
|
# The tenant +owner+ belongs to. Identity unless the host app says
|
|
1185
1209
|
# otherwise, which is right for every single-tenant install.
|
|
1210
|
+
# The host's user menu items for +user+ in +owner+'s dashboard, as
|
|
1211
|
+
# [{ label:, href: }] with blank or malformed entries dropped. Empty when
|
|
1212
|
+
# user_menu_links is unset or raises, which is logged, not raised: a
|
|
1213
|
+
# broken menu seam must not take the dashboard down with it.
|
|
1214
|
+
# @return [Array<Hash{Symbol => String}>]
|
|
1215
|
+
def user_menu_links_for(user, owner)
|
|
1216
|
+
links = user_menu_links
|
|
1217
|
+
links = links.call(user, owner) if links.respond_to?(:call)
|
|
1218
|
+
Array(links).filter_map do |link|
|
|
1219
|
+
label = link[:label] || link["label"]
|
|
1220
|
+
href = link[:href] || link["href"]
|
|
1221
|
+
next if label.blank? || href.blank?
|
|
1222
|
+
next unless href.to_s.match?(%r{\A(/|https?://)})
|
|
1223
|
+
|
|
1224
|
+
{ label: label.to_s, href: href.to_s }
|
|
1225
|
+
end
|
|
1226
|
+
rescue StandardError => error
|
|
1227
|
+
Rails.logger.warn("[ActionAgent] user_menu_links failed: #{error.class}: #{error.message}")
|
|
1228
|
+
[]
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1186
1231
|
def tenant_for(owner)
|
|
1187
1232
|
return owner if tenant_resolver.nil?
|
|
1188
1233
|
|
|
@@ -1303,12 +1348,14 @@ module ActionAgent
|
|
|
1303
1348
|
@github_app_slug = nil
|
|
1304
1349
|
@github_app_client_id = nil
|
|
1305
1350
|
@github_app_client_secret = nil
|
|
1351
|
+
@github_app_webhook_secret = nil
|
|
1306
1352
|
@trace_retention = nil
|
|
1307
1353
|
@trace_owner_resolver = nil
|
|
1308
1354
|
@usage_recorder = nil
|
|
1309
1355
|
@usage_resolver = nil
|
|
1310
1356
|
@upgrade_url = nil
|
|
1311
1357
|
@sign_out_path = nil
|
|
1358
|
+
@user_menu_links = nil
|
|
1312
1359
|
@sign_in_path = nil
|
|
1313
1360
|
@mcp_catalog = []
|
|
1314
1361
|
@agent_actor_resolver = nil
|