actionagent 1.3.0 → 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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.css +1 -1
  3. data/app/assets/builds/action_agent.js +70 -48
  4. data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
  5. data/app/controllers/action_agent/api/agents_controller.rb +116 -9
  6. data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +83 -0
  7. data/app/controllers/action_agent/api/evaluations_controller.rb +22 -7
  8. data/app/controllers/action_agent/api/interaction_messages_controller.rb +98 -0
  9. data/app/controllers/action_agent/dashboard_controller.rb +1 -0
  10. data/app/models/action_agent/agent.rb +60 -18
  11. data/app/models/action_agent/agent_run.rb +99 -0
  12. data/app/models/action_agent/evaluation_run.rb +14 -6
  13. data/app/models/action_agent/evaluation_scenario_result.rb +24 -4
  14. data/app/models/action_agent/telemetry_trace.rb +16 -1
  15. data/app/serializers/action_agent/agent_message_serializer.rb +1 -0
  16. data/app/services/action_agent/agent_execution_service.rb +294 -16
  17. data/app/services/action_agent/agent_registrar.rb +7 -6
  18. data/app/services/action_agent/agent_toolbox.rb +45 -3
  19. data/app/services/action_agent/dashboard_assistant_service.rb +342 -0
  20. data/app/services/action_agent/evaluation_evidence.rb +234 -0
  21. data/app/services/action_agent/evaluation_runner_service.rb +13 -3
  22. data/app/services/action_agent/evaluation_tool_resolver.rb +10 -2
  23. data/app/services/action_agent/mcp_client.rb +167 -0
  24. data/app/services/action_agent/mcp_tool_dispatcher.rb +116 -0
  25. data/app/services/action_agent/playwright_mcp_client.rb +11 -126
  26. data/app/services/action_agent/scenario_evaluation_runner.rb +49 -15
  27. data/config/routes.rb +11 -1
  28. data/lib/action_agent/assistant_request_filter.rb +22 -0
  29. data/lib/action_agent/engine.rb +5 -0
  30. data/lib/action_agent/version.rb +1 -1
  31. data/lib/action_agent.rb +32 -0
  32. data/lib/generators/action_agent/templates/action_agent.rb.erb +12 -0
  33. metadata +10 -6
@@ -1,20 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- require "net/http"
5
- require "resolv"
6
-
7
- # Minimal MCP client (streamable HTTP transport) for a Playwright MCP
8
- # server — typically `npx @playwright/mcp --port 8931` running beside the
9
- # app in development, or a sandbox-provisioned browser container in
10
- # production. Speaks just enough JSON-RPC for tools/call: initialize once
11
- # per process, then call tools under the session id the server hands back.
12
- class PlaywrightMCPClient
4
+ # The Playwright MCP server as one configured MCPClient: a process-wide
5
+ # instance pointed at the url PLAYWRIGHT_MCP_URL names.
6
+ class PlaywrightMCPClient < MCPClient
13
7
  DEFAULT_URL = ENV.fetch("PLAYWRIGHT_MCP_URL", "http://host.orb.internal:8931/mcp")
14
- OPEN_TIMEOUT_SECONDS = 5
15
- READ_TIMEOUT_SECONDS = 60
16
-
17
- class Error < StandardError; end
18
8
 
19
9
  def self.instance
20
10
  @instance ||= new
@@ -25,124 +15,19 @@ module ActionAgent
25
15
  end
26
16
 
27
17
  def initialize(url: DEFAULT_URL)
28
- @uri = URI(url)
29
- @mutex = Mutex.new
18
+ super(url: url, label: "Playwright")
30
19
  end
31
20
 
32
- # Returns { text:, is_error: } — the tool result's text content.
21
+ # Restarting the shared instance on an unreachable server is this
22
+ # subclass's concern: the next call re-initializes rather than reusing a
23
+ # session the server has forgotten. The message names the local fix.
33
24
  def call_tool(name, arguments = {})
34
- Rails.logger.debug("[PlaywrightMCPClient] call #{name} args=#{arguments.inspect[0, 200]}")
35
- ensure_session!
36
- response = post(
37
- { jsonrpc: "2.0", id: next_id, method: "tools/call",
38
- params: { name: name, arguments: arguments } },
39
- session: @session_id
40
- )
41
- result = response["result"]
42
- unless result
43
- Rails.logger.warn("[PlaywrightMCPClient] #{name} unexpected response: #{response.inspect[0, 500]}")
44
- raise Error, (response.dig("error", "message") || "empty MCP response")
45
- end
46
-
47
- text = Array(result["content"]).filter_map { |block| block["text"] }.join("\n")
48
- { text: text, is_error: result["isError"] ? true : false }
49
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, SocketError => e
25
+ super
26
+ rescue MCPClient::Error => e
50
27
  self.class.reset!
51
- raise Error, "Playwright MCP server unreachable at #{@uri} (#{e.class}): start it with `npx @playwright/mcp --port 8931`"
52
- end
53
-
54
- private
55
-
56
- def ensure_session!
57
- @mutex.synchronize do
58
- next if @session_id
59
-
60
- _body, response = post_raw(
61
- { jsonrpc: "2.0", id: next_id, method: "initialize",
62
- params: { protocolVersion: "2025-03-26", capabilities: {},
63
- clientInfo: { name: "activeagents", version: "1.0" } } }
64
- )
65
- @session_id = response["mcp-session-id"]
66
- raise Error, "MCP server did not return a session id" unless @session_id
67
-
68
- post({ jsonrpc: "2.0", method: "notifications/initialized" }, session: @session_id)
69
- end
70
- end
71
-
72
- def post(payload, session: nil)
73
- body, _response = post_raw(payload, session: session)
74
- body
75
- end
76
-
77
- def post_raw(payload, session: nil)
78
- # Tool calls run inside the provider SDK's streaming enumerator — a
79
- # fiber, where Net::HTTP reads of SSE bodies misbehave (headers arrive,
80
- # body comes back empty). A dedicated thread always does real blocking
81
- # IO outside any fiber/scheduler context.
82
- Thread.new { blocking_post_raw(payload, session: session) }.value
83
- end
84
-
85
- def blocking_post_raw(payload, session: nil)
86
- http = Net::HTTP.new(@uri.host, @uri.port)
87
- # Container->host bridge hostnames (host.orb.internal) publish an IPv6
88
- # address whose path doesn't reach the server; dual-stack connects then
89
- # fail intermittently. Pin to IPv4 while keeping the Host header.
90
- if (ipv4 = ipv4_address)
91
- http.ipaddr = ipv4
92
- end
93
- http.open_timeout = OPEN_TIMEOUT_SECONDS
94
- http.read_timeout = READ_TIMEOUT_SECONDS
95
- request = Net::HTTP::Post.new(@uri.request_uri)
96
- request["Content-Type"] = "application/json"
97
- request["Accept"] = "application/json, text/event-stream"
98
- request["Mcp-Session-Id"] = session if session
99
- request.body = payload.to_json
100
-
101
- response = http.request(request)
102
- Rails.logger.debug(
103
- "[PlaywrightMCPClient] #{payload[:method]} -> #{response.code} " \
104
- "ct=#{response['Content-Type']} bytes=#{response.body.to_s.bytesize} session=#{session ? 'yes' : 'no'}"
105
- )
106
- unless response.code.to_i.between?(200, 299)
107
- Rails.logger.warn("[PlaywrightMCPClient] HTTP #{response.code}: #{response.body.to_s[0, 300]}")
108
- raise Error, "MCP server returned HTTP #{response.code}"
109
- end
110
-
111
- parsed = parse_body(response)
112
- if parsed.empty? && payload[:id]
113
- Rails.logger.warn("[PlaywrightMCPClient] unparsed body (#{response['Content-Type']}): #{response.body.to_s[0, 500]}")
114
- end
115
- [ parsed, response ]
116
- end
117
-
118
- # Streamable HTTP answers as plain JSON or as an SSE stream whose data:
119
- # lines carry the JSON-RPC response — accept both.
120
- def parse_body(response)
121
- body = response.body.to_s
122
- return {} if body.empty?
123
-
124
- if response["Content-Type"].to_s.include?("text/event-stream")
125
- body.lines
126
- .select { |line| line.start_with?("data:") }
127
- .filter_map { |line| JSON.parse(line.delete_prefix("data:").strip) rescue nil }
128
- .find { |json| json["result"] || json["error"] } || {}
129
- else
130
- JSON.parse(body)
131
- end
132
- rescue JSON::ParserError
133
- {}
134
- end
135
-
136
- def ipv4_address
137
- return @ipv4_address if defined?(@ipv4_address)
138
-
139
- @ipv4_address = Resolv.getaddresses(@uri.host).find { |address| address =~ Resolv::IPv4::Regex }
140
- rescue Resolv::ResolvError
141
- @ipv4_address = nil
142
- end
28
+ raise Error, "#{e.message}: start it with `npx @playwright/mcp --port 8931`" if e.message.include?("unreachable")
143
29
 
144
- def next_id
145
- @id = (@id || 0) + 1
30
+ raise
146
31
  end
147
32
  end
148
33
  end
@@ -45,21 +45,35 @@ module ActionAgent
45
45
  return run
46
46
  end
47
47
 
48
- ensure_judge_defined_kpis! if @evaluation.judge_defined?
49
-
50
48
  records = scenarios.index_by(&:key)
51
- report = Evals::Runner.new(
52
- scenarios: scenarios.map { |scenario| Evals::Scenario.from_hash(scenario.as_json_summary) },
53
- models: specs,
54
- criteria: sample_criteria,
55
- judge: evals_judge,
56
- available_tools: tool_roster,
57
- instructions: @evaluation.agent.instructions,
58
- agent_name: @evaluation.agent.name,
59
- threshold: PASS_THRESHOLD,
60
- replay: ->(scenario, spec) { replay(scenario, spec) },
61
- on_result: ->(result) { persist(run, records.fetch(result.scenario.key), result) }
62
- ).call
49
+ tasks = scenarios.map { |scenario| Evals::Scenario.from_hash(scenario.as_json_summary) }
50
+ expected = tasks.product(specs).map { |task, spec| [ task.key, spec.label ] }
51
+ # Every result tests membership twice, so look the pairs up in a set;
52
+ # `expected` stays an array for the completeness comparison below, and
53
+ # `persisted` sorts the same way either way.
54
+ allowed = expected.to_set
55
+ persisted = Set.new
56
+ on_result = lambda do |result|
57
+ pair = [ result.scenario.key, result.label ]
58
+ raise ArgumentError, "unexpected or duplicate scenario evaluation result" unless allowed.include?(pair) && !persisted.include?(pair)
59
+
60
+ persist(run, records.fetch(result.scenario.key), result)
61
+ persisted << pair
62
+ end
63
+ adapter = ActionAgent.scenario_evaluation_adapter_resolver&.call(@evaluation)
64
+ report = if adapter
65
+ raise ArgumentError, "scenario evaluation adapter must be callable" unless adapter.respond_to?(:call)
66
+
67
+ adapter.call(evaluation: @evaluation, owner: owner, scenarios: tasks, models: specs, on_result: on_result)
68
+ else
69
+ ensure_judge_defined_kpis! if @evaluation.judge_defined?
70
+ default_report(tasks, specs, on_result)
71
+ end
72
+ raise ArgumentError, "scenario evaluation adapter must return an ActiveAgent::Evals::Report" unless report.is_a?(Evals::Report)
73
+ reported = report.results.map { |result| [ result.scenario.key, result.label ] }
74
+ unless reported.sort == expected.sort && persisted.sort == expected.sort
75
+ raise ArgumentError, "scenario evaluation adapter must report and persist every selected scenario and model"
76
+ end
63
77
 
64
78
  run.update!(
65
79
  status: :complete,
@@ -76,6 +90,21 @@ module ActionAgent
76
90
 
77
91
  private
78
92
 
93
+ def default_report(tasks, specs, on_result)
94
+ Evals::Runner.new(
95
+ scenarios: tasks,
96
+ models: specs,
97
+ criteria: sample_criteria,
98
+ judge: evals_judge,
99
+ available_tools: tool_roster,
100
+ instructions: @evaluation.agent.instructions,
101
+ agent_name: @evaluation.agent.name,
102
+ threshold: PASS_THRESHOLD,
103
+ replay: ->(scenario, spec) { replay(scenario, spec) },
104
+ on_result: on_result
105
+ ).call
106
+ end
107
+
79
108
  # --- selection --------------------------------------------------------
80
109
 
81
110
  def selected_scenarios
@@ -184,7 +213,10 @@ module ActionAgent
184
213
  cost: result.replay.cost,
185
214
  fault: result.fault,
186
215
  recommendation: result.recommendation,
187
- diagnosis: result.diagnosis || {},
216
+ diagnosis: (result.diagnosis || {}).merge(
217
+ "_replay_metadata" => result.replay.metadata,
218
+ "_scenario_snapshot" => result.scenario.to_h.merge(expectations: result.scenario.expectations)
219
+ ),
188
220
  error_message: result.replay.error
189
221
  )
190
222
  end
@@ -195,6 +227,8 @@ module ActionAgent
195
227
  scores["_recommendations"] = report.recommendations
196
228
  scores["_verdict"] = report.verdict if report.comparing?
197
229
  scores["_selection"] = run.selection
230
+ scores["_metadata"] = report.metadata
231
+ scores["_judge_label"] = report.judge_label || report.judge&.label
198
232
  scores
199
233
  end
200
234
 
data/config/routes.rb CHANGED
@@ -20,6 +20,8 @@ ActionAgent::Engine.routes.draw do
20
20
 
21
21
  # The dashboard's own JSON API, read and written by the React app.
22
22
  namespace :api do
23
+ resource :dashboard_assistant, only: [ :show, :create ], controller: "dashboard_assistant"
24
+
23
25
  # Telemetry ingestion, relative to wherever the engine is mounted:
24
26
  # <mount>/api/traces (e.g. /activeagents/api/traces at the default mount).
25
27
  # Authenticated with a bearer token, not a session.
@@ -36,6 +38,10 @@ ActionAgent::Engine.routes.draw do
36
38
  post :duplicate
37
39
  get :export
38
40
  get :analytics
41
+ # The runner's conversation picker: this agent's persisted contexts,
42
+ # and a fresh one to pin a first message to.
43
+ get :conversations
44
+ post :conversations, action: :create_conversation
39
45
  end
40
46
  collection do
41
47
  get :presets
@@ -111,7 +117,11 @@ ActionAgent::Engine.routes.draw do
111
117
  resource :metrics, only: [ :show ], controller: "metrics"
112
118
 
113
119
  # Conversations (contexts, messages, generations) behind Interactions.
114
- resources :interactions, only: [ :index, :show ]
120
+ # The runner edits a conversation in place — seeds, fixes or drops a
121
+ # turn — so the next run sees exactly the history it should.
122
+ resources :interactions, only: [ :index, :show ] do
123
+ resources :messages, only: [ :create, :update, :destroy ], controller: "interaction_messages"
124
+ end
115
125
 
116
126
  # Agent output evaluations. A scenario suite also manages its scenarios
117
127
  # here, and exposes each run's per-scenario, per-model results.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Runs before Rails' request logger, including for requests later rejected by
5
+ # authentication, consent or CSRF. Match the endpoint under any engine mount;
6
+ # leave the host application's unrelated message/history parameters alone.
7
+ class AssistantRequestFilter
8
+ PATH = %r{/api/dashboard_assistant(?:\.[^/]+)?/?\z}
9
+ PARAMETERS = [ /\Amessage\z/i, /\Ahistory\z/i ].freeze
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ if PATH.match?(env["PATH_INFO"].to_s)
17
+ env["action_dispatch.parameter_filter"] = Array(env["action_dispatch.parameter_filter"]) + PARAMETERS
18
+ end
19
+ @app.call(env)
20
+ end
21
+ end
22
+ end
@@ -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
@@ -19,6 +21,8 @@ module ActionAgent
19
21
  # rather than relying on the host to register one.
20
22
  INFLECTION_OVERRIDES = {
21
23
  "mcp_catalog" => "MCPCatalog",
24
+ "mcp_client" => "MCPClient",
25
+ "mcp_tool_dispatcher" => "MCPToolDispatcher",
22
26
  "mcp_controller" => "MCPController",
23
27
  "mcp_recording_middleware" => "MCPRecordingMiddleware",
24
28
  "mcp_servers_controller" => "MCPServersController",
@@ -35,6 +39,7 @@ module ActionAgent
35
39
  }.freeze
36
40
 
37
41
  config.action_agent = ActiveSupport::OrderedOptions.new
42
+ config.app_middleware.insert_before Rails::Rack::Logger, ActionAgent::AssistantRequestFilter
38
43
 
39
44
  # Whether a request is a browser asking for a page, as opposed to an API
40
45
  # or MCP client: the routes use it to tell the dashboard's client-side
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.3.0"
4
+ VERSION = "1.5.0"
5
5
  end
data/lib/action_agent.rb CHANGED
@@ -236,6 +236,25 @@ module ActionAgent
236
236
  # @return [Boolean]
237
237
  attr_accessor :execution_enabled
238
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
+
239
258
  # Where the dashboard's upgrade CTAs should send people. Unset in a
240
259
  # self-hosted install, where there is nothing to upgrade, and the CTAs
241
260
  # say so instead of linking nowhere.
@@ -332,6 +351,16 @@ module ActionAgent
332
351
  @execution_enabled != false
333
352
  end
334
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
+
335
364
  # Tells the host app that +owner+ performed +kind+. Never raises: a
336
365
  # bookkeeping failure must not fail the action that was already taken.
337
366
  def record_usage(owner, kind)
@@ -452,6 +481,9 @@ module ActionAgent
452
481
  @provider_credentials_resolver = nil
453
482
  @sandbox_backends = {}
454
483
  @execution_enabled = true
484
+ @assistant_enabled = nil
485
+
486
+ @scenario_evaluation_adapter_resolver = nil
455
487
  @table_name_prefix = "active_agent_"
456
488
  @agent_polymorphic_name = nil
457
489
  @encrypt_credentials = true
@@ -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
  # ==========================================================================
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-09-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activeagent
@@ -129,8 +128,10 @@ files:
129
128
  - app/controllers/action_agent/api/analytics_controller.rb
130
129
  - app/controllers/action_agent/api/api_keys_controller.rb
131
130
  - app/controllers/action_agent/api/base_controller.rb
131
+ - app/controllers/action_agent/api/dashboard_assistant_controller.rb
132
132
  - app/controllers/action_agent/api/evaluations_controller.rb
133
133
  - app/controllers/action_agent/api/instance_tiers_controller.rb
134
+ - app/controllers/action_agent/api/interaction_messages_controller.rb
134
135
  - app/controllers/action_agent/api/interactions_controller.rb
135
136
  - app/controllers/action_agent/api/mcp_controller.rb
136
137
  - app/controllers/action_agent/api/mcp_servers_controller.rb
@@ -193,10 +194,14 @@ files:
193
194
  - app/services/action_agent/agent_registrar.rb
194
195
  - app/services/action_agent/agent_scorecard.rb
195
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
196
199
  - app/services/action_agent/evaluation_runner_service.rb
197
200
  - app/services/action_agent/evaluation_tool_resolver.rb
198
201
  - app/services/action_agent/mcp_catalog.rb
202
+ - app/services/action_agent/mcp_client.rb
199
203
  - app/services/action_agent/mcp_recording_middleware.rb
204
+ - app/services/action_agent/mcp_tool_dispatcher.rb
200
205
  - app/services/action_agent/mock_sandbox_backend.rb
201
206
  - app/services/action_agent/playwright_mcp_client.rb
202
207
  - app/services/action_agent/sandbox_orchestrator.rb
@@ -212,6 +217,7 @@ files:
212
217
  - app/views/layouts/action_agent/react.html.erb
213
218
  - config/routes.rb
214
219
  - lib/action_agent.rb
220
+ - lib/action_agent/assistant_request_filter.rb
215
221
  - lib/action_agent/compatibility.rb
216
222
  - lib/action_agent/engine.rb
217
223
  - lib/action_agent/version.rb
@@ -231,7 +237,6 @@ metadata:
231
237
  documentation_uri: https://docs.activeagents.ai/framework/self-hosted-observability
232
238
  source_code_uri: https://github.com/activeagents/activeagent
233
239
  rubygems_mfa_required: 'true'
234
- post_install_message:
235
240
  rdoc_options: []
236
241
  require_paths:
237
242
  - lib
@@ -246,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
246
251
  - !ruby/object:Gem::Version
247
252
  version: '0'
248
253
  requirements: []
249
- rubygems_version: 3.5.22
250
- signing_key:
254
+ rubygems_version: 4.0.16
251
255
  specification_version: 4
252
256
  summary: The Active Agent dashboard, as a mountable Rails engine
253
257
  test_files: []