activeagent 1.0.1 → 1.0.2

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -4
  3. data/lib/active_agent/base.rb +3 -2
  4. data/lib/active_agent/concerns/provider.rb +6 -2
  5. data/lib/active_agent/concerns/rescue.rb +39 -0
  6. data/lib/active_agent/concerns/streaming.rb +2 -1
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  11. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  16. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  27. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  31. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  32. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  33. data/lib/active_agent/dashboard/engine.rb +39 -0
  34. data/lib/active_agent/dashboard.rb +151 -0
  35. data/lib/active_agent/providers/_base_provider.rb +2 -1
  36. data/lib/active_agent/providers/anthropic_provider.rb +14 -4
  37. data/lib/active_agent/providers/azure/_types.rb +5 -0
  38. data/lib/active_agent/providers/azure/options.rb +111 -0
  39. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  40. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  41. data/lib/active_agent/providers/azure_provider.rb +133 -0
  42. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  43. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  44. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  45. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  46. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  47. data/lib/active_agent/providers/common/messages/_types.rb +6 -2
  48. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  49. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  50. data/lib/active_agent/providers/gemini/options.rb +41 -0
  51. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  52. data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
  53. data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
  54. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  55. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  56. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  57. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  58. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  59. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  60. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  61. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  62. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  63. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  64. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  65. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  66. data/lib/active_agent/railtie.rb +32 -1
  67. data/lib/active_agent/telemetry/configuration.rb +213 -0
  68. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  69. data/lib/active_agent/telemetry/reporter.rb +176 -0
  70. data/lib/active_agent/telemetry/span.rb +267 -0
  71. data/lib/active_agent/telemetry/tracer.rb +184 -0
  72. data/lib/active_agent/telemetry.rb +162 -0
  73. data/lib/active_agent/version.rb +1 -1
  74. data/lib/active_agent.rb +2 -0
  75. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  76. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  77. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  78. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  79. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  80. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  81. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  82. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  83. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  84. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  85. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  86. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  87. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  88. metadata +99 -13
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Background job for provisioning sandbox environments.
6
+ #
7
+ # Creates isolated execution environments (Docker, Cloud Run, etc.)
8
+ # for running agents with tools.
9
+ #
10
+ class SandboxProvisionJob < ApplicationJob
11
+ queue_as :default
12
+
13
+ def perform(sandbox_session_id)
14
+ session = SandboxSession.find(sandbox_session_id)
15
+ return unless session.provisioning?
16
+
17
+ begin
18
+ result = provision_sandbox(session)
19
+
20
+ session.mark_ready!(
21
+ sandbox_url: result[:url],
22
+ sandbox_job_id: result[:job_id]
23
+ )
24
+ rescue => e
25
+ session.update!(
26
+ status: :failed,
27
+ error_message: e.message
28
+ )
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def provision_sandbox(session)
35
+ case ActiveAgent::Dashboard.sandbox_service
36
+ when :cloud_run
37
+ provision_cloud_run(session)
38
+ when :kubernetes
39
+ provision_kubernetes(session)
40
+ else
41
+ provision_local(session)
42
+ end
43
+ end
44
+
45
+ def provision_local(session)
46
+ # Local mode: No actual provisioning needed
47
+ # The sandbox runs in the same process or via Docker
48
+ {
49
+ url: "http://localhost:#{3000 + session.id}",
50
+ job_id: "local-#{session.session_id}"
51
+ }
52
+ end
53
+
54
+ def provision_cloud_run(session)
55
+ # TODO: Implement Cloud Run provisioning
56
+ raise NotImplementedError, "Cloud Run provisioning not yet implemented in engine"
57
+ end
58
+
59
+ def provision_kubernetes(session)
60
+ # TODO: Implement Kubernetes provisioning
61
+ raise NotImplementedError, "Kubernetes provisioning not yet implemented in engine"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ # Processes telemetry traces received from ActiveAgent clients.
5
+ #
6
+ # This job handles the asynchronous processing of trace data to avoid
7
+ # blocking the ingestion endpoint. It:
8
+ # - Creates TelemetryTrace records for each trace
9
+ # - Updates aggregate statistics
10
+ # - Handles any errors gracefully
11
+ #
12
+ # @example Local mode
13
+ # ActiveAgent::ProcessTelemetryTracesJob.perform_later(
14
+ # traces: [...],
15
+ # sdk_info: { name: "activeagent", version: "0.5.0" },
16
+ # received_at: "2024-01-15T10:30:00Z"
17
+ # )
18
+ #
19
+ # @example Multi-tenant mode
20
+ # ActiveAgent::ProcessTelemetryTracesJob.perform_later(
21
+ # account_id: 1,
22
+ # traces: [...],
23
+ # sdk_info: { name: "activeagent", version: "0.5.0" },
24
+ # received_at: "2024-01-15T10:30:00Z"
25
+ # )
26
+ #
27
+ class ProcessTelemetryTracesJob < ::ActiveJob::Base
28
+ queue_as :default
29
+
30
+ # Maximum traces to process in a single job to avoid memory issues
31
+ MAX_TRACES_PER_JOB = 100
32
+
33
+ def perform(account_id: nil, traces:, sdk_info:, received_at:)
34
+ account = resolve_account(account_id)
35
+
36
+ # In multi-tenant mode, require an account
37
+ if ActiveAgent::Dashboard.multi_tenant? && account.nil?
38
+ Rails.logger.warn("[ProcessTelemetryTracesJob] Skipping traces - no valid account")
39
+ return
40
+ end
41
+
42
+ traces = traces.take(MAX_TRACES_PER_JOB)
43
+
44
+ traces.each do |trace|
45
+ process_trace(trace, sdk_info, account)
46
+ rescue StandardError => e
47
+ Rails.logger.error(
48
+ "[ProcessTelemetryTracesJob] Failed to process trace #{trace['trace_id']}: " \
49
+ "#{e.class} - #{e.message}"
50
+ )
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def resolve_account(account_id)
57
+ return nil unless ActiveAgent::Dashboard.multi_tenant?
58
+ return nil unless account_id
59
+
60
+ account_class = ActiveAgent::Dashboard.account_class.constantize
61
+ account_class.find_by(id: account_id)
62
+ end
63
+
64
+ def process_trace(trace, sdk_info, account)
65
+ model = ActiveAgent::Dashboard.trace_model
66
+
67
+ # Build uniqueness scope
68
+ scope = model.where(trace_id: trace["trace_id"])
69
+ scope = scope.where(account: account) if ActiveAgent::Dashboard.multi_tenant? && account
70
+
71
+ # Skip if trace already exists (idempotency)
72
+ return if scope.exists?
73
+
74
+ model.create_from_payload(trace, sdk_info, account: account)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Represents an AI agent configuration.
6
+ #
7
+ # Agents are the core entity in the dashboard, storing all configuration
8
+ # needed to execute AI interactions including provider settings, instructions,
9
+ # tools, and appearance.
10
+ #
11
+ # Supports both local (single-user) and multi-tenant (account-scoped) modes.
12
+ #
13
+ # @example Creating an agent
14
+ # agent = ActiveAgent::Dashboard::Agent.create!(
15
+ # name: "Code Assistant",
16
+ # provider: "openai",
17
+ # model: "gpt-4o"
18
+ # )
19
+ #
20
+ # @example Executing an agent
21
+ # run = agent.execute("Explain this code", code: "def foo; end")
22
+ #
23
+ class Agent < ApplicationRecord
24
+ # Associations - owner is optional to support both modes
25
+ belongs_to :user, class_name: ActiveAgent::Dashboard.user_class, optional: true if ActiveAgent::Dashboard.user_class
26
+ belongs_to :account, class_name: ActiveAgent::Dashboard.account_class, optional: true if ActiveAgent::Dashboard.multi_tenant?
27
+
28
+ has_many :agent_versions, class_name: "ActiveAgent::Dashboard::AgentVersion", dependent: :destroy
29
+ has_many :agent_runs, class_name: "ActiveAgent::Dashboard::AgentRun", dependent: :destroy
30
+
31
+ # Validations
32
+ validates :name, presence: true, length: { minimum: 2, maximum: 100 }
33
+ validates :slug, presence: true, format: { with: /\A[a-z0-9\-_]+\z/ }
34
+ validates :provider, presence: true
35
+ validates :model, presence: true
36
+
37
+ # Ensure slug uniqueness within scope
38
+ if ActiveAgent::Dashboard.multi_tenant?
39
+ validates :slug, uniqueness: { scope: :account_id }
40
+ else
41
+ validates :slug, uniqueness: { scope: :user_id }
42
+ end
43
+
44
+ # Status enum
45
+ enum :status, { draft: 0, active: 1, archived: 2 }
46
+
47
+ # Callbacks
48
+ before_validation :generate_slug, on: :create
49
+ after_create :create_initial_version
50
+ after_update :create_version_on_config_change, if: :configuration_changed?
51
+
52
+ # Scopes
53
+ scope :active_agents, -> { where(status: :active) }
54
+ scope :by_provider, ->(provider) { where(provider: provider) }
55
+ scope :with_tool, ->(tool) { where("tools @> ?", [ tool ].to_json) }
56
+
57
+ # Available presets matching AgentAvatar component
58
+ PRESET_TYPES = %w[
59
+ terminal webDeveloper documentAnalysis writing translation
60
+ playwright research imageAnalysis computerUse productDesign
61
+ ].freeze
62
+
63
+ # Available instruction sets
64
+ INSTRUCTION_SETS = %w[
65
+ github ruby rails aws gcp python typescript docker kubernetes
66
+ ].freeze
67
+
68
+ # Available tools/MCPs
69
+ AVAILABLE_TOOLS = %w[
70
+ terminal playwright filesystem code database slack fetch search edit translate memory
71
+ ].freeze
72
+
73
+ # Available providers
74
+ PROVIDERS = %w[openai anthropic ollama openrouter].freeze
75
+
76
+ # Returns the configuration as a hash for versioning
77
+ def configuration_snapshot
78
+ {
79
+ name: name,
80
+ description: description,
81
+ provider: provider,
82
+ model: model,
83
+ instructions: instructions,
84
+ preset_type: preset_type,
85
+ appearance: appearance,
86
+ instruction_sets: instruction_sets,
87
+ tools: tools,
88
+ mcp_servers: mcp_servers,
89
+ model_config: model_config,
90
+ response_format: response_format
91
+ }
92
+ end
93
+
94
+ # Restore from a version
95
+ def restore_from_version!(version)
96
+ config = version.configuration_snapshot
97
+ update!(
98
+ instructions: config["instructions"],
99
+ preset_type: config["preset_type"],
100
+ appearance: config["appearance"],
101
+ instruction_sets: config["instruction_sets"],
102
+ tools: config["tools"],
103
+ mcp_servers: config["mcp_servers"],
104
+ model_config: config["model_config"],
105
+ response_format: config["response_format"]
106
+ )
107
+ end
108
+
109
+ # Get the latest version
110
+ def latest_version
111
+ agent_versions.order(version_number: :desc).first
112
+ end
113
+
114
+ # Get version count
115
+ def version_count
116
+ agent_versions.count
117
+ end
118
+
119
+ # Generate Ruby agent class code
120
+ def to_agent_class_code
121
+ <<~RUBY
122
+ class #{agent_class_name || name.camelize}Agent < ApplicationAgent
123
+ generate_with :#{provider}, model: "#{model}"#{model_config_code}
124
+
125
+ def perform
126
+ prompt#{instructions_code}
127
+ end
128
+ end
129
+ RUBY
130
+ end
131
+
132
+ # Execute a run with this agent
133
+ def execute(input_prompt, **params)
134
+ run = agent_runs.create!(
135
+ input_prompt: input_prompt,
136
+ input_params: params,
137
+ status: :pending,
138
+ trace_id: SecureRandom.uuid
139
+ )
140
+
141
+ # Queue the execution job
142
+ ActiveAgent::Dashboard::AgentExecutionJob.perform_later(run.id)
143
+
144
+ run
145
+ end
146
+
147
+ # Quick test execution (synchronous)
148
+ def test_execute(input_prompt, **params)
149
+ run = agent_runs.create!(
150
+ input_prompt: input_prompt,
151
+ input_params: params,
152
+ status: :running,
153
+ trace_id: SecureRandom.uuid,
154
+ started_at: Time.current
155
+ )
156
+
157
+ begin
158
+ result = build_and_execute_agent(input_prompt, **params)
159
+
160
+ run.update!(
161
+ output: result[:output],
162
+ output_metadata: result[:metadata],
163
+ status: :complete,
164
+ completed_at: Time.current,
165
+ duration_ms: ((Time.current - run.started_at) * 1000).to_i,
166
+ input_tokens: result.dig(:usage, :input_tokens),
167
+ output_tokens: result.dig(:usage, :output_tokens),
168
+ total_tokens: result.dig(:usage, :total_tokens)
169
+ )
170
+ rescue => e
171
+ run.update!(
172
+ status: :failed,
173
+ completed_at: Time.current,
174
+ error_message: e.message,
175
+ error_backtrace: e.backtrace&.first(10)&.join("\n")
176
+ )
177
+ end
178
+
179
+ run
180
+ end
181
+
182
+ private
183
+
184
+ def generate_slug
185
+ return if slug.present?
186
+
187
+ base_slug = name.to_s.parameterize
188
+ self.slug = base_slug
189
+
190
+ # Ensure uniqueness within scope
191
+ counter = 1
192
+ scope = self.class.where(slug: slug)
193
+ scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id
194
+ scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id
195
+
196
+ while scope.exists?
197
+ self.slug = "#{base_slug}-#{counter}"
198
+ scope = self.class.where(slug: slug)
199
+ scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id
200
+ scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id
201
+ counter += 1
202
+ end
203
+ end
204
+
205
+ def create_initial_version
206
+ agent_versions.create!(
207
+ version_number: 1,
208
+ change_summary: "Initial creation",
209
+ configuration_snapshot: configuration_snapshot
210
+ )
211
+ end
212
+
213
+ def configuration_changed?
214
+ saved_changes.keys.any? do |key|
215
+ %w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key)
216
+ end
217
+ end
218
+
219
+ def create_version_on_config_change
220
+ next_version = (latest_version&.version_number || 0) + 1
221
+ changed_fields = saved_changes.keys.select do |key|
222
+ %w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key)
223
+ end
224
+
225
+ agent_versions.create!(
226
+ version_number: next_version,
227
+ change_summary: "Updated: #{changed_fields.join(', ')}",
228
+ configuration_snapshot: configuration_snapshot
229
+ )
230
+ end
231
+
232
+ def model_config_code
233
+ return "" if model_config.blank?
234
+
235
+ configs = model_config.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
236
+ ", #{configs}"
237
+ end
238
+
239
+ def instructions_code
240
+ return "" if instructions.blank?
241
+
242
+ "\n prompt instructions: <<~INSTRUCTIONS\n #{instructions.gsub("\n", "\n ")}\n INSTRUCTIONS"
243
+ end
244
+
245
+ def build_and_execute_agent(input_prompt, **params)
246
+ # TODO: Implement actual ActiveAgent execution
247
+ # This will create a dynamic agent class and execute it
248
+ {
249
+ output: "Mock response for: #{input_prompt}",
250
+ metadata: { provider: provider, model: model },
251
+ usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 }
252
+ }
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Dashboard
5
+ # Tracks individual agent execution runs.
6
+ #
7
+ # Each run captures input, output, timing, token usage, and any errors
8
+ # that occurred during execution.
9
+ #
10
+ # @example Creating a run
11
+ # run = agent.execute("Analyze this code", code: code)
12
+ # run.status # => "pending"
13
+ #
14
+ # @example Monitoring a run
15
+ # run.in_progress? # => true
16
+ # run.finished? # => false
17
+ #
18
+ class AgentRun < ApplicationRecord
19
+ belongs_to :agent, class_name: "ActiveAgent::Dashboard::Agent"
20
+ has_one :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording", dependent: :nullify
21
+
22
+ # Status enum
23
+ enum :status, { pending: 0, running: 1, complete: 2, failed: 3, cancelled: 4 }
24
+
25
+ # Validations
26
+ validates :trace_id, presence: true
27
+
28
+ # Scopes
29
+ scope :recent, -> { order(created_at: :desc) }
30
+ scope :successful, -> { where(status: :complete) }
31
+ scope :failed_runs, -> { where(status: :failed) }
32
+ scope :today, -> { where("created_at >= ?", Time.current.beginning_of_day) }
33
+
34
+ # Callbacks
35
+ before_validation :set_trace_id, on: :create
36
+ after_update_commit :broadcast_update, if: :saved_change_to_status?
37
+
38
+ # Add a log entry
39
+ def add_log(message, level: :info)
40
+ new_logs = logs || []
41
+ new_logs << {
42
+ timestamp: Time.current.iso8601,
43
+ level: level.to_s,
44
+ message: message
45
+ }
46
+ update!(logs: new_logs)
47
+ end
48
+
49
+ # Calculate duration if not set
50
+ def calculated_duration_ms
51
+ return duration_ms if duration_ms.present?
52
+ return nil unless started_at && completed_at
53
+
54
+ ((completed_at - started_at) * 1000).to_i
55
+ end
56
+
57
+ # Check if run is still in progress
58
+ def in_progress?
59
+ pending? || running?
60
+ end
61
+
62
+ # Check if run is finished
63
+ def finished?
64
+ complete? || failed? || cancelled?
65
+ end
66
+
67
+ # Get a summary for display
68
+ def summary
69
+ {
70
+ id: id,
71
+ status: status,
72
+ input_preview: input_prompt&.truncate(100),
73
+ output_preview: output&.truncate(200),
74
+ duration_ms: calculated_duration_ms,
75
+ tokens: total_tokens,
76
+ created_at: created_at,
77
+ error: error_message
78
+ }
79
+ end
80
+
81
+ # Stream output updates via ActionCable
82
+ def broadcast_update
83
+ return unless defined?(ActionCable)
84
+
85
+ ActionCable.server.broadcast(
86
+ "agent_run_#{id}",
87
+ {
88
+ type: "update",
89
+ run: summary
90
+ }
91
+ )
92
+ end
93
+
94
+ # Cancel a running execution
95
+ def cancel!
96
+ return unless in_progress?
97
+
98
+ update!(
99
+ status: :cancelled,
100
+ completed_at: Time.current,
101
+ error_message: "Cancelled by user"
102
+ )
103
+ broadcast_update
104
+ end
105
+
106
+ private
107
+
108
+ def set_trace_id
109
+ self.trace_id ||= SecureRandom.uuid
110
+ end
111
+ end
112
+ end
113
+ end