actionagent 0.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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 +92 -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 +334 -0
- data/lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb +71 -0
- metadata +209 -12
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# Reconstructs a conversation stream from a telemetry trace, in the shape the
|
|
5
|
+
# dashboard's Interactions view already renders for platform-executed agents.
|
|
6
|
+
#
|
|
7
|
+
# Interactions normally reads solid_agent persistence (AgentContext /
|
|
8
|
+
# AgentMessage / AgentGeneration), which only exists for agents the platform
|
|
9
|
+
# ran itself. Agents running inside a customer's own app — a RubyLLM chat
|
|
10
|
+
# reporting through the telemetry endpoint, say — never write those tables, so
|
|
11
|
+
# the view was blind to them.
|
|
12
|
+
#
|
|
13
|
+
# The spans carry the same story when the reporter opts into content capture:
|
|
14
|
+
# the llm span holds `llm.prompt` / `llm.instructions` / `llm.completion`, and
|
|
15
|
+
# each tool span holds `tool.arguments` / `tool.result`. Ordering them by start
|
|
16
|
+
# time yields prompt → tool call → tool result → response.
|
|
17
|
+
class TraceInteractionSerializer
|
|
18
|
+
def self.summary(trace)
|
|
19
|
+
new(trace).summary
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.detail(trace)
|
|
23
|
+
new(trace).detail
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(trace)
|
|
27
|
+
@trace = trace
|
|
28
|
+
@spans = Array(trace.spans)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def summary
|
|
32
|
+
{
|
|
33
|
+
id: "trace-#{@trace.id}",
|
|
34
|
+
source: "telemetry",
|
|
35
|
+
agent_name: @trace.agent_class,
|
|
36
|
+
action_name: @trace.agent_action,
|
|
37
|
+
display_name: @trace.display_name,
|
|
38
|
+
agent: nil,
|
|
39
|
+
service_name: @trace.service_name,
|
|
40
|
+
environment: @trace.environment,
|
|
41
|
+
model: @trace.model,
|
|
42
|
+
tokens: {
|
|
43
|
+
input: @trace.total_input_tokens,
|
|
44
|
+
output: @trace.total_output_tokens,
|
|
45
|
+
total: @trace.total_input_tokens.to_i + @trace.total_output_tokens.to_i
|
|
46
|
+
},
|
|
47
|
+
message_count: messages.size,
|
|
48
|
+
preview: preview,
|
|
49
|
+
# Tool activity is known even when content capture is off, so a run
|
|
50
|
+
# reported without prompts still shows what it did.
|
|
51
|
+
tool_count: tool_spans.size,
|
|
52
|
+
duration_ms: @trace.total_duration_ms&.to_f,
|
|
53
|
+
generation_count: llm_spans.size,
|
|
54
|
+
created_at: @trace.timestamp.iso8601,
|
|
55
|
+
last_activity_at: @trace.timestamp.iso8601
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detail
|
|
60
|
+
summary.merge(
|
|
61
|
+
instructions: llm_attribute("llm.instructions") || any_attribute("prompt.input.instructions"),
|
|
62
|
+
messages: messages,
|
|
63
|
+
generations: generations
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# What the run was asked and what it answered — the opening prompt and the
|
|
70
|
+
# final assistant turn, since the middle of a stream is tool traffic.
|
|
71
|
+
def preview
|
|
72
|
+
said = ->(role, list) { list.find { |m| m[:role] == role && m[:content].present? }&.[](:content) }
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
input: InteractionPreview.line(said.call("user", messages)),
|
|
76
|
+
# A tool-calling assistant turn carries no prose, so the last one that
|
|
77
|
+
# does is the answer.
|
|
78
|
+
output: InteractionPreview.line(said.call("assistant", messages.reverse))
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The generation span. Older traces wrapped a separate `llm` span inside a
|
|
83
|
+
# root; newer ones merge them, since the provider loop *is* the interaction.
|
|
84
|
+
# Accept both, and never count the same generation twice.
|
|
85
|
+
def llm_spans
|
|
86
|
+
@llm_spans ||= begin
|
|
87
|
+
nested = @spans.select { |span| span["type"] == "llm" }
|
|
88
|
+
nested.presence || @spans.select { |span| span["parent_span_id"].nil? && span.dig("attributes", "llm.model") }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def tool_spans
|
|
93
|
+
@tool_spans ||= @spans.select { |span| span["type"] == "tool" }
|
|
94
|
+
.sort_by { |span| span["start_time"].to_s }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def llm_attribute(key)
|
|
98
|
+
llm_spans.filter_map { |span| span.dig("attributes", key).presence }.first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ActiveAgent's instrumentation puts prompt contents on the prompt span
|
|
102
|
+
# (RubyLLM puts everything on the llm span) — accept either home.
|
|
103
|
+
def any_attribute(key)
|
|
104
|
+
@spans.filter_map { |span| span.dig("attributes", key).presence }.first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ActiveAgent reports the outbound conversation as a JSON array of
|
|
108
|
+
# {role, content} on the prompt span rather than a single llm.prompt.
|
|
109
|
+
def outbound_messages
|
|
110
|
+
raw = any_attribute("prompt.input.messages")
|
|
111
|
+
return [] if raw.blank?
|
|
112
|
+
|
|
113
|
+
entries = raw.is_a?(String) ? JSON.parse(raw) : raw
|
|
114
|
+
entries.is_a?(Array) ? entries.select { |m| m.is_a?(Hash) && m["content"].present? } : []
|
|
115
|
+
rescue JSON::ParserError, TypeError
|
|
116
|
+
[]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# prompt → (tool call → tool result)* → completion. Tool pairs are ordered by
|
|
120
|
+
# span start time, so the stream reads the way the run actually happened.
|
|
121
|
+
def messages
|
|
122
|
+
@messages ||= begin
|
|
123
|
+
stream = []
|
|
124
|
+
index = 0
|
|
125
|
+
prompt_span = @spans.find { |span| span["type"] == "prompt" }
|
|
126
|
+
|
|
127
|
+
if (prompt = llm_attribute("llm.prompt"))
|
|
128
|
+
stream << message(index += 1, role: "user", content: prompt, at: @trace.timestamp, timing: timing_for(prompt_span))
|
|
129
|
+
else
|
|
130
|
+
outbound_messages.each do |entry|
|
|
131
|
+
stream << message(index += 1, role: entry["role"].presence || "user", content: entry["content"], at: @trace.timestamp, timing: timing_for(prompt_span))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
tool_spans.each do |span|
|
|
136
|
+
name = span.dig("attributes", "tool.name")
|
|
137
|
+
call_id = span.dig("attributes", "tool.call_id")
|
|
138
|
+
started = parse_time(span["start_time"]) || @trace.timestamp
|
|
139
|
+
finished = parse_time(span["end_time"]) || started
|
|
140
|
+
|
|
141
|
+
stream << message(
|
|
142
|
+
index += 1, role: "assistant", content: nil, at: started,
|
|
143
|
+
tool_name: name, tool_call_id: call_id,
|
|
144
|
+
tool_calls: parsed(span.dig("attributes", "tool.arguments") || span.dig("attributes", "tool.input.args")),
|
|
145
|
+
timing: timing_for(span)
|
|
146
|
+
)
|
|
147
|
+
stream << message(
|
|
148
|
+
index += 1, role: "tool",
|
|
149
|
+
content: span.dig("attributes", "tool.result") || span.dig("attributes", "tool.output.result"),
|
|
150
|
+
at: finished,
|
|
151
|
+
tool_name: name, tool_call_id: call_id,
|
|
152
|
+
timing: timing_for(span)
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if (completion = llm_attribute("llm.completion") || llm_attribute("llm.output.message"))
|
|
157
|
+
stream << message(index += 1, role: "assistant", content: completion, at: end_time, timing: timing_for(llm_spans.first))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
stream
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Where a message's span sits on the trace's wall clock — lets the
|
|
165
|
+
# conversation view double as a waterfall (span-message pill bars).
|
|
166
|
+
def trace_start
|
|
167
|
+
@trace_start ||= @spans.filter_map { |span| parse_time(span["start_time"]) }.min
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def timing_for(span)
|
|
171
|
+
return {} unless span
|
|
172
|
+
|
|
173
|
+
start = parse_time(span["start_time"])
|
|
174
|
+
offset = (start && trace_start) ? ((start - trace_start) * 1000.0).round(2) : 0
|
|
175
|
+
{
|
|
176
|
+
span_start_ms: [ offset, 0 ].max,
|
|
177
|
+
span_duration_ms: span["duration_ms"]&.to_f&.round(2) || 0,
|
|
178
|
+
trace_duration_ms: @trace.total_duration_ms&.to_f&.round(2)
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def message(index, role:, content:, at:, tool_name: nil, tool_call_id: nil, tool_calls: nil, timing: nil)
|
|
183
|
+
{
|
|
184
|
+
id: "#{@trace.id}-#{index}",
|
|
185
|
+
role: role,
|
|
186
|
+
content: content,
|
|
187
|
+
tool_name: tool_name,
|
|
188
|
+
tool_call_id: tool_call_id,
|
|
189
|
+
tool_calls: tool_calls,
|
|
190
|
+
content_checksum: nil,
|
|
191
|
+
created_at: at.iso8601(3),
|
|
192
|
+
**(timing || {})
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def generations
|
|
197
|
+
llm_spans.map.with_index(1) do |span, index|
|
|
198
|
+
attributes = span["attributes"] || {}
|
|
199
|
+
tokens = span["tokens"] || {}
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
id: "#{@trace.id}-llm-#{index}",
|
|
203
|
+
model: attributes["llm.model"],
|
|
204
|
+
provider: attributes["llm.provider"],
|
|
205
|
+
finish_reason: span["status"] == "ERROR" ? "error" : attributes["llm.finish_reason"],
|
|
206
|
+
tokens: {
|
|
207
|
+
input: tokens["input"].to_i,
|
|
208
|
+
output: tokens["output"].to_i,
|
|
209
|
+
total: tokens.values_at("input", "output", "thinking").compact.sum(&:to_i),
|
|
210
|
+
cached: 0,
|
|
211
|
+
thinking: tokens["thinking"].to_i
|
|
212
|
+
},
|
|
213
|
+
cache_hit: false,
|
|
214
|
+
thinking: tokens["thinking"].to_i.positive?,
|
|
215
|
+
duration_seconds: span["duration_ms"] ? (span["duration_ms"].to_f / 1000).round(3) : nil,
|
|
216
|
+
trace_id: @trace.trace_id,
|
|
217
|
+
created_at: (parse_time(span["start_time"]) || @trace.timestamp).iso8601(3)
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Tool arguments arrive JSON-encoded; hand the view an object when possible
|
|
223
|
+
# so it renders structurally rather than as an escaped string.
|
|
224
|
+
def parsed(value)
|
|
225
|
+
return nil if value.blank?
|
|
226
|
+
# A reporter can send an attribute already decoded — JSON.parse raises
|
|
227
|
+
# TypeError rather than ParserError on a Hash or Array, and one such
|
|
228
|
+
# span would otherwise take down the whole Interactions list.
|
|
229
|
+
return value unless value.is_a?(String)
|
|
230
|
+
|
|
231
|
+
JSON.parse(value)
|
|
232
|
+
rescue JSON::ParserError, TypeError
|
|
233
|
+
value
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def end_time
|
|
237
|
+
parse_time(llm_spans.first&.dig("end_time")) || @trace.timestamp
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def parse_time(value)
|
|
241
|
+
Time.zone.parse(value.to_s)
|
|
242
|
+
rescue ArgumentError, TypeError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|