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, MessageDisplay::CRITICAL_TYPES

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, #quiet_mode?

Constructor Details

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

Returns a new instance of Base.



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

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

#harness_context=(value) ⇒ Object (writeonly)

Expose for testability



52
53
54
# File 'lib/aidp/providers/base.rb', line 52

def harness_context=(value)
  @harness_context = value
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



211
212
213
# File 'lib/aidp/providers/base.rb', line 211

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



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
381
382
# File 'lib/aidp/providers/base.rb', line 356

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



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

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

Instance Method Details

#activity_summaryObject

Get activity summary for metrics



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

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)


292
293
294
# File 'lib/aidp/providers/base.rb', line 292

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



92
93
94
95
96
# File 'lib/aidp/providers/base.rb', line 92

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



86
87
88
# File 'lib/aidp/providers/base.rb', line 86

def display_name
  name
end

#execution_timeObject

Get current execution time



164
165
166
167
# File 'lib/aidp/providers/base.rb', line 164

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



105
106
107
# File 'lib/aidp/providers/base.rb', line 105

def fetch_mcp_servers
  []
end

#harness_configObject

Get provider configuration for harness



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

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



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

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)


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

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



226
227
228
# File 'lib/aidp/providers/base.rb', line 226

def harness_metrics
  @harness_metrics.dup
end

#harness_mode?Boolean

Check if provider is operating in harness mode

Returns:

  • (Boolean)


221
222
223
# File 'lib/aidp/providers/base.rb', line 221

def harness_mode?
  !@harness_context.nil?
end

#mark_completedObject

Mark as completed



182
183
184
# File 'lib/aidp/providers/base.rb', line 182

def mark_completed
  update_activity_state(:completed)
end

#mark_failed(error_message = nil) ⇒ Object

Mark as failed



187
188
189
# File 'lib/aidp/providers/base.rb', line 187

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

#nameObject

Raises:

  • (NotImplementedError)


80
81
82
# File 'lib/aidp/providers/base.rb', line 80

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

#record_activity(message = nil) ⇒ Object

Record activity (called when provider produces output)



175
176
177
178
179
# File 'lib/aidp/providers/base.rb', line 175

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



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

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)


98
99
100
# File 'lib/aidp/providers/base.rb', line 98

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



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
345
346
# File 'lib/aidp/providers/base.rb', line 297

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



216
217
218
# File 'lib/aidp/providers/base.rb', line 216

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



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

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



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

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)


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

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)


206
207
208
# File 'lib/aidp/providers/base.rb', line 206

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)


111
112
113
# File 'lib/aidp/providers/base.rb', line 111

def supports_mcp?
  false
end

#time_since_last_activityObject

Get time since last activity



170
171
172
# File 'lib/aidp/providers/base.rb', line 170

def time_since_last_activity
  Time.now - @last_activity_time
end

#update_activity_state(state, message = nil) ⇒ Object

Update activity state and notify callback



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

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