actionagent 0.0.0 → 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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +103 -0
  4. data/app/assets/builds/action_agent.css +2 -0
  5. data/app/assets/builds/action_agent.js +163 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +128 -0
  7. data/app/controllers/action_agent/api/agents_controller.rb +411 -0
  8. data/app/controllers/action_agent/api/analytics_controller.rb +94 -0
  9. data/app/controllers/action_agent/api/api_keys_controller.rb +45 -0
  10. data/app/controllers/action_agent/api/base_controller.rb +112 -0
  11. data/app/controllers/action_agent/api/evaluations_controller.rb +148 -0
  12. data/app/controllers/action_agent/api/instance_tiers_controller.rb +106 -0
  13. data/app/controllers/action_agent/api/interactions_controller.rb +197 -0
  14. data/app/controllers/action_agent/api/mcp_controller.rb +218 -0
  15. data/app/controllers/action_agent/api/mcp_servers_controller.rb +156 -0
  16. data/app/controllers/action_agent/api/metrics_controller.rb +178 -0
  17. data/app/controllers/action_agent/api/provider_keys_controller.rb +52 -0
  18. data/app/controllers/action_agent/api/provider_models_controller.rb +119 -0
  19. data/app/controllers/action_agent/api/sandboxes_controller.rb +224 -0
  20. data/app/controllers/action_agent/api/session_recordings_controller.rb +372 -0
  21. data/app/controllers/action_agent/api/templates_controller.rb +94 -0
  22. data/app/controllers/action_agent/api/tools_controller.rb +58 -0
  23. data/app/controllers/action_agent/api/trace_reports_controller.rb +68 -0
  24. data/app/controllers/action_agent/api/traces_controller.rb +136 -0
  25. data/app/controllers/action_agent/application_controller.rb +105 -0
  26. data/app/controllers/action_agent/dashboard_controller.rb +104 -0
  27. data/app/controllers/action_agent/traces_controller.rb +121 -0
  28. data/app/jobs/action_agent/agent_execution_job.rb +52 -0
  29. data/app/jobs/action_agent/application_job.rb +12 -0
  30. data/app/jobs/action_agent/process_telemetry_traces_job.rb +86 -0
  31. data/app/jobs/action_agent/sandbox_cleanup_job.rb +42 -0
  32. data/app/jobs/action_agent/sandbox_provision_job.rb +56 -0
  33. data/app/jobs/action_agent/sandbox_run_job.rb +285 -0
  34. data/app/jobs/action_agent/trace_retention_job.rb +57 -0
  35. data/app/models/action_agent/agent.rb +343 -0
  36. data/app/models/action_agent/agent_context.rb +129 -0
  37. data/app/models/action_agent/agent_generation.rb +48 -0
  38. data/app/models/action_agent/agent_memory.rb +50 -0
  39. data/app/models/action_agent/agent_memory_entry.rb +14 -0
  40. data/app/models/action_agent/agent_message.rb +43 -0
  41. data/app/models/action_agent/agent_run.rb +151 -0
  42. data/app/models/action_agent/agent_template.rb +182 -0
  43. data/app/models/action_agent/agent_version.rb +48 -0
  44. data/app/models/action_agent/api_key.rb +53 -0
  45. data/app/models/action_agent/application_record.rb +27 -0
  46. data/app/models/action_agent/evaluation.rb +80 -0
  47. data/app/models/action_agent/evaluation_run.rb +20 -0
  48. data/app/models/action_agent/model_pricing.rb +80 -0
  49. data/app/models/action_agent/provider_key.rb +60 -0
  50. data/app/models/action_agent/recording_action.rb +119 -0
  51. data/app/models/action_agent/recording_snapshot.rb +88 -0
  52. data/app/models/action_agent/sandbox_instance_tier.rb +368 -0
  53. data/app/models/action_agent/sandbox_run.rb +45 -0
  54. data/app/models/action_agent/sandbox_session.rb +160 -0
  55. data/app/models/action_agent/session_recording.rb +178 -0
  56. data/app/models/action_agent/telemetry_trace.rb +357 -0
  57. data/app/models/concerns/action_agent/adapter_aware.rb +50 -0
  58. data/app/models/concerns/action_agent/ownable.rb +86 -0
  59. data/app/models/concerns/action_agent/session_recordable.rb +91 -0
  60. data/app/queries/action_agent/agent_executions.rb +201 -0
  61. data/app/serializers/action_agent/agent_message_serializer.rb +23 -0
  62. data/app/serializers/action_agent/interaction_preview.rb +22 -0
  63. data/app/serializers/action_agent/telemetry_trace_serializer.rb +122 -0
  64. data/app/serializers/action_agent/trace_interaction_serializer.rb +246 -0
  65. data/app/services/action_agent/agent_execution_service.rb +572 -0
  66. data/app/services/action_agent/agent_registrar.rb +197 -0
  67. data/app/services/action_agent/agent_scorecard.rb +191 -0
  68. data/app/services/action_agent/agent_toolbox.rb +504 -0
  69. data/app/services/action_agent/evaluation_runner_service.rb +481 -0
  70. data/app/services/action_agent/mcp_catalog.rb +247 -0
  71. data/app/services/action_agent/mcp_recording_middleware.rb +241 -0
  72. data/app/services/action_agent/mock_sandbox_backend.rb +52 -0
  73. data/app/services/action_agent/playwright_mcp_client.rb +148 -0
  74. data/app/services/action_agent/sandbox_orchestrator.rb +242 -0
  75. data/app/services/action_agent/session_recording_service.rb +228 -0
  76. data/app/services/action_agent/tool_discovery.rb +617 -0
  77. data/app/views/action_agent/dashboard/index.html.erb +5 -0
  78. data/app/views/action_agent/traces/_trace_detail.html.erb +117 -0
  79. data/app/views/action_agent/traces/index.html.erb +135 -0
  80. data/app/views/action_agent/traces/metrics.html.erb +145 -0
  81. data/app/views/action_agent/traces/show.html.erb +36 -0
  82. data/app/views/layouts/action_agent/application.html.erb +94 -0
  83. data/app/views/layouts/action_agent/react.html.erb +19 -0
  84. data/config/routes.rb +144 -0
  85. data/lib/action_agent/compatibility.rb +49 -0
  86. data/lib/action_agent/engine.rb +51 -0
  87. data/lib/action_agent/version.rb +5 -0
  88. data/lib/action_agent.rb +388 -0
  89. data/lib/actionagent.rb +6 -0
  90. data/lib/generators/action_agent/install_generator.rb +137 -0
  91. data/lib/generators/action_agent/templates/action_agent.rb.erb +82 -0
  92. data/lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb +24 -0
  93. data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +319 -0
  94. data/lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb +58 -0
  95. metadata +209 -12
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module ActionAgent
7
+ # Generator for installing the ActiveAgent Dashboard.
8
+ #
9
+ # @example Run the generator
10
+ # rails generate action_agent:install
11
+ #
12
+ # This will:
13
+ # - Create the telemetry_traces table migration
14
+ # - Add mount directive to routes
15
+ # - Create initializer for dashboard configuration
16
+ #
17
+ class InstallGenerator < Rails::Generators::Base
18
+ include ActiveRecord::Generators::Migration
19
+
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ desc "Installs the ActiveAgent Dashboard with telemetry storage"
23
+
24
+ class_option :multi_tenant, type: :boolean, default: false,
25
+ desc: "Scope traces to an Account (adds account_id to the migration)"
26
+
27
+ class_option :skip_migrations, type: :boolean, default: false,
28
+ desc: "Skip copying the migrations"
29
+
30
+ class_option :traces_only, type: :boolean, default: false,
31
+ desc: "Install trace ingestion alone, without the agent/run/evaluation tables"
32
+
33
+ class_option :skip_routes, type: :boolean, default: false,
34
+ desc: "Skip adding the engine mount to routes.rb"
35
+
36
+ def copy_migrations
37
+ return if options[:skip_migrations]
38
+
39
+ # An app that installed the dashboard when it shipped inside activeagent
40
+ # already has this migration. Re-emitting it aborts the whole generator
41
+ # on a duplicate name, so upgrade that install in place instead: the
42
+ # table exists but predates agent attribution, and only agent_id is
43
+ # missing.
44
+ if existing_migration?("create_active_agent_telemetry_traces")
45
+ say_status :skip, "create_active_agent_telemetry_traces already exists", :yellow
46
+
47
+ unless existing_migration?("add_agent_id_to_active_agent_telemetry_traces")
48
+ migration_template(
49
+ "add_agent_id_to_active_agent_telemetry_traces.rb.erb",
50
+ "db/migrate/add_agent_id_to_active_agent_telemetry_traces.rb"
51
+ )
52
+ end
53
+ else
54
+ migration_template(
55
+ "create_active_agent_telemetry_traces.rb.erb",
56
+ "db/migrate/create_active_agent_telemetry_traces.rb"
57
+ )
58
+ end
59
+
60
+ # The rest of the dashboard — agents, runs, versions, conversations,
61
+ # evaluations, sandboxes, recordings, keys. Skippable for an app that
62
+ # only wants to be a trace sink.
63
+ return if options[:traces_only]
64
+
65
+ if existing_migration?("create_active_agent_dashboard_tables")
66
+ say_status :skip, "create_active_agent_dashboard_tables already exists", :yellow
67
+ return
68
+ end
69
+
70
+ migration_template(
71
+ "create_active_agent_dashboard_tables.rb.erb",
72
+ "db/migrate/create_active_agent_dashboard_tables.rb"
73
+ )
74
+ end
75
+
76
+ def add_route
77
+ return if options[:skip_routes]
78
+
79
+ # Rails' `route` action is only idempotent on an exact string match,
80
+ # so an app installed before the default path changed would get a
81
+ # second mount — and two unnamed mounts of the same engine raise
82
+ # "Invalid route name, already in use: 'active_agent'" at boot.
83
+ routes_file = File.join(destination_root, "config/routes.rb")
84
+ if File.exist?(routes_file) && File.read(routes_file).include?("ActionAgent::Engine")
85
+ say_status :skip, "engine already mounted in config/routes.rb", :yellow
86
+ return
87
+ end
88
+
89
+ route 'mount ActionAgent::Engine => "/activeagents"'
90
+ end
91
+
92
+ def create_initializer
93
+ template(
94
+ "action_agent.rb.erb",
95
+ "config/initializers/action_agent.rb"
96
+ )
97
+ end
98
+
99
+ def show_readme
100
+ say "\n"
101
+ say "ActiveAgent Dashboard installed successfully!", :green
102
+ say "\n"
103
+ say "Next steps:"
104
+ say " 1. Run migrations: rails db:migrate"
105
+ say " 2. Configure telemetry in config/active_agent.yml:"
106
+ say " telemetry:"
107
+ say " enabled: true"
108
+ say " local_storage: true"
109
+ say " 3. Visit /activeagents to view the dashboard"
110
+ unless options[:traces_only]
111
+ say "\n"
112
+ say "The dashboard stores API keys and provider credentials encrypted."
113
+ say "Run `rails db:encryption:init` and add the keys to your credentials"
114
+ say "before creating any."
115
+ end
116
+ say "\n"
117
+ end
118
+
119
+ private
120
+
121
+ def migration_version
122
+ "[#{ActiveRecord::Migration.current_version}]"
123
+ end
124
+
125
+ # Whether db/migrate already carries a migration with this name. Checked
126
+ # against the filesystem rather than the database so the generator still
127
+ # works before the database exists.
128
+ def existing_migration?(name)
129
+ Dir.glob(File.join(destination_root.to_s, "db", "migrate", "*_#{name}.rb")).any?
130
+ end
131
+
132
+ # Consumed by the migration template.
133
+ def multi_tenant?
134
+ options[:multi_tenant]
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveAgent Dashboard Configuration
4
+ #
5
+ # This file configures the ActiveAgent telemetry dashboard.
6
+ # The dashboard is mounted at /activeagents by default (see config/routes.rb).
7
+
8
+ ActionAgent.configure do |config|
9
+ # ==========================================================================
10
+ # Authentication (required in production)
11
+ # ==========================================================================
12
+ #
13
+ # Provide a lambda that receives the controller and performs
14
+ # authentication. Return false or raise to deny access. Without one, the
15
+ # dashboard serves a 403 in production — traces contain prompts, outputs
16
+ # and error messages.
17
+ #
18
+ # Basic auth:
19
+ # config.authentication_method = ->(controller) {
20
+ # controller.authenticate_or_request_with_http_basic do |username, password|
21
+ # username == "admin" && password == Rails.application.credentials.dashboard_password
22
+ # end
23
+ # }
24
+ #
25
+ # Devise:
26
+ # config.authentication_method = ->(controller) {
27
+ # controller.authenticate_admin!
28
+ # }
29
+ #
30
+ # No authentication (development only!):
31
+ # config.authentication_method = nil
32
+ #
33
+ config.authentication_method = nil
34
+
35
+ # ==========================================================================
36
+ # Ingest API authentication
37
+ # ==========================================================================
38
+ #
39
+ # Bearer token other apps must send when posting traces to the mounted
40
+ # ingest endpoint (/activeagents/api/traces). Leave unset only when the
41
+ # mount is not reachable beyond your own machine.
42
+ #
43
+ # config.ingest_api_key = Rails.application.credentials.dig(:active_agent, :ingest_api_key)
44
+
45
+ # ==========================================================================
46
+ # Multi-tenant mode (SaaS platforms only)
47
+ # ==========================================================================
48
+ #
49
+ # Scopes traces per account and authenticates ingest with per-account
50
+ # keys. Most self-hosted installs should leave this off.
51
+ #
52
+ # config.multi_tenant = true
53
+ # config.account_class = "Account"
54
+ # config.user_class = "User"
55
+ #
56
+ # How the engine finds the signed-in user and their account. These have to
57
+ # be resolvers rather than method names: the dashboard's controllers are
58
+ # their own base class, so your app's `current_user` helper is not on them.
59
+ # Read the session (or your auth library) directly — a resolver that calls
60
+ # back into `current_user` reaches the engine's own accessor, not yours.
61
+ #
62
+ # config.current_user_resolver = ->(controller) {
63
+ # User.find_by(id: controller.session[:user_id])
64
+ # }
65
+ # config.current_account_resolver = ->(controller) {
66
+ # controller.send(:current_user)&.account
67
+ # }
68
+ #
69
+ # With an owner model configured, an owner that does not resolve sees
70
+ # nothing rather than everything. An empty dashboard for a signed-in user
71
+ # means the resolver above returned nil.
72
+
73
+ # ==========================================================================
74
+ # UI
75
+ # ==========================================================================
76
+ #
77
+ # Custom layout for dashboard views (the default layout loads Tailwind,
78
+ # Turbo and Stimulus from CDNs — override for CSP-strict or air-gapped
79
+ # environments).
80
+ #
81
+ # config.layout = "application"
82
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tops up a telemetry_traces table created before traces carried an agent
4
+ # attribution. Emitted instead of the create-table migration when the install
5
+ # generator finds one already in db/migrate.
6
+ #
7
+ # Without agent_id, AgentRegistrar cannot attribute an ingested trace to a
8
+ # dashboard agent: auto-registration silently does nothing, and the dashboard
9
+ # home page fails as soon as the first agent exists.
10
+ class AddAgentIdToActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_version %>
11
+ def up
12
+ return unless table_exists?(:active_agent_telemetry_traces)
13
+ return if column_exists?(:active_agent_telemetry_traces, :agent_id)
14
+
15
+ add_column :active_agent_telemetry_traces, :agent_id, :bigint
16
+ add_index :active_agent_telemetry_traces, :agent_id
17
+ end
18
+
19
+ def down
20
+ return unless column_exists?(:active_agent_telemetry_traces, :agent_id)
21
+
22
+ remove_column :active_agent_telemetry_traces, :agent_id
23
+ end
24
+ end
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates every table the ActiveAgent Dashboard reads and writes, apart from
4
+ # active_agent_telemetry_traces (which ships separately so an app can install
5
+ # trace ingestion alone).
6
+ #
7
+ # Table names follow ActionAgent.table_name_prefix, so an app that
8
+ # has deliberately set the prefix to "" gets unprefixed tables and one that
9
+ # leaves the default gets active_agent_*.
10
+ #
11
+ # JSON columns are jsonb on PostgreSQL and json elsewhere: the dashboard runs
12
+ # on SQLite and MySQL, it just loses jsonb containment (see
13
+ # ActionAgent::Agent.with_tool, which falls back to a LIKE).
14
+ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_version %>
15
+ def change
16
+ prefix = ActionAgent.table_name_prefix
17
+
18
+ create_table "#{prefix}agents" do |t|
19
+ t.string :name, null: false
20
+ t.string :slug, null: false
21
+ t.text :description
22
+ t.string :provider, default: "openai"
23
+ t.string :model, default: "gpt-4o-mini"
24
+ t.text :instructions
25
+ t.string :preset_type
26
+ t.string :agent_class_name
27
+ t.string :action_name
28
+ t.integer :status, default: 0, null: false
29
+ t.column :appearance, json_type, default: {}
30
+ t.column :instruction_sets, json_type, default: []
31
+ t.column :tools, json_type, default: []
32
+ t.column :mcp_servers, json_type, default: []
33
+ t.column :model_config, json_type, default: {}
34
+ t.column :response_format, json_type, default: {}
35
+ t.column :action_prompts, json_type, default: [], null: false
36
+
37
+ # Agents discovered from reported telemetry rather than authored here.
38
+ t.string :service_name
39
+ t.string :source
40
+ t.datetime :first_observed_at
41
+ t.datetime :last_observed_at
42
+
43
+ t.bigint :user_id
44
+ t.bigint :account_id
45
+ t.timestamps
46
+ t.index :user_id
47
+ t.index :account_id
48
+ t.index [ :user_id, :slug ], unique: true
49
+ t.index :status
50
+ end
51
+
52
+ create_table "#{prefix}agent_versions" do |t|
53
+ t.bigint :agent_id, null: false
54
+ t.integer :version_number, default: 1, null: false
55
+ t.column :configuration_snapshot, json_type, default: {}, null: false
56
+ t.string :change_summary
57
+ t.string :created_by
58
+ t.timestamps
59
+ t.index [ :agent_id, :version_number ], unique: true
60
+ end
61
+
62
+ create_table "#{prefix}agent_runs" do |t|
63
+ t.bigint :agent_id, null: false
64
+ t.string :trace_id
65
+ t.string :action_name
66
+ t.integer :status, default: 0, null: false
67
+ t.text :input_prompt
68
+ t.column :input_params, json_type, default: {}
69
+ t.text :output
70
+ t.column :output_metadata, json_type, default: {}
71
+ t.column :logs, json_type, default: []
72
+ t.text :error_message
73
+ t.text :error_backtrace
74
+ t.integer :input_tokens
75
+ t.integer :output_tokens
76
+ t.integer :total_tokens
77
+ t.integer :duration_ms
78
+ t.datetime :started_at
79
+ t.datetime :completed_at
80
+ t.timestamps
81
+ t.index :agent_id
82
+ t.index :trace_id
83
+ t.index :created_at
84
+ end
85
+
86
+ create_table "#{prefix}agent_templates" do |t|
87
+ t.string :name, null: false
88
+ t.string :slug, null: false
89
+ t.text :description
90
+ t.string :category
91
+ t.string :icon
92
+ t.string :provider, default: "openai"
93
+ t.string :model, default: "gpt-4o-mini"
94
+ t.text :instructions
95
+ t.string :preset_type
96
+ t.column :appearance, json_type, default: {}
97
+ t.column :instruction_sets, json_type, default: []
98
+ t.column :tools, json_type, default: []
99
+ t.column :mcp_servers, json_type, default: {}
100
+ t.column :model_config, json_type, default: {}
101
+ t.boolean :featured, default: false
102
+ t.boolean :free_tier, default: false
103
+ t.boolean :public, default: true
104
+ t.integer :usage_count, default: 0
105
+ t.timestamps
106
+ t.index :slug, unique: true
107
+ end
108
+
109
+ # Conversation persistence — the solid_agent schema, owned by the engine
110
+ # so the dashboard's Interactions view works without that gem installed.
111
+ create_table "#{prefix}agent_contexts" do |t|
112
+ t.string :agent_name, null: false
113
+ t.string :action_name, null: false
114
+ t.string :contextable_type
115
+ t.bigint :contextable_id
116
+ t.text :instructions
117
+ t.column :options, json_type, default: {}
118
+ t.string :trace_id
119
+ t.integer :total_input_tokens, default: 0
120
+ t.integer :total_output_tokens, default: 0
121
+ t.timestamps
122
+ t.index [ :contextable_type, :contextable_id ]
123
+ t.index :trace_id
124
+ end
125
+
126
+ create_table "#{prefix}agent_messages" do |t|
127
+ t.bigint :agent_context_id, null: false
128
+ t.string :role, null: false
129
+ t.text :content
130
+ t.string :content_checksum
131
+ t.string :tool_call_id
132
+ t.string :tool_name
133
+ t.column :tool_arguments, json_type, default: {}
134
+ t.column :tool_result, json_type
135
+ t.column :attachments, json_type, default: []
136
+ t.column :metadata, json_type, default: {}
137
+ t.column :provenance, json_type, default: {}
138
+ t.timestamps
139
+ t.index :agent_context_id
140
+ end
141
+
142
+ create_table "#{prefix}agent_generations" do |t|
143
+ t.bigint :agent_context_id, null: false
144
+ t.text :content
145
+ t.string :model
146
+ t.string :provider
147
+ t.string :finish_reason
148
+ t.integer :input_tokens, default: 0
149
+ t.integer :output_tokens, default: 0
150
+ t.integer :cached_tokens, default: 0
151
+ t.integer :reasoning_tokens, default: 0
152
+ t.float :duration_seconds
153
+ t.column :tool_calls, json_type, default: []
154
+ t.column :raw_response, json_type
155
+ t.column :provenance, json_type, default: {}
156
+ t.string :trace_id
157
+ t.timestamps
158
+ t.index :agent_context_id
159
+ t.index :trace_id
160
+ end
161
+
162
+ create_table "#{prefix}agent_memories" do |t|
163
+ t.string :memorable_type
164
+ t.bigint :memorable_id
165
+ t.string :scope, default: "default", null: false
166
+ t.timestamps
167
+ t.index [ :memorable_type, :memorable_id, :scope ], unique: true,
168
+ name: "index_active_agent_memories_on_memorable_and_scope"
169
+ end
170
+
171
+ create_table "#{prefix}agent_memory_entries" do |t|
172
+ t.bigint :agent_memory_id, null: false
173
+ t.text :content, null: false
174
+ t.string :source_agent
175
+ t.string :category
176
+ t.timestamps
177
+ t.index :agent_memory_id
178
+ end
179
+
180
+ create_table "#{prefix}evaluations" do |t|
181
+ t.bigint :agent_id, null: false
182
+ t.string :name, null: false
183
+ t.string :judge_kind, default: "rules", null: false
184
+ t.string :judge_model
185
+ t.integer :sample_size, default: 20, null: false
186
+ t.column :criteria, json_type, default: [], null: false
187
+ t.column :config, json_type, default: {}, null: false
188
+ t.timestamps
189
+ t.index [ :agent_id, :name ], unique: true
190
+ end
191
+
192
+ create_table "#{prefix}evaluation_runs" do |t|
193
+ t.bigint :evaluation_id, null: false
194
+ t.integer :status, default: 0, null: false
195
+ t.column :scores, json_type, default: {}
196
+ t.integer :samples_evaluated, default: 0
197
+ t.integer :samples_passed, default: 0
198
+ t.text :error_message
199
+ t.datetime :completed_at
200
+ t.timestamps
201
+ t.index :evaluation_id
202
+ end
203
+
204
+ create_table "#{prefix}sandbox_sessions" do |t|
205
+ t.string :session_id, null: false
206
+ t.bigint :agent_template_id
207
+ t.integer :status, default: 0
208
+ t.string :sandbox_type, default: "playwright_mcp"
209
+ t.string :cloud_run_job_id
210
+ t.string :cloud_run_url
211
+ t.column :runs, json_type, default: []
212
+ # MCP servers this session was started with, so the MCP Services view
213
+ # can show a launched server as running rather than offering to start
214
+ # a second copy.
215
+ t.column :mcp_servers, json_type, default: []
216
+ t.integer :runs_count, default: 0
217
+ t.integer :max_runs, default: 10
218
+ t.integer :timeout_seconds, default: 300
219
+ t.integer :total_duration_ms, default: 0
220
+ t.integer :total_tokens, default: 0
221
+ t.text :error_message
222
+ t.datetime :expires_at
223
+ t.datetime :last_activity_at
224
+ t.bigint :user_id
225
+ t.bigint :account_id
226
+ t.timestamps
227
+ t.index :session_id, unique: true
228
+ t.index :user_id
229
+ end
230
+
231
+ create_table "#{prefix}sandbox_runs" do |t|
232
+ t.bigint :sandbox_session_id
233
+ t.text :task, null: false
234
+ t.integer :status, default: 0, null: false
235
+ t.text :result
236
+ t.text :error
237
+ t.column :screenshots, json_type, default: []
238
+ t.integer :tokens_used, default: 0
239
+ t.integer :duration_ms
240
+ t.datetime :started_at
241
+ t.datetime :completed_at
242
+ t.timestamps
243
+ t.index :sandbox_session_id
244
+ end
245
+
246
+ create_table "#{prefix}session_recordings" do |t|
247
+ t.string :name
248
+ t.bigint :agent_run_id
249
+ t.bigint :sandbox_session_id
250
+ t.integer :status, default: 0, null: false
251
+ t.integer :action_count, default: 0
252
+ t.integer :duration_ms
253
+ t.column :metadata, json_type, default: {}
254
+ t.bigint :user_id
255
+ t.bigint :account_id
256
+ t.timestamps
257
+ t.index :agent_run_id
258
+ t.index :sandbox_session_id
259
+ t.index :user_id
260
+ t.index :account_id
261
+ end
262
+
263
+ create_table "#{prefix}recording_actions" do |t|
264
+ t.bigint :session_recording_id, null: false
265
+ t.integer :sequence, null: false
266
+ t.string :action_type, null: false
267
+ t.string :selector
268
+ t.text :value
269
+ t.integer :timestamp_ms, null: false
270
+ t.string :screenshot_key
271
+ t.string :dom_snapshot_key
272
+ t.column :metadata, json_type, default: {}
273
+ t.timestamps
274
+ t.index [ :session_recording_id, :sequence ], unique: true,
275
+ name: "index_active_agent_recording_actions_on_recording_and_sequence"
276
+ end
277
+
278
+ create_table "#{prefix}recording_snapshots" do |t|
279
+ t.bigint :session_recording_id, null: false
280
+ t.bigint :recording_action_id
281
+ t.string :snapshot_type, null: false
282
+ t.string :storage_key, null: false
283
+ t.integer :width
284
+ t.integer :height
285
+ t.integer :file_size_bytes
286
+ t.timestamps
287
+ t.index :session_recording_id
288
+ t.index :recording_action_id
289
+ end
290
+
291
+ create_table "#{prefix}api_keys" do |t|
292
+ t.string :name, null: false
293
+ t.string :token, null: false
294
+ t.string :token_prefix, null: false
295
+ t.datetime :last_used_at
296
+ t.bigint :user_id
297
+ t.bigint :account_id
298
+ t.timestamps
299
+ t.index :token, unique: true
300
+ end
301
+
302
+ create_table "#{prefix}provider_keys" do |t|
303
+ t.string :provider, null: false
304
+ t.string :credential, null: false
305
+ t.bigint :user_id
306
+ t.bigint :account_id
307
+ t.timestamps
308
+ t.index [ :account_id, :provider ]
309
+ t.index [ :user_id, :provider ]
310
+ end
311
+ end
312
+
313
+ private
314
+
315
+ # jsonb where the adapter has it, json where it doesn't.
316
+ def json_type
317
+ @json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
318
+ end
319
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :active_agent_telemetry_traces do |t|
6
+ <% if multi_tenant? -%>
7
+ # Multi-tenant mode: every query is scoped through `for_account`, and
8
+ # ingest authenticates a per-account key. The foreign key requires an
9
+ # existing `accounts` table — on PostgreSQL/MySQL this migration must
10
+ # run after the one that creates it (drop `foreign_key: true` if your
11
+ # tenant table is named differently).
12
+ t.references :account, null: false, foreign_key: true
13
+ <% end -%>
14
+ t.string :trace_id, null: false
15
+ t.string :service_name
16
+ t.string :environment
17
+ t.datetime :timestamp, index: true
18
+ t.column :spans, json_type, default: []
19
+ t.column :resource_attributes, json_type, default: {}
20
+ t.column :sdk_info, json_type, default: {}
21
+ t.decimal :total_duration_ms, precision: 12, scale: 3
22
+ t.integer :total_input_tokens, default: 0
23
+ t.integer :total_output_tokens, default: 0
24
+ t.integer :total_thinking_tokens, default: 0
25
+ t.string :status
26
+ t.string :agent_class, index: true
27
+ t.string :agent_action
28
+ # Set on ingest when the trace can be attributed to a dashboard agent;
29
+ # nullable because a trace must still land when it cannot be.
30
+ t.bigint :agent_id, index: true
31
+ t.text :error_message
32
+
33
+ t.timestamps
34
+ end
35
+
36
+ <% if multi_tenant? -%>
37
+ # Ingest dedupes on trace_id (per account when multi-tenant); the unique
38
+ # index makes that dedupe race-proof rather than check-then-insert.
39
+ add_index :active_agent_telemetry_traces, [:account_id, :trace_id], unique: true
40
+ add_index :active_agent_telemetry_traces, [:account_id, :timestamp]
41
+ <% else -%>
42
+ add_index :active_agent_telemetry_traces, :trace_id, unique: true
43
+ <% end -%>
44
+ add_index :active_agent_telemetry_traces, :status
45
+ add_index :active_agent_telemetry_traces, [:agent_class, :agent_action]
46
+ add_index :active_agent_telemetry_traces, [:service_name, :environment]
47
+ add_index :active_agent_telemetry_traces, :created_at
48
+ end
49
+
50
+ private
51
+
52
+ # jsonb where the adapter has it, json where it doesn't. The dashboard
53
+ # traverses spans in SQL on PostgreSQL, and jsonb_array_elements rejects a
54
+ # json argument, so the column type and the query have to agree.
55
+ def json_type
56
+ @json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
57
+ end
58
+ end