actionagent 0.0.0 → 1.2.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/LICENSE +21 -0
- data/README.md +103 -0
- data/app/assets/builds/action_agent.css +2 -0
- data/app/assets/builds/action_agent.js +163 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +128 -0
- data/app/controllers/action_agent/api/agents_controller.rb +411 -0
- data/app/controllers/action_agent/api/analytics_controller.rb +94 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +45 -0
- data/app/controllers/action_agent/api/base_controller.rb +112 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +148 -0
- data/app/controllers/action_agent/api/instance_tiers_controller.rb +106 -0
- data/app/controllers/action_agent/api/interactions_controller.rb +197 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +218 -0
- data/app/controllers/action_agent/api/mcp_servers_controller.rb +156 -0
- data/app/controllers/action_agent/api/metrics_controller.rb +178 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +52 -0
- data/app/controllers/action_agent/api/provider_models_controller.rb +119 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +224 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +372 -0
- data/app/controllers/action_agent/api/templates_controller.rb +94 -0
- data/app/controllers/action_agent/api/tools_controller.rb +58 -0
- data/app/controllers/action_agent/api/trace_reports_controller.rb +68 -0
- data/app/controllers/action_agent/api/traces_controller.rb +136 -0
- data/app/controllers/action_agent/application_controller.rb +105 -0
- data/app/controllers/action_agent/dashboard_controller.rb +104 -0
- data/app/controllers/action_agent/traces_controller.rb +121 -0
- data/app/jobs/action_agent/agent_execution_job.rb +52 -0
- data/app/jobs/action_agent/application_job.rb +12 -0
- data/app/jobs/action_agent/process_telemetry_traces_job.rb +86 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +42 -0
- data/app/jobs/action_agent/sandbox_provision_job.rb +56 -0
- data/app/jobs/action_agent/sandbox_run_job.rb +285 -0
- data/app/jobs/action_agent/trace_retention_job.rb +57 -0
- data/app/models/action_agent/agent.rb +343 -0
- data/app/models/action_agent/agent_context.rb +129 -0
- data/app/models/action_agent/agent_generation.rb +48 -0
- data/app/models/action_agent/agent_memory.rb +50 -0
- data/app/models/action_agent/agent_memory_entry.rb +14 -0
- data/app/models/action_agent/agent_message.rb +43 -0
- data/app/models/action_agent/agent_run.rb +151 -0
- data/app/models/action_agent/agent_template.rb +182 -0
- data/app/models/action_agent/agent_version.rb +48 -0
- data/app/models/action_agent/api_key.rb +53 -0
- data/app/models/action_agent/application_record.rb +27 -0
- data/app/models/action_agent/evaluation.rb +80 -0
- data/app/models/action_agent/evaluation_run.rb +20 -0
- data/app/models/action_agent/model_pricing.rb +80 -0
- data/app/models/action_agent/provider_key.rb +60 -0
- data/app/models/action_agent/recording_action.rb +119 -0
- data/app/models/action_agent/recording_snapshot.rb +88 -0
- data/app/models/action_agent/sandbox_instance_tier.rb +368 -0
- data/app/models/action_agent/sandbox_run.rb +45 -0
- data/app/models/action_agent/sandbox_session.rb +160 -0
- data/app/models/action_agent/session_recording.rb +178 -0
- data/app/models/action_agent/telemetry_trace.rb +357 -0
- data/app/models/concerns/action_agent/adapter_aware.rb +50 -0
- data/app/models/concerns/action_agent/ownable.rb +86 -0
- data/app/models/concerns/action_agent/session_recordable.rb +91 -0
- data/app/queries/action_agent/agent_executions.rb +201 -0
- data/app/serializers/action_agent/agent_message_serializer.rb +23 -0
- data/app/serializers/action_agent/interaction_preview.rb +22 -0
- data/app/serializers/action_agent/telemetry_trace_serializer.rb +122 -0
- data/app/serializers/action_agent/trace_interaction_serializer.rb +246 -0
- data/app/services/action_agent/agent_execution_service.rb +572 -0
- data/app/services/action_agent/agent_registrar.rb +197 -0
- data/app/services/action_agent/agent_scorecard.rb +191 -0
- data/app/services/action_agent/agent_toolbox.rb +504 -0
- data/app/services/action_agent/evaluation_runner_service.rb +481 -0
- data/app/services/action_agent/mcp_catalog.rb +247 -0
- data/app/services/action_agent/mcp_recording_middleware.rb +241 -0
- data/app/services/action_agent/mock_sandbox_backend.rb +52 -0
- data/app/services/action_agent/playwright_mcp_client.rb +148 -0
- data/app/services/action_agent/sandbox_orchestrator.rb +242 -0
- data/app/services/action_agent/session_recording_service.rb +228 -0
- data/app/services/action_agent/tool_discovery.rb +617 -0
- data/app/views/action_agent/dashboard/index.html.erb +5 -0
- data/app/views/action_agent/traces/_trace_detail.html.erb +117 -0
- data/app/views/action_agent/traces/index.html.erb +135 -0
- data/app/views/action_agent/traces/metrics.html.erb +145 -0
- data/app/views/action_agent/traces/show.html.erb +36 -0
- data/app/views/layouts/action_agent/application.html.erb +94 -0
- data/app/views/layouts/action_agent/react.html.erb +19 -0
- data/config/routes.rb +144 -0
- data/lib/action_agent/compatibility.rb +49 -0
- data/lib/action_agent/engine.rb +51 -0
- data/lib/action_agent/version.rb +5 -0
- data/lib/action_agent.rb +388 -0
- data/lib/actionagent.rb +6 -0
- data/lib/generators/action_agent/install_generator.rb +137 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +82 -0
- data/lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb +24 -0
- data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +319 -0
- data/lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb +58 -0
- metadata +209 -12
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class SandboxProvisionJob < ApplicationJob
|
|
5
|
+
queue_as :sandboxes
|
|
6
|
+
|
|
7
|
+
# Provision a Cloud Run sandbox for the session
|
|
8
|
+
# Each sandbox is an instance of the ActiveAgents application running in sandbox mode
|
|
9
|
+
def perform(sandbox_session_id)
|
|
10
|
+
sandbox = SandboxSession.find(sandbox_session_id)
|
|
11
|
+
return if sandbox.ready? || sandbox.expired?
|
|
12
|
+
|
|
13
|
+
# In development/test, simulate provisioning
|
|
14
|
+
if Rails.env.development? || Rails.env.test?
|
|
15
|
+
simulate_provisioning(sandbox)
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Hand off to whichever backend this install registered — the engine
|
|
20
|
+
# ships only the in-memory one, so a real container/job comes from the
|
|
21
|
+
# host app's backend (see ActionAgent.sandbox_backends).
|
|
22
|
+
result = SandboxOrchestrator.new.create_sandbox(sandbox)
|
|
23
|
+
|
|
24
|
+
sandbox.mark_ready!(
|
|
25
|
+
cloud_run_url: result[:url],
|
|
26
|
+
cloud_run_job_id: result[:sandbox_id]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Broadcast status update
|
|
30
|
+
broadcast_sandbox_update(sandbox)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Rails.logger.error("Sandbox provision failed: #{e.message}")
|
|
33
|
+
sandbox.update!(status: :failed)
|
|
34
|
+
broadcast_sandbox_update(sandbox)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def simulate_provisioning(sandbox)
|
|
40
|
+
# Simulate a small delay for provisioning
|
|
41
|
+
sleep(0.5)
|
|
42
|
+
|
|
43
|
+
sandbox.mark_ready!(
|
|
44
|
+
cloud_run_url: "http://localhost:3000/api/sandbox",
|
|
45
|
+
cloud_run_job_id: "local-#{sandbox.session_id[0..7]}"
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def broadcast_sandbox_update(sandbox)
|
|
50
|
+
ActionCable.server.broadcast(
|
|
51
|
+
"sandbox_#{sandbox.session_id}",
|
|
52
|
+
{ type: "status_update", sandbox: sandbox.summary }
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
class SandboxRunJob < ApplicationJob
|
|
5
|
+
queue_as :sandboxes
|
|
6
|
+
|
|
7
|
+
PROVIDER_MODELS = {
|
|
8
|
+
"anthropic" => "claude-haiku-4-5",
|
|
9
|
+
"openai" => "gpt-4o",
|
|
10
|
+
"ollama" => "llama3.1:8b"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
# Execute a task in the sandbox using ActiveAgent's generate_later
|
|
14
|
+
def perform(sandbox_session_id, run_id, task, provider = "anthropic")
|
|
15
|
+
sandbox = SandboxSession.find(sandbox_session_id)
|
|
16
|
+
return unless sandbox.can_run?
|
|
17
|
+
|
|
18
|
+
started_at = Time.current
|
|
19
|
+
|
|
20
|
+
# Broadcast that run has started
|
|
21
|
+
broadcast_run_started(sandbox, run_id, task, provider)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
# Use ActiveAgent with generate_later for async processing
|
|
25
|
+
# The agent's after_generation callback will handle recording results
|
|
26
|
+
if defined?(ActiveAgent::Base)
|
|
27
|
+
execute_with_active_agent(sandbox, run_id, task, provider, started_at)
|
|
28
|
+
else
|
|
29
|
+
# Fallback to direct API calls if ActiveAgent not available
|
|
30
|
+
execute_with_fallback(sandbox, run_id, task, provider, started_at)
|
|
31
|
+
end
|
|
32
|
+
rescue => e
|
|
33
|
+
Rails.logger.error("Sandbox run failed: #{e.message}")
|
|
34
|
+
sandbox.update!(status: :ready, error_message: e.message)
|
|
35
|
+
broadcast_run_error(sandbox, run_id, provider, e.message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def execute_with_active_agent(sandbox, run_id, task, provider, started_at)
|
|
42
|
+
# Use generate_now here since we're already in a background job
|
|
43
|
+
# The ActiveAgent will handle the API call and callbacks
|
|
44
|
+
response = SandboxDemoAgent.with(
|
|
45
|
+
task: task,
|
|
46
|
+
provider: provider,
|
|
47
|
+
sandbox_session_id: sandbox.id,
|
|
48
|
+
run_id: run_id,
|
|
49
|
+
started_at: started_at
|
|
50
|
+
).ask.generate_now
|
|
51
|
+
|
|
52
|
+
duration_ms = ((Time.current - started_at) * 1000).to_i
|
|
53
|
+
|
|
54
|
+
# Extract result from response
|
|
55
|
+
content = response&.message&.content || "No response generated"
|
|
56
|
+
input_tokens = response&.usage&.[](:input_tokens) || 0
|
|
57
|
+
output_tokens = response&.usage&.[](:output_tokens) || 0
|
|
58
|
+
tokens = input_tokens + output_tokens
|
|
59
|
+
|
|
60
|
+
# Record the run
|
|
61
|
+
run = sandbox.record_run!(
|
|
62
|
+
task: task,
|
|
63
|
+
result: content,
|
|
64
|
+
duration_ms: duration_ms,
|
|
65
|
+
tokens: tokens,
|
|
66
|
+
screenshots: [],
|
|
67
|
+
provider: provider
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
sandbox.update!(status: :ready)
|
|
71
|
+
broadcast_run_complete(sandbox, run_id, run)
|
|
72
|
+
|
|
73
|
+
rescue => e
|
|
74
|
+
Rails.logger.error("ActiveAgent execution failed: #{e.message}")
|
|
75
|
+
# Fallback to direct API
|
|
76
|
+
execute_with_fallback(sandbox, run_id, task, provider, started_at)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def execute_with_fallback(sandbox, run_id, task, provider, started_at)
|
|
80
|
+
result = case provider
|
|
81
|
+
when "anthropic"
|
|
82
|
+
execute_with_anthropic(task)
|
|
83
|
+
when "openai"
|
|
84
|
+
execute_with_openai(task)
|
|
85
|
+
when "ollama"
|
|
86
|
+
execute_with_ollama(task)
|
|
87
|
+
else
|
|
88
|
+
{ content: "Error: Unknown provider '#{provider}'", tokens: 0, screenshots: [] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
duration_ms = ((Time.current - started_at) * 1000).to_i
|
|
92
|
+
|
|
93
|
+
# Record the run
|
|
94
|
+
run = sandbox.record_run!(
|
|
95
|
+
task: task,
|
|
96
|
+
result: result[:content],
|
|
97
|
+
duration_ms: duration_ms,
|
|
98
|
+
tokens: result[:tokens] || 0,
|
|
99
|
+
screenshots: result[:screenshots] || [],
|
|
100
|
+
provider: provider
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
sandbox.update!(status: :ready)
|
|
104
|
+
broadcast_run_complete(sandbox, run_id, run)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def execute_with_anthropic(task)
|
|
108
|
+
api_key = Rails.application.credentials.dig(:anthropic, :api_key) || ENV["ANTHROPIC_API_KEY"]
|
|
109
|
+
|
|
110
|
+
unless api_key.present?
|
|
111
|
+
Rails.logger.warn("Anthropic API key not configured")
|
|
112
|
+
return { content: "Error: Anthropic API key is not configured — set ANTHROPIC_API_KEY or add it to credentials", tokens: 0, screenshots: [] }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
require "net/http"
|
|
116
|
+
require "json"
|
|
117
|
+
|
|
118
|
+
uri = URI("https://api.anthropic.com/v1/messages")
|
|
119
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
120
|
+
http.use_ssl = true
|
|
121
|
+
http.read_timeout = 60
|
|
122
|
+
|
|
123
|
+
request = Net::HTTP::Post.new(uri)
|
|
124
|
+
request["Content-Type"] = "application/json"
|
|
125
|
+
request["x-api-key"] = api_key
|
|
126
|
+
request["anthropic-version"] = "2023-06-01"
|
|
127
|
+
|
|
128
|
+
request.body = {
|
|
129
|
+
model: PROVIDER_MODELS["anthropic"],
|
|
130
|
+
max_tokens: 1024,
|
|
131
|
+
system: default_system_prompt,
|
|
132
|
+
messages: [ { role: "user", content: task } ]
|
|
133
|
+
}.to_json
|
|
134
|
+
|
|
135
|
+
response = http.request(request)
|
|
136
|
+
result = JSON.parse(response.body)
|
|
137
|
+
|
|
138
|
+
if result["error"]
|
|
139
|
+
raise "Anthropic API error: #{result['error']['message']}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
content = result.dig("content", 0, "text") || "No response generated"
|
|
143
|
+
input_tokens = result.dig("usage", "input_tokens") || 0
|
|
144
|
+
output_tokens = result.dig("usage", "output_tokens") || 0
|
|
145
|
+
|
|
146
|
+
{ content: content, tokens: input_tokens + output_tokens, screenshots: [] }
|
|
147
|
+
rescue => e
|
|
148
|
+
Rails.logger.error("Anthropic API call failed: #{e.message}")
|
|
149
|
+
{ content: "Error: #{e.message}", tokens: 0, screenshots: [] }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def execute_with_openai(task)
|
|
153
|
+
api_key = Rails.application.credentials.dig(:openai, :api_key) || ENV["OPENAI_API_KEY"]
|
|
154
|
+
|
|
155
|
+
unless api_key.present?
|
|
156
|
+
Rails.logger.warn("OpenAI API key not configured")
|
|
157
|
+
return { content: "Error: OpenAI API key is not configured — set OPENAI_API_KEY or add it to credentials", tokens: 0, screenshots: [] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
require "net/http"
|
|
161
|
+
require "json"
|
|
162
|
+
|
|
163
|
+
uri = URI("https://api.openai.com/v1/chat/completions")
|
|
164
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
165
|
+
http.use_ssl = true
|
|
166
|
+
http.read_timeout = 60
|
|
167
|
+
|
|
168
|
+
request = Net::HTTP::Post.new(uri)
|
|
169
|
+
request["Content-Type"] = "application/json"
|
|
170
|
+
request["Authorization"] = "Bearer #{api_key}"
|
|
171
|
+
|
|
172
|
+
request.body = {
|
|
173
|
+
model: PROVIDER_MODELS["openai"],
|
|
174
|
+
max_tokens: 1024,
|
|
175
|
+
messages: [
|
|
176
|
+
{ role: "system", content: default_system_prompt },
|
|
177
|
+
{ role: "user", content: task }
|
|
178
|
+
]
|
|
179
|
+
}.to_json
|
|
180
|
+
|
|
181
|
+
response = http.request(request)
|
|
182
|
+
result = JSON.parse(response.body)
|
|
183
|
+
|
|
184
|
+
if result["error"]
|
|
185
|
+
raise "OpenAI API error: #{result['error']['message']}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
content = result.dig("choices", 0, "message", "content") || "No response generated"
|
|
189
|
+
prompt_tokens = result.dig("usage", "prompt_tokens") || 0
|
|
190
|
+
completion_tokens = result.dig("usage", "completion_tokens") || 0
|
|
191
|
+
|
|
192
|
+
{ content: content, tokens: prompt_tokens + completion_tokens, screenshots: [] }
|
|
193
|
+
rescue => e
|
|
194
|
+
Rails.logger.error("OpenAI API call failed: #{e.message}")
|
|
195
|
+
{ content: "Error: #{e.message}", tokens: 0, screenshots: [] }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def execute_with_ollama(task)
|
|
199
|
+
ollama_url = Rails.application.credentials.dig(:ollama, :url) || ENV["OLLAMA_URL"] || "http://localhost:11434"
|
|
200
|
+
|
|
201
|
+
require "net/http"
|
|
202
|
+
require "json"
|
|
203
|
+
|
|
204
|
+
uri = URI("#{ollama_url}/api/generate")
|
|
205
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
206
|
+
http.use_ssl = uri.scheme == "https"
|
|
207
|
+
http.read_timeout = 120 # Ollama can be slower
|
|
208
|
+
|
|
209
|
+
request = Net::HTTP::Post.new(uri)
|
|
210
|
+
request["Content-Type"] = "application/json"
|
|
211
|
+
|
|
212
|
+
request.body = {
|
|
213
|
+
model: PROVIDER_MODELS["ollama"],
|
|
214
|
+
prompt: "#{default_system_prompt}\n\nUser: #{task}\n\nAssistant:",
|
|
215
|
+
stream: false
|
|
216
|
+
}.to_json
|
|
217
|
+
|
|
218
|
+
response = http.request(request)
|
|
219
|
+
result = JSON.parse(response.body)
|
|
220
|
+
|
|
221
|
+
if result["error"]
|
|
222
|
+
raise "Ollama API error: #{result['error']}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
content = result["response"] || "No response generated"
|
|
226
|
+
# Ollama doesn't provide token counts in the same way
|
|
227
|
+
tokens = (result["prompt_eval_count"] || 0) + (result["eval_count"] || 0)
|
|
228
|
+
|
|
229
|
+
{ content: content, tokens: tokens, screenshots: [] }
|
|
230
|
+
rescue => e
|
|
231
|
+
Rails.logger.error("Ollama API call failed: #{e.message}")
|
|
232
|
+
{ content: "Error: #{e.message}\n\nMake sure Ollama is running locally with: ollama serve", tokens: 0, screenshots: [] }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def default_system_prompt
|
|
236
|
+
<<~PROMPT
|
|
237
|
+
You are a helpful AI assistant. Answer the user's question or complete their task concisely and accurately.
|
|
238
|
+
|
|
239
|
+
If the task involves browser automation, describe what actions you would take step by step.
|
|
240
|
+
PROMPT
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def broadcast_run_started(sandbox, run_id, task, provider)
|
|
244
|
+
ActionCable.server.broadcast(
|
|
245
|
+
"sandbox_#{sandbox.session_id}",
|
|
246
|
+
{
|
|
247
|
+
type: "run_started",
|
|
248
|
+
run_id: run_id,
|
|
249
|
+
provider: provider,
|
|
250
|
+
task: task,
|
|
251
|
+
started_at: Time.current.iso8601
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
Rails.logger.info "[SandboxRunJob] Broadcast run_started for #{provider} (#{run_id})"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def broadcast_run_complete(sandbox, run_id, run)
|
|
258
|
+
ActionCable.server.broadcast(
|
|
259
|
+
"sandbox_#{sandbox.session_id}",
|
|
260
|
+
{
|
|
261
|
+
type: "run_complete",
|
|
262
|
+
run_id: run_id,
|
|
263
|
+
provider: run[:provider],
|
|
264
|
+
run: run,
|
|
265
|
+
sandbox: sandbox.summary
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
Rails.logger.info "[SandboxRunJob] Broadcast run_complete for #{run[:provider]} (#{run_id})"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def broadcast_run_error(sandbox, run_id, provider, error)
|
|
272
|
+
ActionCable.server.broadcast(
|
|
273
|
+
"sandbox_#{sandbox.session_id}",
|
|
274
|
+
{
|
|
275
|
+
type: "run_error",
|
|
276
|
+
run_id: run_id,
|
|
277
|
+
provider: provider,
|
|
278
|
+
error: error,
|
|
279
|
+
sandbox: sandbox.summary
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
Rails.logger.info "[SandboxRunJob] Broadcast run_error for #{provider} (#{run_id})"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# Prunes telemetry traces past the configured retention window.
|
|
5
|
+
#
|
|
6
|
+
# NOT SCHEDULED BY DEFAULT: deleting trace history is a decision for the
|
|
7
|
+
# app that owns the data. To enable it, set a window and schedule the job:
|
|
8
|
+
#
|
|
9
|
+
# ActionAgent.trace_retention = 30.days
|
|
10
|
+
#
|
|
11
|
+
# # config/recurring.yml
|
|
12
|
+
# trace_retention:
|
|
13
|
+
# class: ActionAgent::TraceRetentionJob
|
|
14
|
+
# schedule: every day at 4am
|
|
15
|
+
#
|
|
16
|
+
# A host app with per-tenant retention rules (the activeagents.ai platform
|
|
17
|
+
# prunes per plan) passes a callable instead, receiving each owner and
|
|
18
|
+
# returning that owner's window.
|
|
19
|
+
class TraceRetentionJob < ApplicationJob
|
|
20
|
+
queue_as :default
|
|
21
|
+
|
|
22
|
+
BATCH_SIZE = 5_000
|
|
23
|
+
|
|
24
|
+
def perform
|
|
25
|
+
window = ActionAgent.trace_retention
|
|
26
|
+
return if window.nil?
|
|
27
|
+
|
|
28
|
+
if ActionAgent.multi_tenant? && (owners = ActionAgent.owner_class)
|
|
29
|
+
owners.find_each { |owner| prune(traces.for_account(owner), window_for(window, owner)) }
|
|
30
|
+
else
|
|
31
|
+
prune(traces.all, window_for(window, nil))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def traces
|
|
38
|
+
ActionAgent.trace_model
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def window_for(window, owner)
|
|
42
|
+
window.respond_to?(:call) ? window.call(owner) : window
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Deletes in batches so a long-neglected install doesn't build one
|
|
46
|
+
# enormous statement.
|
|
47
|
+
def prune(scope, window)
|
|
48
|
+
return if window.nil?
|
|
49
|
+
|
|
50
|
+
cutoff = window.ago
|
|
51
|
+
loop do
|
|
52
|
+
deleted = scope.where(timestamp: ...cutoff).limit(BATCH_SIZE).delete_all
|
|
53
|
+
break if deleted < BATCH_SIZE
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|