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.
- 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 +53 -6
- 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 +9 -4
- 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 +140 -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 +623 -18
- 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 +26 -2
- 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 +39 -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 +16 -4
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +84 -0
- 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/project.rb +789 -0
- data/app/models/action_agent/project_secret.rb +339 -0
- data/app/models/action_agent/provider_key.rb +200 -3
- 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 +297 -14
- 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/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_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/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_runner_service.rb +4 -5
- data/app/services/action_agent/evaluation_standing.rb +15 -8
- 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 +1218 -83
- 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 +276 -22
- 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 +119 -16
- 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 +152 -1
- 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 +507 -22
- data/lib/generators/action_agent/install_generator.rb +37 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -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 +96 -2
|
@@ -7,7 +7,13 @@ module ActionAgent
|
|
|
7
7
|
|
|
8
8
|
# Provision a Cloud Run sandbox for the session
|
|
9
9
|
# Each sandbox is an instance of the ActiveAgents application running in sandbox mode
|
|
10
|
-
|
|
10
|
+
#
|
|
11
|
+
# @param options [Hash] for a checkout, at most one of
|
|
12
|
+
# "boot" => how to boot it, as SandboxBootSpec.request_options
|
|
13
|
+
# returns it; without it, "auto"
|
|
14
|
+
# "resume" => { "from" => a step name or nil }, to continue a failed
|
|
15
|
+
# boot its backend kept (SandboxSession#resume_boot!)
|
|
16
|
+
def perform(sandbox_session_id, options = {})
|
|
11
17
|
# Deleted before the job ran: nothing to provision. `find` raised here,
|
|
12
18
|
# and the rescue below then called update! on nil.
|
|
13
19
|
sandbox = SandboxSession.find_by(id: sandbox_session_id)
|
|
@@ -29,10 +35,26 @@ module ActionAgent
|
|
|
29
35
|
# Hand off to whichever backend this install registered — the engine
|
|
30
36
|
# ships the in-memory one and :local, so a real container/job comes
|
|
31
37
|
# from the host app's backend (see ActionAgent.sandbox_backends).
|
|
32
|
-
|
|
38
|
+
if sandbox.app_runtime?
|
|
39
|
+
ensure_checkout_available!(sandbox)
|
|
40
|
+
mint_checkout_token!(sandbox)
|
|
41
|
+
end
|
|
33
42
|
|
|
34
43
|
orchestrator = SandboxOrchestrator.new
|
|
35
|
-
|
|
44
|
+
options = options.is_a?(Hash) ? options : {}
|
|
45
|
+
# A project's checkout boots from the project's spec, built here so its
|
|
46
|
+
# secrets' values are read in this process and never enqueued.
|
|
47
|
+
project = sandbox.app_runtime? ? sandbox.project : nil
|
|
48
|
+
result =
|
|
49
|
+
if options["resume"].is_a?(Hash)
|
|
50
|
+
orchestrator.resume_boot(sandbox, from: options["resume"]["from"].presence, boot_config: project&.boot_spec(sandbox))
|
|
51
|
+
elsif project
|
|
52
|
+
orchestrator.create_sandbox(sandbox, boot_config: project.boot_spec(sandbox))
|
|
53
|
+
elsif (spec = sandbox.app_runtime? && boot_spec(options["boot"]))
|
|
54
|
+
orchestrator.create_sandbox(sandbox, boot_config: spec)
|
|
55
|
+
else
|
|
56
|
+
orchestrator.create_sandbox(sandbox)
|
|
57
|
+
end
|
|
36
58
|
# From here on the backend runs a sandbox for this session: whatever
|
|
37
59
|
# goes wrong below, the rescue releases it unless the session recorded
|
|
38
60
|
# its handle.
|
|
@@ -46,6 +68,7 @@ module ActionAgent
|
|
|
46
68
|
return
|
|
47
69
|
end
|
|
48
70
|
|
|
71
|
+
project&.sandbox_ready!(sandbox, app_models: result[:app_models])
|
|
49
72
|
# Broadcast status update
|
|
50
73
|
broadcast_sandbox_update(sandbox)
|
|
51
74
|
rescue StandardError => e
|
|
@@ -65,6 +88,21 @@ module ActionAgent
|
|
|
65
88
|
|
|
66
89
|
private
|
|
67
90
|
|
|
91
|
+
# The boot spec a checkout's boot options ask for, or nil to boot it as
|
|
92
|
+
# its sandbox.yml says. A spec "auto" asked for that cannot be built
|
|
93
|
+
# (the engine's gems come from a git URL with credentials in it) is
|
|
94
|
+
# dropped with a warning, so a checkout that bundles the engine still
|
|
95
|
+
# boots. One asked for with "always" fails the boot with the reason.
|
|
96
|
+
def boot_spec(boot)
|
|
97
|
+
boot = boot.is_a?(Hash) ? boot : {}
|
|
98
|
+
SandboxBootSpec.for_request(boot)
|
|
99
|
+
rescue SandboxBootSpec::Invalid => e
|
|
100
|
+
raise "Sandbox boot spec is invalid: #{e.message}" if %w[always true].include?(boot["bootstrap"].to_s)
|
|
101
|
+
|
|
102
|
+
Rails.logger.warn("[ActionAgent] not bootstrapping checkouts without the engine: #{e.message}")
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
68
106
|
def simulate_provisioning(sandbox)
|
|
69
107
|
# Simulate a small delay for provisioning
|
|
70
108
|
sleep(0.5)
|
|
@@ -79,7 +117,12 @@ module ActionAgent
|
|
|
79
117
|
# selection, between creating the session and this job running.
|
|
80
118
|
# checkout_spec answers nil for the first and raises ArgumentError for
|
|
81
119
|
# the second; both mean the same thing to the owner.
|
|
120
|
+
# A GitHub App installation can also have been unlinked, or found removed
|
|
121
|
+
# or suspended by an earlier mint.
|
|
82
122
|
def ensure_checkout_available!(sandbox)
|
|
123
|
+
installation = sandbox.checkout_installation
|
|
124
|
+
raise reinstall_message(sandbox, installation.removed_at ? :removed : :suspended) if installation && !installation.usable?
|
|
125
|
+
|
|
83
126
|
available = begin
|
|
84
127
|
sandbox.checkout_spec.present?
|
|
85
128
|
rescue ArgumentError
|
|
@@ -90,6 +133,30 @@ module ActionAgent
|
|
|
90
133
|
raise "#{sandbox.repository} is no longer available: reconnect GitHub or reselect it in Settings -> Integrations"
|
|
91
134
|
end
|
|
92
135
|
|
|
136
|
+
# The one mint of a provision (see SandboxSession#mint_checkout_spec!).
|
|
137
|
+
# The token stays on +sandbox+, the object the orchestrator hands the
|
|
138
|
+
# backend and #secrets_for reads, so the backend clones with exactly the
|
|
139
|
+
# value this job scrubs.
|
|
140
|
+
def mint_checkout_token!(sandbox)
|
|
141
|
+
sandbox.mint_checkout_spec!
|
|
142
|
+
rescue GithubClient::InstallationUnavailable => e
|
|
143
|
+
raise reinstall_message(sandbox, e.reason)
|
|
144
|
+
rescue GithubClient::Error => e
|
|
145
|
+
raise "Could not get a GitHub token to check out #{sandbox.repository}: #{e.message}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def reinstall_message(sandbox, reason)
|
|
149
|
+
installation = sandbox.checkout_installation
|
|
150
|
+
on = installation ? " on #{installation.github_account_login}" : ""
|
|
151
|
+
if reason == :suspended
|
|
152
|
+
"The GitHub App installation#{on} is suspended, so #{sandbox.repository} cannot be checked out. " \
|
|
153
|
+
"Once it is unsuspended on GitHub, use Check again in Settings -> Integrations and start the sandbox again."
|
|
154
|
+
else
|
|
155
|
+
"The GitHub App installation#{on} was removed, so #{sandbox.repository} cannot be checked out. " \
|
|
156
|
+
"Reinstall the GitHub App in Settings -> Integrations and start the sandbox again."
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
93
160
|
# Marks the session ready with the backend's endpoint, under a row lock
|
|
94
161
|
# so a concurrent DELETE either lands first (and this reports false) or
|
|
95
162
|
# finds the session ready with a handle to terminate: SandboxSession#expire!
|
|
@@ -134,13 +201,16 @@ module ActionAgent
|
|
|
134
201
|
return unless sandbox.provisioning?
|
|
135
202
|
|
|
136
203
|
sandbox.update!(status: :failed, error_message: message)
|
|
204
|
+
sandbox.project&.sandbox_failed!(sandbox)
|
|
137
205
|
broadcast_sandbox_update(sandbox)
|
|
138
206
|
rescue ActiveRecord::RecordNotFound
|
|
139
207
|
nil
|
|
140
208
|
end
|
|
141
209
|
|
|
142
|
-
# What must never reach error_message: the checkout token
|
|
143
|
-
# Code credential this session boots with.
|
|
210
|
+
# What must never reach error_message: the checkout token, the Claude
|
|
211
|
+
# Code credential this session boots with, and its project's secrets.
|
|
212
|
+
# Reads the checkout without minting, so a GitHub App checkout
|
|
213
|
+
# contributes the token this job minted.
|
|
144
214
|
def secrets_for(sandbox)
|
|
145
215
|
return [] if sandbox.nil?
|
|
146
216
|
|
|
@@ -149,16 +219,13 @@ module ActionAgent
|
|
|
149
219
|
rescue StandardError
|
|
150
220
|
nil
|
|
151
221
|
end
|
|
152
|
-
[ spec&.dig(:token), *sandbox.runtime_environment.values ].compact
|
|
222
|
+
[ spec&.dig(:token), *sandbox.runtime_environment.values, *sandbox.project_scrub_values ].compact
|
|
153
223
|
rescue StandardError
|
|
154
224
|
[]
|
|
155
225
|
end
|
|
156
226
|
|
|
157
227
|
def broadcast_sandbox_update(sandbox)
|
|
158
|
-
|
|
159
|
-
"sandbox_#{sandbox.session_id}",
|
|
160
|
-
{ type: "status_update", sandbox: sandbox.summary }
|
|
161
|
-
)
|
|
228
|
+
LiveUpdates.broadcast("sandbox_#{sandbox.session_id}", type: "status_update", id: sandbox.session_id, status: sandbox.status)
|
|
162
229
|
end
|
|
163
230
|
end
|
|
164
231
|
end
|
|
@@ -18,7 +18,7 @@ module ActionAgent
|
|
|
18
18
|
started_at = Time.current
|
|
19
19
|
|
|
20
20
|
# Broadcast that run has started
|
|
21
|
-
broadcast_run_started(sandbox, run_id
|
|
21
|
+
broadcast_run_started(sandbox, run_id)
|
|
22
22
|
|
|
23
23
|
begin
|
|
24
24
|
# Use ActiveAgent with generate_later for async processing
|
|
@@ -32,7 +32,7 @@ module ActionAgent
|
|
|
32
32
|
rescue => e
|
|
33
33
|
Rails.logger.error("Sandbox run failed: #{e.message}")
|
|
34
34
|
back_to_ready(sandbox, error_message: e.message)
|
|
35
|
-
broadcast_run_error(sandbox, run_id
|
|
35
|
+
broadcast_run_error(sandbox, run_id)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -247,46 +247,18 @@ module ActionAgent
|
|
|
247
247
|
PROMPT
|
|
248
248
|
end
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
type: "run_started",
|
|
255
|
-
run_id: run_id,
|
|
256
|
-
provider: provider,
|
|
257
|
-
task: task,
|
|
258
|
-
started_at: Time.current.iso8601
|
|
259
|
-
}
|
|
260
|
-
)
|
|
261
|
-
Rails.logger.info "[SandboxRunJob] Broadcast run_started for #{provider} (#{run_id})"
|
|
250
|
+
# The run messages carry the job's run_id; the run itself is read back
|
|
251
|
+
# from the session.
|
|
252
|
+
def broadcast_run_started(sandbox, run_id)
|
|
253
|
+
LiveUpdates.broadcast("sandbox_#{sandbox.session_id}", type: "run_started", id: run_id, status: "running")
|
|
262
254
|
end
|
|
263
255
|
|
|
264
256
|
def broadcast_run_complete(sandbox, run_id, run)
|
|
265
|
-
|
|
266
|
-
"sandbox_#{sandbox.session_id}",
|
|
267
|
-
{
|
|
268
|
-
type: "run_complete",
|
|
269
|
-
run_id: run_id,
|
|
270
|
-
provider: run[:provider],
|
|
271
|
-
run: run,
|
|
272
|
-
sandbox: sandbox.summary
|
|
273
|
-
}
|
|
274
|
-
)
|
|
275
|
-
Rails.logger.info "[SandboxRunJob] Broadcast run_complete for #{run[:provider]} (#{run_id})"
|
|
257
|
+
LiveUpdates.broadcast("sandbox_#{sandbox.session_id}", type: "run_complete", id: run_id, status: run[:status])
|
|
276
258
|
end
|
|
277
259
|
|
|
278
|
-
def broadcast_run_error(sandbox, run_id
|
|
279
|
-
|
|
280
|
-
"sandbox_#{sandbox.session_id}",
|
|
281
|
-
{
|
|
282
|
-
type: "run_error",
|
|
283
|
-
run_id: run_id,
|
|
284
|
-
provider: provider,
|
|
285
|
-
error: error,
|
|
286
|
-
sandbox: sandbox.summary
|
|
287
|
-
}
|
|
288
|
-
)
|
|
289
|
-
Rails.logger.info "[SandboxRunJob] Broadcast run_error for #{provider} (#{run_id})"
|
|
260
|
+
def broadcast_run_error(sandbox, run_id)
|
|
261
|
+
LiveUpdates.broadcast("sandbox_#{sandbox.session_id}", type: "run_error", id: run_id, status: "failed")
|
|
290
262
|
end
|
|
291
263
|
end
|
|
292
264
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class VerifyEvaluationFixJob < ApplicationJob
|
|
5
|
+
queue_as :sandboxes
|
|
6
|
+
|
|
7
|
+
def perform(id)
|
|
8
|
+
session = CodeSession.find_by(id: id)
|
|
9
|
+
return unless session&.succeeded? && session.evaluation_run_id && session.diff.present?
|
|
10
|
+
sandbox = session.sandbox_session
|
|
11
|
+
run = nil
|
|
12
|
+
# Claim the verification under the checkout's lock: the pending run is
|
|
13
|
+
# what CodeSessionsController#busy_session waits on, so no later edit
|
|
14
|
+
# can race it. The restart happens after the lock is released, since
|
|
15
|
+
# it takes as long as a boot and the lock is a database row lock.
|
|
16
|
+
sandbox.with_lock do
|
|
17
|
+
session.reload
|
|
18
|
+
return if session.verification_run_id
|
|
19
|
+
raise "No reviewable git diff was captured for this fix" unless session.diff.start_with?("diff --git ")
|
|
20
|
+
raise "The sandbox is no longer running" unless sandbox.ready? && sandbox.active?
|
|
21
|
+
raise "Agent execution is disabled" unless ActionAgent.execution_enabled?
|
|
22
|
+
EvaluationFix.check_scenarios!(session.evaluation_run.evaluation, session.fix_item)
|
|
23
|
+
owner = session.evaluation_run.evaluation.agent.owner
|
|
24
|
+
raise "The sandbox is no longer available to the agent" unless SandboxSession.runtime_server_entry(sandbox.runtime_server_key, owner: owner)
|
|
25
|
+
selection = { "sandbox_id" => sandbox.session_id, "keys" => session.fix_item.fetch("scenario_keys"), "models" => session.fix_item.fetch("models") }
|
|
26
|
+
run = session.evaluation_run.evaluation.evaluation_runs.create!(status: :pending, selection: selection)
|
|
27
|
+
session.update!(verification_run: run, verification_error: nil)
|
|
28
|
+
end
|
|
29
|
+
SandboxOrchestrator.new.refresh_runtime(sandbox)
|
|
30
|
+
EvaluationRunJob.perform_later(run.evaluation_id, run.id, run.selection)
|
|
31
|
+
rescue StandardError => error
|
|
32
|
+
message = SecretScrubber.scrub(error.message, session&.secrets).truncate(1000)
|
|
33
|
+
run&.update!(status: :failed, error_message: message, completed_at: Time.current)
|
|
34
|
+
session&.update!(verification_error: message)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -88,7 +88,7 @@ module ActionAgent
|
|
|
88
88
|
# serialize. This constant stays the built-in set so existing references
|
|
89
89
|
# keep their meaning.
|
|
90
90
|
AVAILABLE_TOOLS = %w[
|
|
91
|
-
terminal playwright filesystem code database slack fetch search edit translate memory agents ui
|
|
91
|
+
terminal playwright filesystem code database slack fetch search edit translate memory agents ui ask
|
|
92
92
|
].freeze
|
|
93
93
|
|
|
94
94
|
# One line per capability, for the roster rows that offer them. A name
|
|
@@ -107,7 +107,8 @@ module ActionAgent
|
|
|
107
107
|
"translate" => "Translates text through the translation agent.",
|
|
108
108
|
"memory" => "Reads and writes durable notes across runs of this agent.",
|
|
109
109
|
"agents" => "Delegates a task to another agent in this workspace.",
|
|
110
|
-
"ui" => "Renders a form or table back into the chat surface."
|
|
110
|
+
"ui" => "Renders a form or table back into the chat surface.",
|
|
111
|
+
"ask" => "Pauses the run to ask the user a question or for approval, and continues with the answer."
|
|
111
112
|
}.freeze
|
|
112
113
|
|
|
113
114
|
# Every tool an agent may enable: the built-ins plus each tool generated by
|
|
@@ -246,16 +247,28 @@ module ActionAgent
|
|
|
246
247
|
appearance: appearance,
|
|
247
248
|
instruction_sets: instruction_sets,
|
|
248
249
|
tools: tools,
|
|
250
|
+
approval_required_tools: approval_required_tools,
|
|
249
251
|
mcp_servers: mcp_servers,
|
|
250
252
|
model_config: model_config,
|
|
251
253
|
response_format: response_format
|
|
252
254
|
}
|
|
253
255
|
end
|
|
254
256
|
|
|
257
|
+
# The tool names whose calls wait for a person to approve them before
|
|
258
|
+
# they run: toolbox tools, schema tools and the agent's MCP servers'
|
|
259
|
+
# tools alike. Empty on an install that has not run the migration adding
|
|
260
|
+
# the column.
|
|
261
|
+
# @return [Array<String>]
|
|
262
|
+
def approval_required_tools
|
|
263
|
+
return [] unless has_attribute?(:approval_required_tools)
|
|
264
|
+
|
|
265
|
+
Array(self[:approval_required_tools]).map(&:to_s).reject(&:blank?).uniq
|
|
266
|
+
end
|
|
267
|
+
|
|
255
268
|
# Restore from a version
|
|
256
269
|
def restore_from_version!(version)
|
|
257
270
|
config = version.configuration_snapshot
|
|
258
|
-
|
|
271
|
+
attributes = {
|
|
259
272
|
instructions: config["instructions"],
|
|
260
273
|
action_prompts: config["action_prompts"] || [],
|
|
261
274
|
preset_type: config["preset_type"],
|
|
@@ -265,7 +278,9 @@ module ActionAgent
|
|
|
265
278
|
mcp_servers: config["mcp_servers"],
|
|
266
279
|
model_config: config["model_config"],
|
|
267
280
|
response_format: config["response_format"]
|
|
268
|
-
|
|
281
|
+
}
|
|
282
|
+
attributes[:approval_required_tools] = config["approval_required_tools"] || [] if has_attribute?(:approval_required_tools)
|
|
283
|
+
update!(attributes)
|
|
269
284
|
end
|
|
270
285
|
|
|
271
286
|
# Get the latest version
|
|
@@ -405,12 +420,16 @@ module ActionAgent
|
|
|
405
420
|
# run reaches as if the agent had it in mcp_servers, without saving it on
|
|
406
421
|
# the agent. The caller checks the sandbox is theirs and live; the
|
|
407
422
|
# dispatcher still resolves it among this agent's owner's sessions only.
|
|
423
|
+
#
|
|
424
|
+
# A block is given the run before its job is enqueued, so whatever it
|
|
425
|
+
# records about the run is there when the job starts.
|
|
408
426
|
def execute(input_prompt, action: nil, attachments: [], actor: nil, runtime_sandbox: nil, **params)
|
|
409
427
|
ensure_executable!
|
|
410
428
|
run = create_run(
|
|
411
429
|
input_prompt, action: action, attachments: attachments, params: params,
|
|
412
430
|
actor: actor, runtime_sandbox: runtime_sandbox, status: :pending
|
|
413
431
|
)
|
|
432
|
+
yield run if block_given?
|
|
414
433
|
|
|
415
434
|
# Queue the execution job
|
|
416
435
|
AgentExecutionJob.perform_later(run.id)
|
|
@@ -418,7 +437,9 @@ module ActionAgent
|
|
|
418
437
|
run
|
|
419
438
|
end
|
|
420
439
|
|
|
421
|
-
# Quick test execution (synchronous)
|
|
440
|
+
# Quick test execution (synchronous). A run that pauses for input comes
|
|
441
|
+
# back `awaiting_input` with its requests stored, and AgentResumeJob
|
|
442
|
+
# finishes it once they are answered.
|
|
422
443
|
def test_execute(input_prompt, action: nil, attachments: [], actor: nil, runtime_sandbox: nil, **params)
|
|
423
444
|
ensure_executable!
|
|
424
445
|
run = create_run(
|
|
@@ -431,27 +452,9 @@ module ActionAgent
|
|
|
431
452
|
# Build and execute the agent
|
|
432
453
|
result = AgentExecutionService.call(self, run)
|
|
433
454
|
|
|
434
|
-
run.
|
|
435
|
-
output: result[:output],
|
|
436
|
-
output_metadata: result[:metadata],
|
|
437
|
-
status: :complete,
|
|
438
|
-
completed_at: Time.current,
|
|
439
|
-
duration_ms: ((Time.current - run.started_at) * 1000).to_i,
|
|
440
|
-
input_tokens: result.dig(:usage, :input_tokens),
|
|
441
|
-
output_tokens: result.dig(:usage, :output_tokens),
|
|
442
|
-
total_tokens: result.dig(:usage, :total_tokens)
|
|
443
|
-
)
|
|
455
|
+
run.record_result!(result, segment_started_at: run.started_at)
|
|
444
456
|
rescue => e
|
|
445
|
-
run.
|
|
446
|
-
status: :failed,
|
|
447
|
-
completed_at: Time.current,
|
|
448
|
-
error_message: e.message,
|
|
449
|
-
error_backtrace: e.backtrace&.first(10)&.join("\n"),
|
|
450
|
-
# The class, not only the message: an agent that refused this
|
|
451
|
-
# caller and an agent that broke both fail the run, and only the
|
|
452
|
-
# class tells them apart without reading prose.
|
|
453
|
-
output_metadata: run.output_metadata.to_h.merge("error_class" => e.class.name)
|
|
454
|
-
)
|
|
457
|
+
run.record_failure!(e)
|
|
455
458
|
end
|
|
456
459
|
|
|
457
460
|
run
|
|
@@ -490,6 +493,8 @@ module ActionAgent
|
|
|
490
493
|
input_params = AgentRun.params_with_actor(params, actor)
|
|
491
494
|
if SandboxSession.runtime_server_key?(runtime_sandbox)
|
|
492
495
|
input_params = input_params.merge(AgentRun::SANDBOX_PARAM => runtime_sandbox.to_s)
|
|
496
|
+
browser = running_browser_key(runtime_sandbox)
|
|
497
|
+
input_params = input_params.merge(AgentRun::BROWSER_PARAM => browser) if browser
|
|
493
498
|
end
|
|
494
499
|
|
|
495
500
|
run = agent_runs.create!(
|
|
@@ -562,7 +567,7 @@ module ActionAgent
|
|
|
562
567
|
|
|
563
568
|
VERSIONED_FIELDS = %w[
|
|
564
569
|
instructions action_prompts preset_type appearance instruction_sets
|
|
565
|
-
tools mcp_servers model_config response_format
|
|
570
|
+
tools approval_required_tools mcp_servers model_config response_format
|
|
566
571
|
].freeze
|
|
567
572
|
|
|
568
573
|
def configuration_changed?
|
|
@@ -580,6 +585,14 @@ module ActionAgent
|
|
|
580
585
|
)
|
|
581
586
|
end
|
|
582
587
|
|
|
588
|
+
# The browser key of the sandbox +runtime_sandbox+ names, when that
|
|
589
|
+
# sandbox is this agent's owner's and its browser is running.
|
|
590
|
+
def running_browser_key(runtime_sandbox)
|
|
591
|
+
session_id = runtime_sandbox.to_s.delete_prefix(SandboxSession::RUNTIME_SERVER_PREFIX)
|
|
592
|
+
sandbox = SandboxSession.for_owner(owner).find_by(session_id: session_id)
|
|
593
|
+
sandbox.browser_server_key if sandbox&.browser_running?
|
|
594
|
+
end
|
|
595
|
+
|
|
583
596
|
# Unknown action names fall back to the default rather than failing the
|
|
584
597
|
# run — an action can be renamed between enqueue and execution.
|
|
585
598
|
def normalized_action(action)
|
|
@@ -7,6 +7,9 @@ module ActionAgent
|
|
|
7
7
|
# time, since a run is against the agent as it is.
|
|
8
8
|
belongs_to :agent_version, optional: true
|
|
9
9
|
before_create { self.agent_version_id ||= agent&.latest_version&.id }
|
|
10
|
+
has_many :input_requests, as: :subject, inverse_of: :subject, dependent: :destroy
|
|
11
|
+
has_many :pending_input_requests, -> { pending.for_listing.order(:id) },
|
|
12
|
+
class_name: "ActionAgent::InputRequest", as: :subject, inverse_of: :subject
|
|
10
13
|
|
|
11
14
|
# Raised when a caller hands a run files to attach in a host app that
|
|
12
15
|
# has nowhere to keep them.
|
|
@@ -20,9 +23,10 @@ module ActionAgent
|
|
|
20
23
|
# the model (images and PDFs as data URIs, text inlined) and the
|
|
21
24
|
# persisted user message keeps a manifest of them.
|
|
22
25
|
#
|
|
23
|
-
#
|
|
24
|
-
# --skip-active-storage has no has_many_attached to call
|
|
25
|
-
|
|
26
|
+
# Per ActionAgent.active_storage, like RecordingSnapshot: a host app
|
|
27
|
+
# created with --skip-active-storage has no has_many_attached to call,
|
|
28
|
+
# and a host may switch attachments off.
|
|
29
|
+
has_many_attached :attachments, **ActionAgent.attachment_options if ActionAgent.active_storage_macros?
|
|
26
30
|
|
|
27
31
|
# The key the caller's identity is recorded under in +input_params+.
|
|
28
32
|
# Underscored so it cannot collide with a provider override, and
|
|
@@ -33,6 +37,11 @@ module ActionAgent
|
|
|
33
37
|
# the server, after it checked the caller owns that sandbox, and
|
|
34
38
|
# stripped from anything a client sends, as the actor is.
|
|
35
39
|
SANDBOX_PARAM = "_sandbox_server"
|
|
40
|
+
# The key of that sandbox's browser ("browser:<session_id>"), recorded
|
|
41
|
+
# when the browser was running as the run was created, so a run whose
|
|
42
|
+
# browser stops before it executes fails rather than running without
|
|
43
|
+
# it. Set and stripped like SANDBOX_PARAM.
|
|
44
|
+
BROWSER_PARAM = "_sandbox_browser"
|
|
36
45
|
|
|
37
46
|
# +input_params+ with the caller recorded alongside them.
|
|
38
47
|
#
|
|
@@ -47,7 +56,8 @@ module ActionAgent
|
|
|
47
56
|
# @param actor [Object, nil] the caller
|
|
48
57
|
# @return [Hash]
|
|
49
58
|
def self.params_with_actor(params, actor)
|
|
50
|
-
params = (params || {}).to_h.except(ACTOR_PARAM, ACTOR_PARAM.to_sym, SANDBOX_PARAM, SANDBOX_PARAM.to_sym
|
|
59
|
+
params = (params || {}).to_h.except(ACTOR_PARAM, ACTOR_PARAM.to_sym, SANDBOX_PARAM, SANDBOX_PARAM.to_sym,
|
|
60
|
+
BROWSER_PARAM, BROWSER_PARAM.to_sym)
|
|
51
61
|
gid = actor.respond_to?(:to_global_id) ? actor.to_global_id.to_s : nil
|
|
52
62
|
gid ? params.merge(ACTOR_PARAM => gid) : params
|
|
53
63
|
rescue StandardError => e
|
|
@@ -83,6 +93,14 @@ module ActionAgent
|
|
|
83
93
|
sandbox_server_key&.delete_prefix(SandboxSession::RUNTIME_SERVER_PREFIX)
|
|
84
94
|
end
|
|
85
95
|
|
|
96
|
+
# The "browser:<session_id>" key of that sandbox's browser, when it was
|
|
97
|
+
# running as the run was created, or nil.
|
|
98
|
+
# @return [String, nil]
|
|
99
|
+
def browser_server_key
|
|
100
|
+
key = input_params[BROWSER_PARAM] if input_params.is_a?(Hash)
|
|
101
|
+
key.to_s.presence if sandbox_server_key && SandboxSession.browser_server_key?(key)
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
# Whether this run knows who it is for. A run with a recorded actor that
|
|
87
105
|
# no longer resolves is *not* unattributed — it is broken, and callers
|
|
88
106
|
# that care can tell the two apart.
|
|
@@ -91,14 +109,13 @@ module ActionAgent
|
|
|
91
109
|
input_params.is_a?(Hash) && input_params[ACTOR_PARAM].present?
|
|
92
110
|
end
|
|
93
111
|
|
|
94
|
-
# Whether runs can carry files in this host app: Active Storage
|
|
95
|
-
# the macro applied, and its tables
|
|
96
|
-
#
|
|
97
|
-
# just can't attach
|
|
112
|
+
# Whether runs can carry files in this host app: Active Storage on per
|
|
113
|
+
# ActionAgent.active_storage, the macro applied, and its tables
|
|
114
|
+
# migrated. Never raises — a host that skipped
|
|
115
|
+
# `rails active_storage:install` still runs agents, it just can't attach
|
|
116
|
+
# files to them.
|
|
98
117
|
def self.attachments_available?
|
|
99
|
-
|
|
100
|
-
rescue StandardError
|
|
101
|
-
false
|
|
118
|
+
ActionAgent.active_storage_available? && method_defined?(:attachments)
|
|
102
119
|
end
|
|
103
120
|
|
|
104
121
|
# How an attachment reaches the model, by MIME type with a filename
|
|
@@ -118,8 +135,9 @@ module ActionAgent
|
|
|
118
135
|
"file"
|
|
119
136
|
end
|
|
120
137
|
|
|
121
|
-
#
|
|
122
|
-
|
|
138
|
+
# `awaiting_input` is a run paused until a person answers its
|
|
139
|
+
# InputRequests; AgentResumeJob continues it.
|
|
140
|
+
enum :status, { pending: 0, running: 1, complete: 2, failed: 3, cancelled: 4, awaiting_input: 5 }
|
|
123
141
|
|
|
124
142
|
# Validations
|
|
125
143
|
validates :trace_id, presence: true
|
|
@@ -129,6 +147,23 @@ module ActionAgent
|
|
|
129
147
|
scope :successful, -> { where(status: :complete) }
|
|
130
148
|
scope :failed_runs, -> { where(status: :failed) }
|
|
131
149
|
scope :today, -> { where("created_at >= ?", Time.current.beginning_of_day) }
|
|
150
|
+
# Runs executed on behalf of +actor+, matched on the Global ID recorded
|
|
151
|
+
# under ACTOR_PARAM. None for an actor without one.
|
|
152
|
+
scope :on_behalf_of, ->(actor) {
|
|
153
|
+
gid = actor.respond_to?(:to_global_id) ? actor.to_global_id.to_s : nil
|
|
154
|
+
gid ? where("#{input_param_sql(ACTOR_PARAM)} = ?", gid) : none
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# SQL reading +key+ of input_params as text on the connected database.
|
|
158
|
+
# +key+ is always a literal from this codebase, never user input.
|
|
159
|
+
def self.input_param_sql(key)
|
|
160
|
+
column = "#{quoted_table_name}.input_params"
|
|
161
|
+
case connection.adapter_name.to_s.downcase
|
|
162
|
+
when /postgres/ then "#{column} ->> '#{key}'"
|
|
163
|
+
when /mysql|trilogy/ then "JSON_UNQUOTE(JSON_EXTRACT(#{column}, '$.#{key}'))"
|
|
164
|
+
else "json_extract(#{column}, '$.#{key}')"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
132
167
|
|
|
133
168
|
# Callbacks
|
|
134
169
|
before_validation :set_trace_id, on: :create
|
|
@@ -271,27 +306,103 @@ module ActionAgent
|
|
|
271
306
|
}
|
|
272
307
|
end
|
|
273
308
|
|
|
274
|
-
#
|
|
309
|
+
# Tells subscribers of this run's stream, and of its agent's, that the
|
|
310
|
+
# run's status changed.
|
|
275
311
|
def broadcast_update
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
ActionCable.server.broadcast("agent_runs_#{agent_id}", payload)
|
|
312
|
+
LiveUpdates.broadcast("agent_run_#{id}", type: "update", id: id, status: status)
|
|
313
|
+
LiveUpdates.broadcast("agent_runs_#{agent_id}", type: "update", id: id, status: status)
|
|
279
314
|
end
|
|
280
315
|
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
316
|
+
# Cancels a run that is in progress or waiting for input, and every
|
|
317
|
+
# request for input it still has pending.
|
|
318
|
+
#
|
|
319
|
+
# @param reason [String] recorded as the run's error message
|
|
320
|
+
def cancel!(reason = "Cancelled by user")
|
|
321
|
+
return unless in_progress? || awaiting_input?
|
|
322
|
+
|
|
323
|
+
transaction do
|
|
324
|
+
update!(status: :cancelled, completed_at: Time.current, error_message: reason)
|
|
325
|
+
input_requests.pending.update_all(status: InputRequest.statuses[:cancelled], updated_at: Time.current)
|
|
326
|
+
end
|
|
327
|
+
broadcast_update
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Expires the run's overdue requests for input, which fails the run (see
|
|
331
|
+
# InputRequest#expire!). Returns the run, reloaded when a request
|
|
332
|
+
# expired.
|
|
333
|
+
def expire_overdue_input_requests!
|
|
334
|
+
return self unless awaiting_input?
|
|
335
|
+
|
|
336
|
+
InputRequest.expire_overdue!(input_requests).positive? ? reload : self
|
|
337
|
+
end
|
|
284
338
|
|
|
339
|
+
# Writes one execution segment's result: `complete` with its output, or,
|
|
340
|
+
# for a generation that paused, `awaiting_input` with one InputRequest
|
|
341
|
+
# per paused tool call, stored in the same transaction so an answer can
|
|
342
|
+
# never find the requests before the run waits on them. Tokens and
|
|
343
|
+
# duration add to what earlier segments of the run recorded.
|
|
344
|
+
#
|
|
345
|
+
# @param result [Hash] AgentExecutionService's result
|
|
346
|
+
# @param segment_started_at [Time] when this segment started executing
|
|
347
|
+
def record_result!(result, segment_started_at:)
|
|
348
|
+
usage = result[:usage] || {}
|
|
349
|
+
attributes = {
|
|
350
|
+
output_metadata: result[:metadata],
|
|
351
|
+
duration_ms: duration_ms.to_i + ((Time.current - segment_started_at) * 1000).to_i,
|
|
352
|
+
input_tokens: add_tokens(input_tokens, usage[:input_tokens]),
|
|
353
|
+
output_tokens: add_tokens(output_tokens, usage[:output_tokens]),
|
|
354
|
+
total_tokens: add_tokens(total_tokens, usage[:total_tokens])
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
transaction do
|
|
358
|
+
if result[:input_requests].present?
|
|
359
|
+
InputRequest.record_pause!(self, result[:input_requests], checkpoint: result[:checkpoint])
|
|
360
|
+
update!(attributes.merge(status: :awaiting_input))
|
|
361
|
+
else
|
|
362
|
+
update!(attributes.merge(output: result[:output], status: :complete, completed_at: Time.current))
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Fails the run with +error+, recording its class beside the message: the
|
|
368
|
+
# class is what tells a refusal from a crash without reading prose.
|
|
369
|
+
#
|
|
370
|
+
# @param error [Exception]
|
|
371
|
+
# @param message [String] the message to record, when it must differ from
|
|
372
|
+
# the error's own
|
|
373
|
+
def record_failure!(error, message: error.message)
|
|
285
374
|
update!(
|
|
286
|
-
status: :
|
|
375
|
+
status: :failed,
|
|
287
376
|
completed_at: Time.current,
|
|
288
|
-
error_message:
|
|
377
|
+
error_message: message,
|
|
378
|
+
error_backtrace: error.backtrace&.first(10)&.join("\n"),
|
|
379
|
+
output_metadata: output_metadata.to_h.merge("error_class" => error.class.name)
|
|
289
380
|
)
|
|
290
|
-
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Yields under the run's row lock unless the run was cancelled meanwhile,
|
|
384
|
+
# so a cancel that arrived while the run executed is never overwritten by
|
|
385
|
+
# the result. Returns whether it yielded.
|
|
386
|
+
def unless_cancelled
|
|
387
|
+
with_lock do
|
|
388
|
+
if cancelled?
|
|
389
|
+
add_log("Execution finished after cancellation; result discarded", level: :info)
|
|
390
|
+
next false
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
yield
|
|
394
|
+
true
|
|
395
|
+
end
|
|
291
396
|
end
|
|
292
397
|
|
|
293
398
|
private
|
|
294
399
|
|
|
400
|
+
def add_tokens(recorded, segment)
|
|
401
|
+
return recorded if segment.nil?
|
|
402
|
+
|
|
403
|
+
recorded.to_i + segment.to_i
|
|
404
|
+
end
|
|
405
|
+
|
|
295
406
|
def locate_actor
|
|
296
407
|
return nil unless actor_recorded?
|
|
297
408
|
return nil unless defined?(GlobalID::Locator)
|