actionagent 1.2.2 → 1.5.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 (67) 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 +69 -43
  5. data/app/controllers/action_agent/api/agent_runs_controller.rb +28 -8
  6. data/app/controllers/action_agent/api/agents_controller.rb +191 -56
  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/dashboard_assistant_controller.rb +83 -0
  10. data/app/controllers/action_agent/api/evaluations_controller.rb +252 -7
  11. data/app/controllers/action_agent/api/interaction_messages_controller.rb +98 -0
  12. data/app/controllers/action_agent/api/mcp_controller.rb +13 -3
  13. data/app/controllers/action_agent/api/mcp_servers_controller.rb +28 -8
  14. data/app/controllers/action_agent/api/metrics_controller.rb +44 -11
  15. data/app/controllers/action_agent/api/provider_models_controller.rb +1 -1
  16. data/app/controllers/action_agent/api/sandboxes_controller.rb +6 -0
  17. data/app/controllers/action_agent/api/session_recordings_controller.rb +34 -12
  18. data/app/controllers/action_agent/api/templates_controller.rb +25 -21
  19. data/app/controllers/action_agent/api/traces_controller.rb +25 -5
  20. data/app/controllers/action_agent/api/usage_controller.rb +20 -0
  21. data/app/controllers/action_agent/application_controller.rb +25 -2
  22. data/app/controllers/action_agent/dashboard_controller.rb +3 -1
  23. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +53 -0
  24. data/app/jobs/action_agent/agent_execution_job.rb +40 -20
  25. data/app/jobs/action_agent/application_job.rb +7 -3
  26. data/app/jobs/action_agent/evaluation_run_job.rb +18 -0
  27. data/app/jobs/action_agent/sandbox_cleanup_job.rb +13 -10
  28. data/app/models/action_agent/agent.rb +74 -23
  29. data/app/models/action_agent/agent_run.rb +99 -0
  30. data/app/models/action_agent/agent_template.rb +22 -7
  31. data/app/models/action_agent/evaluation.rb +64 -4
  32. data/app/models/action_agent/evaluation_run.rb +190 -2
  33. data/app/models/action_agent/evaluation_scenario.rb +59 -0
  34. data/app/models/action_agent/evaluation_scenario_result.rb +86 -0
  35. data/app/models/action_agent/recording_action.rb +11 -7
  36. data/app/models/action_agent/sandbox_session.rb +1 -1
  37. data/app/models/action_agent/session_recording.rb +31 -8
  38. data/app/models/action_agent/telemetry_trace.rb +126 -3
  39. data/app/models/concerns/action_agent/adapter_aware.rb +19 -0
  40. data/app/models/concerns/action_agent/ownable.rb +15 -2
  41. data/app/queries/action_agent/metrics_report.rb +498 -0
  42. data/app/serializers/action_agent/agent_message_serializer.rb +1 -0
  43. data/app/services/action_agent/agent_execution_service.rb +294 -16
  44. data/app/services/action_agent/agent_registrar.rb +7 -6
  45. data/app/services/action_agent/agent_toolbox.rb +49 -7
  46. data/app/services/action_agent/dashboard_assistant_service.rb +342 -0
  47. data/app/services/action_agent/evaluation_evidence.rb +234 -0
  48. data/app/services/action_agent/evaluation_runner_service.rb +13 -3
  49. data/app/services/action_agent/evaluation_tool_resolver.rb +162 -0
  50. data/app/services/action_agent/mcp_catalog.rb +46 -8
  51. data/app/services/action_agent/mcp_client.rb +167 -0
  52. data/app/services/action_agent/mcp_recording_middleware.rb +2 -2
  53. data/app/services/action_agent/mcp_tool_dispatcher.rb +116 -0
  54. data/app/services/action_agent/playwright_mcp_client.rb +11 -126
  55. data/app/services/action_agent/sandbox_orchestrator.rb +12 -1
  56. data/app/services/action_agent/scenario_evaluation_runner.rb +260 -0
  57. data/app/services/action_agent/tool_discovery.rb +22 -8
  58. data/config/routes.rb +36 -3
  59. data/lib/action_agent/assistant_request_filter.rb +22 -0
  60. data/lib/action_agent/engine.rb +106 -19
  61. data/lib/action_agent/version.rb +1 -1
  62. data/lib/action_agent.rb +104 -6
  63. data/lib/generators/action_agent/install_generator.rb +20 -7
  64. data/lib/generators/action_agent/templates/action_agent.rb.erb +12 -0
  65. data/lib/generators/action_agent/templates/create_active_agent_evaluation_scenarios.rb.erb +79 -0
  66. data/lib/tasks/action_agent.rake +9 -0
  67. metadata +22 -5
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "assistant_request_filter"
4
+
3
5
  module ActionAgent
4
6
  # Rails engine for the Active Agent dashboard: the agent builder, runs,
5
7
  # conversations, evaluations, traces, metrics, sandboxes and session
@@ -14,13 +16,39 @@ module ActionAgent
14
16
  engine_name "action_agent"
15
17
 
16
18
  # 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
19
+ # camelization, consulted by the inflections initializer below. An engine
20
+ # file that wants a genuine acronym in its constant adds its basename here
21
+ # rather than relying on the host to register one.
22
+ INFLECTION_OVERRIDES = {
23
+ "mcp_catalog" => "MCPCatalog",
24
+ "mcp_client" => "MCPClient",
25
+ "mcp_tool_dispatcher" => "MCPToolDispatcher",
26
+ "mcp_controller" => "MCPController",
27
+ "mcp_recording_middleware" => "MCPRecordingMiddleware",
28
+ "mcp_servers_controller" => "MCPServersController",
29
+ "playwright_mcp_client" => "PlaywrightMCPClient"
30
+ }.freeze
31
+
32
+ # The spelling default camelization produces for each overridden file,
33
+ # mapped to the constant the file actually defines. The const_missing shim
34
+ # below uses it to answer lookups that camelized with default inflections
35
+ # (a plain host's router, or caller code written against the pre-acronym
36
+ # names) with the acronym constant.
37
+ DEFAULT_SPELLINGS = INFLECTION_OVERRIDES.to_h { |basename, constant|
38
+ [ Zeitwerk::Inflector.new.camelize(basename, nil), constant ]
39
+ }.freeze
22
40
 
23
41
  config.action_agent = ActiveSupport::OrderedOptions.new
42
+ config.app_middleware.insert_before Rails::Rack::Logger, ActionAgent::AssistantRequestFilter
43
+
44
+ # Whether a request is a browser asking for a page, as opposed to an API
45
+ # or MCP client: the routes use it to tell the dashboard's client-side
46
+ # deep links apart from protocol traffic on the same path.
47
+ def self.html_request?(request)
48
+ Array(request.accepts).any? { |type| type.respond_to?(:html?) && type.html? }
49
+ rescue StandardError
50
+ false
51
+ end
24
52
 
25
53
  # The dashboard's JS and CSS ship prebuilt in the gem. Adding the
26
54
  # directory to the host app's asset paths is what lets a plain
@@ -34,14 +62,13 @@ module ActionAgent
34
62
  end
35
63
 
36
64
  # This engine's constants are spelled the way Zeitwerk's own inflector
37
- # spells them — Api, McpCatalog, ApiKey — but an engine's files are
65
+ # spells them — Api, ApiKey — but an engine's files are
38
66
  # autoloaded by the host's `rails.main` loader, under the *host's*
39
67
  # inflections. A host that declares `inflect.acronym "API"` or "MCP" (both
40
68
  # 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.
69
+ # ActionAgent::API::TracesController from a file
70
+ # that defines ActionAgent::Api::TracesController. The constant never
71
+ # resolves and the request raises Zeitwerk::NameError.
45
72
  #
46
73
  # Every path under this engine therefore camelizes with Zeitwerk's default
47
74
  # rules, ignoring whatever acronyms the host has registered. Applied by
@@ -51,7 +78,8 @@ module ActionAgent
51
78
  # engine's files from the host's.
52
79
  #
53
80
  # Basenames whose spelling this engine cannot express through default
54
- # camelization (a genuine acronym it wants uppercased) go in OVERRIDES.
81
+ # camelization (a genuine acronym it wants uppercased) go in
82
+ # INFLECTION_OVERRIDES.
55
83
  initializer "action_agent.inflections", before: :set_autoload_paths do
56
84
  engine_root = File.join(root.to_s, "")
57
85
  default = Zeitwerk::Inflector.new
@@ -74,15 +102,22 @@ module ActionAgent
74
102
  # So the namespace answers to both. `const_missing` rather than an eager
75
103
  # alias because the controllers are autoloaded on demand, and naming them at
76
104
  # 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.
105
+ # The router does not consult the autoloader's inflector, so its lookups
106
+ # miss in both directions. An acronym host asks for
107
+ # ActionAgent::API::TracesController while the constant is
108
+ # Api::TracesController: an all-caps run in a missing constant is retried
109
+ # in the spelling default camelization produces (API -> Api). A plain host
110
+ # asks for Api::McpServersController while the constant is
111
+ # MCPServersController (the file is in INFLECTION_OVERRIDES): a missing
112
+ # constant matching an override's default spelling is retried as the
113
+ # acronym constant, via DEFAULT_SPELLINGS — which also keeps caller code
114
+ # written against the pre-acronym names resolving. Only the engine's own
115
+ # namespaces are touched, and only for a constant that is already missing.
84
116
  inflection_shim = Module.new do
85
117
  def const_missing(name)
118
+ acronym = ActionAgent::Engine::DEFAULT_SPELLINGS[name.to_s]
119
+ return const_get(acronym, false) if acronym && const_defined?(acronym, false)
120
+
86
121
  relaxed = name.to_s.gsub(/([A-Z])([A-Z]+)(?=[A-Z][a-z]|\d|\z)/) { "#{$1}#{$2.downcase}" }
87
122
 
88
123
  return super if relaxed == name.to_s || !const_defined?(relaxed, false)
@@ -102,11 +137,63 @@ module ActionAgent
102
137
  end
103
138
  end
104
139
 
140
+ # API keys and provider credentials are encrypted at rest (`encrypts` on
141
+ # ApiKey and ProviderKey), which needs Active Record Encryption keys the
142
+ # host app may never have generated — `rails db:encryption:init` is a
143
+ # step most installs skip, and the engine's own reference host had
144
+ # skipped it too. Without keys, every credential write and every
145
+ # authenticated MCP request raised Errors::Configuration as an HTML 500.
146
+ #
147
+ # Rails seeds ActiveRecord::Encryption from credentials and
148
+ # config.active_record.encryption in its own
149
+ # "active_record_encryption.configuration" initializer. Keys derived
150
+ # after that one land in config but never reach the encryptor, so the
151
+ # config reads back correct while every credential write raises
152
+ # Errors::Configuration — which is what a host that skipped
153
+ # db:encryption:init actually hits. Naming it in `before:` is what puts
154
+ # the derivation ahead of it. When the host set no keys — neither here
155
+ # nor in credentials — derive stable ones from secret_key_base, overridable
156
+ # through the ACTIVE_RECORD_ENCRYPTION_* variables, exactly as the
157
+ # activeagents.ai platform does. Host-provided keys are never
158
+ # overridden, and the derivation is stable as long as secret_key_base is.
159
+ initializer "action_agent.active_record_encryption", before: "active_record_encryption.configuration" do |app|
160
+ next unless ActionAgent.encrypt_credentials
161
+ next unless app.config.respond_to?(:active_record)
162
+
163
+ encryption = app.config.active_record.encryption
164
+ configured = %i[primary_key deterministic_key key_derivation_salt].any? { |key| encryption[key].present? }
165
+ configured ||= begin
166
+ app.credentials.dig(:active_record_encryption, :primary_key).present?
167
+ rescue StandardError
168
+ false
169
+ end
170
+ next if configured
171
+
172
+ derive = lambda do |purpose|
173
+ app.key_generator.generate_key("active_record_encryption.#{purpose}", 32).unpack1("H*")
174
+ end
175
+
176
+ encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].presence || derive.call("primary_key")
177
+ encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"].presence || derive.call("deterministic_key")
178
+ encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"].presence || derive.call("key_derivation_salt")
179
+ end
180
+
105
181
  initializer "action_agent.assets", before: :append_assets_path do |app|
106
182
  builds = root.join("app", "assets", "builds").to_s
107
183
  next unless File.directory?(builds)
108
184
 
109
- next unless app.config.respond_to?(:assets)
185
+ # The prebuilt bundles are served through the host's asset pipeline —
186
+ # propshaft (the Rails default) or sprockets-rails. A host without one
187
+ # (a `rails --api` app, or one that removed propshaft) used to get a
188
+ # silently blank dashboard with two 404s; say why instead.
189
+ unless app.config.respond_to?(:assets)
190
+ Rails.logger&.warn(
191
+ "[ActionAgent] the dashboard's assets cannot be served: this app has no asset pipeline. " \
192
+ "Add propshaft (or sprockets-rails) to the Gemfile so action_agent.js and action_agent.css " \
193
+ "are served from the engine's app/assets/builds."
194
+ )
195
+ next
196
+ end
110
197
 
111
198
  if app.config.assets.respond_to?(:paths)
112
199
  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.5.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
 
@@ -228,12 +236,62 @@ module ActionAgent
228
236
  # @return [Boolean]
229
237
  attr_accessor :execution_enabled
230
238
 
239
+ # Whether the "Ask ActiveAgents" assistant is available.
240
+ #
241
+ # The assistant is a tool for developing and CI-ing agents: it sends
242
+ # recorded prompts, outputs and evaluation report excerpts to a model
243
+ # provider, which is the right trade in a development or CI workspace
244
+ # and a decision nobody should inherit by default in production. Left
245
+ # unset it is on in development and test only. Set it to true to run it
246
+ # somewhere else deliberately, or false to remove it everywhere.
247
+ # @return [Boolean, nil]
248
+ attr_accessor :assistant_enabled
249
+
250
+ # Resolves a host application's runner for one scenario evaluation.
251
+ # Return nil for the engine's normal Agent#test_execute path, or a callable
252
+ # accepting evaluation:, owner:, scenarios:, models:, on_result: and
253
+ # returning an ActiveAgent::Evals::Report. The host runs its own agent and
254
+ # judge and yields every result to on_result for dashboard persistence.
255
+ # @return [Proc, nil]
256
+ attr_accessor :scenario_evaluation_adapter_resolver
257
+
231
258
  # Where the dashboard's upgrade CTAs should send people. Unset in a
232
259
  # self-hosted install, where there is nothing to upgrade, and the CTAs
233
260
  # say so instead of linking nowhere.
234
261
  # @return [String, nil]
235
262
  attr_accessor :upgrade_url
236
263
 
264
+ # The host app's sign-out endpoint, which the header's "Sign out" item
265
+ # POSTs to (with _method=delete and the CSRF token). The engine has no
266
+ # session of its own; unset, the menu item is not shown.
267
+ # @return [String, nil]
268
+ attr_accessor :sign_out_path
269
+
270
+ # Where a browser is sent when it asks for a dashboard page without a
271
+ # valid session — the host app's sign-in page. Unset, an unauthenticated
272
+ # page request gets a minimal session-expired page instead of a bare
273
+ # 401; API clients always get the 401.
274
+ # @return [String, nil]
275
+ attr_accessor :sign_in_path
276
+
277
+ # Answers GET <mount>/api/usage — the plan meter the Organization view
278
+ # and the Run Agents quota banner read. Receives (owner) and returns a
279
+ # Hash in the platform's shape:
280
+ #
281
+ # { runs_used: 12, runs_limit: 100, runs_remaining: 88,
282
+ # can_run: true, plan: "pro" }
283
+ #
284
+ # Unset means unlimited: the engine reports UNLIMITED_USAGE and the
285
+ # views hide the meter.
286
+ # @return [Proc, nil]
287
+ attr_accessor :usage_resolver
288
+
289
+ # What a dashboard with no usage_resolver reports: no limit, nothing
290
+ # counted, always allowed.
291
+ UNLIMITED_USAGE = {
292
+ runs_used: 0, runs_limit: nil, runs_remaining: nil, can_run: true, plan: nil, unlimited: true
293
+ }.freeze
294
+
237
295
  # Called after the dashboard performs a metered action, as
238
296
  # (owner, kind) — the counterpart to quota_checker, for host apps that
239
297
  # track usage against a plan. Unset means nothing is counted.
@@ -262,6 +320,16 @@ module ActionAgent
262
320
  # @return [Boolean]
263
321
  attr_accessor :encrypt_credentials
264
322
 
323
+ # MCP servers the host app itself serves or connects, appended to the
324
+ # built-in catalog (MCPCatalog) so the MCP Services view lists them and
325
+ # telemetry traffic attributes to them. Each entry is a hash shaped like
326
+ # a catalog entry: +key+ and +name+ at minimum, plus any of the optional
327
+ # fields (+description+, +transport+, +url+, +categories+, +docs_url+,
328
+ # +first_party+); +tool_hints+ names the bare tool names that belong to
329
+ # the server. A built-in entry keeps its key on collision.
330
+ # @return [Array<Hash>]
331
+ attr_accessor :mcp_catalog
332
+
265
333
  # Value stored in polymorphic *_type columns for dashboard agents
266
334
  # (agent_memories.memorable_type, agent_contexts.contextable_type).
267
335
  # Unset means the class name. A host app whose existing rows were
@@ -283,6 +351,16 @@ module ActionAgent
283
351
  @execution_enabled != false
284
352
  end
285
353
 
354
+ # Returns whether the dashboard assistant is available. Unconfigured, it
355
+ # follows the environment: development and test yes, everywhere else no.
356
+ #
357
+ # @return [Boolean]
358
+ def assistant_enabled?
359
+ return @assistant_enabled == true unless @assistant_enabled.nil?
360
+
361
+ Rails.env.local?
362
+ end
363
+
286
364
  # Tells the host app that +owner+ performed +kind+. Never raises: a
287
365
  # bookkeeping failure must not fail the action that was already taken.
288
366
  def record_usage(owner, kind)
@@ -292,6 +370,19 @@ module ActionAgent
292
370
  nil
293
371
  end
294
372
 
373
+ # The usage meter for +owner+. Never raises: a bookkeeping failure must
374
+ # not take the views that display it down with it.
375
+ #
376
+ # @return [Hash] the platform's usage shape, UNLIMITED_USAGE by default
377
+ def usage_for(owner)
378
+ return UNLIMITED_USAGE.dup if usage_resolver.nil?
379
+
380
+ usage_resolver.call(owner) || UNLIMITED_USAGE.dup
381
+ rescue StandardError => e
382
+ Rails.logger.warn("[ActionAgent] usage lookup failed: #{e.message}")
383
+ UNLIMITED_USAGE.dup
384
+ end
385
+
295
386
  # Asks the host app whether +owner+ may perform +kind+.
296
387
  #
297
388
  # @return [String, Hash, nil] denial message or payload, nil when allowed
@@ -381,7 +472,7 @@ module ActionAgent
381
472
  @trace_model_class = nil
382
473
  @use_inertia = false
383
474
  @layout = nil
384
- @sandbox_service = :local
475
+ @sandbox_service = :mock
385
476
  @sandbox_limits = nil
386
477
  @storage_service = nil
387
478
  @ingest_api_key = nil
@@ -390,13 +481,20 @@ module ActionAgent
390
481
  @provider_credentials_resolver = nil
391
482
  @sandbox_backends = {}
392
483
  @execution_enabled = true
484
+ @assistant_enabled = nil
485
+
486
+ @scenario_evaluation_adapter_resolver = nil
393
487
  @table_name_prefix = "active_agent_"
394
488
  @agent_polymorphic_name = nil
395
489
  @encrypt_credentials = true
396
490
  @trace_retention = nil
397
491
  @trace_owner_resolver = nil
398
492
  @usage_recorder = nil
493
+ @usage_resolver = nil
399
494
  @upgrade_url = nil
495
+ @sign_out_path = nil
496
+ @sign_in_path = nil
497
+ @mcp_catalog = []
400
498
  end
401
499
  end
402
500
 
@@ -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"
@@ -70,6 +70,18 @@ ActionAgent.configure do |config|
70
70
  # nothing rather than everything. An empty dashboard for a signed-in user
71
71
  # means the resolver above returned nil.
72
72
 
73
+ # ==========================================================================
74
+ # Ask ActiveAgents assistant
75
+ # ==========================================================================
76
+ #
77
+ # A tool for developing and CI-ing agents. Answering a question sends
78
+ # recorded prompts, outputs and evaluation report excerpts to a model
79
+ # provider, so the page and its API are on in development and test only.
80
+ # Set it to true to run it in another environment deliberately, or false
81
+ # to remove it everywhere.
82
+ #
83
+ # config.assistant_enabled = true
84
+
73
85
  # ==========================================================================
74
86
  # UI
75
87
  # ==========================================================================
@@ -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,7 +1,7 @@
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.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.2'
18
+ version: '1.4'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '2'
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '1.2'
28
+ version: '1.4'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '2'
@@ -128,8 +128,10 @@ files:
128
128
  - app/controllers/action_agent/api/analytics_controller.rb
129
129
  - app/controllers/action_agent/api/api_keys_controller.rb
130
130
  - app/controllers/action_agent/api/base_controller.rb
131
+ - app/controllers/action_agent/api/dashboard_assistant_controller.rb
131
132
  - app/controllers/action_agent/api/evaluations_controller.rb
132
133
  - app/controllers/action_agent/api/instance_tiers_controller.rb
134
+ - app/controllers/action_agent/api/interaction_messages_controller.rb
133
135
  - app/controllers/action_agent/api/interactions_controller.rb
134
136
  - app/controllers/action_agent/api/mcp_controller.rb
135
137
  - app/controllers/action_agent/api/mcp_servers_controller.rb
@@ -142,11 +144,14 @@ files:
142
144
  - app/controllers/action_agent/api/tools_controller.rb
143
145
  - app/controllers/action_agent/api/trace_reports_controller.rb
144
146
  - app/controllers/action_agent/api/traces_controller.rb
147
+ - app/controllers/action_agent/api/usage_controller.rb
145
148
  - app/controllers/action_agent/application_controller.rb
146
149
  - app/controllers/action_agent/dashboard_controller.rb
147
150
  - app/controllers/action_agent/traces_controller.rb
151
+ - app/controllers/concerns/action_agent/api/agent_serialization.rb
148
152
  - app/jobs/action_agent/agent_execution_job.rb
149
153
  - app/jobs/action_agent/application_job.rb
154
+ - app/jobs/action_agent/evaluation_run_job.rb
150
155
  - app/jobs/action_agent/process_telemetry_traces_job.rb
151
156
  - app/jobs/action_agent/sandbox_cleanup_job.rb
152
157
  - app/jobs/action_agent/sandbox_provision_job.rb
@@ -165,6 +170,8 @@ files:
165
170
  - app/models/action_agent/application_record.rb
166
171
  - app/models/action_agent/evaluation.rb
167
172
  - app/models/action_agent/evaluation_run.rb
173
+ - app/models/action_agent/evaluation_scenario.rb
174
+ - app/models/action_agent/evaluation_scenario_result.rb
168
175
  - app/models/action_agent/model_pricing.rb
169
176
  - app/models/action_agent/provider_key.rb
170
177
  - app/models/action_agent/recording_action.rb
@@ -178,6 +185,7 @@ files:
178
185
  - app/models/concerns/action_agent/ownable.rb
179
186
  - app/models/concerns/action_agent/session_recordable.rb
180
187
  - app/queries/action_agent/agent_executions.rb
188
+ - app/queries/action_agent/metrics_report.rb
181
189
  - app/serializers/action_agent/agent_message_serializer.rb
182
190
  - app/serializers/action_agent/interaction_preview.rb
183
191
  - app/serializers/action_agent/telemetry_trace_serializer.rb
@@ -186,12 +194,18 @@ files:
186
194
  - app/services/action_agent/agent_registrar.rb
187
195
  - app/services/action_agent/agent_scorecard.rb
188
196
  - app/services/action_agent/agent_toolbox.rb
197
+ - app/services/action_agent/dashboard_assistant_service.rb
198
+ - app/services/action_agent/evaluation_evidence.rb
189
199
  - app/services/action_agent/evaluation_runner_service.rb
200
+ - app/services/action_agent/evaluation_tool_resolver.rb
190
201
  - app/services/action_agent/mcp_catalog.rb
202
+ - app/services/action_agent/mcp_client.rb
191
203
  - app/services/action_agent/mcp_recording_middleware.rb
204
+ - app/services/action_agent/mcp_tool_dispatcher.rb
192
205
  - app/services/action_agent/mock_sandbox_backend.rb
193
206
  - app/services/action_agent/playwright_mcp_client.rb
194
207
  - app/services/action_agent/sandbox_orchestrator.rb
208
+ - app/services/action_agent/scenario_evaluation_runner.rb
195
209
  - app/services/action_agent/session_recording_service.rb
196
210
  - app/services/action_agent/tool_discovery.rb
197
211
  - app/views/action_agent/dashboard/index.html.erb
@@ -203,6 +217,7 @@ files:
203
217
  - app/views/layouts/action_agent/react.html.erb
204
218
  - config/routes.rb
205
219
  - lib/action_agent.rb
220
+ - lib/action_agent/assistant_request_filter.rb
206
221
  - lib/action_agent/compatibility.rb
207
222
  - lib/action_agent/engine.rb
208
223
  - lib/action_agent/version.rb
@@ -211,7 +226,9 @@ files:
211
226
  - lib/generators/action_agent/templates/action_agent.rb.erb
212
227
  - lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb
213
228
  - lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb
229
+ - lib/generators/action_agent/templates/create_active_agent_evaluation_scenarios.rb.erb
214
230
  - lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb
231
+ - lib/tasks/action_agent.rake
215
232
  homepage: https://activeagents.ai
216
233
  licenses:
217
234
  - MIT
@@ -227,14 +244,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
227
244
  requirements:
228
245
  - - ">="
229
246
  - !ruby/object:Gem::Version
230
- version: 3.1.0
247
+ version: 3.2.0
231
248
  required_rubygems_version: !ruby/object:Gem::Requirement
232
249
  requirements:
233
250
  - - ">="
234
251
  - !ruby/object:Gem::Version
235
252
  version: '0'
236
253
  requirements: []
237
- rubygems_version: 4.0.6
254
+ rubygems_version: 4.0.16
238
255
  specification_version: 4
239
256
  summary: The Active Agent dashboard, as a mountable Rails engine
240
257
  test_files: []