actionagent 1.2.2 → 1.3.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/README.md +14 -3
  3. data/app/assets/builds/action_agent.css +1 -1
  4. data/app/assets/builds/action_agent.js +48 -44
  5. data/app/controllers/action_agent/api/agent_runs_controller.rb +25 -7
  6. data/app/controllers/action_agent/api/agents_controller.rb +76 -48
  7. data/app/controllers/action_agent/api/analytics_controller.rb +31 -9
  8. data/app/controllers/action_agent/api/base_controller.rb +16 -0
  9. data/app/controllers/action_agent/api/evaluations_controller.rb +237 -7
  10. data/app/controllers/action_agent/api/mcp_controller.rb +13 -3
  11. data/app/controllers/action_agent/api/mcp_servers_controller.rb +28 -8
  12. data/app/controllers/action_agent/api/metrics_controller.rb +44 -11
  13. data/app/controllers/action_agent/api/provider_models_controller.rb +1 -1
  14. data/app/controllers/action_agent/api/sandboxes_controller.rb +6 -0
  15. data/app/controllers/action_agent/api/session_recordings_controller.rb +34 -12
  16. data/app/controllers/action_agent/api/templates_controller.rb +25 -21
  17. data/app/controllers/action_agent/api/traces_controller.rb +25 -5
  18. data/app/controllers/action_agent/api/usage_controller.rb +20 -0
  19. data/app/controllers/action_agent/application_controller.rb +25 -2
  20. data/app/controllers/action_agent/dashboard_controller.rb +2 -1
  21. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +53 -0
  22. data/app/jobs/action_agent/agent_execution_job.rb +40 -20
  23. data/app/jobs/action_agent/application_job.rb +7 -3
  24. data/app/jobs/action_agent/evaluation_run_job.rb +18 -0
  25. data/app/jobs/action_agent/sandbox_cleanup_job.rb +13 -10
  26. data/app/models/action_agent/agent.rb +14 -5
  27. data/app/models/action_agent/agent_template.rb +22 -7
  28. data/app/models/action_agent/evaluation.rb +64 -4
  29. data/app/models/action_agent/evaluation_run.rb +182 -2
  30. data/app/models/action_agent/evaluation_scenario.rb +59 -0
  31. data/app/models/action_agent/evaluation_scenario_result.rb +66 -0
  32. data/app/models/action_agent/recording_action.rb +11 -7
  33. data/app/models/action_agent/sandbox_session.rb +1 -1
  34. data/app/models/action_agent/session_recording.rb +31 -8
  35. data/app/models/action_agent/telemetry_trace.rb +110 -2
  36. data/app/models/concerns/action_agent/adapter_aware.rb +19 -0
  37. data/app/models/concerns/action_agent/ownable.rb +15 -2
  38. data/app/queries/action_agent/metrics_report.rb +498 -0
  39. data/app/services/action_agent/agent_toolbox.rb +4 -4
  40. data/app/services/action_agent/evaluation_tool_resolver.rb +154 -0
  41. data/app/services/action_agent/mcp_catalog.rb +46 -8
  42. data/app/services/action_agent/mcp_recording_middleware.rb +2 -2
  43. data/app/services/action_agent/playwright_mcp_client.rb +6 -6
  44. data/app/services/action_agent/sandbox_orchestrator.rb +12 -1
  45. data/app/services/action_agent/scenario_evaluation_runner.rb +226 -0
  46. data/app/services/action_agent/tool_discovery.rb +22 -8
  47. data/config/routes.rb +25 -2
  48. data/lib/action_agent/engine.rb +101 -19
  49. data/lib/action_agent/version.rb +1 -1
  50. data/lib/action_agent.rb +72 -6
  51. data/lib/generators/action_agent/install_generator.rb +20 -7
  52. data/lib/generators/action_agent/templates/create_active_agent_evaluation_scenarios.rb.erb +79 -0
  53. data/lib/tasks/action_agent.rake +9 -0
  54. metadata +19 -6
data/config/routes.rb CHANGED
@@ -70,7 +70,7 @@ ActionAgent::Engine.routes.draw do
70
70
  resources :tools, only: [ :index ]
71
71
 
72
72
  # MCP services — detected servers unioned with the default catalog
73
- # (McpCatalog), plus on-demand sandbox provisioning. Keys are catalog
73
+ # (MCPCatalog), plus on-demand sandbox provisioning. Keys are catalog
74
74
  # slugs like "sequential-thinking", so the id segment allows dashes.
75
75
  resources :mcp_servers, only: [ :index, :show ], id: /[^\/]+/ do
76
76
  member do
@@ -113,10 +113,17 @@ ActionAgent::Engine.routes.draw do
113
113
  # Conversations (contexts, messages, generations) behind Interactions.
114
114
  resources :interactions, only: [ :index, :show ]
115
115
 
116
- # Agent output evaluations.
116
+ # Agent output evaluations. A scenario suite also manages its scenarios
117
+ # here, and exposes each run's per-scenario, per-model results.
117
118
  resources :evaluations, only: [ :index, :show, :create, :destroy ] do
118
119
  member do
119
120
  post :run
121
+ get "runs/:run_id", action: :show_run, as: :run_result
122
+ get "runs/:run_id/report", action: :run_report, as: :run_report
123
+ get :scenarios
124
+ put :scenarios, action: :replace_scenarios
125
+ patch "scenarios/:scenario_id", action: :update_scenario, as: :scenario
126
+ delete "scenarios/:scenario_id", action: :destroy_scenario
120
127
  end
121
128
  end
122
129
 
@@ -128,6 +135,12 @@ ActionAgent::Engine.routes.draw do
128
135
  # Model catalogs for the agent builder (Ollama queried live from the
129
136
  # configured host; hosted providers curated).
130
137
  resources :provider_models, only: [ :index ]
138
+
139
+ # The plan meter the Organization view and the Run Agents quota banner
140
+ # read. The engine meters nothing itself: a host that tracks usage
141
+ # against a plan answers through ActionAgent.usage_resolver, and a bare
142
+ # mount reports unlimited rather than 404.
143
+ resource :usage, only: [ :show ], controller: "usage"
131
144
  end
132
145
 
133
146
  # The account's agents presented as an authenticated MCP server (tools +
@@ -136,6 +149,16 @@ ActionAgent::Engine.routes.draw do
136
149
  # namespace's session-authenticated controllers.
137
150
  post "mcp", to: "api/mcp#create"
138
151
 
152
+ # MCP Streamable HTTP (2025-03-26): a client MAY open the server-to-client
153
+ # SSE stream with GET, and ends a session with DELETE. This facade offers
154
+ # no stream and keeps no sessions, so both answer 405 with Allow: POST —
155
+ # the clean "not offered" signal SDK clients expect, instead of the
156
+ # dashboard's HTML page parsed as an event stream. A browser's GET (Accept
157
+ # prefers HTML) is the MCP Services view's deep link, and falls through to
158
+ # the catch-all below like any other client-side route.
159
+ match "mcp", to: "api/mcp#unsupported", via: [ :get, :delete ],
160
+ constraints: ->(request) { request.delete? || !ActionAgent::Engine.html_request?(request) }
161
+
139
162
  # Everything else under the mount is a client-side route: render the
140
163
  # dashboard and let the browser resolve it. Anchored last so it can only
141
164
  # ever catch what the routes above did not, and refuses /api paths so a
@@ -14,14 +14,37 @@ module ActionAgent
14
14
  engine_name "action_agent"
15
15
 
16
16
  # Basenames this engine spells differently from Zeitwerk's default
17
- # camelization, consulted by the inflections initializer below. Empty
18
- # today: every file here is named for the constant default camelization
19
- # produces. An engine file that wants a genuine acronym in its constant
20
- # adds its basename here rather than relying on the host to register one.
21
- INFLECTION_OVERRIDES = {}.freeze
17
+ # camelization, consulted by the inflections initializer below. An engine
18
+ # file that wants a genuine acronym in its constant adds its basename here
19
+ # rather than relying on the host to register one.
20
+ INFLECTION_OVERRIDES = {
21
+ "mcp_catalog" => "MCPCatalog",
22
+ "mcp_controller" => "MCPController",
23
+ "mcp_recording_middleware" => "MCPRecordingMiddleware",
24
+ "mcp_servers_controller" => "MCPServersController",
25
+ "playwright_mcp_client" => "PlaywrightMCPClient"
26
+ }.freeze
27
+
28
+ # The spelling default camelization produces for each overridden file,
29
+ # mapped to the constant the file actually defines. The const_missing shim
30
+ # below uses it to answer lookups that camelized with default inflections
31
+ # (a plain host's router, or caller code written against the pre-acronym
32
+ # names) with the acronym constant.
33
+ DEFAULT_SPELLINGS = INFLECTION_OVERRIDES.to_h { |basename, constant|
34
+ [ Zeitwerk::Inflector.new.camelize(basename, nil), constant ]
35
+ }.freeze
22
36
 
23
37
  config.action_agent = ActiveSupport::OrderedOptions.new
24
38
 
39
+ # Whether a request is a browser asking for a page, as opposed to an API
40
+ # or MCP client: the routes use it to tell the dashboard's client-side
41
+ # deep links apart from protocol traffic on the same path.
42
+ def self.html_request?(request)
43
+ Array(request.accepts).any? { |type| type.respond_to?(:html?) && type.html? }
44
+ rescue StandardError
45
+ false
46
+ end
47
+
25
48
  # The dashboard's JS and CSS ship prebuilt in the gem. Adding the
26
49
  # directory to the host app's asset paths is what lets a plain
27
50
  # `mount ActionAgent::Engine` work without the host running a
@@ -34,14 +57,13 @@ module ActionAgent
34
57
  end
35
58
 
36
59
  # This engine's constants are spelled the way Zeitwerk's own inflector
37
- # spells them — Api, McpCatalog, ApiKey — but an engine's files are
60
+ # spells them — Api, ApiKey — but an engine's files are
38
61
  # autoloaded by the host's `rails.main` loader, under the *host's*
39
62
  # inflections. A host that declares `inflect.acronym "API"` or "MCP" (both
40
63
  # common, and documented by Rails) makes Zeitwerk expect
41
- # ActionAgent::API::TracesController or ActionAgent::MCPCatalog from files
42
- # that define ActionAgent::Api::TracesController and
43
- # ActionAgent::McpCatalog. The constant never resolves and the request
44
- # raises Zeitwerk::NameError.
64
+ # ActionAgent::API::TracesController from a file
65
+ # that defines ActionAgent::Api::TracesController. The constant never
66
+ # resolves and the request raises Zeitwerk::NameError.
45
67
  #
46
68
  # Every path under this engine therefore camelizes with Zeitwerk's default
47
69
  # rules, ignoring whatever acronyms the host has registered. Applied by
@@ -51,7 +73,8 @@ module ActionAgent
51
73
  # engine's files from the host's.
52
74
  #
53
75
  # Basenames whose spelling this engine cannot express through default
54
- # camelization (a genuine acronym it wants uppercased) go in OVERRIDES.
76
+ # camelization (a genuine acronym it wants uppercased) go in
77
+ # INFLECTION_OVERRIDES.
55
78
  initializer "action_agent.inflections", before: :set_autoload_paths do
56
79
  engine_root = File.join(root.to_s, "")
57
80
  default = Zeitwerk::Inflector.new
@@ -74,15 +97,22 @@ module ActionAgent
74
97
  # So the namespace answers to both. `const_missing` rather than an eager
75
98
  # alias because the controllers are autoloaded on demand, and naming them at
76
99
  # boot would load the whole dashboard.
77
- # The router does not consult the autoloader's inflector, so an acronym
78
- # host asks for ActionAgent::API::MCPServersController while the constants
79
- # are Api::McpServersController. Rather than enumerate the pairs, an
80
- # all-caps run in a missing constant is retried in the spelling default
81
- # camelization produces: API -> Api, MCPServersController -> McpServers-
82
- # Controller. Only the engine's own namespaces are touched, and only for a
83
- # constant that is already missing.
100
+ # The router does not consult the autoloader's inflector, so its lookups
101
+ # miss in both directions. An acronym host asks for
102
+ # ActionAgent::API::TracesController while the constant is
103
+ # Api::TracesController: an all-caps run in a missing constant is retried
104
+ # in the spelling default camelization produces (API -> Api). A plain host
105
+ # asks for Api::McpServersController while the constant is
106
+ # MCPServersController (the file is in INFLECTION_OVERRIDES): a missing
107
+ # constant matching an override's default spelling is retried as the
108
+ # acronym constant, via DEFAULT_SPELLINGS — which also keeps caller code
109
+ # written against the pre-acronym names resolving. Only the engine's own
110
+ # namespaces are touched, and only for a constant that is already missing.
84
111
  inflection_shim = Module.new do
85
112
  def const_missing(name)
113
+ acronym = ActionAgent::Engine::DEFAULT_SPELLINGS[name.to_s]
114
+ return const_get(acronym, false) if acronym && const_defined?(acronym, false)
115
+
86
116
  relaxed = name.to_s.gsub(/([A-Z])([A-Z]+)(?=[A-Z][a-z]|\d|\z)/) { "#{$1}#{$2.downcase}" }
87
117
 
88
118
  return super if relaxed == name.to_s || !const_defined?(relaxed, false)
@@ -102,11 +132,63 @@ module ActionAgent
102
132
  end
103
133
  end
104
134
 
135
+ # API keys and provider credentials are encrypted at rest (`encrypts` on
136
+ # ApiKey and ProviderKey), which needs Active Record Encryption keys the
137
+ # host app may never have generated — `rails db:encryption:init` is a
138
+ # step most installs skip, and the engine's own reference host had
139
+ # skipped it too. Without keys, every credential write and every
140
+ # authenticated MCP request raised Errors::Configuration as an HTML 500.
141
+ #
142
+ # Rails seeds ActiveRecord::Encryption from credentials and
143
+ # config.active_record.encryption in its own
144
+ # "active_record_encryption.configuration" initializer. Keys derived
145
+ # after that one land in config but never reach the encryptor, so the
146
+ # config reads back correct while every credential write raises
147
+ # Errors::Configuration — which is what a host that skipped
148
+ # db:encryption:init actually hits. Naming it in `before:` is what puts
149
+ # the derivation ahead of it. When the host set no keys — neither here
150
+ # nor in credentials — derive stable ones from secret_key_base, overridable
151
+ # through the ACTIVE_RECORD_ENCRYPTION_* variables, exactly as the
152
+ # activeagents.ai platform does. Host-provided keys are never
153
+ # overridden, and the derivation is stable as long as secret_key_base is.
154
+ initializer "action_agent.active_record_encryption", before: "active_record_encryption.configuration" do |app|
155
+ next unless ActionAgent.encrypt_credentials
156
+ next unless app.config.respond_to?(:active_record)
157
+
158
+ encryption = app.config.active_record.encryption
159
+ configured = %i[primary_key deterministic_key key_derivation_salt].any? { |key| encryption[key].present? }
160
+ configured ||= begin
161
+ app.credentials.dig(:active_record_encryption, :primary_key).present?
162
+ rescue StandardError
163
+ false
164
+ end
165
+ next if configured
166
+
167
+ derive = lambda do |purpose|
168
+ app.key_generator.generate_key("active_record_encryption.#{purpose}", 32).unpack1("H*")
169
+ end
170
+
171
+ encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].presence || derive.call("primary_key")
172
+ encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"].presence || derive.call("deterministic_key")
173
+ encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"].presence || derive.call("key_derivation_salt")
174
+ end
175
+
105
176
  initializer "action_agent.assets", before: :append_assets_path do |app|
106
177
  builds = root.join("app", "assets", "builds").to_s
107
178
  next unless File.directory?(builds)
108
179
 
109
- next unless app.config.respond_to?(:assets)
180
+ # The prebuilt bundles are served through the host's asset pipeline —
181
+ # propshaft (the Rails default) or sprockets-rails. A host without one
182
+ # (a `rails --api` app, or one that removed propshaft) used to get a
183
+ # silently blank dashboard with two 404s; say why instead.
184
+ unless app.config.respond_to?(:assets)
185
+ Rails.logger&.warn(
186
+ "[ActionAgent] the dashboard's assets cannot be served: this app has no asset pipeline. " \
187
+ "Add propshaft (or sprockets-rails) to the Gemfile so action_agent.js and action_agent.css " \
188
+ "are served from the engine's app/assets/builds."
189
+ )
190
+ next
191
+ end
110
192
 
111
193
  if app.config.assets.respond_to?(:paths)
112
194
  app.config.assets.paths << builds unless app.config.assets.paths.include?(builds)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.2.2"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/action_agent.rb CHANGED
@@ -83,7 +83,10 @@ require "action_agent/compatibility"
83
83
  #
84
84
  # ActionAgent.configure do |config|
85
85
  # config.authentication_method = ->(controller) { controller.authenticate_admin! }
86
- # config.sandbox_service = :local # Docker/Incus
86
+ # # Sandboxes run in the in-memory mock unless the app registers a real
87
+ # # backend (see sandbox_backends) and names it here:
88
+ # config.sandbox_backends = { "incus" => "IncusSandboxService" }
89
+ # config.sandbox_service = :incus
87
90
  # end
88
91
  #
89
92
  # == Multi-tenant Mode
@@ -171,7 +174,10 @@ module ActionAgent
171
174
  # @return [String, nil]
172
175
  attr_accessor :layout
173
176
 
174
- # Sandbox service type (:local, :cloud_run, :kubernetes)
177
+ # Which sandbox backend to provision with: :mock (the only one the
178
+ # engine ships — an in-memory fake that runs nothing) or the name of a
179
+ # backend the host registered in sandbox_backends. An unregistered name
180
+ # falls back to :mock with a logged warning.
175
181
  # @return [Symbol]
176
182
  attr_accessor :sandbox_service
177
183
 
@@ -217,9 +223,11 @@ module ActionAgent
217
223
  # @return [Proc, nil]
218
224
  attr_accessor :provider_credentials_resolver
219
225
 
220
- # Extra sandbox backends contributed by the host app, as
221
- # { "cloud_run" => "CloudRunService" }. The engine ships :mock and
222
- # :local (Docker); cloud backends live in the app that operates them.
226
+ # Sandbox backends contributed by the host app, as
227
+ # { "cloud_run" => "CloudRunService" }. The engine ships only :mock;
228
+ # every real backend (Docker/Incus, Cloud Run, Kubernetes) lives in the
229
+ # app that operates it, which registers it here and selects it with
230
+ # sandbox_service.
223
231
  # @return [Hash{String => String}]
224
232
  attr_accessor :sandbox_backends
225
233
 
@@ -234,6 +242,37 @@ module ActionAgent
234
242
  # @return [String, nil]
235
243
  attr_accessor :upgrade_url
236
244
 
245
+ # The host app's sign-out endpoint, which the header's "Sign out" item
246
+ # POSTs to (with _method=delete and the CSRF token). The engine has no
247
+ # session of its own; unset, the menu item is not shown.
248
+ # @return [String, nil]
249
+ attr_accessor :sign_out_path
250
+
251
+ # Where a browser is sent when it asks for a dashboard page without a
252
+ # valid session — the host app's sign-in page. Unset, an unauthenticated
253
+ # page request gets a minimal session-expired page instead of a bare
254
+ # 401; API clients always get the 401.
255
+ # @return [String, nil]
256
+ attr_accessor :sign_in_path
257
+
258
+ # Answers GET <mount>/api/usage — the plan meter the Organization view
259
+ # and the Run Agents quota banner read. Receives (owner) and returns a
260
+ # Hash in the platform's shape:
261
+ #
262
+ # { runs_used: 12, runs_limit: 100, runs_remaining: 88,
263
+ # can_run: true, plan: "pro" }
264
+ #
265
+ # Unset means unlimited: the engine reports UNLIMITED_USAGE and the
266
+ # views hide the meter.
267
+ # @return [Proc, nil]
268
+ attr_accessor :usage_resolver
269
+
270
+ # What a dashboard with no usage_resolver reports: no limit, nothing
271
+ # counted, always allowed.
272
+ UNLIMITED_USAGE = {
273
+ runs_used: 0, runs_limit: nil, runs_remaining: nil, can_run: true, plan: nil, unlimited: true
274
+ }.freeze
275
+
237
276
  # Called after the dashboard performs a metered action, as
238
277
  # (owner, kind) — the counterpart to quota_checker, for host apps that
239
278
  # track usage against a plan. Unset means nothing is counted.
@@ -262,6 +301,16 @@ module ActionAgent
262
301
  # @return [Boolean]
263
302
  attr_accessor :encrypt_credentials
264
303
 
304
+ # MCP servers the host app itself serves or connects, appended to the
305
+ # built-in catalog (MCPCatalog) so the MCP Services view lists them and
306
+ # telemetry traffic attributes to them. Each entry is a hash shaped like
307
+ # a catalog entry: +key+ and +name+ at minimum, plus any of the optional
308
+ # fields (+description+, +transport+, +url+, +categories+, +docs_url+,
309
+ # +first_party+); +tool_hints+ names the bare tool names that belong to
310
+ # the server. A built-in entry keeps its key on collision.
311
+ # @return [Array<Hash>]
312
+ attr_accessor :mcp_catalog
313
+
265
314
  # Value stored in polymorphic *_type columns for dashboard agents
266
315
  # (agent_memories.memorable_type, agent_contexts.contextable_type).
267
316
  # Unset means the class name. A host app whose existing rows were
@@ -292,6 +341,19 @@ module ActionAgent
292
341
  nil
293
342
  end
294
343
 
344
+ # The usage meter for +owner+. Never raises: a bookkeeping failure must
345
+ # not take the views that display it down with it.
346
+ #
347
+ # @return [Hash] the platform's usage shape, UNLIMITED_USAGE by default
348
+ def usage_for(owner)
349
+ return UNLIMITED_USAGE.dup if usage_resolver.nil?
350
+
351
+ usage_resolver.call(owner) || UNLIMITED_USAGE.dup
352
+ rescue StandardError => e
353
+ Rails.logger.warn("[ActionAgent] usage lookup failed: #{e.message}")
354
+ UNLIMITED_USAGE.dup
355
+ end
356
+
295
357
  # Asks the host app whether +owner+ may perform +kind+.
296
358
  #
297
359
  # @return [String, Hash, nil] denial message or payload, nil when allowed
@@ -381,7 +443,7 @@ module ActionAgent
381
443
  @trace_model_class = nil
382
444
  @use_inertia = false
383
445
  @layout = nil
384
- @sandbox_service = :local
446
+ @sandbox_service = :mock
385
447
  @sandbox_limits = nil
386
448
  @storage_service = nil
387
449
  @ingest_api_key = nil
@@ -396,7 +458,11 @@ module ActionAgent
396
458
  @trace_retention = nil
397
459
  @trace_owner_resolver = nil
398
460
  @usage_recorder = nil
461
+ @usage_resolver = nil
399
462
  @upgrade_url = nil
463
+ @sign_out_path = nil
464
+ @sign_in_path = nil
465
+ @mcp_catalog = []
400
466
  end
401
467
  end
402
468
 
@@ -64,12 +64,20 @@ module ActionAgent
64
64
 
65
65
  if existing_migration?("create_active_agent_dashboard_tables")
66
66
  say_status :skip, "create_active_agent_dashboard_tables already exists", :yellow
67
- return
67
+ else
68
+ migration_template(
69
+ "create_active_agent_dashboard_tables.rb.erb",
70
+ "db/migrate/create_active_agent_dashboard_tables.rb"
71
+ )
68
72
  end
69
73
 
74
+ # Scenario suites arrived after the dashboard tables shipped, so an
75
+ # install that already has those still needs this one.
76
+ return if existing_migration?("create_active_agent_evaluation_scenarios")
77
+
70
78
  migration_template(
71
- "create_active_agent_dashboard_tables.rb.erb",
72
- "db/migrate/create_active_agent_dashboard_tables.rb"
79
+ "create_active_agent_evaluation_scenarios.rb.erb",
80
+ "db/migrate/create_active_agent_evaluation_scenarios.rb"
73
81
  )
74
82
  end
75
83
 
@@ -102,10 +110,15 @@ module ActionAgent
102
110
  say "\n"
103
111
  say "Next steps:"
104
112
  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"
113
+ # Nested under the environment key: ActiveAgent::Configuration.load
114
+ # reads only the current environment's subtree when the file has one,
115
+ # and the framework's own generator writes an env-keyed file — so a
116
+ # top-level telemetry: block is silently ignored.
117
+ say " 2. Configure telemetry in config/active_agent.yml, under each environment key:"
118
+ say " development:"
119
+ say " telemetry:"
120
+ say " enabled: true"
121
+ say " local_storage: true"
109
122
  say " 3. Visit /activeagents to view the dashboard"
110
123
  unless options[:traces_only]
111
124
  say "\n"
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds scenario suites to evaluations: the user-authored prompts an
4
+ # evaluation replays through its agent (once per candidate model), and the
5
+ # per-scenario, per-model result each replay produced. Emitted alongside the
6
+ # dashboard tables on a fresh install, and on its own for an install that
7
+ # predates scenario evaluations.
8
+ #
9
+ # Table names follow ActionAgent.table_name_prefix, and JSON columns are
10
+ # jsonb on PostgreSQL and json elsewhere, the same way as
11
+ # create_active_agent_dashboard_tables.
12
+ class CreateActiveAgentEvaluationScenarios < ActiveRecord::Migration<%= migration_version %>
13
+ def change
14
+ prefix = ActionAgent.table_name_prefix
15
+
16
+ create_table "#{prefix}evaluation_scenarios" do |t|
17
+ t.bigint :evaluation_id, null: false
18
+ t.string :key, null: false
19
+ t.string :group
20
+ t.text :prompt, null: false
21
+ t.text :notes
22
+ t.column :expectations, json_type, **json_default({})
23
+ t.integer :position, default: 0, null: false
24
+ t.boolean :enabled, default: true, null: false
25
+ t.timestamps
26
+ t.index [ :evaluation_id, :key ], unique: true, name: "index_#{prefix}evaluation_scenarios_on_evaluation_and_key"
27
+ t.index [ :evaluation_id, :group ], name: "index_#{prefix}evaluation_scenarios_on_evaluation_and_group"
28
+ end
29
+
30
+ create_table "#{prefix}evaluation_scenario_results" do |t|
31
+ t.bigint :evaluation_run_id, null: false
32
+ t.bigint :evaluation_scenario_id, null: false
33
+ t.bigint :agent_run_id
34
+ t.string :model, null: false
35
+ t.string :provider
36
+ t.integer :status, default: 0, null: false
37
+ t.float :score
38
+ t.column :scores, json_type, **json_default({})
39
+ t.text :output
40
+ t.column :tool_calls, json_type, **json_default([])
41
+ t.integer :duration_ms
42
+ t.integer :input_tokens
43
+ t.integer :output_tokens
44
+ t.decimal :cost, precision: 12, scale: 6
45
+ t.string :fault
46
+ t.text :recommendation
47
+ t.column :diagnosis, json_type, **json_default({})
48
+ t.text :error_message
49
+ t.timestamps
50
+ t.index :evaluation_run_id, name: "index_#{prefix}evaluation_scenario_results_on_run"
51
+ t.index :evaluation_scenario_id, name: "index_#{prefix}evaluation_scenario_results_on_scenario"
52
+ t.index [ :evaluation_run_id, :model ], name: "index_#{prefix}evaluation_scenario_results_on_run_and_model"
53
+ end
54
+
55
+ # Which scenarios and models a run covered, so a partial run (one group,
56
+ # one question) reads as such rather than as the whole suite.
57
+ unless column_exists?("#{prefix}evaluation_runs", :selection)
58
+ add_column "#{prefix}evaluation_runs", :selection, json_type, **json_default({})
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def json_type
65
+ @json_type ||= postgres? ? :jsonb : :json
66
+ end
67
+
68
+ # MySQL rejects a default on a JSON column outright, so the column is
69
+ # created without one there.
70
+ def json_default(value, null: nil)
71
+ return {} unless postgres?
72
+
73
+ null.nil? ? { default: value } : { default: value, null: null }
74
+ end
75
+
76
+ def postgres?
77
+ connection.adapter_name.to_s.downcase.include?("postgres")
78
+ end
79
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :action_agent do
4
+ desc "Seed the dashboard's default agent templates (idempotent — existing slugs are kept)"
5
+ task seed_templates: :environment do
6
+ ActionAgent::AgentTemplate.seed_defaults!
7
+ puts "Agent template library: #{ActionAgent::AgentTemplate.count} templates"
8
+ end
9
+ end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-09-09 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activeagent
@@ -15,7 +16,7 @@ dependencies:
15
16
  requirements:
16
17
  - - ">="
17
18
  - !ruby/object:Gem::Version
18
- version: '1.2'
19
+ version: '1.4'
19
20
  - - "<"
20
21
  - !ruby/object:Gem::Version
21
22
  version: '2'
@@ -25,7 +26,7 @@ dependencies:
25
26
  requirements:
26
27
  - - ">="
27
28
  - !ruby/object:Gem::Version
28
- version: '1.2'
29
+ version: '1.4'
29
30
  - - "<"
30
31
  - !ruby/object:Gem::Version
31
32
  version: '2'
@@ -142,11 +143,14 @@ files:
142
143
  - app/controllers/action_agent/api/tools_controller.rb
143
144
  - app/controllers/action_agent/api/trace_reports_controller.rb
144
145
  - app/controllers/action_agent/api/traces_controller.rb
146
+ - app/controllers/action_agent/api/usage_controller.rb
145
147
  - app/controllers/action_agent/application_controller.rb
146
148
  - app/controllers/action_agent/dashboard_controller.rb
147
149
  - app/controllers/action_agent/traces_controller.rb
150
+ - app/controllers/concerns/action_agent/api/agent_serialization.rb
148
151
  - app/jobs/action_agent/agent_execution_job.rb
149
152
  - app/jobs/action_agent/application_job.rb
153
+ - app/jobs/action_agent/evaluation_run_job.rb
150
154
  - app/jobs/action_agent/process_telemetry_traces_job.rb
151
155
  - app/jobs/action_agent/sandbox_cleanup_job.rb
152
156
  - app/jobs/action_agent/sandbox_provision_job.rb
@@ -165,6 +169,8 @@ files:
165
169
  - app/models/action_agent/application_record.rb
166
170
  - app/models/action_agent/evaluation.rb
167
171
  - app/models/action_agent/evaluation_run.rb
172
+ - app/models/action_agent/evaluation_scenario.rb
173
+ - app/models/action_agent/evaluation_scenario_result.rb
168
174
  - app/models/action_agent/model_pricing.rb
169
175
  - app/models/action_agent/provider_key.rb
170
176
  - app/models/action_agent/recording_action.rb
@@ -178,6 +184,7 @@ files:
178
184
  - app/models/concerns/action_agent/ownable.rb
179
185
  - app/models/concerns/action_agent/session_recordable.rb
180
186
  - app/queries/action_agent/agent_executions.rb
187
+ - app/queries/action_agent/metrics_report.rb
181
188
  - app/serializers/action_agent/agent_message_serializer.rb
182
189
  - app/serializers/action_agent/interaction_preview.rb
183
190
  - app/serializers/action_agent/telemetry_trace_serializer.rb
@@ -187,11 +194,13 @@ files:
187
194
  - app/services/action_agent/agent_scorecard.rb
188
195
  - app/services/action_agent/agent_toolbox.rb
189
196
  - app/services/action_agent/evaluation_runner_service.rb
197
+ - app/services/action_agent/evaluation_tool_resolver.rb
190
198
  - app/services/action_agent/mcp_catalog.rb
191
199
  - app/services/action_agent/mcp_recording_middleware.rb
192
200
  - app/services/action_agent/mock_sandbox_backend.rb
193
201
  - app/services/action_agent/playwright_mcp_client.rb
194
202
  - app/services/action_agent/sandbox_orchestrator.rb
203
+ - app/services/action_agent/scenario_evaluation_runner.rb
195
204
  - app/services/action_agent/session_recording_service.rb
196
205
  - app/services/action_agent/tool_discovery.rb
197
206
  - app/views/action_agent/dashboard/index.html.erb
@@ -211,7 +220,9 @@ files:
211
220
  - lib/generators/action_agent/templates/action_agent.rb.erb
212
221
  - lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb
213
222
  - lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb
223
+ - lib/generators/action_agent/templates/create_active_agent_evaluation_scenarios.rb.erb
214
224
  - lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb
225
+ - lib/tasks/action_agent.rake
215
226
  homepage: https://activeagents.ai
216
227
  licenses:
217
228
  - MIT
@@ -220,6 +231,7 @@ metadata:
220
231
  documentation_uri: https://docs.activeagents.ai/framework/self-hosted-observability
221
232
  source_code_uri: https://github.com/activeagents/activeagent
222
233
  rubygems_mfa_required: 'true'
234
+ post_install_message:
223
235
  rdoc_options: []
224
236
  require_paths:
225
237
  - lib
@@ -227,14 +239,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
227
239
  requirements:
228
240
  - - ">="
229
241
  - !ruby/object:Gem::Version
230
- version: 3.1.0
242
+ version: 3.2.0
231
243
  required_rubygems_version: !ruby/object:Gem::Requirement
232
244
  requirements:
233
245
  - - ">="
234
246
  - !ruby/object:Gem::Version
235
247
  version: '0'
236
248
  requirements: []
237
- rubygems_version: 4.0.6
249
+ rubygems_version: 3.5.22
250
+ signing_key:
238
251
  specification_version: 4
239
252
  summary: The Active Agent dashboard, as a mountable Rails engine
240
253
  test_files: []