actionagent 1.8.0 → 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.
- checksums.yaml +4 -4
- data/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +76 -73
- data/app/assets/builds/action_agent_recorder.js +78 -0
- data/app/assets/builds/action_agent_replay.js +226 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
- data/app/controllers/action_agent/api/agents_controller.rb +1 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
- data/app/controllers/action_agent/api/base_controller.rb +63 -2
- data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
- data/app/controllers/action_agent/api/code_sessions_controller.rb +71 -12
- data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
- data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
- data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +58 -7
- data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
- data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
- data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
- data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
- data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
- data/app/controllers/action_agent/api/members_controller.rb +36 -0
- data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
- data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
- data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
- data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
- data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
- data/app/controllers/action_agent/api/projects_controller.rb +512 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
- data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
- data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
- data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
- data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +148 -8
- data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
- data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
- data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
- data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
- data/app/controllers/action_agent/dashboard_controller.rb +17 -6
- data/app/controllers/action_agent/session_player_controller.rb +58 -0
- data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +627 -20
- data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
- data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
- data/app/jobs/action_agent/agent_execution_job.rb +9 -39
- data/app/jobs/action_agent/agent_resume_job.rb +84 -0
- data/app/jobs/action_agent/code_session_job.rb +39 -10
- data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
- data/app/jobs/action_agent/exploration_job.rb +80 -0
- data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
- data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
- data/app/jobs/action_agent/project_setup_job.rb +21 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
- data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
- data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
- data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
- data/app/models/action_agent/agent.rb +72 -26
- data/app/models/action_agent/agent_run.rb +134 -23
- data/app/models/action_agent/agent_template.rb +35 -1
- data/app/models/action_agent/api_key.rb +14 -0
- data/app/models/action_agent/catalog_scenario.rb +75 -0
- data/app/models/action_agent/code_session.rb +21 -5
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +148 -0
- data/app/models/action_agent/evaluation_run.rb +143 -53
- data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
- data/app/models/action_agent/exploration.rb +847 -0
- data/app/models/action_agent/github_installation.rb +159 -0
- data/app/models/action_agent/input_request.rb +291 -0
- data/app/models/action_agent/model_pricing.rb +214 -34
- data/app/models/action_agent/project.rb +789 -0
- data/app/models/action_agent/project_secret.rb +339 -0
- data/app/models/action_agent/provider_key.rb +207 -4
- data/app/models/action_agent/recording_event.rb +145 -0
- data/app/models/action_agent/recording_snapshot.rb +7 -7
- data/app/models/action_agent/sandbox_session.rb +300 -16
- data/app/models/action_agent/scenario_catalog.rb +172 -0
- data/app/models/action_agent/scenario_product.rb +57 -0
- data/app/models/action_agent/scenario_set.rb +138 -0
- data/app/models/action_agent/session_recording.rb +121 -14
- data/app/models/action_agent/telemetry_trace.rb +74 -19
- data/app/models/concerns/action_agent/ownable.rb +84 -13
- data/app/serializers/action_agent/evaluation_serializer.rb +41 -10
- data/app/serializers/action_agent/input_request_serializer.rb +47 -0
- data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
- data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
- data/app/services/action_agent/agent_execution_service.rb +450 -72
- data/app/services/action_agent/agent_scorecard.rb +50 -21
- data/app/services/action_agent/agent_sync.rb +5 -2
- data/app/services/action_agent/agent_tool_roster.rb +19 -4
- data/app/services/action_agent/agent_toolbox.rb +215 -9
- data/app/services/action_agent/browser_live_ticket.rb +78 -0
- data/app/services/action_agent/browser_sidecar.rb +213 -0
- data/app/services/action_agent/browser_sign_in.rb +163 -0
- data/app/services/action_agent/browser_tool_redaction.rb +92 -0
- data/app/services/action_agent/claude_code_auth.rb +70 -4
- data/app/services/action_agent/codex_session_events.rb +37 -0
- data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
- data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
- data/app/services/action_agent/evaluation_fix.rb +101 -0
- data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
- data/app/services/action_agent/evaluation_report_import.rb +84 -5
- data/app/services/action_agent/evaluation_run_cost.rb +609 -0
- data/app/services/action_agent/evaluation_runner_service.rb +29 -10
- data/app/services/action_agent/evaluation_standing.rb +152 -0
- data/app/services/action_agent/explorer_execution_service.rb +517 -0
- data/app/services/action_agent/github_client.rb +405 -64
- data/app/services/action_agent/live_updates.rb +31 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1263 -102
- data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
- data/app/services/action_agent/mcp_client.rb +6 -2
- data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
- data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
- data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
- data/app/services/action_agent/playwright_mcp_client.rb +7 -0
- data/app/services/action_agent/project_boot_fix.rb +309 -0
- data/app/services/action_agent/project_capabilities.rb +200 -0
- data/app/services/action_agent/project_install_pull_request.rb +225 -0
- data/app/services/action_agent/project_preflight.rb +178 -0
- data/app/services/action_agent/project_secret_discovery.rb +142 -0
- data/app/services/action_agent/project_setup.rb +429 -0
- data/app/services/action_agent/provider_credentials.rb +102 -0
- data/app/services/action_agent/recording_event_ingest.rb +181 -0
- data/app/services/action_agent/runtime_tool_roster.rb +52 -0
- data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
- data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
- data/app/services/action_agent/sandbox_browser.rb +299 -0
- data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
- data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
- data/app/services/action_agent/sandbox_mail.rb +103 -0
- data/app/services/action_agent/sandbox_manifest.rb +166 -13
- data/app/services/action_agent/sandbox_orchestrator.rb +290 -23
- data/app/services/action_agent/sandbox_patch.rb +253 -0
- data/app/services/action_agent/scenario_catalog_import.rb +255 -0
- data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +121 -17
- data/app/services/action_agent/secret_scrubber.rb +33 -4
- data/app/services/action_agent/session_index.rb +270 -0
- data/app/services/action_agent/session_recording_service.rb +11 -0
- data/app/services/action_agent/session_timeline.rb +505 -0
- data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
- data/app/views/action_agent/session_player/show.html.erb +19 -0
- data/config/routes.rb +153 -2
- data/lib/action_agent/claude_login_process.rb +107 -0
- data/lib/action_agent/engine.rb +55 -5
- data/lib/action_agent/secret_requests.rb +80 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +513 -22
- data/lib/generators/action_agent/install_generator.rb +45 -5
- data/lib/generators/action_agent/templates/action_agent.rb.erb +161 -0
- data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
- data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
- data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
- data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
- data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
- data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
- data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
- data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
- data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
- data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
- data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
- data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
- data/lib/tasks/action_agent.rake +33 -0
- data/lib/tasks/browser.rake +22 -0
- data/lib/tasks/sandbox.rake +1 -1
- data/lib/tasks/scenario_catalogs.rake +71 -0
- metadata +100 -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
|
|
@@ -22,7 +22,12 @@ module ActionAgent
|
|
|
22
22
|
#
|
|
23
23
|
# The run's per-model summary, criterion scores and recommendations are
|
|
24
24
|
# computed from the stored results, not taken from the report. The judge's
|
|
25
|
-
# verdict and label are taken from it
|
|
25
|
+
# verdict and label are taken from it, and so is what the judge spent —
|
|
26
|
+
# per result (`result.judge_usage`, kept beside the result's diagnosis) and
|
|
27
|
+
# for the run (`report.judge_usage.run`) — and the release the report says
|
|
28
|
+
# it evaluated (`report.release`), which pins the run to that version of
|
|
29
|
+
# the agent (Agent#find_or_record_release!). A report naming no release
|
|
30
|
+
# leaves the run unrecorded rather than claiming the dashboard's latest.
|
|
26
31
|
#
|
|
27
32
|
# An identical retry returns the stored run, before anything is asked of the
|
|
28
33
|
# `admit` callable. Different content under a run_id already stored raises
|
|
@@ -70,6 +75,11 @@ module ActionAgent
|
|
|
70
75
|
IDENTIFIER_PATTERN = /\A[^[:cntrl:]]{1,200}\z/
|
|
71
76
|
AGENT_NAME_PATTERN = /\A[^[:cntrl:]]{2,100}\z/
|
|
72
77
|
TRACE_PATTERN = /\A[a-zA-Z0-9_-]{1,128}\z/
|
|
78
|
+
DIGEST_PATTERN = /\A[a-zA-Z0-9._-]{1,64}\z/
|
|
79
|
+
# The judge usage keys a report may send, per result and for the run.
|
|
80
|
+
JUDGE_USAGE_KEYS = %w[calls input_tokens output_tokens cost model by_kind source].freeze
|
|
81
|
+
JUDGE_USAGE_SOURCES = %w[reported traces meter].freeze
|
|
82
|
+
MAX_JUDGE_KINDS = 10
|
|
73
83
|
SCOPE_PATTERN = %r{\A[\w .:/@-]{1,100}\z}
|
|
74
84
|
STATUSES = %w[passed failed errored].freeze
|
|
75
85
|
# Report metadata that tells one evaluation of a suite from another, in the
|
|
@@ -253,6 +263,7 @@ module ActionAgent
|
|
|
253
263
|
external_tenant: tenant_key,
|
|
254
264
|
external_run_id: @payload["run_id"],
|
|
255
265
|
external_report_digest: digest,
|
|
266
|
+
agent_version: reported_release_version(agent),
|
|
256
267
|
status: :complete,
|
|
257
268
|
selection: selection,
|
|
258
269
|
scores: recorded_scores,
|
|
@@ -267,6 +278,15 @@ module ActionAgent
|
|
|
267
278
|
run
|
|
268
279
|
end
|
|
269
280
|
|
|
281
|
+
# The agent version for the release the report names, or nil for a
|
|
282
|
+
# report that names none.
|
|
283
|
+
def reported_release_version(agent)
|
|
284
|
+
release = report["release"]
|
|
285
|
+
return nil unless release.is_a?(Hash) && release["digest"].present?
|
|
286
|
+
|
|
287
|
+
agent.find_or_record_release!(digest: release["digest"], revision: release["revision"].presence, label: release["label"].presence)
|
|
288
|
+
end
|
|
289
|
+
|
|
270
290
|
# --- ownership -------------------------------------------------------------
|
|
271
291
|
|
|
272
292
|
# Who a newly observed agent belongs to. Whatever the host's
|
|
@@ -492,11 +512,28 @@ module ActionAgent
|
|
|
492
512
|
"key" => result["scenario_key"], "group" => result["group"], "prompt" => result["prompt"],
|
|
493
513
|
"position" => scenario.position, "expectations" => {}
|
|
494
514
|
}
|
|
495
|
-
),
|
|
515
|
+
).merge(bounded_judge_usage(result["judge_usage"]).then { |usage| usage ? { "_judge_usage" => usage } : {} }),
|
|
496
516
|
error_message: truncated(result["error"], TEXT_BYTES)
|
|
497
517
|
)
|
|
498
518
|
end
|
|
499
519
|
|
|
520
|
+
# A judge usage as the engine stores it: the keys the dashboard reads,
|
|
521
|
+
# each cut to its type, or nil for none.
|
|
522
|
+
def bounded_judge_usage(usage)
|
|
523
|
+
return nil unless usage.is_a?(Hash)
|
|
524
|
+
|
|
525
|
+
by_kind = usage["by_kind"].is_a?(Hash) ? usage["by_kind"].first(MAX_JUDGE_KINDS).to_h { |kind, count| [ kind.to_s.first(40), count.to_i ] } : {}
|
|
526
|
+
{
|
|
527
|
+
"calls" => usage["calls"].to_i,
|
|
528
|
+
"input_tokens" => usage["input_tokens"].to_i,
|
|
529
|
+
"output_tokens" => usage["output_tokens"].to_i,
|
|
530
|
+
"cost" => usage["cost"].is_a?(Numeric) ? usage["cost"].to_f : nil,
|
|
531
|
+
"model" => usage["model"].is_a?(String) ? usage["model"].first(200).presence : nil,
|
|
532
|
+
"by_kind" => by_kind,
|
|
533
|
+
"source" => JUDGE_USAGE_SOURCES.include?(usage["source"]) ? usage["source"] : "reported"
|
|
534
|
+
}
|
|
535
|
+
end
|
|
536
|
+
|
|
500
537
|
# The first +bytes+ bytes of +text+, dropping a character the cut splits,
|
|
501
538
|
# or nil for blank text.
|
|
502
539
|
def truncated(text, bytes)
|
|
@@ -521,14 +558,18 @@ module ActionAgent
|
|
|
521
558
|
{
|
|
522
559
|
"_verdict" => report["verdict"],
|
|
523
560
|
"_selection" => selection,
|
|
524
|
-
"_metadata" => metadata
|
|
561
|
+
"_metadata" => metadata,
|
|
562
|
+
"_judge_usage_run" => bounded_judge_usage(report.dig("judge_usage", "run"))
|
|
525
563
|
}.compact.merge("_judge_label" => judge_label)
|
|
526
564
|
end
|
|
527
565
|
|
|
528
566
|
# The run's scores in the shape the Evaluations view renders
|
|
529
|
-
# (ScenarioEvaluationRunner#scores_for), summarized from the stored
|
|
567
|
+
# (ScenarioEvaluationRunner#scores_for), summarized from the stored
|
|
568
|
+
# results as the application reported them — no cost is estimated here,
|
|
569
|
+
# so the summaries stored stay the application's own figures; the API
|
|
570
|
+
# adds the estimates when it serves the run (EvaluationRun#model_summaries).
|
|
530
571
|
def summarized_scores(run)
|
|
531
|
-
rebuilt = run.to_report
|
|
572
|
+
rebuilt = run.to_report(estimate: false)
|
|
532
573
|
rebuilt.criterion_scores.merge(
|
|
533
574
|
"_models" => rebuilt.summary_by_model,
|
|
534
575
|
"_recommendations" => rebuilt.recommendations
|
|
@@ -559,6 +600,9 @@ module ActionAgent
|
|
|
559
600
|
|
|
560
601
|
judge_trace_ids!(metadata["judge_trace_ids"])
|
|
561
602
|
string!(report["judge"], "report.judge", 200)
|
|
603
|
+
validate_release!
|
|
604
|
+
validate_judge_usage!(report.dig("judge_usage", "run"), "report.judge_usage.run") if report["judge_usage"].is_a?(Hash)
|
|
605
|
+
optional_object!(report["judge_usage"], "report.judge_usage")
|
|
562
606
|
object!(report["models"], "report.models")
|
|
563
607
|
raise Invalid, "report.models must contain 1-#{MAX_MODELS} models" unless report["models"].size.between?(1, MAX_MODELS)
|
|
564
608
|
unless results.is_a?(Array) && results.size.between?(1, MAX_RESULTS)
|
|
@@ -605,6 +649,7 @@ module ActionAgent
|
|
|
605
649
|
end
|
|
606
650
|
trace_id!(result_metadata["trace_id"]) if result_metadata["trace_id"]
|
|
607
651
|
judge_trace_ids!(result_metadata["judge_trace_ids"])
|
|
652
|
+
validate_judge_usage!(result["judge_usage"], "result.judge_usage")
|
|
608
653
|
end
|
|
609
654
|
distinct_specs = label_specs.values.uniq
|
|
610
655
|
raise Invalid, "two model labels name the same provider/model" if distinct_specs.size < label_specs.size
|
|
@@ -655,6 +700,40 @@ module ActionAgent
|
|
|
655
700
|
string!(judge.dig("suggested_tool", "name"), "result.diagnosis.judge.suggested_tool.name", 200)
|
|
656
701
|
end
|
|
657
702
|
|
|
703
|
+
# The release the report says it evaluated: a digest the dashboard can
|
|
704
|
+
# match to a version cut on deploy, and the deploy and label as text.
|
|
705
|
+
def validate_release!
|
|
706
|
+
release = report["release"]
|
|
707
|
+
return if release.nil?
|
|
708
|
+
|
|
709
|
+
object!(release, "report.release")
|
|
710
|
+
unless release["digest"].nil? || (release["digest"].is_a?(String) && DIGEST_PATTERN.match?(release["digest"]))
|
|
711
|
+
raise Invalid, "report.release.digest must be 1-64 letters, digits or . _ -"
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
string!(release["revision"], "report.release.revision", 200)
|
|
715
|
+
string!(release["label"], "report.release.label", 200)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# What the judge spent, as ActiveAgent::Evals::Report writes it per
|
|
719
|
+
# result and for the run.
|
|
720
|
+
def validate_judge_usage!(usage, name)
|
|
721
|
+
return if usage.nil?
|
|
722
|
+
|
|
723
|
+
object!(usage, name)
|
|
724
|
+
%w[calls input_tokens output_tokens].each do |key|
|
|
725
|
+
next if usage[key].nil?
|
|
726
|
+
raise Invalid, "#{name}.#{key} must be a non-negative integer" unless usage[key].is_a?(Integer) && usage[key] >= 0 && usage[key] <= NUMERIC_LIMITS["input_tokens"]
|
|
727
|
+
end
|
|
728
|
+
numeric!(usage["cost"], "#{name}.cost", max: NUMERIC_LIMITS["cost"])
|
|
729
|
+
string!(usage["model"], "#{name}.model", 200)
|
|
730
|
+
string!(usage["source"], "#{name}.source", 20)
|
|
731
|
+
optional_object!(usage["by_kind"], "#{name}.by_kind")
|
|
732
|
+
(usage["by_kind"] || {}).each_value do |count|
|
|
733
|
+
raise Invalid, "#{name}.by_kind counts must be non-negative integers" unless count.is_a?(Integer) && count >= 0
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
658
737
|
def validate_verdict!
|
|
659
738
|
verdict = report["verdict"]
|
|
660
739
|
return if verdict.nil?
|