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
|
@@ -32,7 +32,7 @@ module ActionAgent
|
|
|
32
32
|
avg_durations = windowed.where.not(duration_ms: nil).group(:agent_id).average(:duration_ms)
|
|
33
33
|
token_sums = windowed.group(:agent_id).sum("COALESCE(total_tokens, 0)")
|
|
34
34
|
last_runs = AgentRun.where(agent_id: ids).group(:agent_id).maximum(:created_at)
|
|
35
|
-
|
|
35
|
+
evals = evaluation_tiles(ids)
|
|
36
36
|
|
|
37
37
|
trace_stats = telemetry_stats(ids, window_start)
|
|
38
38
|
trace_last = unclaimed_traces(ids, nil).group(:agent_id).maximum(:timestamp)
|
|
@@ -44,7 +44,7 @@ module ActionAgent
|
|
|
44
44
|
traced = stats[:count].to_i
|
|
45
45
|
total = runs + traced
|
|
46
46
|
succeeded = completed_counts[id].to_i + stats[:ok].to_i
|
|
47
|
-
|
|
47
|
+
eval_tile = evals[id] || {}
|
|
48
48
|
|
|
49
49
|
{
|
|
50
50
|
window_days: (WINDOW / 1.day).to_i,
|
|
@@ -55,9 +55,13 @@ module ActionAgent
|
|
|
55
55
|
avg_duration_ms: blended_duration(avg_durations[id], runs, stats[:avg_duration], traced),
|
|
56
56
|
tokens: token_sums[id].to_i + stats[:tokens].to_i,
|
|
57
57
|
cost: costs[id],
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# The evaluation tile: the pass rate pooled over the headline runs
|
|
59
|
+
# of the agent's evaluations that stand against its current version.
|
|
60
|
+
eval_score: eval_tile[:score],
|
|
61
|
+
eval_samples_passed: eval_tile[:passed],
|
|
62
|
+
eval_samples_evaluated: eval_tile[:evaluated],
|
|
63
|
+
eval_runs: eval_tile[:runs],
|
|
64
|
+
eval_not_counted: eval_tile[:not_counted],
|
|
61
65
|
last_run_at: [ last_runs[id], trace_last[id] ].compact.max&.iso8601
|
|
62
66
|
}
|
|
63
67
|
end
|
|
@@ -170,22 +174,47 @@ module ActionAgent
|
|
|
170
174
|
end
|
|
171
175
|
private_class_method :blended_duration
|
|
172
176
|
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
177
|
+
# The evaluation tile per agent: passes pooled over the headline runs
|
|
178
|
+
# (newest complete run) of its evaluations that are current against the
|
|
179
|
+
# agent's version or unrecorded, as the Evaluations page pools them —
|
|
180
|
+
# never a stale suite last run against older code, nor an archived one
|
|
181
|
+
# — so the tile is the pass rate of the agent as it is now, not a mean
|
|
182
|
+
# of criterion scores over whatever ran last.
|
|
183
|
+
#
|
|
184
|
+
# { agent_id => { score:, passed:, evaluated:, runs:, not_counted: } }
|
|
185
|
+
#
|
|
186
|
+
# `score` is the pooled pass rate 0..1 (nil with nothing to count),
|
|
187
|
+
# `runs` the headline runs pooled and `not_counted` the evaluations
|
|
188
|
+
# left out as stale or archived. The newest complete run per evaluation
|
|
189
|
+
# is selected by id first (runs are only ever appended) and those rows
|
|
190
|
+
# fetched by id, which runs the same on every database.
|
|
191
|
+
def self.evaluation_tiles(agent_ids)
|
|
192
|
+
evaluations = Evaluation.where(agent_id: agent_ids).includes(:agent).to_a
|
|
193
|
+
return {} if evaluations.empty?
|
|
194
|
+
|
|
195
|
+
newest = EvaluationRun.complete.where(evaluation_id: evaluations.map(&:id)).group(:evaluation_id).maximum(:id)
|
|
196
|
+
runs = EvaluationRun.where(id: newest.values).includes(:agent_version).index_by(&:evaluation_id)
|
|
197
|
+
EvaluationStanding.preload(evaluations)
|
|
198
|
+
|
|
199
|
+
evaluations.group_by(&:agent_id).to_h do |agent_id, mine|
|
|
200
|
+
tile = { score: nil, passed: 0, evaluated: 0, runs: 0, not_counted: 0 }
|
|
201
|
+
mine.each do |evaluation|
|
|
202
|
+
run = runs[evaluation.id]
|
|
203
|
+
next unless run
|
|
204
|
+
|
|
205
|
+
unless evaluation.standing_info.with_headline(run).counted?
|
|
206
|
+
tile[:not_counted] += 1
|
|
207
|
+
next
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
tile[:runs] += 1
|
|
211
|
+
tile[:passed] += run.samples_passed.to_i
|
|
212
|
+
tile[:evaluated] += run.samples_evaluated.to_i
|
|
213
|
+
end
|
|
214
|
+
tile[:score] = (tile[:passed].to_f / tile[:evaluated]).round(3) if tile[:evaluated].positive?
|
|
215
|
+
[ agent_id, tile ]
|
|
216
|
+
end
|
|
188
217
|
end
|
|
189
|
-
private_class_method :
|
|
218
|
+
private_class_method :evaluation_tiles
|
|
190
219
|
end
|
|
191
220
|
end
|
|
@@ -32,7 +32,8 @@ module ActionAgent
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# @param agents [Array<Class>] ActiveAgent::Base subclasses
|
|
35
|
-
# @param owner [Object] the record agents and API keys scope to
|
|
35
|
+
# @param owner [Object, nil] the record agents and API keys scope to; nil
|
|
36
|
+
# only in an install with no owner model, where agents have no owner
|
|
36
37
|
# @param provider [String, Symbol, nil] defaults to the class's own
|
|
37
38
|
# @param model [String, nil] defaults to the class's own
|
|
38
39
|
def self.call(agents, owner:, provider: nil, model: nil)
|
|
@@ -48,7 +49,9 @@ module ActionAgent
|
|
|
48
49
|
|
|
49
50
|
# @return [Result]
|
|
50
51
|
def call
|
|
51
|
-
|
|
52
|
+
if @owner.nil? && Agent.owner_association
|
|
53
|
+
return Result.new(rows: [], errors: "An owner is required: the engine scopes agents to one.")
|
|
54
|
+
end
|
|
52
55
|
|
|
53
56
|
rows = Agent.transaction { @agents.map { |klass| upsert(klass) } }
|
|
54
57
|
Result.new(rows: rows, errors: nil)
|
|
@@ -33,6 +33,14 @@ module ActionAgent
|
|
|
33
33
|
# capabilities and the schema tools, +mcp_servers+ for services and their
|
|
34
34
|
# per-server allow-lists (an entry with no +tools+ key offers everything
|
|
35
35
|
# the server serves).
|
|
36
|
+
#
|
|
37
|
+
# Every tool row, and every tool a service lists, carries +approval_names+:
|
|
38
|
+
# the names Agent#approval_required_tools takes for it, which are the names
|
|
39
|
+
# the model calls it by. A capability names each function it exposes, and
|
|
40
|
+
# a schema or MCP tool its own name. The list is empty for a row whose
|
|
41
|
+
# calls the dashboard cannot hold for approval: the input tools, which ask
|
|
42
|
+
# on their own, and a tool the agent class declares in code, which its
|
|
43
|
+
# class runs.
|
|
36
44
|
class AgentToolRoster
|
|
37
45
|
AGENT_DEFINED = "agent_defined"
|
|
38
46
|
DASHBOARD = "dashboard"
|
|
@@ -167,7 +175,8 @@ module ActionAgent
|
|
|
167
175
|
usage_row(by_name[name]).merge(
|
|
168
176
|
name: name,
|
|
169
177
|
description: by_name[name]&.dig(:description),
|
|
170
|
-
enabled: allowed.nil? || allowed.include?(name)
|
|
178
|
+
enabled: allowed.nil? || allowed.include?(name),
|
|
179
|
+
approval_names: [ name ]
|
|
171
180
|
)
|
|
172
181
|
end
|
|
173
182
|
end
|
|
@@ -200,7 +209,8 @@ module ActionAgent
|
|
|
200
209
|
# window may still show it being called, but that is history. A
|
|
201
210
|
# tool the agent class declares in code is offered by the class.
|
|
202
211
|
enabled: schema ? saved_tools.include?(name) : true,
|
|
203
|
-
editable: schema
|
|
212
|
+
editable: schema,
|
|
213
|
+
approval_names: schema ? [ name ] : []
|
|
204
214
|
)
|
|
205
215
|
end
|
|
206
216
|
end
|
|
@@ -222,15 +232,20 @@ module ActionAgent
|
|
|
222
232
|
source: DASHBOARD,
|
|
223
233
|
description: Agent::TOOL_DESCRIPTIONS[capability],
|
|
224
234
|
enabled: saved_tools.include?(capability),
|
|
225
|
-
editable: true
|
|
235
|
+
editable: true,
|
|
236
|
+
approval_names: capability_functions(capability) - AgentToolbox::INPUT_FUNCTIONS
|
|
226
237
|
)
|
|
227
238
|
end
|
|
228
239
|
end
|
|
229
240
|
|
|
241
|
+
def capability_functions(capability)
|
|
242
|
+
AgentToolbox::DEFINITIONS[capability]&.map { |definition| definition[:name].to_s } || []
|
|
243
|
+
end
|
|
244
|
+
|
|
230
245
|
# A capability is one checkbox over the several functions it exposes
|
|
231
246
|
# ("memory" is save_memory + recall_memory), so its usage is their sum.
|
|
232
247
|
def capability_usage(capability)
|
|
233
|
-
names = (
|
|
248
|
+
names = capability_functions(capability) + [ capability ]
|
|
234
249
|
rows = detected.select { |tool| names.include?(tool[:name]) }
|
|
235
250
|
return nil if rows.empty?
|
|
236
251
|
|
|
@@ -50,6 +50,28 @@ module ActionAgent
|
|
|
50
50
|
# Stateful: navigate changes what snapshot/click see, so these bypass
|
|
51
51
|
# the toolbox result cache.
|
|
52
52
|
"playwright_mcp" => [
|
|
53
|
+
# Not a browser action: the point where the agent stops and a person
|
|
54
|
+
# continues on the page it reached. AgentExecutionService answers it
|
|
55
|
+
# against the run's session recording (see #request_handoff there),
|
|
56
|
+
# which is why it has no FUNCTIONS entry.
|
|
57
|
+
{
|
|
58
|
+
name: "request_handoff",
|
|
59
|
+
description: "Hand the browser session to a person and stop. Call this the moment the page asks for something only its owner may give: payment details, a password, a one-time code, a consent. Pass the URL of the page you stopped on and the non-secret values you already entered, so the person can check them. After calling it, take no further browser actions; report where you stopped and what the person does next.",
|
|
60
|
+
parameters: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
reason: { type: "string", description: "What the page is asking for, e.g. \"payment details\" or \"a login code sent by email\"" },
|
|
64
|
+
url: { type: "string", description: "The URL of the page the person continues on" },
|
|
65
|
+
form_values: {
|
|
66
|
+
type: "object",
|
|
67
|
+
description: "Values already entered, field label to value. Never a card number, password or code.",
|
|
68
|
+
additionalProperties: { type: "string" }
|
|
69
|
+
},
|
|
70
|
+
instructions: { type: "string", description: "One or two sentences on what the person should do next" }
|
|
71
|
+
},
|
|
72
|
+
required: [ "reason", "url" ]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
53
75
|
{
|
|
54
76
|
name: "browser_navigate",
|
|
55
77
|
description: "Open a URL in the managed browser. Returns a text snapshot of the page with element refs.",
|
|
@@ -77,6 +99,68 @@ module ActionAgent
|
|
|
77
99
|
},
|
|
78
100
|
required: [ "ref" ]
|
|
79
101
|
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "browser_type",
|
|
105
|
+
description: "Type text into an editable element from the latest snapshot: a text box, a search field. Set submit to press Enter afterwards.",
|
|
106
|
+
parameters: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
ref: { type: "string", description: "Element ref from the snapshot" },
|
|
110
|
+
element: { type: "string", description: "Human-readable description of the element" },
|
|
111
|
+
text: { type: "string", description: "The text to type" },
|
|
112
|
+
submit: { type: "boolean", description: "Press Enter after typing" }
|
|
113
|
+
},
|
|
114
|
+
required: [ "ref", "text" ]
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "browser_fill_form",
|
|
119
|
+
description: "Fill several form fields in one step. Each field names its ref from the latest snapshot, a label, its type and the value to enter; a checkbox takes true or false, a combobox the option's text.",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
fields: {
|
|
124
|
+
type: "array",
|
|
125
|
+
description: "The fields to fill",
|
|
126
|
+
items: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
ref: { type: "string", description: "Element ref from the snapshot" },
|
|
130
|
+
name: { type: "string", description: "The field's label, e.g. Email" },
|
|
131
|
+
type: { type: "string", enum: %w[textbox checkbox radio combobox slider], description: "The field's type" },
|
|
132
|
+
value: { type: "string", description: "What to enter" }
|
|
133
|
+
},
|
|
134
|
+
required: [ "ref", "name", "type", "value" ]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
required: [ "fields" ]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "browser_select_option",
|
|
143
|
+
description: "Choose an option in a dropdown from the latest snapshot.",
|
|
144
|
+
parameters: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
ref: { type: "string", description: "Element ref from the snapshot" },
|
|
148
|
+
element: { type: "string", description: "Human-readable description of the element" },
|
|
149
|
+
values: { type: "array", items: { type: "string" }, description: "The option values or texts to select" }
|
|
150
|
+
},
|
|
151
|
+
required: [ "ref", "values" ]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "browser_press_key",
|
|
156
|
+
description: "Press a key in the browser, such as Enter, Escape, Tab or ArrowDown.",
|
|
157
|
+
parameters: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
key: { type: "string", description: "The key to press" }
|
|
161
|
+
},
|
|
162
|
+
required: [ "key" ]
|
|
163
|
+
}
|
|
80
164
|
}
|
|
81
165
|
],
|
|
82
166
|
# Subject-bound like memory: routed by AgentExecutionService (needs the
|
|
@@ -148,6 +232,36 @@ module ActionAgent
|
|
|
148
232
|
}
|
|
149
233
|
}
|
|
150
234
|
],
|
|
235
|
+
# Asking a person mid-run. Each call pauses the run until the answer
|
|
236
|
+
# arrives (ActionAgent::InputRequest), so AgentExecutionService routes
|
|
237
|
+
# them, and they are NOT in FUNCTIONS below.
|
|
238
|
+
"ask" => [
|
|
239
|
+
{
|
|
240
|
+
name: "ask_user",
|
|
241
|
+
description: "Ask the user a question and wait for the answer before continuing. Pass options to have them " \
|
|
242
|
+
"pick one of a fixed set of answers. Use when you need information only the user has.",
|
|
243
|
+
parameters: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
question: { type: "string", description: "The question, as the user should read it" },
|
|
247
|
+
options: { type: "array", items: { type: "string" }, description: "Answers to choose from; omit for a free-text answer" }
|
|
248
|
+
},
|
|
249
|
+
required: [ "question" ]
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "request_approval",
|
|
254
|
+
description: "Ask the user to approve an action before you take it, and wait for their decision. Call it before " \
|
|
255
|
+
"any step with side effects the user has not already agreed to. Returns approved: true, or an error when declined.",
|
|
256
|
+
parameters: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
action: { type: "string", description: "What you want to do, in one sentence the user can approve or decline" }
|
|
260
|
+
},
|
|
261
|
+
required: [ "action" ]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
],
|
|
151
265
|
# Memory tools mirror solid_agent's HasMemory contract. They are NOT in
|
|
152
266
|
# FUNCTIONS below — execution is subject-bound, so AgentExecutionService
|
|
153
267
|
# routes them to the run's AgentMemory instead of this module.
|
|
@@ -179,6 +293,28 @@ module ActionAgent
|
|
|
179
293
|
]
|
|
180
294
|
}.freeze
|
|
181
295
|
|
|
296
|
+
# Asks the user for a secret, such as an API key, delivered to the agent's
|
|
297
|
+
# server-side handler (ActionAgent::SecretRequests) and never to the
|
|
298
|
+
# model. Offered only to an agent with a registered handler, never
|
|
299
|
+
# through an agent's tools list, so it is not in DEFINITIONS.
|
|
300
|
+
REQUEST_SECRET_DEFINITION = {
|
|
301
|
+
name: "request_secret",
|
|
302
|
+
description: "Ask the user for a secret value, such as an API key or a password. The value is handed to the " \
|
|
303
|
+
"system without you ever seeing it; you receive only confirmation that it was provided.",
|
|
304
|
+
parameters: {
|
|
305
|
+
type: "object",
|
|
306
|
+
properties: {
|
|
307
|
+
name: { type: "string", description: "The secret's name, e.g. STRIPE_API_KEY" },
|
|
308
|
+
prompt: { type: "string", description: "What to tell the user about the value you need" }
|
|
309
|
+
},
|
|
310
|
+
required: [ "name", "prompt" ]
|
|
311
|
+
}
|
|
312
|
+
}.freeze
|
|
313
|
+
|
|
314
|
+
# The tools that pause a run to ask a person, routed by
|
|
315
|
+
# AgentExecutionService.
|
|
316
|
+
INPUT_FUNCTIONS = %w[ask_user request_approval request_secret].freeze
|
|
317
|
+
|
|
182
318
|
# Function name => implementation method, for routing tool calls.
|
|
183
319
|
FUNCTIONS = {
|
|
184
320
|
"fetch_url" => :fetch_url,
|
|
@@ -188,24 +324,51 @@ module ActionAgent
|
|
|
188
324
|
"browser_navigate" => :browser_navigate,
|
|
189
325
|
"browser_snapshot" => :browser_snapshot,
|
|
190
326
|
"browser_click" => :browser_click,
|
|
327
|
+
"browser_type" => :browser_type,
|
|
328
|
+
"browser_fill_form" => :browser_fill_form,
|
|
329
|
+
"browser_select_option" => :browser_select_option,
|
|
330
|
+
"browser_press_key" => :browser_press_key,
|
|
191
331
|
"render_ui" => :render_ui
|
|
192
332
|
}.freeze
|
|
193
333
|
|
|
194
334
|
# Stateful tools whose results must never be replayed from cache — and
|
|
195
335
|
# render_ui, whose result is the call itself, so there is nothing to
|
|
196
|
-
# replay.
|
|
197
|
-
UNCACHED_FUNCTIONS = %w[
|
|
336
|
+
# replay, and the tools whose result is a person's answer.
|
|
337
|
+
UNCACHED_FUNCTIONS = (%w[
|
|
338
|
+
browser_navigate browser_snapshot browser_click browser_type browser_fill_form
|
|
339
|
+
browser_select_option browser_press_key render_ui
|
|
340
|
+
] + INPUT_FUNCTIONS).freeze
|
|
341
|
+
|
|
342
|
+
# The group whose tools drive the process-wide browser
|
|
343
|
+
# (PlaywrightMCPClient), and those tools. A run against a sandbox with a
|
|
344
|
+
# running browser gets that browser's tools instead, and a multi-tenant
|
|
345
|
+
# install never offers or calls these: every tenant's runs would share
|
|
346
|
+
# one browser.
|
|
347
|
+
SHARED_BROWSER_GROUP = "playwright_mcp"
|
|
348
|
+
SHARED_BROWSER_FUNCTIONS = DEFINITIONS.fetch(SHARED_BROWSER_GROUP).map { |definition| definition[:name] }.freeze
|
|
198
349
|
|
|
199
350
|
# Hosts browse_page may fetch — the platform's own trusted docs.
|
|
200
351
|
BROWSE_ALLOWED_HOSTS = %w[docs.activeagents.ai].freeze
|
|
201
352
|
|
|
202
353
|
class << self
|
|
203
354
|
# Tool definitions for the subset of an agent's enabled tools that have
|
|
204
|
-
# server-side implementations.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
355
|
+
# server-side implementations. The shared browser's are left out when
|
|
356
|
+
# +browser_attached+ (the run reaches its sandbox's browser, which
|
|
357
|
+
# serves tools of the same names) or when shared_browser? is false.
|
|
358
|
+
# Never request_secret, whatever the list names.
|
|
359
|
+
def definitions_for(tool_names, browser_attached: false)
|
|
360
|
+
names = Array(tool_names).map(&:to_s)
|
|
361
|
+
names -= [ SHARED_BROWSER_GROUP ] if browser_attached || !shared_browser?
|
|
362
|
+
|
|
363
|
+
names.flat_map do |name|
|
|
364
|
+
DEFINITIONS[name] || schema_tool_definitions(name)
|
|
365
|
+
end.reject { |definition| definition[:name].to_s == REQUEST_SECRET_DEFINITION[:name] }
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Whether runs may use the process-wide browser: never in a
|
|
369
|
+
# multi-tenant install.
|
|
370
|
+
def shared_browser?
|
|
371
|
+
!ActionAgent.multi_tenant?
|
|
209
372
|
end
|
|
210
373
|
|
|
211
374
|
def function?(name)
|
|
@@ -230,6 +393,9 @@ module ActionAgent
|
|
|
230
393
|
# instead of re-running the side effect.
|
|
231
394
|
def call(name, **kwargs)
|
|
232
395
|
return { error: "Unknown tool: #{name}" } unless function?(name)
|
|
396
|
+
if SHARED_BROWSER_FUNCTIONS.include?(name.to_s) && !shared_browser?
|
|
397
|
+
return { error: "#{name} needs a browser: start the browser of the sandbox this agent runs against" }
|
|
398
|
+
end
|
|
233
399
|
|
|
234
400
|
# Who the call is for is never one of the call's arguments: it comes
|
|
235
401
|
# off here, before a tool sees them. That keeps a built-in from
|
|
@@ -354,8 +520,48 @@ module ActionAgent
|
|
|
354
520
|
playwright_mcp("browser_snapshot", {})
|
|
355
521
|
end
|
|
356
522
|
|
|
357
|
-
def browser_click(ref
|
|
358
|
-
playwright_mcp("browser_click",
|
|
523
|
+
def browser_click(ref: nil, target: nil, element: nil, button: nil)
|
|
524
|
+
playwright_mcp("browser_click", browser_target(ref || target, element).merge({ button: button }.compact))
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def browser_type(text:, ref: nil, target: nil, element: nil, submit: nil, slowly: nil)
|
|
528
|
+
arguments = browser_target(ref || target, element).merge({ text: text.to_s, submit: submit, slowly: slowly }.compact)
|
|
529
|
+
playwright_mcp("browser_type", arguments)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def browser_fill_form(fields:)
|
|
533
|
+
normalized = Array(fields).map do |field|
|
|
534
|
+
field = field.to_h.transform_keys(&:to_s)
|
|
535
|
+
{
|
|
536
|
+
target: (field["target"] || field["ref"]).to_s,
|
|
537
|
+
name: field["name"].to_s,
|
|
538
|
+
type: field["type"].presence || "textbox",
|
|
539
|
+
value: field["value"].to_s
|
|
540
|
+
}
|
|
541
|
+
end
|
|
542
|
+
raise ArgumentError, "every field needs a ref" if normalized.any? { |field| field[:target].blank? }
|
|
543
|
+
|
|
544
|
+
playwright_mcp("browser_fill_form", { fields: normalized })
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def browser_select_option(values:, ref: nil, target: nil, element: nil)
|
|
548
|
+
playwright_mcp("browser_select_option", browser_target(ref || target, element).merge(values: Array(values).map(&:to_s)))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def browser_press_key(key:)
|
|
552
|
+
playwright_mcp("browser_press_key", { key: key.to_s })
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Playwright MCP addresses an element by +target+ — a ref from the
|
|
556
|
+
# latest snapshot, or a selector — and names it in +element+ for the
|
|
557
|
+
# permission prompt it may show. Earlier releases of the server called
|
|
558
|
+
# the ref +ref+, and that is still what the tool schemas ask the model
|
|
559
|
+
# for, so both spellings are accepted here and sent as the server now
|
|
560
|
+
# expects.
|
|
561
|
+
def browser_target(target, element)
|
|
562
|
+
raise ArgumentError, "an element ref from the snapshot is required" if target.blank?
|
|
563
|
+
|
|
564
|
+
{ target: target.to_s, element: (element.presence || target).to_s }
|
|
359
565
|
end
|
|
360
566
|
|
|
361
567
|
SNAPSHOT_LINK = /\[Snapshot\]\(([^)]+)\)/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
module ActionAgent
|
|
8
|
+
# Used to issue the tickets that let a dashboard viewer into a sandbox
|
|
9
|
+
# browser's live view. The viewer's page sends the ticket as the first
|
|
10
|
+
# message on the live view's WebSocket, and the browser sidecar checks it
|
|
11
|
+
# (browser-sidecar/lib/live-ticket.mjs), so a ticket never travels in a URL.
|
|
12
|
+
#
|
|
13
|
+
# A ticket is base64url JSON claims, a dot, and the base64url HMAC-SHA256
|
|
14
|
+
# of that first part. The key is the HMAC-SHA256 of CONTEXT under the
|
|
15
|
+
# browser's token, which the sidecar was given on stdin when it started:
|
|
16
|
+
# a ticket opens only the browser it was issued for, and only until that
|
|
17
|
+
# browser stops. The sidecar accepts each ticket once, within LIFETIME.
|
|
18
|
+
#
|
|
19
|
+
# Claims:
|
|
20
|
+
# v 1
|
|
21
|
+
# sid the sandbox session
|
|
22
|
+
# sub the user it was issued to, as a String, or nil without one
|
|
23
|
+
# name their name, shown to other viewers ("held by Ada"), or nil
|
|
24
|
+
# mode "view" to watch, "control" to also take over
|
|
25
|
+
# iat when it was issued, in epoch seconds
|
|
26
|
+
# exp when it expires, in epoch seconds
|
|
27
|
+
# jti its unique id
|
|
28
|
+
class BrowserLiveTicket
|
|
29
|
+
CONTEXT = "activeagents/browser-live-ticket/v1"
|
|
30
|
+
MODES = %w[view control].freeze
|
|
31
|
+
# The sidecar refuses a ticket issued for longer than 60 seconds.
|
|
32
|
+
LIFETIME = 30.seconds
|
|
33
|
+
MAX_NAME_LENGTH = 80
|
|
34
|
+
|
|
35
|
+
# Returns { ticket:, mode:, expires_at: } for +sandbox+'s running browser.
|
|
36
|
+
#
|
|
37
|
+
# @param user [Object, nil] the viewer, whose id and name the ticket carries
|
|
38
|
+
# @param mode [String] one of MODES
|
|
39
|
+
# @raise [ArgumentError] for an unknown mode, or a sandbox whose browser
|
|
40
|
+
# has no token
|
|
41
|
+
def self.issue(sandbox, user:, mode:, now: Time.current, jti: SecureRandom.urlsafe_base64(24))
|
|
42
|
+
raise ArgumentError, "mode must be one of #{MODES.join(', ')}" unless MODES.include?(mode)
|
|
43
|
+
raise ArgumentError, "The sandbox's browser has no token" if sandbox.browser_token.blank?
|
|
44
|
+
|
|
45
|
+
issued_at = now.to_i
|
|
46
|
+
claims = {
|
|
47
|
+
v: 1,
|
|
48
|
+
sid: sandbox.session_id,
|
|
49
|
+
sub: user&.id&.to_s,
|
|
50
|
+
name: display_name(user),
|
|
51
|
+
mode: mode,
|
|
52
|
+
iat: issued_at,
|
|
53
|
+
exp: issued_at + LIFETIME.to_i,
|
|
54
|
+
jti: jti
|
|
55
|
+
}
|
|
56
|
+
{ ticket: sign(sandbox.browser_token, claims), mode: mode, expires_at: Time.zone.at(claims[:exp]) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The ticket for +claims+, signed with the key derived from +token+.
|
|
60
|
+
def self.sign(token, claims)
|
|
61
|
+
payload = Base64.urlsafe_encode64(JSON.generate(claims), padding: false)
|
|
62
|
+
signature = OpenSSL::HMAC.digest("SHA256", key(token), payload)
|
|
63
|
+
"#{payload}.#{Base64.urlsafe_encode64(signature, padding: false)}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.key(token)
|
|
67
|
+
OpenSSL::HMAC.digest("SHA256", token, CONTEXT)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.display_name(user)
|
|
71
|
+
return nil unless user
|
|
72
|
+
|
|
73
|
+
name = user.try(:display_name).presence || user.try(:name).presence || user.try(:email_address).presence || user.try(:email).presence
|
|
74
|
+
name&.to_s&.truncate(MAX_NAME_LENGTH)
|
|
75
|
+
end
|
|
76
|
+
private_class_method :key, :display_name
|
|
77
|
+
end
|
|
78
|
+
end
|