actionagent 1.8.1 → 1.9.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 (159) 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 +76 -73
  4. data/app/assets/builds/action_agent_recorder.js +78 -0
  5. data/app/assets/builds/action_agent_replay.js +226 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
  7. data/app/controllers/action_agent/api/agents_controller.rb +1 -0
  8. data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
  9. data/app/controllers/action_agent/api/base_controller.rb +63 -2
  10. data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
  11. data/app/controllers/action_agent/api/code_sessions_controller.rb +53 -6
  12. data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
  13. data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
  14. data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
  15. data/app/controllers/action_agent/api/evaluations_controller.rb +9 -4
  16. data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
  17. data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
  18. data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
  19. data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
  20. data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
  21. data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
  22. data/app/controllers/action_agent/api/members_controller.rb +36 -0
  23. data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
  24. data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
  25. data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
  26. data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
  27. data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
  28. data/app/controllers/action_agent/api/projects_controller.rb +512 -0
  29. data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
  30. data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
  31. data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
  32. data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
  33. data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
  34. data/app/controllers/action_agent/api/sandboxes_controller.rb +140 -8
  35. data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
  36. data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
  37. data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
  38. data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
  39. data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
  40. data/app/controllers/action_agent/dashboard_controller.rb +17 -6
  41. data/app/controllers/action_agent/session_player_controller.rb +58 -0
  42. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
  43. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +623 -18
  44. data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
  45. data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
  46. data/app/jobs/action_agent/agent_execution_job.rb +9 -39
  47. data/app/jobs/action_agent/agent_resume_job.rb +84 -0
  48. data/app/jobs/action_agent/code_session_job.rb +26 -2
  49. data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
  50. data/app/jobs/action_agent/exploration_job.rb +80 -0
  51. data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
  52. data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
  53. data/app/jobs/action_agent/project_setup_job.rb +21 -0
  54. data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
  55. data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
  56. data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
  57. data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
  58. data/app/models/action_agent/agent.rb +39 -26
  59. data/app/models/action_agent/agent_run.rb +134 -23
  60. data/app/models/action_agent/agent_template.rb +35 -1
  61. data/app/models/action_agent/api_key.rb +14 -0
  62. data/app/models/action_agent/catalog_scenario.rb +75 -0
  63. data/app/models/action_agent/code_session.rb +16 -4
  64. data/app/models/action_agent/draft_pull_request.rb +131 -0
  65. data/app/models/action_agent/evaluation.rb +84 -0
  66. data/app/models/action_agent/exploration.rb +847 -0
  67. data/app/models/action_agent/github_installation.rb +159 -0
  68. data/app/models/action_agent/input_request.rb +291 -0
  69. data/app/models/action_agent/project.rb +789 -0
  70. data/app/models/action_agent/project_secret.rb +339 -0
  71. data/app/models/action_agent/provider_key.rb +200 -3
  72. data/app/models/action_agent/recording_event.rb +145 -0
  73. data/app/models/action_agent/recording_snapshot.rb +7 -7
  74. data/app/models/action_agent/sandbox_session.rb +297 -14
  75. data/app/models/action_agent/scenario_catalog.rb +172 -0
  76. data/app/models/action_agent/scenario_product.rb +57 -0
  77. data/app/models/action_agent/scenario_set.rb +138 -0
  78. data/app/models/action_agent/session_recording.rb +121 -14
  79. data/app/models/action_agent/telemetry_trace.rb +74 -19
  80. data/app/models/concerns/action_agent/ownable.rb +84 -13
  81. data/app/serializers/action_agent/input_request_serializer.rb +47 -0
  82. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  83. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  84. data/app/services/action_agent/agent_execution_service.rb +450 -72
  85. data/app/services/action_agent/agent_sync.rb +5 -2
  86. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  87. data/app/services/action_agent/agent_toolbox.rb +215 -9
  88. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  89. data/app/services/action_agent/browser_sidecar.rb +213 -0
  90. data/app/services/action_agent/browser_sign_in.rb +163 -0
  91. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  92. data/app/services/action_agent/claude_code_auth.rb +70 -4
  93. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  94. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  95. data/app/services/action_agent/evaluation_fix.rb +101 -0
  96. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  97. data/app/services/action_agent/evaluation_runner_service.rb +4 -5
  98. data/app/services/action_agent/evaluation_standing.rb +15 -8
  99. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  100. data/app/services/action_agent/github_client.rb +405 -64
  101. data/app/services/action_agent/live_updates.rb +31 -0
  102. data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
  103. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  104. data/app/services/action_agent/mcp_client.rb +6 -2
  105. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  106. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  107. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  108. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  109. data/app/services/action_agent/project_boot_fix.rb +309 -0
  110. data/app/services/action_agent/project_capabilities.rb +200 -0
  111. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  112. data/app/services/action_agent/project_preflight.rb +178 -0
  113. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  114. data/app/services/action_agent/project_setup.rb +429 -0
  115. data/app/services/action_agent/provider_credentials.rb +102 -0
  116. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  117. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  118. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  119. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  120. data/app/services/action_agent/sandbox_browser.rb +299 -0
  121. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  122. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  123. data/app/services/action_agent/sandbox_mail.rb +103 -0
  124. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  125. data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
  126. data/app/services/action_agent/sandbox_patch.rb +253 -0
  127. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  128. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  129. data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
  130. data/app/services/action_agent/secret_scrubber.rb +33 -4
  131. data/app/services/action_agent/session_index.rb +270 -0
  132. data/app/services/action_agent/session_recording_service.rb +11 -0
  133. data/app/services/action_agent/session_timeline.rb +505 -0
  134. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  135. data/app/views/action_agent/session_player/show.html.erb +19 -0
  136. data/config/routes.rb +152 -1
  137. data/lib/action_agent/claude_login_process.rb +107 -0
  138. data/lib/action_agent/engine.rb +55 -5
  139. data/lib/action_agent/secret_requests.rb +80 -0
  140. data/lib/action_agent/version.rb +1 -1
  141. data/lib/action_agent.rb +507 -22
  142. data/lib/generators/action_agent/install_generator.rb +37 -0
  143. data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
  144. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  145. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  146. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  147. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  148. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  149. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  150. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  151. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  152. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  153. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  154. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  155. data/lib/tasks/action_agent.rake +33 -0
  156. data/lib/tasks/browser.rake +22 -0
  157. data/lib/tasks/sandbox.rake +1 -1
  158. data/lib/tasks/scenario_catalogs.rake +71 -0
  159. metadata +96 -2
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # A bounded, server-built brief for one persisted fix card. Request bodies
5
+ # choose an existing card and a subset of its scope; they cannot inject a
6
+ # different prompt, agent, scenario or model into the automatic fix loop.
7
+ class EvaluationFix
8
+ class Invalid < StandardError; end
9
+ attr_reader :run, :item
10
+
11
+ def initialize(run, requested)
12
+ @run = run
13
+ raise Invalid, "Only completed scenario runs have implementable fixes" unless run.complete? && run.evaluation.scenario_suite?
14
+ requested = requested.to_h.deep_stringify_keys
15
+ original = EvaluationSerializer.fix_items(run).find do |candidate|
16
+ candidate = candidate.deep_stringify_keys
17
+ candidate["kind"] == requested["kind"] &&
18
+ (requested["kind"] == "instruction" ? candidate["quote"] == requested["quote"] : candidate["fault"] == requested["fault"])
19
+ end
20
+ raise Invalid, "This fix card no longer exists on the selected run" unless original && %w[fault instruction].include?(requested["kind"])
21
+
22
+ @item = original.deep_stringify_keys.slice("kind", "fault", "quote", "recommendation", "scenario_keys", "models")
23
+ keys = Array(requested["scenario_keys"])
24
+ allowed_models = Array(@item["models"]).presence || model_labels
25
+ models = Array(requested["models"]).presence || allowed_models
26
+ unless keys.any? && (keys - Array(@item["scenario_keys"])).empty? && models.any? && (models - allowed_models).empty?
27
+ raise Invalid, "Choose scenarios and models from this fix card"
28
+ end
29
+ @item.merge!("scenario_keys" => keys.uniq, "models" => models.uniq)
30
+ @item["scenarios"] = run.scenario_results.includes(:scenario).filter_map do |result|
31
+ snapshot = result.evaluated_scenario.slice("key", "prompt", "expectations", "notes", "enabled")
32
+ snapshot if keys.include?(snapshot["key"])
33
+ end.uniq
34
+ self.class.check_scenarios!(run.evaluation, @item)
35
+ end
36
+
37
+ def self.check_scenarios!(evaluation, item)
38
+ Array(item["scenarios"]).each do |snapshot|
39
+ current = evaluation.scenarios.find_by(key: snapshot.fetch("key"))
40
+ unless current&.enabled? && current.as_json_summary.stringify_keys.slice(*snapshot.keys) == snapshot
41
+ raise Invalid, "The selected scenarios changed since this run. Run the evaluation again before implementing a fix."
42
+ end
43
+ end
44
+ end
45
+
46
+ def model_labels
47
+ run.scenario_results.map { |result| [ result.provider, result.model ].compact_blank.join("/") }.uniq
48
+ end
49
+
50
+ # How much of each piece of evidence the brief quotes, at full size. A
51
+ # card with many scenarios and models, or a retry, shrinks them all
52
+ # (see #prompt) rather than refusing the card.
53
+ EXCERPTS = { prompt: 1000, expectations: 1000, diagnosis: 1200, answer: 1000,
54
+ previous_result: 1500, previous_diff: 5000, previous_results: 3000 }.freeze
55
+ SCALES = [ 1, 0.5, 0.25, 0.1 ].freeze
56
+
57
+ def prompt(previous: nil)
58
+ SCALES.each do |scale|
59
+ text = brief(previous, EXCERPTS.transform_values { |size| (size * scale).to_i })
60
+ return text if text.length <= CodeSession::MAX_PROMPT_CHARACTERS
61
+ end
62
+ raise Invalid, "This fix has too much context for one session; run the evaluation on fewer scenarios or models"
63
+ end
64
+
65
+ private
66
+
67
+ def brief(previous, limit)
68
+ agent = run.evaluation.agent
69
+ name = agent.agent_class_name.presence || agent.telemetry_agent_class
70
+ path = name.to_s.underscore
71
+ lines = [ "# Implement one evaluation fix", "", "Agent: #{name}", "Evaluation: #{run.evaluation.name} · run #{run.id}",
72
+ "", "## What to fix", item["fault"], item["recommendation"], item["quote"],
73
+ "", "## Where to look", "- app/agents/#{path}.rb and its tools.",
74
+ "- app/views/agents/#{path.delete_suffix('_agent')}/ and app/views/#{path}/ for instructions and prompts.",
75
+ "- Follow the checkout's own instructions and locate the source of a synced agent before editing.",
76
+ "", "## Failing scenarios" ]
77
+ run.scenario_results.includes(:scenario).each do |result|
78
+ scenario = result.evaluated_scenario
79
+ next unless item["scenario_keys"].include?(scenario["key"])
80
+ label = [ result.provider, result.model ].compact_blank.join("/")
81
+ next unless item["models"].include?(label) || item["models"].include?(result.model)
82
+ lines += [ "", "### #{scenario['key']} · #{label}",
83
+ "Prompt: #{scenario['prompt'].to_s.truncate(limit[:prompt])}", "Expectations: #{scenario['expectations'].to_json.truncate(limit[:expectations])}",
84
+ "Before: #{result.status} · score #{result.score} · fault #{result.fault}",
85
+ "Diagnosis: #{result.diagnosis.to_json.truncate(limit[:diagnosis])}", "Answer: #{result.output.to_s.truncate(limit[:answer])}" ]
86
+ end
87
+ if previous
88
+ lines += [ "", "## Previous attempt", previous.result.to_s.truncate(limit[:previous_result]),
89
+ "Previous diff:", previous.diff.to_s.truncate(limit[:previous_diff]),
90
+ "Verification results: #{EvaluationFixComparison.new(previous).rows.to_json.truncate(limit[:previous_results])}" ]
91
+ end
92
+ lines += [ "", "## Verify", "Change the agent, not the scenarios or their expectations.",
93
+ "Run the relevant checkout tests. Do not commit or push.",
94
+ "The dashboard will re-run these exact scenarios and models against this sandbox when you finish.",
95
+ "Do not call dashboard URLs or dashboard MCP tools from this sandbox.",
96
+ "Never read or copy the sandbox's Claude configuration or credentials.",
97
+ "Summarize the change and any remaining limitations." ]
98
+ lines.compact.join("\n")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ class EvaluationFixComparison
5
+ def initialize(session)
6
+ @session = session
7
+ end
8
+
9
+ def self.change(before, after)
10
+ return "pending" unless after
11
+ return after.passed? ? "added_pass" : "added_fail" unless before
12
+ return after.passed? ? "unchanged_pass" : "regressed" if before.passed?
13
+ after.passed? ? "fixed" : "still_failing"
14
+ end
15
+
16
+ def rows
17
+ before = indexed(@session.evaluation_run)
18
+ after = indexed(@session.verification_run)
19
+ (before.keys | after.keys).map do |key|
20
+ first, last = before[key], after[key]
21
+ { scenario_key: key[0], model: key[1], change: self.class.change(first, last),
22
+ before: first&.status, after: last&.status, before_score: first&.score&.to_f, after_score: last&.score&.to_f,
23
+ score_change: first&.score && last&.score ? (last.score - first.score).to_f.round(3) : nil,
24
+ before_fault: first&.fault, fault: last&.fault }
25
+ end
26
+ end
27
+
28
+ def summary
29
+ { run_id: @session.verification_run_id, status: @session.verification_run&.status,
30
+ error: @session.verification_error || @session.verification_run&.error_message,
31
+ counts: rows.group_by { |row| row[:change] }.transform_values(&:size), rows: rows }
32
+ end
33
+
34
+ # The before/after section a fix's draft pull request carries, cut to
35
+ # +limit+ characters from the end (the session summary goes first), so
36
+ # appending it never pushes a body over DraftPullRequest's maximum.
37
+ def pull_request_body(mount:, limit: DraftPullRequest::MAX_BODY_CHARACTERS)
38
+ base = @session.evaluation_run
39
+ title = @session.fix_item["quote"].presence || @session.fix_item["fault"].to_s.humanize
40
+ lines = [ "## Evaluation fix", title, "",
41
+ "[Original run](#{mount}/evaluations/#{base.evaluation_id}/runs/#{base.id})",
42
+ "[Verification run](#{mount}/evaluations/#{base.evaluation_id}/runs/#{@session.verification_run_id})", "",
43
+ "| Scenario / model | Before | After | Score change | Fault |", "| --- | --- | --- | --- | --- |" ]
44
+ rows.each do |row|
45
+ cells = [ "#{row[:scenario_key]} / #{row[:model]}", row[:before], row[:after] || "pending", row[:score_change], row[:fault] || "—" ]
46
+ lines << "| #{cells.map { |cell| cell.to_s.gsub('|', '\\|').gsub(/[\r\n]/, ' ') }.join(' | ')} |"
47
+ end
48
+ lines += [ "", "## Session summary", @session.result.to_s, "", "Code session ##{@session.id}. Review the diff and full-suite regressions before merging." ]
49
+ SecretScrubber.scrub(lines.join("\n"), @session.secrets).truncate([ limit, 0 ].max, omission: "\n…")
50
+ end
51
+
52
+ private
53
+
54
+ def indexed(run)
55
+ return {} unless run
56
+ item = @session.fix_item || {}
57
+ run.scenario_results.includes(:scenario).each_with_object({}) do |result, index|
58
+ key = result.evaluated_scenario["key"]
59
+ model = [ result.provider, result.model ].compact_blank.join("/")
60
+ next unless Array(item["scenario_keys"]).include?(key) &&
61
+ (Array(item["models"]).include?(model) || Array(item["models"]).include?(result.model))
62
+ index[[ key, model ]] = result
63
+ end
64
+ end
65
+ end
66
+ end
@@ -594,13 +594,12 @@ module ActionAgent
594
594
 
595
595
  # The evaluated agent owner's credential for +name+ (Settings ->
596
596
  # Provider API Keys, or whatever the host app resolves); preferred over
597
- # the host's config/active_agent.yml credentials for the judge.
597
+ # the host's config/active_agent.yml credentials for the judge. Resolved
598
+ # with no actor: the judge scores for the organization, so it never runs
599
+ # on a member's personal key.
598
600
  def owner_provider_options(name)
599
601
  @owner_provider_options ||= {}
600
- @owner_provider_options[name.to_s] ||= begin
601
- from_host = ActionAgent.provider_credentials(owner, name.to_s)
602
- from_host.presence || ProviderKey.for_owner(owner).find_by(provider: name.to_s)&.generation_options || {}
603
- end
602
+ @owner_provider_options[name.to_s] ||= ProviderCredentials.resolve(owner: owner, actor: nil, provider: name).options
604
603
  end
605
604
 
606
605
  def owner
@@ -17,16 +17,19 @@ module ActionAgent
17
17
  # archived — the evaluation is archived (Evaluation#archive!)
18
18
  # none — no complete run
19
19
  #
20
- # Only edits the model can see count as a new version: the instructions,
21
- # the action prompts, the tools, the MCP servers, the model config and
22
- # the response format. A dashboard edit to the agent's appearance cuts an
23
- # AgentVersion like any other edit, but the run before it scored the same
24
- # agent and stays current.
20
+ # Only edits that change how a run goes count as a new version: what the
21
+ # model is given (the instructions, the action prompts, the tools, the MCP
22
+ # servers, the model config and the response format), and the tools whose
23
+ # calls wait for a person's approval, which pause a replay. A dashboard
24
+ # edit to the agent's appearance cuts an AgentVersion like any other edit,
25
+ # but the run before it scored the same agent and stays current.
25
26
  class EvaluationStanding
26
27
  STANDINGS = %w[current stale unrecorded archived none].freeze
27
28
  VERSION_STATES = %w[current earlier unrecorded].freeze
28
29
  # The configuration snapshot keys the model is given.
29
30
  MODEL_FACING = %w[instructions action_prompts tools mcp_servers model_config response_format].freeze
31
+ # The configuration snapshot keys that change how a run goes.
32
+ SCORED = (MODEL_FACING + %w[approval_required_tools]).freeze
30
33
 
31
34
  # Gives every evaluation its standing with one query per table rather
32
35
  # than one per evaluation: the agents' latest versions and whether each
@@ -95,7 +98,7 @@ module ActionAgent
95
98
 
96
99
  latest = latest_version
97
100
  return "current" if latest.nil? || latest.id == version.id
98
- return "current" if model_facing(version) == model_facing(latest)
101
+ return "current" if scored_configuration(version) == scored_configuration(latest)
99
102
 
100
103
  "earlier"
101
104
  end
@@ -137,9 +140,13 @@ module ActionAgent
137
140
  @has_releases = evaluation.agent&.agent_versions&.releases&.exists? || false
138
141
  end
139
142
 
140
- def model_facing(version)
143
+ # A snapshot taken before an agent had an approval list reads as an empty
144
+ # one, so the list's arrival alone cuts no new version.
145
+ def scored_configuration(version)
141
146
  snapshot = version.configuration_snapshot.to_h.stringify_keys
142
- MODEL_FACING.to_h { |key| [ key, snapshot[key] ] }
147
+ configuration = SCORED.to_h { |key| [ key, snapshot[key] ] }
148
+ configuration["approval_required_tools"] = Array(configuration["approval_required_tools"])
149
+ configuration
143
150
  end
144
151
  end
145
152
  end