Class: Aidp::Providers::Base

Inherits:
Object
  • Object
show all
Includes:
MessageDisplay, Adapter
Defined in:
lib/aidp/providers/base.rb

Direct Known Subclasses

Aider, Anthropic, Codex, Cursor, Gemini, GithubCopilot, Kilocode, Opencode

Constant Summary collapse

ACTIVITY_STATES =

Activity indicator states

{
  idle: "⏳",
  working: "🔄",
  stuck: "⚠️",
  completed: "✅",
  failed: "❌"
}.freeze
DEFAULT_STUCK_TIMEOUT =

Default timeout for stuck detection (2 minutes)

120
TIMEOUT_QUICK_MODE =

Configurable timeout values (can be overridden via environment or config) These defaults provide reasonable values for different execution scenarios

120
TIMEOUT_DEFAULT =

2 minutes - for quick testing

300
TIMEOUT_REPOSITORY_ANALYSIS =

5 minutes - standard interactive timeout

180
TIMEOUT_ARCHITECTURE_ANALYSIS =

3 minutes - repository analysis

600
TIMEOUT_TEST_ANALYSIS =

10 minutes - architecture analysis

300
TIMEOUT_FUNCTIONALITY_ANALYSIS =

5 minutes - test analysis

600
TIMEOUT_DOCUMENTATION_ANALYSIS =

10 minutes - functionality analysis

300
TIMEOUT_STATIC_ANALYSIS =

5 minutes - documentation analysis

450
TIMEOUT_REFACTORING_RECOMMENDATIONS =

7.5 minutes - static analysis

600
TIMEOUT_IMPLEMENTATION =

10 minutes - refactoring

900
TIER_TIMEOUT_MULTIPLIERS =

Tier-based timeout multipliers (applied on top of base timeouts) Higher tiers need more time for deeper reasoning

{
  "mini" => 1.0,      # 5 minutes default (300s base)
  "standard" => 2.0,  # 10 minutes default (600s base)
  "thinking" => 6.0,  # 30 minutes default (1800s base)
  "pro" => 6.0,       # 30 minutes default (1800s base)
  "max" => 12.0       # 60 minutes default (3600s base)
}.freeze

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Adapter

#capabilities, #classify_error, #dangerous_mode=, #dangerous_mode_enabled?, #dangerous_mode_flags, #error_metadata, #error_patterns, #health_status, #logging_metadata, #redact_secrets, #retryable_error?, #supports_dangerous_mode?, #validate_config

Methods included from MessageDisplay

#display_message, included, #message_display_prompt

Constructor Details

#initialize(output: nil, prompt: TTY::Prompt.new) ⇒ Base

Returns a new instance of Base.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/aidp/providers/base.rb', line 52

def initialize(output: nil, prompt: TTY::Prompt.new)
  @activity_state = :idle
  @last_activity_time = Time.now
  @start_time = nil
  @step_name = nil
  @activity_callback = nil
  @stuck_timeout = DEFAULT_STUCK_TIMEOUT
  @output_count = 0
  @last_output_time = Time.now
  @job_context = nil
  @harness_context = nil
  @output = output
  @prompt = prompt
  @model = nil
  @harness_metrics = {
    total_requests: 0,
    successful_requests: 0,
    failed_requests: 0,
    rate_limited_requests: 0,
    total_tokens_used: 0,
    total_cost: 0.0,
    average_response_time: 0.0,
    last_request_time: nil
  }
end

Instance Attribute Details

#activity_stateObject (readonly)

Returns the value of attribute activity_state.



50
51
52
# File 'lib/aidp/providers/base.rb', line 50

def activity_state
  @activity_state
end

#last_activity_timeObject (readonly)

Returns the value of attribute last_activity_time.



50
51
52
# File 'lib/aidp/providers/base.rb', line 50

def last_activity_time
  @last_activity_time
end

#modelObject (readonly)

Returns the value of attribute model.



50
51
52
# File 'lib/aidp/providers/base.rb', line 50

def model
  @model
end

#start_timeObject (readonly)

Returns the value of attribute start_time.



50
51
52
# File 'lib/aidp/providers/base.rb', line 50

def start_time
  @start_time
end

#step_nameObject (readonly)

Returns the value of attribute step_name.



50
51
52
# File 'lib/aidp/providers/base.rb', line 50

def step_name
  @step_name
end

#stuck_timeoutObject (readonly)

Get stuck timeout for this provider



209
210
211
# File 'lib/aidp/providers/base.rb', line 209

def stuck_timeout
  @stuck_timeout
end

Class Method Details

.discover_models_from_registry(model_pattern, provider_name) ⇒ Array<Hash>

Helper method for registry-based model discovery

Providers that use the model registry can call this method to discover models based on a model family pattern.

Parameters:

  • model_pattern (Regexp)

    Pattern to match model families

  • provider_name (String)

    Name of the provider

Returns:

  • (Array<Hash>)

    Array of discovered models



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/aidp/providers/base.rb', line 354

def self.discover_models_from_registry(model_pattern, provider_name)
  require_relative "../harness/model_registry"
  registry = Aidp::Harness::ModelRegistry.new

  # Get all models from registry that match the pattern
  models = registry.all_families.filter_map do |family|
    next unless model_pattern.match?(family)

    info = registry.get_model_info(family)
    next unless info

    {
      name: family,
      family: family,
      tier: info["tier"],
      capabilities: info["capabilities"] || [],
      context_window: info["context_window"],
      provider: provider_name
    }
  end

  Aidp.log_info("#{provider_name}_provider", "using registry models", count: models.size)
  models
rescue => e
  Aidp.log_debug("#{provider_name}_provider", "discovery failed", error: e.message)
  []
end

.firewall_requirementsHash

Get firewall requirements for this provider

Returns domains and IP ranges that need to be accessible for this provider to function properly. Used by devcontainer firewall configuration.

Override in subclasses to provide provider-specific requirements

Returns:

  • (Hash)

    Firewall requirements with :domains and :ip_ranges keys

    • domains: Array of domain strings

    • ip_ranges: Array of CIDR strings



392
393
394
395
396
397
# File 'lib/aidp/providers/base.rb', line 392

def self.firewall_requirements
  {
    domains: [],
    ip_ranges: []
  }
end

Instance Method Details

#activity_summaryObject

Get activity summary for metrics



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/aidp/providers/base.rb', line 190

def activity_summary
  {
    provider: name,
    step_name: @step_name,
    start_time: @start_time&.iso8601,
    end_time: Time.now.iso8601,
    duration: execution_time,
    final_state: @activity_state,
    stuck_detected: stuck?,
    output_count: @output_count
  }
end

#available?Boolean

Check if provider is available (override in subclasses)

Returns:

  • (Boolean)


290
291
292
# File 'lib/aidp/providers/base.rb', line 290

def available?
  true # Default to true, override in subclasses
end

#configure(config) ⇒ Object

Configure the provider with options

Parameters:

  • config (Hash)

    Configuration options, may include :model



90
91
92
93
94
# File 'lib/aidp/providers/base.rb', line 90

def configure(config)
  if config[:model]
    @model = resolve_model_name(config[:model].to_s)
  end
end

#display_nameObject

Human-friendly display name for UI Override in subclasses to provide a better display name



84
85
86
# File 'lib/aidp/providers/base.rb', line 84

def display_name
  name
end

#execution_timeObject

Get current execution time



162
163
164
165
# File 'lib/aidp/providers/base.rb', line 162

def execution_time
  return 0 unless @start_time
  Time.now - @start_time
end

#fetch_mcp_serversObject

Fetch MCP servers configured for this provider Returns an array of server hashes with keys: :name, :status, :description, :enabled Override in subclasses to provide provider-specific MCP server detection



103
104
105
# File 'lib/aidp/providers/base.rb', line 103

def fetch_mcp_servers
  []
end

#harness_configObject

Get provider configuration for harness



279
280
281
282
283
284
285
286
287
# File 'lib/aidp/providers/base.rb', line 279

def harness_config
  {
    name: name,
    supports_activity_monitoring: supports_activity_monitoring?,
    default_timeout: @stuck_timeout,
    available: available?,
    health_status: harness_health_status
  }
end

#harness_health_statusObject

Get provider health status for harness



255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/aidp/providers/base.rb', line 255

def harness_health_status
  {
    provider: name,
    activity_state: @activity_state,
    stuck: stuck?,
    success_rate: calculate_success_rate,
    average_response_time: @harness_metrics[:average_response_time],
    total_requests: @harness_metrics[:total_requests],
    rate_limit_ratio: calculate_rate_limit_ratio,
    last_activity: @last_activity_time,
    health_score: calculate_health_score
  }
end

#harness_healthy?Boolean

Check if provider is healthy for harness use

Returns:

  • (Boolean)


270
271
272
273
274
275
276
# File 'lib/aidp/providers/base.rb', line 270

def harness_healthy?
  return false if stuck?
  return false if @harness_metrics[:total_requests] > 0 && calculate_success_rate < 0.5
  return false if calculate_rate_limit_ratio > 0.3

  true
end

#harness_metricsObject

Get harness metrics



224
225
226
# File 'lib/aidp/providers/base.rb', line 224

def harness_metrics
  @harness_metrics.dup
end

#harness_mode?Boolean

Check if provider is operating in harness mode

Returns:

  • (Boolean)


219
220
221
# File 'lib/aidp/providers/base.rb', line 219

def harness_mode?
  !@harness_context.nil?
end

#mark_completedObject

Mark as completed



180
181
182
# File 'lib/aidp/providers/base.rb', line 180

def mark_completed
  update_activity_state(:completed)
end

#mark_failed(error_message = nil) ⇒ Object

Mark as failed



185
186
187
# File 'lib/aidp/providers/base.rb', line 185

def mark_failed(error_message = nil)
  update_activity_state(:failed, error_message)
end

#nameObject

Raises:

  • (NotImplementedError)


78
79
80
# File 'lib/aidp/providers/base.rb', line 78

def name
  raise NotImplementedError, "#{self.class} must implement #name"
end

#record_activity(message = nil) ⇒ Object

Record activity (called when provider produces output)



173
174
175
176
177
# File 'lib/aidp/providers/base.rb', line 173

def record_activity(message = nil)
  @output_count += 1
  @last_output_time = Time.now
  update_activity_state(:working, message)
end

#record_harness_request(success:, tokens_used: 0, cost: 0.0, response_time: 0.0, rate_limited: false) ⇒ Object

Record harness request metrics



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/aidp/providers/base.rb', line 229

def record_harness_request(success:, tokens_used: 0, cost: 0.0, response_time: 0.0, rate_limited: false)
  @harness_metrics[:total_requests] += 1
  @harness_metrics[:last_request_time] = Time.now

  if success
    @harness_metrics[:successful_requests] += 1
  else
    @harness_metrics[:failed_requests] += 1
  end

  if rate_limited
    @harness_metrics[:rate_limited_requests] += 1
  end

  @harness_metrics[:total_tokens_used] += tokens_used
  @harness_metrics[:total_cost] += cost

  # Update average response time
  total_time = @harness_metrics[:average_response_time] * (@harness_metrics[:total_requests] - 1) + response_time
  @harness_metrics[:average_response_time] = total_time / @harness_metrics[:total_requests]

  # Notify harness context if available
  @harness_context&.record_provider_metrics(name, @harness_metrics)
end

#send_message(prompt:, session: nil, options: {}) ⇒ Object

Raises:

  • (NotImplementedError)


96
97
98
# File 'lib/aidp/providers/base.rb', line 96

def send_message(prompt:, session: nil, options: {})
  raise NotImplementedError, "#{self.class} must implement #send_message"
end

#send_with_harness(prompt:, session: nil, _options: {}) ⇒ Object

Enhanced send method that integrates with harness



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/aidp/providers/base.rb', line 295

def send_with_harness(prompt:, session: nil, _options: {})
  start_time = Time.now
  success = false
  rate_limited = false
  tokens_used = 0
  cost = 0.0
  error_message = nil

  begin
    # Call the original send_message method
    result = send_message(prompt: prompt, session: session)
    success = true

    # Extract token usage and cost if available
    if result.is_a?(Hash) && result[:token_usage]
      tokens_used = result[:token_usage][:total] || 0
      cost = result[:token_usage][:cost] || 0.0
    end

    # Check for rate limiting in result
    if result.is_a?(Hash) && result[:rate_limited]
      rate_limited = true
    end

    result
  rescue => e
    error_message = e.message

    # Check if error is rate limiting
    if e.message.match?(/rate.?limit/i) || e.message.match?(/quota/i) || e.message.match?(/session limit/i)
      rate_limited = true
    end

    raise e
  ensure
    response_time = Time.now - start_time
    record_harness_request(
      success: success,
      tokens_used: tokens_used,
      cost: cost,
      response_time: response_time,
      rate_limited: rate_limited
    )

    # Log to harness context if available
    if @harness_context && error_message
      @harness_context.record_provider_error(name, error_message, rate_limited)
    end
  end
end

#set_harness_context(harness_runner) ⇒ Object

Set harness context for provider



214
215
216
# File 'lib/aidp/providers/base.rb', line 214

def set_harness_context(harness_runner)
  @harness_context = harness_runner
end

#set_job_context(job_id:, execution_id:, job_manager:) ⇒ Object

Set job context for background execution



114
115
116
117
118
119
120
# File 'lib/aidp/providers/base.rb', line 114

def set_job_context(job_id:, execution_id:, job_manager:)
  @job_context = {
    job_id: job_id,
    execution_id: execution_id,
    job_manager: job_manager
  }
end

#setup_activity_monitoring(step_name, activity_callback = nil, stuck_timeout = nil) ⇒ Object

Set up activity monitoring for a step



123
124
125
126
127
128
129
130
131
132
# File 'lib/aidp/providers/base.rb', line 123

def setup_activity_monitoring(step_name, activity_callback = nil, stuck_timeout = nil)
  @step_name = step_name
  @activity_callback = activity_callback
  @stuck_timeout = stuck_timeout || DEFAULT_STUCK_TIMEOUT
  @start_time = Time.now
  @last_activity_time = @start_time
  @output_count = 0
  @last_output_time = @start_time
  update_activity_state(:working)
end

#stuck?Boolean

Check if provider appears to be stuck

Returns:

  • (Boolean)


154
155
156
157
158
159
# File 'lib/aidp/providers/base.rb', line 154

def stuck?
  return false unless @activity_state == :working

  time_since_activity = Time.now - @last_activity_time
  time_since_activity > @stuck_timeout
end

#supports_activity_monitoring?Boolean

Check if provider supports activity monitoring

Returns:

  • (Boolean)


204
205
206
# File 'lib/aidp/providers/base.rb', line 204

def supports_activity_monitoring?
  true # Default to true, override in subclasses if needed
end

#supports_mcp?Boolean

Check if this provider supports MCP servers Override in subclasses to provide accurate MCP support detection

Returns:

  • (Boolean)


109
110
111
# File 'lib/aidp/providers/base.rb', line 109

def supports_mcp?
  false
end

#time_since_last_activityObject

Get time since last activity



168
169
170
# File 'lib/aidp/providers/base.rb', line 168

def time_since_last_activity
  Time.now - @last_activity_time
end

#update_activity_state(state, message = nil) ⇒ Object

Update activity state and notify callback



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/aidp/providers/base.rb', line 135

def update_activity_state(state, message = nil)
  @activity_state = state
  @last_activity_time = Time.now if state == :working

  # Log state change to job if in background mode
  if @job_context
    level = case state
    when :completed then "info"
    when :failed then "error"
    else "debug"
    end

    log_to_job(message || "Provider state changed to #{state}", level)
  end

  @activity_callback&.call(state, message, self)
end