actionagent 1.7.2 → 1.8.1
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 +59 -54
- data/app/controllers/action_agent/api/agents_controller.rb +19 -3
- data/app/controllers/action_agent/api/code_sessions_controller.rb +168 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +74 -131
- data/app/controllers/action_agent/api/github_connections_controller.rb +147 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +38 -5
- data/app/controllers/action_agent/api/mcp_servers_controller.rb +10 -1
- data/app/controllers/action_agent/api/provider_keys_controller.rb +54 -3
- data/app/controllers/action_agent/api/provider_models_controller.rb +10 -6
- data/app/controllers/action_agent/api/sandboxes_controller.rb +105 -6
- data/app/controllers/concerns/action_agent/api/evaluation_run_starting.rb +93 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +509 -0
- data/app/controllers/concerns/action_agent/api/run_sandbox.rb +65 -0
- data/app/jobs/action_agent/code_session_job.rb +171 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +80 -11
- data/app/jobs/action_agent/sandbox_provision_job.rb +122 -14
- data/app/jobs/action_agent/sandbox_run_job.rb +10 -3
- data/app/models/action_agent/agent.rb +49 -6
- data/app/models/action_agent/agent_run.rb +20 -1
- data/app/models/action_agent/code_session.rb +145 -0
- data/app/models/action_agent/evaluation.rb +64 -0
- data/app/models/action_agent/evaluation_run.rb +166 -54
- data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
- data/app/models/action_agent/github_connection.rb +75 -0
- data/app/models/action_agent/model_pricing.rb +214 -34
- data/app/models/action_agent/provider_key.rb +148 -10
- data/app/models/action_agent/sandbox_session.rb +194 -17
- data/app/serializers/action_agent/evaluation_serializer.rb +149 -0
- data/app/serializers/action_agent/telemetry_trace_serializer.rb +10 -2
- data/app/services/action_agent/agent_execution_service.rb +4 -2
- data/app/services/action_agent/agent_scorecard.rb +50 -21
- data/app/services/action_agent/agent_tool_roster.rb +20 -9
- data/app/services/action_agent/claude_code_auth.rb +86 -0
- data/app/services/action_agent/codex_session_events.rb +37 -0
- data/app/services/action_agent/dashboard_assistant_service.rb +47 -5
- 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 +25 -5
- data/app/services/action_agent/evaluation_standing.rb +145 -0
- data/app/services/action_agent/evaluation_tool_resolver.rb +18 -0
- data/app/services/action_agent/github_client.rb +111 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1715 -0
- data/app/services/action_agent/local_sandbox_databases.rb +257 -0
- data/app/services/action_agent/mcp_client.rb +5 -1
- data/app/services/action_agent/mcp_tool_dispatcher.rb +137 -17
- data/app/services/action_agent/mock_sandbox_backend.rb +39 -0
- data/app/services/action_agent/ollama_host_probe.rb +75 -0
- data/app/services/action_agent/payload_bounds.rb +36 -0
- data/app/services/action_agent/sandbox_manifest.rb +67 -0
- data/app/services/action_agent/sandbox_orchestrator.rb +82 -14
- data/app/services/action_agent/scenario_evaluation_runner.rb +52 -5
- data/app/services/action_agent/secret_scrubber.rb +37 -0
- data/app/services/action_agent/tool_discovery.rb +19 -5
- data/config/routes.rb +23 -4
- data/lib/action_agent/engine.rb +1 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +153 -3
- data/lib/generators/action_agent/install_generator.rb +35 -5
- data/lib/generators/action_agent/templates/action_agent.rb.erb +47 -0
- data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
- data/lib/generators/action_agent/templates/add_provider_key_api_key.rb.erb +25 -0
- data/lib/generators/action_agent/templates/create_active_agent_code_sessions.rb.erb +59 -0
- data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +2 -0
- data/lib/generators/action_agent/templates/create_active_agent_github_connections.rb.erb +61 -0
- data/lib/tasks/claude_code.rake +16 -0
- data/lib/tasks/sandbox.rake +26 -0
- metadata +28 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# How Claude Code sessions authenticate (ActionAgent.claude_code_auth), and
|
|
5
|
+
# the one rule for whether an owner's Claude Code is "connected", shared by
|
|
6
|
+
# the sandbox listing, the assistant's configuration and the session API so
|
|
7
|
+
# they never disagree.
|
|
8
|
+
#
|
|
9
|
+
# :api_key the owner's Anthropic API key, stored as a ProviderKey. A
|
|
10
|
+
# Claude subscription token stored by an earlier version does
|
|
11
|
+
# not count (ProviderKey#needs_replacing?).
|
|
12
|
+
# :local_login the login of the machine the dashboard runs on, as
|
|
13
|
+
# `claude auth status` reports it (see
|
|
14
|
+
# LocalSandboxBackend.claude_login_status). The dashboard never
|
|
15
|
+
# sees that credential.
|
|
16
|
+
#
|
|
17
|
+
# Anthropic does not let third-party products store or route requests
|
|
18
|
+
# through Claude.ai subscription credentials, so there is no third mode:
|
|
19
|
+
# https://code.claude.com/docs/en/legal-and-compliance.md
|
|
20
|
+
module ClaudeCodeAuth
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @return [String] "api_key" or "local_login"
|
|
24
|
+
def mode
|
|
25
|
+
ActionAgent.claude_code_auth.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def local_login?
|
|
29
|
+
mode == "local_login"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Why +orchestrator+'s backend cannot run Claude Code sessions with the
|
|
33
|
+
# configured authentication, or nil. A machine's own login is the
|
|
34
|
+
# dashboard user's, so only a backend running sessions as that user, on
|
|
35
|
+
# that machine, may use it: a container or a remote host would either
|
|
36
|
+
# find no login or need it copied there, which is what this mode exists
|
|
37
|
+
# to never do.
|
|
38
|
+
def backend_refusal(orchestrator)
|
|
39
|
+
return unless local_login?
|
|
40
|
+
return if orchestrator.local?
|
|
41
|
+
|
|
42
|
+
"ActionAgent.claude_code_auth = :local_login uses this machine's own Claude Code login, so it works only " \
|
|
43
|
+
"with the :local sandbox backend, not #{orchestrator.backend_name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether the owner of +provider_keys+ (a ProviderKey scope already
|
|
47
|
+
# narrowed to them) has an API key Claude Code can run on.
|
|
48
|
+
def api_key_connected?(provider_keys)
|
|
49
|
+
provider_keys.where(provider: "claude_code").any? { |key| key.runtime_environment.present? }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# What the dashboard reports about Claude Code for the owner of
|
|
53
|
+
# +provider_keys+: booleans and the login's method, never a credential.
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] { mode:, connected:, login: } (login for :local_login only)
|
|
56
|
+
def status(provider_keys)
|
|
57
|
+
if local_login?
|
|
58
|
+
# Only the :local backend uses the login, and no other one needs
|
|
59
|
+
# this machine's CLI asked about it.
|
|
60
|
+
login = local_backend? ? LocalSandboxBackend.claude_login_status : LocalSandboxBackend::LOGGED_OUT
|
|
61
|
+
{ mode: mode, connected: login[:logged_in], login: login.slice(:logged_in, :auth_method) }
|
|
62
|
+
else
|
|
63
|
+
{ mode: mode, connected: api_key_connected?(provider_keys) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A backend that cannot even be loaded is not the :local one.
|
|
68
|
+
def local_backend?
|
|
69
|
+
SandboxOrchestrator.new.local?
|
|
70
|
+
rescue StandardError, LoadError
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Why a Claude Code session cannot start in +sandbox+ for want of
|
|
75
|
+
# credentials, or nil.
|
|
76
|
+
def credential_refusal(sandbox)
|
|
77
|
+
if local_login?
|
|
78
|
+
return if LocalSandboxBackend.claude_login_status[:logged_in]
|
|
79
|
+
|
|
80
|
+
"Claude Code is not logged in on this machine: run `claude /login` as the user the dashboard runs as"
|
|
81
|
+
elsif sandbox.runtime_environment.blank?
|
|
82
|
+
"Claude Code is not connected: connect an Anthropic API key in Settings -> Integrations first"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# Keep Codex's native JSONL transcript while adapting its terminal event
|
|
5
|
+
# to CodeSession's shared result contract. A process exit alone is never
|
|
6
|
+
# enough to mark a coding session successful.
|
|
7
|
+
class CodexSessionEvents
|
|
8
|
+
def consume(event)
|
|
9
|
+
return unless event.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
case event["type"]
|
|
12
|
+
when "thread.started"
|
|
13
|
+
@thread_id = event["thread_id"]
|
|
14
|
+
when "item.completed"
|
|
15
|
+
item = event["item"]
|
|
16
|
+
@answer = item["text"] if item.is_a?(Hash) && item["type"] == "agent_message"
|
|
17
|
+
when "turn.completed"
|
|
18
|
+
return result(false, @answer, event["usage"])
|
|
19
|
+
when "turn.failed", "error"
|
|
20
|
+
error = event["error"]
|
|
21
|
+
message = error.is_a?(Hash) ? error["message"] : event["message"]
|
|
22
|
+
return result(true, message.presence || "Codex reported a failed turn", {})
|
|
23
|
+
end
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def result(failed, answer, usage)
|
|
30
|
+
{
|
|
31
|
+
"type" => "result", "subtype" => failed ? "error_during_execution" : "success",
|
|
32
|
+
"is_error" => failed, "result" => answer, "session_id" => @thread_id,
|
|
33
|
+
"usage" => usage.is_a?(Hash) ? usage.slice("input_tokens", "output_tokens") : {}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -31,7 +31,7 @@ module ActionAgent
|
|
|
31
31
|
PROCESSING_DISCLOSURE = "The selected provider receives your current message, bounded conversation history, and authorized report excerpts requested through assistant tools."
|
|
32
32
|
LIMITATIONS = [
|
|
33
33
|
"Reports describe recorded behavior; no repository branch or current main has been verified.",
|
|
34
|
-
"GitHub
|
|
34
|
+
"This assistant cannot connect GitHub, start checkout sandboxes, or run Claude Code sessions: do that in Settings → Integrations. COI execution is not available.",
|
|
35
35
|
"Agent proposals are drafts only. Review them in the builder before saving or running."
|
|
36
36
|
].freeze
|
|
37
37
|
INSTRUCTIONS = <<~TEXT.freeze
|
|
@@ -55,9 +55,12 @@ module ActionAgent
|
|
|
55
55
|
agents. These groups do not provide repository editing, arbitrary browser
|
|
56
56
|
automation, or private database access. Instructions alone do not add tools
|
|
57
57
|
or data access. Explain missing capabilities when proposing an agent.
|
|
58
|
-
GitHub
|
|
59
|
-
evaluations,
|
|
60
|
-
|
|
58
|
+
You cannot connect GitHub, start checkout sandboxes, run Claude Code
|
|
59
|
+
sessions, run evaluations, or publish PRs. The developer connects GitHub
|
|
60
|
+
and Claude Code, starts checkout sandboxes, and runs Claude Code sessions
|
|
61
|
+
in Settings → Integrations; point them there instead of claiming a sandbox
|
|
62
|
+
or session started. COI execution is not available in this version.
|
|
63
|
+
Explain those limitations without suggesting fake authentication links.
|
|
61
64
|
TEXT
|
|
62
65
|
TOOL_DEFINITIONS = [
|
|
63
66
|
{
|
|
@@ -107,7 +110,7 @@ module ActionAgent
|
|
|
107
110
|
providers: DEFAULT_MODELS.map { |id, model| { id: id, configured: provider_configured?(id), default_model: model } },
|
|
108
111
|
defaults: { provider: nil, model: nil },
|
|
109
112
|
processing: { consent_required: true, disclosure: PROCESSING_DISCLOSURE },
|
|
110
|
-
connections:
|
|
113
|
+
connections: connections,
|
|
111
114
|
limits: { message_characters: MAX_MESSAGE_CHARACTERS, history_messages: MAX_HISTORY_MESSAGES, history_characters: MAX_HISTORY_CHARACTERS },
|
|
112
115
|
limitations: LIMITATIONS
|
|
113
116
|
}
|
|
@@ -191,6 +194,45 @@ module ActionAgent
|
|
|
191
194
|
|
|
192
195
|
private
|
|
193
196
|
|
|
197
|
+
# What the owner has set up in Settings → Integrations, reported by the
|
|
198
|
+
# assistant's configuration endpoint (booleans only, never a token). The
|
|
199
|
+
# model is not told: it starts nothing, and sandboxes and Claude Code
|
|
200
|
+
# sessions are started from that view (see LIMITATIONS).
|
|
201
|
+
#
|
|
202
|
+
# Claude Code is connected by ClaudeCodeAuth's rule, as the sandbox
|
|
203
|
+
# listing reports it: an API key the owner connected (auth "api_key"), or
|
|
204
|
+
# this machine's own login (auth "local_login", with its status).
|
|
205
|
+
def connections
|
|
206
|
+
claude_code = ClaudeCodeAuth.status(ProviderKey.for_owner(@owner))
|
|
207
|
+
{
|
|
208
|
+
github: { supported: true, connected: GithubConnection.for_owner(@owner).exists? },
|
|
209
|
+
claude_code: {
|
|
210
|
+
supported: code_sessions_supported?,
|
|
211
|
+
connected: claude_code[:connected],
|
|
212
|
+
auth: claude_code[:mode],
|
|
213
|
+
login: claude_code[:login]
|
|
214
|
+
}.compact,
|
|
215
|
+
coi: { supported: false }
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Claude Code needs a sandbox backend that runs sessions, which :mock and
|
|
220
|
+
# :local do and a host's may not. A backend that cannot even be loaded (a
|
|
221
|
+
# misspelled class in ActionAgent.sandbox_backends, or a class file that
|
|
222
|
+
# requires an SDK the host doesn't bundle) runs none, and must not take
|
|
223
|
+
# the assistant's configuration down with it. The SDK case raises
|
|
224
|
+
# LoadError, a ScriptError rather than a StandardError.
|
|
225
|
+
#
|
|
226
|
+
# Nor can one whose Claude Code authentication does not work there
|
|
227
|
+
# (ActionAgent.claude_code_auth = :local_login needs :local).
|
|
228
|
+
def code_sessions_supported?
|
|
229
|
+
orchestrator = SandboxOrchestrator.new
|
|
230
|
+
orchestrator.supports?(:code_session) && ClaudeCodeAuth.backend_refusal(orchestrator).nil?
|
|
231
|
+
rescue StandardError, LoadError => e
|
|
232
|
+
Rails.logger.warn("[ActionAgent] sandbox backend unavailable: #{e.message}")
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
|
|
194
236
|
def require_processing_consent!
|
|
195
237
|
return if @allow_provider_processing == true
|
|
196
238
|
|
|
@@ -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?
|